連動
連動の作り方
中央の正方形が四分の一回転するあいだ、両脇はぴったり結ばれたように滑り、ダイヤのあたりで全部が止まって見える。両脇の位置は毎フレーム、「回転する正方形にちょうど触れるギリギリ」を計算して決まる — 動かしているのは中央の回転 1 本で、両脇はその回転に幾何で従う。止まって見える瞬間も、接点の進みがそこでいったん滞るからだ。回転 1 本と接触の法則 1 行を、短い関数に書き起こして読み解く。
- Published
- 2026年6月10日
- Topics
- Tangency · Geometry · Easing · SVG
回転 1 本と法則 1 行を書く
- 回転キーは 3 個 — 直立からダイヤへ、また直立へ(どのフレームに打つかは
rotationKeysで自分で決める) - イージングカーブは区間ごとに 1 本、計 2 本
- 両脇の正方形にキーフレームはゼロ
- 押し出される距離はゼロから最大まで増えて、またゼロへ戻る
これをコードに起こす。部品は、キーの間をカーブで埋める関数と、回転に打つキーフレーム、そして法則の 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 に渡すキーフレームは、1 個ごとに「次の区間を埋めるカーブ」を持つ。中身は CSS の cubic-bezier() とまったく同じ計算で、climbHandles と descendHandles に置く 4 つの数は bezier-easing のような既製のライブラリ にそのまま渡せる。範囲の外では端の値を返すから、最初のキーより前の直立は何も書かなくても出る。
// 動かしているのは中央の回転 1 本。両脇はそれを接触の法則で読むだけ。
// 小文字ではじまる裸の名前(riseStart や side)は、自分の画面に合わせて決める数 —
// この記事が持ち帰ってほしいのは値ではなく、この「形」だ。
const climbCurve = cubicBezier(...climbHandles); // CSS の cubic-bezier() と同じ規格 — 緩急は好みの 4 つの数で
const descendCurve = cubicBezier(...descendHandles); // 帰り区間用 — 同じ規格でもう 1 本
// 回転に打つキーフレーム: 直立 → ダイヤ → 四分の一回転で直立に戻る。打ってあるのはこれだけ
const rotationKeys = [
{ frame: riseStart, value: 0, curve: climbCurve }, // → ダイヤへ上る
{ frame: diamondFrame, value: diamondAngle, curve: descendCurve }, // → 直立へ下りる
{ frame: loopEnd, value: quarterTurn },
];
// フレーム番号を渡すと、中央の角度と両脇の位置を返す関数
function squaresAt(frame) {
const angle = keyAt(rotationKeys, frame);
// 法則 1 行: 回転した正方形の横幅の半分は「ダイヤからの離れ具合」で決まる
const phi = Math.abs(((angle % quarterTurn) + quarterTurn) % quarterTurn - diamondAngle);
const halfWidth = (side / Math.SQRT2) * Math.cos((phi * Math.PI) / 180);
const push = Math.max(0, halfWidth - side / 2 - gap); // 隙間 gap を保って押し出す
return {
center: { rotate: angle },
left: { x: leftHome - push }, // 両脇にキーはゼロ — 法則は左右対称
right: { x: rightHome + push },
};
}((angle % quarterTurn) + quarterTurn) % quarterTurn は、角度が何回転ぶん進んでいても、ゼロから quarterTurn までのひと区画へ折りたたむ書き方だ。横幅の式が「diamondAngle からの離れ具合」で書かれているのはそのためで、どんな角度を入れても成り立つ。
辺 side の正方形は、まっすぐ立っているとき横幅も side のまま。ダイヤでは対角線が横を向き、横幅は Math.SQRT2 倍に広がる — 増えたぶんが押し出しになり、ゼロから最大へ、またゼロへ往復する。右も左も同じ式で対称に動き、時間差はない。gap は「触れないギリギリ」を保つための隙間だ。
置き方は、辺 side の正方形 3 つを同じ高さに並べ、中央だけ rotate(angle) で回す。requestAnimationFrame で経過秒に毎秒のコマ数を掛けて frame を出し、ループ 1 周の長さで余りを取れば、上のデモと同じ作りのループになる。
遊ぶなら回転を増やす。法則は quarterTurn ごとに繰り返すから、四分の一回転を与えれば押し出しは 1 回、半回転を与えると 2 回起きる — 回数はあらかじめ決め打ちされておらず、入力した回転から自動で決まる。