逐步构建生成艺术 — Node Garden 示例
It looks like the article text you’d like translated isn’t included in your message. Could you please provide the full content (or the portion you want translated) so I can translate it into Simplified Chinese while preserving the formatting and code blocks?
我们正在构建的内容
在创作生成艺术时,你通常会先在脑海中构思一个图像:“我想让它这样运动。”
当你尝试实现时,往往会觉得很复杂,不知道从哪里入手。
一种实用的方法是 “把它拆解成元素,从底层开始构建。”
本文将以 node garden(经典的生成艺术草图)为例,演示这种方法。
1️⃣ 将模糊的想法转化为文字
对于这个 node garden,我设想了以下内容:
| Idea | Description |
|---|---|
| Points floating | 点在画布上漂浮。 |
| Lines connect when they get close | 当两个点的距离在一定范围内时,它们之间会出现一条线。 |
| They influence each other and change movement | 节点之间相互施加力(排斥/吸引),改变运动轨迹。 |
| Something organic feeling | 每个节点都有自己的“性格”;线条以动画方式拉伸,呈现有机感。 |
即使是“有机感”这种模糊概念,也应该先写下来。
2️⃣ 将每个描述拆解为具体过程
| Visual Element | Process |
|---|---|
| Points floating | 绘制点;赋予它们 velocity(速度)。 |
| Connect when close | 计算距离,检查阈值(threshold),绘制线条。 |
| Influence each other | 应用力(例如排斥力)。 |
| Organic feeling | 为每个节点分配随机的 “charge”,并为线条的生长添加动画。 |
具体的拆解方式因人而异。我通常把 可见元素 与 运动规则 分开,然后组织成分层的依赖结构。
3️⃣ 层级结构(依赖关系)
Layer 4: Charge + Stretch Animation
↑
Layer 3: Line Connection & Opacity
↑
Layer 2: Point Movement & Screen Wrap
↑
Layer 1: Point Drawing
上层离不开下层。例如,要实现 “基于 charge 的力”,首先需要 “距离计算”,而计算距离又需要节点的位置,依此类推。
🛠️ 编写代码 – 在每一层进行验证
下面您会看到逐步的代码片段。每个块都是一个独立的步骤,您可以在继续之前进行测试。
步骤 1 – 绘制点
// CONFIG ---------------------------------------------------------
const CONFIG = {
width: 340,
height: 340,
numNodes: 60,
nodeRadius: 3,
};
// NODE CLASS ----------------------------------------------------
class Node {
constructor(x, y) {
this.x = x;
this.y = y;
this.radius = CONFIG.nodeRadius;
}
draw(ctx) {
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fill();
}
}
// INITIALISE --------------------------------------------------
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const nodes = Array.from({ length: CONFIG.numNodes }, () =>
new Node(
Math.random() * CONFIG.width,
Math.random() * CONFIG.height
)
);
// DRAW ---------------------------------------------------------
ctx.clearRect(0, 0, CONFIG.width, CONFIG.height);
nodes.forEach(node => node.draw(ctx));
步骤 2 – 为点赋予速度并使其移动
class Node {
constructor(x, y) {
this.x = x;
this.y = y;
this.vx = Math.random() * 3 - 1.5; // –1.5 … 1.5
this.vy = Math.random() * 3 - 1.5;
this.radius = CONFIG.nodeRadius;
}
update() {
this.x += this.vx;
this.y += this.vy;
// Wrap around the edges
if (this.x > CONFIG.width) this.x = 0;
else if (this.x CONFIG.height) this.y = 0;
else if (this.y {
node.update();
node.draw(ctx);
});
requestAnimationFrame(loop);
}
loop();
步骤 3 – 计算距离并在距离阈值内绘制连线
// EXTEND CONFIG -------------------------------------------------
const CONFIG = {
// …previous values
minDist: 100,
lineWidth: 1.5,
};
class Node {
// …previous methods
distanceTo(other) {
const dx = this.x - other.x;
const dy = this.y - other.y;
return { dx, dy, dist: Math.sqrt(dx * dx + dy * dy) };
}
}
// UPDATED LOOP -------------------------------------------------
function loop() {
ctx.clearRect(0, 0, CONFIG.width, CONFIG.height);
ctx.lineWidth = CONFIG.lineWidth;
for (let i = 0; i CONFIG.maxSpeed) {
this.vx = (this.vx / speed) * CONFIG.maxSpeed;
this.vy = (this.vy / speed) * CONFIG.maxSpeed;
}
this.x += this.vx;
this.y += this.vy;
// …wrap logic (same as before)
}
applyForce(ax, ay) {
this.vx += ax;
this.vy += ay;
}
distanceTo(other) {
const dx = this.x - other.x;
const dy = this.y - other.y;
return { dx, dy, dist: Math.hypot(dx, dy) };
}
}
// CONNECTION STATE ---------------------------------------------
const connections = new Map(); // key: "i-j", value: { progress: 0…1 }
function getConnectionKey(i, j) {
return i {
node.update();
node.draw(ctx);
});
// Compute forces & connections
for (let i = 0; i **Note:** The code above is intentionally concise for readability. In a production project you might want to extract utilities, add UI controls, or optimise the double‑loop.
注意: 上述代码为了易读性故意写得简洁。在实际项目中,您可能需要抽取工具函数、添加 UI 控件,或优化双重循环的性能。
🎉 你现在拥有的
- 点 在画布上漂浮并环绕。
- 线 当点靠近时出现,透明度取决于距离。
- 电荷 使节点相互吸引或排斥。
- 动画线条生长 为整个系统赋予有机的“活体”感觉。
随意实验:更改节点数量、微调力场,或添加颜色渐变。分层方法使得在不破坏现有功能的情况下轻松扩展花园。祝编码愉快!
代码片段
// Draw a connection between two nodes with a given opacity and progress
function drawConnection(node1, node2, opacity, progress) {
const x1 = node1.x + (node2.x - node1.x) * progress;
const y1 = node1.y + (node2.y - node1.y) * progress;
const x2 = node2.x - (node2.x - node1.x) * progress;
const y2 = node2.y - (node2.y - node1.y) * progress;
ctx.beginPath();
ctx.strokeStyle = `rgba(255, 255, 255, ${opacity})`;
ctx.moveTo(node1.x, node1.y);
ctx.lineTo(x1, y1);
ctx.moveTo(node2.x, node2.y);
ctx.lineTo(x2, y2);
ctx.stroke();
}
// Inside loop
if (dist < CONFIG.minDist) {
const key = getConnectionKey(i, j);
const currentProgress = connections.get(key) || 0;
const newProgress = Math.min(1, currentProgress + CONFIG.lineGrowSpeed);
connections.set(key, newProgress);
const opacity = 1 - dist / CONFIG.minDist;
drawConnection(node1, node2, opacity, newProgress);
// Multiply charges (same sign → repel, opposite sign → attract)
const interaction = node1.charge * node2.charge;
const ax = dx * CONFIG.springAmount * interaction;
const ay = dy * CONFIG.springAmount * interaction;
node1.applyForce(ax, ay);
node2.applyForce(-ax, -ay);
}
Source: …
艺术创作流程概览
在实现艺术表达时,关键在于将脑中的图像:
- 语言化 – 用文字表达这个想法。
- 拆解为元素 – 确认构成最终作品的各个独立部分。
- 组织为层次 – 决定元素的添加顺序,从背景到前景。
- 从底层开始构建 – 先建立基础层,再向上工作。
在每一层,你都可以验证“它在运行、它是正确的”,这让发现问题变得容易。你也可以在中途决定“我不需要这个元素”,并将其移除。