@guinetik/gcanvas 1.0.4 → 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/CNAME +1 -0
- package/dist/aizawa.html +27 -0
- package/dist/animations.html +31 -0
- package/dist/basic.html +38 -0
- package/dist/baskara.html +31 -0
- package/dist/bezier.html +35 -0
- package/dist/beziersignature.html +29 -0
- package/dist/blackhole.html +28 -0
- package/dist/blob.html +35 -0
- package/dist/clifford.html +25 -0
- package/dist/cmb.html +24 -0
- package/dist/coordinates.html +698 -0
- package/dist/cube3d.html +23 -0
- package/dist/dadras.html +26 -0
- package/dist/dejong.html +25 -0
- package/dist/demos.css +303 -0
- package/dist/dino.html +42 -0
- package/dist/easing.html +28 -0
- package/dist/events.html +195 -0
- package/dist/fluent.html +647 -0
- package/dist/fluid-simple.html +22 -0
- package/dist/fluid.html +37 -0
- package/dist/fractals.html +36 -0
- package/dist/gameobjects.html +626 -0
- package/dist/gcanvas.es.js +14368 -9093
- 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/genart.html +26 -0
- package/dist/gendream.html +26 -0
- package/dist/group.html +36 -0
- package/dist/halvorsen.html +27 -0
- package/dist/home.html +587 -0
- package/dist/hyperbolic001.html +23 -0
- package/dist/hyperbolic002.html +23 -0
- package/dist/hyperbolic003.html +23 -0
- package/dist/hyperbolic004.html +23 -0
- package/dist/hyperbolic005.html +22 -0
- package/dist/index.html +446 -0
- package/dist/isometric.html +34 -0
- package/dist/js/aizawa.js +425 -0
- package/dist/js/animations.js +452 -0
- package/dist/js/basic.js +204 -0
- package/dist/js/baskara.js +751 -0
- package/dist/js/bezier.js +692 -0
- package/dist/js/beziersignature.js +241 -0
- package/dist/js/blackhole/accretiondisk.obj.js +379 -0
- package/dist/js/blackhole/blackhole.obj.js +318 -0
- package/dist/js/blackhole/index.js +409 -0
- package/dist/js/blackhole/particle.js +56 -0
- package/dist/js/blackhole/starfield.obj.js +218 -0
- package/dist/js/blob.js +2276 -0
- package/dist/js/clifford.js +236 -0
- package/dist/js/cmb.js +594 -0
- package/dist/js/coordinates.js +840 -0
- package/dist/js/cube3d.js +789 -0
- package/dist/js/dadras.js +405 -0
- package/dist/js/dejong.js +257 -0
- package/dist/js/dino.js +1420 -0
- package/dist/js/easing.js +477 -0
- package/dist/js/fluent.js +183 -0
- package/dist/js/fluid-simple.js +253 -0
- package/dist/js/fluid.js +527 -0
- package/dist/js/fractals.js +932 -0
- package/dist/js/fractalworker.js +93 -0
- package/dist/js/gameobjects.js +176 -0
- package/dist/js/genart.js +268 -0
- package/dist/js/gendream.js +209 -0
- package/dist/js/group.js +140 -0
- package/dist/js/halvorsen.js +405 -0
- package/dist/js/hyperbolic001.js +310 -0
- package/dist/js/hyperbolic002.js +388 -0
- package/dist/js/hyperbolic003.js +319 -0
- package/dist/js/hyperbolic004.js +345 -0
- package/dist/js/hyperbolic005.js +340 -0
- package/dist/js/info-toggle.js +25 -0
- package/dist/js/isometric.js +851 -0
- package/dist/js/kerr.js +1547 -0
- package/dist/js/lavalamp.js +590 -0
- package/dist/js/layout.js +354 -0
- package/dist/js/lorenz.js +425 -0
- package/dist/js/mondrian.js +285 -0
- package/dist/js/opacity.js +275 -0
- package/dist/js/painter.js +484 -0
- package/dist/js/particles-showcase.js +514 -0
- package/dist/js/particles.js +299 -0
- package/dist/js/patterns.js +397 -0
- package/dist/js/penrose/artifact.js +69 -0
- package/dist/js/penrose/blackhole.js +121 -0
- package/dist/js/penrose/constants.js +73 -0
- package/dist/js/penrose/game.js +943 -0
- package/dist/js/penrose/lore.js +278 -0
- package/dist/js/penrose/penrosescene.js +892 -0
- package/dist/js/penrose/ship.js +216 -0
- package/dist/js/penrose/sounds.js +211 -0
- package/dist/js/penrose/voidparticle.js +55 -0
- package/dist/js/penrose/voidscene.js +258 -0
- package/dist/js/penrose/voidship.js +144 -0
- package/dist/js/penrose/wormhole.js +46 -0
- package/dist/js/pipeline.js +555 -0
- package/dist/js/plane3d.js +256 -0
- package/dist/js/platformer.js +1579 -0
- package/dist/js/rossler.js +480 -0
- package/dist/js/scene.js +304 -0
- package/dist/js/scenes.js +320 -0
- package/dist/js/schrodinger.js +706 -0
- package/dist/js/schwarzschild.js +1015 -0
- package/dist/js/shapes.js +628 -0
- package/dist/js/space/alien.js +171 -0
- package/dist/js/space/boom.js +98 -0
- package/dist/js/space/boss.js +353 -0
- package/dist/js/space/buff.js +73 -0
- package/dist/js/space/bullet.js +102 -0
- package/dist/js/space/constants.js +85 -0
- package/dist/js/space/game.js +1884 -0
- package/dist/js/space/hud.js +112 -0
- package/dist/js/space/laserbeam.js +179 -0
- package/dist/js/space/lightning.js +277 -0
- package/dist/js/space/minion.js +192 -0
- package/dist/js/space/missile.js +212 -0
- package/dist/js/space/player.js +430 -0
- package/dist/js/space/powerup.js +90 -0
- package/dist/js/space/starfield.js +58 -0
- package/dist/js/space/starpower.js +90 -0
- package/dist/js/spacetime.js +559 -0
- package/dist/js/sphere3d.js +229 -0
- package/dist/js/sprite.js +473 -0
- package/dist/js/starfaux/config.js +118 -0
- package/dist/js/starfaux/enemy.js +353 -0
- package/dist/js/starfaux/hud.js +78 -0
- package/dist/js/starfaux/index.js +482 -0
- package/dist/js/starfaux/laser.js +182 -0
- package/dist/js/starfaux/player.js +468 -0
- package/dist/js/starfaux/terrain.js +560 -0
- package/dist/js/study001.js +275 -0
- package/dist/js/study002.js +366 -0
- package/dist/js/study003.js +331 -0
- package/dist/js/study004.js +389 -0
- package/dist/js/study005.js +209 -0
- package/dist/js/study006.js +194 -0
- package/dist/js/study007.js +192 -0
- package/dist/js/study008.js +413 -0
- package/dist/js/svgtween.js +204 -0
- package/dist/js/tde/accretiondisk.js +471 -0
- package/dist/js/tde/blackhole.js +219 -0
- package/dist/js/tde/blackholescene.js +209 -0
- package/dist/js/tde/config.js +59 -0
- package/dist/js/tde/index.js +820 -0
- package/dist/js/tde/jets.js +290 -0
- package/dist/js/tde/lensedstarfield.js +154 -0
- package/dist/js/tde/tdestar.js +297 -0
- package/dist/js/tde/tidalstream.js +372 -0
- package/dist/js/tde_old/blackhole.obj.js +354 -0
- package/dist/js/tde_old/debris.obj.js +791 -0
- package/dist/js/tde_old/flare.obj.js +239 -0
- package/dist/js/tde_old/index.js +448 -0
- package/dist/js/tde_old/star.obj.js +812 -0
- package/dist/js/tetris/config.js +157 -0
- package/dist/js/tetris/grid.js +286 -0
- package/dist/js/tetris/index.js +1195 -0
- package/dist/js/tetris/renderer.js +634 -0
- package/dist/js/tetris/tetrominos.js +280 -0
- package/dist/js/thomas.js +394 -0
- package/dist/js/tiles.js +312 -0
- package/dist/js/tweendemo.js +79 -0
- package/dist/js/visibility.js +102 -0
- package/dist/kerr.html +28 -0
- package/dist/lavalamp.html +27 -0
- package/dist/layouts.html +37 -0
- package/dist/logo.svg +4 -0
- package/dist/loop.html +84 -0
- package/dist/lorenz.html +27 -0
- package/dist/mondrian.html +32 -0
- package/dist/og_image.png +0 -0
- package/dist/opacity.html +36 -0
- package/dist/painter.html +39 -0
- package/dist/particles-showcase.html +28 -0
- package/dist/particles.html +24 -0
- package/dist/patterns.html +33 -0
- package/dist/penrose-game.html +31 -0
- package/dist/pipeline.html +737 -0
- package/dist/plane3d.html +24 -0
- package/dist/platformer.html +43 -0
- package/dist/rossler.html +27 -0
- package/dist/scene-interactivity-test.html +220 -0
- package/dist/scene.html +33 -0
- package/dist/scenes.html +96 -0
- package/dist/schrodinger.html +27 -0
- package/dist/schwarzschild.html +27 -0
- package/dist/shapes.html +16 -0
- package/dist/space.html +85 -0
- package/dist/spacetime.html +27 -0
- package/dist/sphere3d.html +24 -0
- package/dist/sprite.html +18 -0
- package/dist/starfaux.html +22 -0
- package/dist/study001.html +23 -0
- package/dist/study002.html +23 -0
- package/dist/study003.html +23 -0
- package/dist/study004.html +23 -0
- package/dist/study005.html +22 -0
- package/dist/study006.html +24 -0
- package/dist/study007.html +24 -0
- package/dist/study008.html +22 -0
- package/dist/svgtween.html +29 -0
- package/dist/tde.html +28 -0
- package/dist/tetris3d.html +25 -0
- package/dist/thomas.html +27 -0
- package/dist/tiles.html +28 -0
- package/dist/transforms.html +400 -0
- package/dist/tween.html +45 -0
- package/dist/visibility.html +33 -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,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lorenz Attractor 3D Visualization
|
|
3
|
+
*
|
|
4
|
+
* The classic "butterfly effect" attractor discovered by Edward Lorenz (1963)
|
|
5
|
+
* while studying atmospheric convection. Particles follow the chaotic
|
|
6
|
+
* trajectories colored by velocity (blue=slow, red=fast).
|
|
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.lorenz for equations)
|
|
22
|
+
attractor: {
|
|
23
|
+
dt: 0.005, // Integration time step
|
|
24
|
+
scale: 12, // Scale factor for display (Lorenz is larger)
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
// Particle settings
|
|
28
|
+
particles: {
|
|
29
|
+
count: 400,
|
|
30
|
+
trailLength: 250,
|
|
31
|
+
spawnRange: 2, // Initial position range around origin
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// Center offset - Lorenz attractor orbits around z≈27 (ρ-1)
|
|
35
|
+
center: {
|
|
36
|
+
x: 5,
|
|
37
|
+
y: 0,
|
|
38
|
+
z: 27,
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Camera settings - angled to show butterfly shape
|
|
42
|
+
camera: {
|
|
43
|
+
perspective: 800,
|
|
44
|
+
rotationX: -2, // Tilt to see butterfly spread
|
|
45
|
+
rotationY: -3, // Rotated to face the wings
|
|
46
|
+
inertia: true,
|
|
47
|
+
friction: 0.95,
|
|
48
|
+
clampX: false,
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// Visual settings
|
|
52
|
+
visual: {
|
|
53
|
+
minHue: 30, // Orange-red (fast)
|
|
54
|
+
maxHue: 200, // Cyan-blue (slow)
|
|
55
|
+
maxSpeed: 50, // Speed normalization threshold
|
|
56
|
+
saturation: 85,
|
|
57
|
+
lightness: 55,
|
|
58
|
+
maxAlpha: 0.85,
|
|
59
|
+
hueShiftSpeed: 15, // Degrees per second (0 to disable)
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// Glitch/blink effect
|
|
63
|
+
blink: {
|
|
64
|
+
chance: 0.015,
|
|
65
|
+
minDuration: 0.05,
|
|
66
|
+
maxDuration: 0.25,
|
|
67
|
+
intensityBoost: 1.4,
|
|
68
|
+
saturationBoost: 1.15,
|
|
69
|
+
alphaBoost: 1.25,
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
// Zoom settings
|
|
73
|
+
zoom: {
|
|
74
|
+
min: 0.2,
|
|
75
|
+
max: 2.5,
|
|
76
|
+
speed: 0.5,
|
|
77
|
+
easing: 0.12,
|
|
78
|
+
baseScreenSize: 600,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
83
|
+
// HELPER FUNCTIONS
|
|
84
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Convert HSL to RGB
|
|
88
|
+
*/
|
|
89
|
+
function hslToRgb(h, s, l) {
|
|
90
|
+
s /= 100;
|
|
91
|
+
l /= 100;
|
|
92
|
+
const k = (n) => (n + h / 30) % 12;
|
|
93
|
+
const a = s * Math.min(l, 1 - l);
|
|
94
|
+
const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
|
95
|
+
return {
|
|
96
|
+
r: Math.round(255 * f(0)),
|
|
97
|
+
g: Math.round(255 * f(8)),
|
|
98
|
+
b: Math.round(255 * f(4)),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
103
|
+
// ATTRACTOR PARTICLE
|
|
104
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A particle following attractor dynamics
|
|
108
|
+
*/
|
|
109
|
+
class AttractorParticle {
|
|
110
|
+
/**
|
|
111
|
+
* @param {Function} stepFn - Attractor step function
|
|
112
|
+
* @param {number} spawnRange - Initial position range
|
|
113
|
+
*/
|
|
114
|
+
constructor(stepFn, spawnRange) {
|
|
115
|
+
this.stepFn = stepFn;
|
|
116
|
+
this.position = {
|
|
117
|
+
x: (Math.random() - 0.5) * spawnRange,
|
|
118
|
+
y: (Math.random() - 0.5) * spawnRange,
|
|
119
|
+
z: (Math.random() - 0.5) * spawnRange + CONFIG.center.z, // Start near attractor
|
|
120
|
+
};
|
|
121
|
+
this.trail = [];
|
|
122
|
+
this.speed = 0;
|
|
123
|
+
|
|
124
|
+
// Blink/glitch state
|
|
125
|
+
this.blinkTime = 0;
|
|
126
|
+
this.blinkIntensity = 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Update blink state
|
|
131
|
+
*/
|
|
132
|
+
updateBlink(dt) {
|
|
133
|
+
const { chance, minDuration, maxDuration } = CONFIG.blink;
|
|
134
|
+
|
|
135
|
+
if (this.blinkTime > 0) {
|
|
136
|
+
this.blinkTime -= dt;
|
|
137
|
+
this.blinkIntensity = Math.max(
|
|
138
|
+
0,
|
|
139
|
+
this.blinkTime > 0
|
|
140
|
+
? Math.sin((this.blinkTime / ((minDuration + maxDuration) * 0.5)) * Math.PI)
|
|
141
|
+
: 0
|
|
142
|
+
);
|
|
143
|
+
} else {
|
|
144
|
+
if (Math.random() < chance) {
|
|
145
|
+
this.blinkTime = minDuration + Math.random() * (maxDuration - minDuration);
|
|
146
|
+
this.blinkIntensity = 1;
|
|
147
|
+
} else {
|
|
148
|
+
this.blinkIntensity = 0;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Update particle position using attractor
|
|
155
|
+
*/
|
|
156
|
+
update(dt, scale) {
|
|
157
|
+
// Use the attractor step function
|
|
158
|
+
const result = this.stepFn(this.position, dt);
|
|
159
|
+
|
|
160
|
+
// Update position
|
|
161
|
+
this.position = result.position;
|
|
162
|
+
this.speed = result.speed;
|
|
163
|
+
|
|
164
|
+
// Add to trail (scaled and centered for display)
|
|
165
|
+
// Subtract center offset so attractor rotates around its center
|
|
166
|
+
this.trail.unshift({
|
|
167
|
+
x: (this.position.x - CONFIG.center.x) * scale,
|
|
168
|
+
y: (this.position.y - CONFIG.center.y) * scale,
|
|
169
|
+
z: (this.position.z - CONFIG.center.z) * scale,
|
|
170
|
+
speed: this.speed,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Trim trail
|
|
174
|
+
if (this.trail.length > CONFIG.particles.trailLength) {
|
|
175
|
+
this.trail.pop();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
181
|
+
// DEMO CLASS
|
|
182
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Lorenz Attractor Demo
|
|
186
|
+
*/
|
|
187
|
+
class LorenzDemo extends Game {
|
|
188
|
+
constructor(canvas) {
|
|
189
|
+
super(canvas);
|
|
190
|
+
this.backgroundColor = "#000";
|
|
191
|
+
this.enableFluidSize();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
init() {
|
|
195
|
+
super.init();
|
|
196
|
+
|
|
197
|
+
// Get attractor info for display
|
|
198
|
+
this.attractor = Attractors.lorenz;
|
|
199
|
+
console.log(`Attractor: ${this.attractor.name}`);
|
|
200
|
+
console.log(`Equations:`, this.attractor.equations);
|
|
201
|
+
|
|
202
|
+
// Create stepper function with default params
|
|
203
|
+
this.stepFn = this.attractor.createStepper();
|
|
204
|
+
|
|
205
|
+
// Calculate initial zoom
|
|
206
|
+
const { min, max, baseScreenSize } = CONFIG.zoom;
|
|
207
|
+
const initialZoom = Math.min(max, Math.max(min, Screen.minDimension() / baseScreenSize));
|
|
208
|
+
this.zoom = initialZoom;
|
|
209
|
+
this.targetZoom = initialZoom;
|
|
210
|
+
this.defaultZoom = initialZoom;
|
|
211
|
+
|
|
212
|
+
// Camera with mouse control
|
|
213
|
+
this.camera = new Camera3D({
|
|
214
|
+
perspective: CONFIG.camera.perspective,
|
|
215
|
+
rotationX: CONFIG.camera.rotationX,
|
|
216
|
+
rotationY: CONFIG.camera.rotationY,
|
|
217
|
+
inertia: CONFIG.camera.inertia,
|
|
218
|
+
friction: CONFIG.camera.friction,
|
|
219
|
+
clampX: CONFIG.camera.clampX,
|
|
220
|
+
});
|
|
221
|
+
this.camera.enableMouseControl(this.canvas);
|
|
222
|
+
|
|
223
|
+
// Gesture handler for zoom
|
|
224
|
+
this.gesture = new Gesture(this.canvas, {
|
|
225
|
+
onZoom: (delta) => {
|
|
226
|
+
this.targetZoom *= 1 + delta * CONFIG.zoom.speed;
|
|
227
|
+
},
|
|
228
|
+
onPan: null,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Double-click to reset
|
|
232
|
+
this.canvas.addEventListener("dblclick", () => {
|
|
233
|
+
this.targetZoom = this.defaultZoom;
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Log camera params on mouse release (for finding good starting angle)
|
|
237
|
+
this.canvas.addEventListener("mouseup", () => {
|
|
238
|
+
console.log(`Camera: rotationX: ${this.camera.rotationX.toFixed(3)}, rotationY: ${this.camera.rotationY.toFixed(3)}`);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Initialize particles using the attractor step function
|
|
242
|
+
this.particles = [];
|
|
243
|
+
for (let i = 0; i < CONFIG.particles.count; i++) {
|
|
244
|
+
this.particles.push(
|
|
245
|
+
new AttractorParticle(this.stepFn, CONFIG.particles.spawnRange)
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// WebGL line renderer
|
|
250
|
+
const maxSegments = CONFIG.particles.count * CONFIG.particles.trailLength;
|
|
251
|
+
this.lineRenderer = new WebGLLineRenderer(maxSegments, {
|
|
252
|
+
width: this.width,
|
|
253
|
+
height: this.height,
|
|
254
|
+
blendMode: "additive",
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
this.segments = [];
|
|
258
|
+
|
|
259
|
+
if (!this.lineRenderer.isAvailable()) {
|
|
260
|
+
console.warn("WebGL not available, falling back to Canvas 2D");
|
|
261
|
+
this.useWebGL = false;
|
|
262
|
+
} else {
|
|
263
|
+
this.useWebGL = true;
|
|
264
|
+
console.log(`WebGL enabled, ${maxSegments} max segments`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
this.time = 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
onResize() {
|
|
271
|
+
if (this.lineRenderer?.isAvailable()) {
|
|
272
|
+
this.lineRenderer.resize(this.width, this.height);
|
|
273
|
+
}
|
|
274
|
+
const { min, max, baseScreenSize } = CONFIG.zoom;
|
|
275
|
+
this.defaultZoom = Math.min(max, Math.max(min, Screen.minDimension() / baseScreenSize));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
update(dt) {
|
|
279
|
+
super.update(dt);
|
|
280
|
+
this.camera.update(dt);
|
|
281
|
+
|
|
282
|
+
// Normalize rotation to prevent unbounded values
|
|
283
|
+
const TAU = Math.PI * 2;
|
|
284
|
+
this.camera.rotationY = ((this.camera.rotationY % TAU) + TAU) % TAU;
|
|
285
|
+
|
|
286
|
+
this.zoom += (this.targetZoom - this.zoom) * CONFIG.zoom.easing;
|
|
287
|
+
this.time += dt;
|
|
288
|
+
|
|
289
|
+
for (const particle of this.particles) {
|
|
290
|
+
particle.update(CONFIG.attractor.dt, CONFIG.attractor.scale);
|
|
291
|
+
particle.updateBlink(dt);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
collectSegments(cx, cy) {
|
|
296
|
+
const { minHue, maxHue, maxSpeed, saturation, lightness, maxAlpha, hueShiftSpeed } =
|
|
297
|
+
CONFIG.visual;
|
|
298
|
+
const { intensityBoost, saturationBoost, alphaBoost } = CONFIG.blink;
|
|
299
|
+
const hueOffset = (this.time * hueShiftSpeed) % 360;
|
|
300
|
+
|
|
301
|
+
this.segments.length = 0;
|
|
302
|
+
|
|
303
|
+
for (const particle of this.particles) {
|
|
304
|
+
if (particle.trail.length < 2) continue;
|
|
305
|
+
|
|
306
|
+
const blink = particle.blinkIntensity;
|
|
307
|
+
|
|
308
|
+
for (let i = 1; i < particle.trail.length; i++) {
|
|
309
|
+
const curr = particle.trail[i];
|
|
310
|
+
const prev = particle.trail[i - 1];
|
|
311
|
+
|
|
312
|
+
const p1 = this.camera.project(prev.x, prev.y, prev.z);
|
|
313
|
+
const p2 = this.camera.project(curr.x, curr.y, curr.z);
|
|
314
|
+
|
|
315
|
+
if (p1.scale <= 0 || p2.scale <= 0) continue;
|
|
316
|
+
|
|
317
|
+
const age = i / particle.trail.length;
|
|
318
|
+
const speedNorm = Math.min(curr.speed / maxSpeed, 1);
|
|
319
|
+
const baseHue = maxHue - speedNorm * (maxHue - minHue);
|
|
320
|
+
const hue = (baseHue + hueOffset) % 360;
|
|
321
|
+
|
|
322
|
+
const sat = Math.min(100, saturation * (1 + blink * (saturationBoost - 1)));
|
|
323
|
+
const lit = Math.min(100, lightness * (1 + blink * (intensityBoost - 1)));
|
|
324
|
+
const rgb = hslToRgb(hue, sat, lit);
|
|
325
|
+
const alpha = Math.min(1, (1 - age) * maxAlpha * (1 + blink * (alphaBoost - 1)));
|
|
326
|
+
|
|
327
|
+
this.segments.push({
|
|
328
|
+
x1: cx + p1.x * this.zoom,
|
|
329
|
+
y1: cy + p1.y * this.zoom,
|
|
330
|
+
x2: cx + p2.x * this.zoom,
|
|
331
|
+
y2: cy + p2.y * this.zoom,
|
|
332
|
+
r: rgb.r,
|
|
333
|
+
g: rgb.g,
|
|
334
|
+
b: rgb.b,
|
|
335
|
+
a: alpha,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return this.segments.length;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
renderCanvas2D(cx, cy) {
|
|
344
|
+
const { minHue, maxHue, maxSpeed, saturation, lightness, maxAlpha, hueShiftSpeed } =
|
|
345
|
+
CONFIG.visual;
|
|
346
|
+
const { intensityBoost, saturationBoost, alphaBoost } = CONFIG.blink;
|
|
347
|
+
const hueOffset = (this.time * hueShiftSpeed) % 360;
|
|
348
|
+
|
|
349
|
+
const ctx = this.ctx;
|
|
350
|
+
ctx.save();
|
|
351
|
+
ctx.globalCompositeOperation = "lighter";
|
|
352
|
+
ctx.lineCap = "round";
|
|
353
|
+
|
|
354
|
+
for (const particle of this.particles) {
|
|
355
|
+
if (particle.trail.length < 2) continue;
|
|
356
|
+
|
|
357
|
+
const blink = particle.blinkIntensity;
|
|
358
|
+
|
|
359
|
+
for (let i = 1; i < particle.trail.length; i++) {
|
|
360
|
+
const curr = particle.trail[i];
|
|
361
|
+
const prev = particle.trail[i - 1];
|
|
362
|
+
|
|
363
|
+
const p1 = this.camera.project(prev.x, prev.y, prev.z);
|
|
364
|
+
const p2 = this.camera.project(curr.x, curr.y, curr.z);
|
|
365
|
+
|
|
366
|
+
if (p1.scale <= 0 || p2.scale <= 0) continue;
|
|
367
|
+
|
|
368
|
+
const age = i / particle.trail.length;
|
|
369
|
+
const speedNorm = Math.min(curr.speed / maxSpeed, 1);
|
|
370
|
+
const baseHue = maxHue - speedNorm * (maxHue - minHue);
|
|
371
|
+
const hue = (baseHue + hueOffset) % 360;
|
|
372
|
+
|
|
373
|
+
const sat = Math.min(100, saturation * (1 + blink * (saturationBoost - 1)));
|
|
374
|
+
const lit = Math.min(100, lightness * (1 + blink * (intensityBoost - 1)));
|
|
375
|
+
const alpha = Math.min(1, (1 - age) * maxAlpha * (1 + blink * (alphaBoost - 1)));
|
|
376
|
+
|
|
377
|
+
ctx.strokeStyle = `hsla(${hue}, ${sat}%, ${lit}%, ${alpha})`;
|
|
378
|
+
ctx.lineWidth = 1;
|
|
379
|
+
|
|
380
|
+
ctx.beginPath();
|
|
381
|
+
ctx.moveTo(cx + p1.x * this.zoom, cy + p1.y * this.zoom);
|
|
382
|
+
ctx.lineTo(cx + p2.x * this.zoom, cy + p2.y * this.zoom);
|
|
383
|
+
ctx.stroke();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
ctx.restore();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
render() {
|
|
391
|
+
super.render();
|
|
392
|
+
if (!this.particles) return;
|
|
393
|
+
|
|
394
|
+
const cx = this.width / 2;
|
|
395
|
+
const cy = this.height / 2;
|
|
396
|
+
|
|
397
|
+
if (this.useWebGL && this.lineRenderer.isAvailable()) {
|
|
398
|
+
const segmentCount = this.collectSegments(cx, cy);
|
|
399
|
+
if (segmentCount > 0) {
|
|
400
|
+
this.lineRenderer.clear();
|
|
401
|
+
this.lineRenderer.updateLines(this.segments);
|
|
402
|
+
this.lineRenderer.render(segmentCount);
|
|
403
|
+
this.lineRenderer.compositeOnto(this.ctx, 0, 0);
|
|
404
|
+
}
|
|
405
|
+
} else {
|
|
406
|
+
this.renderCanvas2D(cx, cy);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
destroy() {
|
|
411
|
+
this.gesture?.destroy();
|
|
412
|
+
this.lineRenderer?.destroy();
|
|
413
|
+
super.destroy?.();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
418
|
+
// INITIALIZATION
|
|
419
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
420
|
+
|
|
421
|
+
window.addEventListener("load", () => {
|
|
422
|
+
const canvas = document.getElementById("game");
|
|
423
|
+
const demo = new LorenzDemo(canvas);
|
|
424
|
+
demo.start();
|
|
425
|
+
});
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Button,
|
|
3
|
+
Game,
|
|
4
|
+
GridLayout,
|
|
5
|
+
HorizontalLayout,
|
|
6
|
+
Rectangle,
|
|
7
|
+
Scene,
|
|
8
|
+
ToggleButton,
|
|
9
|
+
VerticalLayout,
|
|
10
|
+
TextShape,
|
|
11
|
+
Position,
|
|
12
|
+
ShapeGOFactory,
|
|
13
|
+
Easing,
|
|
14
|
+
Tweenetik,
|
|
15
|
+
} from "/gcanvas.es.min.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* MondrianDemo - Interactive grid-based composition
|
|
19
|
+
* Inspired by the works of Piet Mondrian
|
|
20
|
+
*/
|
|
21
|
+
export class MondrianDemo extends Game {
|
|
22
|
+
constructor(canvas) {
|
|
23
|
+
super(canvas);
|
|
24
|
+
this.enableFluidSize();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
init() {
|
|
28
|
+
super.init();
|
|
29
|
+
|
|
30
|
+
// Setup
|
|
31
|
+
this.backgroundColor = "#ffffff";
|
|
32
|
+
this.gridScene = new Scene(this, { debugColor: "black" });
|
|
33
|
+
|
|
34
|
+
// Configure grid scene
|
|
35
|
+
const margin = 20;
|
|
36
|
+
this.gridScene.width = this.width - margin * 2;
|
|
37
|
+
this.gridScene.height = this.height - margin * 2;
|
|
38
|
+
this.gridScene.x = margin;
|
|
39
|
+
this.gridScene.y = margin;
|
|
40
|
+
|
|
41
|
+
// Generate initial composition
|
|
42
|
+
this.generateMondrianRectangles(
|
|
43
|
+
this.gridScene.width,
|
|
44
|
+
this.gridScene.height
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// Add to pipeline
|
|
48
|
+
this.pipeline.add(this.gridScene);
|
|
49
|
+
|
|
50
|
+
// Handle click events
|
|
51
|
+
this.events.on("click", () => this.animateExplosion());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
animateExplosion() {
|
|
55
|
+
const centerX = this.width / 2;
|
|
56
|
+
const centerY = this.height / 2;
|
|
57
|
+
const maxDimension = Math.max(this.width, this.height);
|
|
58
|
+
const children = this.gridScene._collection.getSortedChildren();
|
|
59
|
+
|
|
60
|
+
children.forEach((rect) => {
|
|
61
|
+
// Calculate angle and distance from center
|
|
62
|
+
const angle = Math.atan2(rect.y - centerY, rect.x - centerX);
|
|
63
|
+
const distanceFromCenter = Math.sqrt(
|
|
64
|
+
Math.pow(rect.x - centerX, 2) + Math.pow(rect.y - centerY, 2)
|
|
65
|
+
);
|
|
66
|
+
const normalizedDistance = Math.min(
|
|
67
|
+
1,
|
|
68
|
+
distanceFromCenter / (Math.min(this.width, this.height) / 2)
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Calculate animation parameters
|
|
72
|
+
const flyDistance = maxDimension * 1.5;
|
|
73
|
+
const destX = centerX + Math.cos(angle) * flyDistance;
|
|
74
|
+
const destY = centerY + Math.sin(angle) * flyDistance;
|
|
75
|
+
const delay = (1 - normalizedDistance) * 0.5;
|
|
76
|
+
const rotation = (Math.random() * 2 - 1) * Math.PI * 4;
|
|
77
|
+
|
|
78
|
+
// Animate rectangle flying away
|
|
79
|
+
Tweenetik.to(
|
|
80
|
+
rect,
|
|
81
|
+
{
|
|
82
|
+
opacity: 0,
|
|
83
|
+
x: destX,
|
|
84
|
+
y: destY,
|
|
85
|
+
scaleX: 0.2,
|
|
86
|
+
scaleY: 0.2,
|
|
87
|
+
rotation: rotation,
|
|
88
|
+
},
|
|
89
|
+
0.5 + normalizedDistance * 0.5,
|
|
90
|
+
Easing.easeInSine,
|
|
91
|
+
{
|
|
92
|
+
delay: delay,
|
|
93
|
+
onComplete: () => {
|
|
94
|
+
this.generateMondrianRectangles(
|
|
95
|
+
this.gridScene.width,
|
|
96
|
+
this.gridScene.height
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
generateMondrianRectangles(totalWidth, totalHeight, options = {}) {
|
|
105
|
+
// Clear existing rectangles
|
|
106
|
+
this.gridScene.clear();
|
|
107
|
+
|
|
108
|
+
// Default options
|
|
109
|
+
const {
|
|
110
|
+
lineWidth = 8,
|
|
111
|
+
step = totalHeight / 6,
|
|
112
|
+
splitProbability = 0.5,
|
|
113
|
+
} = options;
|
|
114
|
+
|
|
115
|
+
// Colors
|
|
116
|
+
const white = "#F2F5F1";
|
|
117
|
+
const colors = ["#D40920", "#1356A2", "#F7D842", "#999999"];
|
|
118
|
+
|
|
119
|
+
// Start with one rectangle covering the entire area
|
|
120
|
+
let squares = [
|
|
121
|
+
{
|
|
122
|
+
x: 0,
|
|
123
|
+
y: 0,
|
|
124
|
+
width: totalWidth,
|
|
125
|
+
height: totalHeight,
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
// Generate split points based on grid
|
|
130
|
+
const splitPoints = Array.from(
|
|
131
|
+
{ length: Math.ceil(Math.max(totalWidth, totalHeight) / step) },
|
|
132
|
+
(_, i) => i * step
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Split function
|
|
136
|
+
const splitSquaresAt = (coord) => {
|
|
137
|
+
const { x, y } = coord;
|
|
138
|
+
|
|
139
|
+
for (let i = squares.length - 1; i >= 0; i--) {
|
|
140
|
+
const square = squares[i];
|
|
141
|
+
|
|
142
|
+
// Split on x-coordinate
|
|
143
|
+
if (
|
|
144
|
+
x &&
|
|
145
|
+
x > square.x &&
|
|
146
|
+
x < square.x + square.width &&
|
|
147
|
+
Math.random() < splitProbability
|
|
148
|
+
) {
|
|
149
|
+
squares.splice(i, 1);
|
|
150
|
+
|
|
151
|
+
squares.push(
|
|
152
|
+
{
|
|
153
|
+
x: square.x,
|
|
154
|
+
y: square.y,
|
|
155
|
+
width: x - square.x,
|
|
156
|
+
height: square.height,
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
x: x,
|
|
160
|
+
y: square.y,
|
|
161
|
+
width: square.width - (x - square.x),
|
|
162
|
+
height: square.height,
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Split on y-coordinate
|
|
168
|
+
if (
|
|
169
|
+
y &&
|
|
170
|
+
y > square.y &&
|
|
171
|
+
y < square.y + square.height &&
|
|
172
|
+
Math.random() < splitProbability
|
|
173
|
+
) {
|
|
174
|
+
squares.splice(i, 1);
|
|
175
|
+
|
|
176
|
+
squares.push(
|
|
177
|
+
{
|
|
178
|
+
x: square.x,
|
|
179
|
+
y: square.y,
|
|
180
|
+
width: square.width,
|
|
181
|
+
height: y - square.y,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
x: square.x,
|
|
185
|
+
y: y,
|
|
186
|
+
width: square.width,
|
|
187
|
+
height: square.height - (y - square.y),
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Apply splits
|
|
195
|
+
splitPoints.forEach((point) => {
|
|
196
|
+
splitSquaresAt({ y: point });
|
|
197
|
+
splitSquaresAt({ x: point });
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Assign colors to some squares
|
|
201
|
+
for (let i = 0; i < colors.length * 3; i++) {
|
|
202
|
+
const randomIndex = Math.floor(Math.random() * squares.length);
|
|
203
|
+
squares[randomIndex].color =
|
|
204
|
+
Math.random() < 0.8 ? colors[i % colors.length] : "black";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Calculate center for animations
|
|
208
|
+
const centerX = this.width / 2;
|
|
209
|
+
const centerY = this.height / 2;
|
|
210
|
+
const maxDimension = Math.max(this.width, this.height);
|
|
211
|
+
|
|
212
|
+
// Create and animate rectangles
|
|
213
|
+
squares.forEach((square, i, allSquares) => {
|
|
214
|
+
// Create rectangle
|
|
215
|
+
const rect = new Rectangle({
|
|
216
|
+
width: square.width - lineWidth,
|
|
217
|
+
height: square.height - lineWidth,
|
|
218
|
+
color: square.color || white,
|
|
219
|
+
stroke: "#000000",
|
|
220
|
+
lineWidth: lineWidth,
|
|
221
|
+
crisp: false,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Calculate positions and animation parameters
|
|
225
|
+
const finalX = square.x + square.width / 2;
|
|
226
|
+
const finalY = square.y + square.height / 2;
|
|
227
|
+
const angle = Math.atan2(finalY - centerY, finalX - centerX);
|
|
228
|
+
const flyDistance = maxDimension * 1.5;
|
|
229
|
+
const startX = centerX + Math.cos(angle) * flyDistance;
|
|
230
|
+
const startY = centerY + Math.sin(angle) * flyDistance;
|
|
231
|
+
|
|
232
|
+
// Create game object
|
|
233
|
+
const go = ShapeGOFactory.create(this, rect, {
|
|
234
|
+
x: startX,
|
|
235
|
+
y: startY,
|
|
236
|
+
scaleX: 0.0,
|
|
237
|
+
scaleY: 0.0,
|
|
238
|
+
crisp: false,
|
|
239
|
+
rotation: (Math.random() * 2 - 1) * Math.PI,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
this.gridScene.add(go);
|
|
243
|
+
|
|
244
|
+
// Calculate animation timing
|
|
245
|
+
const distance = Math.sqrt(
|
|
246
|
+
Math.pow(finalX - centerX, 2) + Math.pow(finalY - centerY, 2)
|
|
247
|
+
);
|
|
248
|
+
const normalizedDistance = Math.min(
|
|
249
|
+
1,
|
|
250
|
+
distance / (Math.min(this.width, this.height) / 2)
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const middle = allSquares.length / 2;
|
|
254
|
+
const distanceFromMiddle = Math.abs(i - middle);
|
|
255
|
+
const delay = (distanceFromMiddle / middle) * 0.1;
|
|
256
|
+
|
|
257
|
+
// Animate rectangle flying in
|
|
258
|
+
Tweenetik.to(
|
|
259
|
+
go,
|
|
260
|
+
{
|
|
261
|
+
x: finalX,
|
|
262
|
+
y: finalY,
|
|
263
|
+
scaleX: 1,
|
|
264
|
+
scaleY: 1,
|
|
265
|
+
rotation: 0,
|
|
266
|
+
},
|
|
267
|
+
0.5 + normalizedDistance * 0.5,
|
|
268
|
+
Easing.easeOutCirc,
|
|
269
|
+
{ delay: delay }
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Update function - called each frame
|
|
276
|
+
*/
|
|
277
|
+
update(dt) {
|
|
278
|
+
if (this.boundsDirty) {
|
|
279
|
+
this.gridScene.width = this.width - 40;
|
|
280
|
+
this.gridScene.height = this.height - 40;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
super.update(dt);
|
|
284
|
+
}
|
|
285
|
+
}
|