canvas-generative

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Canvas Generative

Canvas生成艺术

Algorithmic and generative art with Canvas 2D. Concise rules here. Deep-dive and reference implementations in
references/
.

基于Canvas 2D的算法与生成艺术。 此处为简明规则。深入内容及参考实现请查看
references/
目录。

Canvas 2D Setup

Canvas 2D 配置

DPR-Aware Sizing

适配DPR的尺寸设置

Every canvas must be sharp on Retina/HiDPI displays. Set the buffer size to the physical pixel size, scale down with CSS.
js
function setupCanvas(canvas, width, height) {
  const dpr = window.devicePixelRatio || 1;
  canvas.width = width * dpr;
  canvas.height = height * dpr;
  canvas.style.width = `${width}px`;
  canvas.style.height = `${height}px`;
  const ctx = canvas.getContext('2d');
  ctx.scale(dpr, dpr);
  return ctx;
}
所有Canvas在Retina/HiDPI显示器上必须显示清晰。将缓冲区大小设置为物理像素尺寸,通过CSS缩小显示。
js
function setupCanvas(canvas, width, height) {
  const dpr = window.devicePixelRatio || 1;
  canvas.width = width * dpr;
  canvas.height = height * dpr;
  canvas.style.width = `${width}px`;
  canvas.style.height = `${height}px`;
  const ctx = canvas.getContext('2d');
  ctx.scale(dpr, dpr);
  return ctx;
}

Resize Handler

尺寸调整处理器

js
function handleResize(canvas, ctx, draw) {
  const ro = new ResizeObserver(([entry]) => {
    const { width, height } = entry.contentRect;
    const dpr = window.devicePixelRatio || 1;
    canvas.width = width * dpr;
    canvas.height = height * dpr;
    ctx.scale(dpr, dpr);
    draw(); // re-render after resize
  });
  ro.observe(canvas.parentElement);
  return () => ro.disconnect();
}
js
function handleResize(canvas, ctx, draw) {
  const ro = new ResizeObserver(([entry]) => {
    const { width, height } = entry.contentRect;
    const dpr = window.devicePixelRatio || 1;
    canvas.width = width * dpr;
    canvas.height = height * dpr;
    ctx.scale(dpr, dpr);
    draw(); // 调整尺寸后重新渲染
  });
  ro.observe(canvas.parentElement);
  return () => ro.disconnect();
}

Animation Loop (RAF)

动画循环(RAF)

js
let animId;
let prevTime = 0;

function loop(time) {
  const dt = Math.min((time - prevTime) / 1000, 0.1); // cap delta to avoid spiral of death
  prevTime = time;

  update(dt);
  render(ctx);

  animId = requestAnimationFrame(loop);
}

// Start
animId = requestAnimationFrame(loop);
// Stop
cancelAnimationFrame(animId);

js
let animId;
let prevTime = 0;

function loop(time) {
  const dt = Math.min((time - prevTime) / 1000, 0.1); // 限制delta值以避免性能崩溃
  prevTime = time;

  update(dt);
  render(ctx);

  animId = requestAnimationFrame(loop);
}

// 启动
animId = requestAnimationFrame(loop);
// 停止
cancelAnimationFrame(animId);

Noise

噪声

TypeCharacteristicsBest For
PerlinSmooth, grid-aligned bias, cheaperTerrain, clouds, gentle organic textures
SimplexNo grid artifacts, better gradients, slightly costlierFlow fields, organic motion, seamless tiling
Worley (Cellular)Distance-to-nearest-point, cell-likeVoronoi patterns, caustics, cracks, cell textures
Usage rules:
  • Always scale input coordinates (divide by a
    noiseScale
    factor) -- raw pixel coords produce visual noise
  • Use octaves (fractal Brownian motion) for detail: sum multiple noise calls at increasing frequency and decreasing amplitude
  • Seed your noise for reproducibility
js
// fBm pattern
function fbm(x, y, octaves = 4, lacunarity = 2, gain = 0.5) {
  let value = 0, amplitude = 1, frequency = 1, maxAmp = 0;
  for (let i = 0; i < octaves; i++) {
    value += amplitude * noise2D(x * frequency, y * frequency);
    maxAmp += amplitude;
    amplitude *= gain;
    frequency *= lacunarity;
  }
  return value / maxAmp; // normalize to [-1, 1]
}

