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.
@@ -1,54 +1,648 @@
1
1
  # Generative Art Reference
2
2
 
3
- ## Process
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
- ### Step 1: Algorithmic Philosophy
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
- ### Step 2: Implementation
9
- Express the philosophy through p5.js with seeded randomness. The algorithm should be 90% generative, 10% parameters.
17
+ ## 1. Philosophy
10
18
 
11
- ### Core Technical Requirements
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
- let seed = 12345;
16
- randomSeed(seed);
17
- noiseSeed(seed);
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
- **Parameter Structure:**
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
- let params = {
24
- seed: 12345,
25
- particleCount: 1000,
26
- noiseScale: 0.005,
27
- speed: 1.5,
28
- // Add parameters specific to the algorithm
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
- **Single-File HTML:**
33
- Everything inline, no external files except p5.js CDN:
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
- <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.min.js"></script>
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
- ### Required UI Features
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
- ### Philosophy Examples
496
+ Let users share specific outputs via URL.
44
497
 
45
- **Flow Fields**: Layered Perlin noise driving particle trails. Thousands of particles follow vector forces, accumulating into organic density maps.
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
- **Quantum Harmonics**: Particles on a grid with phase values evolving through sine waves. Constructive/destructive interference creates emergent patterns.
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
- **Stochastic Crystallization**: Randomized circle packing or Voronoi tessellation. Random points evolve through relaxation algorithms until equilibrium.
633
+ ---
50
634
 
51
- **Recursive Whispers**: Branching structures that subdivide recursively, constrained by golden ratios with noise perturbations.
635
+ ## 10. Common Mistakes
52
636
 
53
- ### Quality Bar
54
- The output should look like it was refined by a computational artist over hundreds of iterations. Every parameter should feel carefully tuned. The result should reward sustained viewing.
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 visible oscillation (too playful for most UIs)
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