@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,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
|
+
});
|
package/dist/lorenz.html
ADDED
|
@@ -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>
|
package/dist/thomas.html
ADDED
|
@@ -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>
|