GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

パーティクル 背景 9選 — スクロール値で流れが変わるキャンバスシーン

パーティクル 背景: 9つのうち1つはスクロール速度の値が速くなるほどパーティクルが生成される頻度の値が増え、1つはパーティクルごとの奥行きの値によって同じスクロール移動量が異なる距離で適用され、もう1つはスクロール位置の値がある地点に近づくほどその場の渦の吸引力の値が大きくなります。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

9つとも通信を一切行わず、localStorageへの書き込みもなく、Math.randomも使いません。読み取る入力はスクロールの値(位置・速度・方向のいずれか)一つだけで、各シーンでそれに応えるパーティクルの性質もいつも一つだけです——上昇速度の値か、生成頻度の値か、レーンの方向の値か、奥行きのオフセット値か、ばねの目標値か、バーストの個数の値か、色相の値か、吸引半径の値か、ノイズのオフセット値かのいずれかです。この範囲の狭さのおかげで、パーティクル 背景 素材として使う9つのシーンのどれが目的のセクションに合うか、実際に動く性質の名前を知るだけでほぼ判断でき、下のコードは仕組みを裏付けるだけで済みます。また9つともctx.clearRect()だけで前フレームを消し、背景を塗りつぶすことはないため、パーティクル 背景透過のまま下のセクションに重ねて使えます。

9つのパターンの解剖 — どの値がスクロールに反応するか

01番のstep()はスクロール速度をWIND_GAINで拡大した値とFIELD_FREQのサイン場でRISE_SPEEDの値を横に曲げながらパーティクルを上昇させます。02番のstep()はスクロール速度をSPAWN_GAINによる生成率に変換し、各パーティクルは発射後FRICTIONの値で減速します。03番のstep()はlaneSignとdirSignの組み合わせを読み取り、スクロール方向が反転した瞬間にレーンごとのLANE_SPEEDでの流れの向きを反転させます。04番のproject()はFOVと奥行きの割り算をDOLLY_RANGEで拡大したcameraZ()にかけて処理し、同じスクロール距離でも浅いパーティクルほど大きく、深いパーティクルほど小さく動かします。05番のstep()はスクロールが止まるとSTIFFNESSのばねをSETTLE_DAMPの値で減衰させながら静止値へ戻し、idleMsの値が止まっている時間を数えます。06番のsampleScroll()はスクロール速度の値がFLICK_THRESHOLDを下から上へ超える立ち上がりを監視してburst()を呼び出します。07番のbandColor()はスクロール進行率の値が区切りを超えるたびにBAND_A・BAND_B・BAND_Cの3色帯を補間します。08番のstep()はスクロール位置の値が地点に近づくほどREST_SHRINKの値で静止半径を縮め、その半径(下限8px)でVORTEX_Kを割った角速度で内側へ回転させます。09番のstep()は自作のvalueNoise()——256個の要素を持つ順列テーブルをハッシュして滑らかにした値であってMath.randomの呼び出しではありません——をSCROLL_WARPとNOISE_SCALEでスクロールの値だけ押し流して読み取ります。9つとも自分のスクロールコンテナに対してscrollとresizeを監視し、wheel・touchstart・pointerdownのいずれかが来た瞬間を実際の訪問者と見なしてhuman()を呼び、.is-demoの自動再生を完全に止めます。それまでの間、同じ自動再生がdriveTick()という同一の形の三角波でscrollTopの値を動かします。自動再生も各シーン自身のstep()も、タイマーではなくすべてrequestAnimationFrameを通じて動き、Math.randomは9つのどこにも使われていません——代わりにそれぞれが小さな線形合同法の乱数生成器を一つだけ種として持ち、同じ種なら常に同じ初期配置になります。prefers-reduced-motion: reduceの下では、9つともそのフレームごとのループを完全に飛ばしてpaintStatic()を一度だけ呼び出すので、モーション低減を求める訪問者にも空のキャンバスではなく静止した配置が見えます。