类型特性适用场景
Perlin平滑、网格对齐偏差、性能消耗低地形、云层、柔和的有机纹理
Simplex无网格伪影、渐变效果更好、性能消耗略高流场、有机运动、无缝平铺
Worley(细胞噪声)基于到最近点的距离、细胞状结构Voronoi图案、焦散效果、裂纹、细胞纹理
使用规则:
  • 务必缩放输入坐标(除以
    noiseScale
    因子)——原始像素坐标会产生视觉噪点
  • 使用八度(分形布朗运动)增加细节:叠加多次不同频率和振幅的噪声调用结果
  • 设置噪声种子以保证可复现性
js
// fBm图案
function fbm(x, y, octaves = 4, lacunarity = 2, gain = 0.5) {
  let value = 0, amplitude = 1, frequency = 1, maxAmp = 0;
  for (let i = 0; i < octaves; i++) {
    value += amplitude * noise2D(x * frequency, y * frequency);
    maxAmp += amplitude;
    amplitude *= gain;
    frequency *= lacunarity;
  }
  return value / maxAmp; // 归一化到[-1, 1]
}

Particle Systems

粒子系统

Pool Pattern (No GC Pressure)

对象池模式(避免GC压力)

Pre-allocate a fixed array. Never
new
or
splice
at runtime.
js
const POOL_SIZE = 10000;
const particles = new Array(POOL_SIZE);
let aliveCount = 0;

// Init pool
for (let i = 0; i < POOL_SIZE; i++) {
  particles[i] = { x: 0, y: 0, vx: 0, vy: 0, life: 0, maxLife: 0, active: false };
}

function spawn(x, y) {
  if (aliveCount >= POOL_SIZE) return;
  const p = particles[aliveCount++];
  p.x = x; p.y = y;
  p.vx = (Math.random() - 0.5) * 2;
  p.vy = (Math.random() - 0.5) * 2;
  p.life = 0; p.maxLife = 60 + Math.random() * 60;
  p.active = true;
}

function update() {
  for (let i = aliveCount - 1; i >= 0; i--) {
    const p = particles[i];
    p.x += p.vx; p.y += p.vy;
    p.life++;
    if (p.life >= p.maxLife) {
      // Swap with last alive, shrink pool
      particles[i] = particles[--aliveCount];
      particles[aliveCount] = p;
      p.active = false;
    }
  }
}

预分配固定大小的数组。运行时绝不使用
new
splice
js
const POOL_SIZE = 10000;
const particles = new Array(POOL_SIZE);
let aliveCount = 0;

// 初始化对象池
for (let i = 0; i < POOL_SIZE; i++) {
  particles[i] = { x: 0, y: 0, vx: 0, vy: 0, life: 0, maxLife: 0, active: false };
}

function spawn(x, y) {
  if (aliveCount >= POOL_SIZE) return;
  const p = particles[aliveCount++];
  p.x = x; p.y = y;
  p.vx = (Math.random() - 0.5) * 2;
  p.vy = (Math.random() - 0.5) * 2;
  p.life = 0; p.maxLife = 60 + Math.random() * 60;
  p.active = true;
}

function update() {
  for (let i = aliveCount - 1; i >= 0; i--) {
    const p = particles[i];
    p.x += p.vx; p.y += p.vy;
    p.life++;
    if (p.life >= p.maxLife) {
      // 与最后一个活跃粒子交换,缩小活跃池
      particles[i] = particles[--aliveCount];
      particles[aliveCount] = p;
      p.active = false;
    }
  }
}

Flow Fields

流场

Grid of angle vectors that steer particles. The classic generative recipe.
  1. Build grid: Divide canvas into cells, compute an angle per cell (from noise)
  2. Lookup: Particle position maps to grid cell, retrieve angle
  3. Steer: Apply angle as velocity, accumulate over frames
js
const cols = Math.ceil(width / cellSize);
const rows = Math.ceil(height / cellSize);
const field = new Float32Array(cols * rows);

// Fill with noise-based angles
for (let y = 0; y < rows; y++) {
  for (let x = 0; x < cols; x++) {
    field[y * cols + x] = noise2D(x * 0.05, y * 0.05) * Math.PI * 2;
  }
}

// Particle follow
function followField(p) {
  const col = Math.floor(p.x / cellSize);
  const row = Math.floor(p.y / cellSize);
  if (col >= 0 && col < cols && row >= 0 && row < rows) {
    const angle = field[row * cols + col];
    p.vx += Math.cos(angle) * force;
    p.vy += Math.sin(angle) * force;
  }
  // Damping to prevent runaway velocity
  p.vx *= 0.98;
  p.vy *= 0.98;
}

由角度向量组成的网格,用于引导粒子运动。经典的生成艺术方案。
  1. 构建网格: 将画布划分为单元格,为每个单元格计算一个基于噪声的角度
  2. 查找角度: 粒子位置映射到网格单元格,获取对应角度
  3. 引导运动: 将角度转换为速度,逐帧累积
