本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100
セクション

スクロールでセクションの奥・中・手前の図形が違う速さで流れる(パララックス)

デモ

操作:スクロール。 「さわってみる」を押すと自動再生が止まり、マウスで動かせます。 触っている間は、ホイールでデモの中がスクロールします。

仕様

動作
DOM
種類
レシピ
使うもの
ScrollTrigger
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

奥・中・手前の 3 つの層に、丸や角丸の四角を散らして重ねます。節を通り抜けるあいだ舞台を画面に止めておき、スクロールに合わせて各層を上へ流します。流れる距離を層ごとに変えると、どの層も同じスクロールの間に動くので、距離の違いがそのまま速さの違いになり、奥行きが生まれます。

流れる距離は、奥から画面の高さの 0.4 倍・0.9 倍・1.6 倍です。奥の層ほど図形を大きく、地に近い色(面の色)にし、手前の層ほど小さく、地との差が大きい色(文字の色)にしています。大きさ・色・速さの 3 つがそろって奥行きを示すので、速さの差だけのときより立体に見えます。中の層だけくすみローズにして、単調にならないようにしています。

節は画面の高さの 4 倍の長さにし、中の舞台を position: sticky で止めます。ScrollTrigger の範囲は、節の上端が画面の上端に着いてから下端が画面の下端に着くまでで、イーズを付けずにスクロール量へそのまま結びます(scrub)。流す距離は画面の高さから計算するので、invalidateOnRefresh を付けて、画面の大きさが変わったら測り直します。

図形は、縦に「画面の高さ ×(1 + 流れる距離)」の範囲まで散らしておきます。画面の中だけに置くと、流れたあとに下のほうが空になります。位置と大きさ(0.6〜1.4 倍)は読み込むたびに散らし直すので、決まった配置にしたいときは、乱数の代わりに固定の値を入れます。動きを減らす設定では流さず、節の高さも画面 1 つ分にします。

層ごとの図形の数・大きさ・流れる距離は HTML の属性で、色と節の長さは CSS の変数で変えられます。節を長くするほど、ゆっくり流れます。カーソルで層がずれる形や、画像の列が違う速さで流れる形と違い、図形だけの層で奥行きを見せる、間奏のような節です。章の区切りや、ブランドの世界観を見せる場面に向いています。

コード

HTML
<!-- 図形だけの飾りの節なので、読み上げからは外す -->
<div class="parallax" aria-hidden="true">
  <div class="parallax__stage">
    <!--
      層は奥から手前の順に置く。図形はスクリプトが層ごとに散らして置く。
      data-count は図形の数、data-size は大きさ(画面の短い辺に対する比)、
      data-travel は節を通り抜ける間に流れる距離(画面の高さに対する比)。手前ほど大きくすると速く流れる
    -->
    <div class="parallax__layer is-far" data-count="10" data-size="0.34" data-travel="0.4"></div>
    <div class="parallax__layer is-mid" data-count="14" data-size="0.15" data-travel="0.9"></div>
    <div class="parallax__layer is-near" data-count="18" data-size="0.06" data-travel="1.6"></div>
  </div>
</div>

CSS
.parallax {
  --bg: #111111;
  --far: #262626; /* 奥の層。地に近い色で沈める */
  --mid: #d29b97; /* 中の層。中身の色(くすみローズ)で単調にしない */
  --near: #eeece6; /* 手前の層。地との差が大きい色で浮かせる */
  --length: 400vh; /* 節の長さ。長いほど、ゆっくり流れる */
  height: var(--length);
  background-color: var(--bg);
}

/* 節を通り抜ける間、画面に止めておく舞台 */
.parallax__stage {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  overflow: hidden;
}

.parallax__layer { position: absolute; inset: 0; }
.parallax__layer.is-far { --color: var(--far); }
.parallax__layer.is-mid { --color: var(--mid); }
.parallax__layer.is-near { --color: var(--near); }

/* 位置と大きさは、スクリプトが図形ごとに --x・--y・--size で入れる。中心をその位置に合わせる */
.parallax__shape {
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: var(--size);
  aspect-ratio: 1;
  translate: -50% -50%;
  border-radius: 50%;
  background-color: var(--color);
}

.parallax__layer.is-near .parallax__shape { border-radius: 22%; }

/* 動きを減らす設定では流さないので、節も画面 1 つ分の高さにする */
@media (prefers-reduced-motion: reduce) {
  .parallax { height: 100vh; height: 100svh; }
}

JavaScript
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger);

const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

document.querySelectorAll(".parallax").forEach((section) => {
  const stage = section.querySelector(".parallax__stage");
  const layers = [...section.querySelectorAll(".parallax__layer")];

  // 図形を層ごとに散らす。大きさは 0.6〜1.4 倍にばらし、縦は流れる距離の分だけ下まで散らす(流れても、下から次が入ってくる)
  for (const layer of layers) {
    const size = Number(layer.dataset.size);
    const travel = Number(layer.dataset.travel);
    for (let i = 0; i < Number(layer.dataset.count); i++) {
      const shape = document.createElement("span");
      shape.className = "parallax__shape";
      shape.style.setProperty("--size", `${(size * 100 * gsap.utils.random(0.6, 1.4)).toFixed(2)}vmin`);
      shape.style.setProperty("--x", `${gsap.utils.random(0, 100).toFixed(2)}%`);
      shape.style.setProperty("--y", `${gsap.utils.random(0, 100 + travel * 100).toFixed(2)}%`);
      layer.append(shape);
    }
  }

  // 動きを減らす設定では、散らした図形を止めたまま見せる
  if (reduceMotion) return;

  // 節の上端が画面の上端に着いてから、下端が画面の下端に着くまで(舞台が止まっている間)に流す
  const timeline = gsap.timeline({
    scrollTrigger: {
      trigger: section,
      start: "top top",
      end: "bottom bottom",
      scrub: true, // スクロール量にそのまま連動させる(数値の scrub は実時間で追うので使わない)
      invalidateOnRefresh: true, // 画面の高さが変わったら、流す距離を測り直す
    },
  });
  // どの層も同じスクロールの間に動くので、流れる距離の違いがそのまま速さの違いになる
  for (const layer of layers) {
    const travel = Number(layer.dataset.travel);
    timeline.fromTo(layer, { y: 0 }, { y: () => -stage.offsetHeight * travel, ease: "none" }, 0);
  }
});

インタラクションデザインについて、
お気軽にご相談ください。

ここに並べているのは、Web サイトの中で動く UI のサンプルです。新しく作るサイトに組み込むことも、いま公開中のサイトに、ボタンやヒーロー、スクロールの演出だけを足すこともできます。設計から実装までの進め方はWeb サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。