@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,352 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { Scene3D } from "../../src/game/objects/scene3d";
|
|
3
|
+
import { Camera3D } from "../../src/util/camera3d";
|
|
4
|
+
import { Painter } from "../../src/painter/painter";
|
|
5
|
+
|
|
6
|
+
// Mock game object
|
|
7
|
+
const createMockGame = () => ({
|
|
8
|
+
width: 800,
|
|
9
|
+
height: 600,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// Mock child game object
|
|
13
|
+
const createMockChild = (options = {}) => ({
|
|
14
|
+
x: options.x ?? 0,
|
|
15
|
+
y: options.y ?? 0,
|
|
16
|
+
z: options.z ?? undefined,
|
|
17
|
+
visible: options.visible ?? true,
|
|
18
|
+
draw: vi.fn(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("Scene3D", () => {
|
|
22
|
+
let mockGame;
|
|
23
|
+
let camera;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
mockGame = createMockGame();
|
|
27
|
+
camera = new Camera3D({ perspective: 800 });
|
|
28
|
+
|
|
29
|
+
// Reset Painter mocks
|
|
30
|
+
if (Painter.save) Painter.save.mockClear?.();
|
|
31
|
+
if (Painter.restore) Painter.restore.mockClear?.();
|
|
32
|
+
if (Painter.translateTo) Painter.translateTo = vi.fn();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("constructor", () => {
|
|
36
|
+
it("should throw if camera is not provided", () => {
|
|
37
|
+
expect(() => {
|
|
38
|
+
new Scene3D(mockGame, {});
|
|
39
|
+
}).toThrow("Scene3D requires a camera option");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should initialize with camera", () => {
|
|
43
|
+
const scene = new Scene3D(mockGame, { camera });
|
|
44
|
+
|
|
45
|
+
expect(scene.camera).toBe(camera);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should default depthSort to true", () => {
|
|
49
|
+
const scene = new Scene3D(mockGame, { camera });
|
|
50
|
+
|
|
51
|
+
expect(scene.depthSort).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should default scaleByDepth to true", () => {
|
|
55
|
+
const scene = new Scene3D(mockGame, { camera });
|
|
56
|
+
|
|
57
|
+
expect(scene.scaleByDepth).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should accept custom options", () => {
|
|
61
|
+
const scene = new Scene3D(mockGame, {
|
|
62
|
+
camera,
|
|
63
|
+
depthSort: false,
|
|
64
|
+
scaleByDepth: false,
|
|
65
|
+
x: 100,
|
|
66
|
+
y: 200,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(scene.depthSort).toBe(false);
|
|
70
|
+
expect(scene.scaleByDepth).toBe(false);
|
|
71
|
+
expect(scene.x).toBe(100);
|
|
72
|
+
expect(scene.y).toBe(200);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("draw with depth sorting", () => {
|
|
77
|
+
it("should sort children back-to-front when depthSort is true", () => {
|
|
78
|
+
const scene = new Scene3D(mockGame, {
|
|
79
|
+
camera,
|
|
80
|
+
depthSort: true,
|
|
81
|
+
scaleByDepth: false,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Setup Painter mock
|
|
85
|
+
Painter.save = vi.fn();
|
|
86
|
+
Painter.restore = vi.fn();
|
|
87
|
+
Painter.translateTo = vi.fn();
|
|
88
|
+
Painter.ctx = { scale: vi.fn() };
|
|
89
|
+
|
|
90
|
+
// Add children at different z depths
|
|
91
|
+
const front = createMockChild({ x: 0, y: 0, z: -100 });
|
|
92
|
+
const middle = createMockChild({ x: 0, y: 0, z: 0 });
|
|
93
|
+
const back = createMockChild({ x: 0, y: 0, z: 100 });
|
|
94
|
+
|
|
95
|
+
scene._collection = {
|
|
96
|
+
children: [front, middle, back],
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Call draw
|
|
100
|
+
scene.draw();
|
|
101
|
+
|
|
102
|
+
// Children should be rendered back-to-front
|
|
103
|
+
// So 'back' should be called before 'front'
|
|
104
|
+
const callOrder = [];
|
|
105
|
+
if (back.draw.mock.calls.length) callOrder.push("back");
|
|
106
|
+
if (middle.draw.mock.calls.length) callOrder.push("middle");
|
|
107
|
+
if (front.draw.mock.calls.length) callOrder.push("front");
|
|
108
|
+
|
|
109
|
+
// All should be called
|
|
110
|
+
expect(back.draw).toHaveBeenCalled();
|
|
111
|
+
expect(middle.draw).toHaveBeenCalled();
|
|
112
|
+
expect(front.draw).toHaveBeenCalled();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should not sort when depthSort is false", () => {
|
|
116
|
+
const scene = new Scene3D(mockGame, {
|
|
117
|
+
camera,
|
|
118
|
+
depthSort: false,
|
|
119
|
+
scaleByDepth: false,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
Painter.save = vi.fn();
|
|
123
|
+
Painter.restore = vi.fn();
|
|
124
|
+
Painter.translateTo = vi.fn();
|
|
125
|
+
Painter.ctx = { scale: vi.fn() };
|
|
126
|
+
|
|
127
|
+
const child1 = createMockChild({ z: 100 });
|
|
128
|
+
const child2 = createMockChild({ z: -100 });
|
|
129
|
+
|
|
130
|
+
scene._collection = {
|
|
131
|
+
children: [child1, child2],
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
scene.draw();
|
|
135
|
+
|
|
136
|
+
// Both should be drawn
|
|
137
|
+
expect(child1.draw).toHaveBeenCalled();
|
|
138
|
+
expect(child2.draw).toHaveBeenCalled();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("draw with perspective scaling", () => {
|
|
143
|
+
it("should scale children by perspective when scaleByDepth is true", () => {
|
|
144
|
+
const scene = new Scene3D(mockGame, {
|
|
145
|
+
camera,
|
|
146
|
+
depthSort: false,
|
|
147
|
+
scaleByDepth: true,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
Painter.save = vi.fn();
|
|
151
|
+
Painter.restore = vi.fn();
|
|
152
|
+
Painter.translateTo = vi.fn();
|
|
153
|
+
Painter.ctx = { scale: vi.fn() };
|
|
154
|
+
|
|
155
|
+
const child = createMockChild({ z: 0 });
|
|
156
|
+
scene._collection = {
|
|
157
|
+
children: [child],
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
scene.draw();
|
|
161
|
+
|
|
162
|
+
// Scale should be called
|
|
163
|
+
expect(Painter.ctx.scale).toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should not scale when scaleByDepth is false", () => {
|
|
167
|
+
const scene = new Scene3D(mockGame, {
|
|
168
|
+
camera,
|
|
169
|
+
depthSort: false,
|
|
170
|
+
scaleByDepth: false,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
Painter.save = vi.fn();
|
|
174
|
+
Painter.restore = vi.fn();
|
|
175
|
+
Painter.translateTo = vi.fn();
|
|
176
|
+
Painter.ctx = { scale: vi.fn() };
|
|
177
|
+
|
|
178
|
+
const child = createMockChild({ z: 0 });
|
|
179
|
+
scene._collection = {
|
|
180
|
+
children: [child],
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
scene.draw();
|
|
184
|
+
|
|
185
|
+
// Scale should not be called
|
|
186
|
+
expect(Painter.ctx.scale).not.toHaveBeenCalled();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("draw visibility and culling", () => {
|
|
191
|
+
it("should skip invisible children", () => {
|
|
192
|
+
const scene = new Scene3D(mockGame, {
|
|
193
|
+
camera,
|
|
194
|
+
depthSort: false,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
Painter.save = vi.fn();
|
|
198
|
+
Painter.restore = vi.fn();
|
|
199
|
+
Painter.translateTo = vi.fn();
|
|
200
|
+
Painter.ctx = { scale: vi.fn() };
|
|
201
|
+
|
|
202
|
+
const visible = createMockChild({ visible: true });
|
|
203
|
+
const hidden = createMockChild({ visible: false });
|
|
204
|
+
|
|
205
|
+
scene._collection = {
|
|
206
|
+
children: [visible, hidden],
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
scene.draw();
|
|
210
|
+
|
|
211
|
+
expect(visible.draw).toHaveBeenCalled();
|
|
212
|
+
expect(hidden.draw).not.toHaveBeenCalled();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("should cull children behind camera", () => {
|
|
216
|
+
const scene = new Scene3D(mockGame, {
|
|
217
|
+
camera: new Camera3D({ perspective: 100 }),
|
|
218
|
+
depthSort: false,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
Painter.save = vi.fn();
|
|
222
|
+
Painter.restore = vi.fn();
|
|
223
|
+
Painter.translateTo = vi.fn();
|
|
224
|
+
Painter.ctx = { scale: vi.fn() };
|
|
225
|
+
|
|
226
|
+
// Child far behind camera (z = -200, camera.perspective = 100)
|
|
227
|
+
// After projection: if z < -perspective + 10, cull it
|
|
228
|
+
const farBehind = createMockChild({ z: 200 }); // After projection z will be positive
|
|
229
|
+
const inFront = createMockChild({ z: -50 });
|
|
230
|
+
|
|
231
|
+
scene._collection = {
|
|
232
|
+
children: [farBehind, inFront],
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
scene.draw();
|
|
236
|
+
|
|
237
|
+
// Both should be drawn as they're not behind camera
|
|
238
|
+
expect(inFront.draw).toHaveBeenCalled();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("should default z to 0 for children without z property", () => {
|
|
242
|
+
const scene = new Scene3D(mockGame, {
|
|
243
|
+
camera,
|
|
244
|
+
depthSort: false,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
Painter.save = vi.fn();
|
|
248
|
+
Painter.restore = vi.fn();
|
|
249
|
+
Painter.translateTo = vi.fn();
|
|
250
|
+
Painter.ctx = { scale: vi.fn() };
|
|
251
|
+
|
|
252
|
+
const noZ = createMockChild({ x: 100, y: 100 });
|
|
253
|
+
delete noZ.z; // No z property
|
|
254
|
+
|
|
255
|
+
scene._collection = {
|
|
256
|
+
children: [noZ],
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// Should not throw
|
|
260
|
+
expect(() => scene.draw()).not.toThrow();
|
|
261
|
+
expect(noZ.draw).toHaveBeenCalled();
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe("draw transformations", () => {
|
|
266
|
+
it("should save and restore for each child", () => {
|
|
267
|
+
const scene = new Scene3D(mockGame, {
|
|
268
|
+
camera,
|
|
269
|
+
depthSort: false,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
Painter.save = vi.fn();
|
|
273
|
+
Painter.restore = vi.fn();
|
|
274
|
+
Painter.translateTo = vi.fn();
|
|
275
|
+
Painter.ctx = { scale: vi.fn() };
|
|
276
|
+
|
|
277
|
+
const child1 = createMockChild();
|
|
278
|
+
const child2 = createMockChild();
|
|
279
|
+
|
|
280
|
+
scene._collection = {
|
|
281
|
+
children: [child1, child2],
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
scene.draw();
|
|
285
|
+
|
|
286
|
+
// Should call save/restore for each child
|
|
287
|
+
expect(Painter.save).toHaveBeenCalledTimes(2);
|
|
288
|
+
expect(Painter.restore).toHaveBeenCalledTimes(2);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("should translate to projected position", () => {
|
|
292
|
+
const scene = new Scene3D(mockGame, {
|
|
293
|
+
camera: new Camera3D(), // No rotation
|
|
294
|
+
depthSort: false,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
Painter.save = vi.fn();
|
|
298
|
+
Painter.restore = vi.fn();
|
|
299
|
+
Painter.translateTo = vi.fn();
|
|
300
|
+
Painter.ctx = { scale: vi.fn() };
|
|
301
|
+
|
|
302
|
+
const child = createMockChild({ x: 100, y: 50, z: 0 });
|
|
303
|
+
scene._collection = {
|
|
304
|
+
children: [child],
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
scene.draw();
|
|
308
|
+
|
|
309
|
+
// With no rotation and z=0, projected position should match input
|
|
310
|
+
expect(Painter.translateTo).toHaveBeenCalledWith(100, 50);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe("integration with Camera3D projection", () => {
|
|
315
|
+
it("should use camera projection for positioning", () => {
|
|
316
|
+
const mockCamera = {
|
|
317
|
+
project: vi.fn().mockReturnValue({
|
|
318
|
+
x: 150,
|
|
319
|
+
y: 75,
|
|
320
|
+
z: 50,
|
|
321
|
+
scale: 0.8,
|
|
322
|
+
}),
|
|
323
|
+
perspective: 800,
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const scene = new Scene3D(mockGame, {
|
|
327
|
+
camera: mockCamera,
|
|
328
|
+
depthSort: false,
|
|
329
|
+
scaleByDepth: true,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
Painter.save = vi.fn();
|
|
333
|
+
Painter.restore = vi.fn();
|
|
334
|
+
Painter.translateTo = vi.fn();
|
|
335
|
+
Painter.ctx = { scale: vi.fn() };
|
|
336
|
+
|
|
337
|
+
const child = createMockChild({ x: 100, y: 50, z: 25 });
|
|
338
|
+
scene._collection = {
|
|
339
|
+
children: [child],
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
scene.draw();
|
|
343
|
+
|
|
344
|
+
// Should call camera.project with child coordinates
|
|
345
|
+
expect(mockCamera.project).toHaveBeenCalledWith(100, 50, 25);
|
|
346
|
+
|
|
347
|
+
// Should use projected values
|
|
348
|
+
expect(Painter.translateTo).toHaveBeenCalledWith(150, 75);
|
|
349
|
+
expect(Painter.ctx.scale).toHaveBeenCalledWith(0.8, 0.8);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
});
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GCanvas Collision Detection Types
|
|
3
|
+
* Collision detection utilities for 2D game development.
|
|
4
|
+
* @module collision
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Bounds } from './common';
|
|
8
|
+
|
|
9
|
+
// ==========================================================================
|
|
10
|
+
// Collision Shapes
|
|
11
|
+
// ==========================================================================
|
|
12
|
+
|
|
13
|
+
/** Circle definition for collision detection */
|
|
14
|
+
export interface CollisionCircle {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
radius: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Line segment definition for collision detection */
|
|
21
|
+
export interface LineSegment {
|
|
22
|
+
x1: number;
|
|
23
|
+
y1: number;
|
|
24
|
+
x2: number;
|
|
25
|
+
y2: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Sweep collision result */
|
|
29
|
+
export interface SweepResult {
|
|
30
|
+
/** Time of collision (0-1 within the movement) */
|
|
31
|
+
time: number;
|
|
32
|
+
/** X component of collision normal */
|
|
33
|
+
normalX: number;
|
|
34
|
+
/** Y component of collision normal */
|
|
35
|
+
normalY: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Overlap result between two rectangles */
|
|
39
|
+
export interface OverlapResult {
|
|
40
|
+
/** Horizontal overlap amount */
|
|
41
|
+
x: number;
|
|
42
|
+
/** Vertical overlap amount */
|
|
43
|
+
y: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Minimum translation vector to separate colliding objects */
|
|
47
|
+
export interface MTVResult {
|
|
48
|
+
/** X translation to separate */
|
|
49
|
+
x: number;
|
|
50
|
+
/** Y translation to separate */
|
|
51
|
+
y: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ==========================================================================
|
|
55
|
+
// Collision Static Class
|
|
56
|
+
// ==========================================================================
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Static collision detection utilities.
|
|
60
|
+
* Provides various algorithms for 2D collision detection.
|
|
61
|
+
*/
|
|
62
|
+
export class Collision {
|
|
63
|
+
/**
|
|
64
|
+
* Test if two axis-aligned rectangles intersect (AABB collision)
|
|
65
|
+
*/
|
|
66
|
+
static rectRect(a: Bounds, b: Bounds): boolean;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Alias for rectRect - matches common naming convention
|
|
70
|
+
*/
|
|
71
|
+
static intersects(a: Bounds, b: Bounds): boolean;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Test if a point is inside a rectangle
|
|
75
|
+
*/
|
|
76
|
+
static pointRect(px: number, py: number, rect: Bounds): boolean;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Test if two circles intersect
|
|
80
|
+
*/
|
|
81
|
+
static circleCircle(a: CollisionCircle, b: CollisionCircle): boolean;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Test if a point is inside a circle
|
|
85
|
+
*/
|
|
86
|
+
static pointCircle(px: number, py: number, circle: CollisionCircle): boolean;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Test if a circle and rectangle intersect
|
|
90
|
+
*/
|
|
91
|
+
static circleRect(circle: CollisionCircle, rect: Bounds): boolean;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Test if a line segment intersects a rectangle
|
|
95
|
+
* @param x1 - Line start X
|
|
96
|
+
* @param y1 - Line start Y
|
|
97
|
+
* @param x2 - Line end X
|
|
98
|
+
* @param y2 - Line end Y
|
|
99
|
+
* @param rect - Target rectangle
|
|
100
|
+
* @param thickness - Optional line thickness
|
|
101
|
+
*/
|
|
102
|
+
static lineRect(x1: number, y1: number, x2: number, y2: number, rect: Bounds, thickness?: number): boolean;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Test if two line segments intersect
|
|
106
|
+
*/
|
|
107
|
+
static lineLine(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number): boolean;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Test if multiple line segments intersect a rectangle
|
|
111
|
+
* Useful for complex shapes like lightning bolts
|
|
112
|
+
*/
|
|
113
|
+
static segmentsRect(segments: LineSegment[], rect: Bounds, thickness?: number): boolean;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get the intersection depth between two rectangles
|
|
117
|
+
* Returns null if no collision
|
|
118
|
+
*/
|
|
119
|
+
static getOverlap(a: Bounds, b: Bounds): OverlapResult | null;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get the minimum translation vector to separate two rectangles
|
|
123
|
+
* Returns null if no collision
|
|
124
|
+
*/
|
|
125
|
+
static getMTV(a: Bounds, b: Bounds): MTVResult | null;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check if a moving rectangle will collide with a static one (sweep test)
|
|
129
|
+
* Useful for fast-moving objects like bullets
|
|
130
|
+
* @param rect - Moving rectangle
|
|
131
|
+
* @param vx - Velocity X
|
|
132
|
+
* @param vy - Velocity Y
|
|
133
|
+
* @param target - Target rectangle
|
|
134
|
+
*/
|
|
135
|
+
static sweep(rect: Bounds, vx: number, vy: number, target: Bounds): SweepResult | null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ==========================================================================
|
|
139
|
+
// CollisionSystem Class
|
|
140
|
+
// ==========================================================================
|
|
141
|
+
|
|
142
|
+
/** Object that can participate in collision detection */
|
|
143
|
+
export interface Collidable {
|
|
144
|
+
getBounds?(): Bounds;
|
|
145
|
+
bounds?: Bounds;
|
|
146
|
+
x?: number;
|
|
147
|
+
y?: number;
|
|
148
|
+
width?: number;
|
|
149
|
+
height?: number;
|
|
150
|
+
active?: boolean;
|
|
151
|
+
destroyed?: boolean;
|
|
152
|
+
alive?: boolean;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Collision pair callback options */
|
|
156
|
+
export interface CollisionPairOptions {
|
|
157
|
+
/** If true, only trigger once per pair per frame */
|
|
158
|
+
once?: boolean;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Callback for collision events */
|
|
162
|
+
export type CollisionCallback<A = Collidable, B = Collidable> = (objA: A, objB: B) => void;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Manages collision groups and detection.
|
|
166
|
+
* Provides an organized way to manage multiple groups of collidable objects
|
|
167
|
+
* and efficiently check collisions between them.
|
|
168
|
+
*/
|
|
169
|
+
export class CollisionSystem {
|
|
170
|
+
constructor();
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Create a new collision group
|
|
174
|
+
* @returns this for chaining
|
|
175
|
+
*/
|
|
176
|
+
createGroup(name: string): this;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Add an object to a collision group
|
|
180
|
+
* @returns this for chaining
|
|
181
|
+
*/
|
|
182
|
+
add<T extends Collidable>(groupName: string, obj: T): this;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Remove an object from a collision group
|
|
186
|
+
* @returns True if object was in the group
|
|
187
|
+
*/
|
|
188
|
+
remove(groupName: string, obj: Collidable): boolean;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Remove an object from all groups
|
|
192
|
+
*/
|
|
193
|
+
removeFromAll(obj: Collidable): void;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Clear all objects from a group
|
|
197
|
+
*/
|
|
198
|
+
clearGroup(groupName: string): void;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Clear all groups
|
|
202
|
+
*/
|
|
203
|
+
clearAll(): void;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get all objects in a group
|
|
207
|
+
*/
|
|
208
|
+
getGroup<T extends Collidable>(groupName: string): T[];
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Register a collision callback between two groups
|
|
212
|
+
* @returns this for chaining
|
|
213
|
+
*/
|
|
214
|
+
onCollision<A extends Collidable, B extends Collidable>(
|
|
215
|
+
groupA: string,
|
|
216
|
+
groupB: string,
|
|
217
|
+
callback: CollisionCallback<A, B>,
|
|
218
|
+
options?: CollisionPairOptions
|
|
219
|
+
): this;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Remove all collision callbacks for a pair of groups
|
|
223
|
+
*/
|
|
224
|
+
offCollision(groupA: string, groupB: string): void;
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Check and handle all registered collision pairs
|
|
228
|
+
* Call this each frame in your update loop
|
|
229
|
+
*/
|
|
230
|
+
update(): void;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Check collisions between two specific groups (without callbacks)
|
|
234
|
+
* @returns Array of [objA, objB] colliding pairs
|
|
235
|
+
*/
|
|
236
|
+
check<A extends Collidable, B extends Collidable>(groupA: string, groupB: string): [A, B][];
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Check if an object collides with any object in a group
|
|
240
|
+
* @returns First colliding object, or null
|
|
241
|
+
*/
|
|
242
|
+
checkAgainstGroup<T extends Collidable>(obj: Collidable, groupName: string): T | null;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Check if an object collides with any object in a group
|
|
246
|
+
* @returns All colliding objects
|
|
247
|
+
*/
|
|
248
|
+
checkAllAgainstGroup<T extends Collidable>(obj: Collidable, groupName: string): T[];
|
|
249
|
+
}
|