パターン別コード

01基本フロー場 — スクロールの値がなくてもパーティクルが基準速度の値で静かに上へ流れる背景の流れ

スクロールの値が0でもパーティクルは基準速度の値で上へ流れ続け、スクロールの値が生まれるとその流れの上に他の値が重なります。WIND_GAINで拡大したスクロール速度の値が場全体を横へ押し、FIELD_FREQのサイン場がパーティクルごとの揺れを加えるので、9つ別々の流れではなく一つの穏やかな流れとして見えます——ヒーローセクションや静かなローディング背景に向いています。

基本フロー場 — スクロールの値がなくてもパーティクルが基準速度の値で静かに上へ流れる背景の流れ
新しいタブで開く

01 · 基本フロー場 — スクロールの値がなくてもパーティクルが基準速度の値で静かに上へ流れる背景の流れ — 計測 累積面積 15.706% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

RISE_SPEEDWIND_GAINFIELD_FREQ
  function step(dtSec) {
    var wind = scrollVel * WIND_GAIN;
    for (var i = 0; i < particles.length; i++) {
      var p = particles[i];
      p.y -= RISE_SPEED * p.speedVar * dtSec;
      var sway = Math.sin(p.y * FIELD_FREQ + p.phase) * SWAY_AMP;
      p.x += (wind + sway) * dtSec;
      if (p.y < -8) spawnAt(p, false);
      if (p.x < -8) p.x = W + 8; else if (p.x > W + 8) p.x = -8;
    }
  }

数値が語ること

01番はフレームごとに累積面積の15.71%を平均強度88.9で占め、23フレーム中23フレームで動きます——9つの中で最も高い累積面積です。静止したスクロールの間も上昇の流れが止まらず、サンプリングしたほぼ全てのフレームで再描画が続くからです。

02速度連動生成 — スクロール速度の値が速くなるほど新しく生成されるパーティクルの頻度の値が増える方式

スクロール速度の値が速くなるほど1フレームで生成されるパーティクル数の値が増え、止まると生成頻度の値は基準値に戻ります。SPAWN_GAINが速度への反応の鋭さを決め、LAUNCH_GAINが新しいパーティクルの初速を決め、FRICTIONがその初速を発射後に減衰させます——スクロール中だけ賑やかに見せたいヒーローバナーに向いています。

速度連動生成 — スクロール速度の値が速くなるほど新しく生成されるパーティクルの頻度の値が増える方式
新しいタブで開く

02 · 速度連動生成 — スクロール速度の値が速くなるほど新しく生成されるパーティクルの頻度の値が増える方式 — 計測 累積面積 8.292% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SPAWN_GAINLAUNCH_GAINFRICTION
  function step(now, dtSec) {
    var rate = Math.min(140, Math.abs(scrollVel) * SPAWN_GAIN);
    spawnAcc += rate * dtSec;
    while (spawnAcc >= 1) { spawnOne(now); spawnAcc -= 1; }

    var decay = Math.exp(-FRICTION * dtSec);
    for (var i = particles.length - 1; i >= 0; i--) {
      var p = particles[i];
      p.vx *= decay; p.vy *= decay;
      p.x += p.vx * dtSec;
      p.y += p.vy * dtSec;
      if (now - p.born > LIFE_MS) particles.splice(i, 1);
    }
  }

数値が語ること

02番は累積面積の8.29%を平均強度85.1で占め、23フレーム中23フレームで動きます。生成のたびにLIFE_MSを超えたパーティクルも同時に取り除かれるので、面積は増え続けるのではなくスクロール速度に合わせて上下します。

03方向反転 — スクロール方向の値が下から上に変わるとパーティクルの流れの方向の値も即座に逆になる方式

スクロール方向の値が上に変わった瞬間、パーティクルが流れていた方向の値が反転し、同じ場所でも上下で流れが変わります。laneSignとdirSignの組み合わせが各レーンの流れる向きを決め、LANE_SPEEDで速度を保ったまま、方向が反転すると全レーンが一斉に切り替わります。

