GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

スピログラフ アート 9パターン — 歯車比で描く幾何模様

二つの歯車の歯数が互いに素なら曲線は閉じ、比がずれた瞬間、紙の上を果てしなくさまよう — 03はその比から模様の対称軸の数を計算する。スピログラフ アートの九つの場面で確かめます。

九つの歯車曲線をこの場で動かして確かめる

下のグリッドをスクロールすると、9つのキャンバスはすでにそれぞれ自分の曲線を描いています — クリックを待つものはありません。どれも出発点は、リングの歯数と転がる歯車の歯数という二つの整数だけです。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

スピログラフ アート 9パターン — 歯数の比が模様のすべてを決める

このコレクションは、プレーンなキャンバスコードで書いた小さなスピログラフの描画ツールです。リングの内側を転がる歯車、外側を転がる歯車、歯数比から数える対称軸、一つの歯車に複数のペン、薄れていく軌跡、歯車を重ねて見せるガイド、ポインターで比が変わる模様、ターンテーブル、四つの比を一枚の壁に並べるギャラリーの九つを扱います。鏡で対称を作る別の方法は万華鏡 アプリ — 鏡の角度で描く対称お絵かき9種で読めます。各項目を開くと、ペンの位置を決めている行をそのまま確認できます。

01内側の歯車を回る基本スピログラフ曲線

小さな歯車が大きなリングの内側を転がる間、ペン位置をリング中心基準の角度で計算します。曲線は内側に丸まりながら一本の線として積み重なり、TURNS は転がる歯車の歯数を最大公約数で割った値で、72枚と27枚なら3周です。

内側の歯車を回る基本スピログラフ曲線
新しいタブで開く

01 · 内側の歯車を回る基本スピログラフ曲線 — 計測 累積面積 65.706% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

GEAR_RING_TEETHSEGMENTS_PER_TURNshadowBlur
  function gcd(a, b) { a = Math.abs(a); b = Math.abs(b); while (b) { var t = b; b = a % b; a = t; } return a; }

  var R = GEAR_RING_TEETH * TOOTH_UNIT;
  var r = GEAR_ROLL_TEETH * TOOTH_UNIT;
  var d = r * PEN_TIP_RATIO;
  var g = gcd(GEAR_RING_TEETH, GEAR_ROLL_TEETH);
  var TURNS = GEAR_ROLL_TEETH / g;
  var TOTAL_T = TURNS * Math.PI * 2;
  var CYCLE_SECONDS = 6;
  var SPEED = TOTAL_T / CYCLE_SECONDS;

  function point(t) {
    var k = (R - r) / r;
    return {
      x: (R - r) * Math.cos(t) + d * Math.cos(k * t),
      y: (R - r) * Math.sin(t) - d * Math.sin(k * t)
    };
  }

項目01はフレームの65.7%を平均強度22.7で塗り替え、23/23フレームで動きが測定されました。

02外側を転がる歯車が描く外側曲線

同じ歯車がリングの外側を転がると、point(t) では半径の差が半径の和に替わり、回転の係数とペン項の符号も一緒に変わるため、線は花びらの形で外へ伸びます。歯数が96枚と36枚なら最大公約数は12で、閉じるまでに3周、花びらは8枚になります。

外側を転がる歯車が描く外側曲線
新しいタブで開く

02 · 外側を転がる歯車が描く外側曲線 — 計測 累積面積 66.216% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

GEAR_TEETH_OUTERTOOTH_UNITdrawFull
  function gcd(a, b) { a = Math.abs(a); b = Math.abs(b); while (b) { var t = b; b = a % b; a = t; } return a; }

  var R = GEAR_TEETH_OUTER * TOOTH_UNIT;
  var r = GEAR_ROLL_TEETH * TOOTH_UNIT;
  var d = r * PEN_TIP_RATIO;
  var g = gcd(GEAR_TEETH_OUTER, GEAR_ROLL_TEETH);
  var TURNS = GEAR_ROLL_TEETH / g;
  var TOTAL_T = TURNS * Math.PI * 2;
  var CYCLE_SECONDS = 6;
  var SPEED = TOTAL_T / CYCLE_SECONDS;

  function point(t) {
    var k = (R + r) / r;
    return {
      x: (R + r) * Math.cos(t) - d * Math.cos(k * t),
      y: (R + r) * Math.sin(t) - d * Math.sin(k * t)
    };
  }

項目02はフレームの66.2%を平均強度26.3で塗り替え、23/23フレームで動きが測定されました。

