@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.
Files changed (349) hide show
  1. package/.github/workflows/release.yaml +70 -0
  2. package/.jshintrc +4 -0
  3. package/.vscode/settings.json +22 -0
  4. package/CLAUDE.md +310 -0
  5. package/blackhole.jpg +0 -0
  6. package/demo.png +0 -0
  7. package/demos/CNAME +1 -0
  8. package/demos/animations.html +31 -0
  9. package/demos/basic.html +38 -0
  10. package/demos/baskara.html +31 -0
  11. package/demos/bezier.html +35 -0
  12. package/demos/beziersignature.html +29 -0
  13. package/demos/blackhole.html +28 -0
  14. package/demos/blob.html +35 -0
  15. package/demos/demos.css +289 -0
  16. package/demos/easing.html +28 -0
  17. package/demos/events.html +195 -0
  18. package/demos/fluent.html +647 -0
  19. package/demos/fractals.html +36 -0
  20. package/demos/genart.html +26 -0
  21. package/demos/gendream.html +26 -0
  22. package/demos/group.html +36 -0
  23. package/demos/home.html +587 -0
  24. package/demos/index.html +364 -0
  25. package/demos/isometric.html +34 -0
  26. package/demos/js/animations.js +452 -0
  27. package/demos/js/basic.js +204 -0
  28. package/demos/js/baskara.js +751 -0
  29. package/demos/js/bezier.js +692 -0
  30. package/demos/js/beziersignature.js +241 -0
  31. package/demos/js/blackhole/accretiondisk.obj.js +379 -0
  32. package/demos/js/blackhole/blackhole.obj.js +318 -0
  33. package/demos/js/blackhole/index.js +409 -0
  34. package/demos/js/blackhole/particle.js +56 -0
  35. package/demos/js/blackhole/starfield.obj.js +218 -0
  36. package/demos/js/blob.js +2263 -0
  37. package/demos/js/easing.js +477 -0
  38. package/demos/js/fluent.js +183 -0
  39. package/demos/js/fractals.js +931 -0
  40. package/demos/js/fractalworker.js +93 -0
  41. package/demos/js/genart.js +268 -0
  42. package/demos/js/gendream.js +209 -0
  43. package/demos/js/group.js +140 -0
  44. package/demos/js/info-toggle.js +25 -0
  45. package/demos/js/isometric.js +863 -0
  46. package/demos/js/kerr.js +1556 -0
  47. package/demos/js/lavalamp.js +590 -0
  48. package/demos/js/layout.js +354 -0
  49. package/demos/js/mondrian.js +285 -0
  50. package/demos/js/opacity.js +275 -0
  51. package/demos/js/painter.js +484 -0
  52. package/demos/js/particles-showcase.js +514 -0
  53. package/demos/js/particles.js +299 -0
  54. package/demos/js/patterns.js +397 -0
  55. package/demos/js/penrose/artifact.js +69 -0
  56. package/demos/js/penrose/blackhole.js +121 -0
  57. package/demos/js/penrose/constants.js +73 -0
  58. package/demos/js/penrose/game.js +943 -0
  59. package/demos/js/penrose/lore.js +278 -0
  60. package/demos/js/penrose/penrosescene.js +892 -0
  61. package/demos/js/penrose/ship.js +216 -0
  62. package/demos/js/penrose/sounds.js +211 -0
  63. package/demos/js/penrose/voidparticle.js +55 -0
  64. package/demos/js/penrose/voidscene.js +258 -0
  65. package/demos/js/penrose/voidship.js +144 -0
  66. package/demos/js/penrose/wormhole.js +46 -0
  67. package/demos/js/pipeline.js +555 -0
  68. package/demos/js/scene.js +304 -0
  69. package/demos/js/scenes.js +320 -0
  70. package/demos/js/schrodinger.js +410 -0
  71. package/demos/js/schwarzschild.js +1023 -0
  72. package/demos/js/shapes.js +628 -0
  73. package/demos/js/space/alien.js +171 -0
  74. package/demos/js/space/boom.js +98 -0
  75. package/demos/js/space/boss.js +353 -0
  76. package/demos/js/space/buff.js +73 -0
  77. package/demos/js/space/bullet.js +102 -0
  78. package/demos/js/space/constants.js +85 -0
  79. package/demos/js/space/game.js +1884 -0
  80. package/demos/js/space/hud.js +112 -0
  81. package/demos/js/space/laserbeam.js +179 -0
  82. package/demos/js/space/lightning.js +277 -0
  83. package/demos/js/space/minion.js +192 -0
  84. package/demos/js/space/missile.js +212 -0
  85. package/demos/js/space/player.js +430 -0
  86. package/demos/js/space/powerup.js +90 -0
  87. package/demos/js/space/starfield.js +58 -0
  88. package/demos/js/space/starpower.js +90 -0
  89. package/demos/js/spacetime.js +559 -0
  90. package/demos/js/svgtween.js +204 -0
  91. package/demos/js/tde/accretiondisk.js +418 -0
  92. package/demos/js/tde/blackhole.js +219 -0
  93. package/demos/js/tde/blackholescene.js +209 -0
  94. package/demos/js/tde/config.js +59 -0
  95. package/demos/js/tde/index.js +695 -0
  96. package/demos/js/tde/jets.js +290 -0
  97. package/demos/js/tde/lensedstarfield.js +147 -0
  98. package/demos/js/tde/tdestar.js +317 -0
  99. package/demos/js/tde/tidalstream.js +356 -0
  100. package/demos/js/tde_old/blackhole.obj.js +354 -0
  101. package/demos/js/tde_old/debris.obj.js +791 -0
  102. package/demos/js/tde_old/flare.obj.js +239 -0
  103. package/demos/js/tde_old/index.js +448 -0
  104. package/demos/js/tde_old/star.obj.js +812 -0
  105. package/demos/js/tiles.js +312 -0
  106. package/demos/js/tweendemo.js +79 -0
  107. package/demos/js/visibility.js +102 -0
  108. package/demos/kerr.html +28 -0
  109. package/demos/lavalamp.html +27 -0
  110. package/demos/layouts.html +37 -0
  111. package/demos/logo.svg +4 -0
  112. package/demos/loop.html +84 -0
  113. package/demos/mondrian.html +32 -0
  114. package/demos/og_image.png +0 -0
  115. package/demos/opacity.html +36 -0
  116. package/demos/painter.html +39 -0
  117. package/demos/particles-showcase.html +28 -0
  118. package/demos/particles.html +24 -0
  119. package/demos/patterns.html +33 -0
  120. package/demos/penrose-game.html +31 -0
  121. package/demos/pipeline.html +737 -0
  122. package/demos/scene.html +33 -0
  123. package/demos/scenes.html +96 -0
  124. package/demos/schrodinger.html +27 -0
  125. package/demos/schwarzschild.html +27 -0
  126. package/demos/shapes.html +16 -0
  127. package/demos/space.html +85 -0
  128. package/demos/spacetime.html +27 -0
  129. package/demos/svgtween.html +29 -0
  130. package/demos/tde.html +28 -0
  131. package/demos/tiles.html +28 -0
  132. package/demos/transforms.html +400 -0
  133. package/demos/tween.html +45 -0
  134. package/demos/visibility.html +33 -0
  135. package/disk_example.png +0 -0
  136. package/docs/README.md +222 -0
  137. package/docs/concepts/architecture-overview.md +204 -0
  138. package/docs/concepts/lifecycle.md +255 -0
  139. package/docs/concepts/rendering-pipeline.md +279 -0
  140. package/docs/concepts/tde-zorder.md +106 -0
  141. package/docs/concepts/two-layer-architecture.md +229 -0
  142. package/docs/getting-started/first-game.md +354 -0
  143. package/docs/getting-started/hello-world.md +269 -0
  144. package/docs/getting-started/installation.md +157 -0
  145. package/docs/modules/collision/README.md +453 -0
  146. package/docs/modules/fluent/README.md +1075 -0
  147. package/docs/modules/game/README.md +303 -0
  148. package/docs/modules/isometric-camera.md +210 -0
  149. package/docs/modules/isometric.md +275 -0
  150. package/docs/modules/painter/README.md +328 -0
  151. package/docs/modules/particle/README.md +559 -0
  152. package/docs/modules/shapes/README.md +221 -0
  153. package/docs/modules/shapes/base/euclidian.md +123 -0
  154. package/docs/modules/shapes/base/geometry2d.md +204 -0
  155. package/docs/modules/shapes/base/renderable.md +215 -0
  156. package/docs/modules/shapes/base/shape.md +262 -0
  157. package/docs/modules/shapes/base/transformable.md +243 -0
  158. package/docs/modules/shapes/hierarchy.md +218 -0
  159. package/docs/modules/state/README.md +577 -0
  160. package/docs/modules/util/README.md +99 -0
  161. package/docs/modules/util/camera3d.md +412 -0
  162. package/docs/modules/util/scene3d.md +395 -0
  163. package/index.html +17 -0
  164. package/jsdoc.json +50 -0
  165. package/package.json +55 -0
  166. package/readme.md +599 -0
  167. package/scripts/build-demo.js +69 -0
  168. package/scripts/bundle4llm.js +276 -0
  169. package/scripts/clearconsole.js +48 -0
  170. package/src/collision/collision-system.js +332 -0
  171. package/src/collision/collision.js +303 -0
  172. package/src/collision/index.js +10 -0
  173. package/src/fluent/fluent-game.js +430 -0
  174. package/src/fluent/fluent-go.js +1060 -0
  175. package/src/fluent/fluent-layer.js +152 -0
  176. package/src/fluent/fluent-scene.js +291 -0
  177. package/src/fluent/index.js +98 -0
  178. package/src/fluent/sketch.js +380 -0
  179. package/src/game/game.js +467 -0
  180. package/src/game/index.js +49 -0
  181. package/src/game/objects/go.js +220 -0
  182. package/src/game/objects/imagego.js +30 -0
  183. package/src/game/objects/index.js +54 -0
  184. package/src/game/objects/isometric-scene.js +260 -0
  185. package/src/game/objects/layoutscene.js +549 -0
  186. package/src/game/objects/scene.js +175 -0
  187. package/src/game/objects/scene3d.js +118 -0
  188. package/src/game/objects/text.js +221 -0
  189. package/src/game/objects/wrapper.js +232 -0
  190. package/src/game/pipeline.js +243 -0
  191. package/src/game/ui/button.js +396 -0
  192. package/src/game/ui/cursor.js +93 -0
  193. package/src/game/ui/fps.js +91 -0
  194. package/src/game/ui/index.js +5 -0
  195. package/src/game/ui/togglebutton.js +93 -0
  196. package/src/game/ui/tooltip.js +249 -0
  197. package/src/index.js +25 -0
  198. package/src/io/events.js +20 -0
  199. package/src/io/index.js +86 -0
  200. package/src/io/input.js +70 -0
  201. package/src/io/keys.js +152 -0
  202. package/src/io/mouse.js +61 -0
  203. package/src/io/touch.js +39 -0
  204. package/src/logger/debugtab.js +138 -0
  205. package/src/logger/index.js +3 -0
  206. package/src/logger/loggable.js +47 -0
  207. package/src/logger/logger.js +113 -0
  208. package/src/math/complex.js +37 -0
  209. package/src/math/constants.js +1 -0
  210. package/src/math/fractal.js +1271 -0
  211. package/src/math/gr.js +201 -0
  212. package/src/math/heat.js +202 -0
  213. package/src/math/index.js +12 -0
  214. package/src/math/noise.js +433 -0
  215. package/src/math/orbital.js +191 -0
  216. package/src/math/patterns.js +1339 -0
  217. package/src/math/penrose.js +259 -0
  218. package/src/math/quantum.js +115 -0
  219. package/src/math/random.js +195 -0
  220. package/src/math/tensor.js +1009 -0
  221. package/src/mixins/anchor.js +131 -0
  222. package/src/mixins/draggable.js +72 -0
  223. package/src/mixins/index.js +2 -0
  224. package/src/motion/bezier.js +132 -0
  225. package/src/motion/bounce.js +58 -0
  226. package/src/motion/easing.js +349 -0
  227. package/src/motion/float.js +130 -0
  228. package/src/motion/follow.js +125 -0
  229. package/src/motion/hop.js +52 -0
  230. package/src/motion/index.js +82 -0
  231. package/src/motion/motion.js +1124 -0
  232. package/src/motion/orbit.js +49 -0
  233. package/src/motion/oscillate.js +39 -0
  234. package/src/motion/parabolic.js +141 -0
  235. package/src/motion/patrol.js +147 -0
  236. package/src/motion/pendulum.js +48 -0
  237. package/src/motion/pulse.js +88 -0
  238. package/src/motion/shake.js +83 -0
  239. package/src/motion/spiral.js +144 -0
  240. package/src/motion/spring.js +150 -0
  241. package/src/motion/swing.js +47 -0
  242. package/src/motion/tween.js +92 -0
  243. package/src/motion/tweenetik.js +139 -0
  244. package/src/motion/waypoint.js +210 -0
  245. package/src/painter/index.js +8 -0
  246. package/src/painter/painter.colors.js +331 -0
  247. package/src/painter/painter.effects.js +230 -0
  248. package/src/painter/painter.img.js +229 -0
  249. package/src/painter/painter.js +295 -0
  250. package/src/painter/painter.lines.js +189 -0
  251. package/src/painter/painter.opacity.js +41 -0
  252. package/src/painter/painter.shapes.js +277 -0
  253. package/src/painter/painter.text.js +273 -0
  254. package/src/particle/emitter.js +124 -0
  255. package/src/particle/index.js +11 -0
  256. package/src/particle/particle-system.js +322 -0
  257. package/src/particle/particle.js +71 -0
  258. package/src/particle/updaters.js +170 -0
  259. package/src/shapes/arc.js +43 -0
  260. package/src/shapes/arrow.js +33 -0
  261. package/src/shapes/bezier.js +42 -0
  262. package/src/shapes/circle.js +62 -0
  263. package/src/shapes/clouds.js +56 -0
  264. package/src/shapes/cone.js +219 -0
  265. package/src/shapes/cross.js +70 -0
  266. package/src/shapes/cube.js +244 -0
  267. package/src/shapes/cylinder.js +254 -0
  268. package/src/shapes/diamond.js +48 -0
  269. package/src/shapes/euclidian.js +111 -0
  270. package/src/shapes/figure.js +115 -0
  271. package/src/shapes/geometry.js +220 -0
  272. package/src/shapes/group.js +375 -0
  273. package/src/shapes/heart.js +42 -0
  274. package/src/shapes/hexagon.js +26 -0
  275. package/src/shapes/image.js +192 -0
  276. package/src/shapes/index.js +111 -0
  277. package/src/shapes/line.js +29 -0
  278. package/src/shapes/pattern.js +90 -0
  279. package/src/shapes/pin.js +44 -0
  280. package/src/shapes/poly.js +31 -0
  281. package/src/shapes/prism.js +226 -0
  282. package/src/shapes/rect.js +35 -0
  283. package/src/shapes/renderable.js +333 -0
  284. package/src/shapes/ring.js +26 -0
  285. package/src/shapes/roundrect.js +95 -0
  286. package/src/shapes/shape.js +117 -0
  287. package/src/shapes/slice.js +26 -0
  288. package/src/shapes/sphere.js +314 -0
  289. package/src/shapes/sphere3d.js +537 -0
  290. package/src/shapes/square.js +15 -0
  291. package/src/shapes/star.js +99 -0
  292. package/src/shapes/svg.js +408 -0
  293. package/src/shapes/text.js +553 -0
  294. package/src/shapes/traceable.js +83 -0
  295. package/src/shapes/transform.js +357 -0
  296. package/src/shapes/transformable.js +172 -0
  297. package/src/shapes/triangle.js +26 -0
  298. package/src/sound/index.js +17 -0
  299. package/src/sound/sound.js +473 -0
  300. package/src/sound/synth.analyzer.js +149 -0
  301. package/src/sound/synth.effects.js +207 -0
  302. package/src/sound/synth.envelope.js +59 -0
  303. package/src/sound/synth.js +229 -0
  304. package/src/sound/synth.musical.js +160 -0
  305. package/src/sound/synth.noise.js +85 -0
  306. package/src/sound/synth.oscillators.js +293 -0
  307. package/src/state/index.js +10 -0
  308. package/src/state/state-machine.js +371 -0
  309. package/src/util/camera3d.js +438 -0
  310. package/src/util/index.js +6 -0
  311. package/src/util/isometric-camera.js +235 -0
  312. package/src/util/layout.js +317 -0
  313. package/src/util/position.js +147 -0
  314. package/src/util/tasks.js +47 -0
  315. package/src/util/zindex.js +287 -0
  316. package/src/webgl/index.js +9 -0
  317. package/src/webgl/shaders/sphere-shaders.js +994 -0
  318. package/src/webgl/webgl-renderer.js +388 -0
  319. package/tde.png +0 -0
  320. package/test/math/orbital.test.js +61 -0
  321. package/test/math/tensor.test.js +114 -0
  322. package/test/particle/emitter.test.js +204 -0
  323. package/test/particle/particle-system.test.js +310 -0
  324. package/test/particle/particle.test.js +116 -0
  325. package/test/particle/updaters.test.js +386 -0
  326. package/test/setup.js +120 -0
  327. package/test/shapes/euclidian.test.js +44 -0
  328. package/test/shapes/geometry.test.js +86 -0
  329. package/test/shapes/group.test.js +86 -0
  330. package/test/shapes/rectangle.test.js +64 -0
  331. package/test/shapes/transform.test.js +379 -0
  332. package/test/util/camera3d.test.js +428 -0
  333. package/test/util/scene3d.test.js +352 -0
  334. package/types/collision.d.ts +249 -0
  335. package/types/common.d.ts +155 -0
  336. package/types/game.d.ts +497 -0
  337. package/types/index.d.ts +309 -0
  338. package/types/io.d.ts +188 -0
  339. package/types/logger.d.ts +127 -0
  340. package/types/math.d.ts +268 -0
  341. package/types/mixins.d.ts +92 -0
  342. package/types/motion.d.ts +678 -0
  343. package/types/painter.d.ts +378 -0
  344. package/types/shapes.d.ts +864 -0
  345. package/types/sound.d.ts +672 -0
  346. package/types/state.d.ts +251 -0
  347. package/types/util.d.ts +253 -0
  348. package/vite.config.js +50 -0
  349. 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
+ }