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

キャンバス 海 アニメーション シーン9選 — 重ねた正弦波で波の場を計算する

海 アニメーションは写真やGIFを繰り返し再生する画面ではなく、格子のマスごとに波の高さをリアルタイムで計算し直して描く画面です。重ねた正弦波で波の場を計算するという原理ひとつの上に、この9つのシーンは水色・泡・反射光・渦へと枝分かれしていきます。

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

なぜこの順番か

最初のシーンが本体です。波長・方向・振幅の異なる正弦波6つを合成して水面グリッドをひとつ計算し、残り8つはすべてそのグリッドの上に派生値をもう一段重ねた結果です。水色を水深として読めば02、高さが閾値を超えるマスだけ選べば03、勾配と太陽ベクトルを内積すれば04になります。05は03の泡を座標ではなくグリッドセルで追跡し、06は同じ勾配を屈折オフセットとして使い、07・08はそのグリッド自体に局所回転と全体エンベロープを重ねて渦と押し寄せる波を作ります。09は最後に距離に応じて振幅と色を抑え、遠景を霞に沈めます。

本体はひとつ、派生は8つ

01のhgt配列(マスごとに計算した高さ)とfieldcalc関数は、9項目のほぼすべてでそのまま使い回されます。変わるのはその高さを読んで何を描くかだけです — 色なら水面、閾値なら泡、勾配なら反射光になります。

近景から遠景へ — 計算が積み上がる順番

01〜06は画面全体が同じ視点から見た水面で、07〜08はその水面の上に局所的な出来事(渦・押し寄せる波)を重ね、09は最後にカメラを引いて水平線まで収めます。素材を選ぶときもこの順番をそのまま辿ると、計算をひとつ覚えるたびに次のシーンが自然に解けていきます。

01正弦波合成の水面 — ガーストナー波の場

波長・方向・振幅の異なる正弦波6つを重ねて水面グリッドに合成し、波頂が前にずれるガーストナーのずれまで入れた、リゾート・クルーズLPのヒーロー向け基本シーンです(実測: 累積面積60.795%、23/23フレームで動きあり)。

正弦波合成の水面 — ガーストナー波の場
新しいタブで開く

01 · 正弦波合成の水面 — ガーストナー波の場 — 計測 累積面積 60.795% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ImageDataupscalerequestAnimationFrame

高さはサイン、ずれはコサインでまとめて計算する

function fieldcalc(t, wstr, wang) {
  var i = 0;
  for (var y = 0; y < ROWS; y++) {
    for (var x = 0; x < COLS; x++, i++) {
      var h = 0, d = 0;
      for (var w2 = 0; w2 < NW; w2++) {
        var WV = WAVES[w2];
        var da = wang * 0.5;
        var dx = WV.dirX * Math.cos(da) - WV.dirY * Math.sin(da);
        var dy = WV.dirX * Math.sin(da) + WV.dirY * Math.cos(da);
        var ph = (x * dx + y * dy) * WV.freq * 0.62 + t * WV.om + WV.ph;
        var a = WV.amp * wstr;
        h += a * Math.sin(ph);           // 高さ — 正弦波6つの合計
        d += Q * a * Math.cos(ph) * dx;  // ガーストナー水平ずれ
      }
      hgt[i] = h;
      disp[i] = d;
    }
  }
}

測定してみるとコサイン項を抜くと水が上下にしか揺れずプールのような印象になりますが、入れると波頂が進行方向に寄って波らしくなります — ポインタで移動ベクトルを実測して風向き・強さの目標を変え、矢印キーでも同じ値を調整できます。

02深さによる減衰 — 浅瀬の水色断面

水面グリッドの高さを水深として読み、ピクセルごとに水色を指数減衰で混ぜる、プール・ダイビング・旅行予約の背景向けシーンです(実測: 累積面積54.177%、強度平均54.2で9本中最高)。

深さによる減衰 — 浅瀬の水色断面
新しいタブで開く

02 · 深さによる減衰 — 浅瀬の水色断面 — 計測 累積面積 54.177% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

createImageDataputImageDataexp

線形ではなく指数で水色を吸い込ませる理由