03歯数比で対称軸の数が決まる模様

リングの歯数を最大公約数で割ると AXIS_COUNT、転がる歯車の歯数を割ると TURN_COUNT になります。90枚と24枚なら対称軸は15本で4周閉じ、キャンバス上のバッジがこの二つの変数からそのまま数字を表示します。

歯数比で対称軸の数が決まる模様
新しいタブで開く

03 · 歯数比で対称軸の数が決まる模様 — 計測 累積面積 59.435% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

PEN_RADIUS_RATIOAXIS_COUNTTURN_COUNT
  function gcd(a, b) { a = Math.abs(a); b = Math.abs(b); while (b) { var t = b; b = a % b; a = t; } return a; }

  var R = GEAR_RING_TEETH * TOOTH_UNIT;
  var r = GEAR_ROLL_TEETH * TOOTH_UNIT;
  var d = r * PEN_RADIUS_RATIO;
  var g = gcd(GEAR_RING_TEETH, GEAR_ROLL_TEETH);
  var AXIS_COUNT = GEAR_RING_TEETH / g;
  var TURN_COUNT = GEAR_ROLL_TEETH / g;
  var TOTAL_T = TURN_COUNT * Math.PI * 2;
  var CYCLE_SECONDS = 6;
  var SPEED = TOTAL_T / CYCLE_SECONDS;

  badge.textContent = '対称軸' + AXIS_COUNT + '本 · ' + TURN_COUNT + '回転で閉じる';

  function point(t) {
    var k = (R - r) / r;
    return {
      x: (R - r) * Math.cos(t) + d * Math.cos(k * t),
      y: (R - r) * Math.sin(t) - d * Math.sin(k * t)
    };
  }

項目03はフレームの59.4%を平均強度19.5で塗り替え、23/23フレームで動きが測定されました。

04複数の穴が同時に描く虹の歯車模様

一つの歯車に PEN_RATIOS の比で四つのペン穴があり、drawPen() が穴ごとに色と位相を変えて座標を別々に計算します。位相は固定シードの乱数から作るので毎回同じ重なりになり、78枚と21枚の歯数では7周で閉じます。

複数の穴が同時に描く虹の歯車模様
新しいタブで開く

04 · 複数の穴が同時に描く虹の歯車模様 — 計測 累積面積 10.149% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

PEN_RATIOSPEN_COLORSPEN_PHASE
  function drawPen(idx, tMax) {
    var d = r * PEN_RATIOS[idx];
    var phase = PEN_PHASE[idx];
    ctx.strokeStyle = PEN_COLORS[idx];
    ctx.lineWidth = Math.max(2.4, scale * 0.032);
    ctx.lineJoin = 'round';
    ctx.lineCap = 'round';
    var steps = Math.max(2, Math.round((tMax / (Math.PI * 2)) * SEGMENTS_PER_TURN));
    ctx.beginPath();
    for (var i = 0; i <= steps; i += 1) {
      var t = (i / steps) * tMax;
      var s = toScreen(point(t + phase, d));
      if (i === 0) ctx.moveTo(s.x, s.y); else ctx.lineTo(s.x, s.y);
    }
    ctx.stroke();
    ctx.save();
    ctx.fillStyle = PEN_COLORS[idx].replace('.85', '.13');
    ctx.fill('evenodd');
    ctx.restore();

項目04はフレームの10.1%を平均強度38.2で塗り替え、23/23フレームで動きが測定されました。

05軌跡がゆっくり消えながら描き続ける場面

毎ティック、キャンバス全体に半透明の四角形を重ねてから、前のペン位置と新しい位置を結ぶ短い線を一本足します。FADE_ALPHA は3を TRAIL_FADE_FRAMES で割った値なので、240フレームなら一歩ごとに約1.25%ずつ薄れ、この場面は描画を止めません。

軌跡がゆっくり消えながら描き続ける場面
新しいタブで開く

05 · 軌跡がゆっくり消えながら描き続ける場面 — 計測 累積面積 49.931% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

TRAIL_FADE_FRAMESFADE_ALPHAsimStep
  function simStep() {
    ctx.fillStyle = 'rgba(23,20,26,' + FADE_ALPHA + ')';
    ctx.fillRect(0, 0, cw, ch);
    t += ANGLE_SPEED * (STEP_MS / 1000);
    var p = toScreen(point(t));
    ctx.strokeStyle = 'rgba(255,247,230,.95)';
    ctx.shadowColor = 'rgba(255,214,200,.7)';
    ctx.shadowBlur = Math.max(8, scale * 0.14);
    ctx.lineWidth = Math.max(3, scale * 0.045);
    ctx.lineJoin = 'round';
    ctx.lineCap = 'round';
    ctx.beginPath();
    ctx.moveTo(prev.x, prev.y);
    ctx.lineTo(p.x, p.y);
    ctx.stroke();
    ctx.shadowBlur = 0;
    var halo = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, Math.max(cw, ch) * 0.34);
    halo.addColorStop(0, 'rgba(255,200,215,.075)');
    halo.addColorStop(0.5, 'rgba(190,225,255,.04)');
    halo.addColorStop(1, 'rgba(190,225,255,0)');
    ctx.fillStyle = halo;
    ctx.fillRect(0, 0, cw, ch);
    prev = p;
  }

