@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,220 @@
1
+ import { Euclidian } from "./euclidian.js";
2
+
3
+ /**
4
+ * Geometry2d
5
+ * ----------
6
+ *
7
+ * A foundational class representing any object with spatial boundaries.
8
+ * Builds upon `Euclidian` by adding:
9
+ *
10
+ * - **Bounding logic** with memoization
11
+ * - **Constraint enforcement** (`minX`, `maxX`, etc.)
12
+ * - **Property change tracking** for dirty bounding recalculations
13
+ *
14
+ * This class is *not* concerned with transforms, rendering, or visibility.
15
+ * Instead, it's the core layer where **position + size = spatial identity**.
16
+ *
17
+ * ### Core Responsibilities
18
+ *
19
+ * 1. **Bounds Calculation**: Caches and computes bounding boxes using the Template Method pattern:
20
+ * - `getBounds()` → returns cached bounds
21
+ * - `calculateBounds()` → override point for subclasses
22
+ * 2. **Constraints**: Applies optional min/max limits on x/y positions
23
+ * 3. **Change Tracking**: Automatically marks bounds as dirty when spatial props are modified
24
+ * 4. **Tick Awareness**: Supports `update(dt)` to re-evaluate bounds when needed
25
+ *
26
+ * ### Coordinate System
27
+ *
28
+ * - The `x` and `y` properties refer to the **center** of the object.
29
+ * - Use `getLocalPosition()` for top-left alignment in layout systems.
30
+ *
31
+ * ### Subclassing Guidelines
32
+ *
33
+ * - Override `calculateBounds()` if the object has non-rectangular or transformed geometry.
34
+ * - Call `markBoundsDirty()` in custom logic if bounds-affecting state changes.
35
+ *
36
+ * @abstract
37
+ * @extends Euclidian
38
+ */
39
+ export class Geometry2d extends Euclidian {
40
+ /**
41
+ * @param {Object} [options={}]
42
+ * @param {number} [options.minX] - Minimum X constraint (optional)
43
+ * @param {number} [options.maxX] - Maximum X constraint (optional)
44
+ * @param {number} [options.minY] - Minimum Y constraint (optional)
45
+ * @param {number} [options.maxY] - Maximum Y constraint (optional)
46
+ * @param {boolean} [options.crisp=true] - Whether to round to whole pixels
47
+ */
48
+ constructor(options = {}) {
49
+ super(options);
50
+ this._minX = options.minX;
51
+ this._maxX = options.maxX;
52
+ this._minY = options.minY;
53
+ this._maxY = options.maxY;
54
+ this._boundsDirty = true;
55
+ this._cachedBounds = null;
56
+ this.crisp = options.crisp ?? true;
57
+ this.logger.log("Geometry2d", this.x, this.y, this.width, this.height);
58
+ }
59
+
60
+ update() {
61
+ this.trace("Geometry2d.update");
62
+ this.applyConstraints();
63
+ this.getBounds(); // Trigger lazy recompute if dirty
64
+ }
65
+
66
+ /**
67
+ * Gets the minimum allowed X value.
68
+ * @type {number|undefined}
69
+ */
70
+ get minX() {
71
+ return this._minX;
72
+ }
73
+ set minX(v) {
74
+ this._minX = v;
75
+ }
76
+
77
+ /**
78
+ * Gets the maximum allowed X value.
79
+ * @type {number|undefined}
80
+ */
81
+ get maxX() {
82
+ return this._maxX;
83
+ }
84
+ set maxX(v) {
85
+ this._maxX = v;
86
+ }
87
+
88
+ /**
89
+ * Gets the minimum allowed Y value.
90
+ * @type {number|undefined}
91
+ */
92
+ get minY() {
93
+ return this._minY;
94
+ }
95
+ set minY(v) {
96
+ this._minY = v;
97
+ }
98
+
99
+ /**
100
+ * Gets the maximum allowed Y value.
101
+ * @type {number|undefined}
102
+ */
103
+ get maxY() {
104
+ return this._maxY;
105
+ }
106
+ set maxY(v) {
107
+ this._maxY = v;
108
+ }
109
+
110
+ /**
111
+ * Whether the bounding box is dirty and needs recalculation.
112
+ * @type {boolean}
113
+ * @readonly
114
+ */
115
+ get boundsDirty() {
116
+ return this._boundsDirty;
117
+ }
118
+
119
+ /**
120
+ * Applies positional constraints and optionally rounds to whole pixels.
121
+ */
122
+ applyConstraints() {
123
+ if (this._minX !== undefined) this.x = Math.max(this.x, this._minX);
124
+ if (this._maxX !== undefined) this.x = Math.min(this.x, this._maxX);
125
+ if (this._minY !== undefined) this.y = Math.max(this.y, this._minY);
126
+ if (this._maxY !== undefined) this.y = Math.min(this.y, this._maxY);
127
+
128
+ if (this.crisp) {
129
+ this.x = Math.round(this.x);
130
+ this.y = Math.round(this.y);
131
+ this.width = Math.round(this.width);
132
+ this.height = Math.round(this.height);
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Returns the object's bounding box.
138
+ * Uses memoization to avoid unnecessary recomputation.
139
+ *
140
+ * @returns {{x: number, y: number, width: number, height: number}}
141
+ */
142
+ getBounds() {
143
+ if (this._boundsDirty || !this._cachedBounds) {
144
+ //this.trace("Geometry2d.getBounds", this.name || this.constructor.name, this._boundsDirty, this._cachedBounds);
145
+ this._cachedBounds = this.calculateBounds();
146
+ this._boundsDirty = false;
147
+ }
148
+ return this._cachedBounds;
149
+ }
150
+
151
+ /**
152
+ * Called by `getBounds()` when bounds are dirty.
153
+ * Can be overridden to support more complex bounds (e.g. transformed shapes).
154
+ *
155
+ * @protected
156
+ * @returns {{x: number, y: number, width: number, height: number}}
157
+ */
158
+ calculateBounds() {
159
+ return {
160
+ width: this.width,
161
+ height: this.height,
162
+ x: this.x,
163
+ y: this.y,
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Returns the object's top-left corner, taking into account group containment.
169
+ * Useful for layouting or aligning objects to pixel grids.
170
+ *
171
+ * @returns {{x: number, y: number}}
172
+ */
173
+ getLocalPosition() {
174
+ // Get the parent group's position if it exists
175
+ let parentX = 0;
176
+ let parentY = 0;
177
+
178
+ // If this object is part of a group, adjust for the group's position
179
+ if (this.parent) {
180
+ parentX = this.parent.x;
181
+ parentY = this.parent.y;
182
+ }
183
+
184
+ return {
185
+ x: (this.x - parentX) - this.width / 2,
186
+ y: (this.y - parentY) - this.height / 2,
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Marks bounds as dirty.
192
+ * Called automatically by internal setters, but exposed for custom logic.
193
+ *
194
+ * @protected
195
+ */
196
+ markBoundsDirty() {
197
+ this._boundsDirty = true;
198
+ }
199
+
200
+ validateProp(v, prop) {
201
+ super.validateProp(v, prop);
202
+ const originalProp = this[prop];
203
+ if(v !== originalProp) {
204
+ //console.log("Geometry2d.marking bounds dirty", this.name || this.constructor.name, prop, v, "originalProp", originalProp);
205
+ this.markBoundsDirty();
206
+ }
207
+ }
208
+
209
+ setTopLeft(x, y) {
210
+ this.x = x + this.width / 2;
211
+ this.y = y + this.height / 2;
212
+ return this;
213
+ }
214
+
215
+ setCenter(x, y) {
216
+ this.x = x;
217
+ this.y = y;
218
+ return this;
219
+ }
220
+ }
@@ -0,0 +1,375 @@
1
+ import { Painter } from "../painter/painter";
2
+ import { Transformable } from "./transformable";
3
+ import { ZOrderedCollection } from "../util";
4
+ /**
5
+ * Group - A powerful container for composing and manipulating multiple transformable objects
6
+ *
7
+ * ### Core Capabilities
8
+ *
9
+ * - Aggregate multiple transformable objects into a single unit
10
+ * - Efficient bounding box calculation with memoization
11
+ * - Performant object management
12
+ * - Property inheritance from group to children
13
+ *
14
+ * ### Rendering Behavior
15
+ *
16
+ * - Applies group's transformations to all children
17
+ * - Renders children in order of addition
18
+ * - Supports both Shape and GameObject hierarchies
19
+ *
20
+ * @extends Transformable
21
+ */
22
+ export class Group extends Transformable {
23
+ /**
24
+ * Creates a new Group instance
25
+ *
26
+ * @param {Object} [options={}] - Additional rendering options
27
+ * @param {boolean} [options.inheritOpacity=true] - Whether opacity should cascade to children
28
+ * @param {boolean} [options.inheritVisible=true] - Whether visibility should cascade to children
29
+ * @param {boolean} [options.inheritScale=false] - Whether scale should cascade to children
30
+ */
31
+ constructor(options = {}) {
32
+ // Call parent constructor with all options
33
+ super(options);
34
+
35
+ // Create the z-ordered collection
36
+ this._collection = new ZOrderedCollection({
37
+ sortByZIndex: options.sortByZIndex || true
38
+ });
39
+ this._collection._owner = this; // Give collection a reference to its owner
40
+
41
+ // Initialize state tracking
42
+ this._childrenVersion = 0;
43
+ this._cachedBounds = null;
44
+
45
+ options.width = Math.max(0, options.width || 0);
46
+ options.height = Math.max(0, options.height || 0);
47
+
48
+ // Track if dimensions were explicitly set in constructor
49
+ this.userDefinedWidth = options.width;
50
+ this.userDefinedHeight = options.height;
51
+
52
+ // Only consider dimensions as user-defined if they were explicitly provided in options
53
+ this.userDefinedDimensions = options.width !== undefined && options.height !== undefined &&
54
+ (options.width > 0 || options.height > 0);
55
+ }
56
+
57
+
58
+ /**
59
+ * Add object to group with type checking
60
+ * @param {Transformable} object - Object to add
61
+ * @returns {Transformable} The added object
62
+ */
63
+ add(object) {
64
+ if (object == null || object == undefined) {
65
+ throw new Error("Object is null or undefined");
66
+ }
67
+ if (!(object instanceof Transformable)) {
68
+ throw new TypeError("Group can only add Transformable instances");
69
+ }
70
+ object.parent = this;
71
+ this._collection.add(object);
72
+ this._childrenVersion++;
73
+ this.markBoundsDirty();
74
+ this.invalidateCache();
75
+ return object;
76
+ }
77
+
78
+ /**
79
+ * Remove object from group
80
+ * @param {Transformable} object - Object to remove
81
+ * @returns {boolean} Whether object was removed
82
+ */
83
+ remove(object) {
84
+ const result = this._collection.remove(object);
85
+ if (result) {
86
+ object.parent = null;
87
+ this._childrenVersion++;
88
+ this.markBoundsDirty();
89
+ this.invalidateCache();
90
+ }
91
+ return result;
92
+ }
93
+
94
+ /**
95
+ * Clear all objects from group
96
+ */
97
+ clear() {
98
+ this._collection.clear();
99
+ this._childrenVersion++;
100
+ this.markBoundsDirty();
101
+ this.invalidateCache();
102
+ }
103
+
104
+ // Z-ordering methods
105
+ bringToFront(object) {
106
+ return this._collection.bringToFront(object);
107
+ }
108
+
109
+ sendToBack(object) {
110
+ return this._collection.sendToBack(object);
111
+ }
112
+
113
+ bringForward(object) {
114
+ return this._collection.bringForward(object);
115
+ }
116
+
117
+ sendBackward(object) {
118
+ return this._collection.sendBackward(object);
119
+ }
120
+
121
+ /**
122
+ * Render group and all children.
123
+ * Transformations are already applied by super.draw().
124
+ */
125
+ draw() {
126
+ super.draw();
127
+ this.logger.log("Group.draw children:", this.children.length);
128
+ this._renderChildren();
129
+ }
130
+
131
+ /**
132
+ * Render children normally (non-cached path)
133
+ * @private
134
+ */
135
+ _renderChildren() {
136
+ const sortedChildren = this._collection.getSortedChildren();
137
+ for (let i = 0; i < sortedChildren.length; i++) {
138
+ const child = sortedChildren[i];
139
+ if (child.visible) {
140
+ Painter.save();
141
+ child.render();
142
+ Painter.restore();
143
+ }
144
+ }
145
+ }
146
+
147
+
148
+ /**
149
+ * Update all children with active update methods
150
+ * @param {number} dt - Delta time in seconds
151
+ */
152
+ update(dt) {
153
+ this.logger.groupCollapsed("Group.update");
154
+ const sortedChildren = this._collection.getSortedChildren();
155
+
156
+ for (let i = 0; i < sortedChildren.length; i++) {
157
+ const child = sortedChildren[i];
158
+ if (child.active && typeof child.update === 'function') {
159
+ child.update(dt);
160
+ }
161
+ }
162
+ super.update(dt);
163
+ this.logger.groupEnd();
164
+ }
165
+
166
+ /**
167
+ * Get group's children array
168
+ * @returns {Array} Children array
169
+ */
170
+ get children() {
171
+ // Check if the collection exists and if it doesn't, return an empty array
172
+ return this._collection?.children || [];
173
+ }
174
+
175
+ /**
176
+ * Override width getter
177
+ * @returns {number} Width
178
+ */
179
+ get width() {
180
+ if (this.userDefinedDimensions) {
181
+ return this._width;
182
+ }
183
+ return this.getBounds().width;
184
+ }
185
+
186
+ /**
187
+ * Override width setter
188
+ * @param {number} v - New width
189
+ */
190
+ set width(v) {
191
+ const max = Math.max(0, v);
192
+ this._width = max;
193
+ this.userDefinedWidth = max;
194
+ this.userDefinedDimensions = (this.userDefinedWidth > 0 || this.userDefinedHeight > 0) &&
195
+ this.userDefinedWidth !== undefined && this.userDefinedHeight !== undefined;
196
+ this.markBoundsDirty();
197
+ }
198
+
199
+ /**
200
+ * Override height getter
201
+ * @returns {number} Height
202
+ */
203
+ get height() {
204
+ if (this.userDefinedDimensions) {
205
+ return this._height;
206
+ }
207
+ return this.getBounds().height;
208
+ }
209
+
210
+ /**
211
+ * Override height setter
212
+ * @param {number} v - New height
213
+ */
214
+ set height(v) {
215
+ const max = Math.max(0, v);
216
+ this._height = max;
217
+ this.userDefinedHeight = max;
218
+ this.userDefinedDimensions = (this.userDefinedWidth > 0 || this.userDefinedHeight > 0) &&
219
+ this.userDefinedWidth !== undefined && this.userDefinedHeight !== undefined;
220
+ this.markBoundsDirty();
221
+ }
222
+
223
+
224
+ /**
225
+ * Override calculateBounds to compute from children
226
+ * @returns {Object} Bounds object
227
+ */
228
+ calculateBounds() {
229
+ // If explicitly sized, use those dimensions
230
+ if (this.userDefinedDimensions) {
231
+ return {
232
+ x: this.x,
233
+ y: this.y,
234
+ width: this._width,
235
+ height: this._height
236
+ };
237
+ }
238
+
239
+ // No children = empty bounds
240
+ if (!this.children?.length) {
241
+ return {
242
+ x: this.x,
243
+ y: this.y,
244
+ width: 0,
245
+ height: 0
246
+ };
247
+ }
248
+
249
+ let minX = Infinity;
250
+ let minY = Infinity;
251
+ let maxX = -Infinity;
252
+ let maxY = -Infinity;
253
+
254
+ // Calculate bounds from all children
255
+ for (const child of this.children) {
256
+ // Get the child's position and dimensions
257
+ const childX = child.x;
258
+ const childY = child.y;
259
+ const childWidth = child.width;
260
+ const childHeight = child.height;
261
+
262
+ // Calculate the child's bounding box edges
263
+ const childLeft = childX - childWidth / 2;
264
+ const childRight = childX + childWidth / 2;
265
+ const childTop = childY - childHeight / 2;
266
+ const childBottom = childY + childHeight / 2;
267
+
268
+ // Update min/max coordinates
269
+ minX = Math.min(minX, childLeft);
270
+ maxX = Math.max(maxX, childRight);
271
+ minY = Math.min(minY, childTop);
272
+ maxY = Math.max(maxY, childBottom);
273
+ }
274
+
275
+ // Calculate dimensions
276
+ const width = maxX - minX;
277
+ const height = maxY - minY;
278
+
279
+ // Return bounds centered on group position
280
+ return {
281
+ x: this.x,
282
+ y: this.y,
283
+ width: width,
284
+ height: height
285
+ };
286
+ }
287
+
288
+ /**
289
+ * Returns debug bounds in local space (centered at origin).
290
+ * Used for debug drawing after transforms have been applied.
291
+ * @returns {{x: number, y: number, width: number, height: number}}
292
+ */
293
+ getDebugBounds() {
294
+ const bounds = this.calculateBounds();
295
+
296
+ // Return bounds centered at local origin (0, 0)
297
+ // This works because debug is drawn after translation to group's position
298
+ return {
299
+ width: bounds.width,
300
+ height: bounds.height,
301
+ x: -bounds.width / 2,
302
+ y: -bounds.height / 2,
303
+ };
304
+ }
305
+
306
+ // ============================================================
307
+ // Group-wide Transform Operations
308
+ // ============================================================
309
+
310
+ /**
311
+ * Applies a transform callback to each child in the group.
312
+ * Useful for batch operations on all children.
313
+ *
314
+ * @param {function(import('./transform.js').Transform, import('./transformable.js').Transformable, number): void} callback
315
+ * Callback receiving (transform, child, index)
316
+ * @returns {Group} this for chaining
317
+ *
318
+ * @example
319
+ * // Scale all children by 0.5
320
+ * group.forEachTransform((t) => t.scale(0.5));
321
+ *
322
+ * // Rotate each child differently
323
+ * group.forEachTransform((t, child, i) => t.rotation(i * 15));
324
+ */
325
+ forEachTransform(callback) {
326
+ this.children.forEach((child, index) => {
327
+ if (child.transform) {
328
+ callback(child.transform, child, index);
329
+ }
330
+ });
331
+ return this;
332
+ }
333
+
334
+ /**
335
+ * Translates all children by the given offset.
336
+ * This moves children relative to their current positions,
337
+ * not the group itself.
338
+ *
339
+ * @param {number} dx - Delta X
340
+ * @param {number} dy - Delta Y
341
+ * @returns {Group} this for chaining
342
+ */
343
+ translateChildren(dx, dy) {
344
+ return this.forEachTransform((t) => t.translateBy(dx, dy));
345
+ }
346
+
347
+ /**
348
+ * Scales all children by the given factor.
349
+ *
350
+ * @param {number} factor - Scale factor (1 = no change)
351
+ * @returns {Group} this for chaining
352
+ */
353
+ scaleChildren(factor) {
354
+ return this.forEachTransform((t) => t.scaleBy(factor));
355
+ }
356
+
357
+ /**
358
+ * Rotates all children by the given amount.
359
+ *
360
+ * @param {number} degrees - Rotation amount in degrees
361
+ * @returns {Group} this for chaining
362
+ */
363
+ rotateChildren(degrees) {
364
+ return this.forEachTransform((t) => t.rotateBy(degrees));
365
+ }
366
+
367
+ /**
368
+ * Resets transforms on all children to default values.
369
+ *
370
+ * @returns {Group} this for chaining
371
+ */
372
+ resetChildTransforms() {
373
+ return this.forEachTransform((t) => t.reset());
374
+ }
375
+ }
@@ -0,0 +1,42 @@
1
+ import { Shape } from "./shape.js";
2
+ import { Painter } from "../painter/painter.js";
3
+
4
+ export class Heart extends Shape {
5
+ constructor(options = {}) {
6
+ super(options);
7
+ }
8
+
9
+ draw() {
10
+ super.draw();
11
+ const w = this.width;
12
+ const h = this.height;
13
+ const topCurveHeight = h * 0.3;
14
+ const lines = Painter.lines;
15
+ lines.beginPath();
16
+ lines.moveTo(0, topCurveHeight);
17
+ // Left arc
18
+ lines.bezierCurveTo(0, 0, -w / 2, 0, -w / 2, topCurveHeight);
19
+ // Bottom point
20
+ lines.bezierCurveTo(-w / 2, h * 0.8, 0, h, 0, h);
21
+ // Right arc
22
+ lines.bezierCurveTo(0, h, w / 2, h * 0.8, w / 2, topCurveHeight);
23
+ lines.bezierCurveTo(w / 2, 0, 0, 0, 0, topCurveHeight);
24
+ lines.closePath();
25
+ if (this.color) {
26
+ Painter.colors.fill(this.color);
27
+ }
28
+
29
+ if (this.stroke) {
30
+ Painter.colors.stroke(this.stroke, this.lineWidth);
31
+ }
32
+ }
33
+
34
+ getBounds() {
35
+ return {
36
+ x: this.x,
37
+ y: this.y + this.height / 2,
38
+ width: this.width,
39
+ height: this.height,
40
+ };
41
+ }
42
+ }
@@ -0,0 +1,26 @@
1
+ import { Shape } from "./shape.js";
2
+ import { Painter } from "../painter/painter.js";
3
+ export class Hexagon extends Shape {
4
+ constructor(radius, options = {}) {
5
+ super(options);
6
+ this.radius = radius;
7
+ }
8
+
9
+ draw() {
10
+ super.draw();
11
+ const points = Array.from({ length: 6 }, (_, i) => {
12
+ const angle = (Math.PI / 3) * i;
13
+ return {
14
+ x: Math.cos(angle) * this.radius,
15
+ y: Math.sin(angle) * this.radius,
16
+ };
17
+ });
18
+
19
+ Painter.shapes.polygon(
20
+ points,
21
+ this.color,
22
+ this.stroke,
23
+ this.lineWidth
24
+ );
25
+ }
26
+ }