← 記事一覧へ

Canvas API で描くスピログラフ

HTML Canvas と requestAnimationFrame を使い、数学的な曲線「スピログラフ(ハイポトロコイド)」をアニメーション描画します。

作品

内側の円が外側の円の内壁を転がるとき、円上の一点が描く軌跡が ハイポトロコイド(スピログラフ) です。 クリック / タップするたびに異なる比率のパターンに切り替わります。

Spirograph — Canvas 2D & requestAnimationFrameR:r = 8:3  |  クリック / タップで次のパターン

Canvas API とは

これまでの作品は CSS や SVG という「宣言的」な手法でした。Canvas はその反対で、JavaScript から ctx.lineTo() などのメソッドを呼び出してピクセルを直接操作する「命令的」な描画 API です。

const canvas = document.querySelector('canvas')
const ctx = canvas.getContext('2d')

ctx.beginPath()
ctx.moveTo(100, 100)
ctx.lineTo(200, 200)
ctx.strokeStyle = '#00DC82'
ctx.stroke()

Vue では ref でキャンバス要素を取得し、onMounted で描画を開始します。

const canvasRef = ref<HTMLCanvasElement | null>(null)

onMounted(() => {
  const ctx = canvasRef.value!.getContext('2d')!
  // ここで描画処理
})

スピログラフの数式

ハイポトロコイドは 3 つのパラメータで定義されます。

変数意味
R外側の固定円の半径
r内側の転がる円の半径
d描画点と内円中心の距離
// 媒介変数 t を 0 から増やしながら座標を計算
const x = (R - r) * Math.cos(t) + d * Math.cos(((R - r) / r) * t)
const y = (R - r) * Math.sin(t) - d * Math.sin(((R - r) / r) * t)

1サイクルの長さを最大公約数で求める

曲線が「閉じる」(元に戻る)のは、内円が外円の内壁をちょうど整数回転したときです。 これは Rr の最大公約数(GCD)から求められます。

function gcd(a: number, b: number): number {
  return b === 0 ? a : gcd(b, a % b)  // ユークリッドの互除法
}

const cycles = r / gcd(R, r)           // 内円の回転数
const totalAngle = 2 * Math.PI * cycles // 描画する総角度

R:r = 120:45 のとき gcd(120, 45) = 15cycles = 3 → 内円を 3 周して閉じます。 R:r = 120:25 のとき gcd(120, 25) = 5cycles = 5 → 5 周。周数が多いほど複雑な形状になります。


requestAnimationFrame でアニメーション描画

全座標を事前計算しておき、毎フレーム少しずつ lineTo で線を伸ばしていきます。

// 全点を先に計算
const pts = Array.from({ length: totalPoints + 1 }, (_, i) => {
  const t = (i / totalPoints) * totalAngle
  return [
    cx + (R - r) * Math.cos(t) + d * Math.cos(((R - r) / r) * t),
    cy + (R - r) * Math.sin(t) - d * Math.sin(((R - r) / r) * t),
  ]
})

let i = 1
function frame() {
  ctx.beginPath()
  ctx.moveTo(pts[i - 1][0], pts[i - 1][1])
  ctx.lineTo(pts[i][0], pts[i][1])
  ctx.stroke()
  i++
  if (i < pts.length) requestAnimationFrame(frame)
}
frame()

requestAnimationFrame はブラウザの描画タイミングに合わせてコールバックを呼ぶため、60fps でスムーズなアニメーションになります。

グロー効果

Canvas でも shadowBlur を設定すると発光感が出ます。

ctx.shadowBlur  = 8
ctx.shadowColor = '#00DC82'

また progress(描画の進行度)に合わせて strokeStyle の不透明度を上げることで、描かれていく線が徐々に明るくなるエフェクトを加えています。

const progress = i / totalPoints
ctx.strokeStyle = `rgba(0, 220, 130, ${0.4 + 0.6 * progress})`

Retina / HiDPI 対応

Canvas は width / height 属性(物理ピクセル)と CSS サイズ(論理ピクセル)が分離しています。 devicePixelRatio を掛けてから scale() することでシャープな描画になります。

const dpr = window.devicePixelRatio || 1
canvas.width  = 400 * dpr
canvas.height = 400 * dpr
ctx.scale(dpr, dpr)
// 以降は 400×400 の座標系で描けば OK

手法まとめ

技術描画モデル向いている表現
CSS宣言的・ボックスモデルレイアウト・トランジション
SVG宣言的・ベクター図形・パスアニメーション
Canvas 2D命令的・ラスター数学的曲線・粒子・ゲーム
✓ TIP

Canvas は一度描いた内容を「消す」ことができません(正確には clearRect で上書きするしかない)。そのため粒子エフェクトなど「前フレームを残しながら描く」表現も Canvas ならではです。