逐步构建生成艺术 — Node Garden 示例

发布: (2026年1月18日 GMT+8 07:14)
8 min read
原文: Dev.to

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,我设想了以下内容:

IdeaDescription
Points floating点在画布上漂浮。
Lines connect when they get close当两个点的距离在一定范围内时,它们之间会出现一条线。
They influence each other and change movement节点之间相互施加力(排斥/吸引),改变运动轨迹。
Something organic feeling每个节点都有自己的“性格”;线条以动画方式拉伸,呈现有机感。

即使是“有机感”这种模糊概念,也应该先写下来。

2️⃣ 将每个描述拆解为具体过程

Visual ElementProcess
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 控件,或优化双重循环的性能。

🎉 你现在拥有的

  1. 在画布上漂浮并环绕。
  2. 线 当点靠近时出现,透明度取决于距离。
  3. 电荷 使节点相互吸引或排斥。
  4. 动画线条生长 为整个系统赋予有机的“活体”感觉。

随意实验:更改节点数量、微调力场,或添加颜色渐变。分层方法使得在不破坏现有功能的情况下轻松扩展花园。祝编码愉快!

代码片段

// 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:

艺术创作流程概览

在实现艺术表达时,关键在于将脑中的图像:

  1. 语言化 – 用文字表达这个想法。
  2. 拆解为元素 – 确认构成最终作品的各个独立部分。
  3. 组织为层次 – 决定元素的添加顺序,从背景到前景。
  4. 从底层开始构建 – 先建立基础层,再向上工作。

在每一层,你都可以验证“它在运行、它是正确的”,这让发现问题变得容易。你也可以在中途决定“我不需要这个元素”,并将其移除。

Back to Blog

相关文章

阅读更多 »

ASCII 云

文章 URL: https://caidan.dev/portfolio/ascii_clouds/ 评论 URL: https://news.ycombinator.com/item?id=46611507 积分: 7 评论: 2