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

css ボタン クリック アニメーション 9選 — キャンバス計算

この記事ではcss ボタン クリック アニメーションを9種類、ピクセル計算で作ります。ボタンがガラスのようにひびが入って見える原因は、押下状態の値がオンになると伸びるひびの角度値ひとつだ。2番はその値でひびを描き、5番は同じ値を縁を走る電流の太さに使い回し、8番は押した位置からその値だけ画面を押し出す。

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

同じキャンバスループの上で、9つの定数だけが違う

9つとも同じ土台から始まります。ボタンに合わせたサイズの <canvas>、1本の requestAnimationFrame ループ、そして click・Enter/Space用の keydown・座標を取る pointerdown・resize の4つのリスナーです。パターンごとに変わるのは各デモの計算が中心に置く一つの定数だけ — 拡散率、分岐数、ジッター間隔、減衰時間 — で、ライブラリもWebGLコンテキストもCDNスクリプトもどれ一つ読み込んでいません。

01クリック地点から広がるインク拡散ボタン

クリックした地点に配列上の一マスへ高い値を書き込むと、diffuseStep() が毎フレーム呼ばれ、DIFFUSE_RATE = 0.46 で隣接マスへ値を平均化しながら、DECAY_PER_FRAME = 0.978 でグリッド全体を同時に減衰させます。render() は毎フレームそのグリッドをそのままピクセルへ描き直すので、見えているにじみはCSSのradial-gradientがサイズを変えているのではなく、グリッド自身が持つ平均化された値そのものです。

クリック地点から広がるインク拡散ボタン
新しいタブで開く

01 · クリック地点から広がるインク拡散ボタン — 計測 累積面積 29.288% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

DIFFUSE_RATEinjectAtdiffuseStep
  function diffuseStep() {
    for (var y = 0; y < ROWS; y++) {
      for (var x = 0; x < COLS; x++) {
        var i = idx(x, y);
        var sum = 0, n = 0;
        if (x > 0) { sum += field[idx(x - 1, y)]; n++; }
        if (x < COLS - 1) { sum += field[idx(x + 1, y)]; n++; }
        if (y > 0) { sum += field[idx(x, y - 1)]; n++; }
        if (y < ROWS - 1) { sum += field[idx(x, y + 1)]; n++; }
        var avg = n ? sum / n : field[i];
        var v = field[i] + (avg - field[i]) * DIFFUSE_RATE;
        next[i] = v * DECAY_PER_FRAME;
      }
    }
    var tmp = field; field = next; next = tmp;
  }

02押下状態の値でひびが入る亀裂線ボタン

押下状態の値がオンになった瞬間、buildTree() が BTN_CANVAS_CRACK_BRANCH_COUNT(既定6)回だけ genBranch() を呼び出し、各呼び出しは CRACK_MAX_DEPTH = 3 まで再帰しながら CRACK_ANGLE_SPREAD で枝分かれし CRACK_LEN_DECAY で世代ごとに縮んでいきます。それぞれの枝は自分専用の t0/t1 を持つので、ひびは一瞬でなく数百ミリ秒かけて外へ伸びていくように見えます。

押下状態の値でひびが入る亀裂線ボタン
新しいタブで開く

02 · 押下状態の値でひびが入る亀裂線ボタン — 計測 累積面積 20.131% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

BTN_CANVAS_CRACK_BRANCH_COUNTgenBranchbuildTree
  function genBranch(x, y, angle, len, depth, t0, segs) {
    if (depth > CRACK_MAX_DEPTH || len < 5) return;
    var ang = angle + (rnd() - 0.5) * 0.35;
    var x2 = x + Math.cos(ang) * len;
    var y2 = y + Math.sin(ang) * len;
    var dur = 120 + len * 4;
    segs.push({ x1: x, y1: y, x2: x2, y2: y2, t0: t0, t1: t0 + dur, w: Math.max(1, 3 - depth) });
    var childT0 = t0 + dur * 0.6;
    var childA = [ang + CRACK_ANGLE_SPREAD * 0.5, ang - CRACK_ANGLE_SPREAD * 0.5];
    for (var i = 0; i < childA.length; i++) {
      genBranch(x2, y2, childA[i] + (rnd() - 0.5) * 0.3, len * CRACK_LEN_DECAY, depth + 1, childT0, segs);
    }
  }

  function buildTree(cx, cy) {
    var segs = [];
    for (var i = 0; i < BTN_CANVAS_CRACK_BRANCH_COUNT; i++) {
      var base = (Math.PI * 2 * i) / BTN_CANVAS_CRACK_BRANCH_COUNT;
      var ang = base + (rnd() - 0.5) * 0.5;
      genBranch(cx, cy, ang, CRACK_SEG_LEN, 0, 0, segs);
    }
    return segs;
  }