function render(attenK) {
  var o = out.data;
  var baseline = RH * 0.4;   // 水面の中心線(バッファpx)
  var ampPx = RH * 0.34;     // 水面振幅(バッファpx)
  for (var cx = 0; cx < COLS; cx++) {
    var surfaceY = baseline + surf[cx] * ampPx;
    if (surfaceY < 0) surfaceY = 0; else if (surfaceY > RH - 2) surfaceY = RH - 2;
    for (var sx = 0; sx < STEP; sx++) {
      var px = cx * STEP + sx;
      for (var y = 0; y < RH; y++) {
        var base = (y * RW + px) * 4;
        if (y < surfaceY) {
          o[base] = 0; o[base + 1] = 0; o[base + 2] = 0; o[base + 3] = 0;
          continue;
        }
        var d = y - surfaceY;
        var u = 1 - Math.exp(-attenK * d);   // 0(浅い)..1(深い)
        if (u < 0) u = 0; else if (u > 1) u = 1;
      }
    }
  }
}

指数減衰は浅い数ピクセルで色が急に変わり、深くなるほど変化がゆるやかになる、実際に水が光を吸い込む曲線と同じ形です — 水面より上は透明にしておき背景カードが空の役をし、風が強まるほどattenKが大きくなって色をより速く吸い込みます。

03波頂の泡 — 頂上の白い筋

合成高さが振幅の閾値を超えるマスだけクリーム色に塗って砕ける白い波頂を示す、サーフィン・マリンスポーツ向けシーンです(実測: 累積面積60.08%、フレーム内最大面積22.649%で9本中2番目に大きい)。

波頂の泡 — 頂上の白い筋
新しいタブで開く

03 · 波頂の泡 — 頂上の白い筋 — 計測 累積面積 60.08% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

thresholdImageDatawind

閾値が風に反比例するように組んだ関数

// whitecap — h が cut を超えたらクリーム色。縁はsmoothstep、頂点はFOAM2で二段階
var f = smoothstep(cut - FOAM_EDGE * eff, cut + FOAM_EDGE * eff, h);
if (f > 0) {
  var top = smoothstep(cut + FOAM_EDGE * eff * 2.2, cut + eff * 0.42, h);
  var fr = RF[0] * (1 - top) + RF2[0] * top;
  var fg = RF[1] * (1 - top) + RF2[1] * top;
  var fb = RF[2] * (1 - top) + RF2[2] * top;
  r = r * (1 - f) + fr * f;
  g = g * (1 - f) + fg * f;
  b = b * (1 - f) + fb * f;
}
// 閾値は風に反比例 — 0.82(弱風) → 0.55(強風)
function thresholdFor(w) {
  return 0.82 - 0.27 * Math.max(0, Math.min(1, (w - 0.35) / (2.1 - 0.35)));
}

縁をsmoothstepで滑らかにしないと閾値の境目が階段状に割れ、頂点専用色(FOAM2)がないと白が一本調子に広がって砕けた感じが出ません。

04鏡面ハイライト — 太陽のグリッター

水面グリッドの勾配と太陽方向の内積を2乗して瞬間の光斑だけ打つ、日の出・夜のビーチキャンペーン向けシーンです(実測: 累積面積41.906%、強度平均43.0)。

鏡面ハイライト — 太陽のグリッター
新しいタブで開く

04 · 鏡面ハイライト — 太陽のグリッター — 計測 累積面積 41.906% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

dotpowImageData

法線内積を2乗してきらめきを絞る計算

var lx = Math.cos(sunAng) * (1 - sunZ), ly = Math.sin(sunAng) * (1 - sunZ), lz = sunZ + 0.3;
var llen = Math.sqrt(lx * lx + ly * ly + lz * lz);
lx /= llen; ly /= llen; lz /= llen;
// 中心差分の勾配 — 法線(nx, ny, nz)の近似
var gx = (hgt[row + x1] - hgt[row + x0]) / Math.max(1, x1 - x0);
var gy = (hgt[y1 * COLS + x] - hgt[y0 * COLS + x]) / Math.max(1, y1 - y0);
var nx = -gx * 5.2, ny = -gy * 5.2, nz = 1;
var nlen = Math.sqrt(nx * nx + ny * ny + nz * nz);
nx /= nlen; ny /= nlen; nz /= nlen;
var d = nx * lx + ny * ly + nz * lz;
if (d < 0) d = 0;
var spec = Math.pow(d, SHINY);   // dot(N,L)^shininess — 狭く鋭いきらめき

