@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,1060 @@
1
+ /**
2
+ * @module FluentGO
3
+ * @description Builder class for GameObject operations in the fluent API
4
+ *
5
+ * Provides chainable methods for adding shapes, motions, and children to GameObjects.
6
+ */
7
+
8
+ import { GameObject } from "../game/objects/go.js";
9
+ import { GameObjectShapeWrapper } from "../game/objects/wrapper.js";
10
+ import { Group } from "../shapes/group.js";
11
+
12
+ // Import all shapes
13
+ import { Circle } from "../shapes/circle.js";
14
+ import { Rectangle } from "../shapes/rect.js";
15
+ import { RoundedRectangle } from "../shapes/roundrect.js";
16
+ import { Square } from "../shapes/square.js";
17
+ import { Triangle } from "../shapes/triangle.js";
18
+ import { Star } from "../shapes/star.js";
19
+ import { Diamond } from "../shapes/diamond.js";
20
+ import { Hexagon } from "../shapes/hexagon.js";
21
+ import { Heart } from "../shapes/heart.js";
22
+ import { Line } from "../shapes/line.js";
23
+ import { Arc } from "../shapes/arc.js";
24
+ import { Ring } from "../shapes/ring.js";
25
+ import { Polygon } from "../shapes/poly.js";
26
+ import { Arrow } from "../shapes/arrow.js";
27
+ import { Cross } from "../shapes/cross.js";
28
+ import { Pin } from "../shapes/pin.js";
29
+ import { Cloud } from "../shapes/clouds.js";
30
+ import { TextShape } from "../shapes/text.js";
31
+ import { ImageShape } from "../shapes/image.js";
32
+ import { SVGShape } from "../shapes/svg.js";
33
+
34
+ // Import motion system
35
+ import { Motion } from "../motion/motion.js";
36
+ import { Tweenetik } from "../motion/tweenetik.js";
37
+
38
+ /**
39
+ * FluentGO - Builder class for GameObject operations
40
+ */
41
+ export class FluentGO {
42
+ /** @type {import('./fluent-scene.js').FluentScene|FluentGO} */
43
+ #parent;
44
+ /** @type {GameObject} */
45
+ #go;
46
+ /** @type {Object} */
47
+ #refs;
48
+ /** @type {Object} */
49
+ #state;
50
+ /** @type {Array} */
51
+ #shapes = [];
52
+ /** @type {Array} */
53
+ #motions = [];
54
+
55
+ /**
56
+ * @param {import('./fluent-scene.js').FluentScene|FluentGO} parent - Parent context
57
+ * @param {GameObject} go - Wrapped GameObject instance
58
+ * @param {Object} refs - Shared refs object
59
+ * @param {Object} state - Shared state object
60
+ */
61
+ constructor(parent, go, refs, state) {
62
+ this.#parent = parent;
63
+ this.#go = go;
64
+ this.#refs = refs;
65
+ this.#state = state;
66
+ }
67
+
68
+ // ─────────────────────────────────────────────────────────
69
+ // SHAPE SHORTCUTS
70
+ // ─────────────────────────────────────────────────────────
71
+
72
+ /**
73
+ * Add a Circle shape
74
+ * @param {Object} [opts] - Circle options
75
+ * @param {number} [opts.radius] - Circle radius
76
+ * @param {string} [opts.fill] - Fill color
77
+ * @param {string} [opts.stroke] - Stroke color
78
+ * @returns {FluentGO}
79
+ */
80
+ circle(opts = {}) {
81
+ const normalized = this.#normalizeShapeOpts(opts);
82
+ const { radius = 30, ...rest } = normalized;
83
+ const shape = new Circle(radius, rest);
84
+ return this.#addShapeInstance(shape);
85
+ }
86
+
87
+ /**
88
+ * Add a Rectangle shape
89
+ * @param {Object} [opts] - Rectangle options
90
+ * @param {number} [opts.width] - Rectangle width
91
+ * @param {number} [opts.height] - Rectangle height
92
+ * @param {string} [opts.fill] - Fill color
93
+ * @returns {FluentGO}
94
+ */
95
+ rect(opts = {}) {
96
+ const normalized = this.#normalizeShapeOpts(opts);
97
+ const shape = new Rectangle(normalized);
98
+ return this.#addShapeInstance(shape);
99
+ }
100
+
101
+ /**
102
+ * Add a RoundedRectangle shape
103
+ * @param {Object} [opts] - RoundedRectangle options
104
+ * @param {number} [opts.radius] - Corner radius
105
+ * @returns {FluentGO}
106
+ */
107
+ roundRect(opts = {}) {
108
+ const normalized = this.#normalizeShapeOpts(opts);
109
+ const { radius = 10, ...rest } = normalized;
110
+ const shape = new RoundedRectangle(radius, rest);
111
+ return this.#addShapeInstance(shape);
112
+ }
113
+
114
+ /**
115
+ * Add a Square shape
116
+ * @param {Object} [opts] - Square options
117
+ * @param {number} [opts.size] - Square size
118
+ * @returns {FluentGO}
119
+ */
120
+ square(opts = {}) {
121
+ const normalized = this.#normalizeShapeOpts(opts);
122
+ const { size = 50, ...rest } = normalized;
123
+ const shape = new Square(size, rest);
124
+ return this.#addShapeInstance(shape);
125
+ }
126
+
127
+ /**
128
+ * Add a Star shape
129
+ * @param {Object} [opts] - Star options
130
+ * @param {number} [opts.points] - Number of points (spikes)
131
+ * @param {number} [opts.radius] - Outer radius
132
+ * @param {number} [opts.inset] - Inner radius ratio (0-1)
133
+ * @returns {FluentGO}
134
+ */
135
+ star(opts = {}) {
136
+ const normalized = this.#normalizeShapeOpts(opts);
137
+ const { radius = 40, points = 5, inset = 0.5, ...rest } = normalized;
138
+ const shape = new Star(radius, points, inset, rest);
139
+ return this.#addShapeInstance(shape);
140
+ }
141
+
142
+ /**
143
+ * Add a Triangle shape
144
+ * @param {Object} [opts] - Triangle options
145
+ * @param {number} [opts.size] - Triangle size
146
+ * @returns {FluentGO}
147
+ */
148
+ triangle(opts = {}) {
149
+ const normalized = this.#normalizeShapeOpts(opts);
150
+ const { size = 50, ...rest } = normalized;
151
+ const shape = new Triangle(size, rest);
152
+ return this.#addShapeInstance(shape);
153
+ }
154
+
155
+ /**
156
+ * Add a Polygon shape
157
+ * @param {Object} [opts] - Polygon options
158
+ * @param {Array} [opts.points] - Polygon vertices
159
+ * @returns {FluentGO}
160
+ */
161
+ poly(opts = {}) {
162
+ const normalized = this.#normalizeShapeOpts(opts);
163
+ const shape = new Polygon(normalized);
164
+ return this.#addShapeInstance(shape);
165
+ }
166
+
167
+ /**
168
+ * Add a Line shape
169
+ * @param {Object} [opts] - Line options
170
+ * @param {number} [opts.length] - Line length
171
+ * @returns {FluentGO}
172
+ */
173
+ line(opts = {}) {
174
+ const normalized = this.#normalizeShapeOpts(opts);
175
+ const { length = 40, ...rest } = normalized;
176
+ const shape = new Line(length, rest);
177
+ return this.#addShapeInstance(shape);
178
+ }
179
+
180
+ /**
181
+ * Add a Hexagon shape
182
+ * @param {Object} [opts] - Hexagon options
183
+ * @param {number} [opts.radius] - Hexagon radius
184
+ * @returns {FluentGO}
185
+ */
186
+ hexagon(opts = {}) {
187
+ const normalized = this.#normalizeShapeOpts(opts);
188
+ const { radius = 30, ...rest } = normalized;
189
+ const shape = new Hexagon(radius, rest);
190
+ return this.#addShapeInstance(shape);
191
+ }
192
+
193
+ /**
194
+ * Add a Diamond shape
195
+ * @param {Object} [opts] - Diamond options
196
+ * @returns {FluentGO}
197
+ */
198
+ diamond(opts = {}) {
199
+ const normalized = this.#normalizeShapeOpts(opts);
200
+ const shape = new Diamond(normalized);
201
+ return this.#addShapeInstance(shape);
202
+ }
203
+
204
+ /**
205
+ * Add a Heart shape
206
+ * @param {Object} [opts] - Heart options
207
+ * @param {number} [opts.size] - Heart size (sets width and height)
208
+ * @param {number} [opts.width] - Heart width
209
+ * @param {number} [opts.height] - Heart height
210
+ * @returns {FluentGO}
211
+ */
212
+ heart(opts = {}) {
213
+ const normalized = this.#normalizeShapeOpts(opts);
214
+ const { size, ...rest } = normalized;
215
+ if (size && !rest.width) rest.width = size;
216
+ if (size && !rest.height) rest.height = size;
217
+ const shape = new Heart(rest);
218
+ return this.#addShapeInstance(shape);
219
+ }
220
+
221
+ /**
222
+ * Add an Arc shape
223
+ * @param {Object} [opts] - Arc options
224
+ * @param {number} [opts.radius] - Arc radius
225
+ * @param {number} [opts.startAngle] - Start angle
226
+ * @param {number} [opts.endAngle] - End angle
227
+ * @returns {FluentGO}
228
+ */
229
+ arc(opts = {}) {
230
+ const normalized = this.#normalizeShapeOpts(opts);
231
+ const { radius = 30, startAngle = 0, endAngle = Math.PI, ...rest } = normalized;
232
+ const shape = new Arc(radius, startAngle, endAngle, rest);
233
+ return this.#addShapeInstance(shape);
234
+ }
235
+
236
+ /**
237
+ * Add a Ring shape
238
+ * @param {Object} [opts] - Ring options
239
+ * @param {number} [opts.innerRadius] - Inner radius
240
+ * @param {number} [opts.outerRadius] - Outer radius
241
+ * @returns {FluentGO}
242
+ */
243
+ ring(opts = {}) {
244
+ const normalized = this.#normalizeShapeOpts(opts);
245
+ const { outerRadius = 40, innerRadius = 20, ...rest } = normalized;
246
+ const shape = new Ring(outerRadius, innerRadius, rest);
247
+ return this.#addShapeInstance(shape);
248
+ }
249
+
250
+ /**
251
+ * Add an Arrow shape
252
+ * @param {Object} [opts] - Arrow options
253
+ * @param {number} [opts.length] - Arrow length
254
+ * @returns {FluentGO}
255
+ */
256
+ arrow(opts = {}) {
257
+ const normalized = this.#normalizeShapeOpts(opts);
258
+ const { length = 50, ...rest } = normalized;
259
+ const shape = new Arrow(length, rest);
260
+ return this.#addShapeInstance(shape);
261
+ }
262
+
263
+ /**
264
+ * Add a Cross shape
265
+ * @param {Object} [opts] - Cross options
266
+ * @param {number} [opts.size] - Cross size
267
+ * @param {number} [opts.thickness] - Cross thickness
268
+ * @returns {FluentGO}
269
+ */
270
+ cross(opts = {}) {
271
+ const normalized = this.#normalizeShapeOpts(opts);
272
+ const { size = 40, thickness = 10, ...rest } = normalized;
273
+ const shape = new Cross(size, thickness, rest);
274
+ return this.#addShapeInstance(shape);
275
+ }
276
+
277
+ /**
278
+ * Add a Pin shape
279
+ * @param {Object} [opts] - Pin options
280
+ * @param {number} [opts.radius] - Pin radius
281
+ * @returns {FluentGO}
282
+ */
283
+ pin(opts = {}) {
284
+ const normalized = this.#normalizeShapeOpts(opts);
285
+ const { radius = 20, ...rest } = normalized;
286
+ const shape = new Pin(radius, rest);
287
+ return this.#addShapeInstance(shape);
288
+ }
289
+
290
+ /**
291
+ * Add a Cloud shape
292
+ * @param {Object} [opts] - Cloud options
293
+ * @param {number} [opts.size] - Cloud size
294
+ * @returns {FluentGO}
295
+ */
296
+ cloud(opts = {}) {
297
+ const normalized = this.#normalizeShapeOpts(opts);
298
+ const { size, width, height, ...rest } = normalized;
299
+ // Cloud constructor: (size, options)
300
+ const cloudSize = size || Math.min(width || 40, height || 40);
301
+ const shape = new Cloud(cloudSize, rest);
302
+ return this.#addShapeInstance(shape);
303
+ }
304
+
305
+ /**
306
+ * Add a TextShape
307
+ * @param {string} content - Text content
308
+ * @param {Object} [opts] - Text options
309
+ * @param {string} [opts.font] - Font specification
310
+ * @param {string} [opts.fill] - Text color
311
+ * @returns {FluentGO}
312
+ */
313
+ text(content, opts = {}) {
314
+ const normalized = this.#normalizeShapeOpts(opts);
315
+ // TextShape uses 'color' not 'fillColor'
316
+ if (normalized.fillColor) {
317
+ normalized.color = normalized.fillColor;
318
+ delete normalized.fillColor;
319
+ }
320
+ const shape = new TextShape(content, normalized);
321
+ return this.#addShapeInstance(shape);
322
+ }
323
+
324
+ /**
325
+ * Add an Image shape
326
+ * @param {HTMLImageElement|ImageData|string} src - Image source
327
+ * @param {Object} [opts] - Image options
328
+ * @returns {FluentGO}
329
+ */
330
+ image(src, opts = {}) {
331
+ const normalized = this.#normalizeShapeOpts(opts);
332
+
333
+ if (typeof src === 'string') {
334
+ // Load image from URL
335
+ const img = new Image();
336
+ img.src = src;
337
+ const shape = new ImageShape(img, normalized);
338
+
339
+ // Update when loaded
340
+ img.onload = () => {
341
+ shape._bitmap = img;
342
+ shape._width = opts.width ?? img.width;
343
+ shape._height = opts.height ?? img.height;
344
+ };
345
+
346
+ return this.#addShapeInstance(shape);
347
+ } else {
348
+ const shape = new ImageShape(src, normalized);
349
+ return this.#addShapeInstance(shape);
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Add an SVG shape
355
+ * @param {string} path - SVG path data
356
+ * @param {Object} [opts] - SVG options
357
+ * @returns {FluentGO}
358
+ */
359
+ svg(path, opts = {}) {
360
+ const normalized = this.#normalizeShapeOpts(opts);
361
+ const shape = new SVGShape({ path, ...normalized });
362
+ return this.#addShapeInstance(shape);
363
+ }
364
+
365
+ /**
366
+ * Generic shape add by class
367
+ * Note: This passes opts as the first argument to the constructor.
368
+ * For shapes with specific signatures, use the dedicated methods
369
+ * (circle, star, etc.) or create the shape instance and use addShapeInstance.
370
+ * @param {Function} ShapeClass - Shape constructor
371
+ * @param {Object} [opts] - Shape options
372
+ * @returns {FluentGO}
373
+ */
374
+ add(ShapeClass, opts = {}) {
375
+ const normalized = this.#normalizeShapeOpts(opts);
376
+ const shape = new ShapeClass(normalized);
377
+ return this.#addShapeInstance(shape);
378
+ }
379
+
380
+ /**
381
+ * Add a shape instance to the GO
382
+ * @private
383
+ */
384
+ #addShapeInstance(shape) {
385
+ this.#shapes.push(shape);
386
+
387
+ // Store shape reference on the GO for rendering
388
+ if (this.#shapes.length === 1) {
389
+ // First shape - store directly and set up rendering
390
+ this.#go._fluentShape = shape;
391
+ this.#go.renderable = shape;
392
+
393
+ // Hook into draw to render the shape
394
+ const originalDraw = this.#go.draw?.bind(this.#go) || (() => {});
395
+ this.#go.draw = function() {
396
+ originalDraw();
397
+ if (this._fluentShape && this.visible) {
398
+ this._fluentShape.render();
399
+ }
400
+ };
401
+
402
+ // Add getBounds for hit-testing support
403
+ const go = this.#go;
404
+ this.#go.getBounds = function() {
405
+ if (go._fluentShape && go._fluentShape.getBounds) {
406
+ return go._fluentShape.getBounds();
407
+ }
408
+ return null;
409
+ };
410
+ } else {
411
+ // Multiple shapes - use a Group
412
+ if (!(this.#go._fluentShape instanceof Group)) {
413
+ const firstShape = this.#shapes[0];
414
+ const group = new Group();
415
+ group.add(firstShape);
416
+ this.#go._fluentShape = group;
417
+ this.#go.renderable = group;
418
+ }
419
+ this.#go._fluentShape.add(shape);
420
+ }
421
+
422
+ return this;
423
+ }
424
+
425
+ /**
426
+ * Normalize shape options (shorthand conversions)
427
+ * @private
428
+ */
429
+ #normalizeShapeOpts(opts) {
430
+ const normalized = { ...opts };
431
+
432
+ // Shorthand: fill → fillColor (for Shape base class which uses 'color')
433
+ if (opts.fill !== undefined) {
434
+ normalized.color = opts.fill;
435
+ normalized.fillColor = opts.fill;
436
+ delete normalized.fill;
437
+ }
438
+
439
+ // Shorthand: stroke → strokeColor
440
+ if (opts.stroke !== undefined) {
441
+ normalized.strokeColor = opts.stroke;
442
+ delete normalized.stroke;
443
+ }
444
+
445
+ return normalized;
446
+ }
447
+
448
+ // ─────────────────────────────────────────────────────────
449
+ // MOTION SHORTCUTS
450
+ // ─────────────────────────────────────────────────────────
451
+
452
+ /**
453
+ * Add oscillation motion
454
+ * @param {Object} [opts] - Motion options
455
+ * @param {string} [opts.prop='y'] - Property to animate
456
+ * @param {number} [opts.min=-50] - Minimum offset
457
+ * @param {number} [opts.max=50] - Maximum offset
458
+ * @param {number} [opts.duration=2] - Duration in seconds
459
+ * @returns {FluentGO}
460
+ */
461
+ oscillate(opts = {}) {
462
+ return this.motion('oscillate', opts);
463
+ }
464
+
465
+ /**
466
+ * Add pulse motion (scale animation)
467
+ * @param {Object} [opts] - Motion options
468
+ * @param {string} [opts.prop='scale'] - Property to animate
469
+ * @param {number} [opts.min=0.8] - Minimum value
470
+ * @param {number} [opts.max=1.2] - Maximum value
471
+ * @param {number} [opts.duration=1] - Duration in seconds
472
+ * @returns {FluentGO}
473
+ */
474
+ pulse(opts = {}) {
475
+ return this.motion('pulse', opts);
476
+ }
477
+
478
+ /**
479
+ * Add orbit motion
480
+ * @param {Object} [opts] - Motion options
481
+ * @param {number} [opts.centerX] - Orbit center X
482
+ * @param {number} [opts.centerY] - Orbit center Y
483
+ * @param {number} [opts.radiusX=100] - X radius
484
+ * @param {number} [opts.radiusY=100] - Y radius
485
+ * @param {number} [opts.duration=3] - Orbit period in seconds
486
+ * @param {boolean} [opts.clockwise=true] - Direction
487
+ * @returns {FluentGO}
488
+ */
489
+ orbit(opts = {}) {
490
+ return this.motion('orbit', opts);
491
+ }
492
+
493
+ /**
494
+ * Add float motion (random wandering)
495
+ * @param {Object} [opts] - Motion options
496
+ * @param {number} [opts.radius=20] - Float radius
497
+ * @param {number} [opts.speed=0.5] - Float speed
498
+ * @param {number} [opts.randomness=0.3] - Randomness factor
499
+ * @param {number} [opts.duration=5] - Duration
500
+ * @returns {FluentGO}
501
+ */
502
+ float(opts = {}) {
503
+ return this.motion('float', opts);
504
+ }
505
+
506
+ /**
507
+ * Add shake motion
508
+ * @param {Object} [opts] - Motion options
509
+ * @param {number} [opts.intensity=5] - Shake intensity
510
+ * @param {number} [opts.frequency=20] - Shake frequency
511
+ * @param {number} [opts.decay=0.9] - Decay factor
512
+ * @param {number} [opts.duration=0.5] - Duration
513
+ * @returns {FluentGO}
514
+ */
515
+ shake(opts = {}) {
516
+ return this.motion('shake', opts);
517
+ }
518
+
519
+ /**
520
+ * Add bounce motion
521
+ * @param {Object} [opts] - Motion options
522
+ * @param {number} [opts.height=100] - Bounce height
523
+ * @param {number} [opts.bounces=3] - Number of bounces
524
+ * @param {number} [opts.duration=2] - Duration
525
+ * @returns {FluentGO}
526
+ */
527
+ bounce(opts = {}) {
528
+ return this.motion('bounce', opts);
529
+ }
530
+
531
+ /**
532
+ * Add spring motion
533
+ * @param {Object} [opts] - Motion options
534
+ * @returns {FluentGO}
535
+ */
536
+ spring(opts = {}) {
537
+ return this.motion('spring', opts);
538
+ }
539
+
540
+ /**
541
+ * Add spiral motion
542
+ * @param {Object} [opts] - Motion options
543
+ * @param {number} [opts.startRadius=50] - Starting radius
544
+ * @param {number} [opts.endRadius=150] - Ending radius
545
+ * @param {number} [opts.revolutions=3] - Number of revolutions
546
+ * @param {number} [opts.duration=4] - Duration
547
+ * @returns {FluentGO}
548
+ */
549
+ spiral(opts = {}) {
550
+ return this.motion('spiral', opts);
551
+ }
552
+
553
+ /**
554
+ * Add pendulum motion
555
+ * @param {Object} [opts] - Motion options
556
+ * @param {number} [opts.amplitude=45] - Swing amplitude in degrees
557
+ * @param {number} [opts.duration=2] - Period
558
+ * @returns {FluentGO}
559
+ */
560
+ pendulum(opts = {}) {
561
+ return this.motion('pendulum', opts);
562
+ }
563
+
564
+ /**
565
+ * Add waypoint/patrol motion
566
+ * @param {Object} [opts] - Motion options
567
+ * @param {Array} [opts.waypoints] - Array of {x, y} points
568
+ * @param {number} [opts.speed=100] - Movement speed
569
+ * @param {number} [opts.waitTime=0] - Wait time at each point
570
+ * @returns {FluentGO}
571
+ */
572
+ waypoint(opts = {}) {
573
+ return this.motion('waypoint', opts);
574
+ }
575
+
576
+ /**
577
+ * Generic motion add
578
+ * @param {string} type - Motion type name
579
+ * @param {Object} [opts] - Motion options
580
+ * @returns {FluentGO}
581
+ */
582
+ motion(type, opts = {}) {
583
+ // Store motion config for processing in update loop
584
+ this.#motions.push({ type, opts });
585
+
586
+ // Initialize motion state on GO
587
+ if (!this.#go._fluentMotions) {
588
+ this.#go._fluentMotions = [];
589
+ this.#go._motionTime = 0;
590
+
591
+ // Store base position for relative motions
592
+ this.#go._baseX = this.#go.x;
593
+ this.#go._baseY = this.#go.y;
594
+
595
+ // Inject motion processing into GO update
596
+ const originalUpdate = this.#go.update?.bind(this.#go) || (() => {});
597
+ const self = this;
598
+ this.#go.update = function(dt) {
599
+ originalUpdate(dt);
600
+ this._motionTime += dt;
601
+ self.#processMotions(dt);
602
+ };
603
+ }
604
+
605
+ this.#go._fluentMotions.push({ type, opts, state: null });
606
+ return this;
607
+ }
608
+
609
+ /**
610
+ * Process all motions on this GO
611
+ * @private
612
+ */
613
+ #processMotions(dt) {
614
+ for (const motion of this.#go._fluentMotions) {
615
+ const result = this.#applyMotion(motion, dt);
616
+ motion.state = result?.state;
617
+ }
618
+ }
619
+
620
+ /**
621
+ * Apply a single motion
622
+ * @private
623
+ */
624
+ #applyMotion(motion, dt) {
625
+ const { type, opts, state } = motion;
626
+ const t = this.#go._motionTime;
627
+ const go = this.#go;
628
+
629
+ switch (type) {
630
+ case 'oscillate': {
631
+ const { prop = 'y', min = -50, max = 50, duration = 2 } = opts;
632
+ const result = Motion.oscillate(min, max, t, duration, true);
633
+
634
+ // Store base value if not set
635
+ const baseProp = `_base_${prop}`;
636
+ if (go[baseProp] === undefined) {
637
+ go[baseProp] = go[prop];
638
+ }
639
+
640
+ go[prop] = go[baseProp] + result.value;
641
+ return result;
642
+ }
643
+
644
+ case 'pulse': {
645
+ const { prop = 'scale', min = 0.8, max = 1.2, duration = 1 } = opts;
646
+ const result = Motion.pulse(min, max, t, duration, true);
647
+
648
+ if (prop === 'scale') {
649
+ go.scaleX = result.value;
650
+ go.scaleY = result.value;
651
+ } else if (prop === 'opacity' && go.renderable) {
652
+ go.renderable.opacity = result.value;
653
+ } else {
654
+ go[prop] = result.value;
655
+ }
656
+ return result;
657
+ }
658
+
659
+ case 'orbit': {
660
+ const {
661
+ centerX = go._baseX,
662
+ centerY = go._baseY,
663
+ radiusX = 100,
664
+ radiusY = 100,
665
+ duration = 3,
666
+ clockwise = true
667
+ } = opts;
668
+
669
+ // Store orbit center on first call
670
+ if (!go._orbitCenter) {
671
+ go._orbitCenter = { x: centerX, y: centerY };
672
+ }
673
+
674
+ const result = Motion.orbit(
675
+ go._orbitCenter.x, go._orbitCenter.y,
676
+ radiusX, radiusY, 0, t, duration, true, clockwise
677
+ );
678
+
679
+ go.x = result.x;
680
+ go.y = result.y;
681
+ return result;
682
+ }
683
+
684
+ case 'float': {
685
+ const { radius = 20, speed = 0.5, randomness = 0.3, duration = 5 } = opts;
686
+
687
+ // Initialize float state
688
+ if (!go._floatState) {
689
+ go._floatState = {
690
+ baseX: go._baseX,
691
+ baseY: go._baseY
692
+ };
693
+ }
694
+
695
+ const result = Motion.float(
696
+ go._floatState, t, duration, speed, randomness, radius, true
697
+ );
698
+
699
+ go.x = result.x;
700
+ go.y = result.y;
701
+ return result;
702
+ }
703
+
704
+ case 'shake': {
705
+ const {
706
+ intensity = 5,
707
+ frequency = 20,
708
+ decay = 0.9,
709
+ duration = 0.5
710
+ } = opts;
711
+
712
+ const result = Motion.shake(
713
+ go._baseX, go._baseY,
714
+ intensity, intensity, frequency, decay, t, duration, true
715
+ );
716
+
717
+ go.x = result.x;
718
+ go.y = result.y;
719
+ return result;
720
+ }
721
+
722
+ case 'bounce': {
723
+ const { height = 100, bounces = 3, duration = 2 } = opts;
724
+ const result = Motion.bounce(height, go._baseY, bounces, t, duration, true);
725
+
726
+ go.y = result.y;
727
+ return result;
728
+ }
729
+
730
+ case 'spiral': {
731
+ const {
732
+ startRadius = 50,
733
+ endRadius = 150,
734
+ revolutions = 3,
735
+ duration = 4
736
+ } = opts;
737
+
738
+ if (!go._spiralCenter) {
739
+ go._spiralCenter = { x: go._baseX, y: go._baseY };
740
+ }
741
+
742
+ const result = Motion.spiral(
743
+ go._spiralCenter.x, go._spiralCenter.y,
744
+ startRadius, endRadius, 0, revolutions, t, duration, true
745
+ );
746
+
747
+ go.x = result.x;
748
+ go.y = result.y;
749
+ return result;
750
+ }
751
+
752
+ case 'pendulum': {
753
+ const { amplitude = 45, duration = 2, damped = false } = opts;
754
+ const result = Motion.pendulum(0, amplitude, t, duration, true, damped);
755
+
756
+ go.rotation = result.value * (Math.PI / 180);
757
+ return result;
758
+ }
759
+
760
+ case 'waypoint': {
761
+ const { waypoints = [], speed = 100, waitTime = 0 } = opts;
762
+
763
+ if (waypoints.length === 0) return { state: null };
764
+
765
+ const result = Motion.waypoint(
766
+ go, t, waypoints, speed, waitTime, true, null, state
767
+ );
768
+
769
+ go.x = result.x ?? go.x;
770
+ go.y = result.y ?? go.y;
771
+ return result;
772
+ }
773
+
774
+ default:
775
+ console.warn(`Unknown motion type: ${type}`);
776
+ return { state: null };
777
+ }
778
+ }
779
+
780
+ // ─────────────────────────────────────────────────────────
781
+ // TWEEN SHORTCUTS
782
+ // ─────────────────────────────────────────────────────────
783
+
784
+ /**
785
+ * Tween properties over time
786
+ * @param {Object} props - Properties to tween { x: 100, y: 200 }
787
+ * @param {Object} [opts] - Tween options
788
+ * @param {number} [opts.duration=1] - Duration in seconds
789
+ * @param {string|Function} [opts.easing='easeOutQuad'] - Easing function
790
+ * @param {number} [opts.delay=0] - Delay before starting
791
+ * @param {Function} [opts.onComplete] - Completion callback
792
+ * @returns {FluentGO}
793
+ */
794
+ tween(props, opts = {}) {
795
+ const { duration = 1, easing = 'easeOutQuad', delay = 0, onComplete } = opts;
796
+
797
+ if (delay > 0) {
798
+ setTimeout(() => {
799
+ Tweenetik.to(this.#go, props, duration, easing, { onComplete });
800
+ }, delay * 1000);
801
+ } else {
802
+ Tweenetik.to(this.#go, props, duration, easing, { onComplete });
803
+ }
804
+
805
+ return this;
806
+ }
807
+
808
+ // ─────────────────────────────────────────────────────────
809
+ // CHILD GAMEOBJECTS
810
+ // ─────────────────────────────────────────────────────────
811
+
812
+ /**
813
+ * Create a child GameObject
814
+ *
815
+ * Supports multiple signatures:
816
+ * - child() - Create plain child GO
817
+ * - child(options) - Create plain child with options
818
+ * - child(CustomClass) - Create custom child class
819
+ * - child(CustomClass, options) - Custom class with options
820
+ * - child(options, builderFn) - Plain child with builder
821
+ * - child(CustomClass, options, builderFn) - Custom class with builder
822
+ *
823
+ * @param {Object|Function} [optsOrClass] - Child GO options or custom class
824
+ * @param {Object|Function} [optsOrBuilder] - Options or builder function
825
+ * @param {Function} [builderFn] - Optional builder callback
826
+ * @returns {FluentGO}
827
+ */
828
+ child(optsOrClass, optsOrBuilder, builderFn) {
829
+ // Parse flexible arguments
830
+ let GOClass, opts, builder;
831
+
832
+ if (typeof optsOrClass === 'function' && optsOrClass.prototype) {
833
+ // child(CustomClass) or child(CustomClass, options) or child(CustomClass, options, builder)
834
+ GOClass = optsOrClass;
835
+ if (typeof optsOrBuilder === 'function') {
836
+ opts = {};
837
+ builder = optsOrBuilder;
838
+ } else {
839
+ opts = optsOrBuilder || {};
840
+ builder = builderFn;
841
+ }
842
+ } else {
843
+ // child() or child(options) or child(options, builder)
844
+ GOClass = GameObject;
845
+ opts = optsOrClass || {};
846
+ builder = optsOrBuilder;
847
+ }
848
+
849
+ // GameObject constructor signature is (game, options)
850
+ const childGO = new GOClass(this.#go.game, opts);
851
+
852
+ this.#go.addChild(childGO);
853
+
854
+ // Register in refs if named
855
+ if (opts.name) {
856
+ this.#refs[opts.name] = childGO;
857
+ }
858
+
859
+ const fluentChild = new FluentGO(this, childGO, this.#refs, this.#state);
860
+
861
+ if (builder) {
862
+ builder(fluentChild);
863
+ return this; // Return parent GO context
864
+ }
865
+
866
+ return fluentChild;
867
+ }
868
+
869
+ // ─────────────────────────────────────────────────────────
870
+ // EVENTS
871
+ // ─────────────────────────────────────────────────────────
872
+
873
+ /**
874
+ * Register event handler on this GO
875
+ * @param {string} event - Event name
876
+ * @param {Function} handler - Handler function
877
+ * @returns {FluentGO}
878
+ */
879
+ on(event, handler) {
880
+ const ctx = {
881
+ go: this.#go,
882
+ shapes: this.#shapes,
883
+ refs: this.#refs,
884
+ state: this.#state
885
+ };
886
+
887
+ // Enable interactivity for mouse/input events
888
+ const interactiveEvents = ['click', 'mousedown', 'mouseup', 'mousemove', 'mouseover', 'mouseout', 'inputdown', 'inputup', 'inputmove'];
889
+ if (interactiveEvents.includes(event)) {
890
+ this.#go.interactive = true;
891
+ }
892
+
893
+ if (this.#go.events) {
894
+ this.#go.events.on(event, (e) => handler(ctx, e));
895
+ }
896
+
897
+ return this;
898
+ }
899
+
900
+ /**
901
+ * Custom update function for this GO
902
+ * @param {Function} fn - Update function (dt, context)
903
+ * @returns {FluentGO}
904
+ */
905
+ update(fn) {
906
+ const originalUpdate = this.#go.update?.bind(this.#go) || (() => {});
907
+ const shapes = this.#shapes;
908
+ const refs = this.#refs;
909
+ const state = this.#state;
910
+ const go = this.#go;
911
+
912
+ this.#go.update = (dt) => {
913
+ originalUpdate(dt);
914
+ fn(dt, { go, shapes, refs, state });
915
+ };
916
+
917
+ return this;
918
+ }
919
+
920
+ // ─────────────────────────────────────────────────────────
921
+ // TRANSFORM SHORTCUTS
922
+ // ─────────────────────────────────────────────────────────
923
+
924
+ /**
925
+ * Set position
926
+ * @param {number} x - X position
927
+ * @param {number} y - Y position
928
+ * @returns {FluentGO}
929
+ */
930
+ pos(x, y) {
931
+ this.#go.x = x;
932
+ this.#go.y = y;
933
+ return this;
934
+ }
935
+
936
+ /**
937
+ * Set scale
938
+ * @param {number} sx - X scale
939
+ * @param {number} [sy] - Y scale (defaults to sx)
940
+ * @returns {FluentGO}
941
+ */
942
+ scale(sx, sy) {
943
+ this.#go.scaleX = sx;
944
+ this.#go.scaleY = sy ?? sx;
945
+ return this;
946
+ }
947
+
948
+ /**
949
+ * Set rotation
950
+ * @param {number} degrees - Rotation in degrees
951
+ * @returns {FluentGO}
952
+ */
953
+ rotate(degrees) {
954
+ this.#go.rotation = degrees * (Math.PI / 180);
955
+ return this;
956
+ }
957
+
958
+ /**
959
+ * Set opacity
960
+ * @param {number} value - Opacity (0-1)
961
+ * @returns {FluentGO}
962
+ */
963
+ opacity(value) {
964
+ this.#go.opacity = value;
965
+ if (this.#go.renderable) {
966
+ this.#go.renderable.opacity = value;
967
+ }
968
+ return this;
969
+ }
970
+
971
+ /**
972
+ * Set z-index
973
+ * @param {number} value - Z-index value
974
+ * @returns {FluentGO}
975
+ */
976
+ zIndex(value) {
977
+ this.#go.zIndex = value;
978
+ return this;
979
+ }
980
+
981
+ // ─────────────────────────────────────────────────────────
982
+ // NAVIGATION
983
+ // ─────────────────────────────────────────────────────────
984
+
985
+ /**
986
+ * Navigate back to parent context
987
+ * @returns {import('./fluent-scene.js').FluentScene|FluentGO}
988
+ */
989
+ end() {
990
+ return this.#parent;
991
+ }
992
+
993
+ /**
994
+ * Create sibling GO (shortcut to scene.go)
995
+ * @param {Object} [opts] - GO options
996
+ * @returns {FluentGO}
997
+ */
998
+ go(opts) {
999
+ // Navigate up to scene and create new GO
1000
+ let parent = this.#parent;
1001
+ while (parent && !parent.sceneInstance) {
1002
+ parent = parent.end?.();
1003
+ }
1004
+ if (parent && parent.go) {
1005
+ return parent.go(opts);
1006
+ }
1007
+ throw new Error('Cannot find scene context');
1008
+ }
1009
+
1010
+ /**
1011
+ * Switch to another scene
1012
+ * @param {string} name - Scene name
1013
+ * @param {Object} [opts] - Scene options
1014
+ * @returns {import('./fluent-scene.js').FluentScene}
1015
+ */
1016
+ scene(name, opts) {
1017
+ let parent = this.#parent;
1018
+ while (parent && !parent.sceneInstance) {
1019
+ parent = parent.end?.();
1020
+ }
1021
+ if (parent && parent.scene) {
1022
+ return parent.scene(name, opts);
1023
+ }
1024
+ throw new Error('Cannot find game context');
1025
+ }
1026
+
1027
+ // ─────────────────────────────────────────────────────────
1028
+ // SHORTCUTS
1029
+ // ─────────────────────────────────────────────────────────
1030
+
1031
+ /**
1032
+ * Start the game
1033
+ * @returns {import('./fluent-game.js').FluentGame}
1034
+ */
1035
+ start() {
1036
+ let parent = this.#parent;
1037
+ while (parent && parent.end) {
1038
+ const next = parent.end();
1039
+ if (next === parent) break;
1040
+ parent = next;
1041
+ }
1042
+ return parent.start();
1043
+ }
1044
+
1045
+ // ─────────────────────────────────────────────────────────
1046
+ // ACCESSORS
1047
+ // ─────────────────────────────────────────────────────────
1048
+
1049
+ /** @returns {GameObject} Underlying GameObject instance */
1050
+ get goInstance() { return this.#go; }
1051
+
1052
+ /** @returns {Array} All shapes added to this GO */
1053
+ get shapes() { return this.#shapes; }
1054
+
1055
+ /** @returns {Object} Named object references */
1056
+ get refs() { return this.#refs; }
1057
+
1058
+ /** @returns {Object} Shared state */
1059
+ get state() { return this.#state; }
1060
+ }