@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,549 @@
1
+ import { Scene } from "..";
2
+ import { Painter } from "../../painter/painter";
3
+ import {
4
+ horizontalLayout,
5
+ verticalLayout,
6
+ tileLayout,
7
+ applyLayout,
8
+ gridLayout,
9
+ } from "../../util/layout";
10
+
11
+ // LayoutScene base class
12
+ export class LayoutScene extends Scene {
13
+ constructor(game, options = {}) {
14
+ super(game, options);
15
+ this.spacing = options.spacing ?? 10;
16
+ this.padding = options.padding ?? 0;
17
+ this.autoSize = options.autoSize ?? true;
18
+ this.align = options.align ?? "start";
19
+ this.debug = options.debug ?? false;
20
+ this._layoutDirty = true; // Initially dirty
21
+
22
+ // Scroll configuration
23
+ this.scrollable = options.scrollable ?? false;
24
+ this.scrollFriction = options.scrollFriction ?? 0.92;
25
+ this.scrollBounce = options.scrollBounce ?? 0.3;
26
+ this.scrollThreshold = options.scrollThreshold ?? 0.5;
27
+ this._viewportWidth = options.viewportWidth ?? null;
28
+ this._viewportHeight = options.viewportHeight ?? null;
29
+
30
+ // Scroll state
31
+ this._scrollOffset = { x: 0, y: 0 };
32
+ this._scrollVelocity = { x: 0, y: 0 };
33
+ this._scrollDragging = false;
34
+ this._scrollDragStart = { x: 0, y: 0 };
35
+ this._scrollDragStartOffset = { x: 0, y: 0 };
36
+ this._lastDragPosition = { x: 0, y: 0 };
37
+ this._lastDragTime = 0;
38
+
39
+ // Setup scroll interaction if enabled
40
+ if (this.scrollable) {
41
+ this._setupScrollInteraction();
42
+ }
43
+
44
+ // Track if we need to initialize scroll position on first layout
45
+ this._scrollInitialized = false;
46
+ }
47
+
48
+ // Template method to be overridden by subclasses
49
+ calculateLayout() {
50
+ // Subclasses override this to return their layout result
51
+ throw new Error("Subclasses must implement calculateLayout()");
52
+ }
53
+
54
+ update(dt) {
55
+ // Check if layout needs update
56
+ if (this._boundsDirty || this._layoutDirty) {
57
+ // Store previous dimensions to detect changes
58
+ const prevWidth = this.width;
59
+ const prevHeight = this.height;
60
+
61
+ // 1. Calculate the layout - delegated to subclasses
62
+ const layoutResult = this.calculateLayout();
63
+
64
+ // 2. Update dimensions if autoSize is enabled
65
+ if (this.autoSize && layoutResult) {
66
+ // Store full content size for scroll bounds calculation
67
+ this._contentWidth = layoutResult.width;
68
+ this._contentHeight = layoutResult.height;
69
+
70
+ const axis = this.getScrollAxis();
71
+ const viewportW = this._viewportWidth;
72
+ const viewportH = this._viewportHeight;
73
+
74
+ // Calculate visible size - cap to viewport when scrollable and content exceeds viewport
75
+ let visibleWidth = layoutResult.width;
76
+ let visibleHeight = layoutResult.height;
77
+
78
+ if (this.scrollable && viewportW !== null && axis.horizontal) {
79
+ visibleWidth = Math.min(layoutResult.width, viewportW);
80
+ }
81
+ if (this.scrollable && viewportH !== null && axis.vertical) {
82
+ visibleHeight = Math.min(layoutResult.height, viewportH);
83
+ }
84
+
85
+ // Break potential infinite loops by only changing when actually different
86
+ if (Math.abs(this.width - visibleWidth) > 0.1) {
87
+ this.width = visibleWidth;
88
+ }
89
+ if (Math.abs(this.height - visibleHeight) > 0.1) {
90
+ this.height = visibleHeight;
91
+ }
92
+ }
93
+
94
+ // 3. Apply positions to children - but only if we have positions
95
+ if (layoutResult && layoutResult.positions) {
96
+ this.applyPositionsToChildren(layoutResult.positions);
97
+ }
98
+
99
+ // 4. Initialize scroll position to show start of content (with padding)
100
+ if (this.scrollable && !this._scrollInitialized && this._needsScrolling()) {
101
+ const bounds = this._getScrollBounds();
102
+ const axis = this.getScrollAxis();
103
+ // Start at max scroll to show beginning of content
104
+ if (axis.horizontal) this._scrollOffset.x = bounds.maxX;
105
+ if (axis.vertical) this._scrollOffset.y = bounds.maxY;
106
+ this._scrollInitialized = true;
107
+ }
108
+
109
+ // 5. Clear dirty flags
110
+ this._boundsDirty = false;
111
+ this._layoutDirty = false;
112
+
113
+ // If dimensions changed and this isn't coming from a bounds update,
114
+ // we need to prevent markBoundsDirty from being called recursively
115
+ if (
116
+ (prevWidth !== this.width || prevHeight !== this.height) &&
117
+ !this._updatingBoundsFromLayout
118
+ ) {
119
+ this._updatingBoundsFromLayout = true;
120
+ // Call parent's markBoundsDirty, but NOT our override
121
+ Scene.prototype.markBoundsDirty.call(this);
122
+ this._updatingBoundsFromLayout = false;
123
+ }
124
+ }
125
+
126
+ // Handle scroll momentum when not dragging
127
+ if (this.scrollable && !this._scrollDragging) {
128
+ this._updateScrollMomentum(dt);
129
+ }
130
+
131
+ // Call parent update
132
+ super.update(dt);
133
+ }
134
+
135
+ markBoundsDirty() {
136
+ if (this._updatingBoundsFromLayout) {
137
+ // Just set the flag without propagating
138
+ this._boundsDirty = true;
139
+ return;
140
+ }
141
+
142
+ // Call parent implementation
143
+ super.markBoundsDirty();
144
+
145
+ // Set layout dirty flag
146
+ this._layoutDirty = true;
147
+ }
148
+
149
+ // Shared method to apply positions using utility function
150
+ applyPositionsToChildren(positions) {
151
+ // Each subclass will override just the positioning options
152
+ applyLayout(this.children, positions, this.getLayoutOffset());
153
+ }
154
+
155
+ // Subclasses override this to return their specific offset needs
156
+ getLayoutOffset() {
157
+ return { offsetX: 0, offsetY: 0 };
158
+ }
159
+
160
+ // Override to mark layout dirty when children change
161
+ add(go) {
162
+ const result = super.add(go);
163
+ this._layoutDirty = true;
164
+ return result;
165
+ }
166
+
167
+ remove(go) {
168
+ const result = super.remove(go);
169
+ this._layoutDirty = true;
170
+ return result;
171
+ }
172
+
173
+ // Scroll axis - subclasses override for different directions
174
+ getScrollAxis() {
175
+ return { horizontal: false, vertical: true }; // Default: vertical scroll
176
+ }
177
+
178
+ // Override bounds for scrollable layouts to use viewport for hit testing
179
+ calculateBounds() {
180
+ if (this.scrollable && (this._viewportWidth || this._viewportHeight)) {
181
+ const w = this._viewportWidth ?? this.width;
182
+ const h = this._viewportHeight ?? this.height;
183
+ return {
184
+ x: -w / 2,
185
+ y: -h / 2,
186
+ width: w,
187
+ height: h,
188
+ };
189
+ }
190
+ return super.calculateBounds();
191
+ }
192
+
193
+ // Get scroll bounds based on content size vs viewport
194
+ _getScrollBounds() {
195
+ // Use content size (full layout size before capping to viewport)
196
+ const contentW = this._contentWidth ?? this.width;
197
+ const contentH = this._contentHeight ?? this.height;
198
+ const viewportW = this._viewportWidth ?? contentW;
199
+ const viewportH = this._viewportHeight ?? contentH;
200
+
201
+ // Content is centered, so scroll range is symmetric around 0
202
+ // This allows scrolling to see both the start (with padding) and end of content
203
+ const scrollRangeX = Math.max(0, contentW - viewportW);
204
+ const scrollRangeY = Math.max(0, contentH - viewportH);
205
+
206
+ return {
207
+ minX: -scrollRangeX / 2,
208
+ maxX: scrollRangeX / 2,
209
+ minY: -scrollRangeY / 2,
210
+ maxY: scrollRangeY / 2,
211
+ };
212
+ }
213
+
214
+ // Clamp scroll position to bounds with elastic bounce-back
215
+ _clampScrollBounds() {
216
+ const bounds = this._getScrollBounds();
217
+ const axis = this.getScrollAxis();
218
+
219
+ if (axis.horizontal) {
220
+ if (this._scrollOffset.x < bounds.minX) {
221
+ this._scrollOffset.x +=
222
+ (bounds.minX - this._scrollOffset.x) * this.scrollBounce;
223
+ this._scrollVelocity.x = 0;
224
+ } else if (this._scrollOffset.x > bounds.maxX) {
225
+ this._scrollOffset.x +=
226
+ (bounds.maxX - this._scrollOffset.x) * this.scrollBounce;
227
+ this._scrollVelocity.x = 0;
228
+ }
229
+ }
230
+
231
+ if (axis.vertical) {
232
+ if (this._scrollOffset.y < bounds.minY) {
233
+ this._scrollOffset.y +=
234
+ (bounds.minY - this._scrollOffset.y) * this.scrollBounce;
235
+ this._scrollVelocity.y = 0;
236
+ } else if (this._scrollOffset.y > bounds.maxY) {
237
+ this._scrollOffset.y +=
238
+ (bounds.maxY - this._scrollOffset.y) * this.scrollBounce;
239
+ this._scrollVelocity.y = 0;
240
+ }
241
+ }
242
+ }
243
+
244
+ // Update scroll momentum physics
245
+ _updateScrollMomentum(dt) {
246
+ const axis = this.getScrollAxis();
247
+
248
+ if (axis.horizontal) {
249
+ this._scrollVelocity.x *= this.scrollFriction;
250
+ if (Math.abs(this._scrollVelocity.x) < this.scrollThreshold) {
251
+ this._scrollVelocity.x = 0;
252
+ }
253
+ this._scrollOffset.x += this._scrollVelocity.x * dt * 60;
254
+ }
255
+
256
+ if (axis.vertical) {
257
+ this._scrollVelocity.y *= this.scrollFriction;
258
+ if (Math.abs(this._scrollVelocity.y) < this.scrollThreshold) {
259
+ this._scrollVelocity.y = 0;
260
+ }
261
+ this._scrollOffset.y += this._scrollVelocity.y * dt * 60;
262
+ }
263
+
264
+ this._clampScrollBounds();
265
+ }
266
+
267
+ // Setup scroll interaction handlers
268
+ _setupScrollInteraction() {
269
+ this.interactive = true;
270
+
271
+ this._scrollInputDownHandler = (e) => {
272
+ // Manually check if the click is within our viewport bounds
273
+ // since Pipeline dispatches to children first, not the Scene itself
274
+ if (this._isPointInViewport(e.x, e.y)) {
275
+ this._onScrollDragStart(e);
276
+ }
277
+ };
278
+ this._scrollInputMoveHandler = (e) => this._onScrollDragMove(e);
279
+ this._scrollInputUpHandler = (e) => this._onScrollDragEnd(e);
280
+
281
+ // Listen on game events instead of this.on() because Pipeline
282
+ // doesn't dispatch inputdown to Scene objects themselves
283
+ this.game.events.on("inputdown", this._scrollInputDownHandler);
284
+ this.game.events.on("inputmove", this._scrollInputMoveHandler);
285
+ this.game.events.on("inputup", this._scrollInputUpHandler);
286
+ }
287
+
288
+ // Check if a point is within the layout's viewport bounds
289
+ _isPointInViewport(screenX, screenY) {
290
+ // Transform screen coordinates to layout's local space
291
+ let localX = screenX - this.x;
292
+ let localY = screenY - this.y;
293
+
294
+ // Account for parent transforms if we have a parent
295
+ let current = this.parent;
296
+ while (current) {
297
+ localX -= current.x || 0;
298
+ localY -= current.y || 0;
299
+ current = current.parent;
300
+ }
301
+
302
+ // Check against viewport bounds (centered at origin)
303
+ const viewportW = this._viewportWidth ?? this.width;
304
+ const viewportH = this._viewportHeight ?? this.height;
305
+ const halfW = viewportW / 2;
306
+ const halfH = viewportH / 2;
307
+
308
+ return localX >= -halfW && localX <= halfW && localY >= -halfH && localY <= halfH;
309
+ }
310
+
311
+ _onScrollDragStart(e) {
312
+ this._scrollDragging = true;
313
+ this._scrollDragStart = { x: e.x, y: e.y };
314
+ this._scrollDragStartOffset = { ...this._scrollOffset };
315
+ this._lastDragPosition = { x: e.x, y: e.y };
316
+ this._lastDragTime = performance.now();
317
+ this._scrollVelocity = { x: 0, y: 0 };
318
+ }
319
+
320
+ _onScrollDragMove(e) {
321
+ if (!this._scrollDragging) return;
322
+
323
+ const axis = this.getScrollAxis();
324
+ const now = performance.now();
325
+ const dt = Math.max(1, now - this._lastDragTime) / 1000;
326
+
327
+ if (axis.horizontal) {
328
+ const deltaX = e.x - this._scrollDragStart.x;
329
+ this._scrollOffset.x = this._scrollDragStartOffset.x + deltaX;
330
+ this._scrollVelocity.x = (e.x - this._lastDragPosition.x) / (dt * 60);
331
+ }
332
+
333
+ if (axis.vertical) {
334
+ const deltaY = e.y - this._scrollDragStart.y;
335
+ this._scrollOffset.y = this._scrollDragStartOffset.y + deltaY;
336
+ this._scrollVelocity.y = (e.y - this._lastDragPosition.y) / (dt * 60);
337
+ }
338
+
339
+ this._lastDragPosition = { x: e.x, y: e.y };
340
+ this._lastDragTime = now;
341
+ }
342
+
343
+ _onScrollDragEnd() {
344
+ this._scrollDragging = false;
345
+ }
346
+
347
+ // Check if content exceeds viewport (scrolling needed)
348
+ _needsScrolling() {
349
+ if (!this.scrollable) return false;
350
+
351
+ // Use content size (full layout size before capping to viewport)
352
+ const contentW = this._contentWidth ?? this.width;
353
+ const contentH = this._contentHeight ?? this.height;
354
+ const viewportW = this._viewportWidth ?? contentW;
355
+ const viewportH = this._viewportHeight ?? contentH;
356
+ const axis = this.getScrollAxis();
357
+
358
+ // Only enable scrolling if content exceeds viewport in the scroll direction
359
+ if (axis.horizontal && contentW > viewportW) return true;
360
+ if (axis.vertical && contentH > viewportH) return true;
361
+
362
+ return false;
363
+ }
364
+
365
+ // Draw with clipping for scrollable layouts
366
+ draw() {
367
+ if (this._needsScrolling()) {
368
+ this._drawScrollable();
369
+ } else {
370
+ super.draw();
371
+ }
372
+ }
373
+
374
+ _drawScrollable() {
375
+ // Apply transforms (rotation, scale, etc.) from Transformable
376
+ this.applyTransforms();
377
+
378
+ // Draw debug bounds BEFORE clipping (shows actual content size)
379
+ this.drawDebug();
380
+
381
+ // Get viewport dimensions with padding inset
382
+ const padding = this.padding ?? 0;
383
+ const viewportW = (this._viewportWidth ?? this.width) - padding * 2;
384
+ const viewportH = (this._viewportHeight ?? this.height) - padding * 2;
385
+
386
+ // Save state, then clip to viewport and apply scroll offset
387
+ Painter.save();
388
+
389
+ // Clip to viewport with padding inset (centered at origin)
390
+ Painter.ctx.beginPath();
391
+ Painter.ctx.rect(-viewportW / 2, -viewportH / 2, viewportW, viewportH);
392
+ Painter.ctx.clip();
393
+ // Clear the path after clipping to prevent shapes from filling the clip rect
394
+ Painter.ctx.beginPath();
395
+
396
+ // Apply scroll offset
397
+ Painter.ctx.translate(this._scrollOffset.x, this._scrollOffset.y);
398
+
399
+ // Render children within clipped/scrolled region
400
+ this._collection
401
+ .getSortedChildren()
402
+ .filter((obj) => obj.visible)
403
+ .forEach((obj) => {
404
+ Painter.save();
405
+ obj.render();
406
+ Painter.restore();
407
+ });
408
+
409
+ Painter.restore();
410
+ }
411
+
412
+ // Programmatic scroll API
413
+ scrollTo(x, y) {
414
+ const axis = this.getScrollAxis();
415
+ if (axis.horizontal) this._scrollOffset.x = x;
416
+ if (axis.vertical) this._scrollOffset.y = y;
417
+ this._scrollVelocity = { x: 0, y: 0 };
418
+ }
419
+
420
+ scrollBy(deltaX, deltaY) {
421
+ const axis = this.getScrollAxis();
422
+ if (axis.horizontal) this._scrollOffset.x += deltaX;
423
+ if (axis.vertical) this._scrollOffset.y += deltaY;
424
+ }
425
+
426
+ getScrollPosition() {
427
+ return { ...this._scrollOffset };
428
+ }
429
+
430
+ resetScroll() {
431
+ this._scrollOffset = { x: 0, y: 0 };
432
+ this._scrollVelocity = { x: 0, y: 0 };
433
+ }
434
+ }
435
+
436
+ // HorizontalLayout with clean implementation
437
+ export class HorizontalLayout extends LayoutScene {
438
+ // Override scroll axis for horizontal scrolling
439
+ getScrollAxis() {
440
+ return { horizontal: true, vertical: false };
441
+ }
442
+
443
+ // Override only the layout-specific methods
444
+ calculateLayout() {
445
+ return horizontalLayout(this.children, {
446
+ spacing: this.spacing,
447
+ padding: this.padding,
448
+ align: this.align,
449
+ centerItems: true,
450
+ });
451
+ }
452
+
453
+ getLayoutOffset() {
454
+ // Use content width (full size) not visible width (capped to viewport)
455
+ const w = this._contentWidth ?? this.width;
456
+ return {
457
+ offsetX: -w / 2,
458
+ offsetY: 0,
459
+ };
460
+ }
461
+ }
462
+
463
+ // VerticalLayout with clean implementation
464
+ export class VerticalLayout extends LayoutScene {
465
+ // Override only the layout-specific methods
466
+ calculateLayout() {
467
+ return verticalLayout(this.children, {
468
+ spacing: this.spacing,
469
+ padding: this.padding,
470
+ align: this.align,
471
+ centerItems: true,
472
+ });
473
+ }
474
+
475
+ getLayoutOffset() {
476
+ // Use content height (full size) not visible height (capped to viewport)
477
+ const h = this._contentHeight ?? this.height;
478
+ return {
479
+ offsetX: 0,
480
+ offsetY: -h / 2,
481
+ };
482
+ }
483
+ }
484
+
485
+ // TileLayout with clean implementation
486
+ export class TileLayout extends LayoutScene {
487
+ constructor(game, options = {}) {
488
+ super(game, options);
489
+ this.columns = options.columns ?? 4;
490
+ }
491
+
492
+ calculateLayout() {
493
+ if (!this.children.length) {
494
+ return null;
495
+ }
496
+
497
+ return tileLayout(this.children, {
498
+ columns: this.columns,
499
+ spacing: this.spacing,
500
+ padding: this.padding,
501
+ centerItems: true,
502
+ });
503
+ }
504
+
505
+ getLayoutOffset() {
506
+ // Use content size (full size) not visible size (capped to viewport)
507
+ const w = this._contentWidth ?? this.width;
508
+ const h = this._contentHeight ?? this.height;
509
+ return {
510
+ offsetX: -w / 2,
511
+ offsetY: -h / 2,
512
+ };
513
+ }
514
+ }
515
+
516
+ export class GridLayout extends LayoutScene {
517
+ constructor(game, options = {}) {
518
+ super(game, options);
519
+ this.columns = options.columns ?? 4;
520
+ this.debug = options.debug ?? false;
521
+ }
522
+
523
+ calculateLayout() {
524
+ //console.log("calculateLayout", this.columns, this.children.length);
525
+ if (!this.children.length) {
526
+ return null;
527
+ }
528
+ //console.log("calculateLayout", this.width, this.height, this.autoSize);
529
+ return gridLayout(this.children, {
530
+ columns : this.columns,
531
+ spacing : this.spacing,
532
+ padding : this.padding,
533
+ centerItems: this.centerItems,
534
+ /* only pass these two when autoSize is *off* */
535
+ width : this.autoSize ? undefined : this.width,
536
+ height: this.autoSize ? undefined : this.height,
537
+ });
538
+ }
539
+
540
+ getLayoutOffset() {
541
+ // Use content size (full size) not visible size (capped to viewport)
542
+ const w = this._contentWidth ?? this.width;
543
+ const h = this._contentHeight ?? this.height;
544
+ return {
545
+ offsetX: -w / 2,
546
+ offsetY: -h / 2,
547
+ };
548
+ }
549
+ }