フラクタルの数学をブラウザで描く — マンデルブロ集合からバーンズリーのシダまで
このトピックを深掘りする
クリエイティブコーディング
ブラウザでの数学的シミュレーション・自然現象の可視化・生成アートの実装技法
全12本中 3 本目。
次に読む記事
Perlinノイズで自然な地形を生成する — ランダムなのに美しい理由
勾配ノイズの数学的な仕組みからオクターブ合成、Canvas上での3Dメッシュ描画まで、Perlinノイズによる自然な地形生成をステップごとに実装・解説する。
波動方程式をブラウザで可視化する — 干渉・回折・共鳴の世界
1D・2D波動方程式を差分法で実装し、干渉・回折・共鳴パターンをCanvasで可視化する。WaveInterference、WaveEquation、ChladniPlateの描画手法と物理の対応を解説。
N体問題をブラウザで — 万有引力シミュレーションの実装
NBody、SolarSystem、GravityWellコンポーネントで使った万有引力計算、ベクトル演算、積分法、軌道描画の実装詳細。
シリーズ全体の流れを見ながら、次に読む記事へ進めます。 初めての方はホームへ →
マンデルブロ集合とは
複素数 c に対して z_{n+1} = z_n² + c を反復し、発散しない c の集合がマンデルブロ集合。シンプルな式から無限の複雑さが生まれる。
エスケープタイムアルゴリズム
各ピクセルを複素平面の点にマッピングし、発散するまでの反復回数で色を決める:
function mandelbrot(
cx: number,
cy: number,
maxIter: number,
): number {
let zx = 0
let zy = 0
for (let i = 0; i < maxIter; i++) {
const zx2 = zx * zx
const zy2 = zy * zy
if (zx2 + zy2 > 4) return i
zy = 2 * zx * zy + cy
zx = zx2 - zy2 + cx
}
return maxIter
}zx² + zy² > 4 が発散判定。|z| > 2 になると必ず発散することが数学的に証明されている。zx2 と zy2 を事前計算しておくのは、掛け算を2回減らすための最適化。
ピクセルから複素平面への変換
Canvas API上の (px, py) を複素平面の (cx, cy) にマッピングする:
function pixelToComplex(
px: number,
py: number,
width: number,
height: number,
centerX: number,
centerY: number,
zoom: number,
) {
const cx = centerX + (px - width / 2) / (width * zoom)
const cy = centerY + (py - height / 2) / (height * zoom)
return { cx, cy }
}zoom=1で全体像、zoom=1000以上で微細構造が見える。同じパターンが無限にスケールダウンしていく。
カラーマッピング
反復回数を色相にマッピングして、美しいグラデーションを実現する:
function iterToColor(iter: number, maxIter: number): [number, number, number] {
if (iter === maxIter) return [0, 0, 0]
const t = iter / maxIter
const r = Math.floor(9 * (1 - t) * t * t * t * 255)
const g = Math.floor(15 * (1 - t) * (1 - t) * t * t * 255)
const b = Math.floor(8.5 * (1 - t) * (1 - t) * (1 - t) * t * 255)
return [r, g, b]
}Bernsteinの多項式をベースにした配色。集合内部(iter === maxIter)は黒にする。
バーンズリーのシダ — IFS
反復関数系(IFS)は確率的にアフィン変換を適用することで自己相似図形を生成する:
type AffineTransform = {
a: number; b: number; c: number; d: number
e: number; f: number
prob: number
}
const FERN: AffineTransform[] = [
{ a: 0, b: 0, c: 0, d: 0.16, e: 0, f: 0, prob: 0.01 },
{ a: 0.85, b: 0.04, c: -0.04, d: 0.85, e: 0, f: 1.6, prob: 0.85 },
{ a: 0.2, b: -0.26, c: 0.23, d: 0.22, e: 0, f: 1.6, prob: 0.07 },
{ a: -0.15, b: 0.28, c: 0.26, d: 0.24, e: 0, f: 0.44, prob: 0.07 },
]
function barnsleyFern(iterations: number): [number, number][] {
const points: [number, number][] = []
let x = 0
let y = 0
for (let i = 0; i < iterations; i++) {
const r = Math.random()
let cumProb = 0
for (const t of FERN) {
cumProb += t.prob
if (r <= cumProb) {
const nx = t.a * x + t.b * y + t.e
const ny = t.c * x + t.d * y + t.f
x = nx
y = ny
break
}
}
points.push([x, y])
}
return points
}4つのアフィン変換と確率の組み合わせだけで、驚くほどリアルなシダの葉が生成される。
FractalTreeの再帰描画
FractalTreeは再帰で枝を分岐させる:
function drawBranch(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
length: number,
angle: number,
depth: number,
) {
if (depth <= 0 || length < 2) return
const endX = x + Math.cos(angle) * length
const endY = y + Math.sin(angle) * length
ctx.beginPath()
ctx.moveTo(x, y)
ctx.lineTo(endX, endY)
ctx.lineWidth = depth * 0.8
ctx.strokeStyle = `hsl(30, \${40 + depth * 5}%, \${20 + depth * 3}%)`
ctx.stroke()
const branchAngle = Math.PI / 6
drawBranch(ctx, endX, endY, length * 0.7, angle - branchAngle, depth - 1)
drawBranch(ctx, endX, endY, length * 0.7, angle + branchAngle, depth - 1)
}深さ10で1024本の枝端。深さ12を超えるとCanvasの描画コールが重くなるため、実用上は10-12が限界。
SierpinskiTriangleのChaos Game
シェルピンスキーの三角形はChaos Gameで描ける。3頂点のいずれかをランダムに選び、現在位置との中点に点を打つ:
function sierpinski(
ctx: CanvasRenderingContext2D,
vertices: [number, number][],
iterations: number,
) {
let x = vertices[0][0]
let y = vertices[0][1]
for (let i = 0; i < iterations; i++) {
const v = vertices[Math.floor(Math.random() * 3)]
x = (x + v[0]) / 2
y = (y + v[1]) / 2
ctx.fillRect(x, y, 1, 1)
}
}ランダムなのに秩序が生まれる。「確率的アルゴリズムが決定論的な図形を生む」フラクタルの本質がここにある。
まとめ:フラクタル描画の道具たち
フラクタルと数学の関係を深く掘りたいなら、道具棚の書籍が入り口になる。数式を「見る」ことで理解する体験は、ブラウザ実装ならではの楽しさ。Processingやp5.jsでもフラクタル描画は人気のテーマだ。