差
差の作り方
重なった円が開いては閉じ、重なったところが抜けて、形を引き算したように見える。この抜けの正体は、奇数回重なった部分を塗る偶奇(even-odd)の規則と、折り返す 1 本の時計だ。コードつきで、組み立ての考え方から順に見ていく。
- Published
- 2026年6月14日
- Topics
- Difference · Even-Odd · Easing · SVG
重なって抜ける形と、折り返す時計を組む
- 手で置くのは、同じ大きさの円が 6 つだけ — 4 つは塗り、2 つは輪郭の線
- 塗りの 4 つは「重なった回数が偶数のところは塗らない」規則で描く — これが「差」に見える正体で、引き算はどこにもしていない
- 6 つは 1 本の時計を共有し、前半で開き、折り返してからは後半が前半をそのまま逆にたどる
const wrap = (frame, span) => ((frame % span) + span) % span;
// 折り返す時計: ミラーで折り返し、後半は前半を逆にたどる
const clockAt = (frame) => {
const t = wrap(frame, periodFrames);
if (t <= mirrorFrame) return t;
return Math.max(2 * mirrorFrame - t, 0);
};
// チャネルの形: 片道で動く ramp と、山なりに動いて戻る bump
const rampAt = (ch, t) => ch.end * cubicBezier(...ch.ease)((t - ch.t0) / (ch.endF - ch.t0));
const bumpAt = (ch, t) => {
if (t <= ch.apexF) return ch.base + (ch.apex - ch.base) * cubicBezier(...ch.e1)((t - ch.t0) / (ch.apexF - ch.t0));
return ch.base + (ch.apex - ch.base) * (1 - cubicBezier(...ch.e2)((t - ch.apexF) / (ch.endF - ch.apexF)));
};clockAt が折り返す時計だ。mirrorFrame までは時間がそのまま進み、そこから先は折り返して同じ道を逆にたどる — だから後半は前半の鏡うつしになり、ループの終わりは始まりに戻る。動く量は 2 種類のチャネルで決める。rampAt は片道で目標まで動き、bumpAt は山ひとつぶん動いて戻る。ease・e1・e2 は CSS の cubic-bezier() と同じ並びで、bezier-easing のような既製のライブラリにそのまま渡せる。
// フレーム番号 → 円の位置
function circlesAt(frame) {
const t = clockAt(frame);
const u = rampAt(uChan, t); // 塗りの円の、軸に沿った開き
const v = bumpAt(vChan, t); // 塗りの円の、横へのふくらみ
const s = rampAt(sChan, t); // 輪郭の円の開き(u を少し遅れて追う)
const w = bumpAt(wChan, t); // 輪郭の円の線の太さ
// ひとつの軸に沿って、符号を変えて置くだけ
const rad = axisDeg * Math.PI / 180;
const ax = Math.cos(rad), ay = Math.sin(rad);
const bx = -Math.sin(rad), by = Math.cos(rad);
const place = (du, dv, r) => ({ cx: cx + ax * du + bx * dv, cy: cy + ay * du + by * dv, r });
return {
// 塗りの円: 共有する (u, v) の符号配置
quad: [place(u, v, quadRadius), place(-u, -v, quadRadius), place(u, -v, quadRadius), place(-u, v, quadRadius)],
// 輪郭の円: 軸上の (±s, 0)
axial: [place(s, 0, axialRadius), place(-s, 0, axialRadius)],
bandWidth: w,
};
}ここが「差」の正体だ。円どうしを引き算してはいない。塗りの 4 つは、共有する u・v の符号を入れ替えて置いただけの、同じ大きさのただの円だ。重なったところは塗りが偶数回かさなって抜け、それが形を切り抜いたように見える。circlesAt のどこにも、円と円のあいだの関係を計算するところはない — 6 つは clockAt の時計と 4 本のチャネルを共有するだけで、たがいを見ていない。前半と後半が鏡うつしになるのも、時計が折り返すからだ。
画面に置くのは円を 6 つ。塗りの 4 つはひとつのパスにまとめ、「偶数回かさなったところは塗らない」規則で描く — これで重なりが抜ける。輪郭の 2 つは、重ねた外周をひとつの線にして bandWidth の太さでなぞる。動かすのは requestAnimationFrame で、経過した時間にコマ数を掛けてフレーム番号を出し、periodFrames で wrap して circlesAt に渡せば、このデモと同じループになる。色は決め打ちにせず、ページの地の色を受け取る。
遊ぶなら軸の向き(axisDeg)を変える。斜めから縦や横にすると、開いていく方向ごと変わる。チャネルの目標を大きくすれば開きが深くなり、折り返すフレームを早めれば往復が速くなる。塗りの円を四角に変えても、6 つを符号で置いてひとつにまとめるこの仕組みはそのまま使える。