mcpField
C
canvas-generative

canvas-generative

github.com/athevon创意与媒体698994 次安装3 周前更新
代码仓库

关于 canvas-generative

面向创意编程的 Canvas 2D 生成艺术技能,提供噪声、粒子、流场和递归结构的实现指引,并关注清晰渲染与动画循环效率。

主要功能

  1. 01适配 DPR 的画布尺寸设置
  2. 02噪声与可复现图案
  3. 03粒子池与流场
  4. 04分形与 L-system
  5. 05离屏渲染与轨迹效果

适用场景

  1. 01在高 DPI 屏幕上绘制清晰的生成艺术
  2. 02使用流场创建粒子系统
  3. 03用 L-system 构建递归结构

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/athevon/genjutsu --skill canvas-generative

安装后在 Claude Code 中输入/canvas-generative确认已加载。

详细介绍

版本敏感。 以下每个 API 名称、SDK 门槛和浏览器支持声明,均已于 2026-09-08 根据一手资料核实。核对了哪些资料、何时核对,详见 _jutsu/VERSIONS.md。如果该日期已久,请在根据版本号采取行动前重新核实。

Canvas 生成艺术

使用 Canvas 2D 创作算法艺术和生成艺术。 此处列出简明规则。深入讲解和参考实现见 references/。


Canvas 2D 设置

适配 DPR 的尺寸

每个 Canvas 都必须在 Retina/HiDPI 显示屏上保持清晰。将缓冲区大小设为物理像素尺寸,并使用 CSS 缩小显示。

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;
}
调整大小处理程序
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();
}
动画循环(RAF)
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);

噪声

类型特点最适用场景
Perlin平滑、有网格对齐偏差、开销较低地形、云、柔和的有机纹理
Simplex无网格伪影、渐变更佳、开销略高流场、有机运动、无缝平铺
Worley(细胞噪声)到最近点的距离、呈细胞状Voronoi 图案、焦散、裂纹、细胞纹理

使用规则:

  • 始终缩放输入坐标(除以 noiseScale 因子)——原始像素坐标会产生视觉噪声
  • 使用倍频(分形布朗运动)增加细节:以递增的频率和递减的振幅,累加多次噪声调用
  • 为噪声设置种子,以确保结果可复现
// 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]
}

粒子系统

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

预先分配固定数组。运行时绝不使用 new 或 splice。

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;
    }
  }
}

流场

由角度向量组成的网格,用于引导粒子。这是经典的生成艺术配方。

  1. 构建网格: 将 Canvas 划分为网格单元,并根据噪声为每个单元计算一个角度
  2. 查找: 将粒子位置映射到网格单元,获取对应角度
  3. 引导: 将角度用作速度,并在多个帧中累积
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;
}

分形与 L 系统

L 系统通过字符串重写和海龟图形编码递归结构。

组成部分作用
公理起始字符串(例如 "F")
规则生成规则(例如 "F" -> "F[+F]F[-F]F")
角度海龟图形每遇到 +/- 时转向的角度
迭代次数应用规则的次数
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;
    }
  }
}

双缓冲模式

先渲染到离屏 Canvas,再复制到可见 Canvas。这样可以消除闪烁并实现拖尾效果。

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);
}

不要这样做

1. 绝不要每帧都用 clearRect 实现拖尾效果

清除操作会抹掉拖尾。请改用半透明填充。

// 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);
2. 绝不要在动画循环中调用 getImageData

getImageData 会从 GPU 读取数据,速度极慢。除非绝对必要(例如只进行一次颜色采样),否则请避免使用。

// 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]];
}
3. 始终考虑 DPR,确保清晰度

未进行 DPR 缩放的 Canvas 在 Retina 显示屏上会显得模糊。参见上文的设置部分。

// 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);
4. 绝不要在热点循环中分配内存

不要在 update() 或 render() 中使用 new、对象展开或创建数组。请预先分配所有内容。

// 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;
  }
}

快速参考:加载子技能

需求加载
噪声实现、粒子池、流场、L 系统、吸引子references/algorithms.md
动画过渡的计时和缓动../motion-principles/SKILL.md
3D 生成艺术(着色器、GPU 粒子)../threejs-r3f/SKILL.md

评论

支持 Markdown