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

css 波紋 アニメーション 9パターン — キャンバスで計算するピクセル波

css 波紋 アニメーションを9パターン集めました。9番のヒーローシーンはすでに完成した波紋だ。その前の5番は反射する水面が揺れる段階で、いちばん最初の1番はポインターの一回のタップが一つの輪を描くところから始まった。

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

css 波紋 アニメーション 9パターン — ピクセルを押し出すキャンバス波の計算

9つとも、どこかから写真を読み込んではいません。それぞれがseedRand()で駆動するdrawPhotoBase()を一度だけ呼び、グラデーションと粒のある画像をオフスクリーンキャンバスに描いてからgetImageData()で読み戻します。以降は毎フレーム、高さの場(ポインターが打ち込むばね減衰の2バッファグリッド、いくつものMath.sin()を足し合わせた波、あるいはポインターを起点にした放射状の減衰)を計算し直し、隣接する高さの差を小さなx/yのピクセルオフセットに変換して、そのオフセットで元の画像をsampleBase()で再サンプリングし、putImageData()で描き戻します。だからこそ、以下の並びは160×100のグリッドに一度の水しぶきが減衰していく1番から、正弦波とレンズの幾何学を扱う2番から8番を経て、1番・2番・5番の計算を自前で4つ目として作らずにそのまま一つのヒーローシーンへ流用する9番へとつながります。

01ポインター波紋スプラッシュ

ポインターが触れた場所ごとに同心円の波紋が広がり、画像のピクセルを押し出します。タップのたびに160×100の高さグリッドへ衝撃が加わり、stepWave()が1フレームごとに1マスずつそれを伝えていきます。RIPPLE_SPLASH_DECAY_MS(400)はMath.pow(0.02, 16 / RIPPLE_SPLASH_DECAY_MS)としてdampingを決めており、値を小さくすると次のタップが来る前に一つの波紋が消え、大きくすると複数の波紋が重なって残ります。

ポインターで広がる波紋
新しいタブで開く

01 · ポインターで広がる波紋 — 計測 累積面積 15.95% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

RIPPLE_SPLASH_DECAY_MSstepWavevirtualPointerOrbit
  var CALC_W = 160, CALC_H = 100;
  var LOOP_MS = 2000;
  var RIPPLE_SPLASH_DECAY_MS = 400;
  var SPLASH_AMP = -520;
  var REFRACT_K = 0.9;

  var gridA = new Float32Array(CALC_W * CALC_H);
  var gridB = new Float32Array(CALC_W * CALC_H);
  var damping = Math.pow(0.02, 16 / RIPPLE_SPLASH_DECAY_MS);

  function cellIndex(x, y) { return y * CALC_W + x; }

  function splashAt(gx, gy) {
    gx = Math.round(gx); gy = Math.round(gy);
    for (var dy = -1; dy <= 1; dy += 1) {
      for (var dx = -1; dx <= 1; dx += 1) {
        var x = gx + dx, y = gy + dy;
        if (x > 0 && x < CALC_W - 1 && y > 0 && y < CALC_H - 1) {
          gridA[cellIndex(x, y)] += SPLASH_AMP * (dx === 0 && dy === 0 ? 1 : 0.45);
        }
      }
    }
  }

測定値: 面積の15.9%が毎ループ塗り替わり、強度平均は56.8、サンプリングした23/23フレームで変化が確認できました。

02操作なしで常に揺れる背景

操作がなくても重なり合う正弦波によって画像の表面が絶えずゆらめきます。ambientHeight()が毎フレーム毎ピクセルでRIPPLE_AMBIENT_WAVE_COUNT(2)本のMath.sin()波を足し合わせており、ポインターへの反応ではなくループのために存在する関数です。この本数を増やすと、角度と速さの異なる波が何層も重なります。それはまさに下の変形表1行目が1番の減衰時間を伸ばし、2行目がこの本数を6まで上げているのと同じ考え方です。