方向反転 — スクロール方向の値が下から上に変わるとパーティクルの流れの方向の値も即座に逆になる方式
新しいタブで開く

03 · 方向反転 — スクロール方向の値が下から上に変わるとパーティクルの流れの方向の値も即座に逆になる方式 — 計測 累積面積 13.023% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

laneSigndirSignLANE_SPEED
  function step(dtSec) {
    for (var i = 0; i < particles.length; i++) {
      var p = particles[i];
      p.y -= RISE_SPEED * dtSec;
      p.x += laneSign(p.lane) * dirSign * LANE_SPEED * dtSec;
      var laneMin = p.lane / LANES * W, laneMax = (p.lane + 1) / LANES * W;
      if (p.x < laneMin - 6) p.x = laneMin - 6; else if (p.x > laneMax + 6) p.x = laneMax + 6;
      if (p.y < -8) spawnAt(p, false);
    }
  }

数値が語ること

03番は累積面積の13.02%を平均強度101.9で占め、23フレーム中23フレームで動きます。方向反転が起きた瞬間、そのレーンの全パーティクルが一度に再描画されるためです——端に近いものだけではありません。

04奥行き遠近レイヤー — パーティクルごとに異なる奥行きの値を持ち、同じスクロール移動量が奥行きの値によって異なる距離で適用される方式

パーティクルごとに奥行きの値が異なるため、同じスクロール移動量でも奥行きの値が浅いパーティクルは大きく、深いパーティクルは小さく動きます。FOVと奥行きの割り算がその差を画面上へ投影し、DOLLY_RANGEがcameraZ()自身の移動量を決めるので、ページを少し動かすだけで奥行きの層全体が本物のカメラドリーのように動きます。

奥行き遠近レイヤー — パーティクルごとに異なる奥行きの値を持ち、同じスクロール移動量が奥行きの値によって異なる距離で適用される方式
新しいタブで開く

04 · 奥行き遠近レイヤー — パーティクルごとに異なる奥行きの値を持ち、同じスクロール移動量が奥行きの値によって異なる距離で適用される方式 — 計測 累積面積 13.938% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

FOVDOLLY_RANGEcameraZ
  function cameraZ() { return scrollPos * DOLLY_RANGE; }

  function project(p, camZ) {
    var depth = p.z - camZ;
    var scale = FOV / (FOV + Math.max(depth, NEAR));
    return { sx: W / 2 + p.wx * scale, sy: H / 2 + p.wy * scale, scale: scale, depth: depth };
  }

  function step(dtSec) {
    var camZ = cameraZ();
    for (var i = 0; i < particles.length; i++) {
      var p = particles[i];
      if (p.z - camZ < NEAR) spawnAt(p, false); // カメラを通り過ぎた粒子は最も遠い位置へ再配置する
    }
  }

数値が語ること

04番は累積面積の13.94%を平均強度85.8で占め、23フレーム中23フレームで動きます。浅い奥行きのパーティクルほど投影された大きさの変化が広い面積を塗り直すため、同じワールド距離を動く深いパーティクルより面積が大きくなるからです。

05停止沈降 — スクロールが止まった瞬間、パーティクルが重力の値の影響を受けて徐々に下へ沈む方式

スクロールの値が止まるとパーティクルの上昇の流れの値が消え、代わりに重力の値が働き、画面が止まっている間パーティクルは少しずつ沈みます。STIFFNESSのばねがSETTLE_DAMPの値で減衰しながら静止値へ戻り、idleMsがスクロールの静けさが続いた時間を数えます。

停止沈降 — スクロールが止まった瞬間、パーティクルが重力の値の影響を受けて徐々に下へ沈む方式
新しいタブで開く

