Canvas API で描くスピログラフ
HTML Canvas と requestAnimationFrame を使い、数学的な曲線「スピログラフ(ハイポトロコイド)」をアニメーション描画します。
作品
内側の円が外側の円の内壁を転がるとき、円上の一点が描く軌跡が ハイポトロコイド(スピログラフ) です。 クリック / タップするたびに異なる比率のパターンに切り替わります。
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サイクルの長さを最大公約数で求める
曲線が「閉じる」(元に戻る)のは、内円が外円の内壁をちょうど整数回転したときです。
これは R と r の最大公約数(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) = 15、cycles = 3 → 内円を 3 周して閉じます。
R:r = 120:25 のとき gcd(120, 25) = 5、cycles = 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 | 命令的・ラスター | 数学的曲線・粒子・ゲーム |
Canvas は一度描いた内容を「消す」ことができません(正確には clearRect で上書きするしかない)。そのため粒子エフェクトなど「前フレームを残しながら描く」表現も Canvas ならではです。