PageSourceSearch

https://bnature-hawaii.com/wordpress/wp-content/themes/bnature-theme/assets/js/script.js?ver=1.0.0

js bnature-hawaii.com collected 2026-10-05 03:47:01 UTC 41,726 bytes, 1,090 lines download raw bytes

1/**
2 * BNature Theme - Front Page JavaScript
3 */
4
5document.addEventListener('DOMContentLoaded', function() {
6    // 初期化
7    init();
8});
9
10/**
11 * 初期化関数
12 */
13function init() {
14    // ヘッダーロゴの初期化
15    initHeaderLogo();
16    
17    // ハンバーガーメニューの初期化
18    initHamburgerMenu();
19    
20    // スムーススクロールの初期化
21    initSmoothScroll();
22    
23    // トップボタンの初期化
24    initToTopButton();
25    
26    // カスタムカーソルの初期化
27    initCustomCursor();
28    
29    // スライドショーの初期化
30    initSlideshow();
31    
32    // フェードインアニメーションの初期化
33    initFadeInAnimation();
34    
35    // メインビジュアルテキストアニメーションの初期化
36    initMainVisualTextAnimation();
37    
38    // Aboutページのロゴアニメーション初期化
39    initAboutLogoAnimation();
40    
41    // タイトルアニメーションの初期化
42    initTitleAnimation();
43    
44    // p-case-detail__subimage-containerの高さ計算初期化
45    initSubimageContainerHeight();
46    
47    // パララックスアニメーションの初期化
48    initParallaxAnimation();
49    
50    // 複合アニメーションの初期化
51    initComplexImageAnimation();
52    
53    // ケースアイテムアニメーションの初期化
54    initCaseItemAnimation();
55    
56    // Load Moreボタンの初期化
57    initLoadMore('.c-case__list');
58    initLoadMore('.c-blog__list');
59    initLoadMore('.c-information__list');
60}
61
62/**
63 * ヘッダーロゴの表示制御
64 */
65function initHeaderLogo() {
66    const headerLogo = document.querySelector('.l-header__logo');
67    const mainvisual = document.querySelector('.p-mainvisual');
68    
69    if (!headerLogo || !mainvisual) return;
70    
71    // Intersection Observerの設定
72    const observerOptions = {
73        root: null,
74        rootMargin: '0px',
75        threshold: 0
76    };
77    
78    const observer = new IntersectionObserver((entries) => {
79        entries.forEach((entry) => {
80            if (entry.isIntersecting) {
81                // mainvisualが画面内にある時:ロゴを非表示
82                headerLogo.style.opacity = '0';
83                headerLogo.style.visibility = 'hidden';
84            } else {
85                // mainvisualが画面外の時:ロゴを表示
86                headerLogo.style.opacity = '1';
87                headerLogo.style.visibility = 'visible';
88            }
89        });
90    }, observerOptions);
91    
92    // mainvisualを監視開始
93    observer.observe(mainvisual);
94}
95
96/**
97 * ハンバーガーメニューの制御
98 */
99function initHamburgerMenu() {
100    const hamburgerButton = document.querySelector('.c-hamburger');
101    const gnav = document.querySelector('.c-gnav');
102    const body = document.body;
103    
104    if (!hamburgerButton || !gnav) return;
105    
106    hamburgerButton.addEventListener('click', function() {
107        // メニューの開閉状態を切り替え
108        const isOpen = gnav.classList.contains('is-open');
109        
110        if (isOpen) {
111            // メニューを閉じる
112            gnav.classList.remove('is-open');
113            hamburgerButton.classList.remove('is-open');
114            // body.classList.remove('is-nav-open');
115        } else {
116            // メニューを開く
117            gnav.classList.add('is-open');
118            hamburgerButton.classList.add('is-open');
119            // body.classList.add('is-nav-open');
120        }
121    });
122    
123    // メニュー外クリック時に閉じる
124    // gnav.addEventListener('click', function(e) {
125    //     if (e.target === gnav) {
126    //         gnav.classList.remove('is-open');
127    //         hamburgerButton.classList.remove('is-open');
128    //         body.classList.remove('is-nav-open');
129    //     }
130    // });
131}
132
133/**
134 * スムーススクロールの制御
135 */
136function initSmoothScroll() {
137    // スムーススクロールの実装
138}
139
140/**
141 * トップに戻るボタンの制御
142 */
143function initToTopButton() {
144    const toTopButton = document.querySelector('.c-to-top');
145    
146    if (toTopButton) {
147        toTopButton.addEventListener('click', function(e) {
148            e.preventDefault();
149            
150            // 最上部にスムーススクロール
151            window.scrollTo({
152                top: 0,
153                behavior: 'smooth'
154            });
155        });
156    }
157} 
158
159/**
160 * カスタムカーソルの制御 - コンポーネントベース
161 */
162function initCustomCursor() {
163    const cursor = document.querySelector('.c-cursor');
164    
165    if (!cursor) return;
166    
167    let mouseX = 0;
168    let mouseY = 0;
169    let currentState = '-default';
170    
171    // コンポーネント別の状態定義(優先順位順)
172    const cursorStates = [
173        { selector: '.p-mainvisual', state: '-arrow-down' },
174        { selector: '.p-sec01__link', state: '-hover' },
175        { selector: '.p-sec02__item', state: '-plus' },
176        { selector: '.c-case__item a', state: '-plus' },
177        { selector: '.c-blog__item a', state: '-arrow-right' },
178        { selector: '.c-information__item a', state: '-hover' },
179        { selector: '.l-section__view-all', state: '-hover' },
180        { selector: '.c-cta a', state: '-banner' },
181        { selector: '.p-banner-about__link', state: '-banner' },
182        { selector: '.c-hamburger', state: '-hover' }
183    ];
184    
185    // その他のaタグ、buttonタグのデフォルト
186    const defaultInteractiveState = '-hover';
187    
188    // マウス座標を追跡
189    document.addEventListener('mousemove', function(e) {
190        mouseX = e.clientX;
191        mouseY = e.clientY;
192    });
193    
194    // requestAnimationFrameを使用してスムーズな追従
195    function updateCursor() {
196        cursor.style.transform = `translate3d(${mouseX - 0.5}px, ${mouseY - 0.5}px, 0px)`;
197        requestAnimationFrame(updateCursor);
198    }
199    
200    // アニメーションループ開始
201    updateCursor();
202    
203    // 初期状態を設定
204    setCursorState('-default');
205    
206    // 状態変更関数
207    function setCursorState(newState) {
208        if (currentState === newState) return;
209        
210        // 状態クラスのリスト
211        const stateClasses = ['-default', '-hover', '-arrow-down', '-arrow-right', '-plus', '-banner', '-countdown', '-none'];
212        
213        // 既存の状態クラスを削除
214        stateClasses.forEach(stateClass => {
215            cursor.classList.remove(stateClass);
216        });
217        
218        // 新しい状態クラスを追加
219        cursor.classList.add(newState);
220        currentState = newState;
221    }
222    
223    // 要素の状態を取得(優先順位に従ってチェック)
224    function getElementState(element) {
225        // 定義された要素を優先順位順にチェック
226        for (let config of cursorStates) {
227            if (element.matches(config.selector) || element.closest(config.selector)) {
228                return config.state;
229            }
230        }
231        
232        // インタラクティブ要素(a, button)の場合はデフォルト状態を返す
233        if (element.matches('a, button') || element.closest('a, button')) {
234            return defaultInteractiveState;
235        }
236        
237        // それ以外は通常状態
238        return '-default';
239    }
240    
241    // マウスがページを離れた時は非表示
242    document.addEventListener('mouseleave', function() {
243        cursor.classList.add('is-hidden');
244    });
245    
246    // マウスがページに入った時は表示
247    document.addEventListener('mouseenter', function() {
248        cursor.classList.remove('is-hidden');
249    });
250    
251    // cursorStatesから動的にセレクタを生成
252    const customSelectors = cursorStates.map(state => state.selector).join(', ');
253    const allElements = document.querySelectorAll(`a, button, ${customSelectors}`);
254    
255    allElements.forEach(element => {
256        element.addEventListener('mouseenter', function() {
257            const state = getElementState(element);
258            setCursorState(state);
259        });
260        
261        element.addEventListener('mouseleave', function() {
262            setCursorState('-default');
263        });
264    });
265} 
266
267/**
268 * スライドショーの制御
269 */
270function initSlideshow() {
271    const slides = document.querySelectorAll('.p-sec02__slideshow-img');
272    const indicator = document.querySelector('.p-sec02__slider-indicator');
273    
274    if (!slides.length || !indicator) return;
275    
276    let currentSlide = 0;
277    const totalSlides = slides.length;
278    
279    // スライドを切り替える関数
280    function changeSlide() {
281        // 現在のアクティブなスライドを非アクティブにする
282        slides[currentSlide].classList.remove('active');
283        
284        // 次のスライドのインデックスを計算
285        currentSlide = (currentSlide + 1) % totalSlides;
286        
287        // 新しいスライドをアクティブにする
288        slides[currentSlide].classList.add('active');
289    }
290    
291    // プログレスバーを開始(CSSアニメーションで自動ループ)
292    indicator.classList.add('active');
293    
294    // 3秒間隔でスライドを切り替え
295    setInterval(changeSlide, 3000);
296}
297
298/**
299 * フェードインアニメーションの制御
300 */
301function initFadeInAnimation() {
302    // p-sec01__content配下の要素を取得
303    const fadeElements = document.querySelectorAll('.p-sec01__title, .p-sec01__text1, .p-sec01__text2, .p-sec01__link');
304    
305    if (!fadeElements.length) return;
306    
307    // 各要素に共通のフェードインクラスを追加
308    fadeElements.forEach(element => {
309        element.classList.add('fade-in-element');
310    });
311    
312    // Intersection Observerの設定
313    const observerOptions = {
314        root: null,
315        rootMargin: '0px',
316        threshold: 0.1
317    };
318    
319    const observer = new IntersectionObserver((entries) => {
320        entries.forEach((entry) => {
321            if (entry.isIntersecting) {
322                // 要素が画面内に入った時、順番にアニメーションを開始
323                const targetElement = entry.target;
324                const elementIndex = Array.from(fadeElements).indexOf(targetElement);
325                
326                // 要素ごとに時間差でアニメーション(0.2秒ずつ遅延)
327                setTimeout(() => {
328                    targetElement.classList.add('fade-in-up');
329                }, elementIndex * 200);
330                
331                // 一度アニメーションしたら監視を停止
332                observer.unobserve(targetElement);
333            }
334        });
335    }, observerOptions);
336    
337    // 各要素を監視開始
338    fadeElements.forEach(element => {
339        observer.observe(element);
340    });
341}
342
343/**
344 * メインビジュアルテキストアニメーションの制御
345 */
346function initMainVisualTextAnimation() {
347    const titleEn = document.querySelector('.p-mainvisual__title-en');
348    const titleJa = document.querySelector('.p-mainvisual__title-ja');
349    
350    if (!titleEn || !titleJa) return;
351    
352    // ::beforeのアニメーション完了後(2秒後)にテキストアニメーションを開始
353    setTimeout(() => {
354        // 英語タイトルを最初にアニメーション
355        titleEn.classList.add('fade-in-slide');
356        
357        // 日本語タイトルを0.3秒遅れてアニメーション
358        setTimeout(() => {
359            titleJa.classList.add('fade-in-slide');
360        }, 300);
361    }, 1000);
362} 
363
364/**
365 * 汎用読み込み機能の制御
366 * @param {string} listSelector - 投稿を追加するリストのセレクタ
367 */
368function initLoadMore(listSelector) {
369    const loadMoreBtn = document.querySelector('.p-archive-more-btn');
370    
371    if (!loadMoreBtn) return;
372    
373    loadMoreBtn.addEventListener('click', function() {
374        const btn = this;
375        const currentPage = parseInt(btn.dataset.currentPage);
376        const maxPages = parseInt(btn.dataset.maxPages);
377        const postType = btn.dataset.postType;
378        const nextPage = currentPage + 1;
379        
380        // Ajax アクションを投稿タイプに応じて決定
381        let ajaxAction;
382        if (postType === 'case') {
383            ajaxAction = 'load_more_cases';
384        } else if (postType === 'information') {
385            ajaxAction = 'load_more_information';
386        } else {
387            ajaxAction = 'load_more_posts';
388        }
389        
390        // ボタンのローディング状態に変更
391        setLoadingState(btn, true);
392        
393        // Ajax通信
394        fetch(ajax_object.ajax_url, {
395            method: 'POST',
396            headers: {
397                'Content-Type': 'application/x-www-form-urlencoded',
398            },
399            body: new URLSearchParams({
400                action: ajaxAction,
401                page: nextPage,
402                post_type: postType,
403                nonce: ajax_object.nonce
404            })
405        })
406        .then(response => response.json())
407        .then(data => {
408            console.log('Ajax response:', data);
409            
410            if (data.success && data.data.html) {
411                // 新しい投稿をリストに追加
412                const list = document.querySelector(listSelector);
413                if (list) {
414                    list.insertAdjacentHTML('beforeend', data.data.html);
415                    
416                    // 新しく追加された要素にアニメーションを初期化
417                    if (listSelector === '.c-case__list') {
418                        // 新しく追加されたケースアイテムにアニメーションを適用
419                        initCaseItemAnimation();
420                    }
421                }
422            } else {
423                console.error('Error loading more posts:', data);
424            }
425            
426            // ボタンの状態を更新
427            btn.dataset.currentPage = nextPage;
428            
429            // 最後のページに達したらボタンを非表示
430            if (nextPage >= maxPages) {
431                btn.style.display = 'none';
432            }
433        })
434        .catch(error => {
435            console.error('Error:', error);
436        })
437        .finally(() => {
438            // ローディング状態を解除
439            setLoadingState(btn, false);
440        });
441    });
442}
443
444/**
445 * Aboutページのロゴ透明度制御
446 */
447function initAboutLogoAnimation() {
448    const logo = document.querySelector('.p-about-mainvisual__logo');
449    const mainvisualImg = document.querySelector('.p-about-mainvisual__img');
450    const messageImage = document.querySelector('.p-about-message__image1');
451    const messageContent2 = document.querySelector('.p-about-message__content2');
452    
453    if (!logo && !mainvisualImg && !messageImage) return;
454    
455    // スクロールイベントリスナー
456    window.addEventListener('scroll', () => {
457        const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
458        const windowHeight = window.innerHeight;
459        
460        // ロゴの透明度制御
461        if (logo) {
462            // 画面の高さの半分を基準に透明度を計算
463            const halfWindowHeight = windowHeight / 2;
464            
465            // スクロール量に基づいて透明度を計算(0〜1の範囲)
466            let logoOpacity = 1 - (scrollTop / halfWindowHeight);
467            
468            // 透明度を0〜1の範囲に制限
469            logoOpacity = Math.max(0, Math.min(1, logoOpacity));
470            
471            // ロゴの透明度を設定
472            logo.style.opacity = logoOpacity;
473        }
474        
475        // メインビジュアル画像の透明度制御
476        if (mainvisualImg) {
477            // 1画面の高さ分を起点、1.5画面分の高さをゴール
478            const startPoint = windowHeight; // 1画面分
479            const endPoint = windowHeight * 1.5; // 1.5画面分
480            
481            // スクロール量に基づいて透明度を計算(0〜1の範囲)
482            let imgOpacity = 1;
483            
484            if (scrollTop >= startPoint && scrollTop <= endPoint) {
485                // 1画面分〜1.5画面分の間で透明度を変化
486                const progress = (scrollTop - startPoint) / (endPoint - startPoint);
487                imgOpacity = 1 - progress;
488            } else if (scrollTop > endPoint) {
489                // 1.5画面分を超えたら完全透明
490                imgOpacity = 0;
491            } else {
492                // 1画面分未満は完全表示
493                imgOpacity = 1;
494            }
495            
496            // 透明度を0〜1の範囲に制限
497            imgOpacity = Math.max(0, Math.min(1, imgOpacity));
498            
499            // メインビジュアル画像の透明度を設定
500            mainvisualImg.style.opacity = imgOpacity;
501        }
502        
503        // メッセージ画像のアニメーション制御
504        if (messageImage && messageContent2) {
505            const content2Rect = messageContent2.getBoundingClientRect();
506            const content2Top = content2Rect.top;
507            const content2Bottom = content2Rect.bottom;
508            const windowHeight = window.innerHeight;
509            const upperThird = windowHeight / 3; // 画面の上1/3の位置
510            
511            // p-about-message__content2が画面の上1/3を通過している間のみp-about-message__image1を表示
512            // content2の上端が画面の上1/3に到達したらフェードイãƒ
512³
513            if (content2Top <= upperThird) {
514                messageImage.classList.add('visible');
515            }
516            
517            // content2の下端が画面の上1/3に到達したらフェードアウト
518            if (content2Bottom <= upperThird) {
519                messageImage.classList.remove('visible');
520            }
521        }
522        
523        // フェードイン画像の表示制御(中央1/3エリア)
524        const fadeInImagesCenter = document.querySelectorAll('.fade-in-image');
525        fadeInImagesCenter.forEach(image => {
526            const imageRect = image.getBoundingClientRect();
527            const imageTop = imageRect.top;
528            const imageBottom = imageRect.bottom;
529            const windowHeight = window.innerHeight;
530            const centerStart = windowHeight / 3; // 画面の中央1/3の開始位置
531            const centerEnd = windowHeight * 2 / 3; // 画面の中央1/3の終了位置
532            
533            // 画像が画面の中央1/3に入っている間のみ表示
534            if (imageBottom > centerStart && imageTop < centerEnd) {
535                image.classList.add('visible');
536            } else {
537                image.classList.remove('visible');
538            }
539        });
540        
541        // p-about-produce__itemのY軸アニメーション制御
542        const produceList = document.querySelector('.p-about-produce__list');
543        if (produceList) {
544            const listRect = produceList.getBoundingClientRect();
545            const listTop = listRect.top;
546            const listBottom = listRect.bottom;
547            const windowHeight = window.innerHeight;
548            
549            // リストが画面内に入っている間のみアニメーション
550            if (listBottom > 0 && listTop < windowHeight) {
551                // 画面内での進行度を計算(0〜1)
552                const progress = Math.max(0, Math.min(1, (windowHeight - listTop) / (windowHeight + listRect.height)));
553                
554                // 各アイテムのY軸移動量を計算
555                const items = produceList.querySelectorAll('.p-about-produce__item');
556                items.forEach((item, index) => {
557                    const initialOffset = index * 40; // 初期オフセット(0px, 20px, 40px)
558                    const finalOffset = -index * 40; // 最終オフセット(0px, -20px, -40px)
559                    const currentOffset = initialOffset + (finalOffset - initialOffset) * progress;
560                    
561                    item.style.transform = `translateY(${currentOffset}px)`;
562                });
563            }
564        }
565        
566        // フェードイン画像の制御
567        const fadeInImages = document.querySelectorAll('.fade-in-image');
568        fadeInImages.forEach(image => {
569            const imageRect = image.getBoundingClientRect();
570            const imageTop = imageRect.top;
571            const imageBottom = imageRect.bottom;
572            const windowHeight = window.innerHeight;
573            const centerPosition = windowHeight / 2; // 画面の中央位置
574            
575            // 画像が画面の中央に来たらフェードイン(一度だけ)
576            if (imageTop <= centerPosition && imageBottom >= centerPosition && !image.classList.contains('visible')) {
577                image.classList.add('visible');
578            }
579        });
580        
581
582        
583
584        
585        // スライドイン要素のアニメーション制御
586        const slideInElements = document.querySelectorAll('.slide-in-element');
587        slideInElements.forEach(element => {
588            const elementRect = element.getBoundingClientRect();
589            const elementTop = elementRect.top;
590            const elementBottom = elementRect.bottom;
591            const windowHeight = window.innerHeight;
592            const centerPosition = windowHeight / 2; // 画面の中央位置
593            
594            // 要素が画面の中央に来たらスライドイン(一度だけ)
595            if (elementTop <= centerPosition && elementBottom >= centerPosition && !element.classList.contains('slide-in-left')) {
596                element.classList.add('slide-in-left');
597            }
598        });
599        
600
601    });
602}
603
604/**
605 * ボタンのローディング状態を制御(統合版)
606 */
607function setLoadingState(btn, isLoading) {
608    const textSpan = btn.querySelector('.p-archive-more-text');
609    const loadingSpan = btn.querySelector('.p-archive-more-loading');
610    
611    if (isLoading) {
612        btn.disabled = true;
613        textSpan.style.display = 'none';
614        loadingSpan.style.display = 'inline';
615    } else {
616        btn.disabled = false;
617        textSpan.style.display = 'inline';
618        loadingSpan.style.display = 'none';
619    }
620} 
621
622/**
623 * タイトルテキストアニメーションの制御
624 */
625function initTitleAnimation() {
626    const titleElements = document.querySelectorAll('.l-section__title');
627    
628    if (!titleElements.length) return;
629    
630    // 各タイトル要素のテキストを1文字ずつspan要素に分割
631    titleElements.forEach(titleElement => {
632        const text = titleElement.textContent;
633        titleElement.innerHTML = '';
634        
635        // 各文字をspan要素で囲む
636        Array.from(text).forEach((char, index) => {
637            const span = document.createElement('span');
638            span.textContent = char === ' ' ? '\u00A0' : char; // スペースは&nbsp;に変換
639            span.classList.add('title-char');
640            span.style.display = 'inline-block';
641            span.style.opacity = '0';
642            span.style.transform = 'translateX(10px)';
643            span.style.transition = 'opacity 1.2s ease, transform 1.2s ease';
644            // 左から順番にアニメーション
645            span.style.transitionDelay = `${index * 0.2}s`;
646            titleElement.appendChild(span);
647        });
648        
649        // アニメーションクラスを追加(これによりCSSで表示状態になる)
650        titleElement.classList.add('title-animation-ready');
651    });
652    
653    // Intersection Observerの設定
654    const observerOptions = {
655        root: null,
656        rootMargin: '0px',
657        threshold: 0.3
658    };
659    
660    const observer = new IntersectionObserver((entries) => {
661        entries.forEach((entry) => {
662            if (entry.isIntersecting) {
663                const titleElement = entry.target;
664                const chars = titleElement.querySelectorAll('.title-char');
665                
666                // 各文字を順番に右から左にスライドインしながらフェードイン
667                chars.forEach(char => {
668                    char.style.opacity = '1';
669                    char.style.transform = 'translateX(0)';
670                });
671                
672                // 一度アニメーションしたら監視を停止
673                observer.unobserve(titleElement);
674            }
675        });
676    }, observerOptions);
677    
678    // 各タイトル要素を監視開始
679    titleElements.forEach(titleElement => {
680        observer.observe(titleElement);
681    });
682}
683
684/**
685 * p-case-detail__subimage-containerの高さを動的に計算する関数
686 */
687function calculateSubimageContainerHeight() {
688    const containers = document.querySelectorAll('.p-case-detail__subimage-container');
689    console.log(`Found ${containers.length} subimage containers`);
690    
691    containers.forEach((container, index) => {
692        const imgElement = container.querySelector('img');
693        if (imgElement) {
694            // HTMLのwidth属性とheight属性の値を取得
695            const widthAttr = imgElement.getAttribute('width');
696            const heightAttr = imgElement.getAttribute('height');
697            
698            // 画像の自然なサイズも取得
699            const naturalWidth = imgElement.naturalWidth;
700            const naturalHeight = imgElement.naturalHeight;
701            
702            console.log(`Container ${index}:`, {
703                widthAttr,
704                heightAttr,
705                naturalWidth,
706                naturalHeight,
707                imgComplete: imgElement.complete,
708                containerWidth: container.offsetWidth
709            });
710            
711            let aspectRatio;
712            
713            // HTML属性が存在する場合はそれを使用、なければ自然なサイズを使用
714            if (widthAttr && heightAttr) {
715                aspectRatio = parseInt(heightAttr) / parseInt(widthAttr);
716                console.log(`Container ${index}: Using HTML attributes, aspectRatio = ${aspectRatio}`);
717            }
717 else if (naturalWidth && naturalHeight) {
718                aspectRatio = naturalHeight / naturalWidth;
719                console.log(`Container ${index}: Using natural dimensions, aspectRatio = ${aspectRatio}`);
720            } else {
721                console.log(`Container ${index}: No valid dimensions found`);
722            }
723            
724            if (aspectRatio) {
725                // コンテナの現在の幅を取得
726                let containerWidth = container.offsetWidth;
727                
728                // 幅が0の場合は、親要素の幅を取得するか、デフォルト値を使用
729                if (containerWidth === 0) {
730                    const parentContainer = container.closest('.p-case-detail__subimage');
731                    if (parentContainer) {
732                        containerWidth = parentContainer.offsetWidth;
733                    }
734                    
735                    // それでも0の場合は、画面幅の80%をデフォルトとして使用
736                    if (containerWidth === 0) {
737                        containerWidth = window.innerWidth * 0.8;
738                        console.log(`Container ${index}: Using default width: ${containerWidth}`);
739                    }
740                }
741                
742                let calculatedHeight;
743                
744                calculatedHeight = containerWidth * aspectRatio;
745                console.log(`Container ${index}: containerWidth = ${containerWidth}, calculatedHeight = ${calculatedHeight}`);
746                
747                // コンテナの高さを設定
748                container.style.height = `${calculatedHeight}px`;
749            } else {
750                console.log(`Container ${index}: No aspect ratio calculated, trying fallback...`);
751                
752                // フォールバック: デフォルトのアスペクト比(16:9)を使用
753                let containerWidth = container.offsetWidth;
754                if (containerWidth === 0) {
755                    const parentContainer = container.closest('.p-case-detail__subimage');
756                    if (parentContainer) {
757                        containerWidth = parentContainer.offsetWidth;
758                    }
759                    if (containerWidth === 0) {
760                        containerWidth = window.innerWidth * 0.8;
761                    }
762                }
763                
764                if (containerWidth > 0) {
765                    const fallbackAspectRatio = 9 / 16; // 16:9の逆数
766                    const fallbackHeight = containerWidth * fallbackAspectRatio;
767                    console.log(`Container ${index}: Using fallback aspect ratio, height = ${fallbackHeight}`);
768                    container.style.height = `${fallbackHeight}px`;
769                } else {
770                    console.log(`Container ${index}: Container width is 0, cannot calculate height`);
771                }
772            }
773        } else {
774            console.log(`Container ${index}: No img element found`);
775        }
776    });
777}
778
779/**
780 * スクロールに合わせて内部のimgを上下に動かす汎用関数
781 */
782function animateImageParallax(containerSelector, minOffset = -15, maxOffset = 15) {
783    const containers = document.querySelectorAll(containerSelector);
784    
785    // セレクタが見つからない場合は終了
786    if (containers.length === 0) {
787        return;
788    }
789    
790
791    
792    containers.forEach(container => {
793        const containerRect = container.getBoundingClientRect();
794        const containerTop = containerRect.top;
795        const containerBottom = containerRect.bottom;
796        const windowHeight = window.innerHeight;
797        
798        // コンテナが画面内にいる間のみアニメーション
799        if (containerBottom > 0 && containerTop < windowHeight) {
800            const imgElement = container.querySelector('img');
801            if (imgElement) {
802                // 複合アニメーションが完了しているかチェック(該当要素のみ)
803                const isAnimationCompleted = container.dataset.animationCompleted === 'true';
804                const shouldApplyParallax = container.dataset.animationCompleted !== undefined ? isAnimationCompleted : true;
805                
806                // 画面内での進行度を計算(0〜1)
807                const progress = Math.max(0, Math.min(1, (windowHeight - containerTop
807) / (windowHeight + containerRect.height)));
808                
809                // 指定された範囲でY軸移動量を計算(小数点以下2桁まで)
810                const currentOffset = Math.round((minOffset + (maxOffset - minOffset) * progress) * 100) / 100;
811                
812                // 既存のtranslate値を保持しつつ、Y軸のみ更新
813                const currentTranslate = imgElement.style.transform || '';
814                const translateMatch = currentTranslate.match(/translate\(([^)]+)\)/);
815                
816                if (translateMatch) {
817                    // 既存のtranslate値がある場合、X軸の値を保持してY軸のみ更新
818                    const translateValues = translateMatch[1].split(',');
819                    const xValue = translateValues[0].trim();
820                    
821                    // 条件に応じてパララックスを適用
822                    if (shouldApplyParallax) {
823                        imgElement.style.transform = `translate(${xValue}, ${currentOffset}px)`;
824                    }
825                } else {
826                    // 既存のtranslate値がない場合、Y軸のみ設定
827                    if (shouldApplyParallax) {
828                        imgElement.style.transform = `translate(0, ${currentOffset}px)`;
829                    }
830                }
831            }
832        }
833    });
834}
835
836/**
837 * p-case-detail__subimage-containerの高さ計算初期化
838 */
839function initSubimageContainerHeight() {
840    console.log('Initializing subimage container height calculation...');
841    
842    // 初期計算(表示されている要素のみ)
843    calculateSubimageContainerHeight();
844    
845    // Intersection Observerを使用して要素が画面に入った時に高さを計算
846    const containers = document.querySelectorAll('.p-case-detail__subimage-container');
847    
848    const observer = new IntersectionObserver((entries) => {
849        entries.forEach((entry) => {
850            if (entry.isIntersecting) {
851                const container = entry.target;
852                const index = Array.from(containers).indexOf(container);
853                console.log(`Container ${index} entered viewport, calculating height...`);
854                
855                // 要素が画面に入った時に高さを計算
856                calculateContainerHeight(container, index);
857                
858                // 一度計算したら監視を停止
859                observer.unobserve(container);
860            }
861        });
862    }, {
863        rootMargin: '50px' // 50px手前から監視開始
864    });
865    
866    // 各コンテナを監視開始
867    containers.forEach(container => {
868        observer.observe(container);
869    });
870    
871    // ウィンドウリサイズ時に再計算
872    window.addEventListener('resize', function() {
873        calculateSubimageContainerHeight();
874    });
875}
876
877/**
878 * 個別のコンテナの高さを計算する関数
879 */
880function calculateContainerHeight(container, index) {
881    const imgElement = container.querySelector('img');
882    if (imgElement) {
883        // HTMLのwidth属性とheight属性の値を取得
884        const widthAttr = imgElement.getAttribute('width');
885        const heightAttr = imgElement.getAttribute('height');
886        
887        // 画像の自然なサイズも取得
888        const naturalWidth = imgElement.naturalWidth;
889        const naturalHeight = imgElement.naturalHeight;
890        
891        console.log(`Container ${index}:`, {
892            widthAttr,
893            heightAttr,
894            naturalWidth,
895            naturalHeight,
896            imgComplete: imgElement.complete,
897            containerWidth: container.offsetWidth
898        });
899        
900        let aspectRatio;
901        
902        // HTML属性が存在する場合はそれを使用、なければ自然なサイズを使用
903        if (widthAttr && heightAttr) {
904            aspectRatio = parseInt(heightAttr) / parseInt(widthAttr);
905            console.log(`Container ${index}: Using HTML attributes, aspectRatio = ${aspectRatio}`);
906        }
906 else if (naturalWidth && naturalHeight) {
907            aspectRatio = naturalHeight / naturalWidth;
908            console.log(`Container ${index}: Using natural dimensions, aspectRatio = ${aspectRatio}`);
909        } else {
910            console.log(`Container ${index}: No valid dimensions found`);
911        }
912        
913        if (aspectRatio) {
914            // コンテナの現在の幅を取得
915            let containerWidth = container.offsetWidth;
916            
917            // 幅が0の場合は、親要素の幅を取得するか、デフォルト値を使用
918            if (containerWidth === 0) {
919                const parentContainer = container.closest('.p-case-detail__subimage');
920                if (parentContainer) {
921                    containerWidth = parentContainer.offsetWidth;
922                }
923                
924                // それでも0の場合は、画面幅の80%をデフォルトとして使用
925                if (containerWidth === 0) {
926                    containerWidth = window.innerWidth * 0.8;
927                    console.log(`Container ${index}: Using default width: ${containerWidth}`);
928                }
929            }
930            
931            let calculatedHeight;
932            
933            // アスペクト比に基づいて高さを計算
934            if (aspectRatio > 1) {
935                // 縦長の画像の場合:コンテナ幅を高さとして使用
936                calculatedHeight = containerWidth;
937                console.log(`Container ${index}: Vertical image, using container width as height: ${calculatedHeight}`);
938            } else {
939                // 横長の画像の場合:アスペクト比に基づいて高さを計算
940                calculatedHeight = containerWidth * aspectRatio;
941                console.log(`Container ${index}: Horizontal image, calculated height: ${calculatedHeight}`);
942            }
943            
944            console.log(`Container ${index}: containerWidth = ${containerWidth}, calculatedHeight = ${calculatedHeight}`);
945            
946            // コンテナの高さを設定
947            container.style.height = `${calculatedHeight}px`;
948        } else {
949            console.log(`Container ${index}: No aspect ratio calculated, trying fallback...`);
950            
951            // フォールバック: デフォルトのアスペクト比(16:9)を使用
952            let containerWidth = container.offsetWidth;
953            if (containerWidth === 0) {
954                const parentContainer = container.closest('.p-case-detail__subimage');
955                if (parentContainer) {
956                    containerWidth = parentContainer.offsetWidth;
957                }
958                if (containerWidth === 0) {
959                    containerWidth = window.innerWidth * 0.8;
960                }
961            }
962            
963            if (containerWidth > 0) {
964                const fallbackAspectRatio = 9 / 16; // 16:9の逆数
965                const fallbackHeight = containerWidth * fallbackAspectRatio;
966                console.log(`Container ${index}: Using fallback aspect ratio, height = ${fallbackHeight}`);
967                container.style.height = `${fallbackHeight}px`;
968            } else {
969                console.log(`Container ${index}: Container width is 0, cannot calculate height`);
970            }
971        }
972    } else {
973        console.log(`Container ${index}: No img element found`);
974    }
975}
976
977/**
978 * 複合アニメーションの初期化
979 */
980function initComplexImageAnimation() {
981    const animatedImages = document.querySelectorAll('.complex-animation-element');
982    
983    if (!animatedImages.length) return;
984    
985    // 各画像に初期状態を設定
986    animatedImages.forEach(image => {
987        const imgElement = image.querySelector('img');
988        if (imgElement) {
989            // アニメーション完了フラグを初期化
990            image.dataset.animationCompleted = 'false';
991        }
992    });
993    
994    // Intersection Observerでアニメーション開始を監視
995    const observer = new IntersectionObserver((entries) => {
996        entries.forEach((entry) => {
997            if (entry.isIntersecting) {
998                const image = entry.target;
999                const imgElement = image.querySelector('img');
1000                
1001                if (imgElement && !image.classList.contains('animation-started')) {
1002                    image.classList.add('animation-started');
1003                    
1004                    // CSS transitionでアニメーション開始
1005                    image.classList.add('animate');
1006                    
1007                    // アニメーション完了後にフラグを設定
1008                    setTimeout(() => {
1009                        image.dataset.animationCompleted = 'true';
1010                    }, 2000); // 2秒後に完了
1011                }
1012            }
1013        });
1014    }, {
1015        threshold: 0.5,
1016        rootMargin: '0px'
1017    });
1018    
1019    // 各画像を監視開始
1020    animatedImages.forEach(image => {
1021        observer.observe(image);
1022    });
1023}
1024
1025
1026
1027/**
1028 * パララックスアニメーションの初期化
1029 */
1030function initParallaxAnimation() {
1031    let ticking = false;
1032    
1033    // パララックスアニメーションを実行する関数
1034    function updateParallax() {
1035        // 汎用パララックスアニメーション
1036        animateImageParallax('.parallax-container', -60, 60);
1037        
1038        // その他のページのパララックスアニメーション
1039        animateImageParallax('.c-case__item-img', -60, 60);
1040        
1041        ticking = false;
1042    }
1043    
1044    // スクロールイベントリスナー
1045    window.addEventListener('scroll', function() {
1046        if (!ticking) {
1047            requestAnimationFrame(updateParallax);
1048            ticking = true;
1049        }
1050    });
1051}
1052
1053/**
1054 * ケースアイテムアニメーションの初期化
1055 */
1056function initCaseItemAnimation() {
1057    const caseItems = document.querySelectorAll('.case-item-animation');
1058    
1059    if (!caseItems.length) return;
1060    
1061    // Intersection Observerでアニメーション開始を監視
1062    const observer = new IntersectionObserver((entries) => {
1063        entries.forEach((entry) => {
1064            if (entry.isIntersecting) {
1065                const caseItem = entry.target;
1066                
1067                if (!caseItem.classList.contains('animation-started')) {
1068                    caseItem.classList.add('animation-started');
1069                    
1070                    // 各要素にアニメーションクラスを追加
1071                    const img = caseItem.querySelector('.c-case__item-img');
1072                    const title = caseItem.querySelector('.c-case__item-title');
1073                    const text = caseItem.querySelector('.c-case__item-text');
1074                    
1075                    if (img) img.classList.add('animate');
1076                    if (title) title.classList.add('animate');
1077                    if (text) text.classList.add('animate');
1078                }
1079            }
1080        });
1081    }, {
1082        threshold: 0.3,
1083        rootMargin: '0px'
1084    });
1085    
1086    // 各ケースアイテムを監視開始
1087    caseItems.forEach(item => {
1088        observer.observe(item);
1089    });
1090}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.