内積の値をそのまま使うと画面が白く滲みますが、2乗(SHINY)を強めるほど光が点に集まります。ポインタの位置が太陽の方位・高さ(sunZ)になり、太陽を画面内で直接動かせます。

05泡のパーティクル — グリッドセル由来の破片

波頂から離れた泡を座標ではなく生まれたグリッドマスで寿命・位置を計算して散らす、夏イベント・ビーチフェス向けシーンです(実測: 累積面積60.21%、強度平均21.0)。

泡のパーティクル — グリッドセル由来の破片
新しいタブで開く

05 · 泡のパーティクル — グリッドセル由来の破片 — 計測 累積面積 60.21% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

grid-cellparticleslifecycle

座標乱数の代わりにグリッドセルで泡を運ぶアドベクション

// advect — 位相の流れと逆方向(直前の位置)から密度を引いてきてこのセルに置く
var sx = x - flowX, sy = y - flowY;
var ix = Math.round(sx), iy = Math.round(sy);
if (ix < 0) ix = 0; else if (ix >= COLS) ix = COLS - 1;
if (iy < 0) iy = 0; else if (iy >= ROWS) iy = ROWS - 1;
var carried = dens[iy * COLS + ix] * decay;
// spawn — 波頂セルだけ決定的な乱数で発生判定(同じセル・同じフレームは同じ判定)
if (hgt[idx] > cut) {
  var r = rngSpawn();
  if (r < spawnRate) carried = Math.min(1, carried + 0.55 + r * 0.4);
}
densNext[idx] = carried;

座標を乱数で引き直すと風が弱まった瞬間に泡が消えてしまいますが、グリッドセルに紐づけて減衰(decay)に沿って薄れさせると自然に馴染みます。

06水面屈折 — ゆらぐ水紋

下絵(グリッド・文字)のピクセルを保持し、水面グリッドの勾配を読み取り位置のオフセットに変えて元絵を引き寄せる、水族館・スパ・製品のウォータールック向け屈折近似シーンです(実測: 累積面積41.565%、強度平均22.9)。

水面屈折 — ゆらぐ水紋
新しいタブで開く

06 · 水面屈折 — ゆらぐ水紋 — 計測 累積面積 41.565% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

getImageDataoffsetgradient

勾配の発散でコースティック強度を測る計算

var gx = (hgt[row + x1] - hgt[row + x0]) / Math.max(1, x1 - x0);
var gy = (hgt[y1 * COLS + x] - hgt[y0 * COLS + x]) / Math.max(1, y1 - y0);
// 発散近似 — 隣接勾配との差をコースティック強度として使う
var gxr = x1 < COLS - 1 ? (hgt[row + Math.min(COLS - 1, x1 + 1)] - hgt[row + x1]) : gx;
var gyd = y1 < ROWS - 1 ? (hgt[Math.min(ROWS - 1, y1 + 1) * COLS + x] - hgt[y1 * COLS + x]) : gy;
var div = Math.abs((gxr - gx) + (gyd - gy)) * 90;
if (div > 1) div = 1;
var offx = gx * REFRACT, offy = gy * REFRACT;
var bx0 = x * STEP + STEP / 2, by0 = y * STEP + STEP / 2;
var c = tileColor(bx0 + offx, by0 + offy);   // オフセット分だけ引き寄せた床の色

屈折オフセット倍率(REFRACT)を上げるほど床の模様が強くねじれますが、大きくしすぎると座標が飛んでタイルが途切れます。勾配の発散が大きいマスにはミント色を加算し、光が集まるコースティックまで重ねています。

07波の渦 — 離岸流の渦パッチ

波の場に局所回転を重ねて波が束になり沖へ抜け出す離岸流を表現する、海洋安全・教育コンテンツ向けシーンです(実測: 累積面積60.735%、フレーム内最大面積25.676%で9本中最大)。

波の渦 — 離岸流の渦パッチ
新しいタブで開く

07 · 波の渦 — 離岸流の渦パッチ — 計測 累積面積 60.735% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

rotationfalloffImageData

距離減衰を2乗して回転を中心に閉じ込める計算

