@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,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Halvorsen Attractor 3D Visualization
|
|
3
|
+
*
|
|
4
|
+
* A symmetric chaotic attractor with three-fold rotational symmetry.
|
|
5
|
+
* Creates beautiful intertwined spiral structures.
|
|
6
|
+
*
|
|
7
|
+
* Uses the Attractors module for pure math functions and WebGL for
|
|
8
|
+
* high-performance line rendering.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Game, Gesture, Screen, Attractors } from "/gcanvas.es.min.js";
|
|
12
|
+
import { Camera3D } from "/gcanvas.es.min.js";
|
|
13
|
+
import { WebGLLineRenderer } from "/gcanvas.es.min.js";
|
|
14
|
+
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
// CONFIGURATION
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const CONFIG = {
|
|
20
|
+
// Attractor settings (uses Attractors.halvorsen for equations)
|
|
21
|
+
attractor: {
|
|
22
|
+
dt: 0.004, // Integration time step
|
|
23
|
+
scale: 25, // Scale factor for display
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
// Particle settings
|
|
27
|
+
particles: {
|
|
28
|
+
count: 420,
|
|
29
|
+
trailLength: 280,
|
|
30
|
+
spawnRange: 1,
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
// Center offset - Halvorsen barycenter is around (-5,-5,-5) due to symmetry
|
|
34
|
+
// With Y/Z swap: x→screen X, z→screen Y, y→depth
|
|
35
|
+
center: {
|
|
36
|
+
x: 0,
|
|
37
|
+
y: 0,
|
|
38
|
+
z: 0,
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Camera settings
|
|
42
|
+
camera: {
|
|
43
|
+
perspective: 300,
|
|
44
|
+
rotationX: 0.615,
|
|
45
|
+
rotationY: 0.495,
|
|
46
|
+
inertia: true,
|
|
47
|
+
friction: 0.95,
|
|
48
|
+
clampX: false,
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// Visual settings - cool blue/purple palette
|
|
52
|
+
visual: {
|
|
53
|
+
minHue: 320, // Pink (fast)
|
|
54
|
+
maxHue: 220, // Blue (slow)
|
|
55
|
+
maxSpeed: 40,
|
|
56
|
+
saturation: 80,
|
|
57
|
+
lightness: 55,
|
|
58
|
+
maxAlpha: 0.85,
|
|
59
|
+
hueShiftSpeed: 15,
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// Glitch/blink effect
|
|
63
|
+
blink: {
|
|
64
|
+
chance: 0.02,
|
|
65
|
+
minDuration: 0.04,
|
|
66
|
+
maxDuration: 0.18,
|
|
67
|
+
intensityBoost: 1.5,
|
|
68
|
+
saturationBoost: 1.2,
|
|
69
|
+
alphaBoost: 1.3,
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
// Zoom settings
|
|
73
|
+
zoom: {
|
|
74
|
+
min: 0.25,
|
|
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
|
+
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, spawnRange) {
|
|
139
|
+
const result = this.stepFn(this.position, dt);
|
|
140
|
+
this.position = result.position;
|
|
141
|
+
this.speed = result.speed;
|
|
142
|
+
|
|
143
|
+
// Small chance to respawn at random position (keeps transient "thickness")
|
|
144
|
+
if (Math.random() < 0.003) {
|
|
145
|
+
this.position = {
|
|
146
|
+
x: (Math.random() - 0.5) * spawnRange,
|
|
147
|
+
y: (Math.random() - 0.5) * spawnRange,
|
|
148
|
+
z: (Math.random() - 0.5) * spawnRange,
|
|
149
|
+
};
|
|
150
|
+
this.trail = [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Add to trail (centered and scaled for display)
|
|
154
|
+
// Swap Y/Z so vertical mouse drag rotates naturally
|
|
155
|
+
this.trail.unshift({
|
|
156
|
+
x: (this.position.x - CONFIG.center.x) * scale,
|
|
157
|
+
y: (this.position.z - CONFIG.center.z) * scale, // Z becomes screen Y (vertical)
|
|
158
|
+
z: (this.position.y - CONFIG.center.y) * scale, // Y becomes depth
|
|
159
|
+
speed: this.speed,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (this.trail.length > CONFIG.particles.trailLength) {
|
|
163
|
+
this.trail.pop();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
169
|
+
// DEMO CLASS
|
|
170
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
class HalvorsenDemo extends Game {
|
|
173
|
+
constructor(canvas) {
|
|
174
|
+
super(canvas);
|
|
175
|
+
this.backgroundColor = "#000";
|
|
176
|
+
this.enableFluidSize();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
init() {
|
|
180
|
+
super.init();
|
|
181
|
+
|
|
182
|
+
this.attractor = Attractors.halvorsen;
|
|
183
|
+
console.log(`Attractor: ${this.attractor.name}`);
|
|
184
|
+
console.log(`Equations:`, this.attractor.equations);
|
|
185
|
+
|
|
186
|
+
this.stepFn = this.attractor.createStepper();
|
|
187
|
+
|
|
188
|
+
const { min, max, baseScreenSize } = CONFIG.zoom;
|
|
189
|
+
const initialZoom = Math.min(max, Math.max(min, Screen.minDimension() / baseScreenSize));
|
|
190
|
+
this.zoom = initialZoom;
|
|
191
|
+
this.targetZoom = initialZoom;
|
|
192
|
+
this.defaultZoom = initialZoom;
|
|
193
|
+
|
|
194
|
+
this.camera = new Camera3D({
|
|
195
|
+
perspective: CONFIG.camera.perspective,
|
|
196
|
+
rotationX: CONFIG.camera.rotationX,
|
|
197
|
+
rotationY: CONFIG.camera.rotationY,
|
|
198
|
+
inertia: CONFIG.camera.inertia,
|
|
199
|
+
friction: CONFIG.camera.friction,
|
|
200
|
+
clampX: CONFIG.camera.clampX,
|
|
201
|
+
});
|
|
202
|
+
this.camera.enableMouseControl(this.canvas);
|
|
203
|
+
|
|
204
|
+
this.gesture = new Gesture(this.canvas, {
|
|
205
|
+
onZoom: (delta) => {
|
|
206
|
+
this.targetZoom *= 1 + delta * CONFIG.zoom.speed;
|
|
207
|
+
},
|
|
208
|
+
onPan: null,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
this.canvas.addEventListener("dblclick", () => {
|
|
212
|
+
this.targetZoom = this.defaultZoom;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Log camera params and barycenter on mouse release
|
|
216
|
+
this.canvas.addEventListener("mouseup", () => {
|
|
217
|
+
console.log(`Camera: rotationX: ${this.camera.rotationX.toFixed(3)}, rotationY: ${this.camera.rotationY.toFixed(3)}`);
|
|
218
|
+
let sumX = 0, sumY = 0, sumZ = 0, count = 0;
|
|
219
|
+
for (const p of this.particles) {
|
|
220
|
+
sumX += p.position.x;
|
|
221
|
+
sumY += p.position.y;
|
|
222
|
+
sumZ += p.position.z;
|
|
223
|
+
count++;
|
|
224
|
+
}
|
|
225
|
+
console.log(`Barycenter: x: ${(sumX/count).toFixed(3)}, y: ${(sumY/count).toFixed(3)}, z: ${(sumZ/count).toFixed(3)}`);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
this.particles = [];
|
|
229
|
+
for (let i = 0; i < CONFIG.particles.count; i++) {
|
|
230
|
+
this.particles.push(
|
|
231
|
+
new AttractorParticle(this.stepFn, CONFIG.particles.spawnRange)
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
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, CONFIG.particles.spawnRange);
|
|
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 * (minHue - maxHue);
|
|
300
|
+
const hue = (baseHue + hueOffset + 360) % 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 * (minHue - maxHue);
|
|
351
|
+
const hue = (baseHue + hueOffset + 360) % 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 HalvorsenDemo(canvas);
|
|
404
|
+
demo.start();
|
|
405
|
+
});
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hyperbolic 001 - Wireframe Flux
|
|
3
|
+
*
|
|
4
|
+
* Inspired by hyperbolic space - a torus wireframe that breathes and
|
|
5
|
+
* deforms with noise. Click to disturb, watch it settle to equilibrium.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Torus geometry with simplex noise deformation
|
|
9
|
+
* - Click to add energy - more clicks = more chaos
|
|
10
|
+
* - Settles to subtle vibration equilibrium
|
|
11
|
+
* - Drag to rotate, auto-rotates when idle
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { gcanvas, Noise, Painter } from "/gcanvas.es.min.js";
|
|
15
|
+
|
|
16
|
+
// Configuration
|
|
17
|
+
const CONFIG = {
|
|
18
|
+
// Torus geometry
|
|
19
|
+
majorRadius: 350,
|
|
20
|
+
minorRadius: 140,
|
|
21
|
+
rows: 50,
|
|
22
|
+
cols: 30,
|
|
23
|
+
|
|
24
|
+
// Noise settings
|
|
25
|
+
noiseScale: 0.005,
|
|
26
|
+
noiseSpeed: 0.5,
|
|
27
|
+
noiseStrengthBase: 15, // Calm equilibrium - subtle breathing
|
|
28
|
+
noiseStrengthMax: 120, // Max when fully energized
|
|
29
|
+
|
|
30
|
+
// Projection
|
|
31
|
+
fov: 800, // Slightly wider FOV for larger object
|
|
32
|
+
|
|
33
|
+
// Colors
|
|
34
|
+
baseColor: { h: 280, s: 100, l: 60 },
|
|
35
|
+
bgColor: "#000000",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 3D Vector helper
|
|
40
|
+
*/
|
|
41
|
+
class Vec3 {
|
|
42
|
+
constructor(x, y, z) {
|
|
43
|
+
this.x = x;
|
|
44
|
+
this.y = y;
|
|
45
|
+
this.z = z;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Main Study Class
|
|
51
|
+
*/
|
|
52
|
+
class WireframeFlux {
|
|
53
|
+
constructor(game) {
|
|
54
|
+
this.game = game;
|
|
55
|
+
this.width = game.width;
|
|
56
|
+
this.height = game.height;
|
|
57
|
+
|
|
58
|
+
// Mesh data
|
|
59
|
+
this.vertices = [];
|
|
60
|
+
this.projected = [];
|
|
61
|
+
this.indices = [];
|
|
62
|
+
|
|
63
|
+
// Animation state
|
|
64
|
+
this.time = 0;
|
|
65
|
+
this.rotation = { x: 0, y: 0 };
|
|
66
|
+
this.targetRotation = { x: 0.5, y: 0.5 };
|
|
67
|
+
this.autoRotate = true;
|
|
68
|
+
|
|
69
|
+
// Interaction state - energy decays toward equilibrium
|
|
70
|
+
this.energy = 0;
|
|
71
|
+
this.noiseStrength = CONFIG.noiseStrengthBase;
|
|
72
|
+
|
|
73
|
+
this.initMesh();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
initMesh() {
|
|
77
|
+
this.vertices = [];
|
|
78
|
+
this.indices = [];
|
|
79
|
+
|
|
80
|
+
// Generate Torus vertices
|
|
81
|
+
for (let i = 0; i < CONFIG.rows; i++) {
|
|
82
|
+
const u = (i / CONFIG.rows) * Math.PI * 2;
|
|
83
|
+
|
|
84
|
+
for (let j = 0; j < CONFIG.cols; j++) {
|
|
85
|
+
const v = (j / CONFIG.cols) * Math.PI * 2;
|
|
86
|
+
|
|
87
|
+
// Torus parametric equation
|
|
88
|
+
// x = (R + r * cos(v)) * cos(u)
|
|
89
|
+
// y = (R + r * cos(v)) * sin(u)
|
|
90
|
+
// z = r * sin(v)
|
|
91
|
+
|
|
92
|
+
const r = CONFIG.minorRadius;
|
|
93
|
+
const R = CONFIG.majorRadius;
|
|
94
|
+
|
|
95
|
+
const x = (R + r * Math.cos(v)) * Math.cos(u);
|
|
96
|
+
const y = (R + r * Math.cos(v)) * Math.sin(u);
|
|
97
|
+
const z = r * Math.sin(v);
|
|
98
|
+
|
|
99
|
+
this.vertices.push(new Vec3(x, y, z));
|
|
100
|
+
|
|
101
|
+
// Generate indices for wireframe (quads)
|
|
102
|
+
const nextI = (i + 1) % CONFIG.rows;
|
|
103
|
+
const nextJ = (j + 1) % CONFIG.cols;
|
|
104
|
+
|
|
105
|
+
const current = i * CONFIG.cols + j;
|
|
106
|
+
const right = nextI * CONFIG.cols + j;
|
|
107
|
+
const down = i * CONFIG.cols + nextJ;
|
|
108
|
+
|
|
109
|
+
// Horizontal line
|
|
110
|
+
this.indices.push([current, right]);
|
|
111
|
+
// Vertical line
|
|
112
|
+
this.indices.push([current, down]);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
update(dt) {
|
|
118
|
+
this.time += dt;
|
|
119
|
+
|
|
120
|
+
// Update dimensions in case of resize
|
|
121
|
+
this.width = this.game.width;
|
|
122
|
+
this.height = this.game.height;
|
|
123
|
+
|
|
124
|
+
// Responsive scaling: Adjust radius based on screen size
|
|
125
|
+
const minDim = Math.min(this.width, this.height);
|
|
126
|
+
const scaleFactor = minDim / 1000; // Base scale on ~1000px screen
|
|
127
|
+
|
|
128
|
+
// Smoothly interpolate the actual drawing scale
|
|
129
|
+
// (We'll use a local property for rendering scale to avoid rebuilding mesh)
|
|
130
|
+
this.renderScale = scaleFactor;
|
|
131
|
+
|
|
132
|
+
// Smooth rotation
|
|
133
|
+
this.rotation.x += (this.targetRotation.x - this.rotation.x) * 0.1;
|
|
134
|
+
this.rotation.y += (this.targetRotation.y - this.rotation.y) * 0.1;
|
|
135
|
+
|
|
136
|
+
// Auto rotate
|
|
137
|
+
if (this.autoRotate) {
|
|
138
|
+
this.targetRotation.y += dt * 0.2;
|
|
139
|
+
this.targetRotation.x += dt * 0.1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Decay energy smoothly toward 0
|
|
143
|
+
this.energy *= 0.95;
|
|
144
|
+
if (this.energy < 0.001) this.energy = 0;
|
|
145
|
+
|
|
146
|
+
// Interpolate noise strength: base (calm) + energy * (max - base)
|
|
147
|
+
const targetStrength = CONFIG.noiseStrengthBase + this.energy * (CONFIG.noiseStrengthMax - CONFIG.noiseStrengthBase);
|
|
148
|
+
this.noiseStrength += (targetStrength - this.noiseStrength) * 0.1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
render(ctx, width, height) {
|
|
152
|
+
const cx = width / 2;
|
|
153
|
+
const cy = height / 2;
|
|
154
|
+
|
|
155
|
+
// Clear
|
|
156
|
+
ctx.fillStyle = CONFIG.bgColor;
|
|
157
|
+
ctx.fillRect(0, 0, width, height);
|
|
158
|
+
|
|
159
|
+
// Pre-calculate rotation matrices
|
|
160
|
+
const cosX = Math.cos(this.rotation.x);
|
|
161
|
+
const sinX = Math.sin(this.rotation.x);
|
|
162
|
+
const cosY = Math.cos(this.rotation.y);
|
|
163
|
+
const sinY = Math.sin(this.rotation.y);
|
|
164
|
+
|
|
165
|
+
// Transform and project vertices
|
|
166
|
+
const projectedVertices = new Array(this.vertices.length);
|
|
167
|
+
|
|
168
|
+
for (let i = 0; i < this.vertices.length; i++) {
|
|
169
|
+
const v = this.vertices[i];
|
|
170
|
+
|
|
171
|
+
// 1. Apply Noise Displacement
|
|
172
|
+
// We calculate noise based on the original position + time
|
|
173
|
+
const nX = v.x * CONFIG.noiseScale;
|
|
174
|
+
const nY = v.y * CONFIG.noiseScale;
|
|
175
|
+
const nZ = v.z * CONFIG.noiseScale;
|
|
176
|
+
const t = this.time * CONFIG.noiseSpeed;
|
|
177
|
+
|
|
178
|
+
const noiseVal = Noise.simplex3(nX + t, nY + t, nZ);
|
|
179
|
+
const displacement = noiseVal * this.noiseStrength;
|
|
180
|
+
|
|
181
|
+
// Displace along the normal (approximated by position for torus)
|
|
182
|
+
// Or just simple directional displacement for "glitch" look
|
|
183
|
+
let dx = v.x + displacement * (v.x / CONFIG.majorRadius);
|
|
184
|
+
let dy = v.y + displacement * (v.y / CONFIG.majorRadius);
|
|
185
|
+
let dz = v.z + displacement * (v.z / CONFIG.minorRadius);
|
|
186
|
+
|
|
187
|
+
// 2. Rotate
|
|
188
|
+
// Rotate around Y
|
|
189
|
+
let x1 = dx * cosY - dz * sinY;
|
|
190
|
+
let z1 = dz * cosY + dx * sinY;
|
|
191
|
+
let y1 = dy;
|
|
192
|
+
|
|
193
|
+
// Rotate around X
|
|
194
|
+
let y2 = y1 * cosX - z1 * sinX;
|
|
195
|
+
let z2 = z1 * cosX + y1 * sinX;
|
|
196
|
+
let x2 = x1;
|
|
197
|
+
|
|
198
|
+
// 3. Project
|
|
199
|
+
// Apply responsive scale factor to coordinates before projection
|
|
200
|
+
const sx = x2 * (this.renderScale || 1);
|
|
201
|
+
const sy = y2 * (this.renderScale || 1);
|
|
202
|
+
const sz = z2 * (this.renderScale || 1);
|
|
203
|
+
|
|
204
|
+
const scale = CONFIG.fov / (CONFIG.fov + sz + 400);
|
|
205
|
+
const px = sx * scale + cx;
|
|
206
|
+
const py = sy * scale + cy;
|
|
207
|
+
|
|
208
|
+
projectedVertices[i] = { x: px, y: py, z: sz, scale: scale };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Draw edges
|
|
212
|
+
ctx.lineWidth = 2 * (this.renderScale || 1); // Scale line width too
|
|
213
|
+
|
|
214
|
+
// We can batch stroke calls by color for performance
|
|
215
|
+
// Simple depth sorting approach: draw lines based on average Z of endpoints
|
|
216
|
+
|
|
217
|
+
for (let i = 0; i < this.indices.length; i++) {
|
|
218
|
+
const [idx1, idx2] = this.indices[i];
|
|
219
|
+
const p1 = projectedVertices[idx1];
|
|
220
|
+
const p2 = projectedVertices[idx2];
|
|
221
|
+
|
|
222
|
+
// Simple culling
|
|
223
|
+
if (p1.z < -CONFIG.fov + 10 || p2.z < -CONFIG.fov + 10) continue;
|
|
224
|
+
|
|
225
|
+
// Depth color
|
|
226
|
+
const avgZ = (p1.z + p2.z) / 2;
|
|
227
|
+
// Map Z (-200 to 200) to hue/lightness
|
|
228
|
+
// Close = Bright, Far = Dim
|
|
229
|
+
const depthAlpha = Math.max(0.1, Math.min(1, (300 - avgZ) / 500));
|
|
230
|
+
|
|
231
|
+
// Color shift based on position
|
|
232
|
+
const hue = (CONFIG.baseColor.h + avgZ * 0.2 + this.time * 10) % 360;
|
|
233
|
+
|
|
234
|
+
ctx.strokeStyle = `hsla(${hue}, ${CONFIG.baseColor.s}%, ${CONFIG.baseColor.l}%, ${depthAlpha})`;
|
|
235
|
+
|
|
236
|
+
ctx.beginPath();
|
|
237
|
+
ctx.moveTo(p1.x, p1.y);
|
|
238
|
+
ctx.lineTo(p2.x, p2.y);
|
|
239
|
+
ctx.stroke();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Initialize
|
|
245
|
+
window.addEventListener("load", () => {
|
|
246
|
+
const canvas = document.getElementById("game");
|
|
247
|
+
if (!canvas) return;
|
|
248
|
+
|
|
249
|
+
const game = gcanvas({ canvas, bg: CONFIG.bgColor, fluid: true });
|
|
250
|
+
const gameInstance = game.game;
|
|
251
|
+
|
|
252
|
+
const flux = new WireframeFlux(gameInstance);
|
|
253
|
+
|
|
254
|
+
// Custom render loop
|
|
255
|
+
gameInstance.clear = function() {
|
|
256
|
+
flux.render(this.ctx, this.width, this.height);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
game.on("update", (dt) => {
|
|
260
|
+
flux.update(dt);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Interaction
|
|
264
|
+
let isDragging = false;
|
|
265
|
+
let lastX = 0;
|
|
266
|
+
let lastY = 0;
|
|
267
|
+
|
|
268
|
+
gameInstance.events.on("mousedown", (e) => {
|
|
269
|
+
isDragging = true;
|
|
270
|
+
lastX = e.x;
|
|
271
|
+
lastY = e.y;
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
gameInstance.events.on("mouseup", () => {
|
|
275
|
+
isDragging = false;
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
gameInstance.events.on("mousemove", (e) => {
|
|
279
|
+
if (isDragging) {
|
|
280
|
+
const dx = (e.x - lastX) * 0.01;
|
|
281
|
+
const dy = (e.y - lastY) * 0.01;
|
|
282
|
+
|
|
283
|
+
flux.targetRotation.y += dx;
|
|
284
|
+
flux.targetRotation.x += dy;
|
|
285
|
+
|
|
286
|
+
// Stop auto-rotation while dragging
|
|
287
|
+
flux.autoRotate = false;
|
|
288
|
+
|
|
289
|
+
lastX = e.x;
|
|
290
|
+
lastY = e.y;
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
gameInstance.events.on("mouseup", () => {
|
|
295
|
+
isDragging = false;
|
|
296
|
+
// Resume auto-rotation after a short delay or immediately?
|
|
297
|
+
// Let's resume it with current momentum perhaps, or just standard auto
|
|
298
|
+
setTimeout(() => { flux.autoRotate = true; }, 500);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Click for glitch intensity
|
|
302
|
+
gameInstance.events.on("click", () => {
|
|
303
|
+
// Add energy on click
|
|
304
|
+
flux.energy = Math.min(flux.energy + 0.4, 1.0);
|
|
305
|
+
CONFIG.baseColor.h = Math.random() * 360;
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
game.start();
|
|
309
|
+
});
|
|
310
|
+
|