@guinetik/gcanvas 1.0.4 → 1.0.5
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/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/coordinates.html +698 -0
- package/dist/cube3d.html +23 -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 +517 -0
- 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/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 +398 -0
- package/dist/isometric.html +34 -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/coordinates.js +840 -0
- package/dist/js/cube3d.js +789 -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/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 +863 -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/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/scene.js +304 -0
- package/dist/js/scenes.js +320 -0
- package/dist/js/schrodinger.js +410 -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/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/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/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/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
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Particle System Showcase
|
|
3
|
+
*
|
|
4
|
+
* Organized display of particle effects in labeled stations:
|
|
5
|
+
* - Fountain (water spray with gravity)
|
|
6
|
+
* - Fire (rising flames with color gradient)
|
|
7
|
+
* - Snow (gentle falling flakes)
|
|
8
|
+
* - Confetti (burst on click)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
Game,
|
|
13
|
+
GameObject,
|
|
14
|
+
Scene3D,
|
|
15
|
+
Rectangle,
|
|
16
|
+
TextShape,
|
|
17
|
+
Painter,
|
|
18
|
+
Camera3D,
|
|
19
|
+
ParticleSystem,
|
|
20
|
+
ParticleEmitter,
|
|
21
|
+
Updaters,
|
|
22
|
+
FPSCounter,
|
|
23
|
+
Position,
|
|
24
|
+
} from "/gcanvas.es.min.js";
|
|
25
|
+
|
|
26
|
+
const CONFIG = {
|
|
27
|
+
backgroundColor: "#0a0a12",
|
|
28
|
+
maxParticles: 20000, // Stress test: 20k particles
|
|
29
|
+
|
|
30
|
+
// Station layout (as ratios of screen size)
|
|
31
|
+
stationWidthRatio: 0.18, // 18% of screen width per station
|
|
32
|
+
stationHeightRatio: 0.45, // 45% of screen height
|
|
33
|
+
stationSpacingRatio: 0.03, // 3% of screen width between stations
|
|
34
|
+
minStationWidth: 120,
|
|
35
|
+
maxStationWidth: 220,
|
|
36
|
+
minStationHeight: 150,
|
|
37
|
+
maxStationHeight: 280,
|
|
38
|
+
|
|
39
|
+
// Fountain settings
|
|
40
|
+
fountain: {
|
|
41
|
+
rate: 150, // 4x more
|
|
42
|
+
velocity: { x: 0, y: -350, z: 0 },
|
|
43
|
+
velocitySpread: { x: 40, y: 20, z: 40 },
|
|
44
|
+
lifetime: { min: 2, max: 4 }, // Longer life
|
|
45
|
+
size: { min: 3, max: 6 },
|
|
46
|
+
color: { r: 100, g: 180, b: 255, a: 1 },
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
// Fire settings
|
|
50
|
+
fire: {
|
|
51
|
+
rate: 250, // Dense flames
|
|
52
|
+
velocity: { x: 0, y: -60, z: 0 },
|
|
53
|
+
velocitySpread: { x: 15, y: 10, z: 15 },
|
|
54
|
+
lifetime: { min: 1, max: 2 }, // Longer life
|
|
55
|
+
size: { min: 6, max: 12 },
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// Snow settings
|
|
59
|
+
snow: {
|
|
60
|
+
rate: 80, // 5x more
|
|
61
|
+
velocity: { x: 0, y: 40, z: 0 },
|
|
62
|
+
velocitySpread: { x: 10, y: 5, z: 10 },
|
|
63
|
+
lifetime: { min: 4, max: 7 }, // Longer life
|
|
64
|
+
size: { min: 2, max: 4 },
|
|
65
|
+
color: { r: 255, g: 255, b: 255, a: 0.9 },
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// Confetti burst settings
|
|
69
|
+
confetti: {
|
|
70
|
+
velocity: { x: 0, y: -150, z: 0 },
|
|
71
|
+
velocitySpread: { x: 100, y: 60, z: 100 },
|
|
72
|
+
lifetime: { min: 2, max: 4 }, // Longer life
|
|
73
|
+
size: { min: 5, max: 10 },
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Station box GameObject for displaying a particle effect
|
|
79
|
+
*/
|
|
80
|
+
class ParticleStation extends GameObject {
|
|
81
|
+
constructor(game, x, y, width, height, label) {
|
|
82
|
+
super(game, { x, y, width, height });
|
|
83
|
+
this.z = 0; // For Scene3D projection
|
|
84
|
+
this.label = label;
|
|
85
|
+
|
|
86
|
+
// Background box using Rectangle shape
|
|
87
|
+
this.bg = new Rectangle({
|
|
88
|
+
width,
|
|
89
|
+
height,
|
|
90
|
+
color: "rgba(255, 255, 255, 0.03)",
|
|
91
|
+
stroke: "rgba(255, 255, 255, 0.15)",
|
|
92
|
+
lineWidth: 1,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Label using TextShape
|
|
96
|
+
this.labelText = new TextShape(label, {
|
|
97
|
+
y: height / 2 - 15,
|
|
98
|
+
font: "12px monospace",
|
|
99
|
+
color: "#00FF00",
|
|
100
|
+
align: "center",
|
|
101
|
+
baseline: "middle",
|
|
102
|
+
});
|
|
103
|
+
this.labelText.x = x;
|
|
104
|
+
this.bg.x = x;
|
|
105
|
+
this.bg.y = y;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Update station size and position
|
|
110
|
+
*/
|
|
111
|
+
updateLayout(x, width, height) {
|
|
112
|
+
this.x = x;
|
|
113
|
+
this.width = width;
|
|
114
|
+
this.height = height;
|
|
115
|
+
|
|
116
|
+
// Update background rectangle
|
|
117
|
+
this.bg.width = width;
|
|
118
|
+
this.bg.height = height;
|
|
119
|
+
this.bg.x = x;
|
|
120
|
+
this.bg.y = this.y;
|
|
121
|
+
|
|
122
|
+
// Update label position
|
|
123
|
+
this.labelText.y = height / 2 - 15;
|
|
124
|
+
this.labelText.x = x;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Get emitter spawn position (center-bottom of station)
|
|
128
|
+
getEmitterPosition() {
|
|
129
|
+
return {
|
|
130
|
+
x: this.x,
|
|
131
|
+
y: this.y + this.height / 2 - 40,
|
|
132
|
+
z: 0,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Get top position for snow
|
|
137
|
+
getTopPosition() {
|
|
138
|
+
return {
|
|
139
|
+
x: this.x,
|
|
140
|
+
y: -this.height / 2 + 10,
|
|
141
|
+
z: 0,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Get bottom position for fountain/fire
|
|
146
|
+
getBottomPosition() {
|
|
147
|
+
return {
|
|
148
|
+
x: this.x,
|
|
149
|
+
y: this.height / 2 - 10,
|
|
150
|
+
z: 0,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Get center position for confetti
|
|
155
|
+
getCenterPosition() {
|
|
156
|
+
return { x: this.x, y: this.y, z: 0 };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
render() {
|
|
160
|
+
super.render();
|
|
161
|
+
this.bg.render();
|
|
162
|
+
this.labelText.render();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
class ParticlesShowcase extends Game {
|
|
167
|
+
constructor(canvas) {
|
|
168
|
+
super(canvas);
|
|
169
|
+
this.backgroundColor = CONFIG.backgroundColor;
|
|
170
|
+
this.enableFluidSize();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
init() {
|
|
174
|
+
super.init();
|
|
175
|
+
|
|
176
|
+
// Calculate initial sizes
|
|
177
|
+
this.updateScaledSizes();
|
|
178
|
+
|
|
179
|
+
// Setup Camera3D (subtle rotation for depth)
|
|
180
|
+
this.camera = new Camera3D({
|
|
181
|
+
rotationX: 0.2,
|
|
182
|
+
rotationY: 0,
|
|
183
|
+
perspective: 800,
|
|
184
|
+
autoRotate: false,
|
|
185
|
+
});
|
|
186
|
+
this.camera.enableMouseControl(this.canvas);
|
|
187
|
+
|
|
188
|
+
// Create stations layout
|
|
189
|
+
this.createStations();
|
|
190
|
+
|
|
191
|
+
// Create particle system with Camera3D
|
|
192
|
+
this.particles = new ParticleSystem(this, {
|
|
193
|
+
camera: this.camera,
|
|
194
|
+
depthSort: true,
|
|
195
|
+
maxParticles: CONFIG.maxParticles,
|
|
196
|
+
blendMode: "screen",
|
|
197
|
+
updaters: [
|
|
198
|
+
Updaters.velocity,
|
|
199
|
+
Updaters.lifetime,
|
|
200
|
+
Updaters.gravity(120),
|
|
201
|
+
Updaters.fadeOut,
|
|
202
|
+
this.fireColorUpdater.bind(this),
|
|
203
|
+
Updaters.shrink(0.2),
|
|
204
|
+
],
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Create emitters for each station
|
|
208
|
+
this.createEmitters();
|
|
209
|
+
|
|
210
|
+
this.pipeline.add(this.particles);
|
|
211
|
+
|
|
212
|
+
// Confetti emitter (for bursts, not continuous)
|
|
213
|
+
this.confettiEmitter = new ParticleEmitter({
|
|
214
|
+
position: { x: 0, y: 0, z: 0 },
|
|
215
|
+
velocity: CONFIG.confetti.velocity,
|
|
216
|
+
velocitySpread: CONFIG.confetti.velocitySpread,
|
|
217
|
+
lifetime: CONFIG.confetti.lifetime,
|
|
218
|
+
size: CONFIG.confetti.size,
|
|
219
|
+
color: { r: 255, g: 255, b: 0, a: 1 },
|
|
220
|
+
rate: 0,
|
|
221
|
+
shape: "triangle",
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Track emitter state
|
|
225
|
+
this.emittersActive = true;
|
|
226
|
+
|
|
227
|
+
// Click to burst confetti
|
|
228
|
+
this.canvas.addEventListener("click", (e) => this.handleClick(e));
|
|
229
|
+
|
|
230
|
+
// Space to toggle emitters
|
|
231
|
+
window.addEventListener("keydown", (e) => {
|
|
232
|
+
if (e.code === "Space") {
|
|
233
|
+
this.toggleEmitters();
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// FPS counter anchored to bottom right
|
|
238
|
+
this.fpsCounter = new FPSCounter(this, {
|
|
239
|
+
anchor: Position.BOTTOM_RIGHT,
|
|
240
|
+
anchorOffsetX: 0,
|
|
241
|
+
anchorOffsetY: 0,
|
|
242
|
+
color: "#0f0",
|
|
243
|
+
});
|
|
244
|
+
this.pipeline.add(this.fpsCounter);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
createStations() {
|
|
248
|
+
const labels = ["Fountain", "Fire", "Snow", "Confetti"];
|
|
249
|
+
const totalWidth =
|
|
250
|
+
labels.length * this.stationWidth +
|
|
251
|
+
(labels.length - 1) * this.stationSpacing;
|
|
252
|
+
const startX = -totalWidth / 2 + this.stationWidth / 2;
|
|
253
|
+
|
|
254
|
+
// Create Scene3D to hold stations with camera projection
|
|
255
|
+
this.stationScene = new Scene3D(this, {
|
|
256
|
+
x: this.width / 2,
|
|
257
|
+
y: this.height / 2,
|
|
258
|
+
camera: this.camera,
|
|
259
|
+
depthSort: false, // Stations are all at z=0
|
|
260
|
+
scaleByDepth: true,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
this.stations = labels.map((label, i) => {
|
|
264
|
+
const x = startX + i * (this.stationWidth + this.stationSpacing);
|
|
265
|
+
const station = new ParticleStation(
|
|
266
|
+
this,
|
|
267
|
+
x,
|
|
268
|
+
0,
|
|
269
|
+
this.stationWidth,
|
|
270
|
+
this.stationHeight,
|
|
271
|
+
label,
|
|
272
|
+
);
|
|
273
|
+
this.stationScene.add(station);
|
|
274
|
+
return station;
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Add to pipeline (will render before particles)
|
|
278
|
+
this.pipeline.add(this.stationScene);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
createEmitters() {
|
|
282
|
+
// Fountain (station 0) - bottom of station, shoots up
|
|
283
|
+
const fountainStation = this.stations[0];
|
|
284
|
+
const fountainPos = fountainStation.getBottomPosition();
|
|
285
|
+
this.particles.addEmitter(
|
|
286
|
+
"fountain",
|
|
287
|
+
new ParticleEmitter({
|
|
288
|
+
position: { x: fountainPos.x, y: fountainPos.y + 20, z: 0 },
|
|
289
|
+
spread: { x: 5, y: 0, z: 5 },
|
|
290
|
+
velocity: CONFIG.fountain.velocity,
|
|
291
|
+
velocitySpread: CONFIG.fountain.velocitySpread,
|
|
292
|
+
lifetime: CONFIG.fountain.lifetime,
|
|
293
|
+
size: CONFIG.fountain.size,
|
|
294
|
+
color: CONFIG.fountain.color,
|
|
295
|
+
rate: CONFIG.fountain.rate,
|
|
296
|
+
shape: "circle",
|
|
297
|
+
}),
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// Fire (station 1) - bottom of station, flames rise up
|
|
301
|
+
const fireStation = this.stations[1];
|
|
302
|
+
const firePos = fireStation.getBottomPosition();
|
|
303
|
+
this.particles.addEmitter(
|
|
304
|
+
"fire",
|
|
305
|
+
new ParticleEmitter({
|
|
306
|
+
position: { x: firePos.x, y: firePos.y, z: 0 },
|
|
307
|
+
spread: { x: 60, y: 0, z: 30 },
|
|
308
|
+
velocity: CONFIG.fire.velocity,
|
|
309
|
+
velocitySpread: { x: 40, y: 15, z: 40 },
|
|
310
|
+
lifetime: CONFIG.fire.lifetime,
|
|
311
|
+
size: CONFIG.fire.size,
|
|
312
|
+
color: { r: 255, g: 120, b: 40, a: 1 },
|
|
313
|
+
rate: CONFIG.fire.rate,
|
|
314
|
+
shape: "square",
|
|
315
|
+
}),
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
// Snow (station 2) - top of station, falls down
|
|
319
|
+
const snowStation = this.stations[2];
|
|
320
|
+
const snowPos = snowStation.getTopPosition();
|
|
321
|
+
this.particles.addEmitter(
|
|
322
|
+
"snow",
|
|
323
|
+
new ParticleEmitter({
|
|
324
|
+
position: { x: snowPos.x, y: snowPos.y - 10, z: 0 },
|
|
325
|
+
spread: { x: this.stationWidth * 0.4, y: 0, z: 30 },
|
|
326
|
+
velocity: CONFIG.snow.velocity,
|
|
327
|
+
velocitySpread: CONFIG.snow.velocitySpread,
|
|
328
|
+
lifetime: CONFIG.snow.lifetime,
|
|
329
|
+
size: CONFIG.snow.size,
|
|
330
|
+
color: CONFIG.snow.color,
|
|
331
|
+
rate: CONFIG.snow.rate,
|
|
332
|
+
shape: "circle",
|
|
333
|
+
}),
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
fireColorUpdater(p, dt) {
|
|
338
|
+
// Only apply to fire particles (squares)
|
|
339
|
+
if (p.shape !== "square") return;
|
|
340
|
+
|
|
341
|
+
const t = p.progress;
|
|
342
|
+
// Orange -> Red -> Dark
|
|
343
|
+
p.color.r = Math.floor(255 * (1 - t * 0.2));
|
|
344
|
+
p.color.g = Math.floor(120 * (1 - t * 0.9));
|
|
345
|
+
p.color.b = Math.floor(40 * (1 - t));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
handleClick(e) {
|
|
349
|
+
// Confetti always spawns at its station center
|
|
350
|
+
const confettiStation = this.stations[3];
|
|
351
|
+
const pos = confettiStation.getCenterPosition();
|
|
352
|
+
|
|
353
|
+
this.confettiEmitter.position.x = pos.x;
|
|
354
|
+
this.confettiEmitter.position.y = pos.y;
|
|
355
|
+
this.confettiEmitter.position.z = pos.z;
|
|
356
|
+
|
|
357
|
+
// Randomize confetti colors
|
|
358
|
+
const colors = [
|
|
359
|
+
{ r: 255, g: 80, b: 80 },
|
|
360
|
+
{ r: 80, g: 255, b: 80 },
|
|
361
|
+
{ r: 80, g: 80, b: 255 },
|
|
362
|
+
{ r: 255, g: 255, b: 80 },
|
|
363
|
+
{ r: 255, g: 80, b: 255 },
|
|
364
|
+
{ r: 80, g: 255, b: 255 },
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
// Burst with random colors
|
|
368
|
+
for (let i = 0; i < 40; i++) {
|
|
369
|
+
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
370
|
+
this.confettiEmitter.color = { ...color, a: 1 };
|
|
371
|
+
this.particles.burst(1, this.confettiEmitter);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
toggleEmitters() {
|
|
376
|
+
this.emittersActive = !this.emittersActive;
|
|
377
|
+
|
|
378
|
+
for (const emitter of this.particles.emitters.values()) {
|
|
379
|
+
emitter.active = this.emittersActive;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Calculate responsive station sizes based on screen dimensions
|
|
385
|
+
*/
|
|
386
|
+
updateScaledSizes() {
|
|
387
|
+
// Calculate station dimensions from ratios with min/max bounds
|
|
388
|
+
this.stationWidth = Math.max(
|
|
389
|
+
CONFIG.minStationWidth,
|
|
390
|
+
Math.min(CONFIG.maxStationWidth, this.width * CONFIG.stationWidthRatio),
|
|
391
|
+
);
|
|
392
|
+
this.stationHeight = Math.max(
|
|
393
|
+
CONFIG.minStationHeight,
|
|
394
|
+
Math.min(CONFIG.maxStationHeight, this.height * CONFIG.stationHeightRatio),
|
|
395
|
+
);
|
|
396
|
+
this.stationSpacing = this.width * CONFIG.stationSpacingRatio;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Update station positions and sizes, then update emitters to match
|
|
401
|
+
*/
|
|
402
|
+
updateStationLayout() {
|
|
403
|
+
if (!this.stations) return;
|
|
404
|
+
|
|
405
|
+
const totalWidth =
|
|
406
|
+
this.stations.length * this.stationWidth +
|
|
407
|
+
(this.stations.length - 1) * this.stationSpacing;
|
|
408
|
+
const startX = -totalWidth / 2 + this.stationWidth / 2;
|
|
409
|
+
|
|
410
|
+
// Update each station's position and size
|
|
411
|
+
for (let i = 0; i < this.stations.length; i++) {
|
|
412
|
+
const x = startX + i * (this.stationWidth + this.stationSpacing);
|
|
413
|
+
this.stations[i].updateLayout(x, this.stationWidth, this.stationHeight);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Update emitter positions to follow stations
|
|
417
|
+
this.updateEmitterPositions();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Update emitter positions to match station positions
|
|
422
|
+
*/
|
|
423
|
+
updateEmitterPositions() {
|
|
424
|
+
if (!this.particles || !this.stations) return;
|
|
425
|
+
|
|
426
|
+
// Fountain emitter
|
|
427
|
+
const fountainEmitter = this.particles.emitters.get("fountain");
|
|
428
|
+
if (fountainEmitter) {
|
|
429
|
+
const pos = this.stations[0].getBottomPosition();
|
|
430
|
+
fountainEmitter.position.x = pos.x;
|
|
431
|
+
fountainEmitter.position.y = pos.y + 20;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Fire emitter
|
|
435
|
+
const fireEmitter = this.particles.emitters.get("fire");
|
|
436
|
+
if (fireEmitter) {
|
|
437
|
+
const pos = this.stations[1].getBottomPosition();
|
|
438
|
+
fireEmitter.position.x = pos.x;
|
|
439
|
+
fireEmitter.position.y = pos.y;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Snow emitter
|
|
443
|
+
const snowEmitter = this.particles.emitters.get("snow");
|
|
444
|
+
if (snowEmitter) {
|
|
445
|
+
const pos = this.stations[2].getTopPosition();
|
|
446
|
+
snowEmitter.position.x = pos.x;
|
|
447
|
+
snowEmitter.position.y = pos.y - 10;
|
|
448
|
+
snowEmitter.spread.x = this.stationWidth * 0.4;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Handle window resize
|
|
454
|
+
*/
|
|
455
|
+
onResize() {
|
|
456
|
+
this.updateScaledSizes();
|
|
457
|
+
this.updateStationLayout();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
update(dt) {
|
|
461
|
+
super.update(dt);
|
|
462
|
+
this.camera.update(dt);
|
|
463
|
+
|
|
464
|
+
// Keep stationScene centered
|
|
465
|
+
this.stationScene.x = this.width / 2;
|
|
466
|
+
this.stationScene.y = this.height / 2;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
render() {
|
|
470
|
+
super.render();
|
|
471
|
+
// Stations are rendered by Scene3D through the pipeline
|
|
472
|
+
// Draw HUD on top
|
|
473
|
+
this.drawHUD();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
drawHUD() {
|
|
477
|
+
Painter.useCtx((ctx) => {
|
|
478
|
+
ctx.font = "11px monospace";
|
|
479
|
+
ctx.textAlign = "left";
|
|
480
|
+
|
|
481
|
+
// Particle count
|
|
482
|
+
ctx.fillStyle = "#666";
|
|
483
|
+
ctx.fillText(
|
|
484
|
+
`Particles: ${this.particles.particleCount} / ${CONFIG.maxParticles}`,
|
|
485
|
+
15,
|
|
486
|
+
this.height - 45,
|
|
487
|
+
);
|
|
488
|
+
ctx.fillText(`Pool: ${this.particles.poolSize}`, 15, this.height - 30);
|
|
489
|
+
|
|
490
|
+
// Emitter status
|
|
491
|
+
ctx.fillStyle = this.emittersActive ? "#8a8" : "#a88";
|
|
492
|
+
ctx.fillText(
|
|
493
|
+
`Emitters: ${this.emittersActive ? "ON" : "OFF"} (Space)`,
|
|
494
|
+
15,
|
|
495
|
+
this.height - 15,
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
// Instructions (bottom center)
|
|
499
|
+
ctx.textAlign = "center";
|
|
500
|
+
ctx.fillStyle = "#444";
|
|
501
|
+
ctx.fillText(
|
|
502
|
+
"click to burst confetti | drag to orbit",
|
|
503
|
+
this.width / 2,
|
|
504
|
+
this.height - 15,
|
|
505
|
+
);
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
window.addEventListener("load", () => {
|
|
511
|
+
const canvas = document.getElementById("game");
|
|
512
|
+
const demo = new ParticlesShowcase(canvas);
|
|
513
|
+
demo.start();
|
|
514
|
+
});
|