ランダム
ランダムの作り方
9 個のドットが、予測できない順に弾む。このランダムに見える順番の正体は、9 マスに弾む順番を書き込んだ固定の表 1 枚だ。動き自体はキーフレームで打ったクリップ 1 本で、9 個全てが同じクリップを一定の間隔でずらして再生している。ばねに見えるアンダーシュートまで含めて、その作りを短い関数に書き起こして読み解く。
- Published
- 2026年6月10日
- Topics
- Scramble · Determinism · Easing · SVG
クリップを 1 本だけ作る
- 静止半径
restRadiusからpeakRadiusへ弾む underRadiusまで落としすぎる — わざと低く打ったキーフレームholdRadiusに拾い、そのまま長く保つ- 最後は単一イーズで静止半径
restRadiusへ戻る
クリップの中身は半径のキー 6 個と、区間ごとのイージングカーブ 5 本だけ。どのフレームにキーを置くかは自分で決める。コードに起こすと、キーの間を埋める関数ひとつと、自分で書くもの 2 つ — クリップに打つキーフレームと、順番表 1 枚になる。ばらばらに見える正体はこの順番表 1 枚で、乱数も種も使っていない。
// 打ったキーフレームのあいだをカーブで埋めて補間する — この記事の動きはぜんぶこの形で書ける
function keyAt(keys, frame) {
if (frame <= keys[0].frame) return keys[0].value; // 最初のキー以前 — 最初の値のまま
if (frame > keys.at(-1).frame) return keys.at(-1).value; // 終わりより後 — 最後の値のまま
const next = keys.findIndex((key) => frame <= key.frame); // いま向かっている先のキーの番号
const a = keys[next - 1], b = keys[next]; // 手前のキーとの 2 個で区間が決まる
const t = (frame - a.frame) / (b.frame - a.frame); // 区間の進み具合 0〜1
return a.value + (b.value - a.value) * a.curve(t); // 差をカーブの形で進める
}keyAt に渡すキーフレームは、それぞれ「次の区間を埋めるカーブ」を持つ。カーブは CSS の cubic-bezier() と同じ 4 つの数で、bezier-easing のような既製のライブラリ がそのまま受け取る。範囲の外では端の値を返す — クリップが始まる前と終わった後の静止は、それだけで出る。
// 動きの部品はクリップ 1 本と、順番を書き込んだ表 1 枚だけ。
// 小文字ではじまる裸の名前(clipStart や restRadius)は、自分の画面に合わせて決める数 —
// この記事が持ち帰ってほしいのは値ではなく、この「形」だ。
const linear = (t) => t; // 値が同じ区間用 — 保ちはこれで出る
// 区間ごとのカーブ — どれも CSS の cubic-bezier() と同じ規格。好みの 4 つの数で決める
const popEase = cubicBezier(...popHandles); // 静止 → 頂点
const dropEase = cubicBezier(...dropHandles); // 頂点 → 落としすぎ
const recoverEase = cubicBezier(...recoverHandles); // 落としすぎ → 拾い
const settleEase = cubicBezier(...settleHandles); // 保ちおわり → 静止
const clipKeys = [ // 値 = 半径。各キーフレームのカーブは「次のキーへ向かう区間」のもの
{ frame: 0, value: restRadius, curve: popEase }, // → 弾む
{ frame: peakFrame, value: peakRadius, curve: dropEase }, // → 落としすぎる
{ frame: underFrame, value: underRadius, curve: recoverEase }, // → 拾う
{ frame: holdStart, value: holdRadius, curve: linear }, // → そのまま保つ
{ frame: holdEnd, value: holdRadius, curve: settleEase }, // → 静止へ戻る
{ frame: clipEnd, value: restRadius },
];
// 「ランダム」の正体はこの表 1 枚 — 9 マスに何番目かを 1 回ずつ書き込んだ固定の順番表。
// fireOrder は乱数を使わず、自分で 1 度だけスクランブルして書く
// フレーム番号を渡すと 9 個の半径を返す関数 — 再生側は frame を loopFrames(ループ 1 周)で割った余りにする
function radiiAt(frame) {
return fireOrder.flat().map((rank) => {
const local = frame - clipStart - rank * stepFrames; // 自分の番のぶん遅れて再生
return keyAt(clipKeys, local); // キーの間を区間カーブで埋める
});
}同じ値に挟まれた区間は、カーブが何であれ値が動かない。clipKeys の長い保持はそうやって作られている — 保持専用の仕組みは何もない。
置き方は 9 個をほぼ等間隔の 3×3 格子に固定するだけで、動くのは半径だけだ。requestAnimationFrame で経過秒に毎秒のコマ数を掛けて frame を出し、loopFrames で余りを取れば、上のデモと同じ作りのループになる。
弾み終わりの戻りは、静止半径 restRadius へ正確に着地する。元の動画はわずかな尾を引いて restRadius まで戻りきらないが、最後に弾む複製のクリップはいちばん遅く始まり、終わるのはループの末尾ぎりぎり — ループの長さをほぼ使い切っていて、尾まで追いかけるとループの継ぎ目が壊れる。だから尾は捨て、最終フレームが先頭とぴったり一致する継ぎ目のないループを選んだ。