arkaos 5.4.0 → 5.6.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.
Files changed (55) hide show
  1. package/README.md +2 -2
  2. package/THE-ARKAOS-GUIDE.md +1 -1
  3. package/VERSION +1 -1
  4. package/arka/SKILL.md +1 -1
  5. package/config/skills-provenance.yaml +27 -0
  6. package/departments/dev/skills/canvas-generative/SKILL.md +362 -0
  7. package/departments/dev/skills/canvas-generative/references/algorithms.md +934 -0
  8. package/departments/dev/skills/canvas-generative/references/genjutsu.LICENSE +21 -0
  9. package/departments/dev/skills/css-native/SKILL.md +353 -0
  10. package/departments/dev/skills/css-native/references/genjutsu.LICENSE +21 -0
  11. package/departments/dev/skills/css-native/references/modern-css.md +465 -0
  12. package/departments/dev/skills/framer-motion/SKILL.md +223 -0
  13. package/departments/dev/skills/framer-motion/references/api.md +312 -0
  14. package/departments/dev/skills/framer-motion/references/genjutsu.LICENSE +21 -0
  15. package/departments/dev/skills/threejs/SKILL.md +173 -0
  16. package/departments/dev/skills/threejs/references/genjutsu.LICENSE +21 -0
  17. package/departments/dev/skills/threejs/references/r3f.md +249 -0
  18. package/departments/dev/skills/threejs/references/scene-setup.md +404 -0
  19. package/departments/dev/skills/threejs/references/shaders.md +452 -0
  20. package/harness/codex/AGENTS.md +1 -1
  21. package/harness/copilot/copilot-instructions.md +1 -1
  22. package/harness/cursor/rules/arkaos.mdc +2 -2
  23. package/harness/gemini/GEMINI.md +1 -1
  24. package/harness/opencode/AGENTS.md +1 -1
  25. package/harness/opencode/agents/arka-architect-gabriel.md +1 -1
  26. package/harness/opencode/agents/arka-brand-director-valentina.md +1 -1
  27. package/harness/opencode/agents/arka-cfo-helena.md +1 -1
  28. package/harness/opencode/agents/arka-chief-of-staff-afonso.md +1 -1
  29. package/harness/opencode/agents/arka-community-strategist-beatriz.md +1 -1
  30. package/harness/opencode/agents/arka-content-strategist-rafael.md +1 -1
  31. package/harness/opencode/agents/arka-conversion-strategist-ines.md +1 -1
  32. package/harness/opencode/agents/arka-coo-sofia.md +1 -1
  33. package/harness/opencode/agents/arka-copy-director-eduardo.md +1 -1
  34. package/harness/opencode/agents/arka-cqo-marta.md +1 -1
  35. package/harness/opencode/agents/arka-cto-marco.md +1 -1
  36. package/harness/opencode/agents/arka-design-ops-lead-iris.md +1 -1
  37. package/harness/opencode/agents/arka-ecom-director-ricardo.md +1 -1
  38. package/harness/opencode/agents/arka-knowledge-director-clara.md +1 -1
  39. package/harness/opencode/agents/arka-leadership-director-rodrigo.md +1 -1
  40. package/harness/opencode/agents/arka-marketing-director-luna.md +1 -1
  41. package/harness/opencode/agents/arka-ops-lead-daniel.md +1 -1
  42. package/harness/opencode/agents/arka-pm-director-carolina.md +1 -1
  43. package/harness/opencode/agents/arka-revops-lead-vicente.md +1 -1
  44. package/harness/opencode/agents/arka-saas-strategist-tiago.md +1 -1
  45. package/harness/opencode/agents/arka-sales-director-miguel.md +1 -1
  46. package/harness/opencode/agents/arka-strategy-director-tomas.md +1 -1
  47. package/harness/opencode/agents/arka-tech-director-francisca.md +1 -1
  48. package/harness/opencode/agents/arka-tech-lead-paulo.md +1 -1
  49. package/harness/opencode/agents/arka-video-producer-simao.md +1 -1
  50. package/harness/zed/.rules +1 -1
  51. package/knowledge/commands-registry.json +1 -1
  52. package/knowledge/skills-manifest.json +61 -1
  53. package/package.json +1 -1
  54. package/pyproject.toml +1 -1
  55. package/scripts/marketplace_gen.py +1 -1