function fieldcalc(t, wstr, wang, cx, cy, sw) {
  var i = 0;
  for (var y = 0; y < ROWS; y++) {
    for (var x = 0; x < COLS; x++, i++) {
      var dxv = x - cx, dyv = y - cy;
      var r = Math.sqrt(dxv * dxv + dyv * dyv);
      var ang = Math.atan2(dyv, dxv);
      var fall = r < VORTEX_R ? 1 - r / VORTEX_R : 0;
      fall *= fall;                    // 減衰を加速 — 中心だけくっきりと
      var twist = SWIRL * sw * fall * ang;
      // この回転(twist)を波の位相に加え、渦の近くだけ波がねじれるようにする
    }
  }
}

減衰を1回だけ掛けると渦の縁まで波が広くねじれて不自然ですが、2乗すると中心の数マスだけくっきり回ります。ポインタ座標がそのまま渦の中心(cx, cy)になるので、画面のどこへでも引き回せます。

08津波スイープ — 押し寄せる波の壁

波の場の振幅を距離・時間の関数で局所的に膨らませ、ひと塊りの水が水平線へ押し寄せるサージを作る、災害・ドキュメンタリ風オープニング向けシーンです(実測: 累積面積60.026%、強度平均37.9)。

津波スイープ — 押し寄せる波の壁
新しいタブで開く

08 · 津波スイープ — 押し寄せる波の壁 — 計測 累積面積 60.026% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

envelopeamplitudetiming

波頂は膨らませ波谷はあまり沈めない非対称エンベロープ

// エンベロープ — 下(手前)から上(水平線)へ移動するゆっくりした波形
var om = SURGE_OM * (0.55 + wind * 0.75);
var yn = y / ROWS;
var sw = Math.sin(yn * Math.PI * 2 / SURGE_LAM - t * om + 0.7);
var crest = sw > 0 ? sw * sw : 0;                 // 押し寄せる水の壁(正の半波のみ)
var dip = sw < 0 ? -sw * sw * 0.42 : 0;           // 通り過ぎた後に沈む引き波
var e = 1 + crest * SURGE_GAIN * (0.55 + wind * 0.5) - dip;
env[y] = e;
// エンベロープが振幅を膨らませ、外側にも少し押す — 水が上に膨らむ
hgt[i] = h * e + (e - 1) * 0.34;

波頂と波谷に同じ係数を掛けると水が同じ幅で上下するだけの呼吸に留まりますが、波谷の沈み込みを0.42倍に抑えると片方へ押し寄せる力が感じられ、風が強まるほどサージ周期(om)も速くなります。

09水平線のかすみ — 遠景を霞ませる

ピクセルの水平線からの距離に比例して水色を空色と混ぜ、振幅も抑えて遠景を霞ませる、旅行・瞑想・ムードアプリ向けシーンです(実測: 累積面積34.095% — 画面の半分が空なので低いのが正常です)。

水平線のかすみ — 遠景を霞ませる
新しいタブで開く

09 · 水平線のかすみ — 遠景を霞ませる — 計測 累積面積 34.095% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

lerpaerialImageData

距離ひとつで周波数・振幅・色をまとめて抑える計算

function fieldcalc(t, wstr, wang, horizonRow) {
  var i = 0;
  for (var y = 0; y < ROWS; y++) {
    if (y < horizonRow) { i += COLS; continue; }   // 水平線より上は空 — 計算せず飛ばす
    var dist = ROWS > horizonRow ? (y - horizonRow) / (ROWS - horizonRow) : 1;
    if (dist > 1) dist = 1; else if (dist < 0) dist = 0;
    var freqMul = 1 + (1 - dist) * 2.4;   // 近いほど波長が狭く見える
    var ampMul = 0.15 + dist * 0.85;      // 遠いほど振幅を抑えて霞に沈める
    for (var x = 0; x < COLS; x++, i++) {
      // 本計算は01と同じで、freqMul・ampMulだけ掛けて入れる
    }
  }
}

無理に波の面積を増やそうとすると水平線より上(空)にまで波を描き込む不自然なシーンになりますが、振幅の抑制と色の混合を同じ距離値でまとめると遠景がひとりでに沈み、ポインタの縦位置が水平線の高さ(horizonRow)の目標になります。

正弦波を重ねるとどうやって海になるのか?

