picasso-skill 2.5.0 → 2.6.1

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,338 +1,47 @@
1
1
  # Generative Art Reference
2
2
 
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
14
-
15
- ---
16
-
17
3
  ## 1. Philosophy
18
4
 
19
5
  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
6
 
21
7
  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
8
 
23
- Three principles guide generative design:
9
+ Three principles:
24
10
  - **Parameterize everything.** Every magic number becomes a parameter. This lets you explore the design space systematically.
25
11
  - **Seed everything.** Reproducibility is non-negotiable. A good output must be recoverable.
26
12
  - **Curate ruthlessly.** Generate hundreds of variations. Ship the best five. The algorithm is a collaborator, not the artist.
27
13
 
28
14
  ---
29
15
 
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.
34
-
35
- ```javascript
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
- }
47
- ```
16
+ ## 2. Core Patterns
48
17
 
49
18
  ### Flow Field
50
-
51
- A complete flow field with particle trails. Particles follow angles derived from Perlin noise, accumulating into organic density maps.
52
-
19
+ Particles follow angles derived from Perlin noise, accumulating into organic density maps.
53
20
  ```javascript
54
- const params = {
55
- seed: 42,
56
- particleCount: 2000,
57
- noiseScale: 0.003,
58
- speed: 2,
59
- trailAlpha: 10,
60
- fieldRotation: 0
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
- }
21
+ const angle = noise(p.x * noiseScale, p.y * noiseScale) * TAU * 2;
22
+ p.x += cos(angle) * speed;
23
+ p.y += sin(angle) * speed;
101
24
  ```
102
25
 
103
26
  ### 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
- }
140
- ```
27
+ Class-based particles with position, velocity, acceleration, lifespan, and lifecycle management. Use `p5.Vector` or plain `{x, y}` objects. Always wrap coordinates at edges.
141
28
 
142
29
  ### Single-File HTML Scaffold
143
-
144
- All generative art ships as a single HTML file with p5.js from CDN.
145
-
146
- ```html
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
- ```
30
+ All generative art ships as a single HTML file with p5.js from CDN. Include: seed display, prev/next/random buttons, export PNG button.
186
31
 
187
32
  ---
188
33
 
189
34
  ## 3. SVG Generative Art
190
35
 
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
36
+ SVG output is resolution-independent and ideal for print, plotters, and crisp digital display.
311
37
 
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
- ```
38
+ - Build SVG strings programmatically; use `<polygon>`, `<path>`, `<circle>` elements
39
+ - For smooth organic curves, use cubic bezier path commands (`C`) with noise-influenced control points
40
+ - Export via `Blob` + `URL.createObjectURL` + click-triggered download
332
41
 
333
42
  ---
334
43
 
335
- ## 5. Noise Functions
44
+ ## 4. Noise Functions
336
45
 
337
46
  ### Perlin vs Simplex
338
47
 
@@ -343,30 +52,6 @@ function pixelEffect(ctx, w, h) {
343
52
  | Performance | Moderate | Faster in higher dimensions |
344
53
  | Use case | p5.js `noise()` default | Preferred for custom implementations |
345
54
 
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
55
  ### Noise Scale Guide
371
56
 
372
57
  | Scale | Effect | Use Case |
@@ -376,141 +61,40 @@ function fractalNoise(x, y, octaves = 4, lacunarity = 2, persistence = 0.5) {
376
61
  | 0.02-0.1 | Visible texture | Surface detail, displacement |
377
62
  | 0.1-0.5 | High frequency, gritty | Texture overlay, grain |
378
63
 
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
- ```
64
+ ### Layering
65
+ Combine noise at different scales: base form (4 octaves) + medium texture + fine grain, weighted (e.g., 0.7 / 0.2 / 0.1). Use fractal noise (multi-octave with lacunarity and persistence) for organic detail.
391
66
 
392
67
  ---
393
68
 
394
- ## 6. Color in Generative Art
69
+ ## 5. Color in Generative Art
395
70
 
396
71
  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
72
 
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
- ```
73
+ ### Palette Strategies
74
+ - **Analogous**: hues clustered within a 30-degree spread
75
+ - **Complementary**: base hue + base+180, with lightness/chroma variations
76
+ - **Triadic**: base + base+120 + base+240
426
77
 
427
78
  ### 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
- ```
79
+ Map noise values to hue ranges for smooth organic transitions: `lerp(hueMin, hueMax, noiseValue)`.
440
80
 
441
81
  ### 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
- ```
82
+ Dark backgrounds with luminous strokes produce the best generative art contrast. Use near-black with a slight hue tint (`oklch(0.08 0.015 260)`), never pure `#000000`.
449
83
 
450
84
  ---
451
85
 
452
- ## 7. Seeded Randomness
86
+ ## 6. Seeded Randomness
453
87
 
454
88
  Every generative piece must be reproducible. Same seed, same output. Always.
455
89
 
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)); }
492
- ```
493
-
494
- ### Seed from URL
495
-
496
- Let users share specific outputs via URL.
497
-
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
- }
503
-
504
- function updateURL(seed) {
505
- const url = new URL(window.location);
506
- url.searchParams.set('seed', seed);
507
- window.history.replaceState({}, '', url);
508
- }
509
- ```
90
+ - Use Mulberry32 or similar fast 32-bit PRNG for custom seeds
91
+ - In p5.js, call `randomSeed(seed)` and `noiseSeed(seed)` in `setup()`
92
+ - Provide seed navigation UI: prev, next, random buttons
93
+ - Store seed in URL params for sharing specific outputs
510
94
 
511
95
  ---
512
96
 
513
- ## 8. Animation vs Static
97
+ ## 7. Animation vs Static
514
98
 
515
99
  ### When to Animate
516
100
  - The piece explores temporal evolution (particles finding equilibrium, growth systems)
@@ -522,127 +106,31 @@ function updateURL(seed) {
522
106
  - The algorithm is computationally expensive (reaction-diffusion, deep recursion)
523
107
  - The piece will be exported as PNG/SVG
524
108
 
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
- ```
109
+ For animated pieces, render a fixed frame count then stop and enable export.
570
110
 
571
111
  ---
572
112
 
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
- ```
113
+ ## 8. Performance
598
114
 
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
- ```
115
+ ### Rules
116
+ - Pre-render static/expensive layers to an offscreen canvas, then composite with `drawImage`
117
+ - Batch draw calls: one `beginPath()` with many `lineTo()` calls, one `stroke()` (not per-particle)
118
+ - Always use `requestAnimationFrame`, never `setInterval`/`setTimeout`
119
+ - On retina screens, set canvas dimensions to `logicalSize * devicePixelRatio` and `ctx.scale(dpr, dpr)`
632
120
 
633
121
  ---
634
122
 
635
- ## 10. Common Mistakes
636
-
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)
123
+ ## 9. Common Mistakes
124
+
125
+ - Using `Math.random()` without seeding (unreproducible outputs)
126
+ - Hardcoding canvas dimensions instead of deriving from container/aspect ratio
127
+ - Forgetting to wrap particle coordinates at edges
128
+ - Calling `background()` every frame in trail-accumulation pieces
129
+ - Using HSL for programmatic color generation (perceptually uneven)
130
+ - Animating when the piece should be static (wasting battery/CPU)
131
+ - Not providing seed navigation UI
132
+ - Rendering at 1x on retina displays (blurry output)
133
+ - One `beginPath/stroke` per particle instead of batching (kills frame rate)
134
+ - Noise scale of 1.0 (produces white noise; useful scales are 0.001-0.1)
135
+ - Random RGB values for palettes (muddy, clashing; use OKLCH)
136
+ - Shipping without an export button