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

跳ねるボール アニメーション 9選 — 衝突と重力の物理計算

跳ねるボール アニメーションを9パターンまとめました。01番のボールは反発係数0.82で速度を交換し、04番のボールは同じ係数でも質量が違うと跳ね返る高さが変わり、05番のボールはポインターが近づくとその係数の代わりに反発力を受けて散らばる。

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

跳ねるボール アニメーション 9つ — 値ひとつが動きをどう変えるか

以下の9つはすべて同じキャンバスループの上で動いています。速度を積分し、衝突か境界を解決し、実際に動いたピクセルだけを描き直すという流れです。項目01はそのループが一番わかりやすく現れたボールで、反発係数ひとつで運動量を交換する単純なペアです。項目05になると同じループが壁の代わりにポインター位置を読み取り、項目09では重力・壁バウンド・ペア衝突を1つのヒーローシーンにまとめています — これは物理シミュレーション 作り方 9選 — バネから慣性スクラブまでがバネや慣性スクラブで使っているのと同じ重ね方です。以下の並びは単純なペアから重力・壁の減衰、質量差、ポインターの力、ドラッグの運動量、積み重なる生成、最後に複合ヒーローシーンへと、1項目ごとにループへ渡す情報を1つずつ増やしています。

01速度を交換する弾性衝突ボール

resolvePair() はボール同士が触れた瞬間に衝突方向に沿って質量と速度を読み取り、BALL_RESTITUTION 0.82 で運動量を交換します。そのため重いボールはほとんど動かず、軽いボールだけが大きく弾き飛ばされ、ビリヤードの球が画面の中でぶつかり合っているように見えます。

速度を交換する弾性衝突ボール
新しいタブで開く

01 · 速度を交換する弾性衝突ボール — 計測 累積面積 40.801% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

BALL_RESTITUTIONWALL_RESTITUTIONresolvePair
  function resolvePair(a, b) {
    var dx = b.x - a.x, dy = b.y - a.y;
    var dist = Math.hypot(dx, dy);
    var minDist = a.r + b.r;
    if (dist <= 0 || dist >= minDist) return;
    var nx = dx / dist, ny = dy / dist;
    var overlap = minDist - dist;
    var totalMass = a.mass + b.mass;
    a.x -= nx * overlap * (b.mass / totalMass);
    a.y -= ny * overlap * (b.mass / totalMass);
    b.x += nx * overlap * (a.mass / totalMass);
    b.y += ny * overlap * (a.mass / totalMass);

    var rvx = b.vx - a.vx, rvy = b.vy - a.vy;
    var vn = rvx * nx + rvy * ny;
    if (vn >= 0) return;
    var j = -(1 + BALL_RESTITUTION) * vn / (1 / a.mass + 1 / b.mass);
    var ix = j * nx, iy = j * ny;
    a.vx -= ix / a.mass; a.vy -= iy / a.mass;
    b.vx += ix / b.mass; b.vy += iy / b.mass;
    var strength = Math.min(1, Math.abs(vn) / 220);
    a.squash = Math.max(a.squash, strength);
    b.squash = Math.max(b.squash, strength);
    impacts.push({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, born: elapsed, strength: strength });
  }

項目01は面積40.8%・強度平均106.2で、23/23 フレームが動きました。

02落下して床で静まる重力落下ボール

stepSim() は毎フレーム GRAVITY を下向きの速度に加算してボールを落とし、床に触れるたびにその速度へ FLOOR_RESTITUTION を掛けて弱めます。速度が SLEEP_THRESHOLD を下回るとボールは静止し、この落下から静止までの流れがローディング画面の背景として自然に見えます。

落下して床で静まる重力落下ボール
新しいタブで開く

02 · 落下して床で静まる重力落下ボール — 計測 累積面積 15.354% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