03ドラッグ速度の値で散る残り火トレイルボタン

dragTo() は直前フレームからの移動距離を速度の値に変え、spawnEmbers() はその速度を Math.round(speed / 26)(1〜9に制限)個の粒子数へ変換し、ポインターが動き続ける限り毎フレーム発生させます。生成された残り火はそれぞれ固有の初速を持ち、GRAVITY = 520 と AIR_DRAG = 0.988 が働いて速度を落としながら消えていきます。

ドラッグ速度の値で散る残り火トレイルボタン
新しいタブで開く

03 · ドラッグ速度の値で散る残り火トレイルボタン — 計測 累積面積 28.606% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

GRAVITYspawnEmbersstepParticles
  function spawnEmbers(x, y, speed, now) {
    var count = Math.max(1, Math.min(9, Math.round(speed / 26)));
    for (var i = 0; i < count; i++) {
      var ang = rnd() * Math.PI * 2;
      var spd = 24 + rnd() * 96;
      particles.push({
        x: x, y: y,
        vx: Math.cos(ang) * spd * 0.34,
        vy: Math.sin(ang) * spd * 0.34 - 34 - rnd() * 46,
        born: now,
        life: EMBER_LIFE_MIN + rnd() * EMBER_LIFE_SPAN,
        size: 2.6 + rnd() * 3.2
      });
    }
  }

04ポインター距離の値で融合し広がるリキッドメタルボタン

BLOB_COUNT = 7 個の粒それぞれが自分の半径を持ち、fieldAt() はキャンバス上の任意の点について7個分の (effR * effR) / dist2 を合計し、ポインターが近いと POINTER_BOOST、クリック直後は BURST_BOOST でその値を押し上げます。合計した場の値が THRESH = 1.0 を超えた場所同士は、7つの円ではなく1つの融合面として描かれます。

ポインター距離の値で融合し広がるリキッドメタルボタン
新しいタブで開く

04 · ポインター距離の値で融合し広がるリキッドメタルボタン — 計測 累積面積 51.592% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

fieldAtBLOB_COUNTbuildBlobs
  function fieldAt(px, py, now) {
    var burstT = now < burstUntil ? clamp01((burstUntil - now) / BURST_MS) : 0;
    var burstBoost = burstT * BURST_BOOST;
    var sum = 0;
    for (var i = 0; i < blobs.length; i++) {
      var b = blobs[i];
      var boost = burstBoost;
      if (pointer) {
        var dpx = pointer.x - b.x, dpy = pointer.y - b.y;
        var distToPointer = Math.sqrt(dpx * dpx + dpy * dpy);
        boost += POINTER_BOOST * Math.exp(-distToPointer / POINTER_FALLOFF);
      }
      var effR = b.r * (1 + boost);
      var dx = px - b.x, dy = py - b.y;
      var dist2 = dx * dx + dy * dy + 1;
      sum += (effR * effR) / dist2;
    }
    return sum;
  }

05ホバー強度の値が上がるほど太くなる電気アーク縁取りボタン

pathPoints() がボタンの角丸の縁をたどり、BTN_CANVAS_ARC_JITTER_MS(既定70ms)ごとに ARC_SEGMENTS = 6 個のアンカーそれぞれへ新しいランダムなずれが入るので、線は滑らかに補間されるのではなく震えているように見えます。ジッター幅を広げる同じ強度の値が CRACK_VALUE_ECHO として線幅の計算にも使われるため、強くなるほど震えも線も同時に太くなります。

ホバー強度の値が上がるほど太くなる電気アーク縁取りボタン
新しいタブで開く

