@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.
Files changed (78) hide show
  1. package/dist/aizawa.html +27 -0
  2. package/dist/clifford.html +25 -0
  3. package/dist/cmb.html +24 -0
  4. package/dist/dadras.html +26 -0
  5. package/dist/dejong.html +25 -0
  6. package/dist/gcanvas.es.js +5130 -372
  7. package/dist/gcanvas.es.min.js +1 -1
  8. package/dist/gcanvas.umd.js +1 -1
  9. package/dist/gcanvas.umd.min.js +1 -1
  10. package/dist/halvorsen.html +27 -0
  11. package/dist/index.html +96 -48
  12. package/dist/js/aizawa.js +425 -0
  13. package/dist/js/bezier.js +5 -5
  14. package/dist/js/clifford.js +236 -0
  15. package/dist/js/cmb.js +594 -0
  16. package/dist/js/dadras.js +405 -0
  17. package/dist/js/dejong.js +257 -0
  18. package/dist/js/halvorsen.js +405 -0
  19. package/dist/js/isometric.js +34 -46
  20. package/dist/js/lorenz.js +425 -0
  21. package/dist/js/painter.js +8 -8
  22. package/dist/js/rossler.js +480 -0
  23. package/dist/js/schrodinger.js +314 -18
  24. package/dist/js/thomas.js +394 -0
  25. package/dist/lorenz.html +27 -0
  26. package/dist/rossler.html +27 -0
  27. package/dist/scene-interactivity-test.html +220 -0
  28. package/dist/thomas.html +27 -0
  29. package/package.json +1 -1
  30. package/readme.md +30 -22
  31. package/src/game/objects/go.js +7 -0
  32. package/src/game/objects/index.js +2 -0
  33. package/src/game/objects/isometric-scene.js +53 -3
  34. package/src/game/objects/layoutscene.js +57 -0
  35. package/src/game/objects/mask.js +241 -0
  36. package/src/game/objects/scene.js +19 -0
  37. package/src/game/objects/wrapper.js +14 -2
  38. package/src/game/pipeline.js +17 -0
  39. package/src/game/ui/button.js +101 -16
  40. package/src/game/ui/theme.js +0 -6
  41. package/src/game/ui/togglebutton.js +25 -14
  42. package/src/game/ui/tooltip.js +12 -4
  43. package/src/index.js +3 -0
  44. package/src/io/gesture.js +409 -0
  45. package/src/io/index.js +4 -1
  46. package/src/io/keys.js +9 -1
  47. package/src/io/screen.js +476 -0
  48. package/src/math/attractors.js +664 -0
  49. package/src/math/heat.js +106 -0
  50. package/src/math/index.js +1 -0
  51. package/src/mixins/draggable.js +15 -19
  52. package/src/painter/painter.shapes.js +11 -5
  53. package/src/particle/particle-system.js +165 -1
  54. package/src/physics/index.js +26 -0
  55. package/src/physics/physics-updaters.js +333 -0
  56. package/src/physics/physics.js +375 -0
  57. package/src/shapes/image.js +5 -5
  58. package/src/shapes/index.js +2 -0
  59. package/src/shapes/parallelogram.js +147 -0
  60. package/src/shapes/righttriangle.js +115 -0
  61. package/src/shapes/svg.js +281 -100
  62. package/src/shapes/text.js +22 -6
  63. package/src/shapes/transformable.js +5 -0
  64. package/src/sound/effects.js +807 -0
  65. package/src/sound/index.js +13 -0
  66. package/src/webgl/index.js +7 -0
  67. package/src/webgl/shaders/clifford-point-shaders.js +131 -0
  68. package/src/webgl/shaders/dejong-point-shaders.js +131 -0
  69. package/src/webgl/shaders/point-sprite-shaders.js +152 -0
  70. package/src/webgl/webgl-clifford-renderer.js +477 -0
  71. package/src/webgl/webgl-dejong-renderer.js +472 -0
  72. package/src/webgl/webgl-line-renderer.js +391 -0
  73. package/src/webgl/webgl-particle-renderer.js +410 -0
  74. package/types/index.d.ts +30 -2
  75. package/types/io.d.ts +217 -0
  76. package/types/physics.d.ts +299 -0
  77. package/types/shapes.d.ts +8 -0
  78. package/types/webgl.d.ts +188 -109
