PageSourceSearch

https://tsunehiro.net/js/script.js

js tsunehiro.net collected 2026-10-07 18:18:07 UTC 13,303 bytes, 421 lines download raw bytes

1document.addEventListener("DOMContentLoaded", function () {
2  // --- テキストのフェードイン処理 ---
3  const textWrapper = document.querySelector(".heroTextWrapper");
4  if (textWrapper) {
5    // ページロード後、100ms(ほんの一瞬)遅らせて発火させるとスムーズです
6    setTimeout(() => {
7      textWrapper.classList.add("is-visible");
8    }, 100);
9  }
10
11  // --- 既存のスライダー処理 ---
12  function initializeHeroSlider(containerId) {
13    const container = document.querySelector(`#${containerId}`);
14    if (!container) return;
15
16    const slides = container.querySelectorAll(".heroSlide");
17    const dots = container.querySelectorAll(".heroIndicatorDot");
18    const totalSlides = slides.length;
19    let currentSlide = 0;
20    let autoPlayInterval;
21
22    function showHeroSlide(index) {
23      slides.forEach((slide, i) => {
24        slide.style.opacity = i === index ? 1 : 0;
25      });
26
27      dots.forEach((dot, i) => {
28        if (i === index) {
29          dot.classList.add("isIndicatorActive");
30        } else {
31          dot.classList.remove("isIndicatorActive");
32        }
33      });
34
35      currentSlide = index;
36    }
37
38    function nextHeroSlide() {
39      let nextIndex = (currentSlide + 1) % totalSlides;
40      showHeroSlide(nextIndex);
41    }
42
43    function startHeroAutoPlay() {
44      autoPlayInterval = setInterval(nextHeroSlide, 5000);
45    }
46
47    dots.forEach((dot, i) => {
48      dot.addEventListener("click", () => {
49        clearInterval(autoPlayInterval);
50        showHeroSlide(i);
51        startHeroAutoPlay();
52      });
53    });
54
55    showHeroSlide(0);
56    startHeroAutoPlay();
57  }
58
59  initializeHeroSlider("heroSliderContainer");
60});
61
62// GSAPとScrollTriggerプラグインを有効化
63gsap.registerPlugin(ScrollTrigger);
64
65document.addEventListener("DOMContentLoaded", function () {
66  const isSp = window.innerWidth <= 767;
67
68  // アニメーションのタイムライン定義
69  // トリガーをセクション全体(.anxietySection)にして上から下への連動性を担保
70  const anxietyTl = gsap.timeline({
71    scrollTrigger: {
72      trigger: ".anxietySection",
73      start: "top 70%", // スクロールに合わせて少し早めに始動
74      toggleActions: "play none none reverse",
75    },
76  });
77
78  // 1. 上のメインタイトルをふわっと上に浮き上がらせる
79  anxietyTl.from(".anxietyMainQuestion", {
80    y: 20,
81    opacity: 0,
82    duration: 0.6,
83    ease: "power2.out",
84  });
85
86  // 2. 中央の人物イラスト(タイトルの終わり際に被せてスタート)
87  anxietyTl.from(
88    ".anxietyPartPeople",
89    {
90      y: 30,
91      opacity: 0,
92      duration: 0.7,
93      ease: "power2.out",
94    },
95    "-=0.3", // タイトル出現の後半から重ねる
96  );
97
98  // 3. 吹き出し1〜4(ポコポコ動く設定)
99  const balloonEase = "back.out(1.5)";
100  anxietyTl
101    .from(
102      ".balloon1",
103      {
104        scale: 0.4,
105        opacity: 0,
106        duration: 0.55,
107        transformOrigin: isSp ? "28% 25%" : "23% 28%",
108        ease: balloonEase,
109      },
110      "-=0.25",
111    )
112    .from(
113      ".balloon2",
114      {
115        scale: 0.4,
116        opacity: 0,
117        duration: 0.55,
118        transformOrigin: isSp ? "72% 25%" : "77% 28%",
119        ease: balloonEase,
120      },
121      "-=0.4",
122    )
123    .from(
124      ".balloon3",
125      {
126        scale: 0.4,
127        opacity: 0,
128        duration: 0.55,
129        transformOrigin: isSp ? "30% 55%" : "18% 55%",
130        ease: balloonEase,
131      },
132      "-=0.4",
133    )
134    .from(
135      ".balloon4",
136      {
137        scale: 0.4,
138        opacity: 0,
139        duration: 0.55,
140        transformOrigin: isSp ? "70% 55%" : "82% 55%",
141        ease: balloonEase,
142      },
143      "-=0.4",
144    );
145
146  // 4. 下の解決メッセージエリア:タイトルと説明文を時間差でフェードイン
147  anxietyTl.from(
148    [".anxietySolutionTitle", ".anxietySolutionDescription"],
149    {
150      y: 20, // 動きを少し上品に抑える
151      opacity: 0,
152      duration: 0.6,
153      stagger: 0.15, // タイトルから0.15秒ズラして説明文を表示
154      ease: "power2.out",
155    },
156    "-=0.2", // 最後の吹き出しが出始めるあたりで始動
157  );
158
159  // 5. 【修正】ボタン単体の登場アニメーション
160  // 上への移動量を「10px」に抑えることで、不自然な飛び跳ね感を解消。
161  // 元のCSSの定位置(translateY(0))へ滑らかに吸い付くように着地させます。
162  anxietyTl.from(
163    ".anxietyServiceButton",
164    {
165      y: 10, // 10px下から静かに現れる
166      opacity: 0,
167      duration: 0.5,
168      ease: "power1.out", // 緩やかに減速するイージング
169    },
170    "-=0.25", // 説明文のフェードインに少し被せる
171  );
172
173  // ★重要★ 登場が終わった瞬間、GSAPのインラインスタイルを削除してCSS(:hover)にバトンタッチ
174  anxietyTl.set(".anxietyServiceButton", { clearProps: "all" });
175});
176
177// ==========================================
178// 作品スライダーの設定(変更なし)
179// ==========================================
180const worksSlideSpeedPC = 11000;
181const worksSlideSpeedSP = 7000;
182
183window.addEventListener("load", function () {
184  const topWorksSwiper = new Swiper("#topWorksSwiper", {
185    loop: true,
186    allowTouchMove: false,
187
188    // ★JSでスライダー全体の向きを「右から左(Right to Left)」に設定
189    dir: "rtl",
190
191    autoplay: {
192      delay: 0,
193      disableOnInteraction: false,
194      // reverseDirection は不要(競合して止まる原因になるため削除)
195    },
196
197    speed: worksSlideSpeedSP,
198
199    breakpoints: {
200      768: {
201        slidesPerView: 3.5,
202        spaceBetween: 30,
203        speed: worksSlideSpeedPC,
204      },
205      0: {
206        slidesPerView: 1.5,
207        spaceBetween: 15,
208      },
209    },
210  });
211});
212
213document.addEventListener("DOMContentLoaded", function () {
214  // 1. 下層ヘッダーのアニメーション発火(もしすでにあればここに記述)
215  const headInner = document.querySelector(".headSection .inner");
216  if (headInner) {
217    setTimeout(() => {
218      headInner.classList.add("is-visible");
219    }, 100);
220  }
221
222  // 2. サービス紹介セクションのスクロール監視
223  const serviceSection = document.querySelector(".serviceIntroSection");
224
225  if (serviceSection) {
226    // 画面に要素がどれくらい入ったら発火させるかの設定
227    const observerOptions = {
228      root: null, // ブラウザの画面全体を基準にする
229      rootMargin: "0px",
230      threshold: 0.2, // 要素が20%くらい画面に入ったら発火
231    };
232
233    const observer = new IntersectionObserver(function (entries, observer) {
234      entries.forEach((entry) => {
235        // 画面内に入ったらクラスを付与
236        if (entry.isIntersecting) {
237          entry.target.classList.add("is-visible");
238          observer.unobserve(entry.target); // 一度発火したら監視を終了する(チラつき防止)
239        }
240      });
241    }, observerOptions);
242
243    // 監視を開始
244    observer.observe(serviceSection);
245  }
246});
247
248// SHOWROOMページ menuのスクロール時の色変化
249document.addEventListener("DOMContentLoaded", () => {
250  // 判定対象のセクションを配列で取得
251  const targetSections = [
252    document.getElementById("kitchenSection"),
253    document.getElementById("bathroomSection"),
254    document.getElementById("toiletSection"),
255    document.getElementById("washroomSection"),
256  ].filter((el) => el !== null);
257
258  const sidebarNavLinks = document.querySelectorAll(".sidebarNavList a");
259
260  const changeSectionColorOnScroll = () => {
261    // 画面幅がPCサイズ(768px以上)の時だけメニュー色変更を実行
262    if (window.innerWidth >= 768) {
263      let currentSectionId = "";
264
265      // 固定ヘッダーの最大高さ(96px)に少し余裕を持たせた位置(180px)を判定のラインにする
266      const triggerPoint = 180;
267
268      targetSections.forEach((section) => {
269        // 画面最上部から各セクションの頭までの「リアルタイムの距離」を取得
270        const sectionRectTop = section.getBoundingClientRect().top;
271
272        // セクションの頭がトリガーラインより上(=ヘッダー付近までスクロールされた)なら
273        if (sectionRectTop <= triggerPoint) {
274          currentSectionId = section.getAttribute("id");
275        }
276      });
277
278      // 一旦すべてのメニューのアクティブクラスを消し、現在地だけに付与
279      sidebarNavLinks.forEach((link) => {
280        link.classList.remove("is-active");
281        if (link.getAttribute("href") === `#${currentSectionId}`) {
282          link.classList.add("is-active");
283        }
284      });
285    }
286
287    // 既存の全体用スクロール処理(section.active-color用)
288    const scrollPosition = window.scrollY;
289    const allSections = document.querySelectorAll("section");
290    allSections.forEach((section) => {
291      const sectionTop = section.offsetTop;
292      const sectionHeight = section.offsetHeight;
293      if (
294        scrollPosition + window.innerHeight - 200 > sectionTop &&
295        scrollPosition + 200 < sectionTop + sectionHeight
296      ) {
297        section.classList.add("active-color");
298      } else {
299        section.classList.remove("active-color");
300      }
301    });
302  };
303
304  window.addEventListener("scroll", changeSectionColorOnScroll);
305  changeSectionColorOnScroll(); // 初期読み込み時にも判定を実行
306
307  // --- 以下、既存の重なり検知処理(そのまま維持) ---
308  const sidebarNavListItems = document.querySelectorAll(".sidebarNavList li");
309  const fullWidthImage = document.querySelector(".fullWidthAspectRatioImage");
310
311  if (fullWidthImage) {
312    const checkOverlapForNav = () => {
313      const imageRect = fullWidthImage.getBoundingClientRect();
314      sidebarNavListItems.forEach((listItem) => {
315        const link = listItem.querySelector("a");
316        if (!link) return;
317        const linkRect = link.getBoundingClientRect();
318        const isOverlapping = !(
319          imageRect.right < linkRect.left ||
320          imageRect.left > linkRect.right ||
321          imageRect.bottom < linkRect.top ||
322          imageRect.top > linkRect.bottom
323        );
324        if (isOverlapping) {
325          link.classList.add("overlapped");
326        } else {
327          link.classList.remove("overlapped");
328        }
329      });
330    };
331    window.addEventListener("scroll", checkOverlapForNav);
332    checkOverlapForNav();
333  }
334});
335
336// Modal JS
337document.addEventListener("DOMContentLoaded", function () {
338  const modalId = "galleryImageModal";
339
340  // 1. モーダル要素を生成してbody末尾に追加
341  if (!document.getElementById(modalId)) {
342    document.body.insertAdjacentHTML(
343      "beforeend",
344      `
345            <div id="${modalId}" class="galleryImageModal">
346                <span class="modalClose">&times;</span>
347                <img class="modalContent" id="modalImageTarget">
348            </div>
349        `,
350    );
351  }
352
353  const modalElement = document.getElementById(modalId);
354  const modalImg = document.getElementById("modalImageTarget");
355  const closeBtn = modalElement.querySelector(".modalClose");
356
357  // フェードアウト処理
358  function closeModal() {
359    modalElement.classList.remove("modalVisible");
360  }
361
362  // 2. ギャラリー画像(modalTriggerクラス)を取得してクリックイベント登録
363  const triggers = document.querySelectorAll(".modalTrigger");
364
365  triggers.forEach((img) => {
366    img.addEventListener("click", function () {
367      modalImg.src = this.src;
368      modalImg.alt = this.alt;
369      modalElement.classList.add("modalVisible");
370    });
371  });
372
373  // 3. 閉じる操作の設定
374  if (closeBtn) {
375    closeBtn.addEventListener("click", closeModal);
376  }
377
378  // 背景クリックで閉じる
379  modalElement.addEventListener("click", function (event) {
380    if (event.target === modalElement) {
381      closeModal();
382    }
383  });
384
385  // Escキーで閉じる
386  document.addEventListener("keydown", function (event) {
387    if (
388      event.key === "Escape" &&
389      modalElement.classList.contains("modalVisible")
390    ) {
391      closeModal();
392    }
393  });
394});
395
396// スクロールアニメーションの共通設定
397document.addEventListener("DOMContentLoaded", () => {
398  // 監視対象にする要素のオプション設定
399  const animOptions = {
400    root: null, // ビューポート(画面)を基準にする
401    rootMargin: "0px 0px -10% 0px", // 画面の下端より10%内側に入った段階で発動
402    threshold: 0, // 1pxでも入ったらトリガー
403  };
404
405  // 交差したときに実行する関数
406  const animObserver = new IntersectionObserver((entries, observer) => {
407    entries.forEach((entry) => {
408      // 要素が画面内に入ってきた場合
409      if (entry.isIntersecting) {
410        entry.target.classList.add("isVisible"); // 出現クラスを付与
411        observer.unobserve(entry.target); // 一度発動したら監視を解除(負荷軽減)
412      }
413    });
414  }, animOptions);
415
416  // 『.scrollAnim』がついたタグをすべて集めて監視を開始
417  const animElements = document.querySelectorAll(".scrollAnim");
418  animElements.forEach((element) => {
419    animObserver.observe(element);
420  });
421});

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.