@@ -0,0 +1,934 @@
1
+ # Algorithms Reference
2
+
3
+ > Complete, commented implementations for generative art.
4
+ > Copy-paste ready. Each algorithm is self-contained.
5
+
6
+ ---
7
+
8
+ ## Simplex Noise 2D/3D
9
+
10
+ Compact implementation based on Stefan Gustavson's work. No external dependencies.
11
+
12
+ ```js
13
+ // ─── Simplex Noise ──────────────────────────────────────────────
14
+ // Attempt to produce smooth, isotropic noise without grid-aligned
15
+ // artifacts. Works in 2D and 3D. Returns values in [-1, 1].
16
+
17
+ const SimplexNoise = (() => {
18
+ // Gradient vectors for 2D (12 directions)
19
+ const grad2 = [
20
+ [1,1],[-1,1],[1,-1],[-1,-1],
21
+ [1,0],[-1,0],[0,1],[0,-1],
22
+ [1,1],[-1,1],[1,-1],[-1,-1]
23
+ ];
24
+
25
+ // Gradient vectors for 3D
26
+ const grad3 = [
27
+ [1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0],
28
+ [1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1],
29
+ [0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1]
30
+ ];
31
+
32
+ // Permutation table (256 entries, doubled to avoid wrapping)
33
+ const perm = new Uint8Array(512);
34
+ const permMod12 = new Uint8Array(512);
35
+
36
+ // Seed the permutation table
37
+ // Pass any number to get reproducible results
38
+ function seed(s) {
39
+ const p = new Uint8Array(256);
40
+ for (let i = 0; i < 256; i++) p[i] = i;
41
+
42
+ // Fisher-Yates shuffle seeded with a simple LCG
43
+ let v = s * 1664525 + 1013904223;
44
+ for (let i = 255; i > 0; i--) {
45
+ v = (v * 1664525 + 1013904223) | 0;
46
+ const j = (v >>> 0) % (i + 1);
47
+ [p[i], p[j]] = [p[j], p[i]];
48
+ }
49
+
50
+ for (let i = 0; i < 512; i++) {
51
+ perm[i] = p[i & 255];
52
+ permMod12[i] = perm[i] % 12;
53
+ }
54
+ }
55
+
56
+ // Skewing factors for 2D simplex
57
+ const F2 = 0.5 * (Math.sqrt(3) - 1); // skew input space to grid
58
+ const G2 = (3 - Math.sqrt(3)) / 6; // unskew back
59
+
60
+ // Skewing factors for 3D
61
+ const F3 = 1 / 3;
62
+ const G3 = 1 / 6;
63
+
64
+ function dot2(g, x, y) { return g[0] * x + g[1] * y; }
65
+ function dot3(g, x, y, z) { return g[0] * x + g[1] * y + g[2] * z; }
66
+
67
+ /**
68
+ * 2D Simplex Noise
69
+ * @param {number} xin - x coordinate
70
+ * @param {number} yin - y coordinate
71
+ * @returns {number} noise value in [-1, 1]
72
+ */
73
+ function noise2D(xin, yin) {
74
+ let n0, n1, n2; // contributions from 3 corners
75
+
76
+ // Skew input to find which simplex cell we're in
77
+ const s = (xin + yin) * F2;
78
+ const i = Math.floor(xin + s);
79
+ const j = Math.floor(yin + s);
80
+
81
+ // Unskew back to get cell origin in (x,y) space
82
+ const t = (i + j) * G2;
83
+ const X0 = i - t;
84
+ const Y0 = j - t;
85
+ const x0 = xin - X0; // distance from cell origin
86
+ const y0 = yin - Y0;
87
+
88
+ // Determine which simplex we're in (upper or lower triangle)
89
+ let i1, j1;
90
+ if (x0 > y0) { i1 = 1; j1 = 0; } // lower triangle
91
+ else { i1 = 0; j1 = 1; } // upper triangle
92
+
93
+ // Offsets for middle and last corner
94
+ const x1 = x0 - i1 + G2;
95
+ const y1 = y0 - j1 + G2;
96
+ const x2 = x0 - 1 + 2 * G2;
97
+ const y2 = y0 - 1 + 2 * G2;
98
+
99
+ // Hash coordinates to gradient indices
100
+ const ii = i & 255;
101
+ const jj = j & 255;
102
+ const gi0 = permMod12[ii + perm[jj]];
103
+ const gi1 = permMod12[ii + i1 + perm[jj + j1]];
104
+ const gi2 = permMod12[ii + 1 + perm[jj + 1]];
105
+
106
+ // Calculate contribution from each corner
107
+ // Each corner has a radial falloff of r^2 = 0.5
108
+ let t0 = 0.5 - x0 * x0 - y0 * y0;
109
+ n0 = t0 < 0 ? 0 : (t0 *= t0, t0 * t0 * dot2(grad2[gi0], x0, y0));
110
+
111
+ let t1 = 0.5 - x1 * x1 - y1 * y1;
112
+ n1 = t1 < 0 ? 0 : (t1 *= t1, t1 * t1 * dot2(grad2[gi1], x1, y1));
113
+
114
+ let t2 = 0.5 - x2 * x2 - y2 * y2;
115
+ n2 = t2 < 0 ? 0 : (t2 *= t2, t2 * t2 * dot2(grad2[gi2], x2, y2));
116
+
117
+ // Scale to [-1, 1]
118
+ return 70 * (n0 + n1 + n2);
119
+ }
120
+
121
+ /**
122
+ * 3D Simplex Noise
123
+ * @param {number} xin - x coordinate
124
+ * @param {number} yin - y coordinate
125
+ * @param {number} zin - z coordinate
126
+ * @returns {number} noise value in [-1, 1]
127
+ */
128
+ function noise3D(xin, yin, zin) {
129
+ let n0, n1, n2, n3;
130
+
131
+ // Skew input space
132
+ const s = (xin + yin + zin) * F3;
133
+ const i = Math.floor(xin + s);
134
+ const j = Math.floor(yin + s);
135
+ const k = Math.floor(zin + s);
136
+
137
+ const t = (i + j + k) * G3;
138
+ const X0 = i - t;
139
+ const Y0 = j - t;
140
+ const Z0 = k - t;
141
+ const x0 = xin - X0;
142
+ const y0 = yin - Y0;
143
+ const z0 = zin - Z0;
144
+
145
+ // Determine which simplex we're in (6 possible tetrahedra)
146
+ let i1, j1, k1, i2, j2, k2;
147
+ if (x0 >= y0) {
148
+ if (y0 >= z0) { i1=1; j1=0; k1=0; i2=1; j2=1; k2=0; }
149
+ else if (x0 >= z0) { i1=1; j1=0; k1=0; i2=1; j2=0; k2=1; }
150
+ else { i1=0; j1=0; k1=1; i2=1; j2=0; k2=1; }
151
+ } else {
152
+ if (y0 < z0) { i1=0; j1=0; k1=1; i2=0; j2=1; k2=1; }
153
+ else if (x0 < z0) { i1=0; j1=1; k1=0; i2=0; j2=1; k2=1; }
154
+ else { i1=0; j1=1; k1=0; i2=1; j2=1; k2=0; }
155
+ }
156
+
157
+ const x1 = x0 - i1 + G3;
158
+ const y1 = y0 - j1 + G3;
159
+ const z1 = z0 - k1 + G3;
160
+ const x2 = x0 - i2 + 2 * G3;
161
+ const y2 = y0 - j2 + 2 * G3;
162
+ const z2 = z0 - k2 + 2 * G3;
163
+ const x3 = x0 - 1 + 3 * G3;
164
+ const y3 = y0 - 1 + 3 * G3;
165
+ const z3 = z0 - 1 + 3 * G3;
166
+
167
+ const ii = i & 255;
168
+ const jj = j & 255;
169
+ const kk = k & 255;
170
+ const gi0 = permMod12[ii + perm[jj + perm[kk]]];
171
+ const gi1 = permMod12[ii + i1 + perm[jj + j1 + perm[kk + k1]]];
172
+ const gi2 = permMod12[ii + i2 + perm[jj + j2 + perm[kk + k2]]];
173
+ const gi3 = permMod12[ii + 1 + perm[jj + 1 + perm[kk + 1]]];
174
+
175
+ let t0 = 0.6 - x0*x0 - y0*y0 - z0*z0;
176
+ n0 = t0 < 0 ? 0 : (t0 *= t0, t0 * t0 * dot3(grad3[gi0], x0, y0, z0));
177
+
178
+ let t1 = 0.6 - x1*x1 - y1*y1 - z1*z1;
179
+ n1 = t1 < 0 ? 0 : (t1 *= t1, t1 * t1 * dot3(grad3[gi1], x1, y1, z1));
180
+
181
+ let t2 = 0.6 - x2*x2 - y2*y2 - z2*z2;
182
+ n2 = t2 < 0 ? 0 : (t2 *= t2, t2 * t2 * dot3(grad3[gi2], x2, y2, z2));
183
+
184
+ let t3 = 0.6 - x3*x3 - y3*y3 - z3*z3;
185
+ n3 = t3 < 0 ? 0 : (t3 *= t3, t3 * t3 * dot3(grad3[gi3], x3, y3, z3));
186
+
187
+ return 32 * (n0 + n1 + n2 + n3);
188
+ }
189
+
190
+ // Auto-seed with a default
191
+ seed(0);
192
+
193
+ return { noise2D, noise3D, seed };
194
+ })();
195
+ ```
196
+
197
+ **Usage:**
198
+ ```js
199
+ SimplexNoise.seed(42); // reproducible
200
+
201
+ const val = SimplexNoise.noise2D(x * 0.01, y * 0.01); // scale coords down!
202
+ // val is in [-1, 1]
203
+ ```
204
+
205
+ ---
206
+
207
+ ## Particle System with Pool
208
+
209
+ Zero-allocation particle system. Pre-allocates everything. Swap-and-shrink for death.
210
+
211
+ ```js
212
+ // ─── Particle Pool System ───────────────────────────────────────
213
+ // Fixed-size pool: no new/delete during runtime.
214
+ // Particles are stored contiguously. Dead particles are swapped
215
+ // to the end, keeping iteration tight.
216
+
217
+ class ParticleSystem {
218
+ constructor(maxParticles = 10000) {
219
+ this.max = maxParticles;
220
+ this.count = 0; // number of alive particles
221
+
222
+ // Struct-of-Arrays layout for cache friendliness
223
+ // Each property is a flat Float32Array
224
+ this.x = new Float32Array(maxParticles);
225
+ this.y = new Float32Array(maxParticles);
226
+ this.vx = new Float32Array(maxParticles);
227
+ this.vy = new Float32Array(maxParticles);
228
+ this.life = new Float32Array(maxParticles); // current age (frames)
229
+ this.maxLife = new Float32Array(maxParticles);
230
+ this.size = new Float32Array(maxParticles);
231
+ this.alpha = new Float32Array(maxParticles);
232
+ }
233
+
234
+ /**
235
+ * Spawn a particle at (x, y) with given velocity and lifespan.
236
+ * Returns false if pool is full.
237
+ */
238
+ spawn(x, y, vx, vy, life, size = 2) {
239
+ if (this.count >= this.max) return false;
240
+
241
+ const i = this.count++;
242
+ this.x[i] = x;
243
+ this.y[i] = y;
244
+ this.vx[i] = vx;
245
+ this.vy[i] = vy;
246
+ this.life[i] = 0;
247
+ this.maxLife[i] = life;
248
+ this.size[i] = size;
249
+ this.alpha[i] = 1;
250
+ return true;
251
+ }
252
+
253
+ /**
254
+ * Spawn N particles in a burst pattern (radial).
255
+ */
256
+ burst(x, y, count, speed = 2, life = 60, size = 2) {
257
+ for (let i = 0; i < count; i++) {
258
+ const angle = (i / count) * Math.PI * 2;
259
+ const v = speed * (0.5 + Math.random() * 0.5); // slight randomness
260
+ this.spawn(
261
+ x, y,
262
+ Math.cos(angle) * v,
263
+ Math.sin(angle) * v,
264
+ life * (0.7 + Math.random() * 0.6),
265
+ size
266
+ );
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Update all alive particles.
272
+ * @param {number} dt - delta time in seconds
273
+ * @param {number} gravity - gravity acceleration (positive = downward)
274
+ * @param {number} friction - velocity multiplier per frame (0.99 = slight drag)
275
+ */
276
+ update(dt = 1/60, gravity = 0, friction = 1) {
277
+ for (let i = this.count - 1; i >= 0; i--) {
278
+ // Apply forces
279
+ this.vy[i] += gravity * dt;
280
+ this.vx[i] *= friction;
281
+ this.vy[i] *= friction;
282
+
283
+ // Integrate position
284
+ this.x[i] += this.vx[i];
285
+ this.y[i] += this.vy[i];
286
+
287
+ // Age
288
+ this.life[i]++;
289
+
290
+ // Fade alpha based on remaining life
291
+ const lifeRatio = this.life[i] / this.maxLife[i];
292
+ this.alpha[i] = 1 - lifeRatio; // linear fade out
293
+
294
+ // Kill if expired: swap with last alive particle
295
+ if (this.life[i] >= this.maxLife[i]) {
296
+ this.count--;
297
+ if (i < this.count) {
298
+ // Swap all properties
299
+ this.x[i] = this.x[this.count];
300
+ this.y[i] = this.y[this.count];
301
+ this.vx[i] = this.vx[this.count];
302
+ this.vy[i] = this.vy[this.count];
303
+ this.life[i] = this.life[this.count];
304
+ this.maxLife[i] = this.maxLife[this.count];
305
+ this.size[i] = this.size[this.count];
306
+ this.alpha[i] = this.alpha[this.count];
307
+ }
308
+ }
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Render all alive particles to a Canvas 2D context.
314
+ * @param {CanvasRenderingContext2D} ctx
315
+ * @param {string} color - base color (rgb format, alpha applied per-particle)
316
+ */
317
+ render(ctx, color = '255, 255, 255') {
318
+ for (let i = 0; i < this.count; i++) {
319
+ ctx.beginPath();
320
+ ctx.arc(this.x[i], this.y[i], this.size[i], 0, Math.PI * 2);
321
+ ctx.fillStyle = `rgba(${color}, ${this.alpha[i].toFixed(3)})`;
322
+ ctx.fill();
323
+ }
324
+ }
325
+ }
326
+ ```
327
+
328
+ **Usage:**
329
+ ```js
330
+ const ps = new ParticleSystem(5000);
331
+
332
+ // The ctx is scaled by dpr (see Canvas 2D Setup), so all drawing —
333
+ // including spawn positions and the trail rect — works in CSS pixels,
334
+ // not backing-buffer pixels. canvas.width/2 would land off-canvas at dpr=2.
335
+ const dpr = window.devicePixelRatio || 1;
336
+ const w = canvas.width / dpr;
337
+ const h = canvas.height / dpr;
338
+
339
+ // Continuous emitter
340
+ function emit() {
341
+ ps.spawn(
342
+ w / 2, h / 2,
343
+ (Math.random() - 0.5) * 4,
344
+ (Math.random() - 0.5) * 4,
345
+ 80 + Math.random() * 40
346
+ );
347
+ }
348
+
349
+ let rafId;
350
+ function loop() {
351
+ for (let i = 0; i < 5; i++) emit(); // 5 particles per frame
352
+ ps.update(1/60, 0.1, 0.99);
353
+
354
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.05)'; // trail
355
+ ctx.fillRect(0, 0, w, h);
356
+ ps.render(ctx);
357
+
358
+ rafId = requestAnimationFrame(loop); // keep the handle: cancelAnimationFrame(rafId) on unmount
359
+ }
360
+ rafId = requestAnimationFrame(loop);
361
+ ```
362
+
363
+ ---
364
+
365
+ ## Complete Flow Field
366
+
367
+ Noise-driven vector field with particle followers. Full implementation with color and trail effects.
368
+
369
+ ```js
370
+ // ─── Flow Field ─────────────────────────────────────────────────
371
+ // A grid of angle vectors derived from noise. Particles follow
372
+ // the field, creating organic line patterns.
373
+
374
+ class FlowField {
375
+ /**
376
+ * @param {number} width - canvas logical width
377
+ * @param {number} height - canvas logical height
378
+ * @param {number} cellSize - grid cell size in pixels
379
+ * @param {object} options
380
+ */
381
+ constructor(width, height, cellSize = 20, options = {}) {
382
+ this.width = width;
383
+ this.height = height;
384
+ this.cellSize = cellSize;
385
+
386
+ this.cols = Math.ceil(width / cellSize);
387
+ this.rows = Math.ceil(height / cellSize);
388
+
389
+ // Flat array of angles (radians)
390
+ this.field = new Float32Array(this.cols * this.rows);
391
+
392
+ // Config
393
+ this.noiseScale = options.noiseScale || 0.003; // how zoomed-in the noise is
394
+ this.noiseZ = 0; // animate noise over time
395
+ this.noiseSpeed = options.noiseSpeed || 0.002; // z-axis speed
396
+
397
+ // Particles
398
+ this.particleCount = options.particles || 1000;
399
+ this.particles = [];
400
+ this.maxSpeed = options.maxSpeed || 2;
401
+ this.force = options.force || 0.3;
402
+
403
+ this._initParticles();
404
+ }
405
+
406
+ _initParticles() {
407
+ for (let i = 0; i < this.particleCount; i++) {
408
+ this.particles.push({
409
+ x: Math.random() * this.width,
410
+ y: Math.random() * this.height,
411
+ vx: 0,
412
+ vy: 0,
413
+ // Previous position for line drawing
414
+ px: 0,
415
+ py: 0,
416
+ age: Math.floor(Math.random() * 200), // stagger lifespans
417
+ maxAge: 200 + Math.floor(Math.random() * 100),
418
+ hue: 0, // set during update
419
+ });
420
+ }
421
+ }
422
+
423
+ /**
424
+ * Rebuild the angle grid from noise.
425
+ * Call once per frame before updating particles.
426
+ */
427
+ updateField() {
428
+ this.noiseZ += this.noiseSpeed;
429
+
430
+ for (let y = 0; y < this.rows; y++) {
431
+ for (let x = 0; x < this.cols; x++) {
432
+ // Noise value in [-1, 1], map to full rotation
433
+ const n = SimplexNoise.noise3D(
434
+ x * this.noiseScale * this.cellSize,
435
+ y * this.noiseScale * this.cellSize,
436
+ this.noiseZ
437
+ );
438
+ this.field[y * this.cols + x] = n * Math.PI * 2;
439
+ }
440
+ }
441
+ }
442
+
443
+ /**
444
+ * Get the field angle at a given pixel position.
445
+ * Returns 0 if out of bounds.
446
+ */
447
+ lookup(x, y) {
448
+ const col = Math.floor(x / this.cellSize);
449
+ const row = Math.floor(y / this.cellSize);
450
+ if (col < 0 || col >= this.cols || row < 0 || row >= this.rows) return 0;
451
+ return this.field[row * this.cols + col];
452
+ }
453
+
454
+ /**
455
+ * Update all particles: follow field, apply velocity, handle wrapping/respawn.
456
+ */
457
+ updateParticles() {
458
+ for (const p of this.particles) {
459
+ // Store previous position (for line drawing)
460
+ p.px = p.x;
461
+ p.py = p.y;
462
+
463
+ // Get field angle at current position
464
+ const angle = this.lookup(p.x, p.y);
465
+
466
+ // Apply force in field direction
467
+ p.vx += Math.cos(angle) * this.force;
468
+ p.vy += Math.sin(angle) * this.force;
469
+
470
+ // Clamp speed
471
+ const speed = Math.sqrt(p.vx * p.vx + p.vy * p.vy);
472
+ if (speed > this.maxSpeed) {
473
+ p.vx = (p.vx / speed) * this.maxSpeed;
474
+ p.vy = (p.vy / speed) * this.maxSpeed;
475
+ }
476
+
477
+ // Move
478
+ p.x += p.vx;
479
+ p.y += p.vy;
480
+
481
+ // Slight friction
482
+ p.vx *= 0.96;
483
+ p.vy *= 0.96;
484
+
485
+ // Color based on angle
486
+ p.hue = ((angle / (Math.PI * 2)) * 360 + 360) % 360;
487
+
488
+ // Age and respawn
489
+ p.age++;
490
+ if (p.age > p.maxAge || p.x < 0 || p.x > this.width || p.y < 0 || p.y > this.height) {
491
+ p.x = Math.random() * this.width;
492
+ p.y = Math.random() * this.height;
493
+ p.px = p.x;
494
+ p.py = p.y;
495
+ p.vx = 0;
496
+ p.vy = 0;
497
+ p.age = 0;
498
+ }
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Render particle trails as lines.
504
+ */
505
+ render(ctx) {
506
+ for (const p of this.particles) {
507
+ // Skip first frame (no previous position)
508
+ if (p.age === 0) continue;
509
+
510
+ const alpha = Math.min(1, (1 - p.age / p.maxAge) * 0.8);
511
+ ctx.beginPath();
512
+ ctx.moveTo(p.px, p.py);
513
+ ctx.lineTo(p.x, p.y);
514
+ ctx.strokeStyle = `hsla(${p.hue}, 70%, 60%, ${alpha})`;
515
+ ctx.lineWidth = 1;
516
+ ctx.stroke();
517
+ }
518
+ }
519
+ }
520
+
521
+ // ─── Usage ──────────────────────────────────────────────────────
522
+ //
523
+ // const canvas = document.getElementById('canvas');
524
+ // const ctx = setupCanvas(canvas, window.innerWidth, window.innerHeight);
525
+ // SimplexNoise.seed(42);
526
+ //
527
+ // const ff = new FlowField(canvas.clientWidth, canvas.clientHeight, 20, {
528
+ // particles: 2000,
529
+ // noiseScale: 0.004,
530
+ // noiseSpeed: 0.001,
531
+ // maxSpeed: 2,
532
+ // force: 0.2,
533
+ // });
534
+ //
535
+ // function loop() {
536
+ // // Trail fade (no clearRect!)
537
+ // ctx.fillStyle = 'rgba(10, 10, 15, 0.03)';
538
+ // ctx.fillRect(0, 0, canvas.clientWidth, canvas.clientHeight);
539
+ //
540
+ // ff.updateField();
541
+ // ff.updateParticles();
542
+ // ff.render(ctx);
543
+ //
544
+ // requestAnimationFrame(loop);
545
+ // }
546
+ // requestAnimationFrame(loop);
547
+ ```
548
+
549
+ ---
550
+
551
+ ## L-System Renderer
552
+
553
+ Complete L-system with string expansion and turtle graphics rendering.
554
+
555
+ ```js
556
+ // ─── L-System ───────────────────────────────────────────────────
557
+ // String-rewriting system + turtle graphics.
558
+ // Produces fractal plants, trees, curves.
559
+
560
+ class LSystem {
561
+ /**
562
+ * @param {string} axiom - starting string
563
+ * @param {Object<string, string>} rules - production rules { 'F': 'F[+F]F[-F]F' }
564
+ * @param {number} angle - turn angle in degrees
565
+ */
566
+ constructor(axiom, rules, angle) {
567
+ this.axiom = axiom;
568
+ this.rules = rules;
569
+ this.angle = (angle * Math.PI) / 180; // convert to radians
570
+ this.commands = axiom;
571
+ }
572
+
573
+ /**
574
+ * Expand the L-system string by N iterations.
575
+ * WARNING: string length grows exponentially. Keep iterations low (4-7).
576
+ */
577
+ generate(iterations) {
578
+ this.commands = this.axiom;
579
+ for (let i = 0; i < iterations; i++) {
580
+ let next = '';
581
+ for (const char of this.commands) {
582
+ next += this.rules[char] || char;
583
+ }
584
+ this.commands = next;
585
+ }
586
+ return this.commands;
587
+ }
588
+
589
+ /**
590
+ * Render the L-system commands to a canvas context.
591
+ * @param {CanvasRenderingContext2D} ctx
592
+ * @param {number} startX - turtle start x
593
+ * @param {number} startY - turtle start y
594
+ * @param {number} startAngle - initial heading in radians (default: up)
595
+ * @param {number} stepLength - length per 'F' step
596
+ * @param {object} style - { strokeStyle, lineWidth, depthFade }
597
+ */
598
+ render(ctx, startX, startY, startAngle = -Math.PI / 2, stepLength = 5, style = {}) {
599
+ const {
600
+ strokeStyle = '#88cc88',
601
+ lineWidth = 1,
602
+ depthFade = true, // fade color with stack depth
603
+ } = style;
604
+
605
+ // Turtle state
606
+ let x = startX;
607
+ let y = startY;
608
+ let angle = startAngle;
609
+ let depth = 0;
610
+ const stack = []; // save/restore stack for [ ]
611
+
612
+ ctx.save();
613
+ ctx.lineCap = 'round';
614
+
615
+ for (const char of this.commands) {
616
+ switch (char) {
617
+ // F: move forward, drawing a line
618
+ case 'F': {
619
+ const nx = x + Math.cos(angle) * stepLength;
620
+ const ny = y + Math.sin(angle) * stepLength;
621
+
622
+ ctx.beginPath();
623
+ ctx.moveTo(x, y);
624
+ ctx.lineTo(nx, ny);
625
+
626
+ if (depthFade) {
627
+ const alpha = Math.max(0.1, 1 - depth * 0.15);
628
+ const width = Math.max(0.5, lineWidth - depth * 0.2);
629
+ ctx.strokeStyle = strokeStyle.includes('rgba')
630
+ ? strokeStyle
631
+ : `rgba(136, 204, 136, ${alpha})`;
632
+ ctx.lineWidth = width;
633
+ } else {
634
+ ctx.strokeStyle = strokeStyle;
635
+ ctx.lineWidth = lineWidth;
636
+ }
637
+
638
+ ctx.stroke();
639
+ x = nx;
640
+ y = ny;
641
+ break;
642
+ }
643
+
644
+ // f: move forward without drawing (pen up)
645
+ case 'f': {
646
+ x += Math.cos(angle) * stepLength;
647
+ y += Math.sin(angle) * stepLength;
648
+ break;
649
+ }
650
+
651
+ // +: turn right (clockwise)
652
+ case '+':
653
+ angle += this.angle;
654
+ break;
655
+
656
+ // -: turn left (counter-clockwise)
657
+ case '-':
658
+ angle -= this.angle;
659
+ break;
660
+
661
+ // [: push state (branch start)
662
+ case '[':
663
+ stack.push({ x, y, angle, depth });
664
+ depth++;
665
+ break;
666
+
667
+ // ]: pop state (branch end)
668
+ case ']': {
669
+ const state = stack.pop();
670
+ if (state) {
671
+ x = state.x;
672
+ y = state.y;
673
+ angle = state.angle;
674
+ depth = state.depth;
675
+ }
676
+ break;
677
+ }
678
+ }
679
+ }
680
+
681
+ ctx.restore();
682
+ }
683
+ }
684
+
685
+ // ─── Preset L-Systems ──────────────────────────────────────────
686
+
687
+ const LSYSTEM_PRESETS = {
688
+ // Fractal plant (bush-like)
689
+ plant: {
690
+ axiom: 'X',
691
+ rules: {
692
+ 'X': 'F+[[X]-X]-F[-FX]+X',
693
+ 'F': 'FF',
694
+ },
695
+ angle: 25,
696
+ iterations: 6,
697
+ stepLength: 3,
698
+ },
699
+
700
+ // Koch snowflake
701
+ koch: {
702
+ axiom: 'F--F--F',
703
+ rules: { 'F': 'F+F--F+F' },
704
+ angle: 60,
705
+ iterations: 4,
706
+ stepLength: 3,
707
+ },
708
+
709
+ // Sierpinski triangle
710
+ sierpinski: {
711
+ axiom: 'F-G-G',
712
+ rules: { 'F': 'F-G+F+G-F', 'G': 'GG' },
713
+ angle: 120,
714
+ iterations: 6,
715
+ stepLength: 4,
716
+ },
717
+
718
+ // Dragon curve
719
+ dragon: {
720
+ axiom: 'FX',
721
+ rules: { 'X': 'X+YF+', 'Y': '-FX-Y' },
722
+ angle: 90,
723
+ iterations: 12,
724
+ stepLength: 5,
725
+ },
726
+
727
+ // Binary tree
728
+ tree: {
729
+ axiom: 'F',
730
+ rules: { 'F': 'FF+[+F-F-F]-[-F+F+F]' },
731
+ angle: 22.5,
732
+ iterations: 4,
733
+ stepLength: 8,
734
+ },
735
+ };
736
+
737
+ // ─── Usage ──────────────────────────────────────────────────────
738
+ //
739
+ // const preset = LSYSTEM_PRESETS.plant;
740
+ // const ls = new LSystem(preset.axiom, preset.rules, preset.angle);
741
+ // ls.generate(preset.iterations);
742
+ //
743
+ // ctx.fillStyle = '#111';
744
+ // ctx.fillRect(0, 0, w, h);
745
+ // ls.render(ctx, w / 2, h, -Math.PI / 2, preset.stepLength, {
746
+ // strokeStyle: '#88cc88',
747
+ // lineWidth: 1.5,
748
+ // depthFade: true,
749
+ // });
750
+ ```
751
+
752
+ ---
753
+
754
+ ## Lorenz Attractor
755
+
756
+ The classic strange attractor. 3D system projected to 2D canvas.
757
+
758
+ ```js
759
+ // ─── Lorenz Attractor ───────────────────────────────────────────
760
+ // A chaotic system of 3 coupled ODEs. The trajectory never repeats
761
+ // and never intersects itself, creating the iconic butterfly shape.
762
+ //
763
+ // dx/dt = sigma * (y - x)
764
+ // dy/dt = x * (rho - z) - y
765
+ // dz/dt = x * y - beta * z
766
+
767
+ class LorenzAttractor {
768
+ /**
769
+ * @param {object} options
770
+ * @param {number} options.sigma - Prandtl number (default 10)
771
+ * @param {number} options.rho - Rayleigh number (default 28)
772
+ * @param {number} options.beta - geometric factor (default 8/3)
773
+ * @param {number} options.dt - integration timestep
774
+ * @param {number} options.trailLength - how many points to remember
775
+ */
776
+ constructor(options = {}) {
777
+ this.sigma = options.sigma || 10;
778
+ this.rho = options.rho || 28;
779
+ this.beta = options.beta || 8 / 3;
780
+ this.dt = options.dt || 0.005;
781
+
782
+ this.trailLength = options.trailLength || 5000;
783
+
784
+ // Current position in 3D space
785
+ this.x = 0.01;
786
+ this.y = 0;
787
+ this.z = 0;
788
+
789
+ // Trail history (ring buffer)
790
+ this.trail = new Float32Array(this.trailLength * 3); // [x,y,z, x,y,z, ...]
791
+ this.trailHead = 0;
792
+ this.trailCount = 0;
793
+ }
794
+
795
+ /**
796
+ * Step the simulation forward by one timestep (Runge-Kutta 4th order).
797
+ * RK4 is crucial for chaotic systems -- Euler integration diverges fast.
798
+ */
799
+ step() {
800
+ const { sigma, rho, beta, dt } = this;
801
+ let { x, y, z } = this;
802
+
803
+ // Derivatives function
804
+ const f = (x, y, z) => ({
805
+ dx: sigma * (y - x),
806
+ dy: x * (rho - z) - y,
807
+ dz: x * y - beta * z,
808
+ });
809
+
810
+ // RK4 integration
811
+ const k1 = f(x, y, z);
812
+ const k2 = f(
813
+ x + k1.dx * dt * 0.5,
814
+ y + k1.dy * dt * 0.5,
815
+ z + k1.dz * dt * 0.5
816
+ );
817
+ const k3 = f(
818
+ x + k2.dx * dt * 0.5,
819
+ y + k2.dy * dt * 0.5,
820
+ z + k2.dz * dt * 0.5
821
+ );
822
+ const k4 = f(
823
+ x + k3.dx * dt,
824
+ y + k3.dy * dt,
825
+ z + k3.dz * dt
826
+ );
827
+
828
+ this.x += (dt / 6) * (k1.dx + 2*k2.dx + 2*k3.dx + k4.dx);
829
+ this.y += (dt / 6) * (k1.dy + 2*k2.dy + 2*k3.dy + k4.dy);
830
+ this.z += (dt / 6) * (k1.dz + 2*k2.dz + 2*k3.dz + k4.dz);
831
+
832
+ // Store in trail ring buffer
833
+ const idx = this.trailHead * 3;
834
+ this.trail[idx] = this.x;
835
+ this.trail[idx + 1] = this.y;
836
+ this.trail[idx + 2] = this.z;
837
+ this.trailHead = (this.trailHead + 1) % this.trailLength;
838
+ if (this.trailCount < this.trailLength) this.trailCount++;
839
+ }
840
+
841
+ /**
842
+ * Step N times (useful for faster evolution per frame).
843
+ */
844
+ stepN(n) {
845
+ for (let i = 0; i < n; i++) this.step();
846
+ }
847
+
848
+ /**
849
+ * Project 3D point to 2D using simple orthographic projection.
850
+ * The Lorenz attractor lives roughly in x:[-20,20], y:[-25,25], z:[0,50].
851
+ * We map x->screen.x and z->screen.y for the classic side view.
852
+ *
853
+ * @param {number} cx - canvas center x
854
+ * @param {number} cy - canvas center y
855
+ * @param {number} scale - zoom factor
856
+ * @returns {{ sx: number, sy: number }} screen coordinates
857
+ */
858
+ project(px, py, pz, cx, cy, scale = 8) {
859
+ return {
860
+ sx: cx + px * scale,
861
+ sy: cy - (pz - 25) * scale, // center z around 25, flip y-axis
862
+ };
863
+ }
864
+
865
+ /**
866
+ * Render the attractor trail to a 2D canvas.
867
+ * Older points are more transparent, creating a fade-in effect.
868
+ */
869
+ render(ctx, cx, cy, scale = 8) {
870
+ if (this.trailCount < 2) return;
871
+
872
+ // Walk the ring buffer from oldest to newest
873
+ const start = this.trailCount < this.trailLength
874
+ ? 0
875
+ : this.trailHead;
876
+
877
+ let prevIdx = start * 3;
878
+ let prev = this.project(
879
+ this.trail[prevIdx], this.trail[prevIdx+1], this.trail[prevIdx+2],
880
+ cx, cy, scale
881
+ );
882
+
883
+ for (let i = 1; i < this.trailCount; i++) {
884
+ const bufIdx = ((start + i) % this.trailLength) * 3;
885
+ const cur = this.project(
886
+ this.trail[bufIdx], this.trail[bufIdx+1], this.trail[bufIdx+2],
887
+ cx, cy, scale
888
+ );
889
+
890
+ // Alpha: oldest = faint, newest = bright
891
+ const t = i / this.trailCount;
892
+ const alpha = t * 0.9 + 0.1;
893
+
894
+ // Color: hue shifts along the trail
895
+ const hue = (t * 240 + 200) % 360; // blue to purple
896
+
897
+ ctx.beginPath();
898
+ ctx.moveTo(prev.sx, prev.sy);
899
+ ctx.lineTo(cur.sx, cur.sy);
900
+ ctx.strokeStyle = `hsla(${hue}, 80%, 60%, ${alpha})`;
901
+ ctx.lineWidth = 0.8;
902
+ ctx.stroke();
903
+
904
+ prev = cur;
905
+ }
906
+ }
907
+ }
908
+
909
+ // ─── Usage ──────────────────────────────────────────────────────
910
+ //
911
+ // const canvas = document.getElementById('canvas');
912
+ // const ctx = setupCanvas(canvas, window.innerWidth, window.innerHeight);
913
+ // const lorenz = new LorenzAttractor({ trailLength: 8000, dt: 0.005 });
914
+ //
915
+ // function loop() {
916
+ // // Dark background with slight trail
917
+ // ctx.fillStyle = 'rgba(5, 5, 15, 0.02)';
918
+ // ctx.fillRect(0, 0, canvas.clientWidth, canvas.clientHeight);
919
+ //
920
+ // // Step multiple times per frame for faster evolution
921
+ // lorenz.stepN(10);
922
+ //
923
+ // // Render centered on canvas
924
+ // lorenz.render(ctx, canvas.clientWidth / 2, canvas.clientHeight / 2, 8);
925
+ //
926
+ // requestAnimationFrame(loop);
927
+ // }
928
+ // requestAnimationFrame(loop);
929
+ //
930
+ // ─── Classic Parameters ─────────────────────────────────────────
931
+ // sigma=10, rho=28, beta=8/3 → classic butterfly
932
+ // sigma=10, rho=99.96, beta=8/3 → tighter knots
933
+ // sigma=14, rho=28, beta=8/3 → wider wings
934
+ ```