ひとりでに揺れる背景波紋
新しいタブで開く

02 · ひとりでに揺れる背景波紋 — 計測 累積面積 66.05% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ambientHeightRIPPLE_AMBIENT_WAVE_COUNTREFRACT_K
  function ambientHeight(x, y, t) {
    var h = 0;
    for (var i = 0; i < RIPPLE_AMBIENT_WAVE_COUNT; i += 1) {
      var ang = (i / RIPPLE_AMBIENT_WAVE_COUNT) * Math.PI + 0.5;
      var freq = 0.05 + i * 0.021;
      var speed = 0.0017 + i * 0.0009;
      var amp = 6.5 / (i + 1);
      var dir = (i % 2 === 0) ? 1 : -1;
      h += amp * Math.sin(x * Math.cos(ang) * freq + y * Math.sin(ang) * freq + t * speed * dir);
    }
    return h;
  }

測定値: 面積の66.0%が毎ループ塗り替わり、強度平均は32.2、サンプリングした23/23フレームで変化が確認できました。

03波の先端に沿って現れるホバーリビール

ホバーすると波紋が広がり、下に敷かれた二枚目の画像が波の先端に沿って徐々に現れます。WAVE_BAND(10)が、広がっていく半径の周りで二枚の画像を混ぜ合わせる帯の幅を決めており、この帯を過ぎると一方の写真が完全に置き換わります。ポインターが離れてもvirtualHover()が同じリビールをループで動かし続けるため、誰も触れていない間も静止画のままにはなりません。

波紋が引くと別の画像が現れるホバー
新しいタブで開く

03 · 波紋が引くと別の画像が現れるホバー — 計測 累積面積 66.526% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

