記事公開日

カルーセルな無限フォトギャラリー(JavaScript/CSS)

カルーセルで4枚の画像が永久にスライドするフォトギャラリー
お客さん

カルーセルな動きのフォトギャラリーをJavaScriptとCSSを使って作りたいの。

スマホのスワイプにも対応して欲しいし、現在の画像の位置を示す丸い玉も付けて欲しいし、次の画像や前の画像に移動できる矢印も付けて。

隊員1号

了解です!

インジケーターや矢印を使用しないときは、HTML上から削除するか、CSSで非表示にしてください。

また、HTML側でdata-img-numを確実に設定してください。

data-img-numは、ダミーを除いた使用する画像の枚数です。

このコードでは、最後の画像までスライダーが動くと、一瞬で最初の画像までスライダーを戻す作業を行うことで、無限ループを疑似的に表現しています。

よって、data-img-numが設定されていないと、うまく動きません。

なお、同じページにフォトギャラリーを複数混在させることもできます。

こちらをどうぞ↓

<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta name="robots" content="noindex">
  <title>画像カルーセル with インジケーター & ナビゲーション矢印</title>
  <style>
    html, body {
      width: 100vw;
      height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
      margin: 0;
      padding: 0;
    }
    /* 画像カルーセル */
    .carousel-wrapper {
      position: relative;
      width: 80%;
      height: auto;
      overflow: hidden;
    }
    .image-carousel {
      display: flex;
      flex-direction: row;
      width: auto;
      height: 100%;
      list-style: none;
      transition: transform 1.5s ease;
      padding: 0;
      margin: 0;
    }
    .image-carousel li {
      transform: translateX(calc(-50% - 2rem));
      flex-shrink: 0;
      flex-basis: 50%;
      position: relative;
      padding:0 1rem;
    }
    .image-carousel li img {
      width: 100%;
      height: auto;
    }
    .image-carousel li span {
      display: block;
      position: absolute;
      right: 1.5rem;
      bottom: 0.5rem;
      color: #fff;
    }
    /* ナビゲーション矢印 */
    .prev-button, .next-button {
      position: absolute;
      top: 50%;
      transform: translateY(-50%);
      background-color: rgba(0, 0, 0, 0.5);
      color: #fff;
      border: none;
      font-size: 1rem;
      padding: 0.5rem;
      cursor: pointer;
      z-index: 10;
    }
    .prev-button {
      left: 1rem;
    }
    .next-button {
      right:1rem;
    }
    .prev-button:hover, .next-button:hover {
      background-color: rgba(0, 0, 0, 0.7);
    }
    /* インジケーター */
    .carousel-indicators {
      position: absolute;
      bottom: 1rem;
      left: 50%;
      transform: translateX(-50%);
      display: flex;
      gap: 0.5rem;
    }
    .carousel-indicators span {
      width: 0.75rem;
      height: 0.75rem;
      border-radius: 50%;
      background: rgba(255, 255, 255, 0.5);
      cursor: pointer;
    }
    .carousel-indicators span.active {
      background: #fff;
    }
  </style>
</head>
<body>

  <div class="carousel-wrapper" data-img-num="4">
    <ul class="image-carousel">
      <li><img src="loop/4.jpg"></li><!-- ダミー -->
      <li><a href="loop/1.jpg"><img src="loop/1.jpg"></a></li>
      <li><a href="loop/2.jpg"><img src="loop/2.jpg"></a></li>
      <li><a href="loop/3.jpg"><img src="loop/3.jpg"></a></li>
      <li><a href="loop/4.jpg"><img src="loop/4.jpg"></a></li>
      <li><img src="loop/1.jpg"></li><!-- ダミー -->
      <li><img src="loop/2.jpg"></li><!-- ダミー -->
      <li><img src="loop/3.jpg"></li><!-- ダミー -->
    </ul>
    <!-- インジケーター -->
    <div class="carousel-indicators"></div>
    <!-- ナビゲーション矢印 -->
    <button class="prev-button">&#9664;</button>
    <button class="next-button">&#9654;</button>
  </div>

