/* ============================================================ top_renewal.js(トップページ専用)2026-10-01 再構成 旧 index.html / index_test_v4.html に散っていたスクリプトを集約 1. 追従ヘッダー(jQuery) 2. ページ内リンクのスムーススクロール(jQuery/重複していた2本を1本に) 3. ヒーロー・カルーセル・フェードイン等(旧 common/js/script.js) 4. アイソメトリックシティ描画(旧 inline: iso-city-script) 5. ヒーローのカウントアップ+ピン(旧 inline: hero-count-script) 6. 口コミカルーセルの見切れ調整(旧 inline: voice-peek-script) ※ オープニング(#rt-opening)は初回描画用に index.html の head に残しています ※ 直前で読み込む前提(DOM 構築済みで実行される) ============================================================ */ /* =========================================================== 1. 追従ヘッダー 通常ヘッダーの下端を過ぎた時点で、複製したヘッダーを上から出す (2026-09-14: 旧 700px 固定だとヘッダーが無い区間ができたため変更) =========================================================== */ jQuery(function ($) { var $win = $(window), $cloneNav = $('nav').clone().addClass('clone-nav').appendTo('body'), showClass = 'is-show'; $win.on('load scroll', function () { var value = $(this).scrollTop(); if (value > ($('.header_wrap').not('.clone-nav').outerHeight() || 95)) { $cloneNav.addClass(showClass); } else { $cloneNav.removeClass(showClass); } }); }); /* =========================================================== 2. ページ内リンクのスムーススクロール =========================================================== */ jQuery(function ($) { $(document).on('click', 'a[href^="#"]', function (e) { var href = $(this).attr('href'); var $target = (href === '#' || href === '') ? $('html') : $(href); var position = ($target.length && $target.offset()) ? $target.offset().top : 0; $('html, body').animate({ scrollTop: position }, 600, 'swing'); e.preventDefault(); }); }); /* =========================================================== 3. ヒーロー・カルーセル・フェードイン等(旧 common/js/script.js) ※ 各ブロックを try/catch で包み、1つ壊れても他は動くようにしている =========================================================== */ try { (function () { 'use strict'; /* =========================================================== HERO TEXT CROSS-FADE + BACKGROUND + PAGINATION SYNC Cycles through 3 slides (.hero__slide), backgrounds (.hero__bg), and dots (.hero__dot) simultaneously by toggling the .is-active class on each matching index. =========================================================== */ var slides = document.querySelectorAll('#heroSlides .hero__slide'); var bgs = document.querySelectorAll('#heroBgs .hero__bg'); var dots = document.querySelectorAll('#heroPagination .hero__dot'); var currentSlide = 0; var SLIDE_DISPLAY_MS = 5000; function goToSlide(index) { /* Deactivate current */ slides[currentSlide].classList.remove('is-active'); bgs[currentSlide] && bgs[currentSlide].classList.remove('is-active'); dots[currentSlide] && dots[currentSlide].classList.remove('is-active'); /* Activate target */ currentSlide = index % slides.length; slides[currentSlide].classList.add('is-active'); bgs[currentSlide] && bgs[currentSlide].classList.add('is-active'); dots[currentSlide] && dots[currentSlide].classList.add('is-active'); } function advanceSlide() { goToSlide((currentSlide + 1) % slides.length); } /* Initial fade-in for slide 0: is-active is pre-set in HTML so the browser paints it at opacity:1 with no transition. Strip it synchronously, wait two rAF ticks (one to register the opacity:0 paint, one to let the transition start), then restore it. The setInterval is unaffected ― it fires 5 s later as normal. */ if (slides.length) { slides[0].classList.remove('is-active'); bgs[0] && bgs[0].classList.remove('is-active'); dots[0] && dots[0].classList.remove('is-active'); requestAnimationFrame(function () { requestAnimationFrame(function () { slides[0].classList.add('is-active'); bgs[0] && bgs[0].classList.add('is-active'); dots[0] && dots[0].classList.add('is-active'); }); }); } /* Dot click handlers ― manual navigation */ dots.forEach(function (dot, i) { dot.addEventListener('click', function () { clearInterval(autoTimer); goToSlide(i); autoTimer = setInterval(advanceSlide, SLIDE_DISPLAY_MS); }); }); var autoTimer = null; if (slides.length > 1) { autoTimer = setInterval(advanceSlide, SLIDE_DISPLAY_MS); } /* =========================================================== HERO NEWS TICKER Rotates through 3 dummy news items, updating the DOM nodes in place. Text truncation is handled purely by CSS clamp. =========================================================== */ var newsItems = [ { date: '2025.12.01', text: 'ここにはお知らせのテキストが入ります。ここにはお知らせのテキストが入ります。ここにはお知らせのテキストが入ります。' }, { date: '2025.11.15', text: '不動産投資セミナー開催のご案内。詳細はこちらよりご確認いただけます。多くの皆様のご参加をお待ちしております。' }, { date: '2025.10.30', text: '新着物件情報が更新されました。全国各地の収益物件をご紹介しております。お気軽にお問い合わせください。' } ]; var elDate = document.getElementById('heroNewsDate'); var elText = document.getElementById('heroNewsText'); var currentNews = 0; var NEWS_INTERVAL_MS = 5000; function advanceNews() { currentNews = (currentNews + 1) % newsItems.length; var item = newsItems[currentNews]; elDate.textContent = item.date; elText.textContent = item.text; } if (elDate && elText) { setInterval(advanceNews, NEWS_INTERVAL_MS); } }()); /* =========================================================== 03_SECTION CAROUSEL ― seamless infinite loop Strategy: triple-clone (reference: rita-hudousan.com/contents/line_test/) Track layout: [copy1: clones of all slides] [copy2: real slides] [copy3: clones] Total slots: TOTAL * 3 (= 12 for 4 slides) trackIndex always lives in copy2 (TOTAL … TOTAL*2-1). On transitionend: if index drifted into copy1 (= 0; i--) { var c = realSlides[i].cloneNode(true); c.setAttribute('aria-hidden', 'true'); track.insertBefore(c, track.firstChild); } /* -- Append copy3: clones of all real slides in forward order -- */ for (var j = 0; j < TOTAL; j++) { var d = realSlides[j].cloneNode(true); d.setAttribute('aria-hidden', 'true'); track.appendChild(d); } /* Track slots: 0…TOTAL-1 = copy1, TOTAL…TOTAL*2-1 = real (copy2), TOTAL*2…TOTAL*3-1 = copy3 Start on the first real slide (copy2 index TOTAL) */ var trackIndex = TOTAL; var locked = false; function calcOffset(idx) { /* Align slide[idx] so its left edge sits at contentMargin from section left */ var sectionW = track.closest('.s03').offsetWidth; var contentMargin = Math.max(0, (sectionW - 1140) / 2); return contentMargin - idx * CARD_W; } function setPos(idx, animate) { track.style.transition = animate ? 'transform 0.45s ease' : 'none'; track.style.transform = 'translateX(' + calcOffset(idx) + 'px)'; } /* Initial position ― no animation */ setPos(trackIndex, false); /* On transition end: snap back to copy2 if we drifted into copy1 or copy3 */ track.addEventListener('transitionend', function () { if (trackIndex < TOTAL) { trackIndex += TOTAL; /* copy1 → copy2 (same logical slide) */ setPos(trackIndex, false); } else if (trackIndex >= TOTAL * 2) { trackIndex -= TOTAL; /* copy3 → copy2 (same logical slide) */ setPos(trackIndex, false); } locked = false; }); var AUTOPLAY_MS = 3500; function goNext() { if (locked) { return; } locked = true; trackIndex++; setPos(trackIndex, true); } function goPrev() { if (locked) { return; } locked = true; trackIndex--; setPos(trackIndex, true); } btnNext.addEventListener('click', function () { clearInterval(timer); goNext(); timer = setInterval(goNext, AUTOPLAY_MS); }); btnPrev.addEventListener('click', function () { clearInterval(timer); goPrev(); timer = setInterval(goNext, AUTOPLAY_MS); }); /* Autoplay */ var timer = setInterval(goNext, AUTOPLAY_MS); var wrap = track.closest('.s03__carousel-wrap'); if (wrap) { wrap.addEventListener('mouseenter', function () { clearInterval(timer); }); wrap.addEventListener('mouseleave', function () { timer = setInterval(goNext, AUTOPLAY_MS); }); } /* Recalculate on resize without animation */ window.addEventListener('resize', function () { setPos(trackIndex, false); }); }()); /* =========================================================== 05_SECTION CAROUSELS (area01 + area02) ― seamless infinite loop Same triple-clone strategy as 03_section (reference site pattern). Track layout per carousel: [copy1: r0..r4] [copy2: real r0..r4] [copy3: r0..r4] = 15 slots copy1: idx 0-4 | copy2: idx 5-9 | copy3: idx 10-14 trackIndex lives in copy2 (5-9). Snap rules on transitionend: idx < TOTAL → +TOTAL (copy1 → copy2, no visible jump) idx ≧ TOTAL×2 → -TOTAL (copy3 → copy2, no visible jump) Initial position: idx = TOTAL+1 = 6 → copy2_real[1] sits at contentMargin + ITEM_OFFSET → copy2_real[0] peeks from the left (one stride behind) Autoplay: 3000 ms, paused on mouseenter, resumed on mouseleave. =========================================================== */ } catch (e) { if (window.console) console.error('[top_renewal.js] REASONカルーセル', e); } try { (function () { 'use strict'; var STRIDE = 346; /* 313px card + 33px gap */ var ITEM_OFFSET = 66; /* inset of real[1] from content edge */ var AUTOPLAY_MS = 3000; function buildCarousel(trackId, prevId, nextId) { var track = document.getElementById(trackId); var btnPrev = document.getElementById(prevId); var btnNext = document.getElementById(nextId); if (!track || !btnPrev || !btnNext) { return; } /* Collect real slides before any DOM changes */ /* 2026-10-01: 物件は %%TOKUSEN%% で出力されるため件数が可変。 クラス名に依存せず track 直下の要素をスライドとして数える(0件なら何もしない) */ var realSlides = Array.prototype.slice.call(track.children); var TOTAL = realSlides.length; if (!TOTAL) { return; } /* -- Prepend copy1: insert each clone before firstChild in reverse order so final DOM is [r0_clone, r1_clone … r4_clone, real …] -- */ for (var i = TOTAL - 1; i >= 0; i--) { var c = realSlides[i].cloneNode(true); c.setAttribute('aria-hidden', 'true'); track.insertBefore(c, track.firstChild); } /* -- Append copy3: clones in forward order -- */ for (var j = 0; j < TOTAL; j++) { var d = realSlides[j].cloneNode(true); d.setAttribute('aria-hidden', 'true'); track.appendChild(d); } /* Start at copy2_real[1] so real[0] peeks from the left */ var trackIndex = TOTAL + 1; /* 6 */ var locked = false; function calcOffset(idx) { var sectionW = track.closest('.s05').offsetWidth; var contentMargin = Math.max(0, (sectionW - 1140) / 2); return contentMargin + ITEM_OFFSET - idx * STRIDE; } function setPos(idx, animate) { track.style.transition = animate ? 'transform 0.45s ease' : 'none'; track.style.transform = 'translateX(' + calcOffset(idx) + 'px)'; } setPos(trackIndex, false); /* Snap back to copy2 after any drift into copy1 or copy3 */ track.addEventListener('transitionend', function () { if (trackIndex < TOTAL) { trackIndex += TOTAL; /* copy1 → copy2 */ setPos(trackIndex, false); } else if (trackIndex >= TOTAL * 2) { trackIndex -= TOTAL; /* copy3 → copy2 */ setPos(trackIndex, false); } locked = false; }); function goNext() { if (locked) { return; } locked = true; trackIndex++; setPos(trackIndex, true); } function goPrev() { if (locked) { return; } locked = true; trackIndex--; setPos(trackIndex, true); } btnNext.addEventListener('click', goNext); btnPrev.addEventListener('click', goPrev); /* Autoplay */ var timer = setInterval(goNext, AUTOPLAY_MS); var wrap = track.closest('.s05__carousel-wrap'); wrap.addEventListener('mouseenter', function () { clearInterval(timer); }); wrap.addEventListener('mouseleave', function () { timer = setInterval(goNext, AUTOPLAY_MS); }); window.addEventListener('resize', function () { setPos(trackIndex, false); }); } buildCarousel('s05Track01', 's05Prev01', 's05Next01'); buildCarousel('s05Track02', 's05Prev02', 's05Next02'); }()); /* =========================================================== SCROLL FADE-IN ― IntersectionObserver for .js-fade Adds .is-visible once each element's top edge crosses the 80% viewport mark (rootMargin -15% bottom). Once triggered, the element is unobserved so the animation never replays. =========================================================== */ } catch (e) { if (window.console) console.error('[top_renewal.js] 物件カルーセル', e); } try { (function () { 'use strict'; if (!('IntersectionObserver' in window)) { /* Graceful fallback: reveal all immediately in old browsers */ var all = document.querySelectorAll('.js-fade'); for (var k = 0; k < all.length; k++) { all[k].classList.add('is-visible'); } return; } var fadeEls = document.querySelectorAll('.js-fade'); if (!fadeEls.length) { return; } var fadeObserver = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (!entry.isIntersecting) { return; } entry.target.classList.add('is-visible'); fadeObserver.unobserve(entry.target); }); }, { threshold: 0, rootMargin: '0px 0px -15% 0px' }); /* Pre-pass: elements already inside the real viewport at load time receive .is-visible immediately. The rootMargin: -15% bottom offset means the observer itself would never fire for elements near the viewport bottom (e.g. .hero__news), so we bypass the observer for those and reveal them directly. Off-screen elements are handed to the observer as normal. */ fadeEls.forEach(function (el) { var rect = el.getBoundingClientRect(); if (rect.top < window.innerHeight && rect.bottom > 0) { el.classList.add('is-visible'); } else { fadeObserver.observe(el); } }); }()); /* =========================================================== TRUST__EVALUATION ― star reveal + score count-up Watches .s07__eval-wrap; fires once when ≧50% is visible. Stars: each .eval-star gets .is-visible with 150ms stagger (back-out bounce via CSS cubic-bezier, 0.3s each). Score: rAF count-up 0.0 → 4.3 over 800ms (power2.out ease) starts after the last star has appeared. =========================================================== */ } catch (e) { if (window.console) console.error('[top_renewal.js] フェードイン', e); } try { (function () { 'use strict'; if (!('IntersectionObserver' in window)) { return; } var evalWrap = document.querySelector('.s07__eval-wrap'); if (!evalWrap) { return; } var fired = false; var evalObserver = new IntersectionObserver(function (entries) { if (fired || !entries[0].isIntersecting) { return; } fired = true; evalObserver.disconnect(); runEvalAnimation(); }, { threshold: 0.5 }); evalObserver.observe(evalWrap); function runEvalAnimation() { var stars = evalWrap.querySelectorAll('.eval-star'); var scoreEl = evalWrap.querySelector('.trust__evaluation-score'); var targetScore = scoreEl ? (parseFloat(scoreEl.textContent) || 4.3) : 4.3; /* -- Sequential star reveal -- */ stars.forEach(function (star, i) { setTimeout(function () { star.classList.add('is-visible'); }, i * 150); }); /* -- Score count-up: starts after last star lands (stagger + 200ms buffer) -- */ if (!scoreEl) { return; } var delay = stars.length * 150 + 200; setTimeout(function () { var startTs = null; var duration = 800; /* ms */ scoreEl.textContent = '0.0'; function easeOut(t) { return 1 - (1 - t) * (1 - t); } /* power2.out */ function tick(ts) { if (!startTs) { startTs = ts; } var progress = Math.min((ts - startTs) / duration, 1); scoreEl.textContent = (easeOut(progress) * targetScore).toFixed(1); if (progress < 1) { requestAnimationFrame(tick); } else { scoreEl.textContent = targetScore.toFixed(1); } } requestAnimationFrame(tick); }, delay); } }()); /* =========================================================== FLOATING BANNER Shows when .s01__content top reaches viewport top, hides once .site-footer enters the viewport. =========================================================== */ } catch (e) { if (window.console) console.error('[top_renewal.js] 口コミ評価', e); } try { document.addEventListener('scroll', function () { var banner = document.querySelector('.floating-banner'); var s01Content = document.querySelector('.s01__content'); var footer = document.querySelector('.site-footer, .f_nav07_wrap'); /* 2026-10-01: フッターは %%TEMPLATE_FOOTER%% の .f_nav07_wrap */ if (!banner || !s01Content || !footer) { return; } var s01Rect = s01Content.getBoundingClientRect(); var footerRect = footer.getBoundingClientRect(); /* Condition 1: s01__content top is at or above the top of the viewport */ var isPastS01Top = s01Rect.top <= 0; /* Condition 2: footer top is below the bottom of the viewport (not visible yet) */ var isBeforeFooter = footerRect.top > window.innerHeight; if (isPastS01Top && isBeforeFooter) { banner.classList.add('is-active'); } else { banner.classList.remove('is-active'); } }); } catch (e) { if (window.console) console.error('[top_renewal.js] 追従バナー', e); } try { /* =========================================================== 4. アイソメトリックシティ(ヒーロー背景 v6) =========================================================== */ (function(){ 'use strict'; var svg = document.getElementById('city'); if (!svg) return; var NS = 'http://www.w3.org/2000/svg'; var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; var canHover = window.matchMedia('(hover:hover) and (pointer:fine)').matches; /* ---- シード付き乱数(毎回同じ「設計された街」) ---- */ var seed = 20260706; function rnd(){ seed = (seed * 1103515245 + 12345) % 2147483648; return seed / 2147483648; } function pick(arr){ return arr[Math.floor(rnd() * arr.length)]; } /* ---- アイソメトリック定数 ---- */ var T = 36, UX = 0.866, UY = 0.5; var BX = 6, BY = 5; // ブロック数 var BW = 3, RW = 1.15; // ブロック幅・道路幅(グリッド単位) var PITCH = BW + RW; var GX = BX * PITCH + RW, GY = BY * PITCH + RW; function P(gx, gy, z){ return { x:(gx - gy) * UX * T, y:(gx + gy) * UY * T - (z || 0) }; } function el(name, attrs, parent){ var e = document.createElementNS(NS, name); for (var k in attrs) e.setAttribute(k, attrs[k]); if (parent) parent.appendChild(e); return e; } /* ---- defs:面グラデーション&接地影 ---- */ var defs = el('defs', {}, svg); var gradStops = {}; function grad(id, c1, c2){ var g = el('linearGradient', { id:id, x1:'0', y1:'0', x2:'0', y2:'1' }, defs); gradStops[id] = [ el('stop', { offset:'0', 'stop-color':c1 }, g), el('stop', { offset:'1', 'stop-color':c2 }, g) ]; } grad('gYR', '#e6c51c', '#d3af09'); /* 黄・右面 */ grad('gYL', '#c6a50d', '#b09002'); /* 黄・左面 */ grad('gWR', '#ffffff', '#f2efe7'); /* 白・右面 */ grad('gWL', '#f1efe7', '#e3e0d4'); /* 白・左面 */ grad('gGY', '#f2da55', '#dcb914'); /* ガラス・黄ビル */ grad('gGW', '#e6edf5', '#c3d2e2'); /* ガラス・白ビル */ var ao = el('radialGradient', { id:'ao' }, defs); el('stop', { offset:'0', 'stop-color':'#5a5340', 'stop-opacity':'.36' }, ao); el('stop', { offset:'.65','stop-color':'#5a5340', 'stop-opacity':'.15' }, ao); el('stop', { offset:'1', 'stop-color':'#5a5340', 'stop-opacity':'0' }, ao); var cityG = el('g', {}, svg); /* ---- 全体バウンディングボックス(生成しながら追跡) ---- */ var PAD = 1.3; /* 地面の張り出し(グリッド単位) */ var bounds = { minX: P(-PAD, GY + PAD).x, maxX: P(GX + PAD, -PAD).x, minY: P(-PAD, -PAD).y, maxY: P(GX + PAD, GY + PAD).y }; function trackTop(gx, gy, h){ var y = P(gx, gy, h).y; if (y < bounds.minY) bounds.minY = y; } /* 接地影ヘルパー(スクリーン座標の楕円。グリッド円→軸平行楕円になる) */ function aoEllipse(parent, gx, gy, w, d, scale, dy){ var pc = P(gx + w/2, gy + d/2, 0); var H = Math.hypot(w, d) * 0.5 * (scale || 1); return el('ellipse', { cx:pc.x, cy:pc.y + (dy || 2), rx:(UX*T)*H, ry:(UY*T)*H, fill:'url(#ao)' }, parent); } /* ---- 地面(アプロン・道路・街区プレート) ---- */ var groundM = 'matrix(' + (UX*T) + ',' + (UY*T) + ',' + (-UX*T) + ',' + (UY*T) + ',0,0)'; var ground = el('g', { transform:groundM }, cityG); /* アプロン(歩道面・角丸) */ el('rect', { x:-PAD, y:-PAD, width:GX+PAD*2, height:GY+PAD*2, rx:1.1, fill:'#eceae1' }, ground); var i, j, k; /* 道路 */ for (i = 0; i <= BX; i++) el('rect', { x:i*PITCH, y:-PAD, width:RW, height:GY+PAD*2, fill:'#c9cacf' }, ground); for (j = 0; j <= BY; j++) el('rect', { x:-PAD, y:j*PITCH, width:GX+PAD*2, height:RW, fill:'#c9cacf' }, ground); /* センターライン */ for (i = 0; i <= BX; i++){ var cx = i*PITCH + RW/2; for (var dpos = -1; dpos < GY + 1; dpos += 0.62) el('rect', { x:cx-0.035, y:dpos, width:0.07, height:0.3, fill:'#fbfbf8' }, ground); } for (j = 0; j <= BY; j++){ var cy = j*PITCH + RW/2; for (var dpos2 = -1; dpos2 < GX + 1; dpos2 += 0.62) el('rect', { x:dpos2, y:cy-0.035, width:0.3, height:0.07, fill:'#fbfbf8' }, ground); } /* 横断歩道(一部の交差点) */ for (i = 0; i <= BX; i++) for (j = 0; j <= BY; j++){ if (rnd() > 0.45) continue; for (k = 0; k < 5; k++){ el('rect', { x:i*PITCH + 0.1 + k*0.2, y:j*PITCH - 0.42, width:0.13, height:0.32, fill:'#f6f6f2' }, ground); el('rect', { x:i*PITCH - 0.42, y:j*PITCH + 0.1 + k*0.2, width:0.32, height:0.13, fill:'#f6f6f2' }, ground); } } /* 街区プレート(角丸・ひとつ明るい面=敷地) */ for (i = 0; i < BX; i++) for (j = 0; j < BY; j++){ el('rect', { x:i*PITCH + RW + 0.06, y:j*PITCH + RW + 0.06, width:BW - 0.12, height:BW - 0.12, rx:0.3, fill:'#f4f2ea' }, ground); } /* ---- 描画順を守るデプスバンド(0.5単位) ---- */ var bandsG = el('g', {}, cityG); var bandMax = Math.ceil((GX + GY) * 2) + 4; var bands = []; for (i = 0; i <= bandMax; i++) bands.push(el('g', {}, bandsG)); function bandFor(depth){ var idx = Math.max(0, Math.min(bandMax, Math.round(depth * 2))); return bands[idx]; } /* ---- パレット ---- */ var basePal = { yellow: { right:'url(#gWR)', left:'url(#gWL)', top:'#d7c327', rim:'#fbfaf7', glass:'url(#gGW)', pier:'#d7c327', stroke:'rgba(26,26,26,.07)', faceClassR:'bf-r', faceClassL:'bf-l' }, white: { right:'url(#gWR)', left:'url(#gWL)', top:'#d7c327', rim:'#fbfaf7', glass:'url(#gGW)', pier:'#d7c327', stroke:'rgba(26,26,26,.07)', faceClassR:'bf-r', faceClassL:'bf-l' } }; var unitPal = { right:'#ffffff', left:'#eceadf', top:'#d7c327', glass:'#e6edf5', pier:'#d7c327', stroke:'rgba(26,26,26,.06)', faceClassR:'bf-r', faceClassL:'bf-l' }; var glintGlass = []; /* ---- 面を描く(ピア/フレーム窓/床スラブ/軒下影/白コーナー) ---- */ function drawFace(parent, origin, dirX, widthPx, faceH, pal, skin, fx){ fx = fx || {}; var g = el('g', { transform:'matrix(' + dirX.x + ',' + dirX.y + ',0,1,' + origin.x + ',' + origin.y + ')' }, parent); el('rect', { x:0, y:0, width:widthPx, height:faceH, fill:pal.face, stroke:pal.stroke, 'stroke-width':0.8, 'class':pal.cls||'' }, g); var floorH = 24, n = Math.floor((faceH - 6) / floorH); var f, c, y0, yb; if (skin === 'piers'){ /* 全面ガラス+床スラブ+白い縦ピア */ el('rect', { x:1.2, y:2.4, width:widthPx - 2.4, height:faceH - 4.8, fill:pal.glass, 'class':'bf-glass' }, g); for (f = 1; f <= n; f++){ y0 = faceH - f * floorH; el('rect', { x:1.2, y:y0, width:widthPx - 2.4, height:3, fill:'#fbfaf7' }, g); } var pw = 6.5, bay = 23; var cols = Math.max(2, Math.round((widthPx - pw) / (bay + pw)) + 1); var step = (widthPx - pw) / (cols - 1); for (c = 0; c < cols; c++) el('rect', { x:c * step, y:0, width:pw, height:faceH, fill:pal.pier, 'class':'bf-pier' }, g); } else if (skin === 'punch'){ /* 白フレーム+奥まったガラスの窓(上端に奥行きの影) */ var colW = 26, colsP = Math.max(1, Math.floor((widthPx - 8) / colW)); var offP = (widthPx - colsP * colW) / 2; for (f = 0; f < n; f++){ y0 = faceH - (f + 1) * floorH; for (c = 0; c < colsP; c++){ var wx = offP + c * colW + 5, wy = y0 + 5; el('rect', { x:wx, y:wy, width:16, height:15, fill:'#fbfaf7', rx:0.6 }, g); var gl = el('rect', { x:wx + 1.7, y:wy + 1.7, width:12.6, height:11.6, fill:pal.glass, 'class':'bf-glass' }, g); el('rect', { x:wx + 1.7, y:wy + 1.7, width:12.6, height:1.7, fill:'rgba(26,26,26,.14)' }, g); glintGlass.push(gl); } } } else if (skin === 'band'){ for (f = 0; f < n; f++){ y0 = faceH - (f + 1) * floorH; el('rect', { x:1.5, y:y0 + floorH - 6.5, width:widthPx - 3, height:5, fill:'#ffffff' }, g); var colW3 = 24, cols3 = Math.max(1, Math.floor((widthPx - 6) / colW3)); var off3 = (widthPx - cols3 * colW3) / 2; for (c = 0; c < cols3; c++){ el('rect', { x:off3 + c*colW3 + 5, y:y0 + 6, width:colW3 - 11, height:floorH - 15, fill:pal.glass, 'class':'bf-glass' }, g); } } } else if (skin === 'balcony'){ for (f = 0; f < n; f++){ y0 = faceH - (f + 1) * floorH; yb = y0 + floorH; el('rect', { x:4, y:y0 + 5.5, width:widthPx - 8, height:floorH - 11, fill:pal.glass, 'class':'bf-glass' }, g); for (var rx0 = 3; rx0 < widthPx - 3; rx0 += 5.5) el('rect', { x:rx0, y:yb - 13.5, width:1.1, height:9, fill:'#ffffff' }, g); el('rect', { x:2, y:yb - 14.6, width:widthPx - 4, height:1.6, fill:'#ffffff' }, g); el('rect', { x:1.5, y:yb - 5, width:widthPx - 3, height:4, fill:'#ffffff' }, g); el('rect', { x:1.5, y:yb - 1, width:widthPx - 3, height:1.1, fill:'rgba(26,26,26,.10)' }, g); } } else if (skin === 'shopfront'){ /* 基壇のストアフロント:ガラス帯+ピア+白いエントランス */ el('rect', { x:1.5, y:3.5, width:widthPx - 3, height:faceH - 7, fill:pal.glass, 'class':'bf-glass' }, g); for (var sx = 0; sx < widthPx - 7; sx += 26){ el('rect', { x:sx + 1.5, y:3.5, width:5.5, height:faceH - 7, fill:pal.pier, 'class':'bf-pier' }, g); } el('rect', { x:widthPx/2 - 6.5, y:5, width:13, height:faceH - 8.5, fill:'#fbfaf7' }, g); el('rect', { x:widthPx/2 - 4.5, y:7, width:9, height:faceH - 12.5, fill:pal.glass, 'class':'bf-glass' }, g); } if (fx.oh) el('rect', { x:0, y:0, width:widthPx, height:2.6, fill:'rgba(26,26,26,.10)' }, g); if (fx.corner) el('rect', { x:widthPx - 1.6, y:0, width:1.6, height:faceH, fill:'#fbfaf7' }, g); return g; } /* ---- 箱を描く(面 → 軒の出た白パラペット の順で描画) ---- */ function drawBox(parent, gx, gy, w, d, h, pal, skinR, skinL, z0, fx){ z0 = z0 || 0; fx = fx || {}; var faceH = h - z0; var TB0 = P(gx + w, gy, h), TD0 = P(gx, gy + d, h); drawFace(parent, TB0, { x:-UX, y:UY }, d*T, faceH, { face:pal.right, glass:pal.glass, pier:pal.pier, stroke:pal.stroke, cls:pal.faceClassR }, skinR, fx); drawFace(parent, TD0, { x:UX, y:UY }, w*T, faceH, { face:pal.left, glass:pal.glass, pier:pal.pier, stroke:pal.stroke, cls:pal.faceClassL }, skinL, fx); /* パラペット(rimOut 時は footprint より外に張り出す=軒) */ var o = fx.rimOut ? 0.075 : 0; var RA = P(gx - o, gy - o, h), RB = P(gx + w + o, gy - o, h), RC = P(gx + w + o, gy + d + o, h), RD = P(gx - o, gy + d + o, h); var pts = RA.x+','+RA.y+' '+RB.x+','+RB.y+' '+RC.x+','+RC.y+' '+RD.x+','+RD.y; if (pal.rim){ var cxm = (RA.x+RB.x+RC.x+RD.x)/4, cym = (RA.y+RB.y+RC.y+RD.y)/4; var ins = function(p){ return (p.x + (cxm-p.x)*0.16) + ',' + (p.y + (cym-p.y)*0.16); }; var innerPts = ins(RA)+' '+ins(RB)+' '+ins(RC)+' '+ins(RD); el('polygon', { points:pts, fill:pal.rim, stroke:pal.stroke, 'stroke-width':0.8, 'class':'bf-rim' }, parent); el('polygon', { points:innerPts, fill:pal.top, 'class':'bf-top' }, parent); } else { el('polygon', { points:pts, fill:pal.top, stroke:pal.stroke, 'stroke-width':0.8, 'class':'bf-top' }, parent); } } /* ---- 投資タイプ(物件カード用) ---- */ var CATS = [ { key:'rc', label:'一棟RCマンション', floors:[9,12], yellowP:1 }, { key:'mansion', label:'一棟マンション', floors:[5,8], yellowP:0.97 }, { key:'apart', label:'一棟アパート', floors:[3,4], yellowP:0.95 }, { key:'office', label:'商業・オフィス', floors:[7,11], yellowP:0.7 } ]; var AREAS = ['札幌市','仙台市','さいたま市','千葉市','東京都','横浜市','名古屋市','大阪市','京都市','神戸市','広島市','福岡市','熊本市','那覇市']; /* ---- 建物を生成 ---- */ var buildings = []; var landmarks = {}; landmarks[Math.floor(rnd()*BX) + '-' + Math.floor(rnd()*BY)] = true; landmarks[Math.floor(rnd()*BX) + '-' + Math.floor(rnd()*BY)] = true; function pickCat(blockKey){ if (landmarks[blockKey] && rnd() > 0.5) return rnd() < 0.6 ? CATS[0] : CATS[3]; var r = rnd(); if (r < 0.10) return CATS[0]; if (r < 0.40) return CATS[1]; if (r < 0.90) return CATS[2]; return CATS[3]; } function addBuilding(gx, gy, w, d, blockKey){ var cat = pickCat(blockKey); var floors = cat.floors[0] + Math.floor(rnd() * (cat.floors[1] - cat.floors[0] + 1)); var h = floors * 24 + Math.floor(rnd()*8); /* デフォルトは白/グレーパレット。黄色はホバー時に CSS で付与 */ var isYellow = rnd() < cat.yellowP; var pal = {}; var src = isYellow ? basePal.yellow : basePal.white; for (var kk in src) pal[kk] = src[kk]; if (!isYellow && rnd() < 0.55) pal.top = '#e8d87f'; var skins = ['punch','piers','band','balcony','punch','band']; var skinR = pick(skins); var skins2 = skinR === 'piers' ? skins.filter(function(k){ return k !== 'piers'; }) : skins; var skinL = pick(skins2); var outer = el('g', { 'class':'b-outer', 'data-cat':cat.key }); aoEllipse(outer, gx, gy, w, d, 0.78, 3); var hover = el('g', { 'class':'b-hover' }, outer); var rise = el('g', { 'class':'b-rise' }, hover); var podH = 30; var tiered = floors >= 7 && rnd() > 0.55; if (floors >= 4){ /* 基壇(白コーニス+ストアフロント)+上層 */ drawBox(rise, gx, gy, w, d, podH, pal, 'shopfront', 'shopfront', 0, { rimOut:true, corner:true }); if (tiered){ var tw = w * (0.62 + rnd()*0.14), td = d * (0.62 + rnd()*0.14); var tx0 = gx + (w - tw) * (rnd() > 0.5 ? 0.78 : 0.22), ty0 = gy + (d - td) * 0.5; drawBox(rise, tx0, ty0, tw, td, h, pal, skinR, skinL, podH, { rimOut:true, oh:true, corner:true }); trackTop(tx0, ty0, h); } else { drawBox(rise, gx, gy, w, d, h, pal, skinR, skinL, podH, { rimOut:true, oh:true, corner:true }); trackTop(gx, gy, h); } } else { drawBox(rise, gx, gy, w, d, h, pal, skinR, skinL, 0, { rimOut:true, oh:true, corner:true }); trackTop(gx, gy, h); } /* ペントハウス/屋上設備(白い箱) */ if (h > 130 && rnd() > 0.45){ var pw2 = w * 0.26, pd2 = d * 0.26; var px0 = gx + w * (0.15 + rnd()*0.35), py0 = gy + d * (0.15 + rnd()*0.35); drawBox(rise, px0, py0, pw2, pd2, h + 11, unitPal, null, null, h, {}); trackTop(px0, py0, h + 11); } var depth = gx + w + gy + d; bandFor(depth).appendChild(outer); var delay = ((GX + GY - depth) * 0.028 + rnd() * 0.25).toFixed(2); rise.style.animationDelay = delay + 's'; var data = { cat: cat, area: AREAS[(Math.floor(gx) + Math.floor(gy)) % AREAS.length], yield: (4 + rnd() * 4.6).toFixed(1), age: Math.floor(2 + rnd() * 32) }; buildings.push({ outer:outer, depth:depth, data:data }); } var bi, bj; for (bi = 0; bi < BX; bi++){ for (bj = 0; bj < BY; bj++){ var ox = bi * PITCH + RW, oy = bj * PITCH + RW; var key = bi + '-' + bj; var m = 0.26, g2 = 0.22; var split = rnd(); if (split < 0.3){ addBuilding(ox + m, oy + m, BW - m*2, BW - m*2, key); } else if (split < 0.65){ var w1 = (BW - m*2 - g2) * (0.4 + rnd()*0.2); addBuilding(ox + m, oy + m, w1, BW - m*2, key); addBuilding(ox + m + w1 + g2, oy + m, BW - m*2 - g2 - w1, BW - m*2, key); } else { var d1 = (BW - m*2 - g2) * (0.4 + rnd()*0.2); addBuilding(ox + m, oy + m, BW - m*2, d1, key); addBuilding(ox + m, oy + m + d1 + g2, BW - m*2, BW - m*2 - g2 - d1, key); } } } /* ---- 街灯 ---- */ for (i = 0; i < 14; i++){ var lbi = Math.floor(rnd()*BX), lbj = Math.floor(rnd()*BY); var lgx = lbi * PITCH + RW - 0.18 + (rnd() > 0.5 ? BW + 0.36 : 0); var lgy = lbj * PITCH + RW + rnd() * BW; var base = P(lgx, lgy, 0), top = P(lgx, lgy, 30); var lg = el('g', {}); el('ellipse', { cx:base.x, cy:base.y + 1, rx:4, ry:1.8, fill:'url(#ao)' }, lg); el('line', { x1:base.x, y1:base.y, x2:top.x, y2:top.y, stroke:'#b3b3b8', 'stroke-width':1.5 }, lg); el('circle', { cx:top.x, cy:top.y, r:3, fill:'#e1c721' }, lg); bandFor(lgx + lgy).appendChild(lg); } /* ---- 樹木 v6(3トーンもこもこ広葉樹+円錐街路樹) ---- */ var TREE_C = { shade:'#649b42', mid:'#8fbf5a', light:'#b9da7e', dot:'#ddf0b0', trunk:'#9c6b35', trunkL:'#b5854a', planter:'#e2dfd3', planterIn:'#cfccbe' }; function treeRound(tgx, tgy, sc){ var b = P(tgx, tgy, 0); var g = el('g', {}); el('ellipse', { cx:b.x, cy:b.y + 1, rx:12*sc, ry:4.4*sc, fill:'url(#ao)' }, g); /* 丸い植込み */ el('ellipse', { cx:b.x, cy:b.y, rx:6.5*sc, ry:2.6*sc, fill:TREE_C.planter }, g); el('ellipse', { cx:b.x, cy:b.y - 0.7*sc, rx:5*sc, ry:1.9*sc, fill:TREE_C.planterIn }, g); /* 幹+枝分かれ */ el('line', { x1:b.x, y1:b.y, x2:b.x, y2:b.y - 15*sc, stroke:TREE_C.trunk, 'stroke-width':3.6*sc, 'stroke-linecap':'round' }, g); el('line', { x1:b.x - 0.4*sc, y1:b.y - 9*sc, x2:b.x - 5.5*sc, y2:b.y - 17*sc, stroke:TREE_C.trunk, 'stroke-width':2.2*sc, 'stroke-linecap':'round' }, g); el('line', { x1:b.x + 0.2*sc, y1:b.y - 11*sc, x2:b.x + 5*sc, y2:b.y - 18.5*sc, stroke:TREE_C.trunkL, 'stroke-width':2*sc, 'stroke-linecap':'round' }, g); /* 樹冠:影→中間→光のブロブ重ね+ハイライト点 */ var cy0 = b.y - 26*sc; var blobs = [ [-5.5, 4.5, 7.0, 'shade'], [ 0.5, 6.0, 6.2, 'shade'], [-8.0, -0.5, 5.2, 'shade'], [ 0.0, 0.0, 8.6, 'mid' ], [-4.5, -4.5, 5.8, 'mid' ], [ 6.0, 2.5, 6.2, 'mid'], [ 7.5, -2.0, 4.6, 'mid'], [ 4.0, -4.5, 5.6, 'light'], [ 0.0, -7.0, 5.0, 'light'], [-2.5, -5.5, 3.8, 'light'] ]; for (var bz = 0; bz < blobs.length; bz++){ var bl = blobs[bz]; el('circle', { cx:b.x + bl[0]*sc, cy:cy0 + bl[1]*sc, r:bl[2]*sc, fill:TREE_C[bl[3]] }, g); } el('circle', { cx:b.x + 5.5*sc, cy:cy0 - 6*sc, r:2.1*sc, fill:TREE_C.dot }, g); el('circle', { cx:b.x + 1.5*sc, cy:cy0 - 9*sc, r:1.7*sc, fill:TREE_C.dot }, g); bandFor(tgx + tgy).appendChild(g); } function treeCone(tgx, tgy, sc){ var b = P(tgx, tgy, 0); var g = el('g', {}); el('ellipse', { cx:b.x, cy:b.y + 1, rx:7.5*sc, ry:3*sc, fill:'url(#ao)' }, g); el('rect', { x:b.x - 1.2*sc, y:b.y - 5*sc, width:2.4*sc, height:5*sc, fill:TREE_C.trunk }, g); /* 段々スカラップ */ var tiers = [ [ 9.0, 7.5, 5.8], [16.5, 6.0, 4.8], [23.0, 4.6, 3.8], [28.5, 3.0, 2.6] ]; for (var ti = 0; ti < tiers.length; ti++){ var tr = tiers[ti]; el('ellipse', { cx:b.x, cy:b.y - tr[0]*sc, rx:tr[1]*sc, ry:tr[2]*sc, fill:TREE_C.shade }, g); el('ellipse', { cx:b.x + 0.9*sc, cy:b.y - (tr[0]+1)*sc, rx:Math.max(0.8, tr[1]-1.1)*sc, ry:Math.max(0.7, tr[2]-0.9)*sc, fill:TREE_C.mid }, g); el('ellipse', { cx:b.x + 1.6*sc, cy:b.y - (tr[0]+1.7)*sc, rx:Math.max(0.6, tr[1]-2.6)*sc, ry:Math.max(0.5, tr[2]-1.8)*sc, fill:TREE_C.light }, g); } bandFor(tgx + tgy).appendChild(g); } /* 街区プレートの手前角:広葉樹 */ for (bi = 0; bi < BX; bi++) for (bj = 0; bj < BY; bj++){ if (rnd() > 0.55) continue; treeRound(bi*PITCH + RW + BW - 0.16 - rnd()*0.05, bj*PITCH + RW + BW - 0.16 - rnd()*0.05, 0.8 + rnd()*0.35); } /* 外周アプロン:円錐の並木+大きめの広葉樹 */ for (i = 0; i < 7; i++){ treeCone(GX + 0.5, 1.6 + i * (GY - 3) / 6, 0.85 + rnd()*0.2); treeCone(1.6 + i * (GX - 3) / 6, GY + 0.5, 0.85 + rnd()*0.2); } treeRound(GX + 0.78, GY * 0.32, 1.15); treeRound(GX * 0.3, GY + 0.78, 1.2); treeRound(GX + 0.78, GY * 0.78, 1.05); /* ---- 通行人:現在 0 人(ループは乱数の消費順を保つため残置) ---- */ var people = []; for (i = 0; i < 0; i++){ var pbi = Math.floor(rnd()*BX), pbj = Math.floor(rnd()*BY); var pgx = pbi * PITCH + (rnd() > 0.5 ? RW * 0.3 : RW + BW + RW * 0.3); var pgy = pbj * PITCH + RW + rnd() * BW; if (rnd() > 0.5){ var tmp = pgx; pgx = pbj * PITCH + RW + rnd() * BW; pgy = tmp; } var pb = P(pgx, pgy, 0); var pg = el('g', {}); var tone = pick(['#e1c721', '#d4b91e', '#c8b01b']); el('ellipse', { cx:pb.x, cy:pb.y + 0.8, rx:3.6, ry:1.5, fill:'url(#ao)' }, pg); el('rect', { x:pb.x - 2.1, y:pb.y - 9.4, width:4.2, height:7.8, rx:2.1, fill:tone }, pg); el('circle', { cx:pb.x, cy:pb.y - 11.7, r:2.5, fill:tone }, pg); bandFor(pgx + pgy).appendChild(pg); people.push({ g:pg, walk: rnd() > 0.62, gx:pgx, gy:pgy, bx:pb.x, by:pb.y, dir: rnd() > 0.5 ? 1 : -1, speed: 0.12 + rnd()*0.1 }); } /* ---- 車(道なりに曲がる:ブロック周回ルート) ---- */ var cars = []; function roadX(i){ return i * PITCH + RW/2; } function roadY(j){ return j * PITCH + RW/2; } function drawCarShape(car){ while (car.g.firstChild) car.g.removeChild(car.g.firstChild); var vertical = car.vert; var w = vertical ? 0.26 : 0.52, d = vertical ? 0.52 : 0.26; aoEllipse(car.g, 0, 0, w, d, 0.75, 2.5); var bodyPal = { right:car.col, left:car.col, top:'#f7f7f7', win:'#fff', shop:'#cfd9e3', stroke:'rgba(255,255,255,.4)' }; var cabPal = { right:'#cfd9e3', left:'#c2ccd6', top:car.col, win:'#fff', shop:'#cfd9e3', stroke:'rgba(255,255,255,.4)' }; drawBox(car.g, 0, 0, w, d, 5, bodyPal, null, null, 0, false); var cw = vertical ? 0.19 : 0.28, cd = vertical ? 0.28 : 0.19; var cgx = vertical ? (w - cw)/2 : w*0.16, cgy = vertical ? d*0.16 : (d - cd)/2; drawBox(car.g, cgx, cgy, cw, cd, 9.5, cabPal, null, null, 4.5, false); } function makeCarLoop(bi, bj, lane, speed, phase){ var x0 = roadX(bi)+lane, x1 = roadX(bi+1)+lane, y0 = roadY(bj)+lane, y1 = roadY(bj+1)+lane; var wp = [ {x:x0,y:y0}, {x:x1,y:y0}, {x:x1,y:y1}, {x:x0,y:y1} ]; var car = { g:el('g', {}), wp:wp, seg:0, t:phase || 0, speed:speed, col:pick(['#8f8f96','#75757d','#ffffff','#e1c721']), vert:false, band:null }; drawCarShape(car); cars.push(car); } makeCarLoop(1, 1, -0.14, 1.25, 0.0); makeCarLoop(3, 2, -0.14, 1.50, 2.2); makeCarLoop(4, 0, -0.14, 1.10, 1.1); makeCarLoop(2, 3, -0.14, 1.60, 3.0); makeCarLoop(0, 2, -0.14, 1.35, 0.6); makeCarLoop(4, 3, -0.14, 1.40, 0.8); /* 手前 */ makeCarLoop(5, 3, -0.14, 1.20, 2.6); /* 手前右 */ makeCarLoop(3, 3, -0.14, 1.55, 1.7); /* 手前中央 */ makeCarLoop(5, 2, -0.14, 1.30, 3.4); makeCarLoop(2, 2, -0.14, 1.45, 0.3); makeCarLoop(4, 1, -0.14, 1.15, 2.1); makeCarLoop(1, 3, -0.14, 1.35, 1.2); /* 手前左 */ makeCarLoop(0, 3, -0.14, 1.50, 2.9); /* 手前左端 */ function carStep(dt){ for (var c = 0; c < cars.length; c++){ var car = cars[c]; var a = car.wp[car.seg], b = car.wp[(car.seg + 1) % car.wp.length]; var segLen = Math.abs(b.x - a.x) + Math.abs(b.y - a.y); car.t += car.speed * dt; var guard = 0; while (segLen > 0 && car.t >= segLen && guard++ < 8){ car.t -= segLen; car.seg = (car.seg + 1) % car.wp.length; a = car.wp[car.seg]; b = car.wp[(car.seg + 1) % car.wp.length]; segLen = Math.abs(b.x - a.x) + Math.abs(b.y - a.y); } var frac = segLen > 0 ? car.t / segLen : 0; var gx = a.x + (b.x - a.x) * frac, gy = a.y + (b.y - a.y) * frac; var vertical = Math.abs(b.y - a.y) > Math.abs(b.x - a.x); if (vertical !== car.vert){ car.vert = vertical; drawCarShape(car); } var pt = P(gx, gy, 0); car.g.setAttribute('transform', 'translate(' + pt.x + ',' + pt.y + ')'); var bd = Math.round((gx + gy) * 2); if (bd !== car.band){ car.band = bd; bandFor(gx + gy).appendChild(car.g); } } } function peopleStep(dt){ for (var p = 0; p < people.length; p++){ var pe = people[p]; if (!pe.walk) continue; pe.gy += pe.speed * dt * pe.dir; var lo = Math.floor(pe.gy / PITCH) * PITCH + RW; if (pe.gy < lo || pe.gy > lo + BW){ pe.dir *= -1; pe.gy = Math.max(lo, Math.min(lo + BW, pe.gy)); } var pt = P(pe.gx, pe.gy, 0); pe.g.setAttribute('transform', 'translate(' + (pt.x - pe.bx) + ',' + (pt.y - pe.by) + ')'); } } if (reduced){ carStep(0); } /* ---- viewBox:slice + ZOOM で四辺にはみ出すカバー構図 ---- */ var VPAD = 18; var ZOOM = 1.3; /* 大きさの調整ポイント(1=全体が収まる / 大きいほど寄る) */ var bw = bounds.maxX - bounds.minX + VPAD*2; var bh = bounds.maxY - bounds.minY + VPAD*2 + 10; var vcx = bounds.minX - VPAD + bw/2; var vcy = bounds.minY - VPAD - 10 + bh/2; svg.setAttribute('viewBox', (vcx - bw/ZOOM/2) + ' ' + (vcy - bh/ZOOM/2) + ' ' + (bw/ZOOM) + ' ' + (bh/ZOOM)); /* ---- 背景の雲(建物の奥) ---- */ function cloud(cx0, cy0, s){ var cg = el('g', { opacity:'.9' }); el('rect', { x:cx0 - 60*s, y:cy0 - 16*s, width:120*s, height:34*s, rx:17*s, fill:'#ffffff' }, cg); el('circle', { cx:cx0 - 22*s, cy:cy0 - 20*s, r:20*s, fill:'#ffffff' }, cg); el('circle', { cx:cx0 + 12*s, cy:cy0 - 26*s, r:26*s, fill:'#ffffff' }, cg); el('circle', { cx:cx0 + 38*s, cy:cy0 - 14*s, r:16*s, fill:'#ffffff' }, cg); cityG.insertBefore(cg, bandsG); } cloud(bounds.minX + 300, bounds.minY + 130, 1.15); cloud(bounds.maxX - 260, bounds.minY + 210, 0.85); cloud(bounds.minX + 560, bounds.minY + 40, 0.62); /* ---- 地面の縁を紙色へフェード ---- */ var gf = el('linearGradient', { id:'groundFade', x1:'0', y1:'0', x2:'0.3', y2:'1' }, defs); el('stop', { offset:'0', 'stop-color':'#f7f7f4', 'stop-opacity':'.7' }, gf); el('stop', { offset:'0.3', 'stop-color':'#f7f7f4', 'stop-opacity':'.28' }, gf); el('stop', { offset:'0.55', 'stop-color':'#f7f7f4', 'stop-opacity':'0' }, gf); var fadeRect = el('rect', { x:bounds.minX - VPAD, y:bounds.minY - VPAD, width:bounds.maxX - bounds.minX + VPAD*2, height:bounds.maxY - bounds.minY + VPAD*2, fill:'url(#groundFade)' }); cityG.insertBefore(fadeRect, cityG.children[1]); /* 地面の直後・雲/建物の下 */ /* ---- 太陽光ループ(光の向きが LIGHT_CYCLE 秒でゆっくり一周) ---- */ var LIGHT_CYCLE = 48; /* 1周の秒数(調整ポイント) */ var TH0 = 24 * Math.PI / 180; /* 初期方位=静止時の見た目 */ var MAT = { gYR:{ lit:['#eacb22','#d6b30b'], dark:['#9d8404','#8a7300'], side:'R' }, gYL:{ lit:['#eacb22','#d6b30b'], dark:['#9d8404','#8a7300'], side:'L' }, gWR:{ lit:['#ffffff','#f2efe7'], dark:['#d8d5c9','#cbc8bb'], side:'R' }, gWL:{ lit:['#ffffff','#f2efe7'], dark:['#d8d5c9','#cbc8bb'], side:'L' }, gGY:{ lit:['#f2da55','#dcb914'], dark:['#b29512','#9d8208'], side:'G' }, gGW:{ lit:['#e6edf5','#c3d2e2'], dark:['#b3c0cf','#9fadbd'], side:'G' } }; function hx(h){ return [parseInt(h.substr(1,2),16), parseInt(h.substr(3,2),16), parseInt(h.substr(5,2),16)]; } for (var mk in MAT){ MAT[mk].litC = [hx(MAT[mk].lit[0]), hx(MAT[mk].lit[1])]; MAT[mk].darkC = [hx(MAT[mk].dark[0]), hx(MAT[mk].dark[1])]; } function mixc(a, b, t){ return 'rgb(' + Math.round(a[0]+(b[0]-a[0])*t) + ',' + Math.round(a[1]+(b[1]-a[1])*t) + ',' + Math.round(a[2]+(b[2]-a[2])*t) + ')'; } var aoList = Array.prototype.slice.call(svg.querySelectorAll('ellipse[fill="url(#ao)"]')); var veil = el('rect', { x:bounds.minX - VPAD, y:bounds.minY - VPAD - 10, width:bounds.maxX - bounds.minX + VPAD*2, height:bounds.maxY - bounds.minY + VPAD*2 + 10, fill:'#e8912d', 'fill-opacity':'0', 'pointer-events':'none' }, cityG); function applyLight(th){ var LR = Math.max(0, Math.cos(th)), LL = Math.max(0, Math.sin(th)); var LG = Math.min(1, (LR + LL) * 0.5 + 0.18); /* ガラスは両面の平均+反射ぶん */ for (var mk2 in MAT){ var m = MAT[mk2]; var L = m.side === 'R' ? LR : (m.side === 'L' ? LL : LG); var t = 0.22 + 0.78 * L; /* 環境光22%を残す */ gradStops[mk2][0].setAttribute('stop-color', mixc(m.darkC[0], m.litC[0], t)); gradStops[mk2][1].setAttribute('stop-color', mixc(m.darkC[1], m.litC[1], t)); } /* 接地影は太陽と反対側へゆっくり移動 */ var sx = -(Math.cos(th) - Math.sin(th)) * UX * 5; var sy = -(Math.cos(th) + Math.sin(th)) * UY * 4; var tr = 'translate(' + sx.toFixed(1) + ',' + sy.toFixed(1) + ')'; for (var ai = 0; ai < aoList.length; ai++) aoList[ai].setAttribute('transform', tr); /* 逆光の時間帯はほんのり夕景 */ veil.setAttribute('fill-opacity', (0.09 * (1 - Math.max(LR, LL))).toFixed(3)); } var lightT = 0, lightAcc = 1; /* ---- 物件カード(ホバー/タップで表示) ---- */ var tip = document.getElementById('tip'); var tipCat = document.getElementById('tipCat'); var tipArea = document.getElementById('tipArea'); var tipMeta = document.getElementById('tipMeta'); var tipOn = false; if (canHover){ buildings.forEach(function(b){ b.outer.addEventListener('mouseenter', function(){ tipCat.textContent = 'PICK UP ― ' + b.data.cat.key.toUpperCase(); tipArea.textContent = b.data.area + '・' + b.data.cat.label; tipMeta.innerHTML = '想定利回り ' + b.data.yield + '% ・ 築' + b.data.age + '年'; tip.classList.add('on'); tipOn = true; }); b.outer.addEventListener('mouseleave', function(){ tip.classList.remove('on'); tipOn = false; }); }); document.addEventListener('mousemove', function(e){ if (!tipOn) return; var x = Math.min(e.clientX + 18, window.innerWidth - 240); var y = e.clientY - 14; tip.style.left = x + 'px'; tip.style.top = y + 'px'; }); } else { tip.style.left = '16px'; tip.style.right = '16px'; tip.style.bottom = '16px'; tip.style.top = 'auto'; buildings.forEach(function(b){ b.outer.addEventListener('click', function(ev){ ev.stopPropagation(); tipCat.textContent = 'PICK UP'; tipArea.textContent = b.data.area + '・' + b.data.cat.label; tipMeta.innerHTML = '想定利回り ' + b.data.yield + '% ・ 築' + b.data.age + '年'; tip.classList.add('on'); }); }); document.addEventListener('click', function(){ tip.classList.remove('on'); }); } /* ---- マウスパララックス(奥行きに応じて建物が別々に動く) ---- */ var stage = document.getElementById('cityStage'); var mx = 0, my = 0, cxr = 0, cyr = 0; var maxDepth = GX + GY; if (canHover && !reduced){ stage.addEventListener('mousemove', function(e){ var r = stage.getBoundingClientRect(); mx = (e.clientX - r.left) / r.width - 0.5; my = (e.clientY - r.top) / r.height - 0.5; }); stage.addEventListener('mouseleave', function(){ mx = 0; my = 0; }); } var last = performance.now(); var rafOn = false, stageVisible = true; function frame(now){ var dt = Math.min(0.05, (now - last) / 1000); last = now; cxr += (mx - cxr) * 0.055; cyr += (my - cyr) * 0.055; if (canHover){ for (var b = 0; b < buildings.length; b++){ var bd = buildings[b]; var n = bd.depth / maxDepth; /* 0=奥 1=手前 */ var dx = -cxr * (4 + n * 26); var dy = -cyr * (2 + n * 12); bd.outer.setAttribute('transform', 'translate(' + dx + ',' + dy + ')'); } } carStep(dt); peopleStep(dt); lightT += dt; lightAcc += dt; if (lightAcc > 0.12){ lightAcc = 0; applyLight(TH0 - (lightT / LIGHT_CYCLE) * Math.PI * 2); } if (stageVisible){ requestAnimationFrame(frame); } else { rafOn = false; } } function startLoop(){ if (reduced || rafOn) return; rafOn = true; last = performance.now(); requestAnimationFrame(frame); } /* セクションが画面外に出たらループを止めてCPUを使わない */ if ('IntersectionObserver' in window){ new IntersectionObserver(function(entries){ stageVisible = entries[0].isIntersecting; if (stageVisible) startLoop(); }).observe(stage); } startLoop(); /* ---- 窓の反射きらめき ---- */ if (!reduced && glintGlass.length){ setInterval(function(){ if (!stageVisible) return; for (var n = 0; n < 3; n++){ var w = glintGlass[Math.floor(Math.random() * glintGlass.length)]; w.classList.remove('win-glint'); void w.getBoundingClientRect(); /* リフローを強制してアニメを再発火 */ w.classList.add('win-glint'); } }, 1700); } })(); } catch (e) { if (window.console) console.error('[top_renewal.js] アイソメトリックシティ', e); } try { /* =========================================================== 5. ヒーローのカウントアップ+屋上のピン 2秒ごとに「上手くいった数」を +1(100で1に戻る)し、 見えている建物の屋上にピンを1つ立てる =========================================================== */ (function(){ var el = document.querySelector(".hero__count-num"); if(!el) return; var target = parseInt(el.getAttribute("data-count-to"),10) || 100; function fmt(n){ return n.toLocaleString("en-US"); } var n = 1; el.textContent = fmt(n); if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; /* 建物ごとのメインの屋上 = 面積が最大の3割未満の小箱(ペントハウス等)を除外した中で最も高い天井 */ function mainRoofRect(b){ var roofs = b.querySelectorAll(".bf-top"); if(roofs.length === 0) return null; var rects = [], maxArea = 0, j; for(j=0; j maxArea) maxArea = ar; } var rr = null; for(j=0; j 200) return; /* ヒーローが見えている時だけ */ var svg = document.getElementById("city"); if(!svg) return; var bs = svg.querySelectorAll(".b-outer"), vis = []; for(var i=0;i0 && r.top>150 && r.top60 && r.right'; } dynamicIcon = '' + textContent + ''; } var d = document.createElement("div"); d.className = "rt-pop"; d.innerHTML = '
' + dynamicIcon + '
' + '
'; d.style.zIndex = "50"; /* 屋根の中心座標(X/Y の中心オフセットは .rt-pop 側の CSS で処理) */ d.style.left = (rr.left + (rr.width / 2)) + "px"; d.style.top = (rr.top + (rr.height / 2)) + "px"; /* .hero の中に入れる(body 直下だと MESSAGE / SEARCH カードを覆ってしまうため) */ (document.getElementById("hero") || document.body).appendChild(d); setTimeout(function(){ if(d.parentNode) d.parentNode.removeChild(d); }, 1600); } setInterval(function(){ n = (n >= target) ? 1 : n + 1; el.textContent = fmt(n); popIcon(); }, 2000); })(); } catch (e) { if (window.console) console.error('[top_renewal.js] カウントアップ', e); } try { /* =========================================================== 6. 口コミカルーセル(Elfsight の Swiper)の見切れ調整 ・最初から左右にカードが見切れた状態(中央3枚+左右40px)にする ・ウィジェットは画面に入ってから読み込まれ、描画完了時に1枚目へ戻すため、 画面に入るたびに8秒間の寄せ直し期間を作る ・訪問者が矢印やカードを触った後は勝手に動かさない ・loop や作り直しは矢印・ドットが壊れるので使わない =========================================================== */ (function(){ var SEL = '#google_voice .swiper', TARGET = 2; var wrapEl = document.getElementById('google_voice'); if (!wrapEl) return; var userTouched = false, armedUntil = 0, hooked = null, poll = null; ['pointerdown','touchstart','keydown'].forEach(function(ev){ wrapEl.addEventListener(ev, function(){ userTouched = true; }, { passive:true, capture:true }); }); function settle(sw){ if (userTouched || Date.now() > armedUntil) return; if (sw.slides && sw.slides.length > TARGET + 2 && sw.activeIndex < TARGET) { sw.slideTo(TARGET, 0, false); } } function apply(){ var el = document.querySelector(SEL); if (!el || !el.swiper) return; var sw = el.swiper; if (hooked !== sw) { hooked = sw; armedUntil = Math.max(armedUntil, Date.now() + 8000); ['afterInit','update','resize','slidesLengthChange','slidesUpdated','imagesReady','slideChangeTransitionEnd'].forEach(function(n){ try { sw.on(n, function(){ setTimeout(function(){ settle(sw); }, 0); }); } catch(e){} }); } try { if (sw.params.slidesPerView !== 'auto' || !sw.params.centeredSlides || sw.params.spaceBetween !== 16) { sw.params.slidesPerView = 'auto'; sw.params.centeredSlides = true; sw.params.spaceBetween = 16; sw.update(); } } catch(e){} settle(sw); } function arm(){ if (userTouched) return; armedUntil = Date.now() + 8000; if (poll) return; poll = setInterval(function(){ apply(); if (userTouched || Date.now() > armedUntil) { clearInterval(poll); poll = null; } }, 300); } arm(); if (window.IntersectionObserver) { new IntersectionObserver(function(ents){ ents.forEach(function(e){ if (e.isIntersecting) arm(); }); }, { rootMargin:'200px 0px' }).observe(wrapEl); } if (window.MutationObserver) { var deb; new MutationObserver(function(){ clearTimeout(deb); deb = setTimeout(function(){ apply(); if (!userTouched && !poll) arm(); }, 200); }) .observe(wrapEl, { childList:true, subtree:true }); } window.addEventListener('resize', function(){ setTimeout(apply, 200); }); })(); } catch (e) { if (window.console) console.error('[top_renewal.js] 口コミ見切れ', e); }