05 · 停止沈降 — スクロールが止まった瞬間、パーティクルが重力の値の影響を受けて徐々に下へ沈む方式 — 計測 累積面積 8.342% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

STIFFNESSSETTLE_DAMPtriangleHold
  function step(dtSec) {
    var target = scrollVel * DRIVE_GAIN;
    for (var i = 0; i < dots.length; i++) {
      var d = dots[i];
      var acc = (target - d.dispX) * d.stiff - d.velX * SETTLE_DAMP;
      d.velX += acc * dtSec;
      d.dispX += d.velX * dtSec;
    }
  }

数値が語ること

05番は累積面積の8.34%を平均強度106.9で占め、23フレーム中23フレームで動きます。スクロールが止まった後もばねが短い間静止値へ戻り続けるため、最後のスクロールイベントより後まで動きが残るからです。

06フリックバースト — 短時間でスクロールの値が大きく跳ねると、その地点でパーティクルが瞬間的に飛び散る方式

短い時間でスクロールの値が大きく跳ねると、その位置でパーティクル数の値が瞬間的に増え四方へ散ります。スクロール速度の値がFLICK_THRESHOLDを下から上へ超えた立ち上がりでburst()が呼ばれ、その地点からBURST_COUNT(40個)のパーティクルが一度に散らばります。

フリックバースト — 短時間でスクロールの値が大きく跳ねると、その地点でパーティクルが瞬間的に飛び散る方式
新しいタブで開く

06 · フリックバースト — 短時間でスクロールの値が大きく跳ねると、その地点でパーティクルが瞬間的に飛び散る方式 — 計測 累積面積 3.719% · 動いたフレーム 12/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

FLICK_THRESHOLDBURST_COUNTflickWave
  function sampleScroll(now) {
    var top = viewport.scrollTop;
    var max = Math.max(1, viewport.scrollHeight - viewport.clientHeight);
    scrollPos = top / max;
    var dt = lastT ? Math.max(1, now - lastT) : 16;
    var v = (top - lastTop) / dt * 1000;
    scrollVel += (v - scrollVel) * 0.4;
    if (Math.abs(scrollVel) > 4) { scrollDir = scrollVel > 0 ? 1 : -1; idleMs = 0; } else { idleMs += dt; }
    lastTop = top; lastT = now;

    var absVel = Math.abs(scrollVel);
    if (prevAbsVel < FLICK_THRESHOLD && absVel >= FLICK_THRESHOLD) burst(now);
    prevAbsVel = absVel;
  }

数値が語ること

06番は累積面積のわずか3.72%を平均強度69.5で占め、23フレーム中12フレームでしか動きません——9つの中で最も低い累積面積、最も少ない動いたフレーム数です。バーストはFLICK_THRESHOLDを実際に超えている立ち上がりの瞬間しか描画されず、それ以外の時間は場が静止しているからです。

07区間別色相転換 — スクロール進行率の値が区間を超えるたびにパーティクルの色の値が次の区間の色に変わる方式

スクロール進行率の値が定めた区間を超えるたびに、パーティクル全体の色の値がその区間に割り当てられた色へ滑らかに変わります。bandColor()が3色の帯を補間し、BOB_FREQとBOB_AMPがパーティクルごとの小さな上下運動を加えるので、色が変わらない間も場が完全には静止しません。

区間別色相転換 — スクロール進行率の値が区間を超えるたびにパーティクルの色の値が次の区間の色に変わる方式
新しいタブで開く

07 · 区間別色相転換 — スクロール進行率の値が区間を超えるたびにパーティクルの色の値が次の区間の色に変わる方式 — 計測 累積面積 7.906% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

bandColorBOB_FREQBOB_AMP
  function lerpRGB(c1, c2, t) {
    return { r: c1.r + (c2.r - c1.r) * t, g: c1.g + (c2.g - c1.g) * t, b: c1.b + (c2.b - c1.b) * t };
  }
  function bandColor(t) {
    t = ((t % 1) + 1) % 1;
    var c = t < 0.5 ? lerpRGB(BAND_A, BAND_B, t * 2) : lerpRGB(BAND_B, BAND_C, (t - 0.5) * 2);
    return 'rgb(' + (c.r | 0) + ',' + (c.g | 0) + ',' + (c.b | 0) + ')';
  }