正弦波をひとつだけ描くと規則正しい縞模様にしかならず、海には見えません。海らしく見せるには波長・方向・速度がそれぞれ違う正弦波を何本も同じグリッドの上に重ねて足し、波頂と波谷を不規則にずらして自然な表面を作る必要があります。市販の海 アニメーション素材の多くも仕組みは同じで、規則的な1本の波を隠すために複数の波をレイヤーとして重ねています。01のWAVES配列は6本の波を作りながら、波長は34から始めて毎回0.55倍ずつ短くし、振幅はインデックスが大きくなるほど0.58/(i*0.62+1)で小さくしていきます — 大きな波が全体の形を決め、小さな波がその上に細波を乗せる構造です。実際の波の重なりをもっと物理的に近づけるにはサイン関数そのものを傾けるガーストナーのずれが要り、この波 アニメーション 作り方では各波の方向ベクトル(dirX)にコサイン項を掛けて、波頂付近の水が進行方向へわずかに寄るようにしています — 頂点が尖り谷がなだらかな実際の波の非対称さを近似する値です。

サインとコサインを対にする理由

高さはMath.sin(ph)で計算しますが、水平方向のずれはMath.cos(ph)で計算します。サインとコサインは位相が90度ずれているため、高さが最も高い地点(サインが最大)ではコサインがほぼ0になり、高さが中間の地点でコサインが最大になります。このずれのおかげで波頂は上に盛り上がりながら前へ寄り、波谷は横にだけ広がるので、単純なサイン波の重ね合わせよりずっと本物の波らしい形になります。

波長が短くなるほど振幅も落とす理由

波長を縮め続けるだけだと、短い波が画面を細かい縞模様で埋め尽くしてしまいます。01でインデックスが大きくなるほど(波長が短くなるほど)振幅も一緒に落としているのはこのためです — 実際の海でも短い波(さざ波)は浅く、長い波(うねり)は深く動きます。この比率を逆にすると画面がノイズのようにチラつき、比率を開きすぎるとさざ波が見えなくなって表面が単調になります。

6本で足りるのか、もっと必要か

波の本数を増やすほど表面は複雑になりますが、ピクセルごとに回る内側のループもその分増えて、フレームあたりの計算量は本数に比例して増えていきます。01は6本で形と速度のバランスを取っており、09のように画面の大半が霞に沈むシーンでは本数を増やしても目に見える違いが小さいためそのまま使い回しています。波の本数はこのあとのバリエーション節で最初に触れる調整点でもあります。

どこで壊れるか — 15.2%から60.8%まで直した4か所

01を最初にレンダリングしたとき、累積面積はわずか15.2%しか出ませんでした。コードはエラーもなく動き、画面には波も見えていたのに、測定値を見て初めてほとんどのフレームで画面がほぼ静止していたことに気づきました。原因はひとつではなく4か所に重なっており、そのうちひとつを直して初めて次が見えてくる、という順番で剥がれていきました。

1つ目 — STEPブロックを連番インデックスで埋めた箇所

低解像度グリッド(縦横STEP マスずつまとめたブロック)を実際のバッファへ引き伸ばして描く際、最初はループを回る順番のままカウンタをひとつずつ増やしてアドレスを取っていました。グリッドマスの並び順と、バッファの実際の行・列の並び順がずれていることに気づいていなかったのです。画面の下側へ行くほど波が裂けてずれて見える現象として現れましたが、縦方向に数行ずれるだけでも見た目には大きく跳ねます。

2つ目 — sassがrgbをパーセントで吐く箇所

2つ目は色が出ない問題でした。sassのmix()関数が色をrgb(52.9%, 78.1%, 69%)のようにパーセント単位でコンパイルしていたのに対し、色を配列に変換するhex2rgb関数は16進文字列しか読めないよう書かれていました。マッチしなくても例外を投げず黙って黒(0,0,0)を返す作りだったため、コンソールにはエラーが一切出ず画面だけが真っ黒に死んでいました。正規表現でパーセント形式も認識し、値に2.55を掛ける変換を入れて初めて色が生き返りました。

3つ目 — 空間周波数にすでに入っている2πを位相でもう一度掛けた箇所

3つ目は波長の計算でした。空間周波数をfreq = 2π/λとして定義しておきながら、位相を計算する箇所で習慣的に係数をもう一度掛けてしまっていたのです。結果として実際の波長がグリッドの領域よりずっと長くなり、画面の中では波がほとんど平らな傾斜にしか見えませんでした。領域の大きさと波長の比率を合わせるスケール係数(0.62)を位相計算に掛け込んで初めて、グリッドの中に複数の波頂と波谷が収まります。

