@guinetik/gcanvas 1.0.5 → 2.0.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/dist/aizawa.html +27 -0
- package/dist/clifford.html +25 -0
- package/dist/cmb.html +24 -0
- package/dist/dadras.html +26 -0
- package/dist/dejong.html +25 -0
- package/dist/gcanvas.es.js +5130 -372
- package/dist/gcanvas.es.min.js +1 -1
- package/dist/gcanvas.umd.js +1 -1
- package/dist/gcanvas.umd.min.js +1 -1
- package/dist/halvorsen.html +27 -0
- package/dist/index.html +96 -48
- package/dist/js/aizawa.js +425 -0
- package/dist/js/bezier.js +5 -5
- package/dist/js/clifford.js +236 -0
- package/dist/js/cmb.js +594 -0
- package/dist/js/dadras.js +405 -0
- package/dist/js/dejong.js +257 -0
- package/dist/js/halvorsen.js +405 -0
- package/dist/js/isometric.js +34 -46
- package/dist/js/lorenz.js +425 -0
- package/dist/js/painter.js +8 -8
- package/dist/js/rossler.js +480 -0
- package/dist/js/schrodinger.js +314 -18
- package/dist/js/thomas.js +394 -0
- package/dist/lorenz.html +27 -0
- package/dist/rossler.html +27 -0
- package/dist/scene-interactivity-test.html +220 -0
- package/dist/thomas.html +27 -0
- package/package.json +1 -1
- package/readme.md +30 -22
- package/src/game/objects/go.js +7 -0
- package/src/game/objects/index.js +2 -0
- package/src/game/objects/isometric-scene.js +53 -3
- package/src/game/objects/layoutscene.js +57 -0
- package/src/game/objects/mask.js +241 -0
- package/src/game/objects/scene.js +19 -0
- package/src/game/objects/wrapper.js +14 -2
- package/src/game/pipeline.js +17 -0
- package/src/game/ui/button.js +101 -16
- package/src/game/ui/theme.js +0 -6
- package/src/game/ui/togglebutton.js +25 -14
- package/src/game/ui/tooltip.js +12 -4
- package/src/index.js +3 -0
- package/src/io/gesture.js +409 -0
- package/src/io/index.js +4 -1
- package/src/io/keys.js +9 -1
- package/src/io/screen.js +476 -0
- package/src/math/attractors.js +664 -0
- package/src/math/heat.js +106 -0
- package/src/math/index.js +1 -0
- package/src/mixins/draggable.js +15 -19
- package/src/painter/painter.shapes.js +11 -5
- package/src/particle/particle-system.js +165 -1
- package/src/physics/index.js +26 -0
- package/src/physics/physics-updaters.js +333 -0
- package/src/physics/physics.js +375 -0
- package/src/shapes/image.js +5 -5
- package/src/shapes/index.js +2 -0
- package/src/shapes/parallelogram.js +147 -0
- package/src/shapes/righttriangle.js +115 -0
- package/src/shapes/svg.js +281 -100
- package/src/shapes/text.js +22 -6
- package/src/shapes/transformable.js +5 -0
- package/src/sound/effects.js +807 -0
- package/src/sound/index.js +13 -0
- package/src/webgl/index.js +7 -0
- package/src/webgl/shaders/clifford-point-shaders.js +131 -0
- package/src/webgl/shaders/dejong-point-shaders.js +131 -0
- package/src/webgl/shaders/point-sprite-shaders.js +152 -0
- package/src/webgl/webgl-clifford-renderer.js +477 -0
- package/src/webgl/webgl-dejong-renderer.js +472 -0
- package/src/webgl/webgl-line-renderer.js +391 -0
- package/src/webgl/webgl-particle-renderer.js +410 -0
- package/types/index.d.ts +30 -2
- package/types/io.d.ts +217 -0
- package/types/physics.d.ts +299 -0
- package/types/shapes.d.ts +8 -0
- package/types/webgl.d.ts +188 -109
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dadras Attractor 3D Visualization
|
|
3
|
+
*
|
|
4
|
+
* A 3D chaotic attractor visualization where particles follow the Dadras
|
|
5
|
+
* dynamical system equations. Trails are colored by velocity (blue=slow,
|
|
6
|
+
* red=fast) with additive blending for a glowing effect.
|
|
7
|
+
*
|
|
8
|
+
* Uses the Attractors module for pure math functions and WebGL for
|
|
9
|
+
* high-performance line rendering.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Game, Gesture, Screen, Attractors } from "/gcanvas.es.min.js";
|
|
13
|
+
import { Camera3D } from "/gcanvas.es.min.js";
|
|
14
|
+
import { WebGLLineRenderer } from "/gcanvas.es.min.js";
|
|
15
|
+
|
|
16
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
// CONFIGURATION
|
|
18
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const CONFIG = {
|
|
21
|
+
// Attractor settings (uses Attractors.dadras for equations)
|
|
22
|
+
attractor: {
|
|
23
|
+
dt: 0.01, // Integration time step
|
|
24
|
+
scale: 50, // Scale factor for display
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
// Particle settings
|
|
28
|
+
particles: {
|
|
29
|
+
count: 500,
|
|
30
|
+
trailLength: 200,
|
|
31
|
+
spawnRange: 5, // Initial position range around origin
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// Camera settings
|
|
35
|
+
camera: {
|
|
36
|
+
perspective: 800,
|
|
37
|
+
rotationX: 0.3,
|
|
38
|
+
rotationY: 0,
|
|
39
|
+
inertia: true,
|
|
40
|
+
friction: 0.95,
|
|
41
|
+
clampX: false,
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
// Visual settings
|
|
45
|
+
visual: {
|
|
46
|
+
minHue: 60, // Red (fast)
|
|
47
|
+
maxHue: 240, // Blue (slow)
|
|
48
|
+
maxSpeed: 30, // Speed normalization threshold
|
|
49
|
+
saturation: 80,
|
|
50
|
+
lightness: 50,
|
|
51
|
+
maxAlpha: 0.9,
|
|
52
|
+
hueShiftSpeed: 20, // Degrees per second (0 to disable)
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// Glitch/blink effect
|
|
56
|
+
blink: {
|
|
57
|
+
chance: 0.02,
|
|
58
|
+
minDuration: 0.05,
|
|
59
|
+
maxDuration: 0.3,
|
|
60
|
+
intensityBoost: 1.5,
|
|
61
|
+
saturationBoost: 1.2,
|
|
62
|
+
alphaBoost: 1.3,
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// Zoom settings
|
|
66
|
+
zoom: {
|
|
67
|
+
min: 0.3,
|
|
68
|
+
max: 3.0,
|
|
69
|
+
speed: 0.5,
|
|
70
|
+
easing: 0.12,
|
|
71
|
+
baseScreenSize: 600,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
76
|
+
// HELPER FUNCTIONS
|
|
77
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Convert HSL to RGB
|
|
81
|
+
*/
|
|
82
|
+
function hslToRgb(h, s, l) {
|
|
83
|
+
s /= 100;
|
|
84
|
+
l /= 100;
|
|
85
|
+
const k = (n) => (n + h / 30) % 12;
|
|
86
|
+
const a = s * Math.min(l, 1 - l);
|
|
87
|
+
const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
|
88
|
+
return {
|
|
89
|
+
r: Math.round(255 * f(0)),
|
|
90
|
+
g: Math.round(255 * f(8)),
|
|
91
|
+
b: Math.round(255 * f(4)),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
96
|
+
// ATTRACTOR PARTICLE
|
|
97
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A particle following attractor dynamics
|
|
101
|
+
*/
|
|
102
|
+
class AttractorParticle {
|
|
103
|
+
/**
|
|
104
|
+
* @param {Function} stepFn - Attractor step function
|
|
105
|
+
* @param {number} spawnRange - Initial position range
|
|
106
|
+
*/
|
|
107
|
+
constructor(stepFn, spawnRange) {
|
|
108
|
+
this.stepFn = stepFn;
|
|
109
|
+
this.position = {
|
|
110
|
+
x: (Math.random() - 0.5) * spawnRange,
|
|
111
|
+
y: (Math.random() - 0.5) * spawnRange,
|
|
112
|
+
z: (Math.random() - 0.5) * spawnRange,
|
|
113
|
+
};
|
|
114
|
+
this.trail = [];
|
|
115
|
+
this.speed = 0;
|
|
116
|
+
|
|
117
|
+
// Blink/glitch state
|
|
118
|
+
this.blinkTime = 0;
|
|
119
|
+
this.blinkIntensity = 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Update blink state
|
|
124
|
+
*/
|
|
125
|
+
updateBlink(dt) {
|
|
126
|
+
const { chance, minDuration, maxDuration } = CONFIG.blink;
|
|
127
|
+
|
|
128
|
+
if (this.blinkTime > 0) {
|
|
129
|
+
this.blinkTime -= dt;
|
|
130
|
+
this.blinkIntensity = Math.max(
|
|
131
|
+
0,
|
|
132
|
+
this.blinkTime > 0
|
|
133
|
+
? Math.sin((this.blinkTime / ((minDuration + maxDuration) * 0.5)) * Math.PI)
|
|
134
|
+
: 0
|
|
135
|
+
);
|
|
136
|
+
} else {
|
|
137
|
+
if (Math.random() < chance) {
|
|
138
|
+
this.blinkTime = minDuration + Math.random() * (maxDuration - minDuration);
|
|
139
|
+
this.blinkIntensity = 1;
|
|
140
|
+
} else {
|
|
141
|
+
this.blinkIntensity = 0;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Update particle position using attractor
|
|
148
|
+
*/
|
|
149
|
+
update(dt, scale) {
|
|
150
|
+
// Use the attractor step function
|
|
151
|
+
const result = this.stepFn(this.position, dt);
|
|
152
|
+
|
|
153
|
+
// Update position
|
|
154
|
+
this.position = result.position;
|
|
155
|
+
this.speed = result.speed;
|
|
156
|
+
|
|
157
|
+
// Add to trail (scaled for display)
|
|
158
|
+
this.trail.unshift({
|
|
159
|
+
x: this.position.x * scale,
|
|
160
|
+
y: this.position.y * scale,
|
|
161
|
+
z: this.position.z * scale,
|
|
162
|
+
speed: this.speed,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Trim trail
|
|
166
|
+
if (this.trail.length > CONFIG.particles.trailLength) {
|
|
167
|
+
this.trail.pop();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
173
|
+
// DEMO CLASS
|
|
174
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Dadras Attractor Demo
|
|
178
|
+
*/
|
|
179
|
+
class DadrasDemo extends Game {
|
|
180
|
+
constructor(canvas) {
|
|
181
|
+
super(canvas);
|
|
182
|
+
this.backgroundColor = "#000";
|
|
183
|
+
this.enableFluidSize();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
init() {
|
|
187
|
+
super.init();
|
|
188
|
+
|
|
189
|
+
// Get attractor info for display
|
|
190
|
+
this.attractor = Attractors.dadras;
|
|
191
|
+
console.log(`Attractor: ${this.attractor.name}`);
|
|
192
|
+
console.log(`Equations:`, this.attractor.equations);
|
|
193
|
+
|
|
194
|
+
// Create stepper function with default params
|
|
195
|
+
this.stepFn = this.attractor.createStepper();
|
|
196
|
+
|
|
197
|
+
// Calculate initial zoom
|
|
198
|
+
const { min, max, baseScreenSize } = CONFIG.zoom;
|
|
199
|
+
const initialZoom = Math.min(max, Math.max(min, Screen.minDimension() / baseScreenSize));
|
|
200
|
+
this.zoom = initialZoom;
|
|
201
|
+
this.targetZoom = initialZoom;
|
|
202
|
+
this.defaultZoom = initialZoom;
|
|
203
|
+
|
|
204
|
+
// Camera with mouse control
|
|
205
|
+
this.camera = new Camera3D({
|
|
206
|
+
perspective: CONFIG.camera.perspective,
|
|
207
|
+
rotationX: CONFIG.camera.rotationX,
|
|
208
|
+
rotationY: CONFIG.camera.rotationY,
|
|
209
|
+
inertia: CONFIG.camera.inertia,
|
|
210
|
+
friction: CONFIG.camera.friction,
|
|
211
|
+
clampX: CONFIG.camera.clampX,
|
|
212
|
+
});
|
|
213
|
+
this.camera.enableMouseControl(this.canvas);
|
|
214
|
+
|
|
215
|
+
// Gesture handler for zoom
|
|
216
|
+
this.gesture = new Gesture(this.canvas, {
|
|
217
|
+
onZoom: (delta) => {
|
|
218
|
+
this.targetZoom *= 1 + delta * CONFIG.zoom.speed;
|
|
219
|
+
},
|
|
220
|
+
onPan: null,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Double-click to reset
|
|
224
|
+
this.canvas.addEventListener("dblclick", () => {
|
|
225
|
+
this.targetZoom = this.defaultZoom;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Initialize particles using the attractor step function
|
|
229
|
+
this.particles = [];
|
|
230
|
+
for (let i = 0; i < CONFIG.particles.count; i++) {
|
|
231
|
+
this.particles.push(new AttractorParticle(this.stepFn, CONFIG.particles.spawnRange));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// WebGL line renderer
|
|
235
|
+
const maxSegments = CONFIG.particles.count * CONFIG.particles.trailLength;
|
|
236
|
+
this.lineRenderer = new WebGLLineRenderer(maxSegments, {
|
|
237
|
+
width: this.width,
|
|
238
|
+
height: this.height,
|
|
239
|
+
blendMode: "additive",
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
this.segments = [];
|
|
243
|
+
|
|
244
|
+
if (!this.lineRenderer.isAvailable()) {
|
|
245
|
+
console.warn("WebGL not available, falling back to Canvas 2D");
|
|
246
|
+
this.useWebGL = false;
|
|
247
|
+
} else {
|
|
248
|
+
this.useWebGL = true;
|
|
249
|
+
console.log(`WebGL enabled, ${maxSegments} max segments`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
this.time = 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
onResize() {
|
|
256
|
+
if (this.lineRenderer?.isAvailable()) {
|
|
257
|
+
this.lineRenderer.resize(this.width, this.height);
|
|
258
|
+
}
|
|
259
|
+
const { min, max, baseScreenSize } = CONFIG.zoom;
|
|
260
|
+
this.defaultZoom = Math.min(max, Math.max(min, Screen.minDimension() / baseScreenSize));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
update(dt) {
|
|
264
|
+
super.update(dt);
|
|
265
|
+
this.camera.update(dt);
|
|
266
|
+
this.zoom += (this.targetZoom - this.zoom) * CONFIG.zoom.easing;
|
|
267
|
+
this.time += dt;
|
|
268
|
+
|
|
269
|
+
for (const particle of this.particles) {
|
|
270
|
+
particle.update(CONFIG.attractor.dt, CONFIG.attractor.scale);
|
|
271
|
+
particle.updateBlink(dt);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
collectSegments(cx, cy) {
|
|
276
|
+
const { minHue, maxHue, maxSpeed, saturation, lightness, maxAlpha, hueShiftSpeed } =
|
|
277
|
+
CONFIG.visual;
|
|
278
|
+
const { intensityBoost, saturationBoost, alphaBoost } = CONFIG.blink;
|
|
279
|
+
const hueOffset = (this.time * hueShiftSpeed) % 360;
|
|
280
|
+
|
|
281
|
+
this.segments.length = 0;
|
|
282
|
+
|
|
283
|
+
for (const particle of this.particles) {
|
|
284
|
+
if (particle.trail.length < 2) continue;
|
|
285
|
+
|
|
286
|
+
const blink = particle.blinkIntensity;
|
|
287
|
+
|
|
288
|
+
for (let i = 1; i < particle.trail.length; i++) {
|
|
289
|
+
const curr = particle.trail[i];
|
|
290
|
+
const prev = particle.trail[i - 1];
|
|
291
|
+
|
|
292
|
+
const p1 = this.camera.project(prev.x, prev.y, prev.z);
|
|
293
|
+
const p2 = this.camera.project(curr.x, curr.y, curr.z);
|
|
294
|
+
|
|
295
|
+
if (p1.scale <= 0 || p2.scale <= 0) continue;
|
|
296
|
+
|
|
297
|
+
const age = i / particle.trail.length;
|
|
298
|
+
const speedNorm = Math.min(curr.speed / maxSpeed, 1);
|
|
299
|
+
const baseHue = maxHue - speedNorm * (maxHue - minHue);
|
|
300
|
+
const hue = (baseHue + hueOffset) % 360;
|
|
301
|
+
|
|
302
|
+
const sat = Math.min(100, saturation * (1 + blink * (saturationBoost - 1)));
|
|
303
|
+
const lit = Math.min(100, lightness * (1 + blink * (intensityBoost - 1)));
|
|
304
|
+
const rgb = hslToRgb(hue, sat, lit);
|
|
305
|
+
const alpha = Math.min(1, (1 - age) * maxAlpha * (1 + blink * (alphaBoost - 1)));
|
|
306
|
+
|
|
307
|
+
this.segments.push({
|
|
308
|
+
x1: cx + p1.x * this.zoom,
|
|
309
|
+
y1: cy + p1.y * this.zoom,
|
|
310
|
+
x2: cx + p2.x * this.zoom,
|
|
311
|
+
y2: cy + p2.y * this.zoom,
|
|
312
|
+
r: rgb.r,
|
|
313
|
+
g: rgb.g,
|
|
314
|
+
b: rgb.b,
|
|
315
|
+
a: alpha,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return this.segments.length;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
renderCanvas2D(cx, cy) {
|
|
324
|
+
const { minHue, maxHue, maxSpeed, saturation, lightness, maxAlpha, hueShiftSpeed } =
|
|
325
|
+
CONFIG.visual;
|
|
326
|
+
const { intensityBoost, saturationBoost, alphaBoost } = CONFIG.blink;
|
|
327
|
+
const hueOffset = (this.time * hueShiftSpeed) % 360;
|
|
328
|
+
|
|
329
|
+
const ctx = this.ctx;
|
|
330
|
+
ctx.save();
|
|
331
|
+
ctx.globalCompositeOperation = "lighter";
|
|
332
|
+
ctx.lineCap = "round";
|
|
333
|
+
|
|
334
|
+
for (const particle of this.particles) {
|
|
335
|
+
if (particle.trail.length < 2) continue;
|
|
336
|
+
|
|
337
|
+
const blink = particle.blinkIntensity;
|
|
338
|
+
|
|
339
|
+
for (let i = 1; i < particle.trail.length; i++) {
|
|
340
|
+
const curr = particle.trail[i];
|
|
341
|
+
const prev = particle.trail[i - 1];
|
|
342
|
+
|
|
343
|
+
const p1 = this.camera.project(prev.x, prev.y, prev.z);
|
|
344
|
+
const p2 = this.camera.project(curr.x, curr.y, curr.z);
|
|
345
|
+
|
|
346
|
+
if (p1.scale <= 0 || p2.scale <= 0) continue;
|
|
347
|
+
|
|
348
|
+
const age = i / particle.trail.length;
|
|
349
|
+
const speedNorm = Math.min(curr.speed / maxSpeed, 1);
|
|
350
|
+
const baseHue = maxHue - speedNorm * (maxHue - minHue);
|
|
351
|
+
const hue = (baseHue + hueOffset) % 360;
|
|
352
|
+
|
|
353
|
+
const sat = Math.min(100, saturation * (1 + blink * (saturationBoost - 1)));
|
|
354
|
+
const lit = Math.min(100, lightness * (1 + blink * (intensityBoost - 1)));
|
|
355
|
+
const alpha = Math.min(1, (1 - age) * maxAlpha * (1 + blink * (alphaBoost - 1)));
|
|
356
|
+
|
|
357
|
+
ctx.strokeStyle = `hsla(${hue}, ${sat}%, ${lit}%, ${alpha})`;
|
|
358
|
+
ctx.lineWidth = 1;
|
|
359
|
+
|
|
360
|
+
ctx.beginPath();
|
|
361
|
+
ctx.moveTo(cx + p1.x * this.zoom, cy + p1.y * this.zoom);
|
|
362
|
+
ctx.lineTo(cx + p2.x * this.zoom, cy + p2.y * this.zoom);
|
|
363
|
+
ctx.stroke();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
ctx.restore();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
render() {
|
|
371
|
+
super.render();
|
|
372
|
+
if (!this.particles) return;
|
|
373
|
+
|
|
374
|
+
const cx = this.width / 2;
|
|
375
|
+
const cy = this.height / 2;
|
|
376
|
+
|
|
377
|
+
if (this.useWebGL && this.lineRenderer.isAvailable()) {
|
|
378
|
+
const segmentCount = this.collectSegments(cx, cy);
|
|
379
|
+
if (segmentCount > 0) {
|
|
380
|
+
this.lineRenderer.clear();
|
|
381
|
+
this.lineRenderer.updateLines(this.segments);
|
|
382
|
+
this.lineRenderer.render(segmentCount);
|
|
383
|
+
this.lineRenderer.compositeOnto(this.ctx, 0, 0);
|
|
384
|
+
}
|
|
385
|
+
} else {
|
|
386
|
+
this.renderCanvas2D(cx, cy);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
destroy() {
|
|
391
|
+
this.gesture?.destroy();
|
|
392
|
+
this.lineRenderer?.destroy();
|
|
393
|
+
super.destroy?.();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
398
|
+
// INITIALIZATION
|
|
399
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
window.addEventListener("load", () => {
|
|
402
|
+
const canvas = document.getElementById("game");
|
|
403
|
+
const demo = new DadrasDemo(canvas);
|
|
404
|
+
demo.start();
|
|
405
|
+
});
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* De Jong Attractor 2D Visualization
|
|
3
|
+
*
|
|
4
|
+
* A 2D iterative attractor by Peter de Jong, similar to Clifford but
|
|
5
|
+
* creating different swirling patterns with its sin/cos structure.
|
|
6
|
+
*
|
|
7
|
+
* Engine-aligned procedural WebGL approach:
|
|
8
|
+
* - Seed buffer stays on the GPU
|
|
9
|
+
* - Vertex shader iterates the De Jong map (like the reference project)
|
|
10
|
+
* - Output is composited onto the main 2D canvas for easy trail accumulation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Game, Gesture, Screen, Attractors, Painter, WebGLDeJongRenderer } from "/gcanvas.es.min.js";
|
|
14
|
+
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
// CONFIGURATION
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const CONFIG = {
|
|
20
|
+
// Attractor settings
|
|
21
|
+
attractor: {
|
|
22
|
+
// GPU shader iterations per point
|
|
23
|
+
iterations: 100,
|
|
24
|
+
|
|
25
|
+
// Parameter animation (mirrors the reference project where 'a' drifts over time)
|
|
26
|
+
params: {
|
|
27
|
+
aBase: -2.0,
|
|
28
|
+
aWobble: 0.6,
|
|
29
|
+
aPeriodSeconds: 8.0, // seconds for one full wobble cycle
|
|
30
|
+
b: -2.0,
|
|
31
|
+
c: -1.2,
|
|
32
|
+
d: 2.0,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
// Procedural point settings
|
|
37
|
+
points: {
|
|
38
|
+
seedCount: 1 << 18, // 262144 (matches reference performance profile)
|
|
39
|
+
pointSize: 1.0,
|
|
40
|
+
pointScale: 0.5, // maps attractor space to clip space (reference uses 0.5)
|
|
41
|
+
shape: "glow", // 'circle' | 'glow' | 'square' | 'softSquare'
|
|
42
|
+
blendMode: "additive", // 'alpha' | 'additive' (WebGL)
|
|
43
|
+
compositeBlendMode: "lighter", // Canvas 2D blend for compositing
|
|
44
|
+
color: { r: 1, g: 1, b: 1, a: 0.12 }, // RGBA 0..1
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// Visual settings - warm palette for De Jong
|
|
48
|
+
visual: {
|
|
49
|
+
// Lorenz-style: map speed → hue (slow=cyan, fast=orange) + hue shifting
|
|
50
|
+
minHue: 30, // fast
|
|
51
|
+
maxHue: 200, // slow
|
|
52
|
+
maxSpeed: 0.8, // speed normalization threshold (tune per-attractor)
|
|
53
|
+
saturation: 85,
|
|
54
|
+
lightness: 55,
|
|
55
|
+
alpha: 0.14,
|
|
56
|
+
hueShiftSpeed: 15, // degrees per second
|
|
57
|
+
fadeSpeed: 0.02, // 0 = no fade (infinite trails), higher = faster fade
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
// Zoom settings
|
|
61
|
+
zoom: {
|
|
62
|
+
min: 0.3,
|
|
63
|
+
max: 3.0,
|
|
64
|
+
speed: 0.5,
|
|
65
|
+
easing: 0.12,
|
|
66
|
+
baseScreenSize: 600,
|
|
67
|
+
initialMultiplier: 0.75, // lower = zoom out more initially
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
// Rotation settings (drag to rotate)
|
|
71
|
+
rotation: {
|
|
72
|
+
speed: 0.01, // radians per pixel
|
|
73
|
+
easing: 0.15,
|
|
74
|
+
autoSpeed: 0.18, // radians/sec (continuous rotation)
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
79
|
+
// DEMO CLASS
|
|
80
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
class DeJongDemo extends Game {
|
|
83
|
+
constructor(canvas) {
|
|
84
|
+
super(canvas);
|
|
85
|
+
this.backgroundColor = "#000";
|
|
86
|
+
this.enableFluidSize();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
init() {
|
|
90
|
+
super.init();
|
|
91
|
+
|
|
92
|
+
this.attractor = Attractors.deJong;
|
|
93
|
+
console.log(`Attractor: ${this.attractor.name}`);
|
|
94
|
+
console.log(`Equations:`, this.attractor.equations);
|
|
95
|
+
|
|
96
|
+
const { min, max, baseScreenSize } = CONFIG.zoom;
|
|
97
|
+
const initialZoomRaw = Screen.minDimension() / baseScreenSize;
|
|
98
|
+
const initialZoom = Math.min(
|
|
99
|
+
max,
|
|
100
|
+
Math.max(min, initialZoomRaw * CONFIG.zoom.initialMultiplier)
|
|
101
|
+
);
|
|
102
|
+
this.zoom = initialZoom;
|
|
103
|
+
this.targetZoom = initialZoom;
|
|
104
|
+
this.defaultZoom = initialZoom;
|
|
105
|
+
|
|
106
|
+
// Continuous auto-rotation + user-controlled offset (drag)
|
|
107
|
+
this.baseRotation = 0;
|
|
108
|
+
this.userRotation = 0;
|
|
109
|
+
this.targetUserRotation = 0;
|
|
110
|
+
|
|
111
|
+
// Gesture handler for zoom + rotation
|
|
112
|
+
this.gesture = new Gesture(this.canvas, {
|
|
113
|
+
onZoom: (delta) => {
|
|
114
|
+
this.targetZoom *= 1 + delta * CONFIG.zoom.speed;
|
|
115
|
+
},
|
|
116
|
+
onPan: (dx) => {
|
|
117
|
+
this.targetUserRotation += dx * CONFIG.rotation.speed;
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Double-click to reset
|
|
122
|
+
this.canvas.addEventListener("dblclick", () => {
|
|
123
|
+
this.targetZoom = this.defaultZoom;
|
|
124
|
+
this.baseRotation = 0;
|
|
125
|
+
this.targetUserRotation = 0;
|
|
126
|
+
this._didFirstClear = false;
|
|
127
|
+
this.renderer?.regenerateSeeds();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
this.time = 0;
|
|
131
|
+
|
|
132
|
+
// Fade-clear state (first frame fills solid black)
|
|
133
|
+
this._didFirstClear = false;
|
|
134
|
+
|
|
135
|
+
this.renderer = new WebGLDeJongRenderer(CONFIG.points.seedCount, {
|
|
136
|
+
width: this.width,
|
|
137
|
+
height: this.height,
|
|
138
|
+
shape: CONFIG.points.shape,
|
|
139
|
+
blendMode: CONFIG.points.blendMode,
|
|
140
|
+
pointSize: CONFIG.points.pointSize,
|
|
141
|
+
pointScale: CONFIG.points.pointScale,
|
|
142
|
+
iterations: CONFIG.attractor.iterations,
|
|
143
|
+
color: CONFIG.points.color,
|
|
144
|
+
colorMode: 1,
|
|
145
|
+
hueRange: { minHue: CONFIG.visual.minHue, maxHue: CONFIG.visual.maxHue },
|
|
146
|
+
maxSpeed: CONFIG.visual.maxSpeed,
|
|
147
|
+
saturation: CONFIG.visual.saturation / 100,
|
|
148
|
+
lightness: CONFIG.visual.lightness / 100,
|
|
149
|
+
alpha: CONFIG.visual.alpha,
|
|
150
|
+
hueShiftSpeed: CONFIG.visual.hueShiftSpeed,
|
|
151
|
+
params: {
|
|
152
|
+
a: CONFIG.attractor.params.aBase,
|
|
153
|
+
b: CONFIG.attractor.params.b,
|
|
154
|
+
c: CONFIG.attractor.params.c,
|
|
155
|
+
d: CONFIG.attractor.params.d,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (!this.renderer.isAvailable()) {
|
|
160
|
+
console.warn("WebGL not available for DeJong demo");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
onResize() {
|
|
165
|
+
const { min, max, baseScreenSize } = CONFIG.zoom;
|
|
166
|
+
const initialZoomRaw = Screen.minDimension() / baseScreenSize;
|
|
167
|
+
this.defaultZoom = Math.min(
|
|
168
|
+
max,
|
|
169
|
+
Math.max(min, initialZoomRaw * CONFIG.zoom.initialMultiplier)
|
|
170
|
+
);
|
|
171
|
+
this._didFirstClear = false;
|
|
172
|
+
this.renderer?.resize(this.width, this.height);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
update(dt) {
|
|
176
|
+
this.zoom += (this.targetZoom - this.zoom) * CONFIG.zoom.easing;
|
|
177
|
+
this.time += dt;
|
|
178
|
+
|
|
179
|
+
// Continuous rotation (never reverses unless user drags against it)
|
|
180
|
+
const TAU = Math.PI * 2;
|
|
181
|
+
this.baseRotation = (this.baseRotation + CONFIG.rotation.autoSpeed * dt) % TAU;
|
|
182
|
+
|
|
183
|
+
// Smooth user rotation offset
|
|
184
|
+
this.userRotation +=
|
|
185
|
+
(this.targetUserRotation - this.userRotation) * CONFIG.rotation.easing;
|
|
186
|
+
super.update(dt);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
clear() {
|
|
190
|
+
// Fade the canvas to create persistent trails.
|
|
191
|
+
// Use a solid first clear to avoid a "transparent start" look.
|
|
192
|
+
if (!this._didFirstClear) {
|
|
193
|
+
Painter.useCtx((ctx) => {
|
|
194
|
+
ctx.fillStyle = "#000";
|
|
195
|
+
ctx.fillRect(0, 0, this.width, this.height);
|
|
196
|
+
});
|
|
197
|
+
this._didFirstClear = true;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const fade = CONFIG.visual.fadeSpeed;
|
|
202
|
+
if (fade <= 0) return;
|
|
203
|
+
|
|
204
|
+
Painter.useCtx((ctx) => {
|
|
205
|
+
ctx.globalCompositeOperation = "source-over";
|
|
206
|
+
ctx.fillStyle = `rgba(0, 0, 0, ${fade})`;
|
|
207
|
+
ctx.fillRect(0, 0, this.width, this.height);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
render() {
|
|
212
|
+
// Custom render: fade-clear (via clear()), then composite WebGL output.
|
|
213
|
+
Painter.setContext(this.ctx);
|
|
214
|
+
if (this.running) this.clear();
|
|
215
|
+
|
|
216
|
+
if (this.renderer?.isAvailable()) {
|
|
217
|
+
// Animate params like the reference (a wobbles over time)
|
|
218
|
+
const p = CONFIG.attractor.params;
|
|
219
|
+
const omega = (2 * Math.PI) / Math.max(0.001, p.aPeriodSeconds);
|
|
220
|
+
const a = p.aBase + Math.sin(this.time * omega) * p.aWobble;
|
|
221
|
+
|
|
222
|
+
this.renderer.setParams({ a, b: p.b, c: p.c, d: p.d });
|
|
223
|
+
this.renderer.setIterations(CONFIG.attractor.iterations);
|
|
224
|
+
this.renderer.setZoom(this.zoom);
|
|
225
|
+
this.renderer.setTransform(
|
|
226
|
+
WebGLDeJongRenderer.rotationMat3(this.baseRotation + this.userRotation)
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// Draw points to the offscreen WebGL canvas, then composite
|
|
230
|
+
this.renderer.clear(0, 0, 0, 0);
|
|
231
|
+
this.renderer.render(this.time);
|
|
232
|
+
|
|
233
|
+
Painter.useCtx((ctx) => {
|
|
234
|
+
const prev = ctx.globalCompositeOperation;
|
|
235
|
+
ctx.globalCompositeOperation = CONFIG.points.compositeBlendMode;
|
|
236
|
+
this.renderer.compositeOnto(ctx, 0, 0);
|
|
237
|
+
ctx.globalCompositeOperation = prev;
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
destroy() {
|
|
243
|
+
this.gesture?.destroy();
|
|
244
|
+
this.renderer?.destroy();
|
|
245
|
+
super.destroy?.();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
250
|
+
// INITIALIZATION
|
|
251
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
window.addEventListener("load", () => {
|
|
254
|
+
const canvas = document.getElementById("game");
|
|
255
|
+
const demo = new DeJongDemo(canvas);
|
|
256
|
+
demo.start();
|
|
257
|
+
});
|