WAVE_BANDWAVE_FRONT_SPEEDvirtualHover
  function renderFrame(t) {
    var maxR = Math.sqrt(CALC_W * CALC_W + CALC_H * CALC_H);
    var idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        var dx0 = x - origin.x, dy0 = y - origin.y;
        var dist = Math.sqrt(dx0 * dx0 + dy0 * dy0);
        var band = Math.sin(dist * 0.35 - t * 0.01);
        var mix = 1 - Math.max(0, Math.min(1, (dist - (radius - WAVE_BAND)) / WAVE_BAND));
        var rippled = dist + band * (mix > 0.02 && mix < 0.98 ? 2.4 : 0);
        var a = sampleAt(bufA, x + band * 0.6, y);
        var b = sampleAt(bufB, x - band * 0.6, y);
        var m = mix;
        outBuf[idx] = a[0] + (b[0] - a[0]) * m;
        outBuf[idx + 1] = a[1] + (b[1] - a[1]) * m;
        outBuf[idx + 2] = a[2] + (b[2] - a[2]) * m;
        outBuf[idx + 3] = 255;

測定値: 面積の66.5%が毎ループ塗り替わり、強度平均は44.4、サンプリングした23/23フレームで変化が確認できました。

04ポインター追従の屈折レンズ

ポインター周辺の円形領域だけ、光が屈折したようにピクセルが歪んで見えます。LENS_RADIUS(26)ピクセルの内側だけが、LENS_REFRACT_K(1.7)を膨らみと波のかかった係数に掛けたオフセットで元の画像を再サンプリングします。半径の外側は自分自身の座標からそのままサンプリングするので、レンズの縁がくっきり見え、写真全体になめらかに溶け込むことはありません。

ポインターを追う屈折レンズ
新しいタブで開く

04 · ポインターを追う屈折レンズ — 計測 累積面積 21.081% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

LENS_RADIUSLENS_REFRACT_KvirtualLens
  function renderFrame(t) {
    var idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        var dxp = x - lens.x, dyp = y - lens.y;
        var dist = Math.sqrt(dxp * dxp + dyp * dyp);
        var rgb;
        if (dist < LENS_RADIUS) {
          var norm = dist / LENS_RADIUS;
          var bulge = (1 - norm) * (1 - norm);
          var ripple = Math.sin(dist * 0.9 - t * 0.012) * 0.6 * (1 - norm);
          var factor = LENS_REFRACT_K * bulge + ripple;
          var ux = dist > 0.0001 ? dxp / dist : 0;
          var uy = dist > 0.0001 ? dyp / dist : 0;
          var sx = x - ux * factor * 4;
          var sy = y - uy * factor * 4;
          rgb = sampleBase(sx, sy);
          var shine = Math.max(-20, Math.min(50, (1 - norm) * 30 - ripple * 14));
          rgb = [
            Math.max(0, Math.min(255, rgb[0] + shine)),
            Math.max(0, Math.min(255, rgb[1] + shine)),
            Math.max(0, Math.min(255, rgb[2] + shine))
          ];

測定値: 面積の21.1%が毎ループ塗り替わり、強度平均は22.5、サンプリングした23/23フレームで変化が確認できました。

05揺れる反射プール

画像の下に上下反転した反射像があり、波の形に沿って揺れます。reflectWave()が深さに応じて位相をずらしたMath.sin()値を返し、REFLECT_SWAY_AMP(6.5)がそれを横方向のサンプリングオフセットに変換します。HORIZONから下に離れた行ほどこのオフセットは下端に向かって弱まっていくので、反射が水面につながっているように見え、一枚板がただ横滑りしているようには見えません。

水面に映るように揺れる反射画像
新しいタブで開く

05 · 水面に映るように揺れる反射画像 — 計測 累積面積 25.191% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

REFLECT_SWAY_AMPREFLECT_GLOW_AMPreflectWave
  function renderFrame(t) {
    var idx = 0;
    var span = CALC_H - HORIZON;
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        var rgb;
        var alpha = 1;
        var glow = 0;
        if (y < HORIZON) {
          rgb = sampleBase(x, y);
        } else {
          var depth = y - HORIZON;
          var srcY = HORIZON - depth;
          var frac = depth / span;
          var wave = reflectWave(depth, t);
          var offset = wave * REFLECT_SWAY_AMP * frac;
          rgb = sampleBase(x + offset, srcY);

測定値: 面積の25.2%が毎ループ塗り替わり、強度平均は23.8、サンプリングした23/23フレームで変化が確認できました。

06反転の裏で波が通り過ぎるカードフリップ

カードが反転する瞬間、波がひとつ画面を横切り、表と裏の画像を入れ替えます。frontXがCARD_FLIP_MS(900)の間にキャンバスを横切って進み、幅FRONT_BAND(9)ピクセルの帯が、波の先端がいま通過している場所で二枚の写真を混ぜ合わせます。波がすでに通り過ぎた列は裏の画像がそのまま見え、まだ届いていない列は表のままなので、動いている縫い目の部分を除けば急に切り替わることはありません。

波が通り過ぎながら反転するカード
新しいタブで開く

06 · 波が通り過ぎながら反転するカード — 計測 累積面積 63.749% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

AUTO_CYCLE_MSCARD_FLIP_MSFRONT_BAND
  function renderFrame(t) {
    var frontX = progress * (CALC_W + FRONT_BAND * 2) - FRONT_BAND;
    var idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      var wobble = Math.sin(y * 0.45 + t * 0.01) * 3.2;
      var localFront = frontX + wobble;
      for (var x = 0; x < CALC_W; x += 1) {
        var rgb;
        var d = x - localFront;
        if (d < -FRONT_BAND) {
          rgb = sampleAt(bufBack, x, y);
        } else if (d > FRONT_BAND) {
          rgb = sampleAt(bufFront, x, y);
        } else {
          var m = (d + FRONT_BAND) / (FRONT_BAND * 2);
          var a = sampleAt(bufFront, x, y);
          var b = sampleAt(bufBack, x, y);
          rgb = [a[0] + (b[0] - a[0]) * m, a[1] + (b[1] - a[1]) * m, a[2] + (b[2] - a[2]) * m];
          var shine = (1 - Math.abs(m - 0.5) * 2) * 34;
          rgb = [rgb[0] + shine, rgb[1] + shine, rgb[2] + shine];

測定値: 面積の63.7%が毎ループ塗り替わり、強度平均は59.5、サンプリングした23/23フレームで変化が確認できました。

07音量に反応する波紋(マイクなし)

仮想の音量レベルが上がるほど、波紋の振幅と広がる速度がともに大きくなります。このファイルのどこにもマイクは登場しません。virtualAudioLevel()が3つのMath.sin()項を組み合わせて0から1の値を作り、tick()がBEAT_INTERVAL_MS(220)ごとにそれを0.25というしきい値と比較します。RIPPLE_AUDIO_GAIN(既定0.4、「反応の強さ」というラベルのスライダーで10から160の範囲で調整可能)がそのレベルにsplashAt()へ渡す前に掛かるので、ゲインを上げると静かな仮想のビートでも大きな波紋を押し出します。

音量に合わせて大きくなる波紋
新しいタブで開く

07 · 音量に合わせて大きくなる波紋 — 計測 累積面積 19.934% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

RIPPLE_AUDIO_GAINBEAT_INTERVAL_MSvirtualAudioLevel
  function tick(ts) {
    if (lastTs === null) lastTs = ts;
    var dt = ts - lastTs;
    lastTs = ts;
    if (playing) {
      elapsed += dt;
      var beatIdx = Math.floor(elapsed / BEAT_INTERVAL_MS);
      if (beatIdx !== lastBeatIdx) {
        lastBeatIdx = beatIdx;
        var level = virtualAudioLevel(elapsed);
        if (level > 0.25) {
          var gx = CALC_W * (0.3 + 0.4 * ((Math.sin(elapsed * 0.0017) + 1) / 2));
          var gy = CALC_H * (0.3 + 0.4 * ((Math.cos(elapsed * 0.0013) + 1) / 2));
          splashAt(gx, gy, -level * RIPPLE_AUDIO_GAIN * gainMul * 10);
        }
      }
      stepWave();
      draw();
    }

測定値: 面積の19.9%が毎ループ塗り替わり、強度平均は33.3、サンプリングした23/23フレームで変化が確認できました。

08文字の形に閉じ込められた波紋

文字の形に切り抜かれた領域の中だけで、画像が液体のように波打ちます。liquidHeight()はテキストマスクのアルファ値が24以上のピクセルでしか呼ばれず、RIPPLE_TEXT_WAVE_COUNT(2)本のサインとコサインを掛け合わせた項を足して高さを求めます。マスクの外側は計算そのものをせず単色で塗るだけなので、この項目の面積比率が9つの中でも低めにとどまる理由になっています。

文字の中で揺れる画像
新しいタブで開く

08 · 文字の中で揺れる画像 — 計測 累積面積 17.21% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

liquidHeightRIPPLE_TEXT_FIT_RATIORIPPLE_TEXT_WAVE_COUNT
  function liquidHeight(x, y, t) {
    var h = 0;
    for (var i = 0; i < RIPPLE_TEXT_WAVE_COUNT; i += 1) {
      var freq = 0.09 + i * 0.05;
      var speed = 0.0064 + i * 0.0032;
      var amp = 8 / (i + 1);
      h += amp * Math.sin(x * freq + t * speed) * Math.cos(y * (freq * 0.7) - t * speed * 0.7);
    }
    return h;

測定値: 面積の17.2%が毎ループ塗り替わり、強度平均は29.6、サンプリングした23/23フレームで変化が確認できました。

09スプラッシュ・アンビエント波・反射を合わせたヒーローシーン

上記の技法をひとつにまとめ、ヒーローセクション全体を埋める波紋の画像シーンに仕上げます。水平線より上ではupperRGB()がポインタースプラッシュの高さグリッドとambientHeight()の勾配をそれぞれ0.85と1.1の重みで足し合わせ、水平線より下ではその合成済みの場をMath.sin()の揺れとともに再利用して反射に見せかけます。つまり9番は、4つ目の計算を自前で用意する代わりに1番・2番・5番の計算を一つのrenderFrame()に折りたたんだものです。

上記技法を合わせたヒーロー波紋シーン
新しいタブで開く

09 · 上記技法を合わせたヒーロー波紋シーン — 計測 累積面積 61.751% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

HERO_SPLASH_DECAY_MSupperRGBambientHeight
  function upperRGB(x, y, t) {
    var i = y * CALC_W + x;
    var hl = x > 0 ? gridA[i - 1] : gridA[i];
    var hr = x < CALC_W - 1 ? gridA[i + 1] : gridA[i];
    var hu = y > 0 ? gridA[i - CALC_W] : gridA[i];
    var hd = y < HORIZON - 1 ? gridA[i + CALC_W] : gridA[i];
    var amb = ambientHeight(x, y, t);
    var ambX = ambientHeight(x + 1, y, t) - ambientHeight(x - 1, y, t);
    var ambY = ambientHeight(x, y + 1, t) - ambientHeight(x, y - 1, t);
    var dx = (hr - hl) * 0.85 + ambX * 1.1;
    var dy = (hd - hu) * 0.85 + ambY * 1.1;
    var rgb = sampleBase(x + dx, y + dy);
    var shine = Math.max(-30, Math.min(55, (hu - hd) * 0.5 + amb * 4));
    return [
      Math.max(0, Math.min(255, rgb[0] + shine)),
      Math.max(0, Math.min(255, rgb[1] + shine)),
      Math.max(0, Math.min(255, rgb[2] + shine))
    ];

測定値: 面積の61.8%が毎ループ塗り替わり、強度平均は38.1、サンプリングした23/23フレームで変化が確認できました。

css 波紋 アニメーション 9パターンをフレームごとに測定した結果

項目 面積% 強度 動いたフレーム
01 ripple-canvas-pointer-splash 15.9 56.8 23/23
02 ripple-canvas-auto-loop-scene 66.0 32.2 23/23
03 ripple-canvas-hover-reveal 66.5 44.4 23/23
04 ripple-canvas-refraction-lens 21.1 22.5 23/23
05 ripple-canvas-reflection-pool 25.2 23.8 23/23
06 ripple-canvas-card-flip-wave 63.7 59.5 23/23
07 ripple-canvas-audio-reactive 19.9 33.3 23/23
08 ripple-canvas-text-liquid 17.2 29.6 23/23
09 ripple-canvas-hero-scene 61.8 38.1 23/23

面積比率が最も低いのは15.9%の1番で、stepWave()がまだ伝え切れていない小さな輪の範囲しか一度の水しぶきでは触れないからです。一方、3番のホバーリビールと2番のアンビエント波はどちらも66%台まで届き、160×100のバッファのほぼ全体を毎フレーム触っています。強度の平均が最も高いのは59.5の6番で、同じ写真の自分のピクセルを少しずらすのではなく、幅9ピクセルの帯でまったく別の写真に丸ごと入れ替えているためです。9項目すべてがサンプリングした23/23 フレームで変化しており、フレーム落ちの例外は一つもありませんでした。

画像 波紋 加工が実際に使われる場面

9項目のうち3つは、触らずに置いておくだけの背景として成立します。2番のアンビエント波と9番の合成ヒーローシーンはどちらもヒーロー背景やローディング画面に向いており、一度書き出した波紋 アニメーション 作り方のgifをループ再生するのとは違い、どちらも高さの場を実際にライブで計算し続けているので、ブラウザ幅を変えても継ぎ目が見えることはありません。5番の揺れる反射はブランドロゴのイントロやポートフォリオのヒーローセクションのように、揺れそのものが見せ場になる場面に合います。残る6つは操作寄りで、1番のスプラッシュは商品画像のホバーやインタラクティブバナーに、3番のリビールは比較画像カードや商品カラー切り替えに、4番のレンズは商品詳細画像やポートフォリオプレビューに、6番の波フリップはカード詳細の切り替えやイベントバナーの差し替えに、7番のゲインスライダーはただの背景というより音楽アルバムカバーやポッドキャストプレーヤー向けの小さな道具に近く、8番の文字の波紋はブランドタイポグラフィのヒーローやイベントタイトルのように文字そのものを生き生きと見せたい場面に合います。

値が波の形を変える場所 — 減衰時間・波形の数・音量ゲイン

体感の違いのほとんどは3つの定数が担っています。RIPPLE_SPLASH_DECAY_MSはstepWave()が一度の水しぶきの高さをどれだけ早く消すかを決め、1番と9番は既定でそれぞれ400と420にして次のタップが来る前に消えるようにしていますが、下の変形表1行目は1番の値を1400まで伸ばし、複数の波紋を重ねたまま残します。RIPPLE_AMBIENT_WAVE_COUNTはambientHeight()が毎フレーム毎ピクセルで足し合わせるMath.sin()項の本数を決め、2番と9番は既定で2ですが、変形表2行目はこれを6まで増やし、単調な一方向の揺れではなく角度と速さの異なる波を重ねます。RIPPLE_AUDIO_GAINは7番の仮想virtualAudioLevel()の値にsplashAt()へ渡る前に掛かる係数で、既定の0.4は静かなビートを控えめに保ちますが、変形表3行目の1.6は小さな仮想レベルでも実際のタップと同じくらい大きく波紋を押し出します。

どこで壊れるか — 動作軽減設定・低スペック端末・大きい画像の性能

9項目とも、draw()が結果を見た目のキャンバスに拡大する前に、同じ160×100のバッファ(CALC_W/CALC_H)を1ピクセルずつレンダー関数の中で歩きます。このサイズなら軽いのですが、同じループをフル解像度の1920×1080のヒーロー画像に当てはめると、Math.sin()やMath.sqrt()、getImageData()の呼び出しが毎フレーム100倍以上に膨れ上がります。低スペックのスマートフォンで最初にコマ落ちが目立つのはおそらく9番で、upperRGB()がスプラッシュグリッドを読むのに加えてambientHeight()を1ピクセルあたり3回も呼んでいるからです。e7jhekfsがここに置かれているすべてのindex.html・style.scssの組を守っているのはわざとで、ここまで読んだ時点でzipを開く前にどのパターンから最初に負荷を確認すべきかはもう分かっているはずです。静止画1枚だけあればアニメーションはいらないという場合も、9項目すべてがただの<canvas>要素なので、好きなフレームを右クリックして画像 波紋 加工済みのpngとして保存できます。サーバー側の書き出しは不要です。

変形3つとアクセシビリティ

変形 値の変更 感じ方
波紋の減衰時間値を調整する 01 RIPPLE_SPLASH_DECAY_MS 400 → 1400 値を小さくすると波紋がすぐに消えてコツンと弾む感じになります。値を大きくすると波紋が長く残り、複数の波紋が重なって見えます。
背景の波形数値を調整する 02 RIPPLE_AMBIENT_WAVE_COUNT 2 → 6 値を小さくすると単一の波形が単調に流れます。値を大きくすると複数の波形が重なり、ずっと複雑で自然な揺れになります。
音量反応ゲイン値を調整する 07 RIPPLE_AUDIO_GAIN 0.4 → 1.6 値を小さくすると、音が大きくなっても波紋はあまり変わりません。値を大きくすると、小さな音でも波紋が大きく揺れます。

prefers-reduced-motion: reduceはどの項目でもアニメーションループを止め、代わりに1枚の静止フレームを描きます。2番・5番・8番はその止まったフレームの内部時計をわざと0ではなく900ミリ秒・700ミリ秒・600ミリ秒にずらしており、動きに敏感な読者にも空っぽの開始フレームではなく、それぞれの波の典型的な形が見えるようにしています。1番・4番・6番・9番はキーボードにも直接応答し、それぞれのキャンバスをtabIndexが0の要素で包んでいますが、keydownハンドラの中身は項目ごとに違います。1番・9番はEnterキーかスペースキーでポインターのタップと同じ水しぶきを起こし、4番は矢印キーでレンズの位置そのものを動かし、6番はEnterキーかスペースキーでクリックと同じtoggleFlip()を呼びます。7番のゲイン調整はネイティブの<input type="range">で「反応の強さ」というラベルが付いているので、矢印キーの操作はブラウザが標準で面倒を見てくれます。3番だけは穴が残っていて、ラッパーにtabindex="0"はあるものの応答するkeydownハンドラがなく、キーボードで要素にフォーカスは合わせられてもリビールは起きません。動かしているのはpointeroverと自動のvirtualHover()ループだけです。getImageData()、ImageData、requestAnimationFrame()、prefers-reduced-motionメディアクエリはいずれもMDNに解説があります。

水の波紋を再現するCSSと、この画像波紋アニメーションは何が違うのか?

波紋 が 広がる cssで検索すると、border-radiusやbox-shadow、あるいはSVGの<feDisplacementMap>フィルターで平らな箱を揺らしているものが大半で、波紋はコンテナの縁やフィルターグラフの中だけにあり、下に写真があってもそのピクセル自体は一切動きません。この記事の9項目はどれも同じ160×100のバッファにgetImageData()をかけ、高さの場を計算し、その場からわずかにずらした位置で元のピクセルを再サンプリングしてからputImageData()で書き戻しています。水 波紋 cssというキーワードで探すときに本当に見るべきなのは、ピクセルデータそのものに触れているか、それとも図形の輪郭をアニメーションさせているだけかという違いです。

同じように値主導で作られたキャンバス系の記事としては、html5 canvas アニメーション9種 — 水滴とブラックホールが別の力学でピクセル単位の歪みを扱っており、css 画像 フィルター 9パターン — 値ひとつで色が変わるは同じ写真をキャンバスのループではなくCSSのフィルター関数に通すとどう見えるかを示し、画像 拡大 表示 UI 9選 — 写真ファイルなしで作るビューアは一枚の画像上でポインターを起点にした操作を扱う点でいちばん近い親戚です。

この9つを試す前によくある質問に答えます

この9つの裏に本物の写真やキャンバスライブラリはありますか?

いいえ、どの項目もrand()だけを使う自前のdrawPhotoBase()を呼び出し、種から生成したグラデーションと粒のある画像をオフスクリーンキャンバスに描いているだけです。写真ファイルの読み込みもキャンバスライブラリの導入も一切なく、zipから実際に取り出して使えるのはgetImageData()とputImageData()が担っている計算そのものです。

低スペックのスマートフォンでいちばん重いのはどれですか?

9番です。すべてのタップが供給するスプラッシュグリッドを読むのに加えて、upperRGB()がambientHeight()を1ピクセルあたり3回呼んでいます。次に重いのは8番のliquidHeight()で、波の関数を1ピクセルあたり2回サンプリングしますが、160×100のバッファ全体ではなく文字の形に切り抜かれたマスクの中だけなので、測定された面積比率はずっと低く抑えられています。

水滴が落ちるようなcssの作例は、この9つの波紋と同じ仕組みで作られていますか?

ピクセルデータを読み書きしている場合に限ります。多くの水滴系の作例は固定した1枚の画像の上でborder-radiusのブロブやSVGフィルターをアニメーションさせているだけです。この記事の4番のレンズと1番のスプラッシュはどちらもgetImageData()を呼んでいるので、水が本当に写真自身のピクセルを歪ませており、上に重ねた図形が動いているわけではありません。

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

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