05 · ホバー強度の値が上がるほど太くなる電気アーク縁取りボタン — 計測 累積面積 23.585% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

BTN_CANVAS_ARC_JITTER_MSARC_SEGMENTSstampGlow
    intensity += (targetIntensity - intensity) * Math.min(1, dt * 7);

    if (now - lastJitter >= BTN_CANVAS_ARC_JITTER_MS) {
      lastJitter = now;
      for (var i = 0; i < ARC_SEGMENTS; i++) {
        anchors[i] = (rnd() - 0.5) * 2 * JITTER_AMP_MAX * (0.12 + intensity);
      }
    }

06無効値がオンになると煙のように消えるボタン

無効値がオンになった瞬間、ボタン自身のピクセルがあらかじめ shards[] 配列へサンプリングされており、render() は assembly の値を ASSEMBLE_SPEED = 2.6 で0へイージングしながら、各破片を自分専用の移動方向と小さな上昇量に沿って動かします。無効値を戻すと同じ assembly のイージングが逆方向に働くので、破片は新しいスプライトではなく元のボタンへ組み直されます。

無効値がオンになると煙のように消えるボタン
新しいタブで開く

06 · 無効値がオンになると煙のように消えるボタン — 計測 累積面積 22.697% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ASSEMBLE_SPEEDroundRecttoggle
  function render(dt) {
    assembly += (target - assembly) * Math.min(1, dt * ASSEMBLE_SPEED);
    ctx.clearRect(0, 0, W, H);
    for (var i = 0; i < shards.length; i++) {
      var s = shards[i];
      var local = Math.max(0, Math.min(1, (assembly - s.delay) / (1 - s.delay)));
      var scatter = 1 - local;
      var px = s.ox + s.dirX * scatter * scatter;
      var py = s.oy + s.dirY * scatter * scatter - scatter * 18;
      var alpha = local;
      if (alpha <= 0.01) continue;
      ctx.save();
      ctx.globalAlpha = alpha;
      ctx.translate(px + s.w / 2, py + s.h / 2);
      ctx.rotate(s.spin * scatter);
      ctx.fillStyle = 'rgb(' + s.r + ',' + s.g + ',' + s.b + ')';
      ctx.fillRect(-s.w / 2, -s.h / 2, s.w, s.h);
      ctx.restore();
    }
  }

07進捗値の分だけ砂のように積もるプログレスボタン

進捗値が上がるたび spawnGrain() がランダムな列へ砂粒を一つ落とし、updateGrains() は GRAVITY = 560 で各粒を落下させ、現在の積み上げ高さに達した粒を配列から取り除きます。updatePile() は各列の高さを targetFill * BH へ EASE_RATE = 5.5 でイージングするので、山の輪郭は粒が落ち切った直後ではなく少し遅れて落ち着きます。

進捗値の分だけ砂のように積もるプログレスボタン
新しいタブで開く

07 · 進捗値の分だけ砂のように積もるプログレスボタン — 計測 累積面積 21.685% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

hashNoisespawnGrainupdatePile
  function spawnGrain() {
    var col = Math.floor(rnd() * COLS);
    var size = 3 + rnd() * 3;
    grains.push({ x: BX + col * COL_W + COL_W / 2, y: SPAWN_Y, vy: 40 + rnd() * 30, size: size, col: col });
  }

  function updateGrains(dt) {
    if (grains.length > 90) grains.length = 90;
    for (var i = grains.length - 1; i >= 0; i--) {
      var g = grains[i];
      g.vy += GRAVITY * dt;
      g.y += g.vy * dt;
      var floorY = BY + BH - pileHeights[g.col];
      if (g.y + g.size / 2 >= floorY) grains.splice(i, 1);
    }
  }

  function updatePile(dt) {
    var targetHeight = targetFill * BH;
    for (var c = 0; c < COLS; c++) {
      var t = Math.max(0, Math.min(BH, targetHeight + colJitter[c]));
      pileHeights[c] += (t - pileHeights[c]) * Math.min(1, dt * EASE_RATE);
    }
  }

08押した座標からピクセルを押し出す屈折リップルボタン

