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.
- package/README.md +2 -2
- package/THE-ARKAOS-GUIDE.md +1 -1
- package/VERSION +1 -1
- package/arka/SKILL.md +1 -1
- package/config/skills-provenance.yaml +27 -0
- package/departments/dev/skills/canvas-generative/SKILL.md +362 -0
- package/departments/dev/skills/canvas-generative/references/algorithms.md +934 -0
- package/departments/dev/skills/canvas-generative/references/genjutsu.LICENSE +21 -0
- package/departments/dev/skills/css-native/SKILL.md +353 -0
- package/departments/dev/skills/css-native/references/genjutsu.LICENSE +21 -0
- package/departments/dev/skills/css-native/references/modern-css.md +465 -0
- package/departments/dev/skills/framer-motion/SKILL.md +223 -0
- package/departments/dev/skills/framer-motion/references/api.md +312 -0
- package/departments/dev/skills/framer-motion/references/genjutsu.LICENSE +21 -0
- package/departments/dev/skills/threejs/SKILL.md +173 -0
- package/departments/dev/skills/threejs/references/genjutsu.LICENSE +21 -0
- package/departments/dev/skills/threejs/references/r3f.md +249 -0
- package/departments/dev/skills/threejs/references/scene-setup.md +404 -0
- package/departments/dev/skills/threejs/references/shaders.md +452 -0
- package/harness/codex/AGENTS.md +1 -1
- package/harness/copilot/copilot-instructions.md +1 -1
- package/harness/cursor/rules/arkaos.mdc +2 -2
- package/harness/gemini/GEMINI.md +1 -1
- package/harness/opencode/AGENTS.md +1 -1
- package/harness/opencode/agents/arka-architect-gabriel.md +1 -1
- package/harness/opencode/agents/arka-brand-director-valentina.md +1 -1
- package/harness/opencode/agents/arka-cfo-helena.md +1 -1
- package/harness/opencode/agents/arka-chief-of-staff-afonso.md +1 -1
- package/harness/opencode/agents/arka-community-strategist-beatriz.md +1 -1
- package/harness/opencode/agents/arka-content-strategist-rafael.md +1 -1
- package/harness/opencode/agents/arka-conversion-strategist-ines.md +1 -1
- package/harness/opencode/agents/arka-coo-sofia.md +1 -1
- package/harness/opencode/agents/arka-copy-director-eduardo.md +1 -1
- package/harness/opencode/agents/arka-cqo-marta.md +1 -1
- package/harness/opencode/agents/arka-cto-marco.md +1 -1
- package/harness/opencode/agents/arka-design-ops-lead-iris.md +1 -1
- package/harness/opencode/agents/arka-ecom-director-ricardo.md +1 -1
- package/harness/opencode/agents/arka-knowledge-director-clara.md +1 -1
- package/harness/opencode/agents/arka-leadership-director-rodrigo.md +1 -1
- package/harness/opencode/agents/arka-marketing-director-luna.md +1 -1
- package/harness/opencode/agents/arka-ops-lead-daniel.md +1 -1
- package/harness/opencode/agents/arka-pm-director-carolina.md +1 -1
- package/harness/opencode/agents/arka-revops-lead-vicente.md +1 -1
- package/harness/opencode/agents/arka-saas-strategist-tiago.md +1 -1
- package/harness/opencode/agents/arka-sales-director-miguel.md +1 -1
- package/harness/opencode/agents/arka-strategy-director-tomas.md +1 -1
- package/harness/opencode/agents/arka-tech-director-francisca.md +1 -1
- package/harness/opencode/agents/arka-tech-lead-paulo.md +1 -1
- package/harness/opencode/agents/arka-video-producer-simao.md +1 -1
- package/harness/zed/.rules +1 -1
- package/knowledge/commands-registry.json +1 -1
- package/knowledge/skills-manifest.json +61 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- 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
|
+
```
|