配置移行
配置移行の作り方
9 つの点が中央に集まり、リングの形に開き、渦を巻いて別の場所へ戻る — まるで細かく振り付けたように見える。手で形を決めるのは「グリッド」と「リング」の 2 つの並びで、渦も席の入れ替わりも、この 2 つの並びと点の移し方から自然に生まれる。コードつきで作り方を一から追う。
- Published
- 2026年6月13日
- Topics
- Arrangement · Turntable · Easing · SVG
2 つの配置と、あいだをつなぐ 3 つの動きを作る
- 手で形を決める配置は 2 つ — 中心を共有する、四角く並んだ「グリッド」と、輪に並んだ「リング」
- 2 つの配置から各点の位置を先に割り出す — 休み位置からの距離と向き、リング上の行き先、回したあとに座る場所
- あいだをつなぐ 3 つの動き — 中央へ引きこんで外へ放る、リングの形で少し止まる、回しながら次の席へ寄せる
const deg = Math.PI / 180;
const signedArc = (from, to) => ((to - from + 180) % 360 + 360) % 360 - 180;
// 2 つの配置から、各点の位置を先に割り出す
const restRadius = restPositions.map(([x, y]) => Math.hypot(x - cx, y - cy));
const restAngle = restPositions.map(([x, y]) => Math.atan2(y - cy, x - cx) / deg);
const slotAngle = restToRing.map((slot) => ringAnglesDeg[slot]);
const angleGap = restAngle.map((a, dot) => signedArc(a, slotAngle[dot]));
const slotPoint = ringAnglesDeg.map((a) => [
cx + ringRadius * Math.cos(a * deg),
cy + ringRadius * Math.sin(a * deg),
]);
const back = -turntableDeg * deg;
const destLocal = restToRing.map((slot, dot) => {
if (dot === centerDestinationDot) return [0, 0]; // 行き先が中心の点はそこを狙う
const [x, y] = restPositions[ringToRest[slot]];
const lx = x - cx;
const ly = y - cy;
return [lx * Math.cos(back) - ly * Math.sin(back), lx * Math.sin(back) + ly * Math.cos(back)];
});はじめのコードは、2 つの配置から各点の位置を先に割り出す。restToRing はどの点がリングのどこへ向かうか、ringToRest は戻るときどの休み席へ座り直すかの対応だ。この 2 つの対応が決めるのは、各点の行き先と戻る席。渦の巻き方そのものは、あとで出てくる回し方しだいだ。中央のかたまりも、渦も、席替えも、一つずつ動かしているわけではない — 2 つの配置と、この点の移し方から自然に出てくる。
const wrap = (frame, span) => ((frame % span) + span) % span;
const [lobeIn, lobeOut] = lobeXHandles;
const swingEase = cubicBezier(lobeIn, 0, lobeOut, 1);
const lastArrival = Math.max(...arrivals);
// 最初の動き: いちど中央へ引きこんでから外のスロットへ放る(共有のふくらみ)
const expand = (frame, dot) => {
const span =
frame <= departures[dot] ? 0 : frame >= arrivals[dot] ? 1 : (frame - departures[dot]) / (arrivals[dot] - departures[dot]);
if (dot === centerSeatDot) { // 休み席が中心の点はへこむ余地がない → 半径は素直に外へ
const radius = restRadius[dot] + swingEase(span) * (ringRadius - restRadius[dot]);
const angle = slotAngle[dot] * deg;
return [cx + radius * Math.cos(angle), cy + radius * Math.sin(angle)];
}
const dip = cubicBezier(lobeIn, -lobeDepths[dot], lobeOut, 1)(span);
const radius = restRadius[dot] + dip * (ringRadius - restRadius[dot]);
const angle = (restAngle[dot] + swingEase(span) * angleGap[dot]) * deg;
return [cx + radius * Math.cos(angle), cy + radius * Math.sin(angle)];
};
// 最後の動き: 共通の回す角度で全点を回し、各点はリングから次の席へ寄せる
const turnEase = cubicBezier(...phiBezier);
const chordEase = cubicBezier(...sConBezier);
const turnAt = (frame, delay) => {
const local = frame - delay;
if (local <= windowStart) return 0;
if (local >= windowEnd) return turntableDeg;
return turntableDeg * turnEase((local - windowStart) / (windowEnd - windowStart));
};
const chordAt = (frame, delay) => {
const local = frame - delay;
if (local <= windowStart) return 0;
if (local >= windowEnd) return 1;
return chordEase((local - windowStart) / (windowEnd - windowStart));
};
function dotsAt(frame) { // フレーム番号 → 各点の置き場所。動きを選び、点ごとに読む
const f = wrap(frame, periodFrames);
return restPositions.map((_, dot) => {
const slot = restToRing[dot];
const delay = delaysPerSlot[slot];
if (f <= lastArrival) { const [x, y] = expand(f, dot); return { cx: x, cy: y, r: dotRadius }; }
if (f - delay < windowStart) return { cx: slotPoint[slot][0], cy: slotPoint[slot][1], r: dotRadius }; // 中ほどの動き: リングで静止
const chord = chordAt(f, delay);
const rlx = slotPoint[slot][0] - cx;
const rly = slotPoint[slot][1] - cy;
const lx = rlx + chord * (destLocal[dot][0] - rlx);
const ly = rly + chord * (destLocal[dot][1] - rly);
const turn = turnAt(f, delay) * deg;
return { cx: cx + lx * Math.cos(turn) - ly * Math.sin(turn), cy: cy + lx * Math.sin(turn) + ly * Math.cos(turn), r: dotRadius };
});
}expand が最初の動きだ。各点は自分の departures で動きはじめ、arrivals で止まる。いちど中央へ引きこまれてから、外のスロットへ放り出される。へこみの深さは lobeDepths で決まる。
最後の動きでは、turnAt が全点に共通の回す角度を返し、chordAt がリングから次の席へどれだけ近づいたかを返す。これらの動きの速さは、どれもイージングカーブで決まる。swingEase・phiBezier・sConBezier に置く数は CSS の cubic-bezier() と同じ 4 つ。そのまま bezier-easing のような既製のライブラリ に渡せる。
画面に置くのは点を 9 つ、返ってきた cx・cy・r のとおりに描くだけだ。動かすのは requestAnimationFrame で、経過した時間にコマ数を掛けてフレーム番号を出し、periodFrames で wrap してから dotsAt に渡せば、このデモと同じループになる。turntableDeg のぶんだけ回るとリングの行き先がそっくり元の位置に重なり、各点は別の休み席へ座り直して 1 周が閉じるので、ループの継ぎ目は出ない。
遊ぶなら配置を書き換える。restToRing と ringToRest の対応を変えると、戻りの渦の巻き方と席順がまるごと変わる。turntableDeg を増やせばリングを回る道のりが伸び、delaysPerSlot をいじれば点が散り散りに動き出すか、いっせいに動き出すかが変わる。配置の点を別の形に置き換えても、2 つの配置をつなぐこの仕組みはそのまま使える。