关于 canvas-generative
面向创意编程的 Canvas 2D 生成艺术技能,提供噪声、粒子、流场和递归结构的实现指引,并关注清晰渲染与动画循环效率。
主要功能
- 01适配 DPR 的画布尺寸设置
- 02噪声与可复现图案
- 03粒子池与流场
- 04分形与 L-system
- 05离屏渲染与轨迹效果
适用场景
- 01在高 DPI 屏幕上绘制清晰的生成艺术
- 02使用流场创建粒子系统
- 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;
}
}
}
流场
由角度向量组成的网格,用于引导粒子。这是经典的生成艺术配方。
- 构建网格: 将 Canvas 划分为网格单元,并根据噪声为每个单元计算一个角度
- 查找: 将粒子位置映射到网格单元,获取对应角度
- 引导: 将角度用作速度,并在多个帧中累积
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