数値が語ること

07番は累積面積の7.91%を平均強度124.4で占め、23フレーム中23フレームで動きます。色の補間が位置の変化がわずかな時でも毎フレーム全パーティクルの塗り色を塗り直すからです。

08渦の吸引 — スクロール位置の値が特定の地点に近づくほど、その地点の渦の吸引力の値が大きくなる方式

スクロール位置の値が指定した地点に近づくほどその地点の渦の吸引力の値が大きくなり、パーティクルはより大きく渦を描いて引き込まれます。REST_SHRINKがスクロール位置が地点に近づくほど静止半径を縮め、PULL_RATEが実際の半径がその縮む目標へ近づく速さを決め、VORTEX_Kをその半径(下限8px)で割った値が引き込まれた後の回転の速さを決めます。

渦の吸引 — スクロール位置の値が特定の地点に近づくほど、その地点の渦の吸引力の値が大きくなる方式
新しいタブで開く

08 · 渦の吸引 — スクロール位置の値が特定の地点に近づくほど、その地点の渦の吸引力の値が大きくなる方式 — 計測 累積面積 7.172% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

VORTEX_KPULL_RATEREST_SHRINK
  function step(dtSec) {
    var target = restR * (1 - scrollPos * REST_SHRINK);
    for (var i = 0; i < particles.length; i++) {
      var p = particles[i];
      p.r += (target - p.r) * PULL_RATE * dtSec;
      var angVel = VORTEX_K / Math.max(p.r, 8);
      p.theta += angVel * dtSec * 0.01 * spinDir;
    }
  }

数値が語ること

08番は累積面積の7.17%を平均強度104.2で占め、23フレーム中23フレームで動きます。渦自体の半径変化が全パーティクルに同時に影響する一方、回転運動だけならもっと狭い輪しか塗り直さないからです。

09ノイズフロー歪み — スクロールの値が2Dノイズ場のオフセットの値を押し流し、パーティクルの経路が滑らかに曲がる流れに変わる方式

スクロールの値が増えるほどノイズ場のオフセットの値が移動し、同じ場所のパーティクル経路も時間とともに滑らかに異なる方向へ曲がります。自作のvalueNoise()がNOISE_SCALEの大きさの場をSCROLL_WARPでスクロールの値だけ押し流して読み取るので、曲がり方は急な一粒ごとのジャンプではなく滑らかで連続的です。

ノイズフロー歪み — スクロールの値が2Dノイズ場のオフセットの値を押し流し、パーティクルの経路が滑らかに曲がる流れに変わる方式
新しいタブで開く

09 · ノイズフロー歪み — スクロールの値が2Dノイズ場のオフセットの値を押し流し、パーティクルの経路が滑らかに曲がる流れに変わる方式 — 計測 累積面積 13.367% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

valueNoiseSCROLL_WARPNOISE_SCALE
  var PERM_SIZE = 256;
  var perm = (function () {
    var p = [];
    for (var i = 0; i < PERM_SIZE; i++) p.push(i);
    for (var j = PERM_SIZE - 1; j > 0; j--) {
      var k = Math.floor(rng() * (j + 1));
      var t = p[j]; p[j] = p[k]; p[k] = t;
    }
    return p.concat(p);
  })();
  function hash2(ix, iy) {
    var a = perm[(ix & 255)];
    return perm[(a + iy) & 255] / 255;
  }
  function smooth(t) { return t * t * (3 - 2 * t); }
  function valueNoise(x, y) {
    var x0 = Math.floor(x), y0 = Math.floor(y);
    var fx = smooth(x - x0), fy = smooth(y - y0);
    var v00 = hash2(x0, y0), v10 = hash2(x0 + 1, y0);
    var v01 = hash2(x0, y0 + 1), v11 = hash2(x0 + 1, y0 + 1);
    var a = v00 + (v10 - v00) * fx;
    var b = v01 + (v11 - v01) * fx;
    return a + (b - a) * fy;
  }

