Generative Art을 단계별로 구축하기 — Node Garden 예제
I’m sorry, but I can’t retrieve the article from the link you provided. Could you please paste the text you’d like translated here? Once I have the content, I’ll translate it into Korean while preserving the original formatting and code blocks.
Source:
우리가 만들고 있는 것
생성 예술을 만들 때 보통 머릿속에 이미지를 떠올립니다: “이렇게 움직였으면 좋겠어.”
구현하려고 하면 복잡해 보이고 어디서 시작해야 할지 모를 때가 많습니다.
유용한 접근법은 “요소별로 나누어 아래부터 차근차근 쌓아 올린다.” 입니다.
이 글에서는 노드 가든 – 고전적인 생성 예술 스케치 – 을 예시로 들어 그 접근법을 보여드리겠습니다.
1️⃣ 막연한 아이디어를 말로 표현하기
이 노드 가든을 위해 저는 다음과 같이 상상했습니다:
| 아이디어 | 설명 |
|---|---|
| 떠다니는 점 | 점들이 캔버스 위를 떠다닌다. |
| 가까워지면 선이 연결 | 두 점이 일정 거리 이내에 있으면 선이 나타난다. |
| 서로 영향을 주고 움직임이 변함 | 노드들이 서로에게 힘(반발/흡인)을 가한다. |
| 유기적인 느낌 | 각 노드마다 개성이 있으며, 선이 애니메이션처럼 늘어난다. |
“뭔가 유기적인 느낌” 같은 막연한 개념도 지금은 적어 두는 것이 좋습니다.
2️⃣ 각 설명을 구체적인 프로세스로 나누기
| 시각 요소 | 프로세스 |
|---|---|
| 떠다니는 점 | 점을 그리고, 속도를 부여한다. |
| 가까워지면 연결 | 거리를 계산하고, 임계값을 확인해 선을 그린다. |
| 서로 영향을 줌 | 힘(예: 반발)을 적용한다. |
| 유기적인 느낌 | 각 노드에 무작위 “전하”를 할당하고, 선 성장 애니메이션을 만든다. |
구체적인 분해 방식은 사람마다 다를 수 있습니다. 저는 보통 시각 요소와 움직임 규칙을 구분한 뒤, 이를 계층형 의존 구조로 정리합니다.
3️⃣ 레이어 구조 (의존성)
Layer 4: 전하 + 스트레치 애니메이션
↑
Layer 3: 선 연결 & 투명도
↑
Layer 2: 점 이동 & 화면 랩
↑
Layer 1: 점 그리기
상위 레이어는 하위 레이어 없이는 존재할 수 없습니다.
예를 들어 “전하에 의한 힘”을 구현하려면 먼저 “거리 계산”이 필요하고, 거리를 계산하려면 노드의 위치 정보가 필요합니다.
🛠️ 코드를 작성해봅시다 – 각 레이어마다 검증하기
아래에서 단계별 코드 스니펫을 확인할 수 있습니다. 각 블록은 독립적인 단계이며, 다음 단계로 넘어가기 전에 테스트할 수 있습니다.
Step 1 – Draw Points
// 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));
Step 2 – Give Points Velocity and Make Them Move
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();
Step 3 – Calculate Distance & Draw Lines When Within a Threshold
// 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.
Note: 위 코드는 가독성을 위해 의도적으로 간결하게 작성되었습니다. 실제 프로젝트에서는 유틸리티를 분리하거나 UI 컨트롤을 추가하고, 이중 루프를 최적화하는 등의 작업이 필요할 수 있습니다.
🎉 지금 가지고 있는 것
- Points가 캔버스 위를 떠다니며 경계에서 반대쪽으로 이동합니다.
- Lines는 포인트가 가까워질 때 나타나며, 투명도는 거리 기반으로 조절됩니다.
- Charges는 노드가 서로 끌어당기거나 밀어내도록 합니다.
- Animated line growth는 전체 시스템에 유기적이고 “생명감 있는” 느낌을 줍니다.
Feel free to experiment: change the number of nodes, tweak the forces, or add colour gradients. The layered approach makes it easy to extend the garden without breaking existing functionality. Happy coding!
코드 스니펫
// 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);
}
예술적 프로세스 개요
예술적 표현을 구현할 때 중요한 것은 머릿속에 있는 이미지를 다음과 같은 과정으로 옮기는 것입니다:
- 구두화하기 – 아이디어를 말로 표현합니다.
- 요소로 나누기 – 최종 작품을 구성할 개별 구성 요소를 식별합니다.
- 레이어로 조직하기 – 배경에서 전경까지 요소가 추가될 순서를 결정합니다.
- 바닥부터 구축하기 – 기본 레이어부터 시작해 위로 작업합니다.
각 레이어마다 “작동하고, 올바른지”를 확인할 수 있어 문제를 쉽게 발견할 수 있습니다. 또한 중간에 “이 요소는 필요 없어요”라고 판단하여 제거할 수도 있습니다.