picasso-skill 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -45
- package/agents/picasso.md +326 -0
- package/bin/install.mjs +54 -24
- package/package.json +5 -3
- package/skills/picasso/references/accessibility.md +172 -0
- package/skills/picasso/references/design-system.md +14 -14
- package/skills/picasso/references/generative-art.md +626 -32
- package/skills/picasso/references/motion-and-animation.md +2 -2
- package/skills/picasso/references/react-patterns.md +193 -91
- package/skills/picasso/references/responsive-design.md +349 -15
- package/skills/picasso/references/sensory-design.md +294 -50
- package/SKILL.md +0 -202
- package/references/anti-patterns.md +0 -95
- package/references/color-and-contrast.md +0 -174
- package/references/component-patterns.md +0 -113
- package/references/design-system.md +0 -176
- package/references/generative-art.md +0 -54
- package/references/interaction-design.md +0 -162
- package/references/motion-and-animation.md +0 -260
- package/references/react-patterns.md +0 -216
- package/references/responsive-design.md +0 -118
- package/references/sensory-design.md +0 -125
- package/references/spatial-design.md +0 -176
- package/references/typography.md +0 -168
|
@@ -1,54 +1,648 @@
|
|
|
1
1
|
# Generative Art Reference
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Table of Contents
|
|
4
|
+
1. Philosophy
|
|
5
|
+
2. p5.js Patterns
|
|
6
|
+
3. SVG Generative Art
|
|
7
|
+
4. Canvas 2D API
|
|
8
|
+
5. Noise Functions
|
|
9
|
+
6. Color in Generative Art
|
|
10
|
+
7. Seeded Randomness
|
|
11
|
+
8. Animation vs Static
|
|
12
|
+
9. Performance
|
|
13
|
+
10. Common Mistakes
|
|
4
14
|
|
|
5
|
-
|
|
6
|
-
Before writing code, write a manifesto (4-6 paragraphs) that describes the computational aesthetic. Name the movement (1-2 words). Describe how the philosophy manifests through computational processes, noise functions, particle behaviors, temporal evolution, and parametric variation.
|
|
15
|
+
---
|
|
7
16
|
|
|
8
|
-
|
|
9
|
-
Express the philosophy through p5.js with seeded randomness. The algorithm should be 90% generative, 10% parameters.
|
|
17
|
+
## 1. Philosophy
|
|
10
18
|
|
|
11
|
-
|
|
19
|
+
Generative art is intentional design expressed through algorithms. Randomness is a tool, not the goal. The artist defines the system, its constraints, and its parameter space. The algorithm explores that space. Every output should feel curated, as if the artist chose it from a thousand variations.
|
|
20
|
+
|
|
21
|
+
The quality bar: a viewer should not think "a computer made this." They should think "someone designed this." The difference is in constraint. Unconstrained randomness produces noise. Constrained randomness produces beauty.
|
|
22
|
+
|
|
23
|
+
Three principles guide generative design:
|
|
24
|
+
- **Parameterize everything.** Every magic number becomes a parameter. This lets you explore the design space systematically.
|
|
25
|
+
- **Seed everything.** Reproducibility is non-negotiable. A good output must be recoverable.
|
|
26
|
+
- **Curate ruthlessly.** Generate hundreds of variations. Ship the best five. The algorithm is a collaborator, not the artist.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 2. p5.js Patterns
|
|
31
|
+
|
|
32
|
+
### Canvas Setup
|
|
33
|
+
Always size the canvas to the container or a specific aspect ratio. Never hardcode pixel dimensions without a reason.
|
|
12
34
|
|
|
13
|
-
**Seeded Randomness:**
|
|
14
35
|
```javascript
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
36
|
+
function setup() {
|
|
37
|
+
const canvas = createCanvas(windowWidth, windowHeight);
|
|
38
|
+
canvas.parent('canvas-container');
|
|
39
|
+
pixelDensity(2); // retina support
|
|
40
|
+
randomSeed(params.seed);
|
|
41
|
+
noiseSeed(params.seed);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function windowResized() {
|
|
45
|
+
resizeCanvas(windowWidth, windowHeight);
|
|
46
|
+
}
|
|
18
47
|
```
|
|
19
|
-
Same seed always produces the same output. Different seeds reveal different facets.
|
|
20
48
|
|
|
21
|
-
|
|
49
|
+
### Flow Field
|
|
50
|
+
|
|
51
|
+
A complete flow field with particle trails. Particles follow angles derived from Perlin noise, accumulating into organic density maps.
|
|
52
|
+
|
|
22
53
|
```javascript
|
|
23
|
-
|
|
24
|
-
seed:
|
|
25
|
-
particleCount:
|
|
26
|
-
noiseScale: 0.
|
|
27
|
-
speed:
|
|
28
|
-
|
|
54
|
+
const params = {
|
|
55
|
+
seed: 42,
|
|
56
|
+
particleCount: 2000,
|
|
57
|
+
noiseScale: 0.003,
|
|
58
|
+
speed: 2,
|
|
59
|
+
trailAlpha: 10,
|
|
60
|
+
fieldRotation: 0
|
|
29
61
|
};
|
|
62
|
+
|
|
63
|
+
let particles = [];
|
|
64
|
+
|
|
65
|
+
function setup() {
|
|
66
|
+
createCanvas(800, 800);
|
|
67
|
+
randomSeed(params.seed);
|
|
68
|
+
noiseSeed(params.seed);
|
|
69
|
+
background(15);
|
|
70
|
+
|
|
71
|
+
for (let i = 0; i < params.particleCount; i++) {
|
|
72
|
+
particles.push({
|
|
73
|
+
x: random(width),
|
|
74
|
+
y: random(height),
|
|
75
|
+
prevX: 0,
|
|
76
|
+
prevY: 0
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function draw() {
|
|
82
|
+
for (const p of particles) {
|
|
83
|
+
const angle = noise(p.x * params.noiseScale, p.y * params.noiseScale) *
|
|
84
|
+
TAU * 2 + params.fieldRotation;
|
|
85
|
+
p.prevX = p.x;
|
|
86
|
+
p.prevY = p.y;
|
|
87
|
+
p.x += cos(angle) * params.speed;
|
|
88
|
+
p.y += sin(angle) * params.speed;
|
|
89
|
+
|
|
90
|
+
// Wrap around edges
|
|
91
|
+
if (p.x < 0) p.x = width;
|
|
92
|
+
if (p.x > width) p.x = 0;
|
|
93
|
+
if (p.y < 0) p.y = height;
|
|
94
|
+
if (p.y > height) p.y = 0;
|
|
95
|
+
|
|
96
|
+
stroke(255, params.trailAlpha);
|
|
97
|
+
strokeWeight(0.5);
|
|
98
|
+
line(p.prevX, p.prevY, p.x, p.y);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Particle System
|
|
104
|
+
|
|
105
|
+
Self-contained particle class with lifecycle management.
|
|
106
|
+
|
|
107
|
+
```javascript
|
|
108
|
+
class Particle {
|
|
109
|
+
constructor(x, y, hue) {
|
|
110
|
+
this.pos = createVector(x, y);
|
|
111
|
+
this.vel = p5.Vector.random2D().mult(random(0.5, 2));
|
|
112
|
+
this.acc = createVector(0, 0);
|
|
113
|
+
this.lifespan = 255;
|
|
114
|
+
this.hue = hue;
|
|
115
|
+
this.size = random(2, 6);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
applyForce(force) {
|
|
119
|
+
this.acc.add(force);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
update() {
|
|
123
|
+
this.vel.add(this.acc);
|
|
124
|
+
this.vel.limit(4);
|
|
125
|
+
this.pos.add(this.vel);
|
|
126
|
+
this.acc.mult(0);
|
|
127
|
+
this.lifespan -= 2;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
draw() {
|
|
131
|
+
noStroke();
|
|
132
|
+
fill(`oklch(0.75 0.15 ${this.hue} / ${this.lifespan / 255})`);
|
|
133
|
+
circle(this.pos.x, this.pos.y, this.size);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
isDead() {
|
|
137
|
+
return this.lifespan <= 0;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
30
140
|
```
|
|
31
141
|
|
|
32
|
-
|
|
33
|
-
|
|
142
|
+
### Single-File HTML Scaffold
|
|
143
|
+
|
|
144
|
+
All generative art ships as a single HTML file with p5.js from CDN.
|
|
145
|
+
|
|
34
146
|
```html
|
|
35
|
-
|
|
147
|
+
<!DOCTYPE html>
|
|
148
|
+
<html lang="en">
|
|
149
|
+
<head>
|
|
150
|
+
<meta charset="UTF-8">
|
|
151
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
152
|
+
<title>Generative Piece</title>
|
|
153
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js"></script>
|
|
154
|
+
<style>
|
|
155
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
156
|
+
body { background: #0f0f0f; overflow: hidden; }
|
|
157
|
+
#controls {
|
|
158
|
+
position: fixed; bottom: 16px; left: 16px;
|
|
159
|
+
display: flex; gap: 8px; z-index: 10;
|
|
160
|
+
}
|
|
161
|
+
#controls button {
|
|
162
|
+
padding: 6px 14px; border: 1px solid rgba(255,255,255,0.2);
|
|
163
|
+
background: rgba(0,0,0,0.6); color: #fff; border-radius: 6px;
|
|
164
|
+
cursor: pointer; font-size: 13px;
|
|
165
|
+
}
|
|
166
|
+
#seed-display {
|
|
167
|
+
position: fixed; top: 16px; left: 16px;
|
|
168
|
+
color: rgba(255,255,255,0.4); font: 13px/1 monospace;
|
|
169
|
+
}
|
|
170
|
+
</style>
|
|
171
|
+
</head>
|
|
172
|
+
<body>
|
|
173
|
+
<div id="seed-display"></div>
|
|
174
|
+
<div id="controls">
|
|
175
|
+
<button onclick="prevSeed()">Prev</button>
|
|
176
|
+
<button onclick="nextSeed()">Next</button>
|
|
177
|
+
<button onclick="randomizeSeed()">Random</button>
|
|
178
|
+
<button onclick="saveCanvas('output', 'png')">Export PNG</button>
|
|
179
|
+
</div>
|
|
180
|
+
<script>
|
|
181
|
+
/* params, setup, draw, seed controls here */
|
|
182
|
+
</script>
|
|
183
|
+
</body>
|
|
184
|
+
</html>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 3. SVG Generative Art
|
|
190
|
+
|
|
191
|
+
SVG output is resolution-independent and ideal for print, plotters, and crisp digital display. Build SVG strings programmatically or use DOM manipulation.
|
|
192
|
+
|
|
193
|
+
### Programmatic SVG Construction
|
|
194
|
+
|
|
195
|
+
```javascript
|
|
196
|
+
function generateSVG(width, height, seed) {
|
|
197
|
+
const rng = createRNG(seed);
|
|
198
|
+
const paths = [];
|
|
199
|
+
|
|
200
|
+
for (let i = 0; i < 50; i++) {
|
|
201
|
+
const cx = rng() * width;
|
|
202
|
+
const cy = rng() * height;
|
|
203
|
+
const points = [];
|
|
204
|
+
|
|
205
|
+
for (let a = 0; a < Math.PI * 2; a += 0.1) {
|
|
206
|
+
const r = 40 + rng() * 30;
|
|
207
|
+
const x = cx + Math.cos(a) * r;
|
|
208
|
+
const y = cy + Math.sin(a) * r;
|
|
209
|
+
points.push(`${x.toFixed(2)},${y.toFixed(2)}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const hue = (i * 7.2) % 360;
|
|
213
|
+
paths.push(
|
|
214
|
+
`<polygon points="${points.join(' ')}" ` +
|
|
215
|
+
`fill="oklch(0.7 0.12 ${hue})" fill-opacity="0.3" ` +
|
|
216
|
+
`stroke="oklch(0.5 0.15 ${hue})" stroke-width="0.5" />`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}">
|
|
221
|
+
<rect width="${width}" height="${height}" fill="oklch(0.12 0.01 260)" />
|
|
222
|
+
${paths.join('\n ')}
|
|
223
|
+
</svg>`;
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Path Generation with Cubic Beziers
|
|
228
|
+
|
|
229
|
+
For smooth organic curves, use cubic bezier path commands. Generate control points with noise-influenced offsets.
|
|
230
|
+
|
|
231
|
+
```javascript
|
|
232
|
+
function noisyPath(startX, startY, steps, rng, noiseScale = 0.02) {
|
|
233
|
+
let d = `M ${startX} ${startY}`;
|
|
234
|
+
let x = startX, y = startY;
|
|
235
|
+
|
|
236
|
+
for (let i = 0; i < steps; i++) {
|
|
237
|
+
const angle = simplexNoise2D(x * noiseScale, y * noiseScale) * Math.PI * 2;
|
|
238
|
+
const length = 20 + rng() * 40;
|
|
239
|
+
const cp1x = x + Math.cos(angle) * length * 0.5;
|
|
240
|
+
const cp1y = y + Math.sin(angle) * length * 0.5;
|
|
241
|
+
x += Math.cos(angle) * length;
|
|
242
|
+
y += Math.sin(angle) * length;
|
|
243
|
+
const cp2x = x - Math.cos(angle) * length * 0.3;
|
|
244
|
+
const cp2y = y - Math.sin(angle) * length * 0.3;
|
|
245
|
+
d += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${x} ${y}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return d;
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Exporting SVG
|
|
253
|
+
|
|
254
|
+
```javascript
|
|
255
|
+
function downloadSVG(svgString, filename = 'generative.svg') {
|
|
256
|
+
const blob = new Blob([svgString], { type: 'image/svg+xml' });
|
|
257
|
+
const url = URL.createObjectURL(blob);
|
|
258
|
+
const a = document.createElement('a');
|
|
259
|
+
a.href = url;
|
|
260
|
+
a.download = filename;
|
|
261
|
+
a.click();
|
|
262
|
+
URL.revokeObjectURL(url);
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## 4. Canvas 2D API
|
|
269
|
+
|
|
270
|
+
When you need raw performance without a library, use the native Canvas 2D context.
|
|
271
|
+
|
|
272
|
+
```javascript
|
|
273
|
+
function initCanvas(width, height) {
|
|
274
|
+
const canvas = document.createElement('canvas');
|
|
275
|
+
canvas.width = width * devicePixelRatio;
|
|
276
|
+
canvas.height = height * devicePixelRatio;
|
|
277
|
+
canvas.style.width = `${width}px`;
|
|
278
|
+
canvas.style.height = `${height}px`;
|
|
279
|
+
const ctx = canvas.getContext('2d');
|
|
280
|
+
ctx.scale(devicePixelRatio, devicePixelRatio);
|
|
281
|
+
return { canvas, ctx };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function drawFlowField(ctx, w, h, seed) {
|
|
285
|
+
const rng = createRNG(seed);
|
|
286
|
+
ctx.fillStyle = 'oklch(0.08 0.01 260)';
|
|
287
|
+
ctx.fillRect(0, 0, w, h);
|
|
288
|
+
|
|
289
|
+
ctx.strokeStyle = 'oklch(0.85 0.04 200 / 0.04)';
|
|
290
|
+
ctx.lineWidth = 0.5;
|
|
291
|
+
|
|
292
|
+
for (let i = 0; i < 3000; i++) {
|
|
293
|
+
let x = rng() * w;
|
|
294
|
+
let y = rng() * h;
|
|
295
|
+
ctx.beginPath();
|
|
296
|
+
ctx.moveTo(x, y);
|
|
297
|
+
|
|
298
|
+
for (let step = 0; step < 80; step++) {
|
|
299
|
+
const angle = simplexNoise2D(x * 0.003, y * 0.003) * Math.PI * 4;
|
|
300
|
+
x += Math.cos(angle) * 1.5;
|
|
301
|
+
y += Math.sin(angle) * 1.5;
|
|
302
|
+
ctx.lineTo(x, y);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
ctx.stroke();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Pixel-Level Manipulation
|
|
311
|
+
|
|
312
|
+
For effects like reaction-diffusion or cellular automata, work directly with pixel data.
|
|
313
|
+
|
|
314
|
+
```javascript
|
|
315
|
+
function pixelEffect(ctx, w, h) {
|
|
316
|
+
const imageData = ctx.getImageData(0, 0, w, h);
|
|
317
|
+
const data = imageData.data;
|
|
318
|
+
|
|
319
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
320
|
+
const px = (i / 4) % w;
|
|
321
|
+
const py = Math.floor(i / 4 / w);
|
|
322
|
+
const n = simplexNoise2D(px * 0.01, py * 0.01);
|
|
323
|
+
data[i] = Math.floor(n * 128 + 128); // R
|
|
324
|
+
data[i + 1] = Math.floor(n * 64 + 128); // G
|
|
325
|
+
data[i + 2] = Math.floor(n * 180 + 75); // B
|
|
326
|
+
data[i + 3] = 255; // A
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
ctx.putImageData(imageData, 0, 0);
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## 5. Noise Functions
|
|
336
|
+
|
|
337
|
+
### Perlin vs Simplex
|
|
338
|
+
|
|
339
|
+
| Property | Perlin | Simplex |
|
|
340
|
+
|---|---|---|
|
|
341
|
+
| Dimensions | Works well in 2D-3D | Scales cleanly to 4D+ |
|
|
342
|
+
| Artifacts | Grid-aligned directional artifacts | No directional bias |
|
|
343
|
+
| Performance | Moderate | Faster in higher dimensions |
|
|
344
|
+
| Use case | p5.js `noise()` default | Preferred for custom implementations |
|
|
345
|
+
|
|
346
|
+
### Practical Usage
|
|
347
|
+
|
|
348
|
+
```javascript
|
|
349
|
+
// Single-octave noise: smooth, blobby
|
|
350
|
+
const value = noise(x * scale, y * scale);
|
|
351
|
+
|
|
352
|
+
// Multi-octave (fractal) noise: organic detail
|
|
353
|
+
function fractalNoise(x, y, octaves = 4, lacunarity = 2, persistence = 0.5) {
|
|
354
|
+
let total = 0;
|
|
355
|
+
let frequency = 1;
|
|
356
|
+
let amplitude = 1;
|
|
357
|
+
let maxValue = 0;
|
|
358
|
+
|
|
359
|
+
for (let i = 0; i < octaves; i++) {
|
|
360
|
+
total += simplexNoise2D(x * frequency, y * frequency) * amplitude;
|
|
361
|
+
maxValue += amplitude;
|
|
362
|
+
amplitude *= persistence;
|
|
363
|
+
frequency *= lacunarity;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return total / maxValue; // normalized to -1..1
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Noise Scale Guide
|
|
371
|
+
|
|
372
|
+
| Scale | Effect | Use Case |
|
|
373
|
+
|---|---|---|
|
|
374
|
+
| 0.001-0.005 | Very smooth, continent-like | Large flow fields, terrain |
|
|
375
|
+
| 0.005-0.02 | Gentle undulation | Particle paths, soft gradients |
|
|
376
|
+
| 0.02-0.1 | Visible texture | Surface detail, displacement |
|
|
377
|
+
| 0.1-0.5 | High frequency, gritty | Texture overlay, grain |
|
|
378
|
+
|
|
379
|
+
### Layering Noise
|
|
380
|
+
|
|
381
|
+
Combine noise at different scales for complex organic results.
|
|
382
|
+
|
|
383
|
+
```javascript
|
|
384
|
+
function layeredNoise(x, y) {
|
|
385
|
+
const base = fractalNoise(x, y, 4, 2, 0.5); // large form
|
|
386
|
+
const detail = simplexNoise2D(x * 0.08, y * 0.08); // medium texture
|
|
387
|
+
const grain = simplexNoise2D(x * 0.5, y * 0.5); // fine grain
|
|
388
|
+
return base * 0.7 + detail * 0.2 + grain * 0.1;
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## 6. Color in Generative Art
|
|
395
|
+
|
|
396
|
+
Use OKLCH for all generative color work. Its perceptual uniformity means programmatic palette generation produces visually coherent results, unlike HSL where "equal" lightness values look uneven.
|
|
397
|
+
|
|
398
|
+
### Palette Generation Algorithms
|
|
399
|
+
|
|
400
|
+
```javascript
|
|
401
|
+
// Analogous palette: hues clustered within a range
|
|
402
|
+
function analogousPalette(baseHue, count = 5, spread = 30) {
|
|
403
|
+
return Array.from({ length: count }, (_, i) => {
|
|
404
|
+
const hue = (baseHue - spread / 2 + (spread / (count - 1)) * i) % 360;
|
|
405
|
+
return `oklch(0.7 0.15 ${hue})`;
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Complementary with variation
|
|
410
|
+
function complementaryPalette(baseHue) {
|
|
411
|
+
return [
|
|
412
|
+
`oklch(0.65 0.2 ${baseHue})`,
|
|
413
|
+
`oklch(0.75 0.1 ${baseHue})`,
|
|
414
|
+
`oklch(0.65 0.2 ${(baseHue + 180) % 360})`,
|
|
415
|
+
`oklch(0.80 0.08 ${(baseHue + 180) % 360})`,
|
|
416
|
+
];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Triadic palette
|
|
420
|
+
function triadicPalette(baseHue, lightness = 0.65, chroma = 0.18) {
|
|
421
|
+
return [0, 120, 240].map(offset =>
|
|
422
|
+
`oklch(${lightness} ${chroma} ${(baseHue + offset) % 360})`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Color from Noise
|
|
428
|
+
|
|
429
|
+
Map noise values to hue ranges for smooth, organic color transitions.
|
|
430
|
+
|
|
431
|
+
```javascript
|
|
432
|
+
function noiseColor(x, y, noiseScale = 0.005) {
|
|
433
|
+
const n = noise(x * noiseScale, y * noiseScale); // 0..1
|
|
434
|
+
const hue = lerp(200, 320, n); // blue to magenta
|
|
435
|
+
const lightness = lerp(0.5, 0.8, n);
|
|
436
|
+
const chroma = lerp(0.08, 0.2, n);
|
|
437
|
+
return `oklch(${lightness} ${chroma} ${hue})`;
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Background and Contrast
|
|
442
|
+
|
|
443
|
+
Dark backgrounds with luminous strokes produce the best generative art contrast. Use near-black with a slight hue tint, never pure `#000000`.
|
|
444
|
+
|
|
445
|
+
```javascript
|
|
446
|
+
const bg = 'oklch(0.08 0.015 260)'; // dark blue-tinted black
|
|
447
|
+
const stroke = 'oklch(0.85 0.12 200 / 0.06)'; // faint luminous trail
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## 7. Seeded Randomness
|
|
453
|
+
|
|
454
|
+
Every generative piece must be reproducible. Same seed, same output. Always.
|
|
455
|
+
|
|
456
|
+
### Minimal Seeded RNG
|
|
457
|
+
|
|
458
|
+
A fast, high-quality 32-bit PRNG (Mulberry32) that fits in any project.
|
|
459
|
+
|
|
460
|
+
```javascript
|
|
461
|
+
function createRNG(seed) {
|
|
462
|
+
let s = seed | 0;
|
|
463
|
+
return function () {
|
|
464
|
+
s = (s + 0x6d2b79f5) | 0;
|
|
465
|
+
let t = Math.imul(s ^ (s >>> 15), 1 | s);
|
|
466
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
467
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const rng = createRNG(42);
|
|
472
|
+
rng(); // 0.0..1.0, deterministic
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### Seed Management in p5.js
|
|
476
|
+
|
|
477
|
+
```javascript
|
|
478
|
+
let currentSeed = 42;
|
|
479
|
+
|
|
480
|
+
function regenerate(newSeed) {
|
|
481
|
+
currentSeed = newSeed;
|
|
482
|
+
randomSeed(currentSeed);
|
|
483
|
+
noiseSeed(currentSeed);
|
|
484
|
+
clear();
|
|
485
|
+
background(15);
|
|
486
|
+
draw(); // or loop() for animated pieces
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function nextSeed() { regenerate(currentSeed + 1); }
|
|
490
|
+
function prevSeed() { regenerate(currentSeed - 1); }
|
|
491
|
+
function randomizeSeed() { regenerate(Math.floor(Math.random() * 999999)); }
|
|
36
492
|
```
|
|
37
493
|
|
|
38
|
-
###
|
|
39
|
-
1. **Seed navigation**: prev/next/random buttons, seed display, jump-to-seed input
|
|
40
|
-
2. **Parameter controls**: sliders for numeric values, color pickers for palette
|
|
41
|
-
3. **Actions**: regenerate, reset defaults, download PNG
|
|
494
|
+
### Seed from URL
|
|
42
495
|
|
|
43
|
-
|
|
496
|
+
Let users share specific outputs via URL.
|
|
44
497
|
|
|
45
|
-
|
|
498
|
+
```javascript
|
|
499
|
+
function seedFromURL() {
|
|
500
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
501
|
+
return urlParams.has('seed') ? parseInt(urlParams.get('seed'), 10) : 42;
|
|
502
|
+
}
|
|
46
503
|
|
|
47
|
-
|
|
504
|
+
function updateURL(seed) {
|
|
505
|
+
const url = new URL(window.location);
|
|
506
|
+
url.searchParams.set('seed', seed);
|
|
507
|
+
window.history.replaceState({}, '', url);
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
## 8. Animation vs Static
|
|
514
|
+
|
|
515
|
+
### When to Animate
|
|
516
|
+
- The piece explores temporal evolution (particles finding equilibrium, growth systems)
|
|
517
|
+
- Real-time interactivity adds meaning (mouse-reactive fields, audio-reactive visuals)
|
|
518
|
+
- The animation reveals the process (watching the flow field build creates wonder)
|
|
519
|
+
|
|
520
|
+
### When to Stay Static
|
|
521
|
+
- The final composition is the point (print-quality output)
|
|
522
|
+
- The algorithm is computationally expensive (reaction-diffusion, deep recursion)
|
|
523
|
+
- The piece will be exported as PNG/SVG
|
|
524
|
+
|
|
525
|
+
### Animation Loop Pattern
|
|
526
|
+
|
|
527
|
+
```javascript
|
|
528
|
+
let isAnimating = true;
|
|
529
|
+
|
|
530
|
+
function draw() {
|
|
531
|
+
if (!isAnimating) return;
|
|
532
|
+
// drawing logic here
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// For non-p5 contexts
|
|
536
|
+
function animate(ctx, state) {
|
|
537
|
+
function frame(timestamp) {
|
|
538
|
+
if (!state.running) return;
|
|
539
|
+
update(state, timestamp);
|
|
540
|
+
render(ctx, state);
|
|
541
|
+
state.frameId = requestAnimationFrame(frame);
|
|
542
|
+
}
|
|
543
|
+
state.frameId = requestAnimationFrame(frame);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function stop(state) {
|
|
547
|
+
state.running = false;
|
|
548
|
+
cancelAnimationFrame(state.frameId);
|
|
549
|
+
}
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### Export Workflow for Animated Pieces
|
|
553
|
+
|
|
554
|
+
Render a fixed number of frames, then stop and allow PNG export.
|
|
555
|
+
|
|
556
|
+
```javascript
|
|
557
|
+
const TOTAL_FRAMES = 300;
|
|
558
|
+
let frameCount = 0;
|
|
559
|
+
|
|
560
|
+
function draw() {
|
|
561
|
+
if (frameCount >= TOTAL_FRAMES) {
|
|
562
|
+
noLoop();
|
|
563
|
+
document.getElementById('export-btn').disabled = false;
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
// render frame
|
|
567
|
+
frameCount++;
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
---
|
|
572
|
+
|
|
573
|
+
## 9. Performance
|
|
574
|
+
|
|
575
|
+
### Offscreen Canvas
|
|
576
|
+
|
|
577
|
+
Pre-render static or expensive layers to an offscreen canvas, then composite.
|
|
578
|
+
|
|
579
|
+
```javascript
|
|
580
|
+
function createOffscreenLayer(width, height, drawFn) {
|
|
581
|
+
const offscreen = document.createElement('canvas');
|
|
582
|
+
offscreen.width = width;
|
|
583
|
+
offscreen.height = height;
|
|
584
|
+
const offCtx = offscreen.getContext('2d');
|
|
585
|
+
drawFn(offCtx, width, height);
|
|
586
|
+
return offscreen;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Usage: render background noise once
|
|
590
|
+
const bgLayer = createOffscreenLayer(800, 800, (ctx, w, h) => {
|
|
591
|
+
// expensive noise texture
|
|
592
|
+
pixelEffect(ctx, w, h);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// In the main draw loop, just composite
|
|
596
|
+
mainCtx.drawImage(bgLayer, 0, 0);
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### Batching Draw Calls
|
|
600
|
+
|
|
601
|
+
Group similar operations. One `beginPath()` with many `lineTo()` calls is much faster than many individual `beginPath()/stroke()` pairs.
|
|
602
|
+
|
|
603
|
+
```javascript
|
|
604
|
+
// Fast: single path for all particles of the same color
|
|
605
|
+
ctx.beginPath();
|
|
606
|
+
ctx.strokeStyle = 'oklch(0.8 0.1 220 / 0.05)';
|
|
607
|
+
for (const p of particles) {
|
|
608
|
+
ctx.moveTo(p.prevX, p.prevY);
|
|
609
|
+
ctx.lineTo(p.x, p.y);
|
|
610
|
+
}
|
|
611
|
+
ctx.stroke(); // one draw call
|
|
612
|
+
|
|
613
|
+
// Slow: individual stroke per particle (avoid this)
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
### requestAnimationFrame Discipline
|
|
617
|
+
|
|
618
|
+
Never use `setInterval` or `setTimeout` for animation. Always use `requestAnimationFrame`. It syncs to the display refresh, pauses in background tabs, and produces smooth frame pacing.
|
|
619
|
+
|
|
620
|
+
### Pixel Density
|
|
621
|
+
|
|
622
|
+
On retina screens, match the device pixel ratio but keep the logical coordinate space the same.
|
|
623
|
+
|
|
624
|
+
```javascript
|
|
625
|
+
const dpr = window.devicePixelRatio || 1;
|
|
626
|
+
canvas.width = logicalWidth * dpr;
|
|
627
|
+
canvas.height = logicalHeight * dpr;
|
|
628
|
+
canvas.style.width = `${logicalWidth}px`;
|
|
629
|
+
canvas.style.height = `${logicalHeight}px`;
|
|
630
|
+
ctx.scale(dpr, dpr);
|
|
631
|
+
```
|
|
48
632
|
|
|
49
|
-
|
|
633
|
+
---
|
|
50
634
|
|
|
51
|
-
|
|
635
|
+
## 10. Common Mistakes
|
|
52
636
|
|
|
53
|
-
|
|
54
|
-
|
|
637
|
+
- Using `Math.random()` without seeding (outputs are unreproducible and cannot be shared or revisited)
|
|
638
|
+
- Hardcoding canvas dimensions instead of deriving from container or aspect ratio (breaks on different screens)
|
|
639
|
+
- Forgetting to wrap particle coordinates at edges (particles fly offscreen and waste computation)
|
|
640
|
+
- Calling `background()` every frame in pieces meant to accumulate trails (erases the trail effect)
|
|
641
|
+
- Using HSL for programmatic color generation (perceptually uneven; a green and blue at the same HSL lightness look completely different in brightness)
|
|
642
|
+
- Animating when the piece should be static (wasting battery and CPU for a composition that is complete after frame 1)
|
|
643
|
+
- Not providing seed navigation UI (users cannot explore the design space or recover a good output)
|
|
644
|
+
- Rendering at 1x on retina displays (produces blurry output; always account for `devicePixelRatio`)
|
|
645
|
+
- Creating one `Path2D` or `beginPath/stroke` per particle instead of batching (kills frame rate above a few hundred elements)
|
|
646
|
+
- Using `noise()` with a scale of 1.0 (produces white noise; useful noise scales are typically 0.001 to 0.1)
|
|
647
|
+
- Generating palettes with random RGB values (produces muddy, clashing colors; derive palettes algorithmically in OKLCH)
|
|
648
|
+
- Shipping without an export button (the whole point is producing artifacts worth keeping)
|
|
@@ -49,8 +49,8 @@ Invest time in this order. A well-choreographed page load does more than fifty m
|
|
|
49
49
|
### Never Use
|
|
50
50
|
- `linear` for UI animations (looks mechanical)
|
|
51
51
|
- `ease` (the CSS default is mediocre)
|
|
52
|
-
- `bounce` / elastic easing (looks dated and gimmicky)
|
|
53
|
-
- Spring animations with
|
|
52
|
+
- `bounce` / elastic easing with visible oscillation (looks dated and gimmicky). Subtle single-pass overshoot (like `--ease-spring` above) is acceptable.
|
|
53
|
+
- Spring animations with multiple bounces (too playful for most UIs)
|
|
54
54
|
|
|
55
55
|
---
|
|
56
56
|
|