<script>
  // ページ内の全ての.carousel-wrapper要素に対して処理を行う
  document.querySelectorAll('.carousel-wrapper').forEach((wrapper) => {
    // スライド全体(画像リスト)の要素を取得
    const slideWrapper = wrapper.querySelector('.image-carousel');
    // 各スライド(li)要素を取得
    const slideElements = slideWrapper.querySelectorAll('li');

    // data-img-num属性から画像の枚数を取得
    const dataImgNum = wrapper.getAttribute('data-img-num');
    if (dataImgNum === null) {
      alert('Error: data-img-numが設定されていません');
      return; // data-img-numが設定されていなければ処理を中断
    }
    const imgNum = parseInt(dataImgNum);
    
    // 現在のスライドのインデックス、タッチ操作用の開始・終了位置、オートスライドのタイマーを初期化
    let slideIndex = 0;
    let startX = 0;
    let endX = 0;
    let autoSlide = setInterval(nextSlide, 1000); // 1秒ごとに次のスライドへ自動移動

    // インジケーター(下部の丸いドット)のコンテナを取得し、存在するか確認
    const indicatorsContainer = wrapper.querySelector('.carousel-indicators');
    const hasIndicators = indicatorsContainer !== null;

    // インジケーターがある場合、画像の枚数分だけドットを生成
    if (hasIndicators) {
      for (let i = 0; i < imgNum; i++) {
        const dot = document.createElement('span');
        // 最初のドットにactiveクラスを付与
        if (i === 0) dot.classList.add('active');
        // ドットがクリックされた時の処理
        dot.addEventListener('click', () => {
          stopAutoSlide();           // 自動スライドを停止
          slideIndex = i;            // クリックしたドットのインデックスに切り替え
          updateSlidePosition();     // スライドの位置を更新
          updateIndicators();        // インジケーターの状態を更新
        });
        // インジケーターコンテナにドットを追加
        indicatorsContainer.appendChild(dot);
      }
    }

    // インジケーターの表示状態を更新する関数
    function updateIndicators() {
      if (!hasIndicators) return; // インジケーターがない場合は何もしない
      // 現在のスライドインデックスをもとに表示すべきドットの位置を計算(ループに対応)
      const indicatorIndex = (slideIndex + imgNum) % imgNum;
      const dots = indicatorsContainer.querySelectorAll('span');
      dots.forEach((dot, i) => {
        // 現在のドットにactiveクラスを付与、その他からは除去
        dot.classList.toggle('active', i === indicatorIndex);
      });
    }

    // スライドの位置を更新する関数
    // 引数instantがtrueの場合は、アニメーションなしで位置を切り替える
    function updateSlidePosition(instant = false) {
      // 1つのスライドの幅を取得
      const slideSize = slideElements[0].offsetWidth;
      // トランジションの有無を切り替え
      slideWrapper.style.transition = instant ? 'none' : 'transform 0.5s ease-in-out';
      // translateXを使ってスライドを横方向に移動
      slideWrapper.style.transform = `translateX(${slideSize * slideIndex * -1}px)`;
    }

    // 次のスライドへ移動する関数
    function nextSlide() {
      // ループ処理:最後のスライドの次の場合の特殊処理
      if (slideIndex === imgNum) {
        slideIndex++;            // 一旦次へ移動
        updateSlidePosition();   // 通常アニメーションで移動
        updateIndicators();
        slideIndex = 0;          // インデックスをリセット
        updateSlidePosition(true); // 即時に元の位置へジャンプ(アニメーションなし)
        slideIndex = 1;          // 次のスライドへ移動するためにインデックスを更新
        // フレームをまたいで通常のアニメーションで移動させる
        requestAnimationFrame(() => {
          requestAnimationFrame(() => {
            updateSlidePosition();
            updateIndicators();
          });
        });
      } else {
        // 通常のスライド移動処理
        slideIndex++;
        updateSlidePosition();
        updateIndicators();
      }
    }

    // 前のスライドへ移動する関数(ループ対応)
    function prevSlide() {
      slideIndex = (slideIndex - 1 + slideElements.length) % slideElements.length;
      updateSlidePosition();
      updateIndicators();
    }

    // 自動スライドのタイマーを停止する関数
    function stopAutoSlide() {
      clearInterval(autoSlide);
      autoSlide = null;
    }

    // 前後のナビゲーションボタンを取得
    const prevButton = wrapper.querySelector('.prev-button');
    const nextButton = wrapper.querySelector('.next-button');

    // 次へボタンのクリックイベント処理
    if (nextButton) {
      nextButton.addEventListener('click', () => {
        // ループの境界に来た場合の特殊処理
        if (slideIndex === imgNum) {
          slideIndex++;
          updateSlidePosition();
          updateIndicators();
          slideIndex = 0;
          updateSlidePosition(true);
          slideIndex = 1;
          requestAnimationFrame(() => {
            requestAnimationFrame(() => {
              updateSlidePosition();
              updateIndicators();
            });
          });
        } else {
          stopAutoSlide(); // ボタン操作時は自動スライドを停止
          nextSlide();
        }
      });
    }

    // 前へボタンのクリックイベント処理
    if (prevButton) {
      prevButton.addEventListener('click', () => {
        // 先頭スライドの場合のループ処理
        if (slideIndex < 1) {
          slideIndex = imgNum;
          updateSlidePosition(true);
          requestAnimationFrame(() => {
            requestAnimationFrame(() => {
              updateSlidePosition();
              updateIndicators();
            });
          });
        }
        stopAutoSlide(); // ボタン操作時は自動スライドを停止
        prevSlide();
      });
    }

    // タッチ操作開始時の処理(スマホ等でのスワイプ対応)
    slideWrapper.addEventListener('touchstart', (e) => {
      // タッチ開始位置のX座標を記録
      startX = e.touches[0].clientX;
    });

    // タッチ操作終了時の処理
    slideWrapper.addEventListener('touchend', (e) => {
      // タッチ終了位置のX座標を記録
      endX = e.changedTouches[0].clientX;
      // スワイプ距離を計算
      let swipeDistance = endX - startX;
      // 右方向へのスワイプ(前のスライドへ)
      if (swipeDistance > 50) {
        if (slideIndex < 1) {
          slideIndex = imgNum;
          updateSlidePosition(true);
          requestAnimationFrame(() => {
            requestAnimationFrame(() => {
              updateSlidePosition();
              updateIndicators();
            });
          });
        }
        stopAutoSlide();
        prevSlide();
      } else if (swipeDistance < -50) {
        // 左方向へのスワイプ(次のスライドへ)
        stopAutoSlide();
        nextSlide();
      }
    });
  });
</script>
</body>
</html>

DEMO

この記事のURLをコピー

メールアドレスは公開されませんのでご安心ください。また、* が付いている欄は必須項目となります。

内容に問題なければ、下記の「コメントを送信する」ボタンを押してください。

関連情報

運営者プロフィール
運営者のプロフィール画像
隊員1号

IT系フリーランスとして10年の経験を持つレスキュー隊。HTML・CSS・JS・PHPなど幅広いスキルを持つ。

詳しいプロフィール