js
const cols = Math.ceil(width / cellSize);
const rows = Math.ceil(height / cellSize);
const field = new Float32Array(cols * rows);

// 用基于噪声的角度填充流场
for (let y = 0; y < rows; y++) {
  for (let x = 0; x < cols; x++) {
    field[y * cols + x] = noise2D(x * 0.05, y * 0.05) * Math.PI * 2;
  }
}

// 粒子跟随流场
function followField(p) {
  const col = Math.floor(p.x / cellSize);
  const row = Math.floor(p.y / cellSize);
  if (col >= 0 && col < cols && row >= 0 && row < rows) {
    const angle = field[row * cols + col];
    p.vx += Math.cos(angle) * force;
    p.vy += Math.sin(angle) * force;
  }
  // 阻尼处理防止速度失控
  p.vx *= 0.98;
  p.vy *= 0.98;
}

Fractals & L-Systems

分形与L系统

An L-system encodes recursive structure as string rewriting + turtle graphics.
ComponentRole
AxiomStarting string (e.g.
"F"
)
RulesProduction rules (e.g.
"F" -> "F[+F]F[-F]F"
)
AngleTurtle turn angle per
+
/
-
IterationsHow many times to apply rules
js
function lsystem(axiom, rules, iterations) {
  let current = axiom;
  for (let i = 0; i < iterations; i++) {
    current = current.split('').map(c => rules[c] || c).join('');
  }
  return current;
}

function drawLSystem(ctx, commands, len, angle) {
  const stack = [];
  for (const c of commands) {
    switch (c) {
      case 'F': ctx.lineTo(ctx._x += Math.cos(ctx._a) * len, ctx._y += Math.sin(ctx._a) * len); break;
      case '+': ctx._a += angle; break;
      case '-': ctx._a -= angle; break;
      case '[': stack.push({ x: ctx._x, y: ctx._y, a: ctx._a }); break;
      case ']': { const s = stack.pop(); ctx._x = s.x; ctx._y = s.y; ctx._a = s.a; ctx.moveTo(s.x, s.y); } break;
    }
  }
}