リップル帯の内側にある各ピクセルは、自分自身の位置ではなくずらした位置から getImageData で再サンプリングされ、そのずれ幅は shiftMag = CRACK_VALUE_ECHO * ampDecay * envelope * Math.sin(phase) で決まります — 02番が分岐数に使うのと同じ6が、ここではピクセルを押し出す量に化けています。BTN_CANVAS_RIPPLE_DECAY_MS(既定320ms)は ampDecay が0へ落ちる速さを決め、値を短くすると波は一、二フレームで収まります。

押した座標からピクセルを押し出す屈折リップルボタン
新しいタブで開く

08 · 押した座標からピクセルを押し出す屈折リップルボタン — 計測 累積面積 17.142% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

CRACK_VALUE_ECHOTEXTURE_CELLtoCanvasXY
      for (var y = minY; y <= maxY; y++) {
        for (var x = minX; x <= maxX; x++) {
          var dx = x - r.x, dy = y - r.y;
          var dist = Math.sqrt(dx * dx + dy * dy);
          var bandDist = dist - radius;
          if (bandDist > BAND_WIDTH || bandDist < -BAND_WIDTH) continue;
          var envelope = 1 - Math.abs(bandDist) / BAND_WIDTH;
          var phase = dist * FREQ - (elapsed / 1000) * ANG_SPEED;
          var shiftMag = CRACK_VALUE_ECHO * ampDecay * envelope * Math.sin(phase);
          var nx = dist > 0.0001 ? dx / dist : 0, ny = dist > 0.0001 ? dy / dist : 0;
          var sx = clampInt(Math.round(x + nx * shiftMag), 0, W - 1);
          var sy = clampInt(Math.round(y + ny * shiftMag), 0, H - 1);
          var srcIdx = (sy * W + sx) * 4, dstIdx = (y * W + x) * 4;
          outData[dstIdx] = baseData[srcIdx];
          outData[dstIdx + 1] = baseData[srcIdx + 1];
          outData[dstIdx + 2] = baseData[srcIdx + 2];
          outData[dstIdx + 3] = baseData[srcIdx + 3];
        }
      }

09エラー値が反転すると砂嵐のように揺れるノイズボタン

trigger() は真偽値 errorOn を切り替えるだけで、drawNoise() は NOISE_CELL = 3 の格子の各セルを TICK_MS = 40 ごとにハッシュから再計算するので、静止画にランダムな透明度を重ねているのではなく、ティックごとに決定論的なハッシュで描き直しています。座標を必要としないぶん、他の8つが使う makeLcg() の乱数列はこの項目には存在しません。

エラー値が反転すると砂嵐のように揺れるノイズボタン
新しいタブで開く

09 · エラー値が反転すると砂嵐のように揺れるノイズボタン — 計測 累積面積 22.181% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

BTN_CANVAS_STATIC_BURST_MSdrawNoiseTICK_MS
  function drawNoise(envelope, tick) {
    ctx.save();
    roundRect(ctx, BX, BY, BW, BH, 16);
    ctx.clip();
    ctx.globalAlpha = envelope;
    for (var y = BY; y < BY + BH; y += NOISE_CELL) {
      for (var x = BX; x < BX + BW; x += NOISE_CELL) {
        var n = hashNoise(x / NOISE_CELL, y / NOISE_CELL, SEED + tick);
        var v = Math.floor(n * 255);
        ctx.fillStyle = 'rgb(' + v + ',' + v + ',' + v + ')';
        ctx.fillRect(x, y, NOISE_CELL, NOISE_CELL);
      }
    }
    ctx.restore();
    ctx.globalAlpha = 1;
  }

見た目を決めているのは3種類の値だけ

9つを「定数が実際に何を決めているか」でまとめると、形は3種類しかありません。

再計算される場が形を決める — 01・04・08

01のインクグリッド、04のメタボール合計、08のピクセルずらしの場は、どれも毎フレーム配列やループから丸ごと再計算されており、2つのキーフレームをCSSプロパティでイージングしているわけではありません。01は60×38マスの配列、04は7個の粒、08はキャンバス全域のピクセルと対象の粒度は違いますが、どれも「前フレームの値から次フレームの値を計算し直す」という同じ操作を繰り返しています。

分岐や粒子の数が複雑さを決める — 02・03・07

