skills/awwwards-animations/references/algorithmic-art.md

867 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Algorithmic & Generative Art
React patterns for mathematical art, fractals, flow fields, and generative visuals using Canvas 2D and p5.js.
## Table of Contents
- [Fractal Trees](#fractal-trees)
- [L-Systems](#l-systems)
- [Mathematical Curves](#mathematical-curves)
- [Flow Fields](#flow-fields)
- [Strange Attractors](#strange-attractors)
- [Reaction-Diffusion](#reaction-diffusion)
- [Cellular Automata](#cellular-automata)
- [Noise Patterns](#noise-patterns)
- [Sacred Geometry](#sacred-geometry)
---
## Fractal Trees
Recursive branching with animated growth.
```tsx
'use client'
import { useRef, useEffect, useCallback } from 'react'
interface BranchParams {
x: number; y: number; length: number; angle: number; depth: number
maxDepth: number; progress: number
}
export function FractalTree({ maxDepth = 10, branchAngle = 25 }) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const animRef = useRef<number>(0)
const progressRef = useRef(0)
const drawBranch = useCallback((ctx: CanvasRenderingContext2D, params: BranchParams) => {
const { x, y, length, angle, depth, maxDepth, progress } = params
if (depth > maxDepth || length < 2) return
const depthProgress = Math.max(0, Math.min(1, progress * maxDepth - depth))
if (depthProgress <= 0) return
const endX = x + Math.cos((angle * Math.PI) / 180) * length * depthProgress
const endY = y - Math.sin((angle * Math.PI) / 180) * length * depthProgress
ctx.beginPath()
ctx.moveTo(x, y)
ctx.lineTo(endX, endY)
ctx.strokeStyle = `hsl(${120 + depth * 15}, 60%, ${30 + depth * 5}%)`
ctx.lineWidth = Math.max(1, (maxDepth - depth) * 1.5)
ctx.stroke()
const newLength = length * 0.72
const spread = branchAngle + Math.sin(depth * 0.5) * 5
drawBranch(ctx, { x: endX, y: endY, length: newLength, angle: angle + spread, depth: depth + 1, maxDepth, progress })
drawBranch(ctx, { x: endX, y: endY, length: newLength, angle: angle - spread, depth: depth + 1, maxDepth, progress })
}, [branchAngle])
useEffect(() => {
const canvas = canvasRef.current!
const ctx = canvas.getContext('2d')!
canvas.width = canvas.offsetWidth * 2
canvas.height = canvas.offsetHeight * 2
ctx.scale(2, 2)
const animate = () => {
progressRef.current = Math.min(1, progressRef.current + 0.008)
ctx.clearRect(0, 0, canvas.offsetWidth, canvas.offsetHeight)
drawBranch(ctx, {
x: canvas.offsetWidth / 2, y: canvas.offsetHeight,
length: canvas.offsetHeight * 0.28, angle: 90,
depth: 0, maxDepth, progress: progressRef.current,
})
if (progressRef.current < 1) animRef.current = requestAnimationFrame(animate)
}
animate()
return () => cancelAnimationFrame(animRef.current)
}, [maxDepth, drawBranch])
return <canvas ref={canvasRef} className="w-full h-full" />
}
```
## L-Systems
Lindenmayer systems with turtle graphics.
```tsx
'use client'
import { useRef, useEffect } from 'react'
interface LSystemRule { [key: string]: string }
function generateLSystem(axiom: string, rules: LSystemRule, iterations: number): string {
let current = axiom
for (let i = 0; i < iterations; i++) {
current = current.split('').map(c => rules[c] || c).join('')
}
return current
}
interface TurtleState { x: number; y: number; angle: number }
function drawLSystem(
ctx: CanvasRenderingContext2D,
instructions: string,
startX: number, startY: number,
stepLength: number, turnAngle: number
) {
const stack: TurtleState[] = []
let state: TurtleState = { x: startX, y: startY, angle: -90 }
ctx.beginPath()
ctx.moveTo(state.x, state.y)
for (const char of instructions) {
switch (char) {
case 'F': case 'G':
state.x += Math.cos((state.angle * Math.PI) / 180) * stepLength
state.y += Math.sin((state.angle * Math.PI) / 180) * stepLength
ctx.lineTo(state.x, state.y)
break
case '+': state.angle += turnAngle; break
case '-': state.angle -= turnAngle; break
case '[': stack.push({ ...state }); break
case ']':
state = stack.pop()!
ctx.moveTo(state.x, state.y)
break
}
}
ctx.stroke()
}
// Presets
const L_SYSTEM_PRESETS = {
kochSnowflake: { axiom: 'F--F--F', rules: { F: 'F+F--F+F' }, angle: 60, iterations: 4 },
sierpinski: { axiom: 'F-G-G', rules: { F: 'F-G+F+G-F', G: 'GG' }, angle: 120, iterations: 6 },
dragonCurve: { axiom: 'FX', rules: { X: 'X+YF+', Y: '-FX-Y' }, angle: 90, iterations: 12 },
plant: { axiom: 'X', rules: { X: 'F+[[X]-X]-F[-FX]+X', F: 'FF' }, angle: 25, iterations: 6 },
hilbert: { axiom: 'A', rules: { A: '-BF+AFA+FB-', B: '+AF-BFB-FA+' }, angle: 90, iterations: 5 },
} as const
export function LSystemCanvas({ preset = 'plant' }: { preset?: keyof typeof L_SYSTEM_PRESETS }) {
const canvasRef = useRef<HTMLCanvasElement>(null)
useEffect(() => {
const canvas = canvasRef.current!
const ctx = canvas.getContext('2d')!
canvas.width = canvas.offsetWidth * 2
canvas.height = canvas.offsetHeight * 2
ctx.scale(2, 2)
const { axiom, rules, angle, iterations } = L_SYSTEM_PRESETS[preset]
const instructions = generateLSystem(axiom, rules, iterations)
ctx.strokeStyle = '#4ade80'
ctx.lineWidth = 0.5
const step = preset === 'plant' ? 4 : preset === 'hilbert' ? canvas.offsetWidth / Math.pow(2, iterations) : 3
const startX = preset === 'plant' ? canvas.offsetWidth / 2 : 20
const startY = preset === 'plant' ? canvas.offsetHeight : canvas.offsetHeight - 20
drawLSystem(ctx, instructions, startX, startY, step, angle)
}, [preset])
return <canvas ref={canvasRef} className="w-full h-full bg-gray-950" />
}
```
## Mathematical Curves
Parametric curves: Lissajous, polar roses, spirals, superformula.
```tsx
'use client'
import { useRef, useEffect } from 'react'
type CurveType = 'lissajous' | 'rose' | 'spiral' | 'superformula'
interface CurveParams {
type: CurveType
a?: number; b?: number // Lissajous frequencies / rose petals
m?: number; n1?: number; n2?: number; n3?: number // Superformula
}
function getCurvePoint(t: number, params: CurveParams, scale: number): [number, number] {
const { type, a = 3, b = 4, m = 6, n1 = 1, n2 = 1, n3 = 1 } = params
switch (type) {
case 'lissajous':
return [Math.sin(a * t) * scale, Math.sin(b * t + Math.PI / 4) * scale]
case 'rose': {
const r = Math.cos(a * t) * scale
return [r * Math.cos(t), r * Math.sin(t)]
}
case 'spiral': {
const r = t * scale * 0.02
return [r * Math.cos(t), r * Math.sin(t)]
}
case 'superformula': {
const phi = t
const r1 = Math.pow(Math.abs(Math.cos(m * phi / 4) / 1), n2)
const r2 = Math.pow(Math.abs(Math.sin(m * phi / 4) / 1), n3)
const r = Math.pow(r1 + r2, -1 / n1) * scale
return [r * Math.cos(phi), r * Math.sin(phi)]
}
}
}
export function MathCurve({ type = 'lissajous', ...params }: CurveParams) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const animRef = useRef<number>(0)
const tRef = useRef(0)
useEffect(() => {
const canvas = canvasRef.current!
const ctx = canvas.getContext('2d')!
canvas.width = canvas.offsetWidth * 2
canvas.height = canvas.offsetHeight * 2
ctx.scale(2, 2)
const cx = canvas.offsetWidth / 2
const cy = canvas.offsetHeight / 2
const scale = Math.min(cx, cy) * 0.7
const animate = () => {
tRef.current += 0.03
const maxT = tRef.current
ctx.fillStyle = 'rgba(0, 0, 0, 0.03)'
ctx.fillRect(0, 0, canvas.offsetWidth, canvas.offsetHeight)
ctx.beginPath()
for (let t = 0; t < Math.min(maxT, Math.PI * 20); t += 0.01) {
const [x, y] = getCurvePoint(t, { type, ...params }, scale)
if (t === 0) ctx.moveTo(cx + x, cy + y)
else ctx.lineTo(cx + x, cy + y)
}
ctx.strokeStyle = `hsl(${(tRef.current * 20) % 360}, 70%, 60%)`
ctx.lineWidth = 1.5
ctx.stroke()
animRef.current = requestAnimationFrame(animate)
}
animate()
return () => cancelAnimationFrame(animRef.current)
}, [type, params])
return <canvas ref={canvasRef} className="w-full h-full bg-black" />
}
```
## Flow Fields
Perlin noisedriven particle system.
```tsx
'use client'
import { useRef, useEffect } from 'react'
// Simplified Perlin-like noise (use `simplex-noise` package for production)
function noise2D(x: number, y: number): number {
const n = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453
return (n - Math.floor(n)) * 2 - 1
}
function smoothNoise(x: number, y: number, scale: number): number {
const sx = x / scale
const sy = y / scale
const ix = Math.floor(sx)
const iy = Math.floor(sy)
const fx = sx - ix
const fy = sy - iy
const a = noise2D(ix, iy)
const b = noise2D(ix + 1, iy)
const c = noise2D(ix, iy + 1)
const d = noise2D(ix + 1, iy + 1)
const ux = fx * fx * (3 - 2 * fx)
const uy = fy * fy * (3 - 2 * fy)
return a + ux * (b - a) + uy * (c - a) + ux * uy * (a - b - c + d)
}
interface Particle { x: number; y: number; vx: number; vy: number; life: number }
export function FlowField({ particleCount = 2000, noiseScale = 120 }) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const animRef = useRef<number>(0)
useEffect(() => {
const canvas = canvasRef.current!
const ctx = canvas.getContext('2d')!
const w = canvas.offsetWidth
const h = canvas.offsetHeight
canvas.width = w * 2
canvas.height = h * 2
ctx.scale(2, 2)
let time = 0
const particles: Particle[] = Array.from({ length: particleCount }, () => ({
x: Math.random() * w, y: Math.random() * h,
vx: 0, vy: 0, life: Math.random() * 100,
}))
ctx.fillStyle = '#000'
ctx.fillRect(0, 0, w, h)
const animate = () => {
ctx.fillStyle = 'rgba(0, 0, 0, 0.01)'
ctx.fillRect(0, 0, w, h)
time += 0.002
particles.forEach(p => {
const angle = smoothNoise(p.x + time * 50, p.y, noiseScale) * Math.PI * 4
p.vx = Math.cos(angle) * 1.5
p.vy = Math.sin(angle) * 1.5
p.x += p.vx
p.y += p.vy
p.life--
if (p.x < 0 || p.x > w || p.y < 0 || p.y > h || p.life <= 0) {
p.x = Math.random() * w
p.y = Math.random() * h
p.life = 50 + Math.random() * 100
}
const hue = (smoothNoise(p.x, p.y, noiseScale * 2) + 1) * 180
ctx.fillStyle = `hsla(${hue}, 70%, 60%, 0.6)`
ctx.fillRect(p.x, p.y, 1.5, 1.5)
})
animRef.current = requestAnimationFrame(animate)
}
animate()
return () => cancelAnimationFrame(animRef.current)
}, [particleCount, noiseScale])
return <canvas ref={canvasRef} className="w-full h-full" />
}
```
## Strange Attractors
Lorenz and Rössler systems rendered in Canvas.
```tsx
'use client'
import { useRef, useEffect } from 'react'
type AttractorType = 'lorenz' | 'rossler'
function step(type: AttractorType, x: number, y: number, z: number, dt: number): [number, number, number] {
if (type === 'lorenz') {
const sigma = 10, rho = 28, beta = 8 / 3
return [
x + (sigma * (y - x)) * dt,
y + (x * (rho - z) - y) * dt,
z + (x * y - beta * z) * dt,
]
}
// Rössler
const a = 0.2, b = 0.2, c = 5.7
return [
x + (-y - z) * dt,
y + (x + a * y) * dt,
z + (b + z * (x - c)) * dt,
]
}
export function StrangeAttractor({ type = 'lorenz' }: { type?: AttractorType }) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const animRef = useRef<number>(0)
useEffect(() => {
const canvas = canvasRef.current!
const ctx = canvas.getContext('2d')!
const w = canvas.offsetWidth
const h = canvas.offsetHeight
canvas.width = w * 2
canvas.height = h * 2
ctx.scale(2, 2)
let x = 0.1, y = 0, z = 0
const dt = 0.005
const points: [number, number, number][] = []
const maxPoints = 8000
let frame = 0
ctx.fillStyle = '#000'
ctx.fillRect(0, 0, w, h)
const animate = () => {
for (let i = 0; i < 20; i++) {
;[x, y, z] = step(type, x, y, z, dt)
points.push([x, y, z])
if (points.length > maxPoints) points.shift()
}
ctx.fillStyle = 'rgba(0, 0, 0, 0.02)'
ctx.fillRect(0, 0, w, h)
const rot = frame * 0.003
const scale = type === 'lorenz' ? 6 : 15
const cx = w / 2
const cy = h / 2
ctx.beginPath()
points.forEach(([px, py, pz], i) => {
const rx = px * Math.cos(rot) - pz * Math.sin(rot)
const ry = py
const sx = cx + rx * scale
const sy = cy + ry * scale * (type === 'lorenz' ? -1 : 1)
if (i === 0) ctx.moveTo(sx, sy)
else ctx.lineTo(sx, sy)
})
ctx.strokeStyle = `hsla(${frame % 360}, 80%, 60%, 0.3)`
ctx.lineWidth = 0.5
ctx.stroke()
frame++
animRef.current = requestAnimationFrame(animate)
}
animate()
return () => cancelAnimationFrame(animRef.current)
}, [type])
return <canvas ref={canvasRef} className="w-full h-full" />
}
```
## Reaction-Diffusion
Gray-Scott model for organic patterns.
```tsx
'use client'
import { useRef, useEffect } from 'react'
export function ReactionDiffusion({ width = 200, height = 200, feed = 0.055, kill = 0.062 }) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const animRef = useRef<number>(0)
useEffect(() => {
const canvas = canvasRef.current!
const ctx = canvas.getContext('2d')!
canvas.width = width
canvas.height = height
// Two chemical concentrations
const gridA = new Float32Array(width * height).fill(1)
const gridB = new Float32Array(width * height).fill(0)
const nextA = new Float32Array(width * height)
const nextB = new Float32Array(width * height)
// Seed center with chemical B
for (let y = height / 2 - 10; y < height / 2 + 10; y++) {
for (let x = width / 2 - 10; x < width / 2 + 10; x++) {
gridB[y * width + x] = 1
}
}
const dA = 1.0, dB = 0.5
const imageData = ctx.createImageData(width, height)
function laplacian(grid: Float32Array, x: number, y: number): number {
const i = y * width + x
let sum = -grid[i]
sum += grid[((y - 1 + height) % height) * width + x] * 0.2
sum += grid[((y + 1) % height) * width + x] * 0.2
sum += grid[y * width + (x - 1 + width) % width] * 0.2
sum += grid[y * width + (x + 1) % width] * 0.2
sum += grid[((y - 1 + height) % height) * width + (x - 1 + width) % width] * 0.05
sum += grid[((y - 1 + height) % height) * width + (x + 1) % width] * 0.05
sum += grid[((y + 1) % height) * width + (x - 1 + width) % width] * 0.05
sum += grid[((y + 1) % height) * width + (x + 1) % width] * 0.05
return sum
}
const animate = () => {
for (let step = 0; step < 5; step++) {
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = y * width + x
const a = gridA[i], b = gridB[i]
const abb = a * b * b
nextA[i] = a + (dA * laplacian(gridA, x, y) - abb + feed * (1 - a))
nextB[i] = b + (dB * laplacian(gridB, x, y) + abb - (kill + feed) * b)
nextA[i] = Math.max(0, Math.min(1, nextA[i]))
nextB[i] = Math.max(0, Math.min(1, nextB[i]))
}
}
gridA.set(nextA)
gridB.set(nextB)
}
for (let i = 0; i < width * height; i++) {
const val = Math.floor((1 - gridB[i]) * 255)
const idx = i * 4
imageData.data[idx] = val * 0.2
imageData.data[idx + 1] = val * 0.5
imageData.data[idx + 2] = val
imageData.data[idx + 3] = 255
}
ctx.putImageData(imageData, 0, 0)
animRef.current = requestAnimationFrame(animate)
}
animate()
return () => cancelAnimationFrame(animRef.current)
}, [width, height, feed, kill])
return (
<canvas
ref={canvasRef}
className="w-full h-full"
style={{ imageRendering: 'pixelated' }}
/>
)
}
```
## Cellular Automata
Game of Life and elementary automata as visual patterns.
```tsx
'use client'
import { useRef, useEffect, useCallback } from 'react'
type AutomatonType = 'gameOfLife' | 'elementary'
export function CellularAutomaton({ type = 'gameOfLife', rule = 110, cellSize = 4 }: {
type?: AutomatonType; rule?: number; cellSize?: number
}) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const animRef = useRef<number>(0)
const stepGameOfLife = useCallback((grid: Uint8Array, cols: number, rows: number) => {
const next = new Uint8Array(grid.length)
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
let neighbors = 0
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
if (dx === 0 && dy === 0) continue
const nx = (x + dx + cols) % cols
const ny = (y + dy + rows) % rows
neighbors += grid[ny * cols + nx]
}
}
const alive = grid[y * cols + x]
next[y * cols + x] = alive
? (neighbors === 2 || neighbors === 3 ? 1 : 0)
: (neighbors === 3 ? 1 : 0)
}
}
return next
}, [])
useEffect(() => {
const canvas = canvasRef.current!
const ctx = canvas.getContext('2d')!
const w = canvas.offsetWidth
const h = canvas.offsetHeight
canvas.width = w * 2
canvas.height = h * 2
ctx.scale(2, 2)
const cols = Math.floor(w / cellSize)
const rows = Math.floor(h / cellSize)
if (type === 'gameOfLife') {
let grid = new Uint8Array(cols * rows)
// Random init
for (let i = 0; i < grid.length; i++) grid[i] = Math.random() > 0.7 ? 1 : 0
const animate = () => {
ctx.fillStyle = '#000'
ctx.fillRect(0, 0, w, h)
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
if (grid[y * cols + x]) {
ctx.fillStyle = `hsl(${(x + y) * 3}, 70%, 60%)`
ctx.fillRect(x * cellSize, y * cellSize, cellSize - 1, cellSize - 1)
}
}
}
grid = stepGameOfLife(grid, cols, rows)
animRef.current = requestAnimationFrame(animate)
}
animate()
} else {
// Elementary automaton (1D evolving downward)
let row = new Uint8Array(cols)
row[Math.floor(cols / 2)] = 1
let currentRow = 0
ctx.fillStyle = '#000'
ctx.fillRect(0, 0, w, h)
const animate = () => {
if (currentRow >= rows) {
ctx.drawImage(canvas, 0, cellSize * 2, w * 2, h * 2, 0, 0, w, h)
currentRow = rows - 1
}
for (let x = 0; x < cols; x++) {
if (row[x]) {
ctx.fillStyle = `hsl(${currentRow * 2}, 70%, 60%)`
ctx.fillRect(x * cellSize, currentRow * cellSize, cellSize - 1, cellSize - 1)
}
}
const newRow = new Uint8Array(cols)
for (let x = 0; x < cols; x++) {
const left = row[(x - 1 + cols) % cols]
const center = row[x]
const right = row[(x + 1) % cols]
const pattern = (left << 2) | (center << 1) | right
newRow[x] = (rule >> pattern) & 1
}
row = newRow
currentRow++
animRef.current = requestAnimationFrame(animate)
}
animate()
}
return () => cancelAnimationFrame(animRef.current)
}, [type, rule, cellSize, stepGameOfLife])
return <canvas ref={canvasRef} className="w-full h-full" />
}
```
## Noise Patterns
Perlin/Simplex noise for generative textures. For production use the `simplex-noise` package.
```tsx
'use client'
import { useRef, useEffect } from 'react'
// Install: npm install simplex-noise
import { createNoise3D } from 'simplex-noise'
export function NoiseTexture({ scale = 100, speed = 0.5, colorMode = 'gradient' }: {
scale?: number; speed?: number; colorMode?: 'gradient' | 'contour' | 'domain-warp'
}) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const animRef = useRef<number>(0)
useEffect(() => {
const canvas = canvasRef.current!
const ctx = canvas.getContext('2d')!
const w = 300, h = 300
canvas.width = w
canvas.height = h
const noise3D = createNoise3D()
const imageData = ctx.createImageData(w, h)
let t = 0
const animate = () => {
t += speed * 0.01
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
let val: number
if (colorMode === 'domain-warp') {
const warpX = noise3D(x / scale, y / scale, t) * 50
const warpY = noise3D(x / scale + 100, y / scale + 100, t) * 50
val = (noise3D((x + warpX) / scale, (y + warpY) / scale, t) + 1) / 2
} else {
val = (noise3D(x / scale, y / scale, t) + 1) / 2
}
const idx = (y * w + x) * 4
if (colorMode === 'contour') {
const line = Math.abs(Math.sin(val * Math.PI * 8)) > 0.95 ? 255 : 0
imageData.data[idx] = line
imageData.data[idx + 1] = line
imageData.data[idx + 2] = line
} else {
const hue = val * 360
// HSL to RGB approximate
const c = 0.6, m = 0.2
imageData.data[idx] = (val * 0.3 + 0.1) * 255
imageData.data[idx + 1] = val * 200
imageData.data[idx + 2] = (1 - val * 0.5) * 255
}
imageData.data[idx + 3] = 255
}
}
ctx.putImageData(imageData, 0, 0)
animRef.current = requestAnimationFrame(animate)
}
animate()
return () => cancelAnimationFrame(animRef.current)
}, [scale, speed, colorMode])
return (
<canvas
ref={canvasRef}
className="w-full h-full"
style={{ imageRendering: 'pixelated' }}
/>
)
}
```
## Sacred Geometry
Golden spiral, Flower of Life, and Metatron's Cube.
```tsx
'use client'
import { useRef, useEffect } from 'react'
type SacredType = 'golden-spiral' | 'flower-of-life' | 'metatron'
export function SacredGeometry({ type = 'flower-of-life' }: { type?: SacredType }) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const animRef = useRef<number>(0)
useEffect(() => {
const canvas = canvasRef.current!
const ctx = canvas.getContext('2d')!
const w = canvas.offsetWidth
const h = canvas.offsetHeight
canvas.width = w * 2
canvas.height = h * 2
ctx.scale(2, 2)
const cx = w / 2, cy = h / 2
let progress = 0
const drawFlowerOfLife = (p: number) => {
ctx.clearRect(0, 0, w, h)
ctx.strokeStyle = '#c084fc'
ctx.lineWidth = 1
const r = Math.min(w, h) * 0.12
const rings = [
[[0, 0]],
Array.from({ length: 6 }, (_, i) => {
const a = (i * 60 * Math.PI) / 180
return [Math.cos(a) * r, Math.sin(a) * r]
}),
Array.from({ length: 6 }, (_, i) => {
const a = ((i * 60 + 30) * Math.PI) / 180
return [Math.cos(a) * r * Math.sqrt(3), Math.sin(a) * r * Math.sqrt(3)]
}),
]
const allCenters = rings.flat()
const visibleCount = Math.floor(p * allCenters.length)
allCenters.slice(0, visibleCount).forEach(([ox, oy], i) => {
ctx.globalAlpha = Math.min(1, (p * allCenters.length - i) * 0.5)
ctx.beginPath()
ctx.arc(cx + ox, cy + oy, r, 0, Math.PI * 2)
ctx.stroke()
})
ctx.globalAlpha = 1
}
const drawGoldenSpiral = (p: number) => {
ctx.clearRect(0, 0, w, h)
const phi = (1 + Math.sqrt(5)) / 2
const maxAngle = p * Math.PI * 10
ctx.beginPath()
ctx.strokeStyle = '#fbbf24'
ctx.lineWidth = 2
for (let a = 0; a < maxAngle; a += 0.02) {
const r = Math.pow(phi, (a * 2) / Math.PI) * 2
const x = cx + r * Math.cos(a)
const y = cy + r * Math.sin(a)
if (a === 0) ctx.moveTo(x, y)
else ctx.lineTo(x, y)
if (r > Math.max(w, h)) break
}
ctx.stroke()
// Draw golden rectangles
ctx.strokeStyle = 'rgba(251, 191, 36, 0.3)'
let size = 2
let rx = cx, ry = cy
for (let i = 0; i < Math.floor(p * 12); i++) {
ctx.strokeRect(rx - size / 2, ry - size / 2, size, size)
size *= phi
}
}
const drawMetatron = (p: number) => {
ctx.clearRect(0, 0, w, h)
const r = Math.min(w, h) * 0.3
// 13 circles of Metatron's Cube
const centers: [number, number][] = [[0, 0]]
for (let ring = 1; ring <= 2; ring++) {
const count = 6
const dist = r * ring * 0.5
for (let i = 0; i < count; i++) {
const a = ((i * 60 + (ring === 2 ? 30 : 0)) * Math.PI) / 180
centers.push([Math.cos(a) * dist, Math.sin(a) * dist])
}
}
const circleCount = Math.floor(p * centers.length)
// Draw connecting lines
ctx.strokeStyle = 'rgba(96, 165, 250, 0.3)'
ctx.lineWidth = 0.5
const lineProgress = Math.max(0, (p - 0.3) / 0.7)
for (let i = 0; i < centers.length; i++) {
for (let j = i + 1; j < centers.length; j++) {
if (Math.random() < lineProgress) {
ctx.beginPath()
ctx.moveTo(cx + centers[i][0], cy + centers[i][1])
ctx.lineTo(cx + centers[j][0], cy + centers[j][1])
ctx.stroke()
}
}
}
// Draw circles
ctx.strokeStyle = '#60a5fa'
ctx.lineWidth = 1.5
centers.slice(0, circleCount).forEach(([ox, oy]) => {
ctx.beginPath()
ctx.arc(cx + ox, cy + oy, r * 0.25, 0, Math.PI * 2)
ctx.stroke()
})
}
const animate = () => {
progress = Math.min(1, progress + 0.005)
switch (type) {
case 'flower-of-life': drawFlowerOfLife(progress); break
case 'golden-spiral': drawGoldenSpiral(progress); break
case 'metatron': drawMetatron(progress); break
}
if (progress < 1) animRef.current = requestAnimationFrame(animate)
}
animate()
return () => cancelAnimationFrame(animRef.current)
}, [type])
return <canvas ref={canvasRef} className="w-full h-full bg-gray-950" />
}
```
## Performance Tips
- **Canvas resolution**: Use `devicePixelRatio` for retina, but cap at 2x for performance
- **Particle count**: Keep under 5000 for 60fps, use web workers for heavy computation
- **RequestAnimationFrame**: Always clean up with `cancelAnimationFrame` on unmount
- **OffscreenCanvas**: Use for heavy rendering in web workers
- **Float32Array**: Use typed arrays for grid-based simulations (reaction-diffusion, automata)
- **Batch draw calls**: Minimize `beginPath/stroke` calls per frame