数値が語ること

09番は累積面積の13.37%を占め、9つの中で最も高い平均強度125.8で、23フレーム中23フレームで動きます。ノイズ場がフレームごとにパーティクルごと少しずつ異なる方向の値を与え、まとまったパーティクルの塊を一斉に同じように動かすわけではないからです。

値が位置と色を変える基準 — 速度値・奥行き値・位置値

パーティクル 背景 フリーの素材でもたいてい一様な流れになりますが、この9つのうち3つは自分のパーティクルの性質をそれぞれ異なるスクロールの基準に結びつけています。02番の生成率はスクロール速度の値に直接応えます——SPAWN_GAINは今どれだけ速く動いているかだけを読み取り、どれだけ移動したかは見ないので、遅く一定にスクロールすれば一定に生成され、速いフリックならフリックが終わった瞬間に収まるバーストで生成されます。04番の奥行きレイヤーはスクロール距離の値に応えますが均一ではありません——FOVと奥行きの割り算が同じスクロール距離でもパーティクルの投影位置の値をどれだけ動かすかを変えるので、浅いパーティクルはキャンバスを横切るほど動き、深いパーティクルは同じスクロール量でもほとんど動きません。08番の渦の吸引はそれとは対照的にスクロール位置の値だけに応えます——REST_SHRINKは現在のスクロール位置がある地点にどれだけ近いかだけを気にするので、その地点まで素早くスクロールしても、ゆっくりスクロールして止まっても、同じ位置なら同じ吸引力になります。この3つの定数はどのシーンに置かれても自分の基準を離れません——02番の生成率は速度に、04番の投影は奥行きに、08番の吸引は位置に応え続け、周りの数値だけが変わります。

この速度・奥行き・位置の違いは、こうしたシーンがこのページを離れて実際のセクションで使われるときに最も意味を持ちます。スクロール トリガー アニメーション 9選 — GSAPなしのバニラJSではスクロールの閾値を一度超えた瞬間に一つのアニメーションを発火させるだけで、上の9つのシーンのようにフレームごとにスクロール速度や位置の値を読み続けるわけではありません。スクロール 慣性 css 9選 — 値が自ら収まっていく9つの計算パターンはスクロールが止まると一つのUIの値を静止値へ戻すだけで、ここの05番と同じ停止沈降の考え方を一つのパーティクル群全体ではなく一つの値だけに適用します。スクロールテリング 作り方 9選 — コピペOKのCSSはセクション間をスクロールが移動するたびにセクション全体のレイアウトの値を切り替えるだけで、この9つのシーンのように一つのパーティクルの性質を連続的に変えるわけではありません。zipはx3fyh6j2で一度開けば、この9つのシーン全部がまとめて使えるようになり、項目ごとに別のパスワードを探す必要はありません。

どこで壊れるか — 速いスクロール、タブ切り替え、reduced-motion

実際のスクロールはデモのループよりずっと乱れており、この9つのシーンではそれが3つの異なる場所で表れます。

FLICK_THRESHOLDを一度も下回らない速いスクロール

06番のsampleScroll()はburst()を立ち上がりのときだけ呼びます——scrollVelの絶対値が下からFLICK_THRESHOLDを超えた瞬間だけです。すでにその閾値を超えた状態で始まり、一度も下回らないまま続く速いスクロールは、最初に超えた瞬間だけ一度バーストして、それ以降は起きません。区間全体を速くスクロールし続けても、その最初の一発の後は不思議なほど静かに見えることがあります。

タブが隠れている間にシーンへ戻ったとき