L系统通过字符串重写和海龟图形来编码递归结构。
组件作用
公理起始字符串(例如
"F"
规则生成规则(例如
"F" -> "F[+F]F[-F]F"
角度海龟每次
+
/
-
操作的转向角度
迭代次数应用规则的次数
js
function lsystem(axiom, rules, iterations) {
  let current = axiom;
  for (let i = 0; i < iterations; i++) {
    current = current.split('').map(c => rules[c] || c).join('');
  }
  return current;
}

function drawLSystem(ctx, commands, len, angle) {
  const stack = [];
  for (const c of commands) {
    switch (c) {
      case 'F': ctx.lineTo(ctx._x += Math.cos(ctx._a) * len, ctx._y += Math.sin(ctx._a) * len); break;
      case '+': ctx._a += angle; break;
      case '-': ctx._a -= angle; break;
      case '[': stack.push({ x: ctx._x, y: ctx._y, a: ctx._a }); break;
      case ']': { const s = stack.pop(); ctx._x = s.x; ctx._y = s.y; ctx._a = s.a; ctx.moveTo(s.x, s.y); } break;
    }
  }
}

Double Buffer Pattern

双缓冲模式

Render to an offscreen canvas, then blit to the visible one. Eliminates flicker and enables trail effects.
js
const offscreen = document.createElement('canvas');
offscreen.width = canvas.width;
offscreen.height = canvas.height;
const offCtx = offscreen.getContext('2d');

function render() {
  // Draw to offscreen
  offCtx.fillStyle = 'rgba(0, 0, 0, 0.05)'; // trail fade
  offCtx.fillRect(0, 0, offscreen.width, offscreen.height);
  drawParticles(offCtx);

  // Blit to screen
  ctx.drawImage(offscreen, 0, 0);
}

渲染到离屏Canvas,然后再复制到可见Canvas。消除闪烁并实现轨迹效果。
js
const offscreen = document.createElement('canvas');
offscreen.width = canvas.width;
offscreen.height = canvas.height;
const offCtx = offscreen.getContext('2d');

function render() {
  // 绘制到离屏Canvas
  offCtx.fillStyle = 'rgba(0, 0, 0, 0.05)'; // 轨迹渐隐效果
  offCtx.fillRect(0, 0, offscreen.width, offscreen.height);
  drawParticles(offCtx);

  // 复制到屏幕Canvas
  ctx.drawImage(offscreen, 0, 0);
}

Do Not

注意事项

1. Never clearRect every frame for trail effects

1. 实现轨迹效果时,绝不要每帧调用clearRect

Clearing destroys the trail. Use a semi-transparent fill instead.
js
// BAD -- kills trails
ctx.clearRect(0, 0, w, h);

// GOOD -- fades previous frame
ctx.fillStyle = 'rgba(0, 0, 0, 0.02)';
ctx.fillRect(0, 0, w, h);
清除画布会破坏轨迹。改用半透明填充。
js
// 错误写法——会清除轨迹
ctx.clearRect(0, 0, w, h);

// 正确写法——渐隐上一帧内容
ctx.fillStyle = 'rgba(0, 0, 0, 0.02)';
ctx.fillRect(0, 0, w, h);

2. Never getImageData in the animation loop

2. 绝不要在动画循环中调用getImageData

getImageData
reads back from GPU -- extremely slow. Avoid unless absolutely required (e.g. one-time color sampling).
js
// BAD -- 60fps GPU readback
function loop() {
  const data = ctx.getImageData(0, 0, w, h); // blocks rendering pipeline
  processPixels(data);
  requestAnimationFrame(loop);
}

// GOOD -- sample once, cache
const colorMap = ctx.getImageData(0, 0, w, h);
function getColor(x, y) {
  const i = (y * w + x) * 4;
  return [colorMap.data[i], colorMap.data[i+1], colorMap.data[i+2]];
}
getImageData
会从GPU读取数据——速度极慢。除非绝对必要(例如一次性颜色采样),否则避免使用。
js
// 错误写法——60fps下持续从GPU读取数据
function loop() {
  const data = ctx.getImageData(0, 0, w, h); // 阻塞渲染流水线
  processPixels(data);
  requestAnimationFrame(loop);
}

// 正确写法——采样一次并缓存
const colorMap = ctx.getImageData(0, 0, w, h);
function getColor(x, y) {
  const i = (y * w + x) * 4;
  return [colorMap.data[i], colorMap.data[i+1], colorMap.data[i+2]];
}

3. Always respect DPR for sharpness

3. 务必适配DPR以保证清晰度

A canvas without DPR scaling looks blurry on Retina displays. See setup section above.
js
// BAD
canvas.width = 800;
canvas.height = 600;

// GOOD
const dpr = window.devicePixelRatio || 1;
canvas.width = 800 * dpr;
canvas.height = 600 * dpr;
canvas.style.width = '800px';
canvas.style.height = '600px';
ctx.scale(dpr, dpr);
未做DPR缩放的Canvas在Retina显示器上会模糊。请参考上方的配置部分。
js
// 错误写法
canvas.width = 800;
canvas.height = 600;

// 正确写法
const dpr = window.devicePixelRatio || 1;
canvas.width = 800 * dpr;
canvas.height = 600 * dpr;
canvas.style.width = '800px';
canvas.style.height = '600px';
ctx.scale(dpr, dpr);

4. Never allocate in the hot loop

4. 绝不要在热点循环中分配内存

No
new
, no object spread, no array creation inside
update()
or
render()
. Pre-allocate everything.
js
// BAD
function update() {
  particles.forEach(p => {
    const force = { x: Math.cos(a), y: Math.sin(a) }; // new object every frame per particle
    p.vx += force.x;
  });
}

// GOOD
let fx = 0, fy = 0; // reuse
function update() {
  for (let i = 0; i < aliveCount; i++) {
    fx = Math.cos(a); fy = Math.sin(a);
    particles[i].vx += fx;
  }
}

update()
render()
中不要使用
new
、对象扩展或数组创建。提前分配所有资源。
js
// 错误写法
function update() {
  particles.forEach(p => {
    const force = { x: Math.cos(a), y: Math.sin(a) }; // 每帧每个粒子都创建新对象
    p.vx += force.x;
  });
}

// 正确写法
let fx = 0, fy = 0; // 复用变量
function update() {
  for (let i = 0; i < aliveCount; i++) {
    fx = Math.cos(a); fy = Math.sin(a);
    particles[i].vx += fx;
  }
}

Quick Reference: Loading Sub-skills

快速参考:加载子技能

NeedLoad
Noise implementations, particle pool, flow field, L-system, attractors
references/algorithms.md
Timing and easing for animated transitions
../motion-principles/SKILL.md
3D generative (shaders, GPU particles)
../threejs-r3f/SKILL.md
需求加载路径
噪声实现、粒子对象池、流场、L系统、吸引子
references/algorithms.md
动画过渡的时序与缓动效果
../motion-principles/SKILL.md
3D生成艺术(着色器、GPU粒子)
../threejs-r3f/SKILL.md