本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100

セクション

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

デモ

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

仕様

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

しくみと調整

スクロールに合わせて、大小の丸や角丸の四角が上へ流れていきます。手前のものほど速く、奥のものほどゆっくり動き、そのあいだセクションは画面に止まったままです。図形は奥・中・手前の層に分けて散らしてあり、層ごとの動きで変えているのは流れる距離だけです。どの層も同じスクロールのあいだに動くので、距離の違いがそのまま速さの違いとして見えます。距離は奥から、画面の高さの0.4倍・0.9倍・1.6倍です。

奥行きは速さだけに任せず、大きさと色にも受け持たせました。奥の層ほど図形を大きく、背景色に近い色にし、手前の層ほど小さく、背景色との差が大きい色にしています。大きさ・色・速さが同じ方向を指すので、速さの差だけのときより立体的に見えます。中の層だけをローズカラーにしたのは、単調さを避けるためです。

表示部分を止めているのは position: sticky です。セクションを画面の高さの4倍にして、上端が画面の上端に着いてから下端が画面の下端に着くまでを、イージングなしの scrub でスクロール量に連動させています。流す距離は画面の高さから計算するので、invalidateOnRefresh で大きさが変わるたびに測り直します。

図形を置く範囲は、画面の中だけでは足りません。それでは層が上へ流れたあと、下のほうが空になります。縦は「画面の高さ ×(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 サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。