項目05はフレームの49.9%を平均強度18.8で塗り替え、23/23フレームで動きが測定されました。

06曲線の上に歯車とペンが見えるガイド

転がる歯車の円、rollCenterAt() が返すその中心、ペンの先端を、伸びていく曲線の上に重ねて描くので、今の角度が常に見えます。曲線は各サイクルの HOLD_FRACTION で描き終わってそのまま止まり、60枚と24枚の歯数ではバッジに接点角度と5本の対称軸が出ます。

曲線の上に歯車とペンが見えるガイド
新しいタブで開く

06 · 曲線の上に歯車とペンが見えるガイド — 計測 累積面積 59.176% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

HOLD_FRACTIONAXIS_COUNTrollCenterAt
  function gcd(a, b) { while (b) { var t = b; b = a % b; a = t; } return a; }
  var g = gcd(GEAR_RING_TEETH, GEAR_ROLL_TEETH);
  var TURNS = GEAR_ROLL_TEETH / g;
  var AXIS_COUNT = GEAR_RING_TEETH / g;

  var R = GEAR_RING_TEETH * TOOTH_UNIT;
  var r = GEAR_ROLL_TEETH * TOOTH_UNIT;
  var d = r * PEN_TIP_RATIO;
  var K = (R - r) / r;

  function penAt(t) {
    return {
      x: (R - r) * Math.cos(t) + d * Math.cos(K * t),
      y: (R - r) * Math.sin(t) - d * Math.sin(K * t)
    };
  }
  function rollCenterAt(t) {
    return { x: (R - r) * Math.cos(t), y: (R - r) * Math.sin(t) };
  }

項目06はフレームの59.2%を平均強度62.8で塗り替え、23/23フレームで動きが測定されました。

07ポインター距離で歯数比が変わる模様

中心からポインターまでの距離を POINTER_MAX_DIST で割り、ROLL_TEETH_MIN から ROLL_TEETH_MAX までの歯車の歯数に対応させます。経路は丸めた歯数が実際に変わったときだけ計算し直され、左右の矢印キーも同じ値を KEY_STEP ずつ動かします。

ポインター距離で歯数比が変わる模様
新しいタブで開く

07 · ポインター距離で歯数比が変わる模様 — 計測 累積面積 11.26% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

POINTER_MAX_DISTROLL_TEETH_MINROLL_TEETH_MAX
  function rebuildForRatio(ratio) {
    ratioParam = Math.max(0, Math.min(1, ratio));
    var newRoll = Math.round(ROLL_TEETH_MIN + ratioParam * (ROLL_TEETH_MAX - ROLL_TEETH_MIN));
    if (newRoll !== currentRoll) {
      currentRoll = newRoll;
      pathData = computePath(currentRoll);
    }
  }

  function handlePointer(px, py) {
    var dist = Math.hypot(px, py);
    markerAngle = Math.atan2(py, px);
    markerDist = Math.min(POINTER_MAX_DIST, dist);
    rebuildForRatio(dist / POINTER_MAX_DIST);
  }

項目07はフレームの11.3%を平均強度103.6で塗り替え、23/23フレームで動きが測定されました。

08完成した模様が回るターンテーブル場面

48枚と18枚で描き終えた曲線を一つの閉じた経路にし、キャンバス全体を ROTATE_SPEED の変換で回します。1周は9秒で、最大公約数が6なので対称軸は8本あり、輪郭は8分の1周ごとに同じ見た目に戻ります。

完成した模様が回るターンテーブル場面
新しいタブで開く