@@ -0,0 +1,394 @@
1
+ /**
2
+ * Thomas Attractor 3D Visualization
3
+ *
4
+ * Thomas' Cyclically Symmetric Attractor (1999) discovered by René Thomas.
5
+ * Features elegant symmetry and smooth cyclical motion with a simple
6
+ * sinusoidal structure.
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.thomas for equations)
22
+ attractor: {
23
+ dt: 0.08, // Thomas needs larger dt
24
+ scale: 60, // Scale factor for display
25
+ },
26
+
27
+ // Particle settings
28
+ particles: {
29
+ count: 300,
30
+ trailLength: 300,
31
+ spawnRange: 2, // Initial position range
32
+ },
33
+
34
+ // Center offset - adjust to match attractor's visual barycenter
35
+ center: {
36
+ x: -0.2,
37
+ y: -0.2,
38
+ z: 0,
39
+ },
40
+
41
+ // Camera settings
42
+ camera: {
43
+ perspective: 800,
44
+ rotationX: 0.3,
45
+ rotationY: 0.2,
46
+ inertia: true,
47
+ friction: 0.95,
48
+ clampX: false,
49
+ },
50
+
51
+ // Visual settings - green/teal palette for Thomas
52
+ visual: {
53
+ minHue: 120, // Green (fast)
54
+ maxHue: 200, // Cyan-blue (slow)
55
+ maxSpeed: 2.5, // Thomas is slow-moving
56
+ saturation: 85,
57
+ lightness: 50,
58
+ maxAlpha: 0.8,
59
+ hueShiftSpeed: 8,
60
+ },
61
+
62
+ // Glitch/blink effect
63
+ blink: {
64
+ chance: 0.012,
65
+ minDuration: 0.06,
66
+ maxDuration: 0.25,
67
+ intensityBoost: 1.4,
68
+ saturationBoost: 1.15,
69
+ alphaBoost: 1.2,
70
+ },
71
+
72
+ // Zoom settings
73
+ zoom: {
74
+ min: 0.3,
75
+ max: 3.0,
76
+ speed: 0.5,
77
+ easing: 0.12,
78
+ baseScreenSize: 600,
79
+ },
80
+ };
81
+
82
+ // ─────────────────────────────────────────────────────────────────────────────
83
+ // HELPER FUNCTIONS
84
+ // ─────────────────────────────────────────────────────────────────────────────
85
+
86
+ function hslToRgb(h, s, l) {
87
+ s /= 100;
88
+ l /= 100;
89
+ const k = (n) => (n + h / 30) % 12;
90
+ const a = s * Math.min(l, 1 - l);
91
+ const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
92
+ return {
93
+ r: Math.round(255 * f(0)),
94
+ g: Math.round(255 * f(8)),
95
+ b: Math.round(255 * f(4)),
96
+ };
97
+ }
98
+
99
+ // ─────────────────────────────────────────────────────────────────────────────
100
+ // ATTRACTOR PARTICLE
101
+ // ─────────────────────────────────────────────────────────────────────────────
102
+
103
+ class AttractorParticle {
104
+ constructor(stepFn, spawnRange) {
105
+ this.stepFn = stepFn;
106
+ this.position = {
107
+ x: (Math.random() - 0.5) * spawnRange,
108
+ y: (Math.random() - 0.5) * spawnRange,
109
+ z: (Math.random() - 0.5) * spawnRange,
110
+ };
111
+ this.trail = [];
112
+ this.speed = 0;
113
+ this.blinkTime = 0;
114
+ this.blinkIntensity = 0;
115
+ }
116
+
117
+ updateBlink(dt) {
118
+ const { chance, minDuration, maxDuration } = CONFIG.blink;
119
+
120
+ if (this.blinkTime > 0) {
121
+ this.blinkTime -= dt;
122
+ this.blinkIntensity = Math.max(
123
+ 0,
124
+ this.blinkTime > 0
125
+ ? Math.sin((this.blinkTime / ((minDuration + maxDuration) * 0.5)) * Math.PI)
126
+ : 0
127
+ );
128
+ } else {
129
+ if (Math.random() < chance) {
130
+ this.blinkTime = minDuration + Math.random() * (maxDuration - minDuration);
131
+ this.blinkIntensity = 1;
132
+ } else {
133
+ this.blinkIntensity = 0;
134
+ }
135
+ }
136
+ }
137
+
138
+ update(dt, scale) {
139
+ const result = this.stepFn(this.position, dt);
140
+ this.position = result.position;
141
+ this.speed = result.speed;
142
+
143
+ // Add to trail (centered and scaled for display)
144
+ this.trail.unshift({
145
+ x: (this.position.x - CONFIG.center.x) * scale,
146
+ y: (this.position.y - CONFIG.center.y) * scale,
147
+ z: (this.position.z - CONFIG.center.z) * scale,
148
+ speed: this.speed,
149
+ });
150
+
151
+ if (this.trail.length > CONFIG.particles.trailLength) {
152
+ this.trail.pop();
153
+ }
154
+ }
155
+ }
156
+
157
+ // ─────────────────────────────────────────────────────────────────────────────
158
+ // DEMO CLASS
159
+ // ─────────────────────────────────────────────────────────────────────────────
160
+
161
+ class ThomasDemo extends Game {
162
+ constructor(canvas) {
163
+ super(canvas);
164
+ this.backgroundColor = "#000";
165
+ this.enableFluidSize();
166
+ }
167
+
168
+ init() {
169
+ super.init();
170
+
171
+ this.attractor = Attractors.thomas;
172
+ console.log(`Attractor: ${this.attractor.name}`);
173
+ console.log(`Equations:`, this.attractor.equations);
174
+
175
+ this.stepFn = this.attractor.createStepper();
176
+
177
+ const { min, max, baseScreenSize } = CONFIG.zoom;
178
+ const initialZoom = Math.min(max, Math.max(min, Screen.minDimension() / baseScreenSize));
179
+ this.zoom = initialZoom;
180
+ this.targetZoom = initialZoom;
181
+ this.defaultZoom = initialZoom;
182
+
183
+ this.camera = new Camera3D({
184
+ perspective: CONFIG.camera.perspective,
185
+ rotationX: CONFIG.camera.rotationX,
186
+ rotationY: CONFIG.camera.rotationY,
187
+ inertia: CONFIG.camera.inertia,
188
+ friction: CONFIG.camera.friction,
189
+ clampX: CONFIG.camera.clampX,
190
+ });
191
+ this.camera.enableMouseControl(this.canvas);
192
+
193
+ this.gesture = new Gesture(this.canvas, {
194
+ onZoom: (delta) => {
195
+ this.targetZoom *= 1 + delta * CONFIG.zoom.speed;
196
+ },
197
+ onPan: null,
198
+ });
199
+
200
+ this.canvas.addEventListener("dblclick", () => {
201
+ this.targetZoom = this.defaultZoom;
202
+ });
203
+
204
+ // Log camera params and barycenter on mouse release
205
+ this.canvas.addEventListener("mouseup", () => {
206
+ console.log(`Camera: rotationX: ${this.camera.rotationX.toFixed(3)}, rotationY: ${this.camera.rotationY.toFixed(3)}`);
207
+ let sumX = 0, sumY = 0, sumZ = 0, count = 0;
208
+ for (const p of this.particles) {
209
+ sumX += p.position.x;
210
+ sumY += p.position.y;
211
+ sumZ += p.position.z;
212
+ count++;
213
+ }
214
+ console.log(`Barycenter: x: ${(sumX/count).toFixed(3)}, y: ${(sumY/count).toFixed(3)}, z: ${(sumZ/count).toFixed(3)}`);
215
+ });
216
+
217
+ this.particles = [];
218
+ for (let i = 0; i < CONFIG.particles.count; i++) {
219
+ this.particles.push(
220
+ new AttractorParticle(this.stepFn, CONFIG.particles.spawnRange)
221
+ );
222
+ }
223
+
224
+ const maxSegments = CONFIG.particles.count * CONFIG.particles.trailLength;
225
+ this.lineRenderer = new WebGLLineRenderer(maxSegments, {
226
+ width: this.width,
227
+ height: this.height,
228
+ blendMode: "additive",
229
+ });
230
+
231
+ this.segments = [];
232
+
233
+ if (!this.lineRenderer.isAvailable()) {
234
+ console.warn("WebGL not available, falling back to Canvas 2D");
235
+ this.useWebGL = false;
236
+ } else {
237
+ this.useWebGL = true;
238
+ console.log(`WebGL enabled, ${maxSegments} max segments`);
239
+ }
240
+
241
+ this.time = 0;
242
+ }
243
+
244
+ onResize() {
245
+ if (this.lineRenderer?.isAvailable()) {
246
+ this.lineRenderer.resize(this.width, this.height);
247
+ }
248
+ const { min, max, baseScreenSize } = CONFIG.zoom;
249
+ this.defaultZoom = Math.min(max, Math.max(min, Screen.minDimension() / baseScreenSize));
250
+ }
251
+
252
+ update(dt) {
253
+ super.update(dt);
254
+ this.camera.update(dt);
255
+ this.zoom += (this.targetZoom - this.zoom) * CONFIG.zoom.easing;
256
+ this.time += dt;
257
+
258
+ for (const particle of this.particles) {
259
+ particle.update(CONFIG.attractor.dt, CONFIG.attractor.scale);
260
+ particle.updateBlink(dt);
261
+ }
262
+ }
263
+
264
+ collectSegments(cx, cy) {
265
+ const { minHue, maxHue, maxSpeed, saturation, lightness, maxAlpha, hueShiftSpeed } =
266
+ CONFIG.visual;
267
+ const { intensityBoost, saturationBoost, alphaBoost } = CONFIG.blink;
268
+ const hueOffset = (this.time * hueShiftSpeed) % 360;
269
+
270
+ this.segments.length = 0;
271
+
272
+ for (const particle of this.particles) {
273
+ if (particle.trail.length < 2) continue;
274
+
275
+ const blink = particle.blinkIntensity;
276
+
277
+ for (let i = 1; i < particle.trail.length; i++) {
278
+ const curr = particle.trail[i];
279
+ const prev = particle.trail[i - 1];
280
+
281
+ const p1 = this.camera.project(prev.x, prev.y, prev.z);
282
+ const p2 = this.camera.project(curr.x, curr.y, curr.z);
283
+
284
+ if (p1.scale <= 0 || p2.scale <= 0) continue;
285
+
286
+ const age = i / particle.trail.length;
287
+ const speedNorm = Math.min(curr.speed / maxSpeed, 1);
288
+ const baseHue = maxHue - speedNorm * (maxHue - minHue);
289
+ const hue = (baseHue + hueOffset) % 360;
290
+
291
+ const sat = Math.min(100, saturation * (1 + blink * (saturationBoost - 1)));
292
+ const lit = Math.min(100, lightness * (1 + blink * (intensityBoost - 1)));
293
+ const rgb = hslToRgb(hue, sat, lit);
294
+ const alpha = Math.min(1, (1 - age) * maxAlpha * (1 + blink * (alphaBoost - 1)));
295
+
296
+ this.segments.push({
297
+ x1: cx + p1.x * this.zoom,
298
+ y1: cy + p1.y * this.zoom,
299
+ x2: cx + p2.x * this.zoom,
300
+ y2: cy + p2.y * this.zoom,
301
+ r: rgb.r,
302
+ g: rgb.g,
303
+ b: rgb.b,
304
+ a: alpha,
305
+ });
306
+ }
307
+ }
308
+
309
+ return this.segments.length;
310
+ }
311
+
312
+ renderCanvas2D(cx, cy) {
313
+ const { minHue, maxHue, maxSpeed, saturation, lightness, maxAlpha, hueShiftSpeed } =
314
+ CONFIG.visual;
315
+ const { intensityBoost, saturationBoost, alphaBoost } = CONFIG.blink;
316
+ const hueOffset = (this.time * hueShiftSpeed) % 360;
317
+
318
+ const ctx = this.ctx;
319
+ ctx.save();
320
+ ctx.globalCompositeOperation = "lighter";
321
+ ctx.lineCap = "round";
322
+
323
+ for (const particle of this.particles) {
324
+ if (particle.trail.length < 2) continue;
325
+
326
+ const blink = particle.blinkIntensity;
327
+
328
+ for (let i = 1; i < particle.trail.length; i++) {
329
+ const curr = particle.trail[i];
330
+ const prev = particle.trail[i - 1];
331
+
332
+ const p1 = this.camera.project(prev.x, prev.y, prev.z);
333
+ const p2 = this.camera.project(curr.x, curr.y, curr.z);
334
+
335
+ if (p1.scale <= 0 || p2.scale <= 0) continue;
336
+
337
+ const age = i / particle.trail.length;
338
+ const speedNorm = Math.min(curr.speed / maxSpeed, 1);
339
+ const baseHue = maxHue - speedNorm * (maxHue - minHue);
340
+ const hue = (baseHue + hueOffset) % 360;
341
+
342
+ const sat = Math.min(100, saturation * (1 + blink * (saturationBoost - 1)));
343
+ const lit = Math.min(100, lightness * (1 + blink * (intensityBoost - 1)));
344
+ const alpha = Math.min(1, (1 - age) * maxAlpha * (1 + blink * (alphaBoost - 1)));
345
+
346
+ ctx.strokeStyle = `hsla(${hue}, ${sat}%, ${lit}%, ${alpha})`;
347
+ ctx.lineWidth = 1;
348
+
349
+ ctx.beginPath();
350
+ ctx.moveTo(cx + p1.x * this.zoom, cy + p1.y * this.zoom);
351
+ ctx.lineTo(cx + p2.x * this.zoom, cy + p2.y * this.zoom);
352
+ ctx.stroke();
353
+ }
354
+ }
355
+
356
+ ctx.restore();
357
+ }
358
+
359
+ render() {
360
+ super.render();
361
+ if (!this.particles) return;
362
+
363
+ const cx = this.width / 2;
364
+ const cy = this.height / 2;
365
+
366
+ if (this.useWebGL && this.lineRenderer.isAvailable()) {
367
+ const segmentCount = this.collectSegments(cx, cy);
368
+ if (segmentCount > 0) {
369
+ this.lineRenderer.clear();
370
+ this.lineRenderer.updateLines(this.segments);
371
+ this.lineRenderer.render(segmentCount);
372
+ this.lineRenderer.compositeOnto(this.ctx, 0, 0);
373
+ }
374
+ } else {
375
+ this.renderCanvas2D(cx, cy);
376
+ }
377
+ }
378
+
379
+ destroy() {
380
+ this.gesture?.destroy();
381
+ this.lineRenderer?.destroy();
382
+ super.destroy?.();
383
+ }
384
+ }
385
+
386
+ // ─────────────────────────────────────────────────────────────────────────────
387
+ // INITIALIZATION
388
+ // ─────────────────────────────────────────────────────────────────────────────
389
+
390
+ window.addEventListener("load", () => {
391
+ const canvas = document.getElementById("game");
392
+ const demo = new ThomasDemo(canvas);
393
+ demo.start();
394
+ });
@@ -0,0 +1,27 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Lorenz Attractor 3D</title>
7
+ <link rel="stylesheet" href="demos.css" />
8
+ <script src="./js/info-toggle.js"></script>
9
+ </head>
10
+ <body>
11
+ <div id="info">
12
+ <strong>Lorenz Attractor</strong> — The Butterfly Effect (1963)<br/>
13
+ <span style="color:#CCC">
14
+ <li>dx/dt = σ(y - x)</li>
15
+ <li>dy/dt = x(ρ - z) - y</li>
16
+ <li>dz/dt = xy - βz</li>
17
+ <li>σ=10, ρ=28, β=8/3</li>
18
+ <li>Blue = slow, Red = fast</li>
19
+ <li>Drag to rotate</li>
20
+ <li>Scroll/pinch to zoom</li>
21
+ <li>Double-click to reset</li>
22
+ </span>
23
+ </div>
24
+ <canvas id="game"></canvas>
25
+ <script type="module" src="./js/lorenz.js"></script>
26
+ </body>
27
+ </html>
@@ -0,0 +1,27 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Rössler Attractor 3D</title>
7
+ <link rel="stylesheet" href="demos.css" />
8
+ <script src="./js/info-toggle.js"></script>
9
+ </head>
10
+ <body>
11
+ <div id="info">
12
+ <strong>Rössler Attractor</strong> (1976)<br/>
13
+ <span style="color:#CCC">
14
+ <li>dx/dt = -y - z</li>
15
+ <li>dy/dt = x + ay</li>
16
+ <li>dz/dt = b + z(x - c)</li>
17
+ <li>a=0.2, b=0.2, c=5.7</li>
18
+ <li>Simple spiral with fold-back</li>
19
+ <li>Purple = slow, Orange = fast</li>
20
+ <li>Drag to rotate</li>
21
+ <li>Scroll/pinch to zoom</li>
22
+ </span>
23
+ </div>
24
+ <canvas id="game"></canvas>
25
+ <script type="module" src="./js/rossler.js"></script>
26
+ </body>
27
+ </html>
@@ -0,0 +1,220 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Scene Interactivity Test</title>
7
+ <link rel="stylesheet" href="demos.css" />
8
+ <script src="./js/info-toggle.js"></script>
9
+ </head>
10
+
11
+ <body>
12
+ <div id="info">
13
+ <strong>Scene Interactivity Test</strong> — Verifies that
14
+ GameObjects inside Scenes receive input events correctly.<br />
15
+ <span style="color: #ccc">
16
+ <li>Click on any colored box to see event fired</li>
17
+ <li>Hover over boxes to see hover state change</li>
18
+ <li>Nested scenes work too!</li>
19
+ </span>
20
+ </div>
21
+ <canvas id="game"></canvas>
22
+
23
+ <script type="module">
24
+ import {
25
+ Game,
26
+ Scene,
27
+ GameObject,
28
+ Rectangle,
29
+ TextShape,
30
+ Text,
31
+ FPSCounter,
32
+ } from "/gcanvas.es.min.js";
33
+
34
+ class ClickableBox extends GameObject {
35
+ constructor(game, color, label) {
36
+ super(game, { width: 150, height: 100 });
37
+
38
+ // Enable interactivity
39
+ this.interactive = true;
40
+
41
+ this.color = color;
42
+ this.defaultColor = color;
43
+ this.clickCount = 0;
44
+
45
+ // Create shapes
46
+ this.box = new Rectangle({
47
+ width: 150,
48
+ height: 100,
49
+ color: this.color,
50
+ strokeColor: "#fff",
51
+ lineWidth: 2,
52
+ });
53
+
54
+ this.label = new TextShape(label, {
55
+ font: "bold 14px monospace",
56
+ color: "#fff",
57
+ align: "center",
58
+ baseline: "top",
59
+ });
60
+
61
+ this.counter = new TextShape("Clicks: 0", {
62
+ font: "12px monospace",
63
+ color: "#fff",
64
+ align: "center",
65
+ baseline: "bottom",
66
+ });
67
+
68
+ // Setup events
69
+ this.on("inputdown", (e) => {
70
+ this.clickCount++;
71
+ this.counter.text = `Clicks: ${this.clickCount}`;
72
+ console.log(
73
+ `${label} clicked! Total: ${this.clickCount}`,
74
+ );
75
+
76
+ // Flash effect
77
+ this.box.color = "#fff";
78
+ setTimeout(() => {
79
+ this.box.color = this.hovered
80
+ ? this.brighten(this.defaultColor)
81
+ : this.defaultColor;
82
+ }, 100);
83
+ });
84
+
85
+ this.on("mouseover", () => {
86
+ this.hovered = true;
87
+ this.box.color = this.brighten(this.defaultColor);
88
+ this.box.lineWidth = 3;
89
+ });
90
+
91
+ this.on("mouseout", () => {
92
+ this.hovered = false;
93
+ this.box.color = this.defaultColor;
94
+ this.box.lineWidth = 2;
95
+ });
96
+ }
97
+
98
+ brighten(color) {
99
+ // Simple color brightening
100
+ const hex = color.replace("#", "");
101
+ const r = Math.min(
102
+ 255,
103
+ parseInt(hex.substr(0, 2), 16) + 40,
104
+ );
105
+ const g = Math.min(
106
+ 255,
107
+ parseInt(hex.substr(2, 2), 16) + 40,
108
+ );
109
+ const b = Math.min(
110
+ 255,
111
+ parseInt(hex.substr(4, 2), 16) + 40,
112
+ );
113
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
114
+ }
115
+
116
+ draw() {
117
+ super.draw();
118
+
119
+ // Draw box
120
+ this.box.x = 0;
121
+ this.box.y = 0;
122
+ this.box.render();
123
+
124
+ // Draw label
125
+ this.label.x = 0;
126
+ this.label.y = -30;
127
+ this.label.render();
128
+
129
+ // Draw counter
130
+ this.counter.x = 0;
131
+ this.counter.y = 30;
132
+ this.counter.render();
133
+ }
134
+ }
135
+
136
+ class SceneInteractivityTest extends Game {
137
+ constructor(canvas) {
138
+ super(canvas);
139
+ this.backgroundColor = "#000";
140
+ this.enableFluidSize();
141
+ }
142
+
143
+ init() {
144
+ super.init();
145
+
146
+ // Create main scene
147
+ const mainScene = new Scene(this, { name: "MainScene" });
148
+ mainScene.x = this.width / 2;
149
+ mainScene.y = this.height / 2 - 100;
150
+
151
+ // Add boxes directly to main scene
152
+ const box1 = new ClickableBox(this, "#e74c3c", "Box 1");
153
+ box1.x = -200;
154
+ box1.y = -80;
155
+ mainScene.add(box1);
156
+
157
+ const box2 = new ClickableBox(this, "#3498db", "Box 2");
158
+ box2.x = 0;
159
+ box2.y = -80;
160
+ mainScene.add(box2);
161
+
162
+ const box3 = new ClickableBox(this, "#2ecc71", "Box 3");
163
+ box3.x = 200;
164
+ box3.y = -80;
165
+ mainScene.add(box3);
166
+
167
+ // Create nested scene
168
+ const nestedScene = new Scene(this, {
169
+ name: "NestedScene",
170
+ });
171
+ nestedScene.y = 120;
172
+
173
+ // Add boxes to nested scene
174
+ const nestedBox1 = new ClickableBox(
175
+ this,
176
+ "#9b59b6",
177
+ "Nested 1",
178
+ );
179
+ nestedBox1.x = -100;
180
+ nestedScene.add(nestedBox1);
181
+
182
+ const nestedBox2 = new ClickableBox(
183
+ this,
184
+ "#f39c12",
185
+ "Nested 2",
186
+ );
187
+ nestedBox2.x = 100;
188
+ nestedScene.add(nestedBox2);
189
+
190
+ // Add nested scene to main scene
191
+ mainScene.add(nestedScene);
192
+
193
+ // Add main scene to pipeline
194
+ this.pipeline.add(mainScene);
195
+
196
+ // Add FPS counter
197
+ this.pipeline.add(
198
+ new FPSCounter(this, { anchor: "bottom-right" }),
199
+ );
200
+
201
+ this.mainScene = mainScene;
202
+ }
203
+
204
+ update(dt) {
205
+ super.update(dt);
206
+
207
+ // Keep scene centered
208
+ this.mainScene.x = this.width / 2;
209
+ this.mainScene.y = this.height / 2 - 50;
210
+ }
211
+ }
212
+
213
+ window.addEventListener("load", () => {
214
+ const canvas = document.getElementById("game");
215
+ const demo = new SceneInteractivityTest(canvas);
216
+ demo.start();
217
+ });
218
+ </script>
219
+ </body>
220
+ </html>
@@ -0,0 +1,27 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Thomas Attractor 3D</title>
7
+ <link rel="stylesheet" href="demos.css" />
8
+ <script src="./js/info-toggle.js"></script>
9
+ </head>
10
+ <body>
11
+ <div id="info">
12
+ <strong>Thomas' Cyclically Symmetric Attractor</strong> (1999)<br/>
13
+ <span style="color:#CCC">
14
+ <li>dx/dt = sin(y) - bx</li>
15
+ <li>dy/dt = sin(z) - by</li>
16
+ <li>dz/dt = sin(x) - bz</li>
17
+ <li>b = 0.208186</li>
18
+ <li>Elegant three-fold symmetry</li>
19
+ <li>Blue = slow, Green = fast</li>
20
+ <li>Drag to rotate</li>
21
+ <li>Scroll/pinch to zoom</li>
22
+ </span>
23
+ </div>
24
+ <canvas id="game"></canvas>
25
+ <script type="module" src="./js/thomas.js"></script>
26
+ </body>
27
+ </html>