@guinetik/gcanvas 1.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/.github/workflows/release.yaml +70 -0
- package/.jshintrc +4 -0
- package/.vscode/settings.json +22 -0
- package/CLAUDE.md +310 -0
- package/blackhole.jpg +0 -0
- package/demo.png +0 -0
- package/demos/CNAME +1 -0
- package/demos/animations.html +31 -0
- package/demos/basic.html +38 -0
- package/demos/baskara.html +31 -0
- package/demos/bezier.html +35 -0
- package/demos/beziersignature.html +29 -0
- package/demos/blackhole.html +28 -0
- package/demos/blob.html +35 -0
- package/demos/demos.css +289 -0
- package/demos/easing.html +28 -0
- package/demos/events.html +195 -0
- package/demos/fluent.html +647 -0
- package/demos/fractals.html +36 -0
- package/demos/genart.html +26 -0
- package/demos/gendream.html +26 -0
- package/demos/group.html +36 -0
- package/demos/home.html +587 -0
- package/demos/index.html +364 -0
- package/demos/isometric.html +34 -0
- package/demos/js/animations.js +452 -0
- package/demos/js/basic.js +204 -0
- package/demos/js/baskara.js +751 -0
- package/demos/js/bezier.js +692 -0
- package/demos/js/beziersignature.js +241 -0
- package/demos/js/blackhole/accretiondisk.obj.js +379 -0
- package/demos/js/blackhole/blackhole.obj.js +318 -0
- package/demos/js/blackhole/index.js +409 -0
- package/demos/js/blackhole/particle.js +56 -0
- package/demos/js/blackhole/starfield.obj.js +218 -0
- package/demos/js/blob.js +2263 -0
- package/demos/js/easing.js +477 -0
- package/demos/js/fluent.js +183 -0
- package/demos/js/fractals.js +931 -0
- package/demos/js/fractalworker.js +93 -0
- package/demos/js/genart.js +268 -0
- package/demos/js/gendream.js +209 -0
- package/demos/js/group.js +140 -0
- package/demos/js/info-toggle.js +25 -0
- package/demos/js/isometric.js +863 -0
- package/demos/js/kerr.js +1556 -0
- package/demos/js/lavalamp.js +590 -0
- package/demos/js/layout.js +354 -0
- package/demos/js/mondrian.js +285 -0
- package/demos/js/opacity.js +275 -0
- package/demos/js/painter.js +484 -0
- package/demos/js/particles-showcase.js +514 -0
- package/demos/js/particles.js +299 -0
- package/demos/js/patterns.js +397 -0
- package/demos/js/penrose/artifact.js +69 -0
- package/demos/js/penrose/blackhole.js +121 -0
- package/demos/js/penrose/constants.js +73 -0
- package/demos/js/penrose/game.js +943 -0
- package/demos/js/penrose/lore.js +278 -0
- package/demos/js/penrose/penrosescene.js +892 -0
- package/demos/js/penrose/ship.js +216 -0
- package/demos/js/penrose/sounds.js +211 -0
- package/demos/js/penrose/voidparticle.js +55 -0
- package/demos/js/penrose/voidscene.js +258 -0
- package/demos/js/penrose/voidship.js +144 -0
- package/demos/js/penrose/wormhole.js +46 -0
- package/demos/js/pipeline.js +555 -0
- package/demos/js/scene.js +304 -0
- package/demos/js/scenes.js +320 -0
- package/demos/js/schrodinger.js +410 -0
- package/demos/js/schwarzschild.js +1023 -0
- package/demos/js/shapes.js +628 -0
- package/demos/js/space/alien.js +171 -0
- package/demos/js/space/boom.js +98 -0
- package/demos/js/space/boss.js +353 -0
- package/demos/js/space/buff.js +73 -0
- package/demos/js/space/bullet.js +102 -0
- package/demos/js/space/constants.js +85 -0
- package/demos/js/space/game.js +1884 -0
- package/demos/js/space/hud.js +112 -0
- package/demos/js/space/laserbeam.js +179 -0
- package/demos/js/space/lightning.js +277 -0
- package/demos/js/space/minion.js +192 -0
- package/demos/js/space/missile.js +212 -0
- package/demos/js/space/player.js +430 -0
- package/demos/js/space/powerup.js +90 -0
- package/demos/js/space/starfield.js +58 -0
- package/demos/js/space/starpower.js +90 -0
- package/demos/js/spacetime.js +559 -0
- package/demos/js/svgtween.js +204 -0
- package/demos/js/tde/accretiondisk.js +418 -0
- package/demos/js/tde/blackhole.js +219 -0
- package/demos/js/tde/blackholescene.js +209 -0
- package/demos/js/tde/config.js +59 -0
- package/demos/js/tde/index.js +695 -0
- package/demos/js/tde/jets.js +290 -0
- package/demos/js/tde/lensedstarfield.js +147 -0
- package/demos/js/tde/tdestar.js +317 -0
- package/demos/js/tde/tidalstream.js +356 -0
- package/demos/js/tde_old/blackhole.obj.js +354 -0
- package/demos/js/tde_old/debris.obj.js +791 -0
- package/demos/js/tde_old/flare.obj.js +239 -0
- package/demos/js/tde_old/index.js +448 -0
- package/demos/js/tde_old/star.obj.js +812 -0
- package/demos/js/tiles.js +312 -0
- package/demos/js/tweendemo.js +79 -0
- package/demos/js/visibility.js +102 -0
- package/demos/kerr.html +28 -0
- package/demos/lavalamp.html +27 -0
- package/demos/layouts.html +37 -0
- package/demos/logo.svg +4 -0
- package/demos/loop.html +84 -0
- package/demos/mondrian.html +32 -0
- package/demos/og_image.png +0 -0
- package/demos/opacity.html +36 -0
- package/demos/painter.html +39 -0
- package/demos/particles-showcase.html +28 -0
- package/demos/particles.html +24 -0
- package/demos/patterns.html +33 -0
- package/demos/penrose-game.html +31 -0
- package/demos/pipeline.html +737 -0
- package/demos/scene.html +33 -0
- package/demos/scenes.html +96 -0
- package/demos/schrodinger.html +27 -0
- package/demos/schwarzschild.html +27 -0
- package/demos/shapes.html +16 -0
- package/demos/space.html +85 -0
- package/demos/spacetime.html +27 -0
- package/demos/svgtween.html +29 -0
- package/demos/tde.html +28 -0
- package/demos/tiles.html +28 -0
- package/demos/transforms.html +400 -0
- package/demos/tween.html +45 -0
- package/demos/visibility.html +33 -0
- package/disk_example.png +0 -0
- package/docs/README.md +222 -0
- package/docs/concepts/architecture-overview.md +204 -0
- package/docs/concepts/lifecycle.md +255 -0
- package/docs/concepts/rendering-pipeline.md +279 -0
- package/docs/concepts/tde-zorder.md +106 -0
- package/docs/concepts/two-layer-architecture.md +229 -0
- package/docs/getting-started/first-game.md +354 -0
- package/docs/getting-started/hello-world.md +269 -0
- package/docs/getting-started/installation.md +157 -0
- package/docs/modules/collision/README.md +453 -0
- package/docs/modules/fluent/README.md +1075 -0
- package/docs/modules/game/README.md +303 -0
- package/docs/modules/isometric-camera.md +210 -0
- package/docs/modules/isometric.md +275 -0
- package/docs/modules/painter/README.md +328 -0
- package/docs/modules/particle/README.md +559 -0
- package/docs/modules/shapes/README.md +221 -0
- package/docs/modules/shapes/base/euclidian.md +123 -0
- package/docs/modules/shapes/base/geometry2d.md +204 -0
- package/docs/modules/shapes/base/renderable.md +215 -0
- package/docs/modules/shapes/base/shape.md +262 -0
- package/docs/modules/shapes/base/transformable.md +243 -0
- package/docs/modules/shapes/hierarchy.md +218 -0
- package/docs/modules/state/README.md +577 -0
- package/docs/modules/util/README.md +99 -0
- package/docs/modules/util/camera3d.md +412 -0
- package/docs/modules/util/scene3d.md +395 -0
- package/index.html +17 -0
- package/jsdoc.json +50 -0
- package/package.json +55 -0
- package/readme.md +599 -0
- package/scripts/build-demo.js +69 -0
- package/scripts/bundle4llm.js +276 -0
- package/scripts/clearconsole.js +48 -0
- package/src/collision/collision-system.js +332 -0
- package/src/collision/collision.js +303 -0
- package/src/collision/index.js +10 -0
- package/src/fluent/fluent-game.js +430 -0
- package/src/fluent/fluent-go.js +1060 -0
- package/src/fluent/fluent-layer.js +152 -0
- package/src/fluent/fluent-scene.js +291 -0
- package/src/fluent/index.js +98 -0
- package/src/fluent/sketch.js +380 -0
- package/src/game/game.js +467 -0
- package/src/game/index.js +49 -0
- package/src/game/objects/go.js +220 -0
- package/src/game/objects/imagego.js +30 -0
- package/src/game/objects/index.js +54 -0
- package/src/game/objects/isometric-scene.js +260 -0
- package/src/game/objects/layoutscene.js +549 -0
- package/src/game/objects/scene.js +175 -0
- package/src/game/objects/scene3d.js +118 -0
- package/src/game/objects/text.js +221 -0
- package/src/game/objects/wrapper.js +232 -0
- package/src/game/pipeline.js +243 -0
- package/src/game/ui/button.js +396 -0
- package/src/game/ui/cursor.js +93 -0
- package/src/game/ui/fps.js +91 -0
- package/src/game/ui/index.js +5 -0
- package/src/game/ui/togglebutton.js +93 -0
- package/src/game/ui/tooltip.js +249 -0
- package/src/index.js +25 -0
- package/src/io/events.js +20 -0
- package/src/io/index.js +86 -0
- package/src/io/input.js +70 -0
- package/src/io/keys.js +152 -0
- package/src/io/mouse.js +61 -0
- package/src/io/touch.js +39 -0
- package/src/logger/debugtab.js +138 -0
- package/src/logger/index.js +3 -0
- package/src/logger/loggable.js +47 -0
- package/src/logger/logger.js +113 -0
- package/src/math/complex.js +37 -0
- package/src/math/constants.js +1 -0
- package/src/math/fractal.js +1271 -0
- package/src/math/gr.js +201 -0
- package/src/math/heat.js +202 -0
- package/src/math/index.js +12 -0
- package/src/math/noise.js +433 -0
- package/src/math/orbital.js +191 -0
- package/src/math/patterns.js +1339 -0
- package/src/math/penrose.js +259 -0
- package/src/math/quantum.js +115 -0
- package/src/math/random.js +195 -0
- package/src/math/tensor.js +1009 -0
- package/src/mixins/anchor.js +131 -0
- package/src/mixins/draggable.js +72 -0
- package/src/mixins/index.js +2 -0
- package/src/motion/bezier.js +132 -0
- package/src/motion/bounce.js +58 -0
- package/src/motion/easing.js +349 -0
- package/src/motion/float.js +130 -0
- package/src/motion/follow.js +125 -0
- package/src/motion/hop.js +52 -0
- package/src/motion/index.js +82 -0
- package/src/motion/motion.js +1124 -0
- package/src/motion/orbit.js +49 -0
- package/src/motion/oscillate.js +39 -0
- package/src/motion/parabolic.js +141 -0
- package/src/motion/patrol.js +147 -0
- package/src/motion/pendulum.js +48 -0
- package/src/motion/pulse.js +88 -0
- package/src/motion/shake.js +83 -0
- package/src/motion/spiral.js +144 -0
- package/src/motion/spring.js +150 -0
- package/src/motion/swing.js +47 -0
- package/src/motion/tween.js +92 -0
- package/src/motion/tweenetik.js +139 -0
- package/src/motion/waypoint.js +210 -0
- package/src/painter/index.js +8 -0
- package/src/painter/painter.colors.js +331 -0
- package/src/painter/painter.effects.js +230 -0
- package/src/painter/painter.img.js +229 -0
- package/src/painter/painter.js +295 -0
- package/src/painter/painter.lines.js +189 -0
- package/src/painter/painter.opacity.js +41 -0
- package/src/painter/painter.shapes.js +277 -0
- package/src/painter/painter.text.js +273 -0
- package/src/particle/emitter.js +124 -0
- package/src/particle/index.js +11 -0
- package/src/particle/particle-system.js +322 -0
- package/src/particle/particle.js +71 -0
- package/src/particle/updaters.js +170 -0
- package/src/shapes/arc.js +43 -0
- package/src/shapes/arrow.js +33 -0
- package/src/shapes/bezier.js +42 -0
- package/src/shapes/circle.js +62 -0
- package/src/shapes/clouds.js +56 -0
- package/src/shapes/cone.js +219 -0
- package/src/shapes/cross.js +70 -0
- package/src/shapes/cube.js +244 -0
- package/src/shapes/cylinder.js +254 -0
- package/src/shapes/diamond.js +48 -0
- package/src/shapes/euclidian.js +111 -0
- package/src/shapes/figure.js +115 -0
- package/src/shapes/geometry.js +220 -0
- package/src/shapes/group.js +375 -0
- package/src/shapes/heart.js +42 -0
- package/src/shapes/hexagon.js +26 -0
- package/src/shapes/image.js +192 -0
- package/src/shapes/index.js +111 -0
- package/src/shapes/line.js +29 -0
- package/src/shapes/pattern.js +90 -0
- package/src/shapes/pin.js +44 -0
- package/src/shapes/poly.js +31 -0
- package/src/shapes/prism.js +226 -0
- package/src/shapes/rect.js +35 -0
- package/src/shapes/renderable.js +333 -0
- package/src/shapes/ring.js +26 -0
- package/src/shapes/roundrect.js +95 -0
- package/src/shapes/shape.js +117 -0
- package/src/shapes/slice.js +26 -0
- package/src/shapes/sphere.js +314 -0
- package/src/shapes/sphere3d.js +537 -0
- package/src/shapes/square.js +15 -0
- package/src/shapes/star.js +99 -0
- package/src/shapes/svg.js +408 -0
- package/src/shapes/text.js +553 -0
- package/src/shapes/traceable.js +83 -0
- package/src/shapes/transform.js +357 -0
- package/src/shapes/transformable.js +172 -0
- package/src/shapes/triangle.js +26 -0
- package/src/sound/index.js +17 -0
- package/src/sound/sound.js +473 -0
- package/src/sound/synth.analyzer.js +149 -0
- package/src/sound/synth.effects.js +207 -0
- package/src/sound/synth.envelope.js +59 -0
- package/src/sound/synth.js +229 -0
- package/src/sound/synth.musical.js +160 -0
- package/src/sound/synth.noise.js +85 -0
- package/src/sound/synth.oscillators.js +293 -0
- package/src/state/index.js +10 -0
- package/src/state/state-machine.js +371 -0
- package/src/util/camera3d.js +438 -0
- package/src/util/index.js +6 -0
- package/src/util/isometric-camera.js +235 -0
- package/src/util/layout.js +317 -0
- package/src/util/position.js +147 -0
- package/src/util/tasks.js +47 -0
- package/src/util/zindex.js +287 -0
- package/src/webgl/index.js +9 -0
- package/src/webgl/shaders/sphere-shaders.js +994 -0
- package/src/webgl/webgl-renderer.js +388 -0
- package/tde.png +0 -0
- package/test/math/orbital.test.js +61 -0
- package/test/math/tensor.test.js +114 -0
- package/test/particle/emitter.test.js +204 -0
- package/test/particle/particle-system.test.js +310 -0
- package/test/particle/particle.test.js +116 -0
- package/test/particle/updaters.test.js +386 -0
- package/test/setup.js +120 -0
- package/test/shapes/euclidian.test.js +44 -0
- package/test/shapes/geometry.test.js +86 -0
- package/test/shapes/group.test.js +86 -0
- package/test/shapes/rectangle.test.js +64 -0
- package/test/shapes/transform.test.js +379 -0
- package/test/util/camera3d.test.js +428 -0
- package/test/util/scene3d.test.js +352 -0
- package/types/collision.d.ts +249 -0
- package/types/common.d.ts +155 -0
- package/types/game.d.ts +497 -0
- package/types/index.d.ts +309 -0
- package/types/io.d.ts +188 -0
- package/types/logger.d.ts +127 -0
- package/types/math.d.ts +268 -0
- package/types/mixins.d.ts +92 -0
- package/types/motion.d.ts +678 -0
- package/types/painter.d.ts +378 -0
- package/types/shapes.d.ts +864 -0
- package/types/sound.d.ts +672 -0
- package/types/state.d.ts +251 -0
- package/types/util.d.ts +253 -0
- package/vite.config.js +50 -0
- package/vitest.config.js +13 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { ParticleEmitter } from "../../src/particle/emitter";
|
|
3
|
+
import { Particle } from "../../src/particle/particle";
|
|
4
|
+
|
|
5
|
+
describe("ParticleEmitter", () => {
|
|
6
|
+
describe("constructor", () => {
|
|
7
|
+
it("should initialize with default values", () => {
|
|
8
|
+
const emitter = new ParticleEmitter();
|
|
9
|
+
|
|
10
|
+
expect(emitter.rate).toBe(10);
|
|
11
|
+
expect(emitter.position).toEqual({ x: 0, y: 0, z: 0 });
|
|
12
|
+
expect(emitter.spread).toEqual({ x: 0, y: 0, z: 0 });
|
|
13
|
+
expect(emitter.velocity).toEqual({ x: 0, y: 0, z: 0 });
|
|
14
|
+
expect(emitter.velocitySpread).toEqual({ x: 0, y: 0, z: 0 });
|
|
15
|
+
expect(emitter.lifetime).toEqual({ min: 1, max: 2 });
|
|
16
|
+
expect(emitter.size).toEqual({ min: 1, max: 1 });
|
|
17
|
+
expect(emitter.color).toEqual({ r: 255, g: 255, b: 255, a: 1 });
|
|
18
|
+
expect(emitter.shape).toBe("circle");
|
|
19
|
+
expect(emitter.active).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should accept custom options", () => {
|
|
23
|
+
const emitter = new ParticleEmitter({
|
|
24
|
+
rate: 50,
|
|
25
|
+
position: { x: 100, y: 200 },
|
|
26
|
+
velocity: { y: -100 },
|
|
27
|
+
lifetime: { min: 0.5, max: 1.5 },
|
|
28
|
+
size: { min: 2, max: 5 },
|
|
29
|
+
color: { r: 255, g: 0, b: 0, a: 0.8 },
|
|
30
|
+
shape: "square",
|
|
31
|
+
active: false,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(emitter.rate).toBe(50);
|
|
35
|
+
expect(emitter.position).toEqual({ x: 100, y: 200, z: 0 });
|
|
36
|
+
expect(emitter.velocity).toEqual({ x: 0, y: -100, z: 0 });
|
|
37
|
+
expect(emitter.lifetime).toEqual({ min: 0.5, max: 1.5 });
|
|
38
|
+
expect(emitter.size).toEqual({ min: 2, max: 5 });
|
|
39
|
+
expect(emitter.color).toEqual({ r: 255, g: 0, b: 0, a: 0.8 });
|
|
40
|
+
expect(emitter.shape).toBe("square");
|
|
41
|
+
expect(emitter.active).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("emit", () => {
|
|
46
|
+
it("should initialize particle with emitter settings", () => {
|
|
47
|
+
const emitter = new ParticleEmitter({
|
|
48
|
+
position: { x: 100, y: 200, z: 50 },
|
|
49
|
+
velocity: { x: 10, y: -20, z: 5 },
|
|
50
|
+
lifetime: { min: 2, max: 2 },
|
|
51
|
+
size: { min: 3, max: 3 },
|
|
52
|
+
color: { r: 128, g: 64, b: 32, a: 0.5 },
|
|
53
|
+
shape: "triangle",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const p = new Particle();
|
|
57
|
+
emitter.emit(p);
|
|
58
|
+
|
|
59
|
+
expect(p.x).toBe(100);
|
|
60
|
+
expect(p.y).toBe(200);
|
|
61
|
+
expect(p.z).toBe(50);
|
|
62
|
+
expect(p.vx).toBe(10);
|
|
63
|
+
expect(p.vy).toBe(-20);
|
|
64
|
+
expect(p.vz).toBe(5);
|
|
65
|
+
expect(p.lifetime).toBe(2);
|
|
66
|
+
expect(p.size).toBe(3);
|
|
67
|
+
expect(p.color).toEqual({ r: 128, g: 64, b: 32, a: 0.5 });
|
|
68
|
+
expect(p.shape).toBe("triangle");
|
|
69
|
+
expect(p.age).toBe(0);
|
|
70
|
+
expect(p.alive).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should apply position spread", () => {
|
|
74
|
+
const emitter = new ParticleEmitter({
|
|
75
|
+
position: { x: 0, y: 0, z: 0 },
|
|
76
|
+
spread: { x: 100, y: 100, z: 100 },
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Mock Math.random to return predictable values
|
|
80
|
+
const mockRandom = vi.spyOn(Math, "random");
|
|
81
|
+
mockRandom.mockReturnValue(0.5); // Mid-point = 0 spread
|
|
82
|
+
|
|
83
|
+
const p = new Particle();
|
|
84
|
+
emitter.emit(p);
|
|
85
|
+
|
|
86
|
+
// With Math.random() = 0.5, spread = (0.5 - 0.5) * 2 * spread = 0
|
|
87
|
+
expect(p.x).toBe(0);
|
|
88
|
+
expect(p.y).toBe(0);
|
|
89
|
+
expect(p.z).toBe(0);
|
|
90
|
+
|
|
91
|
+
mockRandom.mockRestore();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should apply velocity spread", () => {
|
|
95
|
+
const emitter = new ParticleEmitter({
|
|
96
|
+
velocity: { x: 100 },
|
|
97
|
+
velocitySpread: { x: 50 },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const mockRandom = vi.spyOn(Math, "random");
|
|
101
|
+
mockRandom.mockReturnValue(0); // (0 - 0.5) * 2 * 50 = -50
|
|
102
|
+
|
|
103
|
+
const p = new Particle();
|
|
104
|
+
emitter.emit(p);
|
|
105
|
+
|
|
106
|
+
expect(p.vx).toBe(50); // 100 + (-50)
|
|
107
|
+
|
|
108
|
+
mockRandom.mockRestore();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should randomize lifetime within range", () => {
|
|
112
|
+
const emitter = new ParticleEmitter({
|
|
113
|
+
lifetime: { min: 1, max: 3 },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const mockRandom = vi.spyOn(Math, "random");
|
|
117
|
+
mockRandom.mockReturnValue(0.5);
|
|
118
|
+
|
|
119
|
+
const p = new Particle();
|
|
120
|
+
emitter.emit(p);
|
|
121
|
+
|
|
122
|
+
expect(p.lifetime).toBe(2); // 1 + 0.5 * (3 - 1)
|
|
123
|
+
|
|
124
|
+
mockRandom.mockRestore();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should randomize size within range", () => {
|
|
128
|
+
const emitter = new ParticleEmitter({
|
|
129
|
+
size: { min: 2, max: 10 },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const mockRandom = vi.spyOn(Math, "random");
|
|
133
|
+
mockRandom.mockReturnValue(0.25);
|
|
134
|
+
|
|
135
|
+
const p = new Particle();
|
|
136
|
+
emitter.emit(p);
|
|
137
|
+
|
|
138
|
+
expect(p.size).toBe(4); // 2 + 0.25 * (10 - 2)
|
|
139
|
+
|
|
140
|
+
mockRandom.mockRestore();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("update", () => {
|
|
145
|
+
it("should return 0 when inactive", () => {
|
|
146
|
+
const emitter = new ParticleEmitter({ rate: 100, active: false });
|
|
147
|
+
|
|
148
|
+
expect(emitter.update(1)).toBe(0);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should return 0 when rate is 0", () => {
|
|
152
|
+
const emitter = new ParticleEmitter({ rate: 0 });
|
|
153
|
+
|
|
154
|
+
expect(emitter.update(1)).toBe(0);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("should emit particles based on rate", () => {
|
|
158
|
+
const emitter = new ParticleEmitter({ rate: 10 }); // 10 particles/second
|
|
159
|
+
|
|
160
|
+
// 0.2 seconds should spawn 2 particles
|
|
161
|
+
const count = emitter.update(0.2);
|
|
162
|
+
|
|
163
|
+
expect(count).toBe(2);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should accumulate time between frames", () => {
|
|
167
|
+
const emitter = new ParticleEmitter({ rate: 10 }); // interval = 0.1s
|
|
168
|
+
|
|
169
|
+
// First frame: 0.05s (not enough for a particle)
|
|
170
|
+
expect(emitter.update(0.05)).toBe(0);
|
|
171
|
+
|
|
172
|
+
// Second frame: 0.05s more (total 0.1s = 1 particle)
|
|
173
|
+
expect(emitter.update(0.05)).toBe(1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should handle high frame rates correctly", () => {
|
|
177
|
+
const emitter = new ParticleEmitter({ rate: 60 });
|
|
178
|
+
|
|
179
|
+
// One frame at 60fps = ~0.0167s
|
|
180
|
+
// At 60 particles/second, interval = 0.0167s, so 1 particle per frame
|
|
181
|
+
let total = 0;
|
|
182
|
+
for (let i = 0; i < 60; i++) {
|
|
183
|
+
total += emitter.update(1 / 60);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
expect(total).toBe(60);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("reset", () => {
|
|
191
|
+
it("should reset the emission timer", () => {
|
|
192
|
+
const emitter = new ParticleEmitter({ rate: 10 });
|
|
193
|
+
|
|
194
|
+
// Accumulate some time
|
|
195
|
+
emitter.update(0.05);
|
|
196
|
+
expect(emitter._timer).toBeGreaterThan(0);
|
|
197
|
+
|
|
198
|
+
// Reset
|
|
199
|
+
emitter.reset();
|
|
200
|
+
|
|
201
|
+
expect(emitter._timer).toBe(0);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { ParticleSystem } from "../../src/particle/particle-system";
|
|
3
|
+
import { ParticleEmitter } from "../../src/particle/emitter";
|
|
4
|
+
import { Updaters } from "../../src/particle/updaters";
|
|
5
|
+
import { Particle } from "../../src/particle/particle";
|
|
6
|
+
|
|
7
|
+
// Mock the game object
|
|
8
|
+
const createMockGame = () => ({
|
|
9
|
+
width: 800,
|
|
10
|
+
height: 600,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe("ParticleSystem", () => {
|
|
14
|
+
let mockGame;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
mockGame = createMockGame();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("constructor", () => {
|
|
21
|
+
it("should initialize with default values", () => {
|
|
22
|
+
const system = new ParticleSystem(mockGame);
|
|
23
|
+
|
|
24
|
+
expect(system.particles).toEqual([]);
|
|
25
|
+
expect(system.pool).toEqual([]);
|
|
26
|
+
expect(system.maxParticles).toBe(5000);
|
|
27
|
+
expect(system.emitters).toBeInstanceOf(Map);
|
|
28
|
+
expect(system.emitters.size).toBe(0);
|
|
29
|
+
expect(system.camera).toBeNull();
|
|
30
|
+
expect(system.depthSort).toBe(false);
|
|
31
|
+
expect(system.blendMode).toBe("source-over");
|
|
32
|
+
expect(system.worldSpace).toBe(false);
|
|
33
|
+
expect(system.particleCount).toBe(0);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should accept custom options", () => {
|
|
37
|
+
const mockCamera = { project: vi.fn() };
|
|
38
|
+
const system = new ParticleSystem(mockGame, {
|
|
39
|
+
maxParticles: 1000,
|
|
40
|
+
camera: mockCamera,
|
|
41
|
+
depthSort: true,
|
|
42
|
+
blendMode: "screen",
|
|
43
|
+
worldSpace: true,
|
|
44
|
+
updaters: [Updaters.velocity],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(system.maxParticles).toBe(1000);
|
|
48
|
+
expect(system.camera).toBe(mockCamera);
|
|
49
|
+
expect(system.depthSort).toBe(true);
|
|
50
|
+
expect(system.blendMode).toBe("screen");
|
|
51
|
+
expect(system.worldSpace).toBe(true);
|
|
52
|
+
expect(system.updaters).toEqual([Updaters.velocity]);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("emitter management", () => {
|
|
57
|
+
it("should add emitters", () => {
|
|
58
|
+
const system = new ParticleSystem(mockGame);
|
|
59
|
+
const emitter = new ParticleEmitter({ rate: 10 });
|
|
60
|
+
|
|
61
|
+
system.addEmitter("fire", emitter);
|
|
62
|
+
|
|
63
|
+
expect(system.emitters.has("fire")).toBe(true);
|
|
64
|
+
expect(system.getEmitter("fire")).toBe(emitter);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should remove emitters", () => {
|
|
68
|
+
const system = new ParticleSystem(mockGame);
|
|
69
|
+
const emitter = new ParticleEmitter({ rate: 10 });
|
|
70
|
+
|
|
71
|
+
system.addEmitter("fire", emitter);
|
|
72
|
+
system.removeEmitter("fire");
|
|
73
|
+
|
|
74
|
+
expect(system.emitters.has("fire")).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should return undefined for non-existent emitter", () => {
|
|
78
|
+
const system = new ParticleSystem(mockGame);
|
|
79
|
+
|
|
80
|
+
expect(system.getEmitter("nonexistent")).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should support chaining on addEmitter/removeEmitter", () => {
|
|
84
|
+
const system = new ParticleSystem(mockGame);
|
|
85
|
+
const emitter = new ParticleEmitter();
|
|
86
|
+
|
|
87
|
+
const result1 = system.addEmitter("test", emitter);
|
|
88
|
+
const result2 = system.removeEmitter("test");
|
|
89
|
+
|
|
90
|
+
expect(result1).toBe(system);
|
|
91
|
+
expect(result2).toBe(system);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("object pooling", () => {
|
|
96
|
+
it("should acquire particles from pool when available", () => {
|
|
97
|
+
const system = new ParticleSystem(mockGame);
|
|
98
|
+
|
|
99
|
+
// Add particle to pool
|
|
100
|
+
const pooledParticle = new Particle();
|
|
101
|
+
system.pool.push(pooledParticle);
|
|
102
|
+
|
|
103
|
+
const acquired = system.acquire();
|
|
104
|
+
|
|
105
|
+
expect(acquired).toBe(pooledParticle);
|
|
106
|
+
expect(system.pool.length).toBe(0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should create new particle when pool is empty", () => {
|
|
110
|
+
const system = new ParticleSystem(mockGame);
|
|
111
|
+
|
|
112
|
+
const acquired = system.acquire();
|
|
113
|
+
|
|
114
|
+
expect(acquired).toBeInstanceOf(Particle);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should release particles back to pool", () => {
|
|
118
|
+
const system = new ParticleSystem(mockGame);
|
|
119
|
+
const particle = new Particle();
|
|
120
|
+
particle.x = 100;
|
|
121
|
+
particle.y = 200;
|
|
122
|
+
|
|
123
|
+
system.release(particle);
|
|
124
|
+
|
|
125
|
+
expect(system.pool.length).toBe(1);
|
|
126
|
+
expect(system.pool[0]).toBe(particle);
|
|
127
|
+
// Particle should be reset
|
|
128
|
+
expect(particle.x).toBe(0);
|
|
129
|
+
expect(particle.y).toBe(0);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("emit", () => {
|
|
134
|
+
it("should emit particles using emitter", () => {
|
|
135
|
+
const system = new ParticleSystem(mockGame);
|
|
136
|
+
const emitter = new ParticleEmitter({
|
|
137
|
+
position: { x: 100, y: 200 },
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
system.emit(5, emitter);
|
|
141
|
+
|
|
142
|
+
expect(system.particles.length).toBe(5);
|
|
143
|
+
// particleCount is updated after update() call
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("should not exceed maxParticles", () => {
|
|
147
|
+
const system = new ParticleSystem(mockGame, { maxParticles: 3 });
|
|
148
|
+
const emitter = new ParticleEmitter();
|
|
149
|
+
|
|
150
|
+
system.emit(10, emitter);
|
|
151
|
+
|
|
152
|
+
expect(system.particles.length).toBe(3);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("burst", () => {
|
|
157
|
+
it("should burst spawn with emitter instance", () => {
|
|
158
|
+
const system = new ParticleSystem(mockGame);
|
|
159
|
+
const emitter = new ParticleEmitter();
|
|
160
|
+
|
|
161
|
+
system.burst(5, emitter);
|
|
162
|
+
|
|
163
|
+
expect(system.particles.length).toBe(5);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should burst spawn with emitter name", () => {
|
|
167
|
+
const system = new ParticleSystem(mockGame);
|
|
168
|
+
const emitter = new ParticleEmitter();
|
|
169
|
+
system.addEmitter("explosion", emitter);
|
|
170
|
+
|
|
171
|
+
system.burst(5, "explosion");
|
|
172
|
+
|
|
173
|
+
expect(system.particles.length).toBe(5);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should do nothing with invalid emitter name", () => {
|
|
177
|
+
const system = new ParticleSystem(mockGame);
|
|
178
|
+
|
|
179
|
+
system.burst(5, "nonexistent");
|
|
180
|
+
|
|
181
|
+
expect(system.particles.length).toBe(0);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("update", () => {
|
|
186
|
+
it("should update particles with all updaters", () => {
|
|
187
|
+
const customUpdater = vi.fn();
|
|
188
|
+
const system = new ParticleSystem(mockGame, {
|
|
189
|
+
updaters: [customUpdater],
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const emitter = new ParticleEmitter({ lifetime: { min: 10, max: 10 } });
|
|
193
|
+
system.emit(2, emitter);
|
|
194
|
+
|
|
195
|
+
system.update(0.016);
|
|
196
|
+
|
|
197
|
+
expect(customUpdater).toHaveBeenCalledTimes(2);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("should remove dead particles", () => {
|
|
201
|
+
const system = new ParticleSystem(mockGame, {
|
|
202
|
+
updaters: [Updaters.lifetime],
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const emitter = new ParticleEmitter({
|
|
206
|
+
lifetime: { min: 0.1, max: 0.1 },
|
|
207
|
+
});
|
|
208
|
+
system.emit(3, emitter);
|
|
209
|
+
|
|
210
|
+
// Large time step to kill particles
|
|
211
|
+
system.update(1);
|
|
212
|
+
|
|
213
|
+
expect(system.particles.length).toBe(0);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("should return dead particles to pool", () => {
|
|
217
|
+
const system = new ParticleSystem(mockGame, {
|
|
218
|
+
updaters: [Updaters.lifetime],
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const emitter = new ParticleEmitter({
|
|
222
|
+
lifetime: { min: 0.1, max: 0.1 },
|
|
223
|
+
});
|
|
224
|
+
system.emit(3, emitter);
|
|
225
|
+
|
|
226
|
+
system.update(1);
|
|
227
|
+
|
|
228
|
+
expect(system.pool.length).toBe(3);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should spawn particles from active emitters", () => {
|
|
232
|
+
const system = new ParticleSystem(mockGame);
|
|
233
|
+
const emitter = new ParticleEmitter({
|
|
234
|
+
rate: 100, // 100 per second
|
|
235
|
+
lifetime: { min: 10, max: 10 },
|
|
236
|
+
});
|
|
237
|
+
system.addEmitter("continuous", emitter);
|
|
238
|
+
|
|
239
|
+
system.update(0.1); // 10 particles expected
|
|
240
|
+
|
|
241
|
+
expect(system.particles.length).toBe(10);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("should not spawn from inactive emitters", () => {
|
|
245
|
+
const system = new ParticleSystem(mockGame);
|
|
246
|
+
const emitter = new ParticleEmitter({
|
|
247
|
+
rate: 100,
|
|
248
|
+
active: false,
|
|
249
|
+
});
|
|
250
|
+
system.addEmitter("disabled", emitter);
|
|
251
|
+
|
|
252
|
+
system.update(0.1);
|
|
253
|
+
|
|
254
|
+
expect(system.particles.length).toBe(0);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe("clear", () => {
|
|
259
|
+
it("should remove all particles", () => {
|
|
260
|
+
const system = new ParticleSystem(mockGame);
|
|
261
|
+
const emitter = new ParticleEmitter();
|
|
262
|
+
system.emit(10, emitter);
|
|
263
|
+
|
|
264
|
+
system.clear();
|
|
265
|
+
|
|
266
|
+
expect(system.particles.length).toBe(0);
|
|
267
|
+
expect(system.particleCount).toBe(0);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("should return particles to pool", () => {
|
|
271
|
+
const system = new ParticleSystem(mockGame);
|
|
272
|
+
const emitter = new ParticleEmitter();
|
|
273
|
+
system.emit(10, emitter);
|
|
274
|
+
|
|
275
|
+
system.clear();
|
|
276
|
+
|
|
277
|
+
expect(system.pool.length).toBe(10);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("stats", () => {
|
|
282
|
+
it("should track particle count", () => {
|
|
283
|
+
const system = new ParticleSystem(mockGame);
|
|
284
|
+
const emitter = new ParticleEmitter({
|
|
285
|
+
lifetime: { min: 10, max: 10 },
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
expect(system.particleCount).toBe(0);
|
|
289
|
+
|
|
290
|
+
system.emit(5, emitter);
|
|
291
|
+
system.update(0);
|
|
292
|
+
|
|
293
|
+
expect(system.particleCount).toBe(5);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("should track pool size", () => {
|
|
297
|
+
const system = new ParticleSystem(mockGame);
|
|
298
|
+
|
|
299
|
+
expect(system.poolSize).toBe(0);
|
|
300
|
+
|
|
301
|
+
const emitter = new ParticleEmitter({
|
|
302
|
+
lifetime: { min: 0.01, max: 0.01 },
|
|
303
|
+
});
|
|
304
|
+
system.emit(5, emitter);
|
|
305
|
+
system.update(1); // Kill all
|
|
306
|
+
|
|
307
|
+
expect(system.poolSize).toBe(5);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { Particle } from "../../src/particle/particle";
|
|
3
|
+
|
|
4
|
+
describe("Particle", () => {
|
|
5
|
+
describe("constructor", () => {
|
|
6
|
+
it("should initialize with default values", () => {
|
|
7
|
+
const p = new Particle();
|
|
8
|
+
|
|
9
|
+
// Position
|
|
10
|
+
expect(p.x).toBe(0);
|
|
11
|
+
expect(p.y).toBe(0);
|
|
12
|
+
expect(p.z).toBe(0);
|
|
13
|
+
|
|
14
|
+
// Velocity
|
|
15
|
+
expect(p.vx).toBe(0);
|
|
16
|
+
expect(p.vy).toBe(0);
|
|
17
|
+
expect(p.vz).toBe(0);
|
|
18
|
+
|
|
19
|
+
// Appearance
|
|
20
|
+
expect(p.size).toBe(1);
|
|
21
|
+
expect(p.color).toEqual({ r: 255, g: 255, b: 255, a: 1 });
|
|
22
|
+
expect(p.shape).toBe("circle");
|
|
23
|
+
|
|
24
|
+
// Lifecycle
|
|
25
|
+
expect(p.age).toBe(0);
|
|
26
|
+
expect(p.lifetime).toBe(1);
|
|
27
|
+
expect(p.alive).toBe(true);
|
|
28
|
+
|
|
29
|
+
// Custom data
|
|
30
|
+
expect(p.custom).toEqual({});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("reset", () => {
|
|
35
|
+
it("should reset all properties to defaults", () => {
|
|
36
|
+
const p = new Particle();
|
|
37
|
+
|
|
38
|
+
// Modify all properties
|
|
39
|
+
p.x = 100;
|
|
40
|
+
p.y = 200;
|
|
41
|
+
p.z = 300;
|
|
42
|
+
p.vx = 10;
|
|
43
|
+
p.vy = 20;
|
|
44
|
+
p.vz = 30;
|
|
45
|
+
p.size = 5;
|
|
46
|
+
p.color = { r: 100, g: 50, b: 25, a: 0.5 };
|
|
47
|
+
p.shape = "square";
|
|
48
|
+
p.age = 2;
|
|
49
|
+
p.lifetime = 5;
|
|
50
|
+
p.alive = false;
|
|
51
|
+
p.custom.foo = "bar";
|
|
52
|
+
|
|
53
|
+
// Reset
|
|
54
|
+
p.reset();
|
|
55
|
+
|
|
56
|
+
// Verify defaults
|
|
57
|
+
expect(p.x).toBe(0);
|
|
58
|
+
expect(p.y).toBe(0);
|
|
59
|
+
expect(p.z).toBe(0);
|
|
60
|
+
expect(p.vx).toBe(0);
|
|
61
|
+
expect(p.vy).toBe(0);
|
|
62
|
+
expect(p.vz).toBe(0);
|
|
63
|
+
expect(p.size).toBe(1);
|
|
64
|
+
expect(p.color).toEqual({ r: 255, g: 255, b: 255, a: 1 });
|
|
65
|
+
expect(p.shape).toBe("circle");
|
|
66
|
+
expect(p.age).toBe(0);
|
|
67
|
+
expect(p.lifetime).toBe(1);
|
|
68
|
+
expect(p.alive).toBe(true);
|
|
69
|
+
expect(p.custom).toEqual({});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should clear custom data", () => {
|
|
73
|
+
const p = new Particle();
|
|
74
|
+
p.custom.key1 = "value1";
|
|
75
|
+
p.custom.key2 = "value2";
|
|
76
|
+
|
|
77
|
+
p.reset();
|
|
78
|
+
|
|
79
|
+
expect(Object.keys(p.custom).length).toBe(0);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("progress", () => {
|
|
84
|
+
it("should calculate progress as age/lifetime", () => {
|
|
85
|
+
const p = new Particle();
|
|
86
|
+
p.lifetime = 4;
|
|
87
|
+
p.age = 1;
|
|
88
|
+
|
|
89
|
+
expect(p.progress).toBe(0.25);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should return 0 at birth", () => {
|
|
93
|
+
const p = new Particle();
|
|
94
|
+
p.lifetime = 2;
|
|
95
|
+
p.age = 0;
|
|
96
|
+
|
|
97
|
+
expect(p.progress).toBe(0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("should return 1 at death", () => {
|
|
101
|
+
const p = new Particle();
|
|
102
|
+
p.lifetime = 2;
|
|
103
|
+
p.age = 2;
|
|
104
|
+
|
|
105
|
+
expect(p.progress).toBe(1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should handle zero lifetime", () => {
|
|
109
|
+
const p = new Particle();
|
|
110
|
+
p.lifetime = 0;
|
|
111
|
+
p.age = 0;
|
|
112
|
+
|
|
113
|
+
expect(p.progress).toBe(1);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|