FLOOR_RESTITUTIONSLEEP_THRESHOLDallSettled
  function stepSim(dtMs, nowMs) {
    var dtS = dtMs / 1000;
    for (var i = 0; i < balls.length; i += 1) {
      var b = balls[i];
      if (nowMs < b.startAt) continue;
      if (b.settled) {
        b.squash = Math.max(0, b.squash - dtS * 6);
        continue;
      }
      b.vy += GRAVITY * dtS;
      b.y += b.vy * dtS;
      var floor = CALC_H - b.r;
      if (b.y >= floor) {
        b.y = floor;
        if (Math.abs(b.vy) < SLEEP_THRESHOLD) {
          b.vy = 0;
          b.settled = true;
          b.squash = 0.5;
          impacts.push({ x: b.x, y: floor, born: nowMs, strength: 0.35 });
        } else {
          impacts.push({ x: b.x, y: floor, born: nowMs, strength: Math.min(1, Math.abs(b.vy) / 320) });
          b.squash = Math.max(b.squash, Math.min(0.85, Math.abs(b.vy) / 300));
          b.vy = -b.vy * FLOOR_RESTITUTION;
        }

項目02は面積15.4%・強度平均103.7で、20/23 フレームだけが動きました — 静まった後は残りの時間ボールが止まったままだからです。

03壁に当たるたびに力を失う反発係数の壁バウンドボール

画面端に触れた瞬間に stepSim() の境界判定がその軸の速度を反転させて WALL_RESTITUTION を掛けるため、跳ねるたびに少しずつ勢いが弱まります。合計速度が MIN_SPEED を下回ると、これとは別の kick() がシード乱数で新しい角度と速度を選んで再び弾き飛ばし、画面に収まったまま動き続けます。

壁に当たるたびに力を失う反発係数の壁バウンドボール
新しいタブで開く

03 · 壁に当たるたびに力を失う反発係数の壁バウンドボール — 計測 累積面積 35.015% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

KICK_SPEEDMIN_SPEEDkick
  function stepSim(dtMs) {
    var dtS = dtMs / 1000;
    for (var i = 0; i < balls.length; i += 1) {
      var b = balls[i];
      b.x += b.vx * dtS;
      b.y += b.vy * dtS;
      var hit = false;
      if (b.x - b.r < 0) { b.x = b.r; b.vx = -b.vx * WALL_RESTITUTION; hit = true; }
      if (b.x + b.r > CALC_W) { b.x = CALC_W - b.r; b.vx = -b.vx * WALL_RESTITUTION; hit = true; }
      if (b.y - b.r < 0) { b.y = b.r; b.vy = -b.vy * WALL_RESTITUTION; hit = true; }
      if (b.y + b.r > CALC_H) { b.y = CALC_H - b.r; b.vy = -b.vy * WALL_RESTITUTION; hit = true; }
      if (hit) {
        b.hits += 1;
        b.squash = Math.min(1, Math.hypot(b.vx, b.vy) / 260 + 0.25);
      }
      b.squash = Math.max(0, b.squash - dtS * 3.4);
      var speed = Math.hypot(b.vx, b.vy);
      if (speed < MIN_SPEED) kick(b);
    }
  }

項目03は面積35.0%・強度平均96.4で、23/23 フレームが動きました。

04質量差で跳ねる高さが分かれるボール

restitutionFor() はボールごとの質量を MASSES の値からそのまま受け取り(半径はその半分として別に導かれます)、BASE_MASS_REST に BALL_MASS_BOUNCE_FACTOR を掛けた値をその質量で割るため、軽いボールは反発係数が0.95近くになり、重いボールは0.22近くまで下がります。同じ高さから同じ瞬間に4つのボールを落とすことで、この一つの変数だけを比較できます。

質量差で跳ねる高さが分かれるボール
新しいタブで開く

04 · 質量差で跳ねる高さが分かれるボール — 計測 累積面積 11.113% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

BALL_MASS_BOUNCE_FACTORBASE_MASS_RESTrestitutionFor
  function restitutionFor(mass) {
    return Math.max(0.22, Math.min(0.95, BASE_MASS_REST * BALL_MASS_BOUNCE_FACTOR / mass));
  }

  var balls = [];
  function reset() {
    balls = [];
    for (var i = 0; i < MASSES.length; i += 1) {
      balls.push({
        x: CALC_W * XPOS[i],
        y: 26,
        vy: 0,
        mass: MASSES[i],
        r: MASSES[i] * 0.5,
        color: COLORS[i],
        rest: restitutionFor(MASSES[i]),
        squash: 0,
        settled: false
      });
    }
    impacts = [];
  }
  var impacts = [];

  function stepSim(dtMs, nowMs) {

項目04は面積11.1%・強度平均108.9で、23/23 フレームが動きました — ボールが細い縦のレーンから外へ出ないため、9項目の中で最も面積が小さくなっています。

05ポインターが近づくと散らばる反発ボール

applyPointer() は毎フレームすべてのボールとポインターの距離を測り、POINTER_REPEL_RADIUS の内側では距離が近いほど強く押し出し、半径の外にいるボールは自分の定位置を中心にゆっくりとしたサイン波で漂います。この組み合わせによってボールがカーソルを積極的によけているように見えます。

ポインターが近づくと散らばる反発ボール
新しいタブで開く

05 · ポインターが近づくと散らばる反発ボール — 計測 累積面積 21.441% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

POINTER_REPEL_RADIUSREPEL_FORCEDRIFT_SPEED
  function applyPointer(px, py, dtS) {
    for (var i = 0; i < balls.length; i += 1) {
      var b = balls[i];
      var dx = b.x - px, dy = b.y - py;
      var dist = Math.hypot(dx, dy) || 0.001;
      if (dist < POINTER_REPEL_RADIUS) {
        var force = REPEL_FORCE * (1 - dist / POINTER_REPEL_RADIUS) / dist;
        b.vx += dx * force * dtS;
        b.vy += dy * force * dtS;
        b.squash = Math.min(1, b.squash + (1 - dist / POINTER_REPEL_RADIUS) * 0.6);
      }
    }
  }

  function stepSim(dtMs, nowMs, px, py) {
    var dtS = dtMs / 1000;
    applyPointer(px, py, dtS);
    for (var i = 0; i < balls.length; i += 1) {
      var b = balls[i];
      var homeX = b.hx + Math.cos(nowMs / 1000 * DRIFT_SPEED + b.phase) * 10;
      var homeY = b.hy + Math.sin(nowMs / 1000 * DRIFT_SPEED * 0.8 + b.phase) * 10;
      var pull = 0.9;
      b.vx += (homeX - b.x) * pull * dtS;
      b.vy += (homeY - b.y) * pull * dtS;
      b.vx -= b.vx * Math.min(1, DAMPING * dtS);

項目05は面積21.4%・強度平均88.1で、23/23 フレームが動きました。

06ポインターに引き寄せられて回るボール

stepSim() は毎フレーム ATTRACT_K を距離で割ったポインター方向への引力だけを計算し、initBalls() が生成時に一度だけ与えた横向きの初期速度(orbitSpeed、ATTRACT_K の平方根)が DAMPING の減衰の下でも残り続けるため、ボールはポインターに吸い込まれず一定距離を回り続けます。カーソルを追いかける小さな衛星のような動きで、寄っていくのではなく周りを回る装飾に向いています。

ポインターに引き寄せられて回るボール
新しいタブで開く

06 · ポインターに引き寄せられて回るボール — 計測 累積面積 20.388% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ATTRACT_KMIN_DISTorbitSpeed
  function stepSim(dtMs, px, py) {
    var dtS = dtMs / 1000;
    for (var i = 0; i < balls.length; i += 1) {
      var b = balls[i];
      var dx = px - b.x, dy = py - b.y;
      var dist = Math.max(MIN_DIST, Math.hypot(dx, dy));
      var accel = ATTRACT_K / dist;
      var ax = (dx / dist) * accel, ay = (dy / dist) * accel;
      b.vx += ax * dtS;
      b.vy += ay * dtS;
      b.vx -= b.vx * DAMPING * dtS;
      b.vy -= b.vy * DAMPING * dtS;
      b.x += b.vx * dtS;
      b.y += b.vy * dtS;
      var speed = Math.hypot(b.vx, b.vy);
      b.squash = Math.min(0.5, speed / 900);
    }
  }

  function draw(px, py, nowMs) {

項目06は面積20.4%・強度平均77.2で、23/23 フレームが動きました。

07ドラッグした速度のまま飛ぶ投げボール

recordSample() は直近 SAMPLE_WINDOW_MS の間のポインター位置だけを保持し、指を離した瞬間に releaseVelocity() が一番古いサンプルと一番新しいサンプルの差から速度を計算します。そのため投げた瞬間の速度は実際にドラッグした感覚とそのまま一致し、最後の1フレームだけの差分ではかくつく動きになってしまいます。

ドラッグした速度のまま飛ぶ投げボール
新しいタブで開く

07 · ドラッグした速度のまま飛ぶ投げボール — 計測 累積面積 16.193% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SAMPLE_WINDOW_MSKEY_IMPULSEreleaseVelocity
  function recordSample(x, y, t) {
    samples.push({ x: x, y: y, t: t });
    while (samples.length && t - samples[0].t > SAMPLE_WINDOW_MS) samples.shift();
  }

  function releaseVelocity(nowT) {
    if (samples.length < 2) return { vx: 0, vy: 0 };
    var a = samples[0], b = samples[samples.length - 1];
    var dt = Math.max(16, b.t - a.t) / 1000;
    return { vx: (b.x - a.x) / dt, vy: (b.y - a.y) / dt };
  }

  function throwBall(vx, vy) {
    ball.vx = vx; ball.vy = vy;
    if (Math.hypot(vx, vy) > 1) ball.heading = Math.atan2(vy, vx);
  }

  function stepSim(dtMs) {

項目07は面積16.2%・強度平均59.8(9項目中もっとも低い平均強度)で、23/23 フレームが動きました。

08上から降り注ぎ床に積み重なる落下ボール

spawn() は SPAWN_INTERVAL_MS ごとに新しいボールを1つ上から追加し、2つのボールが重なると stepSim() が SEPARATE_ITERS 回の分離処理を行って、それぞれの中心を結ぶ線に沿って重なった分の半分ずつ押し離します。この繰り返しの分離があるおかげで、落ちてきたボールの山が互いに沈み込まずに積み重なります。

上から降り注ぎ床に積み重なる落下ボール
新しいタブで開く

08 · 上から降り注ぎ床に積み重なる落下ボール — 計測 累積面積 33.263% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SPAWN_INTERVAL_MSSEPARATE_ITERSHOLD_MS
    for (var iter = 0; iter < SEPARATE_ITERS; iter += 1) {
      for (var a = 0; a < balls.length; a += 1) {
        for (var c = a + 1; c < balls.length; c += 1) {
          var ba = balls[a], bc = balls[c];
          var dx = bc.x - ba.x, dy = bc.y - ba.y;
          var dist = Math.hypot(dx, dy) || 0.001;
          var minDist = ba.r + bc.r;
          if (dist < minDist) {
            var overlap = (minDist - dist) / 2;
            var nx = dx / dist, ny = dy / dist;
            ba.x -= nx * overlap; ba.y -= ny * overlap;
            bc.x += nx * overlap; bc.y += ny * overlap;
            if (ba.y + ba.r > CALC_H) ba.y = CALC_H - ba.r;
            if (bc.y + bc.r > CALC_H) bc.y = CALC_H - bc.r;
          }
        }
      }
    }
    if (balls.length >= MAX_BALLS) {
      if (holdSince === null) holdSince = nowMs;
      else if (nowMs - holdSince > HOLD_MS) { resetAll(); }
    }
  }

項目08は面積33.3%・強度平均98.9で、23/23 フレームが動きました。

09複数のボール物理を一画面にまとめたヒーローシーン

ここでの stepSim() は HERO_GRAVITY と項目05のポインター反発力、項目01の resolvePair() による衝突処理を同じループの中でまとめて実行し、それぞれの定数を HERO_ 接頭辞で分けて見分けやすくしています。3つの技法を1つの場面に重ねることでヒーロー背景として十分な密度になり、どれか1つだけでは周囲のページより物足りなく見えてしまいます。

複数のボール物理を一画面にまとめたヒーローシーン
新しいタブで開く

09 · 複数のボール物理を一画面にまとめたヒーローシーン — 計測 累積面積 39.741% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

HERO_POINTER_FORCEHERO_WALL_RESTITUTIONHERO_GRAVITY
  function stepSim(dtMs, nowMs, px, py) {
    var dtS = dtMs / 1000;
    for (var i = 0; i < balls.length; i += 1) {
      var b = balls[i];
      b.vy += HERO_GRAVITY * dtS;
      var dx = b.x - px, dy = b.y - py;
      var dist = Math.hypot(dx, dy) || 0.001;
      if (dist < HERO_POINTER_REPEL_RADIUS) {
        var force = HERO_POINTER_FORCE * (1 - dist / HERO_POINTER_REPEL_RADIUS) / dist;
        b.vx += dx * force * dtS;
        b.vy += dy * force * dtS;
      }
      b.x += b.vx * dtS;
      b.y += b.vy * dtS;
      var hit = false;
      if (b.x - b.r < 0) { b.x = b.r; b.vx = -b.vx * HERO_WALL_RESTITUTION; hit = true; }
      if (b.x + b.r > CALC_W) { b.x = CALC_W - b.r; b.vx = -b.vx * HERO_WALL_RESTITUTION; hit = true; }
      if (b.y - b.r < 0) { b.y = b.r; b.vy = -b.vy * HERO_WALL_RESTITUTION; hit = true; }
      if (b.y + b.r > CALC_H) {
        b.y = CALC_H - b.r;
        b.vy = -b.vy * HERO_WALL_RESTITUTION;
        if (Math.abs(b.vy) < 24) b.vy = 0;
        hit = true;
      }
      if (hit) b.squash = Math.max(b.squash, Math.min(0.75, Math.hypot(b.vx, b.vy) / 300));

項目09は面積39.7%・強度平均99.9で、23/23 フレームが動き、単一フレームの最大値は16.7%と9項目の中で最も大きな一撃でした。

9つの跳ねるボールパターンを面積と強度で測る

下の表は、キャプチャ間で画面が塗り直された面積の割合と、塗り直された場所での平均的な明るさの変化を並べたものです。項目04は塗り直された1ピクセルあたりの強度がもっとも高く、項目07はもっとも低く、項目09は全区間の平均は中間でありながら単一フレームで最も広い面積を塗り直しました。ボール 弾む アニメーションは面積が20%未満・強度が100未満のときに一番落ち着いて見え、9項目のうち半分ほどがこの範囲に収まっています。

パターン 累積面積 平均強度 動いたフレーム
01 弾性衝突ペア 40.8% 106.2 23/23
02 重力落下と静止 15.4% 103.7 20/23
03 壁反発の減衰 35.0% 96.4 23/23
04 質量差の跳ね高さ 11.1% 108.9 23/23
05 ポインター反発散乱 21.4% 88.1 23/23
06 ポインター引力軌道 20.4% 77.2 23/23
07 ドラッグ投げの運動量 16.2% 59.8 23/23
08 生成の積み重ね 33.3% 98.9 23/23
09 ヒーロー複合シーン 39.7% 99.9 23/23

値が動きを変える場所 — 反発係数・質量・ポインター半径

下の3つの定数がほとんどの表情を決めています。BALL_RESTITUTION は項目01のペアが触れ合った後にどれだけ速度を保つかを、BALL_MASS_BOUNCE_FACTOR は項目04の軽いボールと重いボールがどれだけ離れた高さに落ち着くかを、POINTER_REPEL_RADIUS は項目05のボールがどれだけ早くポインターの前を空け始めるかを決めます。3つとも描かれるもの自体は変えず、同じ物理ループがどれだけ弾み、どれだけ不均等に、どれだけ慎重に見えるかを変えるだけですが、これこそがただ同じ落下を繰り返すだけのアニメーションと、ブックマークしておきたい重力 アニメーションの違いのほとんどを占めています。javascript ブロック崩し 反射角で変わる9パターンも同じようなフレームごとのループの上で、パドルに当たった時の反射角という別の定数を調整しています。

どこで壊れるか — 動作を減らす設定・狭い画面・ボール数が多いとき

この9つすべてで matchMedia を使って prefers-reduced-motion を読み取り、キャンバス自体を止めるのではなく読み込み時に自動再生用の .is-demo クラスを外しています。そのため項目06は実際にポインターが動いたときだけ軌道を描き、項目07も実際にドラッグしたときだけボールを投げます。fit() は320×200の計算空間をキャンバス要素の実測サイズに合わせて縮小するため、狭いスマートフォン幅では物理計算自体は変えずに各ボールの見た目のサイズだけが小さくなります。低スペック端末で特に注意したいのは項目08で、MAX_BALLS 個のボールが積み重なった後も stepSim() は毎フレームすべてのペアに対して SEPARATE_ITERS 回の分離処理を行い続けるため、重力や描画よりも先にこのペアごとのチェックでフレームが落ちやすくなります。これら9つのビルドの裏にある index.html/style.scss のペア(nqxed599)は、これだけ少ないボール物理アニメーションのコードで9つ同時に動かせることを確かめる一番早い方法です。

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

バリエーション 値の変更 感触
弾性衝突の反発係数を調整する 01 BALL_RESTITUTION 0.82 → 0.55 値を下げると2つのボールがぶつかった後すぐにエネルギーを失って寄り添うように止まり、上げると跳ねる勢いが長く残ります。
質量と跳ね高さの係数を調整する 04 BALL_MASS_BOUNCE_FACTOR 1.4 → 2.2 値を下げると重いボールと軽いボールの跳ね高さの差が縮まり、上げると軽いボールだけがはるかに高く跳ねるようになります。
ポインター反発半径を調整する 05 POINTER_REPEL_RADIUS 90 → 160 値を下げるとポインターがかなり近づいたときだけボールが散らばり、上げるとポインターが来る前から早めに場所を空けるようになります。

9項目すべてが自分のスクリプトの先頭で一度だけ prefers-reduced-motion を確認し、個々のプロパティを止めるのではなく自動再生クラスを外すことで応じているため、動作を減らしたい訪問者にも空白のキャンバスではなく静止したボールが見えます。矢印キーとEnterまたはSpaceキーは9項目のうち4つ、項目05・06・07・09で使え、これらは tabindex="0" を設定して keydown を監視し、仮想ポインターを動かす、軌道を揺らす、持っているボールを投げる、ヒーローシーンを視点移動するといった操作をマウスなしで行えます。CanvasRenderingContext2D、requestAnimationFrame()、prefers-reduced-motion はMDNに文書化されています。

衝突 アニメーションは何を計算しているのか?

この9つはどれも物理エンジンを読み込んでいません。それぞれが自前の単純なボール配列を持ち、実際に経過した時間に対して16msの固定 FIXED_DT を積み上げながら1コマずつ進め、重力・壁・ポインターの力・ペア衝突を Math.hypot() とベクトル計算の数行で解決しています — 物理エンジンが自分のAPIの中に隠している計算と同じ種類のものを、隠さずそのまま書いているだけです。9項目のうち8つは開始位置や角度をローカルの seedRand() で決めているため再読み込みしても毎回同じ見た目になりますが、項目04だけは例外で、4つのボールが固定位置から始まるため乱数シードそのものを必要としません。web ルーレット — 弾いて回し摩擦で止まる回転盤キャンバス9種も、複数のボールの代わりに1枚の回転盤の上で似た摩擦と反発係数の計算を行っています。

FAQ でよくある質問

これらの9つはマウス 追従 アニメーション cssの代わりにそのまま使えますか?

はい。どのデモもスクリプトを内蔵した単一の index.html で完結しているため、ファイルと対になる style.scss をコピーするだけで動きます。CSSの @keyframes やビルドツールには依存していません。

項目07の平均強度が他より低いのはなぜですか?

項目07は場面全体ではなく1つのボールだけを毎フレーム描き直すため、面積は項目02や項目05と同じくらいでも、1フレームごとの明るさの変化はそれより小さくなります — 23/23 フレームで平均した強度59.8は9項目の中で最も低い値です。

これら9つのような跳ねるボール アニメーションに物理エンジンは必要ですか?

必要ありません。ここにある9つはすべて重力・壁・質量・ポインターの力を固定タイムステップとわずかなベクトル計算だけで解決していて、これは物理エンジンが内部で行っているのと同じ計算を、インポートせずそのまま stepSim() に書き込んでいるだけです。

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

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