// ============================================ // Main JavaScript // ============================================ // 季節判定・動画設定 (function() { 'use strict'; // localStorageの利用可否をチェック function isLocalStorageAvailable() { try { const test = '__localStorage_test__'; localStorage.setItem(test, test); localStorage.removeItem(test); return true; } catch (e) { return false; } } // localStorageにデータを安全に保存 function safeSetItem(key, value) { if (isLocalStorageAvailable()) { try { localStorage.setItem(key, value); } catch (e) { console.warn('Failed to save to localStorage:', e); } } } // localStorageからデータを安全に取得 function safeGetItem(key) { if (isLocalStorageAvailable()) { try { return localStorage.getItem(key); } catch (e) { console.warn('Failed to read from localStorage:', e); return null; } } return null; } // Base64デコード関数(メールアドレス復号化用) function decodeBase64(encodedStr) { try { return decodeURIComponent(atob(encodedStr).split('').map(function(c) { return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); }).join('')); } catch (e) { console.warn('Failed to decode email address:', e); return ''; } } // メールリンク動的生成(スパム対策) function setupEmailLinks() { const emailElements = document.querySelectorAll('[data-encoded-email]'); emailElements.forEach(element => { const encodedEmail = element.getAttribute('data-encoded-email'); const decodedEmail = decodeBase64(encodedEmail); if (decodedEmail) { // テキストを設定 element.textContent = decodedEmail; // hrefを設定 element.href = 'mailto:' + decodedEmail; } }); } // 現在の日付から季節を判定 function getCurrentSeason() { const now = new Date(); const month = now.getMonth() + 1; // 0-11 → 1-12 const day = now.getDate(); // 3月1日~5月10日: spring if ((month === 3 && day >= 1) || (month === 4) || (month === 5 && day <= 10)) { return 'spring'; } // 5月11日~9月30日: summer if ((month === 5 && day >= 11) || (month >= 6 && month <= 8) || (month === 9 && day <= 30)) { return 'summer'; } // 10月1日~11月30日: autumn if ((month === 10 && day >= 1) || (month === 11 && day <= 30)) { return 'autumn'; } // 12月1日~2月28日(29日): winter return 'winter'; } /** WordPress が wp_head 前に注入する正規季節(無ければ null) */ function getServerCalendarSeason() { var s = typeof window !== 'undefined' ? window.kodomoMentalSeason : null; if (s === 'spring' || s === 'summer' || s === 'autumn' || s === 'winter') { return s; } return null; } function isValidSeasonToken(s) { return s === 'spring' || s === 'summer' || s === 'autumn' || s === 'winter'; } // 動画ソースを季節に応じて設定 function setupSeasonalVideo(season = null) { const targetSeason = season || getCurrentSeason(); const video = document.querySelector('.p-hero__video'); if (!video) return; function isDesktopSafari() { try { const ua = navigator.userAgent || ''; const isSafari = /Safari\//.test(ua) && !/Chrome\//.test(ua) && !/Chromium\//.test(ua) && !/Edg\//.test(ua); const isMac = /Macintosh/.test(ua); return isSafari && isMac; } catch (_) { return false; } } // 再設定時は前回の非同期処理を無効化(Safari で play の多重・リスナー蓄積による不安定化を防ぐ) const session = String(Date.now()); video.dataset.heroVideoSession = session; function alive() { return video.dataset.heroVideoSession === session; } // Safari(特にWebKit): ソース差し替え後は属性だけでは不十分で、プロパティ明示が必要なことがある video.muted = true; video.defaultMuted = true; if ('playsInline' in video) { video.playsInline = true; } video.setAttribute('playsinline', ''); video.setAttribute('webkit-playsinline', ''); video.setAttribute('muted', ''); // ポスター画像を設定 video.poster = `/assets/videos/${targetSeason}/poster.jpg`; // 既存のsourceを削除 video.innerHTML = ''; // 季節に応じた動画を追加(PC/SP共通) const source = document.createElement('source'); source.src = `/assets/videos/${targetSeason}/hero.mp4`; source.type = 'video/mp4'; video.appendChild(source); // ビデオエラーハンドリング video.onerror = function(e) { if (!alive()) return; console.error('Video error:', e); console.error('Video src:', source.src); console.error('Error code:', video.error ? video.error.code : 'unknown'); }; let readyDispatched = false; let playInFlight = false; let gestureRetryBound = false; let fallbackTimer1 = null; let fallbackTimer2 = null; function clearFallbacks() { if (fallbackTimer1) { clearTimeout(fallbackTimer1); fallbackTimer1 = null; } if (fallbackTimer2) { clearTimeout(fallbackTimer2); fallbackTimer2 = null; } } function notifyReady() { if (!alive() || readyDispatched) return; readyDispatched = true; clearFallbacks(); document.body.dispatchEvent(new Event('video-ready')); } function bindGestureRetry() { if (gestureRetryBound || !alive()) return; gestureRetryBound = true; const retry = function() { if (!alive()) return; attemptPlay(); }; // macOS Safari は pointerdown だけ拾えないケースがあるので広めに拾う window.addEventListener('pointerdown', retry, true); window.addEventListener('mousedown', retry, true); window.addEventListener('touchstart', retry, true); window.addEventListener('keydown', retry, true); const unbind = function() { window.removeEventListener('pointerdown', retry, true); window.removeEventListener('mousedown', retry, true); window.removeEventListener('touchstart', retry, true); window.removeEventListener('keydown', retry, true); }; // 再生できたら解除 video.addEventListener('playing', function onPlay() { if (!alive()) return; video.removeEventListener('playing', onPlay); unbind(); gestureRetryBound = false; }); } function attemptPlay() { if (!alive() || playInFlight) return; if (!video.paused) { notifyReady(); return; } playInFlight = true; video.muted = true; video.defaultMuted = true; const p = video.play(); if (p !== undefined && typeof p.then === 'function') { p.then(function() { playInFlight = false; if (!alive()) return; notifyReady(); }).catch(function(error) { playInFlight = false; if (!alive()) return; // Safari (macOS) では設定が「許可」でも初回は play() が拒否されることがある。 // 原因が見えるようにログを残し、最初のユーザー操作で一度だけ再試行する。 try { console.warn('[hero video] play() rejected:', error && error.name ? error.name : error); } catch (_) {} // NotAllowedError は「ユーザー操作起点の play」が必要なことが多いので、 // 以降はユーザー操作で何度でも attemptPlay できるようにしておく。 if (error && error.name === 'NotAllowedError') { bindGestureRetry(); } }); } else { playInFlight = false; notifyReady(); } } function onCanPlay() { if (!alive()) return; video.removeEventListener('canplay', onCanPlay); attemptPlay(); } // PC Safari は自動再生が拒否されやすいので、クリック起点でのみ開始する(例外扱い) if (isDesktopSafari()) { bindGestureRetry(); video.addEventListener('playing', function onPlaying() { if (!alive()) return; video.removeEventListener('playing', onPlaying); notifyReady(); }); video.load(); return; } video.addEventListener('canplay', onCanPlay); // Safari(macOS) は再生できても、フォーカス/省電力/タブ状態で勝手に pause することがある。 // pause 後にクリックしても再生できるよう、必要ならリトライを復活させる。 video.addEventListener('pause', function() { if (!alive()) return; if (video.ended) return; if (video.paused) { bindGestureRetry(); } }); document.addEventListener('visibilitychange', function() { if (!alive()) return; if (document.visibilityState === 'visible' && video.paused && !video.ended) { bindGestureRetry(); } }); // 単一路線のフォールバックのみ(canplay と loadeddata の二重 play やリスナー蓄積を避ける) fallbackTimer1 = setTimeout(function() { fallbackTimer1 = null; if (!alive()) return; if (!video.paused) { notifyReady(); return; } attemptPlay(); }, 800); fallbackTimer2 = setTimeout(function() { fallbackTimer2 = null; if (!alive()) return; if (!video.paused) { notifyReady(); return; } attemptPlay(); }, 2500); video.addEventListener('playing', function onPlaying() { if (!alive()) return; video.removeEventListener('playing', onPlaying); notifyReady(); }); video.load(); } // 季節切り替えボタンのセットアップ(確認用・ログイン時のみ localStorage と連動) function setupSeasonSwitcher(currentSeason) { const buttons = document.querySelectorAll('.l-header__season-btn'); if (buttons.length === 0) return; buttons.forEach(button => { if (button.getAttribute('data-season') === currentSeason) { button.classList.add('is-active'); } button.addEventListener('click', function() { const season = this.getAttribute('data-season'); // 季節を保存 safeSetItem('selectedSeason', season); setupSeasonalVideo(season); updateSeasonalImages(season); // すべてのボタンのアクティブ状態を更新(スマホ版とPC版両方) buttons.forEach(btn => { if (btn.getAttribute('data-season') === season) { btn.classList.add('is-active'); } else { btn.classList.remove('is-active'); } }); }); }); } // 季節別画像を更新 function updateSeasonalImages(season) { // img タグの季節別画像を更新 const seasonalImages = document.querySelectorAll('[data-seasonal]'); seasonalImages.forEach(img => { const category = img.dataset.category; const filename = img.dataset.filename; img.src = `/assets/images/${season}/${category}/${filename}`; }); // CSS背景画像を更新 updateSeasonalBackgroundImages(season); } // CSS背景画像を更新 function updateSeasonalBackgroundImages(season) { // パララックス画像(トップページ) const parallaxDividerTop = document.querySelector('.p-parallax-divider:not(.p-parallax-divider--sm)'); if (parallaxDividerTop) { parallaxDividerTop.style.backgroundImage = `url('/assets/images/${season}/top/img01.jpg')`; } // パララックス画像(はじめての方へ) const parallaxDividerFirstTime = document.querySelector('.p-parallax-divider--first-time'); if (parallaxDividerFirstTime) { parallaxDividerFirstTime.style.backgroundImage = `url('/assets/images/${season}/firstvisit/img01.jpg')`; } // ページヒーロー背景画像 const pageHero = document.querySelector('[data-seasonal-bg]'); if (pageHero) { const filename = pageHero.dataset.filename; const category = pageHero.dataset.category; pageHero.style.backgroundImage = `url('/assets/images/${season}/${category}/${filename}')`; } // 困りごとセクション背景画像 const concernsBgLeft = document.querySelector('.p-top-concerns__bg-left'); if (concernsBgLeft) { concernsBgLeft.style.backgroundImage = `url('/assets/images/${season}/top/sec01_img01.png')`; } const concernsBgRight = document.querySelector('.p-top-concerns__bg-right'); if (concernsBgRight) { concernsBgRight.style.backgroundImage = `url('/assets/images/${season}/top/sec01_img02.png')`; } } // ハンバーガーメニュー制御 function setupMobileMenu() { const toggle = document.querySelector('.l-header__toggle'); const nav = document.querySelector('.l-header__nav'); if (!toggle || !nav) return; function isMobileViewport() { return window.innerWidth < 1024; } function closeMobileMenu() { if (!nav.classList.contains('is-open')) { return; } nav.classList.add('is-closing'); nav.classList.remove('is-open'); if (isMobileViewport()) { document.body.style.overflow = ''; } setTimeout(() => { nav.classList.remove('is-closing'); nav.style.display = 'none'; }, 500); toggle.setAttribute('aria-expanded', 'false'); } toggle.addEventListener('click', function(e) { e.stopPropagation(); const isExpanded = this.getAttribute('aria-expanded') === 'true'; if (isExpanded) { closeMobileMenu(); } else { nav.style.display = 'flex'; nav.classList.add('is-open'); this.setAttribute('aria-expanded', 'true'); if (isMobileViewport()) { document.body.style.overflow = 'hidden'; } } }); // ナビ外(オーバーレイ左側・ヘッダーロゴ等)クリックで閉じる document.addEventListener('click', function(e) { if (!nav.classList.contains('is-open')) { return; } const t = e.target; if (!(t instanceof Node)) { return; } if (nav.contains(t) || toggle.contains(t)) { return; } closeMobileMenu(); }); } // ヘッダーのスクロール時スタイル切り替え(ホームのみ) function setupHeaderScroll() { if (!document.body.classList.contains('is-home')) return; const header = document.querySelector('.l-header'); if (!header) return; const threshold = 10; // 少し動いたら切替 function onScroll() { if (window.scrollY > threshold) { header.classList.add('is-scrolled'); } else { header.classList.remove('is-scrolled'); } } window.addEventListener('scroll', onScroll, { passive: true }); onScroll(); // 初期判定 } // パララックス効果(困りごとセクション) function setupParallax() { // モーション軽減が有効な場合はスキップ const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (prefersReducedMotion) return; const bgLeft = document.querySelector('.p-top-concerns__bg-left'); const bgRight = document.querySelector('.p-top-concerns__bg-right'); if (!bgLeft || !bgRight) return; function onScroll() { const scrollY = window.scrollY; const offset = scrollY * 0.3; // スクロール速度を0.3倍に bgLeft.style.transform = `translateY(calc(-50% + ${offset}px))`; bgRight.style.transform = `translateY(calc(-50% - ${offset}px))`; // 逆方向で時差効果 } window.addEventListener('scroll', onScroll, { passive: true }); } // パララックス画像仕切り function setupParallaxDivider() { // モーション軽減が有効な場合はスキップ const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (prefersReducedMotion) return; const dividers = document.querySelectorAll('.p-parallax-divider'); if (dividers.length === 0) return; function onScroll() { dividers.forEach(divider => { const image = divider.querySelector('.p-parallax-divider__image'); if (!image) return; const speed = parseFloat(divider.dataset.parallaxSpeed) || 0.3; const rect = divider.getBoundingClientRect(); const windowHeight = window.innerHeight; // 要素がビューポート内に表示されているかをチェック if (rect.top < windowHeight && rect.bottom > 0) { // 要素がビューポート内に表示されている時のみ計算 // 要素が画面上部に近づくほど負の値となり、画像が下に移動 const offset = (rect.top - windowHeight) * speed * 2; image.style.transform = `translateY(${offset}px)`; } }); } window.addEventListener('scroll', onScroll, { passive: true }); onScroll(); // 初期状態を設定 } // ページ読み込み直後のヒーロー要素フェードイン function setupHeroFadeIn() { const heroElements = document.querySelectorAll('.p-hero [data-fade-up]'); if (heroElements.length === 0) return; // モーション軽減が有効な場合はすべての要素を即座に表示 const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (prefersReducedMotion) { heroElements.forEach(element => { element.classList.add('is-visible'); }); return; } // ページ読み込み直後のため、各要素に時差でクラスを追加 heroElements.forEach((element, index) => { const delay = index * 250; // 250ms ずつ時差 setTimeout(() => { element.classList.add('is-visible'); }, delay); }); } // Home: タブレットは CTA 幅を h1 以下に/PC は 2 ボタン幅を揃える function setupHomeHeroCtaButtonWidths() { if (!document.body.classList.contains('is-home')) return; const ctaEl = document.querySelector('.p-hero__cta'); if (!ctaEl) return; const titleEl = document.querySelector('.p-hero__title'); const brownBtn = ctaEl.querySelector('.p-hero__button--brown'); const tealBtn = ctaEl.querySelector('.p-hero__button--teal'); if (!brownBtn || !tealBtn) return; const run = () => { ctaEl.style.maxWidth = ''; const vw = window.innerWidth; // TB(768〜1023px): CTA は .p-hero__title の実幅を超えない if (vw >= 768 && vw < 1024 && titleEl) { const titleW = titleEl.getBoundingClientRect().width; ctaEl.style.maxWidth = `${Math.round(titleW * 0.93)}px`; } brownBtn.style.minWidth = ''; brownBtn.style.width = ''; tealBtn.style.minWidth = ''; tealBtn.style.width = ''; if (vw < 1024) return; const brownW = brownBtn.getBoundingClientRect().width; const tealW = tealBtn.getBoundingClientRect().width; const maxW = Math.max(brownW, tealW); brownBtn.style.minWidth = `${maxW}px`; tealBtn.style.minWidth = `${maxW}px`; brownBtn.style.width = `${maxW}px`; tealBtn.style.width = `${maxW}px`; }; let layoutTimer = null; const scheduleRun = () => { window.clearTimeout(layoutTimer); layoutTimer = window.setTimeout(run, 150); }; if (document.fonts && document.fonts.ready) { document.fonts.ready.then(run).catch(run); } else { run(); } window.addEventListener('resize', scheduleRun); if (titleEl && typeof ResizeObserver !== 'undefined') { const ro = new ResizeObserver(() => scheduleRun()); ro.observe(titleEl); } } // Home: 「対象となる方」背景アニメをテキスト領域と被らせない(左右のみ表示) function setupHomeTopTargetUniverseMask() { if (!document.body.classList.contains('is-home')) return; const universeEl = document.querySelector('.p-top-target__universe[data-universe-bg]'); const innerEl = document.querySelector('.p-top-target .p-section__inner'); if (!universeEl || !innerEl) return; let resizeTimer = null; const run = () => { const u = universeEl.getBoundingClientRect(); const inner = innerEl.getBoundingClientRect(); // PCでは「中心から左右550pxの外側」だけ背景が見えるよう切り抜く // (透明にしたい中心領域:center-550px〜center+550px) const isPc = window.innerWidth >= 1024; let left; let right; if (isPc) { // “背景コンテナ自身の中心”を基準に、厳密に±550pxで抜く const centerInContainer = u.width / 2; left = centerInContainer - 550; right = centerInContainer + 550; } else { // それ以外(SP/TB)は、テキスト領域に被りにくいよう内側の範囲を基準にする left = inner.left - u.left; right = inner.right - u.left; } // 範囲の暴れを防ぐ left = Math.max(0, Math.min(u.width, left)); right = Math.max(0, Math.min(u.width, right)); // 異常値は無視 if (right <= left + 20) return; universeEl.style.setProperty('--target-universe-left', `${left}px`); universeEl.style.setProperty('--target-universe-right', `${right}px`); }; run(); window.addEventListener('resize', () => { window.clearTimeout(resizeTimer); resizeTimer = window.setTimeout(run, 150); }); } // フェードアップアニメーション (Intersection Observer) function setupFadeUpAnimation() { const targets = document.querySelectorAll('[data-fade-up]:not(.p-hero [data-fade-up])'); if (targets.length === 0) return; // モーション軽減が有効な場合はすべての要素を即座に表示 const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (prefersReducedMotion) { targets.forEach(target => { target.classList.add('is-visible'); }); return; } // Intersection Observer のオプション const options = { root: null, // ビューポートを基準 rootMargin: '0px 0px -10% 0px', // ビューポート下部から10%の位置で発火 threshold: 0 // 少しでも見えたら発火 }; // Observer コールバック const callback = (entries, observer) => { entries.forEach((entry, index) => { if (entry.isIntersecting) { // data-delay 属性から遅延時間を取得(ミリ秒) let delay = parseInt(entry.target.dataset.delay); // data-delay が未設定の場合、親要素内での相対位置に基づく遅延を計算 if (isNaN(delay)) { const parent = entry.target.closest('[data-fade-up]') || entry.target.parentElement; if (parent) { // 親要素内で同じセレクタを持つ兄弟要素を取得 const siblings = Array.from(parent.querySelectorAll('[data-fade-up]')); const siblingIndex = siblings.indexOf(entry.target); delay = siblingIndex > 0 ? siblingIndex * 100 : 0; // 100ms ずつ時差 } else { delay = 0; } } setTimeout(() => { entry.target.classList.add('is-visible'); }, delay); // 一度アニメーションしたら監視を解除(再アニメーション不要) observer.unobserve(entry.target); } }); }; // Observer を作成して各要素を監視 const observer = new IntersectionObserver(callback, options); targets.forEach(target => { observer.observe(target); }); } // 宇宙 SVG 背景([data-universe-bg]:対象となる方・当院の診療について 等) function setupUniverseBackgroundAnimations() { if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; var SVG_URLS = [ '/assets/images/common/universe_1.svg', '/assets/images/common/universe_2.svg', '/assets/images/common/universe_3.svg' ]; var PALETTE_KEYS = ['--universe-red', '--universe-yellow', '--universe-blue', '--universe-green']; var ANIM_MS = 9000; var STAGGER_MS = 700; var MAX_SETS = 4; var svgCache = null; function loadSvgs() { if (svgCache) return Promise.resolve(svgCache); return Promise.all(SVG_URLS.map(function(url) { return fetch(url).then(function(r) { if (!r.ok) throw new Error('universe svg ' + url); return r.text(); }); })).then(function(texts) { svgCache = texts; return texts; }); } function initUniverseBg(container) { var section = container.closest('section'); if (!section) return; // セクションごとの密度調整 // - 「対象となる方」は左右マスクで表示領域が狭くなるため、出現数/頻度を増やす var isTopTarget = section.classList.contains('p-top-target') || section.classList.contains('section--target'); // 直近調整値から密度を 2/3 に減らす var maxSetsForSection = isTopTarget ? 3 : MAX_SETS; var gapMinMs = isTopTarget ? 5500 : 4000; var gapMaxMs = isTopTarget ? 10125 : 10000; var initialBurst = isTopTarget ? 2 : 2; var started = false; var spawnTimeoutId = null; var activeCount = 0; function shuffle(arr) { var a = arr.slice(); for (var i = a.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var t = a[i]; a[i] = a[j]; a[j] = t; } return a; } function randomPaletteColor() { var mono = container.getAttribute('data-universe-monotone'); if (mono === 'white') { var a = (0.3 + Math.random() * 0.5).toFixed(2); return 'rgba(255, 255, 255, ' + a + ')'; } var cs = window.getComputedStyle(section); var key = PALETTE_KEYS[Math.floor(Math.random() * PALETTE_KEYS.length)]; var v = cs.getPropertyValue(key).trim(); return v || '#333333'; } function randomUniverseSlotSize() { var r = Math.random(); if (r > 0.78) { return { height: 'clamp(3.5rem, 12vw, 8rem)', maxW: 'min(44vw, 250px)', overlap: 'clamp(-2.5rem, -8.5vw, -1rem)' }; } if (r > 0.42) { return { height: 'clamp(2.85rem, 9vw, 6rem)', maxW: 'min(33vw, 185px)', overlap: 'clamp(-2.05rem, -7vw, -0.78rem)' }; } return { height: 'clamp(2.25rem, 7vw, 4.5rem)', maxW: 'min(26vw, 140px)', overlap: 'clamp(-1.75rem, -6vw, -0.65rem)' }; } function spawnSet(svgs) { if (activeCount >= maxSetsForSection) return; var setEl = document.createElement('div'); setEl.className = 'p-universe-bg__set'; // いったん top を先に確定(計測用) setEl.style.top = 18 + Math.random() * 64 + '%'; var appearOrder = shuffle([0, 1, 2]); for (var slotIndex = 0; slotIndex < 3; slotIndex++) { var slot = document.createElement('div'); slot.className = 'p-universe-bg__slot'; var rank = appearOrder.indexOf(slotIndex); var size = randomUniverseSlotSize(); slot.style.setProperty('--universe-delay', rank * STAGGER_MS + 'ms'); slot.style.setProperty('--universe-svg-height', size.height); slot.style.setProperty('--universe-svg-max-w', size.maxW); slot.style.setProperty('--universe-slot-overlap', size.overlap); slot.style.color = randomPaletteColor(); slot.innerHTML = svgs[slotIndex]; var svg = slot.querySelector('svg'); if (svg) { svg.setAttribute('aria-hidden', 'true'); svg.removeAttribute('width'); svg.removeAttribute('height'); } setEl.appendChild(slot); } // 「対象となる方」は“左右の帯”にだけ出現させ、かつアイテム自体が中央抜き領域にかからないようにする if (isTopTarget) { var rect = container.getBoundingClientRect(); var w = rect.width || 1; var leftCutPx = w / 2 - 550; var rightCutPx = w / 2 + 550; // 端から少しだけ余白(translate(-50%) + 影のはみ出し対策) var pad = Math.max(10, Math.min(18, Math.round(w * 0.012))); // まずDOMに入れて幅を計測(見えない状態で) setEl.style.left = '0px'; setEl.style.visibility = 'hidden'; container.appendChild(setEl); var setW = setEl.getBoundingClientRect().width || 0; var half = setW / 2; var safety = 6; // 左帯/右帯それぞれで「中心X」が取りうる範囲を作る // 左は center <= leftCut - half、右は center >= rightCut + half var leftMin = pad + half; var leftMax = Math.max(leftMin, leftCutPx - half - safety); var rightMin = Math.min(w - pad - half, rightCutPx + half + safety); var rightMax = w - pad - half; var canLeft = leftMax > leftMin + 1; var canRight = rightMax > rightMin + 1; // 左右どちらから出すか(可能な方から) var useLeft = Math.random() < 0.5; if (useLeft && !canLeft) useLeft = false; if (!useLeft && !canRight) useLeft = true; var x; if (useLeft && canLeft) { x = leftMin + Math.random() * (leftMax - leftMin); } else if (canRight) { x = rightMin + Math.random() * (rightMax - rightMin); } else { // 両方とも厳密には取れない場合は、切れが最小になる位置へ寄せる x = Math.max(leftMin, Math.min(rightMax, leftCutPx - half - safety)); } // 表示状態に戻す setEl.style.left = x + 'px'; setEl.style.visibility = ''; } else { setEl.style.left = 12 + Math.random() * 76 + '%'; container.appendChild(setEl); } activeCount++; var removeAfter = ANIM_MS + STAGGER_MS * 2 + 800; window.setTimeout(function() { if (setEl.parentNode) setEl.parentNode.removeChild(setEl); activeCount--; }, removeAfter); } function scheduleNext() { var gap = gapMinMs + Math.random() * Math.max(0, gapMaxMs - gapMinMs); spawnTimeoutId = window.setTimeout(function() { loadSvgs() .then(function(svgs) { if (activeCount < maxSetsForSection) spawnSet(svgs); }) .catch(function() { /* 失敗時は黙って次へ */ }) .then(function() { scheduleNext(); }); }, gap); } function start() { if (started) return; started = true; loadSvgs() .then(function(svgs) { for (var i = 0; i < initialBurst; i++) { if (activeCount < maxSetsForSection) spawnSet(svgs); } scheduleNext(); }) .catch(function() { /* SVG 取得不可 */ }); } var io = new IntersectionObserver( function(entries) { entries.forEach(function(entry) { if (entry.isIntersecting) start(); }); }, { root: null, rootMargin: '0px', threshold: 0.08 } ); io.observe(section); } var nodes = document.querySelectorAll('[data-universe-bg]'); for (var n = 0; n < nodes.length; n++) { initUniverseBg(nodes[n]); } } // 右から左へスライドインするアニメーション (Intersection Observer) function setupSlideInFromRightAnimation() { const targets = document.querySelectorAll('[data-slide-in-right]'); if (targets.length === 0) return; // モーション軽減が有効な場合はすべての要素を即座に表示 const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (prefersReducedMotion) { targets.forEach(target => { target.classList.add('is-visible'); }); return; } // Intersection Observer のオプション const options = { root: null, // ビューポートを基準 rootMargin: '0px 0px -40% 0px', // 要素がビューポート内に60%程度表示されたときに発火 threshold: 0 // 少しでも見えたら発火 }; // Observer コールバック const callback = (entries, observer) => { entries.forEach((entry) => { if (entry.isIntersecting) { // data-delay 属性から遅延時間を取得(ミリ秒) let delay = parseInt(entry.target.dataset.delay) || 0; setTimeout(() => { entry.target.classList.add('is-visible'); }, delay); // 一度アニメーションしたら監視を解除(再アニメーション不要) observer.unobserve(entry.target); } }); }; // Observer を作成して各要素を監視 const observer = new IntersectionObserver(callback, options); targets.forEach(target => { observer.observe(target); }); } // Aboutus: ご挨拶画像スライダー(フェード) function setupGreetingImageSlider() { const sliders = document.querySelectorAll('[data-greeting-slider]'); if (sliders.length === 0) return; const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; sliders.forEach((slider) => { const slides = Array.from(slider.querySelectorAll('.p-greeting__slide')); if (slides.length <= 1) return; let currentIndex = Math.max(0, slides.findIndex((el) => el.classList.contains('is-active'))); if (currentIndex === -1) currentIndex = 0; slides.forEach((el, idx) => { el.classList.toggle('is-active', idx === currentIndex); }); if (prefersReducedMotion) return; const intervalMs = Math.max(1000, parseInt(slider.getAttribute('data-interval-ms'), 10) || 6000); let timerId = null; let paused = false; function show(index) { const nextIndex = ((index % slides.length) + slides.length) % slides.length; slides.forEach((el, idx) => { el.classList.toggle('is-active', idx === nextIndex); }); currentIndex = nextIndex; } function start() { if (timerId != null) return; timerId = window.setInterval(() => { if (paused) return; if (document.visibilityState === 'hidden') return; show(currentIndex + 1); }, intervalMs); } function stop() { if (timerId == null) return; window.clearInterval(timerId); timerId = null; } function setPaused(value) { paused = value; } slider.addEventListener('pointerenter', () => setPaused(true)); slider.addEventListener('pointerleave', () => setPaused(false)); slider.addEventListener('focusin', () => setPaused(true)); slider.addEventListener('focusout', () => setPaused(false)); document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') stop(); else start(); }); start(); }); } // PAGE TOP ボタン function setupPageTopButton() { const pageTopBtn = document.getElementById('pageTopBtn'); if (!pageTopBtn) return; // スクロールイベント window.addEventListener('scroll', function() { if (window.scrollY > 300) { pageTopBtn.classList.add('is-visible'); } else { pageTopBtn.classList.remove('is-visible'); } }); // クリックイベント pageTopBtn.addEventListener('click', function() { window.scrollTo({ top: 0, behavior: 'smooth' }); }); } // ============================================ // 書籍ページネーション // ============================================ function setupBooksPagination() { const booksGrid = document.getElementById('books-grid'); if (!booksGrid) return; const bookItems = Array.from(booksGrid.querySelectorAll('[data-page]')); if (bookItems.length === 0) return; const itemsPerPage = 16; // 4列 × 4行 const totalPages = Math.ceil(bookItems.length / itemsPerPage); let currentPage = 1; const pagination = document.getElementById('books-pagination'); const pagesList = document.getElementById('pages-list'); const prevBtn = document.getElementById('prev-btn'); const nextBtn = document.getElementById('next-btn'); // ページボタンを生成 function generatePaginationButtons() { pagesList.innerHTML = ''; for (let i = 1; i <= totalPages; i++) { const li = document.createElement('li'); const button = document.createElement('button'); button.className = 'p-books-pagination__page'; button.textContent = i; button.dataset.page = i; if (i === currentPage) { button.classList.add('p-books-pagination__page--active'); } button.addEventListener('click', () => goToPage(i)); li.appendChild(button); pagesList.appendChild(li); } } // ページを表示 function showPage(pageNum) { bookItems.forEach(item => { const page = parseInt(item.dataset.page); item.style.display = page === pageNum ? '' : 'none'; }); } // ページ遷移 function goToPage(pageNum) { currentPage = Math.max(1, Math.min(pageNum, totalPages)); showPage(currentPage); generatePaginationButtons(); updatePrevNextButtons(); } // 前後ボタンのアップデート function updatePrevNextButtons() { prevBtn.disabled = currentPage === 1; nextBtn.disabled = currentPage === totalPages; } // イベントリスナー prevBtn.addEventListener('click', () => goToPage(currentPage - 1)); nextBtn.addEventListener('click', () => goToPage(currentPage + 1)); // 初期化 generatePaginationButtons(); updatePrevNextButtons(); showPage(currentPage); } // aboutus: ページ内ナビアンカー(4ボタン)の幅を最長に揃える function setupAboutUsNavAnchorButtonWidths() { if (!document.body.classList.contains('is-aboutus')) return; // PCはCSSで固定幅(200px)にするため、JSでの幅揃えは行わない if (window.innerWidth >= 1024) { const buttons = document.querySelectorAll('.p-section--nav-menu nav a.c-button'); buttons.forEach((btn) => { btn.style.minWidth = ''; btn.style.width = ''; }); return; } const navButtons = Array.from( document.querySelectorAll('.p-section--nav-menu nav a.c-button') ); if (navButtons.length === 0) return; function applyWidths() { // リサイズ等でフォントサイズや折返しが変わるため、一度リセットして再計測 navButtons.forEach((btn) => { btn.style.minWidth = ''; btn.style.width = ''; }); const widths = navButtons.map((btn) => btn.getBoundingClientRect().width); const maxWidth = Math.max(...widths); navButtons.forEach((btn) => { btn.style.minWidth = `${maxWidth}px`; btn.style.width = `${maxWidth}px`; }); } applyWidths(); let resizeTimer = null; window.addEventListener('resize', () => { window.clearTimeout(resizeTimer); resizeTimer = window.setTimeout(applyWidths, 150); }); } // アンカーリンクのヘッダー高さ対応 function setupAnchorScroll() { const header = document.querySelector('.l-header'); const breadcrumb = document.querySelector('.c-breadcrumb'); function getHeaderOffset() { let offset = 0; let headerHeight = 0; let breadcrumbHeight = 0; if (header) { headerHeight = header.offsetHeight; offset += headerHeight; } if (breadcrumb) { breadcrumbHeight = breadcrumb.offsetHeight; offset += breadcrumbHeight; } // さらに余白を追加 offset += 0; console.log(`[Anchor Scroll] Header: ${headerHeight}px, Breadcrumb: ${breadcrumbHeight}px, Total Offset: ${offset}px`); return offset; } function scrollToTarget(hashOrElement) { const target = typeof hashOrElement === 'string' ? document.querySelector(hashOrElement) : hashOrElement; if (!target) return; const headerOffset = getHeaderOffset(); const offsetTop = target.getBoundingClientRect().top + window.scrollY - headerOffset; const hash = typeof hashOrElement === 'string' ? hashOrElement : '#' + target.id; console.log(`[Anchor Scroll] Navigating to ${hash}, scroll position: ${offsetTop}px`); window.scrollTo({ top: offsetTop, behavior: 'smooth' }); // スクロール完了後に実際の位置を確認 setTimeout(() => { const actualTop = target.getBoundingClientRect().top + window.scrollY; const headerOffset = getHeaderOffset(); const diffFromHeader = target.getBoundingClientRect().top; console.log(`[Anchor Scroll] After scroll - Target actual position from top of page: ${actualTop}px, Distance from header: ${diffFromHeader}px, Header offset: ${headerOffset}px`); }, 1000); } // ページ読み込み時にハッシュがある場合 if (window.location.hash) { setTimeout(() => { scrollToTarget(window.location.hash); }, 100); } // アンカーリンククリック時(アイコン等の子要素クリックでも a を拾う) document.addEventListener('click', function(e) { const anchor = e.target.closest && e.target.closest('a[href*="#"]'); if (!anchor || !anchor.hash) { return; } const href = anchor.getAttribute('href'); const currentPath = window.location.pathname; const linkPath = new URL(href, window.location.origin).pathname; // 同じページ内のリンクのみスクロール処理を実行 // 別ページへのリンクはページ遷移後にハッシュ処理で統一的に処理 if (currentPath === linkPath) { const target = document.querySelector(anchor.hash); if (target) { e.preventDefault(); scrollToTarget(anchor.hash); } } }); } /** * お問い合わせ・予約フォーム: 送信前に .textfieldRequiredMsg 等を表示(Spry 非依存) */ function setupMailFormsValidation() { function clearFieldErrors(form) { if (!form) return; form.querySelectorAll('.textfieldRequiredMsg, .textareaRequiredMsg, .selectRequiredMsg, .textfieldInvalidFormatMsg, .textareaMinCharsMsg').forEach(function(el) { el.style.display = 'none'; }); } function showFirstMatchingMsg(container, selectors) { if (!container) return false; for (let i = 0; i < selectors.length; i++) { const el = container.querySelector(selectors[i]); if (el) { el.style.display = 'block'; return true; } } return false; } function rowForNamedInput(form, name) { const input = form.querySelector('[name="' + name + '"]'); if (!input) return null; return input.closest('tr'); } const contactForm = document.getElementById('km-contact-form'); if (contactForm) { contactForm.addEventListener('submit', function(e) { clearFieldErrors(contactForm); let ok = true; const nameEl = contactForm.querySelector('[name="お名前"]'); const emailEl = contactForm.querySelector('[name="メールアドレス"]'); const msgEl = contactForm.querySelector('[name="お問い合わせ内容"]'); const nm = nameEl && String(nameEl.value).trim(); const em = emailEl && String(emailEl.value).trim(); const ms = msgEl && String(msgEl.value).trim(); if (!nm) { showFirstMatchingMsg(contactForm.querySelector('#checkText1'), ['.textfieldRequiredMsg']); ok = false; } if (!em) { showFirstMatchingMsg(contactForm.querySelector('#checkText2'), ['.textfieldRequiredMsg']); ok = false; } else if (emailEl && emailEl.validity && !emailEl.validity.valid) { showFirstMatchingMsg(contactForm.querySelector('#checkText2'), ['.textfieldInvalidFormatMsg']); ok = false; } if (!ms) { showFirstMatchingMsg(contactForm.querySelector('#checkText3'), ['.textareaRequiredMsg', '.textfieldRequiredMsg']); ok = false; } else if (ms.length < 10) { showFirstMatchingMsg(contactForm.querySelector('#checkText3'), ['.textareaMinCharsMsg']); ok = false; } if (!ok) { e.preventDefault(); } }); } const resForm = document.getElementById('km-reservation-form'); if (resForm) { resForm.addEventListener('submit', function(e) { clearFieldErrors(resForm); let ok = true; function req(name) { const v = resForm.querySelector('[name="' + name + '"]'); const val = v ? String(v.value).trim() : ''; if (val) return true; const row = rowForNamedInput(resForm, name); if (row) { showFirstMatchingMsg(row, ['.textfieldRequiredMsg', '.textareaRequiredMsg', '.selectRequiredMsg']); } return false; } if (!req('お名前')) ok = false; if (!req('フリガナ')) ok = false; if (!req('nen')) ok = false; if (!req('gatu')) ok = false; if (!req('niti')) ok = false; if (!req('お名前2')) ok = false; if (!req('フリガナ2')) ok = false; if (!req('nen2')) ok = false; if (!req('gatu2')) ok = false; if (!req('niti2')) ok = false; if (!req('kankei')) ok = false; if (!req('zip')) ok = false; const telEl = resForm.querySelector('[name="tel"]'); const tel2El = resForm.querySelector('[name="tel2"]'); const telVal = telEl ? String(telEl.value).trim() : ''; const tel2Val = tel2El ? String(tel2El.value).trim() : ''; if (!telVal && !tel2Val) { const row = rowForNamedInput(resForm, 'tel'); if (row) { showFirstMatchingMsg(row, ['.textfieldRequiredMsg', '.textareaRequiredMsg', '.selectRequiredMsg']); } ok = false; } const emailEl = resForm.querySelector('[name="メールアドレス"]'); const em = emailEl && String(emailEl.value).trim(); if (!em) { const row = rowForNamedInput(resForm, 'メールアドレス'); if (row) showFirstMatchingMsg(row, ['.textfieldRequiredMsg']); ok = false; } else if (emailEl && emailEl.validity && !emailEl.validity.valid) { const row = rowForNamedInput(resForm, 'メールアドレス'); if (row) showFirstMatchingMsg(row, ['.textfieldInvalidFormatMsg']); ok = false; } const soudan = resForm.querySelector('[name="相談内容"]'); const soudanVal = soudan && String(soudan.value).trim(); if (!soudanVal) { const row = rowForNamedInput(resForm, '相談内容'); if (row) showFirstMatchingMsg(row, ['.textareaRequiredMsg']); ok = false; } else if (soudanVal.length < 10) { const row = rowForNamedInput(resForm, '相談内容'); if (row) showFirstMatchingMsg(row, ['.textareaMinCharsMsg']); ok = false; } if (!req('jikan')) ok = false; if (!ok) e.preventDefault(); }); } } // お知らせアイキャッチ: ライトボックス() function setupNewsLightbox() { const triggers = document.querySelectorAll('.js-news-lightbox'); if (!triggers.length) { return; } let dialog = document.getElementById('newsLightbox'); if (!dialog || !(dialog instanceof HTMLDialogElement)) { dialog = document.createElement('dialog'); dialog.id = 'newsLightbox'; dialog.className = 'c-news-lightbox'; dialog.setAttribute('aria-label', '拡大画像'); dialog.innerHTML = '' + '
' + '' + '
' + '' + '' + '
' + '
'; document.body.appendChild(dialog); } const img = dialog.querySelector('.c-news-lightbox__img'); const closeBtn = dialog.querySelector('.c-news-lightbox__close'); const scrim = dialog.querySelector('.c-news-lightbox__scrim'); if (!img || !closeBtn || !scrim) { return; } let lastTrigger = null; function openLightbox(trigger) { const src = trigger.getAttribute('data-lightbox-src'); if (!src) { return; } lastTrigger = document.activeElement; img.src = src; img.alt = trigger.getAttribute('data-lightbox-alt') || ''; dialog.showModal(); } function closeLightbox() { dialog.close(); } triggers.forEach(function(trigger) { trigger.addEventListener('click', function() { openLightbox(trigger); }); }); closeBtn.addEventListener('click', function(e) { e.stopPropagation(); closeLightbox(); }); scrim.addEventListener('click', function() { closeLightbox(); }); dialog.addEventListener('close', function() { img.removeAttribute('src'); img.alt = ''; if (lastTrigger && typeof lastTrigger.focus === 'function') { lastTrigger.focus(); } lastTrigger = null; }); } // 初期化 document.addEventListener('DOMContentLoaded', function() { const serverSeason = getServerCalendarSeason(); const isLoggedIn = document.body && document.body.classList.contains('logged-in'); const savedSeason = isLoggedIn ? safeGetItem('selectedSeason') : null; const season = (savedSeason && isValidSeasonToken(savedSeason)) ? savedSeason : (serverSeason || getCurrentSeason()); setupSeasonalVideo(season); // 季節に応じた動画を設定 updateSeasonalBackgroundImages(season); // 季節に応じた背景画像を設定 setupSeasonSwitcher(season); // 季節切り替えボタン(確認用) setupHeroFadeIn(); // ヒーロー要素フェードイン(ページ読み込み直後) setupHomeHeroCtaButtonWidths(); // Home: ヒーローCTA幅揃え setupHomeTopTargetUniverseMask(); // Home: 背景アニメの切り抜き位置を実測 setupMobileMenu(); setupHeaderScroll(); // ヘッダースクロール挙動(ホーム) setupParallax(); // パララックス効果(困りごとセクション) setupParallaxDivider(); // パララックス画像仕切り setupFadeUpAnimation(); // フェードアップアニメーション setupUniverseBackgroundAnimations(); // [data-universe-bg] 宇宙 SVG 背景 setupSlideInFromRightAnimation(); // 右から左へスライドインアニメーション setupGreetingImageSlider(); // aboutus: ご挨拶画像スライダー(フェード) setupPageTopButton(); // PAGE TOP ボタン setupEmailLinks(); // メールリンク動的生成(スパム対策) setupAnchorScroll(); // アンカーリンクのヘッダー高さ対応 setupAboutUsNavAnchorButtonWidths(); // aboutus: ページ内ナビ幅揃え setupBooksPagination(); // 書籍ページネーション setupMailFormsValidation(); // お問い合わせ・予約フォーム setupNewsLightbox(); // お知らせアイキャッチ拡大 }); })();