08 · 完成した模様が回るターンテーブル場面 — 計測 累積面積 14.838% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

DISC_RATIOGLOSS_SWEEP_SECONDSAXIS_COUNT
    ctx.save();
    ctx.translate(cx, cy);
    ctx.rotate(rot);
    ctx.beginPath();
    for (var i = 0; i < pathData.length; i += 1) {
      var p = pathData[i];
      var sx = p.x * scale, sy = p.y * scale;
      if (i === 0) { ctx.moveTo(sx, sy); } else { ctx.lineTo(sx, sy); }
    }
    ctx.closePath();
    ctx.strokeStyle = 'rgba(10,79,232,.82)';
    ctx.lineWidth = Math.max(1.6, scale * 0.022);
    ctx.lineJoin = 'round';
    ctx.stroke();
    ctx.restore();

項目08はフレームの14.8%を平均強度96.2で塗り替え、23/23フレームで動きが測定されました。

09比・ペン・残像を一画面にまとめたヒーローギャラリー

makeCurve() がリングの歯数60・54・48・66と、転がる歯車の歯数23・19・17・25から四本の曲線を作り、pointOn() が各曲線のペン位置を決めます。どの組も互いに素なので、閉じるまでに23・19・17・25周かかり、それぞれが自分の HERO_SPEED で描きます。

比・ペン・残像を一画面にまとめたヒーローギャラリー
新しいタブで開く

09 · 比・ペン・残像を一画面にまとめたヒーローギャラリー — 計測 累積面積 48.697% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

HERO_RING_TEETHHERO_PEN_RATIOSHERO_TRAIL_FADE_FRAMES
  function makeCurve(ringTeeth, rollTeeth, penRatio) {
    var R = ringTeeth * HERO_TOOTH_UNIT;
    var r = rollTeeth * HERO_TOOTH_UNIT;
    var d = r * penRatio;
    var K = (R - r) / r;
    var g = gcd(ringTeeth, rollTeeth);
    var turns = rollTeeth / g;
    return { R: R, r: r, d: d, K: K, turns: turns, maxT: 2 * Math.PI * turns, patternRadius: R + d };
  }
  function pointOn(curve, t) {
    return {
      x: (curve.R - curve.r) * Math.cos(t) + curve.d * Math.cos(curve.K * t),
      y: (curve.R - curve.r) * Math.sin(t) - curve.d * Math.sin(curve.K * t)
    };
  }

項目09はフレームの48.7%を平均強度21.7で塗り替え、23/23フレームで動きが測定されました。

技法別に見る歯車曲線のコード

九つの項目を並べて読むと、再描画面積は二つのグループに分かれます。項目01〜03・05・06・09はフレームのほぼ半分から3分の2を占め、04・07・08は1割前後です。広いグループは長く続く線を描くか、消えない軌跡を残すのでループの間に多くの画素が触られ、狭いグループは背景の上に細いペン線を引くか、位置の変わらない輪郭を回すだけです。平均強度は逆の並びで、07(103.6)と08(96.2)が最も高く、少ない画素が大きく動く輪郭の動きと合っています。軌跡の場面05(18.8)は多くの画素が少しずつ変わる側にあたります。

項目 技法 再描画面積 平均強度 動いたフレーム
01 基本の内側曲線 65.7% 22.7 23/23
02 外側を転がる曲線 66.2% 26.3 23/23
03 歯数比と対称軸 59.4% 19.5 23/23
04 複数ペンの虹模様 10.1% 38.2 23/23
05 薄れる軌跡の描画 49.9% 18.8 23/23
06 歯車とペンのガイド 59.2% 62.8 23/23
07 ポインターで比が変わる模様 11.3% 103.6 23/23
08 模様のターンテーブル 14.8% 96.2 23/23
09 ヒーローギャラリー 48.7% 21.7 23/23

値が模様を変える場所 — 歯数・ペン位置・回転速度

模様の見た目はほとんど三つの定数で決まります。歯数は最大公約数を通して花びらの数と周回数を決め、ペンの比(PEN_TIP_RATIO や PEN_RADIUS_RATIO)は小歯車の中心からペンまでの距離を決め、サイクルや速度の定数は線を描く速さを決めます。どれもループの構造には触れず、point(t) がすでに読んでいる数値を変えるだけで、フーリエ変換 アニメーション 9種 — 円の連鎖で星とハートを描き直すが円の半径を入力値として扱うのと同じ考え方です。後ろの変形表は一行ごとに値を一つだけ変えているので、模様が動く様子を一つずつ確かめられます。

