視差
視差の作り方
7 つの円が上下に弾み、奥行きがあるように見える。この奥行きの正体は、全部が同じ 1 本の波を共有し、振れ幅を円ごとに変えていることだ。大きく振れる円ほど手前に、小さく振れる円ほど奥に見える。コードつきで、組み立ての考え方から順に見ていく。
- Published
- 2026年6月13日
- Topics
- Parallax · Amplitude · Easing · SVG
波 1 本と円ごとの振れ幅を用意する
- 動かない 7 つの円 — 位置も大きさも固定で、上下にだけ動く
- 共有する波 1 本 — 上 → 下 → 上 の 3 キーで、各キーで速度ゼロ・下りと上りに別のイージングカーブ
- 円ごとの振れ幅
ampと中心mid— 同じ波に自分のampを掛け、midから上下させる
// 動かしているのは上下の波 1 本だけ。7 つの円はそれぞれの振れ幅でその波を読む。
// 小文字ではじまる裸の名前(periodFrames や bottomKeyFrame)は、自分の画面に
// 合わせて決める数 — この記事が持ち帰ってほしいのは値ではなく、この「形」だ。
const wrap = (value, span) => ((value % span) + span) % span;
const sharedWave = (designTime) => { // 上 → 下 → 上 を行き来する波。下りと上りで別のカーブ
const descEase = cubicBezier(...descHandles); // 下り(上→下)の緩急
const ascEase = cubicBezier(...ascHandles); // 上り(下→上)の緩急
const t = wrap(designTime, periodFrames);
if (t <= bottomKeyFrame) return -1 + 2 * descEase(t / bottomKeyFrame); // 上から下へ
return 1 - 2 * ascEase((t - bottomKeyFrame) / (periodFrames - bottomKeyFrame)); // 下から上へ
};
function dotsAt(frame) { // フレーム番号 → 7 つの円の置き場所。波を計算し、円ごとに振れ幅で読む
const wave = sharedWave(frame + capturePhase);
return circles.map((c) => ({
cx: c.cx,
cy: c.mid + c.amp * wave,
r: c.r,
}));
}descEase と ascEase がこの動きの顔だ。中身は CSS の cubic-bezier() と同じ 4 つの数で、descHandles と ascHandles に置く数は bezier-easing のような既製のライブラリ にそのまま渡せる。波は上の端と下の端を往復する値で、その間をカーブが埋める。下り(上→下)と上り(下→上)でわざと別のカーブを持たせてある — 1 本を両方に使い回すと、元の動画とのずれがはっきり広がった。
上下に弾む 7 つの円は、sharedWave という同じ 1 本の波を読んでいる。違うのは円ごとの amp だけで、出だしをずらすコードも、円ごとに別の波もない。大きい円ほど大きく動くように見えるが、大きさと振れ幅をそろえる法則はない — amp は円ごとに手で決めた値だ。元の動画でも大きさと振れ幅の比はばらばらで、きれいな比例にはならなかった。
画面に置くのは円を 7 つ、返ってきた cx・cy・r のとおりに描くだけだ。動かすのは requestAnimationFrame で、経過秒に毎秒のコマ数を掛けてフレーム番号を出し、periodFrames で余りを取って dotsAt に渡せば、このデモと同じ作りのループになる。波は 1 周のおわりにちょうど出発の上へ戻るので、ループの継ぎ目は出ない。
遊ぶなら amp を書き換える。全部の amp をそろえると奥行きは消えてただの一斉の上下動になり、ばらばらにすると奥行きが戻る。波を読む先を上下動から拡大縮小や濃さに変えても、同じ 1 本の波を共有する仕組みはそのまま使える。