02の分岐数、03の残り火の数、07の砂粒の数は、それぞれ定数・ドラッグ速度・進捗率という値が「今フレーム何個描くか」に変換されたもので、動き方そのものを決めているわけではありません。02は既定で6本、03は速度に応じて1〜9個、07は進捗が上がるたびに1粒ずつと、生成される個数の増え方はそれぞれ違います。

間隔の値がリズムを決める — 05・06・09

05のジッター間隔、06の組み立て速度、09のティック間隔は、それぞれ値がいつ変わってよいかを制限しており、それがジッターを滑らかなイージングではなく震えとして見せています。05は既定70ms、09は40msという時間の単位で区切られ、06は秒あたりの追いつく速さ(ASSEMBLE_SPEED = 2.6)という比率で区切られており、単位は違っても「値がいつ切り替わってよいか」を制限する点は同じです。

ひびの角度値が、もう二度使い回される場所

02番は BTN_CANVAS_CRACK_BRANCH_COUNT = 6 を宣言し、buildTree() の中でその回数だけループしてひびの枝の本数を決めています。05番と08番はそれぞれ独立した定数 CRACK_VALUE_ECHO = 6 を持ち、コメントで02番の名前を直接示しています — 05番はそれをアーク線の lineWidth に掛け、08番はリップルの shiftMag を getImageData の中で計算するのに使います。ファイル間で値を読み込んでいるわけではなく、3つの独立した定数がたまたま同じ6を持っていて、それを結びつけているのは各ファイル自身のコメントだけです。

9つのフレームごとのピクセル数を足すと

以下の数値はすべてこの回の 측정.json から直接読み取ったもので、動いたフレームは連続するフレーム間の差分23回のうち何回動いたかを数えています。

項目 累積面積 平均強度 動いたフレーム
01 インク拡散 29.3% 35.8 23/23
02 亀裂分岐 20.1% 64.7 23/23
03 残り火トレイル 28.6% 45.8 23/23
04 リキッドメタル 51.6% 33.9 23/23
05 電気アーク 23.6% 54.0 23/23
06 煙の分解 22.7% 57.2 23/23
07 砂の積み上げ 21.7% 137.8 23/23
08 屈折リップル 17.1% 37.4 20/23
09 ノイズ砂嵐 22.2% 61.5 23/23

9つの中で最も面積を占めるのは04の融合面で、23分の23に届かなかったのは08だけです — 理由は下のFAQで説明します。

3つの定数を動かして、押し心地の違いを感じる

次の3項目にはそれぞれソースの中に既定値と入れ替えられる第二の値が用意されています。

項目 値 動かすとどうなるか
02 BTN_CANVAS_CRACK_BRANCH_COUNT 6 → 3 値を下げるとひびは一、二本の単純な分岐にとどまり、値を上げると分岐が増えてより複雑に割れたように見えるはずです。
05 BTN_CANVAS_ARC_JITTER_MS 70 → 30 値を短くするとアーク線は毎フレーム速く震えてジリジリした印象が強まり、値を伸ばすと震える間隔が広がりアークはゆっくり揺れる方に近づくはずです。
08 BTN_CANVAS_RIPPLE_DECAY_MS 320 → 220 値を短く縮めると波は一、二フレームで収まり軽くはじく感触だけが残り、値を伸ばすと波は長く広がり端まで何度か揺れるはずです。

9つがそれぞれ最初に壊れる場所

9つとも aria-pressed を含めどんなARIA状態も設定していません。すべてただの <button> に click を結びつけているだけなので、スクリーンリーダーはラベルは読み上げてもひびや積み上がりやエラー値の反転までは伝えません — 実際のフィードバックとして使うなら状態は自分で付け足す必要があります。

9つとも devicePixelRatio を読んでいないため、キャンバスの実ピクセルはCSS上のサイズのまま固定されています。高DPI画面ではインクの拡散グリッドもノイズのセルもリップルの getImageData サンプリングも、画面が本来表示できる解像度の半分で計算されていることになります。

9つすべての resize リスナーは render() を呼び直すだけで、canvas.width/canvas.height を再設定していません。CSS側でキャンバス要素を引き伸ばすレイアウトを組むと、固定解像度のバッファがそのまま引き伸ばされてぼやけます。