どこで壊れるか — 曲線が閉じないとき・狭い画面・線が重なるとき

歯数が整数なら曲線は必ず GEAR_ROLL_TEETH / gcd 周で閉じるので、閉じなくなるのは歯数に Math.PI * 10 のような無理数を入れたときだけで、その場合は線が永遠に積み重なります。23枚と25枚のように互いに素な組は閉じるものの23周かかり、SPEED = TOTAL_T / CYCLE_SECONDS なので同じ6秒のサイクルでも、23周の曲線ではペンが3周の曲線の約8倍の速さで走ります。狭い画面では fit() がキャンバスを縮め、線の太さは scale に従いますが、項目09の25周のセルのように密な曲線は一つの小さなセルに25個の輪が入り、線が混み合います。各項目の九つの index.html はまとめて zip に入っていて、パスワードは 899p5t2j です。歯数を自分で書き換えて、どの比で壊れるかを試せます。

変形3つとアクセシビリティ

変形 値 感じ方
歯車の歯数比を調整する 02 GEAR_TEETH_OUTER 96 → 60 96から60に変えても転がる歯車(36枚)との最大公約数は12のままなので、閉じるまでの周回数は3で変わりませんが、花びらは8枚から5枚に減って模様がまばらになります。
ペン位置の比率を調整する 03 PEN_RADIUS_RATIO 0.6 → 0.95 値を0.6から0.95へ大きくするとペンが縁に寄り、曲線が鋭く折れて外へ大きく伸びる模様になります。反対に小さくするとペンが小歯車の中心近くを回り、丸く穏やかな模様になります。
軌跡の残像長を調整する 05 TRAIL_FADE_FRAMES 240 → 60 値を小さくすると描かれた線がすぐ消え、ペンが通った瞬間の軌跡だけが見え、値を大きくすると完成した模様が長く残り、複数周の痕跡が一画面に重なって積もります。

九つのCanvas APIの場面はすべて、requestAnimationFrameのループを始める前に matchMedia でprefers-reduced-motionを一度だけ読みます。動きを減らす設定のときは、項目01〜04が完成した曲線を一度だけ描き、05と09は軌跡のステップを先に進めて落ち着いた1フレームを見せ、06は84%の位置で止まり、07は一度だけ描画され、08はディスクの静止した一枚を表示します。九つのルートにはすべて tabindex="0" が付いていますが、入力を受け取るのは項目07だけで、ポインターの移動と押下、それに左右の矢印キーが、アニメーションが止まっていても比を変えます。

スピログラフ とは — コードは何を計算しているのか?

毎フレーム、コードは一つの問いに答えます。転がる歯車が角度 t だけ回ったとき、ペンはどこにあるのか、という問いです。リングの内側では (R - r) に t の余弦・正弦を掛けた項と、k = (R - r) / r で回るペンの項を足し、外側では半径を足し合わせた同じ形になり、項目01と02の違いはそこだけです。04の背景グラデーションや曲線の内側の薄い塗りは表現の層で、座標・最大公約数・TURNS = GEAR_ROLL_TEETH / gcd が計算の中身です。記録を再生せずに計算で描く考え方は、html5 canvas アニメーション9種 — 水滴とブラックホールやリサージュ曲線 作り方9選 — 波の干渉で描くキャンバスと同じです。紙の定規を手作りする前に歯数比を画面で試したい方も、「スピログラフ 定規 手作り」の代わりにまずこの計算を読むと、穴の位置が模様にどう効くかがつかめます。

スピログラフ模様を使う前によくある質問

スピログラフ 定規 使い方を知らなくても、このコードで模様は描けますか?

はい。紙の定規の持ち方は関係なく、各項目の先頭にある二つの歯数の定数を書き換えるだけで、最大公約数・対称軸の数・閉じる周回数が自動で計算し直されます。

項目09はなぜ切りのよい数ではなく23や25のような歯数を使うのですか?

互いに素な組は最大公約数が1になるので、曲線が閉じるまでの周回数が最も長くなり、セルの中にたくさんの輪が描かれます。切りのよい数は最大公約数が1より大きくなるため、早く閉じて疎な花のような形になります。

この9つのうち一つだけを自分のページに入れられますか?

はい。各項目のキャンバスと定数、描画コードは、それぞれの index.html の中にまとまっています。一つをコピーしても、残り八つのコードを一緒に持ち込む必要はありません。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。