どのシーンの自動再生もdriveTick()自身の三角波からviewport.scrollTopを動かしており、その波の位相はrequestAnimationFrameが渡す実際のタイムスタンプを使ったnow - driveStartで計算されます。タブが隠れている間requestAnimationFrame自体が止まる一方で、波の位相は実時間のまま裏で進み続けるので、タブへ戻った瞬間に見える仮想スクロール位置は、シーンが視覚的に止まっていた場所ではなく、2秒周期のどこか別の位置になっている可能性があります。

reduced-motionでも読み込み時のスクロール位置がそのまま反映される

paintStatic()はスクロールの値を0だと仮定せず、まずsampleScroll()を一度呼んでから静止した配置を描くので、その静止フレームはページが読み込まれた時点のスクロール位置をそのまま反映します。prefers-reduced-motion: reduceを求める訪問者が長いページの途中で08番を開くと、渦はすでにそのスクロール位置が示す半径まで引き込まれた状態で見え、最も緩んだ静止状態の渦ではありません。

3つのバリエーションとアクセシビリティ

フロー速度を0.6→0.25に下げるとノイズ場のオフセットの値がゆっくり押し流され、パーティクルの経路が急に曲がらず緩やかに流れます。減衰を22→10に下げると、パーティクルが着地値で即停止せず少し行き過ぎてから戻るオーバーシュートが残り、沈降がより弾むように見えます。個数を40→90に増やすと、フリック時に飛び散るパーティクルの値がずっと密になり、画面が一瞬より密度高く見えます。

バリエーション 変わるもの 変わらないもの
ノイズフローを緩やかに FLOW_SPEED 0.6 → 0.25 valueNoise()の順列テーブル・NOISE_SCALE・スクロールからオフセットへの変換式そのもの
沈降をより弾ませる SETTLE_DAMP 22 → 10 STIFFNESS・idleMsのタイマー・どのパーティクルがどのばねに属するか
フリックバーストを密に BURST_COUNT 40 → 90 FLICK_THRESHOLD・立ち上がりの検出方法・burst()自身の発生地点

9つとも自分の<canvas>にaria-hidden="true"を付けています。シーンは装飾であって情報そのものではないからです。そのうえで、今どの値が何をしているかを平易な言葉で説明する目に見えるヒントのspanを一緒に配置し、その説明をキャンバスのピクセルの中だけに閉じ込めません。各スクロールコンテナ自体もrole="region"と、中で何がスクロールしているかを示す自分のaria-labelを持つので、訪問者がキャンバスと関わる前にスクリーンリーダーがその領域を読み上げます。

FAQ

このスクロール パーティクル 背景 cssのセットは9つの別々のスクリプトですか、それとも一つの共通コンポーネントの設定違いですか

9つの独立したファイルです——各シーンが自分の定数と自分のstep関数を持ち、デモの上に示したコードフェンスは同じファイルから直接切り出した抜粋であって、一つのテンプレートに数値だけを差し替える共通の生成処理の出力ではありません。

09番の流れるノイズは本物のPerlinノイズのライブラリを使っていますか、それとも自作ですか

自作です——他のどのシーンとも同じ種を使う乱数生成器で一度だけシャッフルした256個の要素の順列テーブルを、hash2()とsmooth()の補間で連続的な場に変えており、外部のノイズライブラリもCDNも使っていません。

スクロールに反応するのに、なぜ各キャンバスにaria-hiddenを付けているのですか

キャンバス自体は、その横にある目に見えるヒントのテキストにすでに書かれている以上の情報を訪問者に与えないからです。キャンバスを支援技術から隠すことで、絶えず変わるピクセルの内容を読み上げ続けることを避け、代わりにヒントのspanが同じ内容を平易な言葉で伝えます。

Sources: MDN — Canvas API · MDN — CanvasRenderingContext2D · MDN — Window.requestAnimationFrame() · MDN — Document: scroll event

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。