残像
残像の作り方
2 個のドットが円を 1 周するあいだ、速い区間では尾がコマ送りのように分かれ、遅い区間ではひとつながりに溶けて、止まると消える。尾の 1 枚 1 枚の正体は、同じ回転を一定のわずかな遅れで再生した過去の自分だ。動かしているのは回転 1 本で、分かれるか溶けるかは回転の速さで決まる。回転 1 本と複製 9 枚を、短い関数に書き起こして読み解く。
- Published
- 2026年6月11日
- Topics
- Echo · Time Shift · Easing · SVG
回転 1 本と複製 9 枚を用意する
- ドットの半径は
dotRadius— 中心をはさんで正反対に 2 個、静止角はrestAnglesに並べる - 軌道の半径は
orbitRadius - 回転キーは 2 個 — 出だしは静止角のまま、
sweepFramesフレーム目でちょうど 1 周先、イージングカーブ 1 本 sweepFramesからloopFramesまでは静止 — 回転キーはここにはない- 複製は
copies枚 — 上のデモでは 9 枚、1 枚ごとにdelayFramesぶん過去・不透明度はfadePerCopy倍
部品はこれで全部だ。組み立ては、フレーム番号を渡すと描く円の一覧を返す関数をひとつ書くだけ — 回転を 1 本だけ計算して、複製にはそれを過去にずらして読ませる。
// 動かしているのは回転 1 本。残りは全部、その読み替え。
// 小文字ではじまる裸の名前(copies や delayFrames)は、自分の画面に合わせて決める数 —
// この記事が持ち帰ってほしいのは値ではなく、この「形」だ。
const ease = cubicBezier(...easeHandles); // CSS の cubic-bezier() と同じ規格 — 緩急は好みの数で
const rad = (deg) => (deg / 360) * 2 * Math.PI; // 度をラジアンに直す
// 置き場所も自分で決める数 — 軌道の中心(center)・軌道の半径(orbitRadius)・ドットの半径(dotRadius)。
// restAngles には、正反対の 2 個ぶんの静止角を並べる
// フレーム番号を渡すと、描く円の一覧を返す関数(loopFrames フレームでループ)
function dotsAt(frame) {
return restAngles.flatMap((rest) =>
Array.from({ length: copies + 1 }, (_, copy) => { // 0 = 本体、あとは複製
const past = frame - copy * delayFrames; // 複製は少しだけ過去を読む
const t = Math.min(Math.max(past / sweepFrames, 0), 1); // 進み具合を 0〜1 に直す
return {
angle: rest + 360 * ease(t), // 1 周ぶん回す
opacity: fadePerCopy ** copy, // 1 枚ごとに fadePerCopy 倍薄く
};
})
);
}ease がこの動きの顔を握っている。中身は CSS の cubic-bezier() とまったく同じ計算で、easeHandles に置く 4 つの数は bezier-easing のような既製のライブラリ にそのまま渡せる。
カーブの形は、出だしを深く溜めて、行程のなかばで最速に達し、終わり際を長くなだらかに着地する。この 4 つの数を変えると緩急だけが変わり、尾の理屈はそのまま残る。
画面に置くときは、返ってきた角度を、中心(x も y も center)・半径 orbitRadius の円周上の座標へ — x = center + orbitRadius * Math.cos(rad(angle))、y = center + orbitRadius * Math.sin(rad(angle))。中心も半径も自分の画面に合わせて決める数で、rad() は度をラジアンに直す 1 行だ。
あとは requestAnimationFrame で、経過秒に毎秒のコマ数を掛けて frame を出し、loopFrames で余りを取れば、このデモと同じ作りのループになる。SVG の circle でも canvas でもいい。
この組み立てのいちばんおいしいところは、回転が主役である必要すらないことだ。angle を計算している行を横移動や拡大縮小の式に差し替えても、「少しだけ過去を読む複製を重ねる」構造はそのまま通用する。ループする動きなら何にでも、同じ理屈で尾が生える。