4つ目 — 色ランプの両端が一度も現れなかった箇所

面積を最も大きく引き上げたのは4つ目でした。合成した高さはおおよそ-0.75から0.75の間に収まりますが、この値をそのまま0〜1の色ランプ座標に移すと実際には0.15〜0.85の区間にしか値が集まらず、ランプの両端の色(最も明るい波頂の色と最も深い波谷の色)が画面に一度も現れていませんでした。色が中間トーンにばかり固まっていたので波が動いても明暗差が小さく、測定スクリプトが「変化なし」と判定したフレームが大量に残っていたのです。正規化のスケールをh * 1.24 + 0.5に広げてクランプが両端まで届くように直したところ、累積面積は60.795%まで上がりました。この修理の経験をそのまま持ち越すなら — アニメーションが「遅い」「動かない」という印象は、フレーム数ではなく値の範囲が色ランプの端に届いていないことが原因である場合がよくあります。

修理の順番と根拠はこの記事のzipの中にもまとめてあり、解凍するときに入力するパスワードはjy92b5caです。

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

同じ波の場の計算の上で、値を3つ変えるだけでまったく違うムードになります。

バリエーション 変える値 結果
波の本数を減らす NWを6から3へ 波がまばらになり、静かな湖に近い表面になる
波頂の閾値を緩める thresholdForの上限を0.82から0.55へ 泡が点ではなく帯状に広く広がる
パレットの入れ替え $ocean-mintをライラック系へ 昼の海から夕焼けの海のムードへ

3つのバリエーション

波の本数を減らすと計算コストも同時に下がるので、モバイル向けに軽くしたいときはまずこの値を疑うのが手っ取り早い調整です。閾値の変更はthresholdFor関数の返り値の範囲だけをいじれば済み、レイアウトや他の項目には影響しません。パレットの入れ替えはdemo-tokensのSCSS変数を差し替えるだけで、01〜09のすべてのシーンに一括で反映されます。

アクセシビリティ

動く画面である以上、基本として3つを押さえました。prefers-reduced-motion: reduceが検知されるとループを止め、still()関数で計算した静止フレーム1枚だけを描き、波のある画面であることは見せつつ動きはなくします。キャンバスにはtabindexを与え、フォーカスが入ると矢印キーで風の強さと向きを調整できるようにして、マウスがなくても操作が途切れません。キャンバス要素にはaria-labelでシーンを説明し、スクリーンリーダーの利用者にもこのキャンバス 海 アニメーションが何を描く画面なのかが伝わるようにしています。

FAQ

波 アニメーション 作り方でグリッドの解像度はどこまで下げても大丈夫ですか?

この9項目はすべて低解像度グリッドで計算してからdrawImageで拡大して描く方式を使っています。グリッドを実際のピクセル数と同じ密度で組むと計算量が急激に増えますが、波のように大きな塊で動く表面はグリッドを粗く取ってSTEPブロック単位で拡大しても目に違和感が出ません。どこまで粗くできるかは画面サイズと波長の長さによって変わるので、拡大倍率を2倍・4倍と変えながら目で確認するのが安全です。

動く波の背景をページのヒーローに置いても性能に問題は出ませんか?

キャンバスひとつでレンダリングしライブラリを使わないため重さ自体は軽いですが、ページの最上部に置く場合はprefers-reduced-motionを必ず尊重し、タブがバックグラウンドに回ったときにrequestAnimationFrameループを止める処理を加えるのが望ましいです。この9つのシーンに標準で入っているのはprefers-reduced-motionへの対応だけで、タブがバックグラウンドに回ったときの停止はブラウザのrequestAnimationFrameスロットリングに任せています。実サービスに移すときはvisibilitychangeで明示的にループを止める処理を足すのが安全です。

9項目をReactプロジェクトにそのまま移せますか?

移せます。各項目はキャンバスひとつとrequestAnimationFrameループ、いくつかの純粋関数だけで構成されているので、useEffectの中でキャンバスのrefを取り、同じ計算関数をそのまま持っていけば動きます。zipの中にはバニラ版と並んでReactに移植した版も入っており、コンポーネント構成の参考にできます。

もっと読む

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

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