下のzipには9つすべての index.html が xbg2dm72 の奥に入っており、上の表と同じ状態で展開できます。

9つ以外でcss ボタン アニメーション おしゃれ系を探すなら

この9つを離れて探すと、見つかるものの多くは :active のボーダー色やスケール、影といったCSSトランジションです。中にはcss ボタン ホバー エフェクトとして単体のコードを公開しているものもありますが、他人の:activeスケールをフォークしても、getImageData によるリップルや再帰的なひびの木がどう組まれているかまでは分かりません — そこを埋めるためにこの9つを書きました。

こんな画面でこそ9つが生きる

押下状態の値がオンになったときだけひびが入る02番は、取り消せない削除ボタンにそのまま合います — 砕ける動き自体が確認そのものになるからです。09番も同じ発想を逆向きに使え、支払いが失敗してエラー値がオンになった瞬間ボタン自体が一瞬砂嵐になるので、小さな画面で見落とされがちな赤枠より気づかれやすいはずです。

07番の砂の積み上げは、ただの進捗パーセントをアップロードや書き出し中に目に見えて埋まっていくボタンへ変えます。別の行に置いた進捗バーと注意を奪い合うより、ボタンの中で完結する分だけ読みやすく、9つとも本物のボタンのテキストはそのまま保たれるので導入のためにデザインシステムを変える必要はありません。

押し込みバネ・CTAマイクロインタラクション・キャンバスシーンとの違い

ボタン 押し込み UI 9選 — 押し込みの値が戻る計算の9通り も9つそれぞれに一つの値を割り当てていますが、その値は押し込み量がCSSトランスフォームを通じて戻るバネで、キャンバスもピクセル単位の計算も getImageData もありません。05番のようにcss ボタン アニメーション 光る縁を計算で作るのとは別物です。

CTA ボタン デザイン 9選 — 伸びる影から跳ねるバーまで はもっと幅広いクリックのフィードバック — 伸びる影、跳ねるバー、広がる塗り — をコンバージョン重視のボタン向けにまとめたもので、9つのうち何割かはキャンバスなしでも成立します。

html5 canvas アニメーション9種 — 水滴とブラックホール は同じ系統のピクセル計算 — 水滴の屈折や歪みの場 — をボタンではなく画面全体のシーンで動かしており、クリックだけで起動するものでもありません。この9つはキャンバスをボタンのサイズに留め、クリック・キー・ドラッグでしか目を覚まさない点が違います。

prefers-reduced-motion がここで止めているもの

9つとも requestAnimationFrame ループを始める前に window.matchMedia('(prefers-reduced-motion: reduce)') を確認しており、条件に一致するとフレーム関数は二度と呼ばれず、ボタンを押したときは render() が一度だけ直接呼ばれるので状態の変化は見えたままです — ただしアニメーションではなく一枚だけ描かれた静止フレームとしてです。計算そのものは変わらず、拡散グリッドも分岐の木もピクセルのずれも一度は必ず走ります。減っているのは、本来なら毎秒20回以上描き直すはずだった requestAnimationFrame の繰り返しだけです。

9つを解凍する前に、よくある質問に答えられますか

この9つにcss ボタン ホバー エフェクトとして使えるCodePen版はありますか?

いいえ — 9つはこのページのライブデモとパスワード付きzipとしてのみ提供されており、別途フォークできる埋め込みはありません。

9つのどれかにライブラリやWebGL、CDNスクリプトは必要ですか?

いいえ。9つとも1枚の <canvas> とプレーンなJS、2Dコンテキストの呼び出しだけでできており、ライブラリもWebGLコンテキストもCDNスクリプトタグもどのファイルにも含まれていません。

なぜ08番だけ23分の23ではなく23分の20しか動いていないのですか?

BTN_CANVAS_RIPPLE_DECAY_MS は既定で320msあり、ampDecay は最後の数フレームが来る前に十分0へ近づいてしまうため、リップルのピクセルずれが計測の移動しきい値を下回ります。波自体は計算上まだ動いていますが、その時点では小さすぎて動いたピクセルとして記録されないだけです。

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

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