@guinetik/gcanvas 1.0.4 → 2.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 (261) hide show
  1. package/dist/CNAME +1 -0
  2. package/dist/aizawa.html +27 -0
  3. package/dist/animations.html +31 -0
  4. package/dist/basic.html +38 -0
  5. package/dist/baskara.html +31 -0
  6. package/dist/bezier.html +35 -0
  7. package/dist/beziersignature.html +29 -0
  8. package/dist/blackhole.html +28 -0
  9. package/dist/blob.html +35 -0
  10. package/dist/clifford.html +25 -0
  11. package/dist/cmb.html +24 -0
  12. package/dist/coordinates.html +698 -0
  13. package/dist/cube3d.html +23 -0
  14. package/dist/dadras.html +26 -0
  15. package/dist/dejong.html +25 -0
  16. package/dist/demos.css +303 -0
  17. package/dist/dino.html +42 -0
  18. package/dist/easing.html +28 -0
  19. package/dist/events.html +195 -0
  20. package/dist/fluent.html +647 -0
  21. package/dist/fluid-simple.html +22 -0
  22. package/dist/fluid.html +37 -0
  23. package/dist/fractals.html +36 -0
  24. package/dist/gameobjects.html +626 -0
  25. package/dist/gcanvas.es.js +14368 -9093
  26. package/dist/gcanvas.es.min.js +1 -1
  27. package/dist/gcanvas.umd.js +1 -1
  28. package/dist/gcanvas.umd.min.js +1 -1
  29. package/dist/genart.html +26 -0
  30. package/dist/gendream.html +26 -0
  31. package/dist/group.html +36 -0
  32. package/dist/halvorsen.html +27 -0
  33. package/dist/home.html +587 -0
  34. package/dist/hyperbolic001.html +23 -0
  35. package/dist/hyperbolic002.html +23 -0
  36. package/dist/hyperbolic003.html +23 -0
  37. package/dist/hyperbolic004.html +23 -0
  38. package/dist/hyperbolic005.html +22 -0
  39. package/dist/index.html +446 -0
  40. package/dist/isometric.html +34 -0
  41. package/dist/js/aizawa.js +425 -0
  42. package/dist/js/animations.js +452 -0
  43. package/dist/js/basic.js +204 -0
  44. package/dist/js/baskara.js +751 -0
  45. package/dist/js/bezier.js +692 -0
  46. package/dist/js/beziersignature.js +241 -0
  47. package/dist/js/blackhole/accretiondisk.obj.js +379 -0
  48. package/dist/js/blackhole/blackhole.obj.js +318 -0
  49. package/dist/js/blackhole/index.js +409 -0
  50. package/dist/js/blackhole/particle.js +56 -0
  51. package/dist/js/blackhole/starfield.obj.js +218 -0
  52. package/dist/js/blob.js +2276 -0
  53. package/dist/js/clifford.js +236 -0
  54. package/dist/js/cmb.js +594 -0
  55. package/dist/js/coordinates.js +840 -0
  56. package/dist/js/cube3d.js +789 -0
  57. package/dist/js/dadras.js +405 -0
  58. package/dist/js/dejong.js +257 -0
  59. package/dist/js/dino.js +1420 -0
  60. package/dist/js/easing.js +477 -0
  61. package/dist/js/fluent.js +183 -0
  62. package/dist/js/fluid-simple.js +253 -0
  63. package/dist/js/fluid.js +527 -0
  64. package/dist/js/fractals.js +932 -0
  65. package/dist/js/fractalworker.js +93 -0
  66. package/dist/js/gameobjects.js +176 -0
  67. package/dist/js/genart.js +268 -0
  68. package/dist/js/gendream.js +209 -0
  69. package/dist/js/group.js +140 -0
  70. package/dist/js/halvorsen.js +405 -0
  71. package/dist/js/hyperbolic001.js +310 -0
  72. package/dist/js/hyperbolic002.js +388 -0
  73. package/dist/js/hyperbolic003.js +319 -0
  74. package/dist/js/hyperbolic004.js +345 -0
  75. package/dist/js/hyperbolic005.js +340 -0
  76. package/dist/js/info-toggle.js +25 -0
  77. package/dist/js/isometric.js +851 -0
  78. package/dist/js/kerr.js +1547 -0
  79. package/dist/js/lavalamp.js +590 -0
  80. package/dist/js/layout.js +354 -0
  81. package/dist/js/lorenz.js +425 -0
  82. package/dist/js/mondrian.js +285 -0
  83. package/dist/js/opacity.js +275 -0
  84. package/dist/js/painter.js +484 -0
  85. package/dist/js/particles-showcase.js +514 -0
  86. package/dist/js/particles.js +299 -0
  87. package/dist/js/patterns.js +397 -0
  88. package/dist/js/penrose/artifact.js +69 -0
  89. package/dist/js/penrose/blackhole.js +121 -0
  90. package/dist/js/penrose/constants.js +73 -0
  91. package/dist/js/penrose/game.js +943 -0
  92. package/dist/js/penrose/lore.js +278 -0
  93. package/dist/js/penrose/penrosescene.js +892 -0
  94. package/dist/js/penrose/ship.js +216 -0
  95. package/dist/js/penrose/sounds.js +211 -0
  96. package/dist/js/penrose/voidparticle.js +55 -0
  97. package/dist/js/penrose/voidscene.js +258 -0
  98. package/dist/js/penrose/voidship.js +144 -0
  99. package/dist/js/penrose/wormhole.js +46 -0
  100. package/dist/js/pipeline.js +555 -0
  101. package/dist/js/plane3d.js +256 -0
  102. package/dist/js/platformer.js +1579 -0
  103. package/dist/js/rossler.js +480 -0
  104. package/dist/js/scene.js +304 -0
  105. package/dist/js/scenes.js +320 -0
  106. package/dist/js/schrodinger.js +706 -0
  107. package/dist/js/schwarzschild.js +1015 -0
  108. package/dist/js/shapes.js +628 -0
  109. package/dist/js/space/alien.js +171 -0
  110. package/dist/js/space/boom.js +98 -0
  111. package/dist/js/space/boss.js +353 -0
  112. package/dist/js/space/buff.js +73 -0
  113. package/dist/js/space/bullet.js +102 -0
  114. package/dist/js/space/constants.js +85 -0
  115. package/dist/js/space/game.js +1884 -0
  116. package/dist/js/space/hud.js +112 -0
  117. package/dist/js/space/laserbeam.js +179 -0
  118. package/dist/js/space/lightning.js +277 -0
  119. package/dist/js/space/minion.js +192 -0
  120. package/dist/js/space/missile.js +212 -0
  121. package/dist/js/space/player.js +430 -0
  122. package/dist/js/space/powerup.js +90 -0
  123. package/dist/js/space/starfield.js +58 -0
  124. package/dist/js/space/starpower.js +90 -0
  125. package/dist/js/spacetime.js +559 -0
  126. package/dist/js/sphere3d.js +229 -0
  127. package/dist/js/sprite.js +473 -0
  128. package/dist/js/starfaux/config.js +118 -0
  129. package/dist/js/starfaux/enemy.js +353 -0
  130. package/dist/js/starfaux/hud.js +78 -0
  131. package/dist/js/starfaux/index.js +482 -0
  132. package/dist/js/starfaux/laser.js +182 -0
  133. package/dist/js/starfaux/player.js +468 -0
  134. package/dist/js/starfaux/terrain.js +560 -0
  135. package/dist/js/study001.js +275 -0
  136. package/dist/js/study002.js +366 -0
  137. package/dist/js/study003.js +331 -0
  138. package/dist/js/study004.js +389 -0
  139. package/dist/js/study005.js +209 -0
  140. package/dist/js/study006.js +194 -0
  141. package/dist/js/study007.js +192 -0
  142. package/dist/js/study008.js +413 -0
  143. package/dist/js/svgtween.js +204 -0
  144. package/dist/js/tde/accretiondisk.js +471 -0
  145. package/dist/js/tde/blackhole.js +219 -0
  146. package/dist/js/tde/blackholescene.js +209 -0
  147. package/dist/js/tde/config.js +59 -0
  148. package/dist/js/tde/index.js +820 -0
  149. package/dist/js/tde/jets.js +290 -0
  150. package/dist/js/tde/lensedstarfield.js +154 -0
  151. package/dist/js/tde/tdestar.js +297 -0
  152. package/dist/js/tde/tidalstream.js +372 -0
  153. package/dist/js/tde_old/blackhole.obj.js +354 -0
  154. package/dist/js/tde_old/debris.obj.js +791 -0
  155. package/dist/js/tde_old/flare.obj.js +239 -0
  156. package/dist/js/tde_old/index.js +448 -0
  157. package/dist/js/tde_old/star.obj.js +812 -0
  158. package/dist/js/tetris/config.js +157 -0
  159. package/dist/js/tetris/grid.js +286 -0
  160. package/dist/js/tetris/index.js +1195 -0
  161. package/dist/js/tetris/renderer.js +634 -0
  162. package/dist/js/tetris/tetrominos.js +280 -0
  163. package/dist/js/thomas.js +394 -0
  164. package/dist/js/tiles.js +312 -0
  165. package/dist/js/tweendemo.js +79 -0
  166. package/dist/js/visibility.js +102 -0
  167. package/dist/kerr.html +28 -0
  168. package/dist/lavalamp.html +27 -0
  169. package/dist/layouts.html +37 -0
  170. package/dist/logo.svg +4 -0
  171. package/dist/loop.html +84 -0
  172. package/dist/lorenz.html +27 -0
  173. package/dist/mondrian.html +32 -0
  174. package/dist/og_image.png +0 -0
  175. package/dist/opacity.html +36 -0
  176. package/dist/painter.html +39 -0
  177. package/dist/particles-showcase.html +28 -0
  178. package/dist/particles.html +24 -0
  179. package/dist/patterns.html +33 -0
  180. package/dist/penrose-game.html +31 -0
  181. package/dist/pipeline.html +737 -0
  182. package/dist/plane3d.html +24 -0
  183. package/dist/platformer.html +43 -0
  184. package/dist/rossler.html +27 -0
  185. package/dist/scene-interactivity-test.html +220 -0
  186. package/dist/scene.html +33 -0
  187. package/dist/scenes.html +96 -0
  188. package/dist/schrodinger.html +27 -0
  189. package/dist/schwarzschild.html +27 -0
  190. package/dist/shapes.html +16 -0
  191. package/dist/space.html +85 -0
  192. package/dist/spacetime.html +27 -0
  193. package/dist/sphere3d.html +24 -0
  194. package/dist/sprite.html +18 -0
  195. package/dist/starfaux.html +22 -0
  196. package/dist/study001.html +23 -0
  197. package/dist/study002.html +23 -0
  198. package/dist/study003.html +23 -0
  199. package/dist/study004.html +23 -0
  200. package/dist/study005.html +22 -0
  201. package/dist/study006.html +24 -0
  202. package/dist/study007.html +24 -0
  203. package/dist/study008.html +22 -0
  204. package/dist/svgtween.html +29 -0
  205. package/dist/tde.html +28 -0
  206. package/dist/tetris3d.html +25 -0
  207. package/dist/thomas.html +27 -0
  208. package/dist/tiles.html +28 -0
  209. package/dist/transforms.html +400 -0
  210. package/dist/tween.html +45 -0
  211. package/dist/visibility.html +33 -0
  212. package/package.json +1 -1
  213. package/readme.md +30 -22
  214. package/src/game/objects/go.js +7 -0
  215. package/src/game/objects/index.js +2 -0
  216. package/src/game/objects/isometric-scene.js +53 -3
  217. package/src/game/objects/layoutscene.js +57 -0
  218. package/src/game/objects/mask.js +241 -0
  219. package/src/game/objects/scene.js +19 -0
  220. package/src/game/objects/wrapper.js +14 -2
  221. package/src/game/pipeline.js +17 -0
  222. package/src/game/ui/button.js +101 -16
  223. package/src/game/ui/theme.js +0 -6
  224. package/src/game/ui/togglebutton.js +25 -14
  225. package/src/game/ui/tooltip.js +12 -4
  226. package/src/index.js +3 -0
  227. package/src/io/gesture.js +409 -0
  228. package/src/io/index.js +4 -1
  229. package/src/io/keys.js +9 -1
  230. package/src/io/screen.js +476 -0
  231. package/src/math/attractors.js +664 -0
  232. package/src/math/heat.js +106 -0
  233. package/src/math/index.js +1 -0
  234. package/src/mixins/draggable.js +15 -19
  235. package/src/painter/painter.shapes.js +11 -5
  236. package/src/particle/particle-system.js +165 -1
  237. package/src/physics/index.js +26 -0
  238. package/src/physics/physics-updaters.js +333 -0
  239. package/src/physics/physics.js +375 -0
  240. package/src/shapes/image.js +5 -5
  241. package/src/shapes/index.js +2 -0
  242. package/src/shapes/parallelogram.js +147 -0
  243. package/src/shapes/righttriangle.js +115 -0
  244. package/src/shapes/svg.js +281 -100
  245. package/src/shapes/text.js +22 -6
  246. package/src/shapes/transformable.js +5 -0
  247. package/src/sound/effects.js +807 -0
  248. package/src/sound/index.js +13 -0
  249. package/src/webgl/index.js +7 -0
  250. package/src/webgl/shaders/clifford-point-shaders.js +131 -0
  251. package/src/webgl/shaders/dejong-point-shaders.js +131 -0
  252. package/src/webgl/shaders/point-sprite-shaders.js +152 -0
  253. package/src/webgl/webgl-clifford-renderer.js +477 -0
  254. package/src/webgl/webgl-dejong-renderer.js +472 -0
  255. package/src/webgl/webgl-line-renderer.js +391 -0
  256. package/src/webgl/webgl-particle-renderer.js +410 -0
  257. package/types/index.d.ts +30 -2
  258. package/types/io.d.ts +217 -0
  259. package/types/physics.d.ts +299 -0
  260. package/types/shapes.d.ts +8 -0
  261. package/types/webgl.d.ts +188 -109
@@ -431,6 +431,63 @@ export class LayoutScene extends Scene {
431
431
  this._scrollOffset = { x: 0, y: 0 };
432
432
  this._scrollVelocity = { x: 0, y: 0 };
433
433
  }
434
+
435
+ /**
436
+ * Returns scroll offset for hit testing coordinate transformation.
437
+ * @returns {{x: number, y: number}} Scroll offset to apply
438
+ */
439
+ getHitTestOffset() {
440
+ if (!this.scrollable) {
441
+ return { x: 0, y: 0 };
442
+ }
443
+ return {
444
+ x: this._scrollOffset?.x || 0,
445
+ y: this._scrollOffset?.y || 0,
446
+ };
447
+ }
448
+
449
+ /**
450
+ * Checks if a child is within the visible viewport and should be hittable.
451
+ * @param {GameObject} child - The child to check
452
+ * @returns {boolean} True if child is within viewport
453
+ */
454
+ isChildHittable(child) {
455
+ // If not scrollable or doesn't need scrolling, all children are hittable
456
+ if (!this.scrollable || !this._needsScrolling()) {
457
+ return true;
458
+ }
459
+
460
+ const axis = this.getScrollAxis();
461
+ const vpW = this._viewportWidth ?? this.width;
462
+ const vpH = this._viewportHeight ?? this.height;
463
+ const scrollX = this._scrollOffset?.x || 0;
464
+ const scrollY = this._scrollOffset?.y || 0;
465
+
466
+ // Child position with scroll applied (relative to viewport center)
467
+ const childScrolledX = child.x + scrollX;
468
+ const childScrolledY = child.y + scrollY;
469
+
470
+ // Get child dimensions (use half for centered bounds check)
471
+ const childHalfW = (child.width || 0) / 2;
472
+ const childHalfH = (child.height || 0) / 2;
473
+
474
+ // Check if child overlaps with viewport
475
+ if (axis.horizontal) {
476
+ // Child right edge must be past viewport left, child left edge must be before viewport right
477
+ if (childScrolledX + childHalfW < -vpW / 2 || childScrolledX - childHalfW > vpW / 2) {
478
+ return false;
479
+ }
480
+ }
481
+
482
+ if (axis.vertical) {
483
+ // Child bottom edge must be past viewport top, child top edge must be before viewport bottom
484
+ if (childScrolledY + childHalfH < -vpH / 2 || childScrolledY - childHalfH > vpH / 2) {
485
+ return false;
486
+ }
487
+ }
488
+
489
+ return true;
490
+ }
434
491
  }
435
492
 
436
493
  // HorizontalLayout with clean implementation
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Mask - Clipping mask for scenes and game objects
3
+ *
4
+ * Supports multiple shapes with animatable properties:
5
+ * - circle: radius
6
+ * - rectangle: width, height, cornerRadius
7
+ * - ellipse: radiusX, radiusY
8
+ * - path: custom path function
9
+ *
10
+ * @example
11
+ * // Create a circular mask that grows
12
+ * const mask = new Mask({
13
+ * shape: 'circle',
14
+ * x: game.width / 2,
15
+ * y: game.height / 2,
16
+ * radius: 0,
17
+ * });
18
+ *
19
+ * // Animate the radius
20
+ * mask.radius = 100;
21
+ *
22
+ * // Apply to context
23
+ * mask.apply(ctx);
24
+ * // ... render masked content ...
25
+ * mask.remove(ctx);
26
+ */
27
+ export class Mask {
28
+ constructor(options = {}) {
29
+ // Shape type
30
+ this.shape = options.shape || 'circle';
31
+
32
+ // Position (center point)
33
+ this.x = options.x ?? 0;
34
+ this.y = options.y ?? 0;
35
+
36
+ // Scale (multiplies dimensions)
37
+ this.scaleX = options.scaleX ?? 1;
38
+ this.scaleY = options.scaleY ?? 1;
39
+
40
+ // Circle properties
41
+ this.radius = options.radius ?? 100;
42
+
43
+ // Rectangle properties
44
+ this.width = options.width ?? 200;
45
+ this.height = options.height ?? 200;
46
+ this.cornerRadius = options.cornerRadius ?? 0;
47
+
48
+ // Ellipse properties
49
+ this.radiusX = options.radiusX ?? 100;
50
+ this.radiusY = options.radiusY ?? 100;
51
+
52
+ // Custom path function: (ctx, mask) => void
53
+ this.pathFn = options.pathFn ?? null;
54
+
55
+ // Invert the mask (show outside, hide inside)
56
+ this.invert = options.invert ?? false;
57
+
58
+ // Track if currently applied (for safety)
59
+ this._applied = false;
60
+ }
61
+
62
+ /**
63
+ * Apply the mask to a canvas context
64
+ * @param {CanvasRenderingContext2D} ctx - The canvas context
65
+ */
66
+ apply(ctx) {
67
+ if (this._applied) {
68
+ console.warn('Mask already applied. Call remove() first.');
69
+ return;
70
+ }
71
+
72
+ ctx.save();
73
+ ctx.beginPath();
74
+
75
+ if (this.invert) {
76
+ // For inverted mask, draw full canvas then cut out the shape
77
+ // This requires knowing the canvas size, so we use a large rect
78
+ ctx.rect(-10000, -10000, 20000, 20000);
79
+ this._drawPath(ctx);
80
+ // evenodd fill rule creates the "hole"
81
+ ctx.clip('evenodd');
82
+ } else {
83
+ this._drawPath(ctx);
84
+ ctx.clip();
85
+ }
86
+
87
+ this._applied = true;
88
+ }
89
+
90
+ /**
91
+ * Remove the mask (restore context)
92
+ * @param {CanvasRenderingContext2D} ctx - The canvas context
93
+ */
94
+ remove(ctx) {
95
+ if (!this._applied) {
96
+ return;
97
+ }
98
+ ctx.restore();
99
+ this._applied = false;
100
+ }
101
+
102
+ /**
103
+ * Draw the mask path (internal)
104
+ * @param {CanvasRenderingContext2D} ctx
105
+ */
106
+ _drawPath(ctx) {
107
+ const sx = this.scaleX;
108
+ const sy = this.scaleY;
109
+
110
+ switch (this.shape) {
111
+ case 'circle':
112
+ // Use average scale for circle
113
+ const scale = (sx + sy) / 2;
114
+ ctx.arc(this.x, this.y, this.radius * scale, 0, Math.PI * 2);
115
+ break;
116
+
117
+ case 'rectangle':
118
+ const w = this.width * sx;
119
+ const h = this.height * sy;
120
+ const cr = this.cornerRadius * Math.min(sx, sy);
121
+
122
+ if (cr > 0) {
123
+ // Rounded rectangle
124
+ this._roundedRect(ctx, this.x - w/2, this.y - h/2, w, h, cr);
125
+ } else {
126
+ // Simple rectangle
127
+ ctx.rect(this.x - w/2, this.y - h/2, w, h);
128
+ }
129
+ break;
130
+
131
+ case 'ellipse':
132
+ ctx.ellipse(this.x, this.y, this.radiusX * sx, this.radiusY * sy, 0, 0, Math.PI * 2);
133
+ break;
134
+
135
+ case 'path':
136
+ if (this.pathFn) {
137
+ this.pathFn(ctx, this);
138
+ }
139
+ break;
140
+
141
+ default:
142
+ console.warn(`Unknown mask shape: ${this.shape}`);
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Draw a rounded rectangle path
148
+ * @param {CanvasRenderingContext2D} ctx
149
+ * @param {number} x
150
+ * @param {number} y
151
+ * @param {number} w
152
+ * @param {number} h
153
+ * @param {number} r - Corner radius
154
+ */
155
+ _roundedRect(ctx, x, y, w, h, r) {
156
+ // Clamp corner radius to half of smallest dimension
157
+ r = Math.min(r, w / 2, h / 2);
158
+
159
+ ctx.moveTo(x + r, y);
160
+ ctx.lineTo(x + w - r, y);
161
+ ctx.arcTo(x + w, y, x + w, y + r, r);
162
+ ctx.lineTo(x + w, y + h - r);
163
+ ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
164
+ ctx.lineTo(x + r, y + h);
165
+ ctx.arcTo(x, y + h, x, y + h - r, r);
166
+ ctx.lineTo(x, y + r);
167
+ ctx.arcTo(x, y, x + r, y, r);
168
+ ctx.closePath();
169
+ }
170
+
171
+ /**
172
+ * Set position
173
+ * @param {number} x
174
+ * @param {number} y
175
+ * @returns {Mask} this for chaining
176
+ */
177
+ setPosition(x, y) {
178
+ this.x = x;
179
+ this.y = y;
180
+ return this;
181
+ }
182
+
183
+ /**
184
+ * Set uniform scale
185
+ * @param {number} scale
186
+ * @returns {Mask} this for chaining
187
+ */
188
+ setScale(scale) {
189
+ this.scaleX = scale;
190
+ this.scaleY = scale;
191
+ return this;
192
+ }
193
+
194
+ /**
195
+ * Set non-uniform scale
196
+ * @param {number} sx
197
+ * @param {number} sy
198
+ * @returns {Mask} this for chaining
199
+ */
200
+ setScaleXY(sx, sy) {
201
+ this.scaleX = sx;
202
+ this.scaleY = sy;
203
+ return this;
204
+ }
205
+
206
+ /**
207
+ * Create a circle mask
208
+ * @param {number} x - Center X
209
+ * @param {number} y - Center Y
210
+ * @param {number} radius
211
+ * @returns {Mask}
212
+ */
213
+ static circle(x, y, radius) {
214
+ return new Mask({ shape: 'circle', x, y, radius });
215
+ }
216
+
217
+ /**
218
+ * Create a rectangle mask
219
+ * @param {number} x - Center X
220
+ * @param {number} y - Center Y
221
+ * @param {number} width
222
+ * @param {number} height
223
+ * @param {number} cornerRadius - Optional rounded corners
224
+ * @returns {Mask}
225
+ */
226
+ static rectangle(x, y, width, height, cornerRadius = 0) {
227
+ return new Mask({ shape: 'rectangle', x, y, width, height, cornerRadius });
228
+ }
229
+
230
+ /**
231
+ * Create an ellipse mask
232
+ * @param {number} x - Center X
233
+ * @param {number} y - Center Y
234
+ * @param {number} radiusX
235
+ * @param {number} radiusY
236
+ * @returns {Mask}
237
+ */
238
+ static ellipse(x, y, radiusX, radiusY) {
239
+ return new Mask({ shape: 'ellipse', x, y, radiusX, radiusY });
240
+ }
241
+ }
@@ -186,4 +186,23 @@ export class Scene extends GameObject {
186
186
  get children() {
187
187
  return this._collection.children;
188
188
  }
189
+
190
+ /**
191
+ * Returns additional offset to apply during hit testing.
192
+ * Override in subclasses (e.g., LayoutScene) to account for scroll offset.
193
+ * @returns {{x: number, y: number}} Additional offset for hit test coordinate transform
194
+ */
195
+ getHitTestOffset() {
196
+ return { x: 0, y: 0 };
197
+ }
198
+
199
+ /**
200
+ * Checks if a child should be hittable (receive input events).
201
+ * Override in subclasses (e.g., LayoutScene) to implement viewport culling.
202
+ * @param {GameObject} child - The child to check
203
+ * @returns {boolean} True if child should be hittable
204
+ */
205
+ isChildHittable(child) {
206
+ return true;
207
+ }
189
208
  }
@@ -68,7 +68,13 @@ export class GameObjectShapeWrapper extends GameObject {
68
68
  * @param {Object} options - Configuration options
69
69
  */
70
70
  constructor(game, shape, options = {}) {
71
- super(game, options);
71
+ // IMPORTANT: Strip 'anchor' from options passed to GameObject
72
+ // In shape context, 'anchor' means "image rendering anchor" (center, top-left, etc.)
73
+ // In GameObject, 'anchor' triggers applyAnchor mixin for auto-positioning
74
+ // These are different concepts - don't let shape anchor trigger positioning
75
+ const { anchor: _shapeAnchor, ...goOptions } = options;
76
+
77
+ super(game, goOptions);
72
78
 
73
79
  // Validate shape
74
80
  if (!shape || shape == null || shape == undefined) {
@@ -224,9 +230,15 @@ export class GameObjectShapeWrapper extends GameObject {
224
230
 
225
231
  /**
226
232
  * Draw method to render the shape
233
+ *
234
+ * IMPORTANT: Call shape.draw() NOT shape.render()!
235
+ * The wrapper's render() has already translated to (this.x, this.y).
236
+ * Calling shape.render() would call Painter.translateTo(shape.x, shape.y)
237
+ * which OVERWRITES (not adds to) the current translation.
238
+ * By calling shape.draw() directly, we render at the wrapper's position.
227
239
  */
228
240
  draw() {
229
241
  super.draw();
230
- this.shape.render();
242
+ this.shape.draw();
231
243
  }
232
244
  }
@@ -78,6 +78,17 @@ export class Pipeline extends Loggable {
78
78
  if (scene.children && scene.children.length > 0) {
79
79
  for (let i = scene.children.length - 1; i >= 0; i--) {
80
80
  const child = scene.children[i];
81
+
82
+ // Check if child is hittable (e.g., within viewport for scrollable layouts)
83
+ if (scene.isChildHittable && !scene.isChildHittable(child)) {
84
+ // Force mouseout if child was hovered but is now outside viewport
85
+ if (child._hovered) {
86
+ child._hovered = false;
87
+ child.events.emit("mouseout", e);
88
+ }
89
+ continue;
90
+ }
91
+
81
92
  if (child instanceof Scene) {
82
93
  this._hoverScene(child, e); // recurse into nested scenes
83
94
  } else {
@@ -152,6 +163,12 @@ export class Pipeline extends Loggable {
152
163
  // First check children (they render on top, so should get priority)
153
164
  for (let i = scene.children.length - 1; i >= 0; i--) {
154
165
  const child = scene.children[i];
166
+
167
+ // Check if child is hittable (e.g., within viewport for scrollable layouts)
168
+ if (scene.isChildHittable && !scene.isChildHittable(child)) {
169
+ continue;
170
+ }
171
+
155
172
  if (child instanceof Scene) {
156
173
  // Recurse deeper if child is also a Scene
157
174
  const hit = this._dispatchToScene(child, type, e);
@@ -110,8 +110,9 @@ export class Button extends GameObject {
110
110
  // Basic position and sizing
111
111
  this.x = x;
112
112
  this.y = y;
113
- this.width = width;
114
- this.height = height;
113
+ // Ensure minimum touch target size (44x44px recommended for mobile)
114
+ this.width = Math.max(width, 44);
115
+ this.height = Math.max(height, 44);
115
116
  this.padding = padding;
116
117
  this.textAlign = textAlign;
117
118
  this.textBaseline = textBaseline;
@@ -200,6 +201,10 @@ export class Button extends GameObject {
200
201
  /**
201
202
  * Update label position based on alignment and baseline settings
202
203
  * @private
204
+ *
205
+ * Note: TextShape now centers its bounding box at (x, y) regardless of textAlign.
206
+ * For non-center alignments, we adjust the position by half the text dimensions
207
+ * so that left-aligned text STARTS at the left edge, not centers there.
203
208
  */
204
209
  alignText() {
205
210
  if (!this.label) return;
@@ -207,13 +212,22 @@ export class Button extends GameObject {
207
212
  const halfWidth = this.width / 2;
208
213
  const halfHeight = this.height / 2;
209
214
 
210
- // Horizontal alignment
215
+ // Get text dimensions (available after TextShape._calculateBounds)
216
+ const textHalfWidth = (this.label._width || 0) / 2;
217
+ const textHalfHeight = (this.label._height || 0) / 2;
218
+
219
+ // Horizontal alignment - position where text CENTER should be
220
+ // TextShape centers its bounding box at (x, y), so we adjust accordingly
211
221
  switch (this.textAlign) {
212
222
  case "left":
213
- this.label.x = -halfWidth + this.padding;
223
+ // Text should START at left edge + padding
224
+ // So text CENTER should be at left edge + padding + half text width
225
+ this.label.x = -halfWidth + this.padding + textHalfWidth;
214
226
  break;
215
227
  case "right":
216
- this.label.x = halfWidth - this.padding;
228
+ // Text should END at right edge - padding
229
+ // So text CENTER should be at right edge - padding - half text width
230
+ this.label.x = halfWidth - this.padding - textHalfWidth;
217
231
  break;
218
232
  case "center":
219
233
  default:
@@ -221,13 +235,15 @@ export class Button extends GameObject {
221
235
  break;
222
236
  }
223
237
 
224
- // Vertical alignment
238
+ // Vertical alignment - position where text CENTER should be
225
239
  switch (this.textBaseline) {
226
240
  case "top":
227
- this.label.y = -halfHeight + this.padding;
241
+ // Text should START at top edge + padding
242
+ this.label.y = -halfHeight + this.padding + textHalfHeight;
228
243
  break;
229
244
  case "bottom":
230
- this.label.y = halfHeight - this.padding;
245
+ // Text should END at bottom edge - padding
246
+ this.label.y = halfHeight - this.padding - textHalfHeight;
231
247
  break;
232
248
  case "middle":
233
249
  default:
@@ -257,16 +273,85 @@ export class Button extends GameObject {
257
273
  this.onPressed = onPressed;
258
274
  this.onRelease = onRelease;
259
275
 
260
- this.on("mouseover", this.setState.bind(this, "hover"));
261
- this.on("mouseout", this.setState.bind(this, "default"));
262
- this.on("inputdown", this.setState.bind(this, "pressed"));
263
- this.on("inputup", () => {
264
- // Fire onClick if user was in "pressed" state
265
- if (this.state === "pressed" && typeof onClick === "function") {
276
+ // Track pointer state for proper hover handling
277
+ this._pointerOver = false;
278
+ this._isTouch = false;
279
+
280
+ // Mouse hover events (desktop only)
281
+ this.on("mouseover", (e) => {
282
+ this._pointerOver = true;
283
+ if (!this._isTouch) {
284
+ this.setState("hover");
285
+ }
286
+ });
287
+
288
+ this.on("mouseout", (e) => {
289
+ this._pointerOver = false;
290
+ if (!this._isTouch) {
291
+ this.setState("default");
292
+ }
293
+ });
294
+
295
+ // Touch/pointer down - prevent default for mobile-friendly behavior
296
+ this.on("inputdown", (e) => {
297
+ // Detect touch input
298
+ if (e.touches || (e.nativeEvent && e.nativeEvent.type === 'touchstart')) {
299
+ this._isTouch = true;
300
+ // Prevent default touch behaviors (scrolling, zooming)
301
+ if (e.nativeEvent) {
302
+ e.nativeEvent.preventDefault();
303
+ }
304
+ }
305
+ this._pointerOver = true;
306
+ this.setState("pressed");
307
+ });
308
+
309
+ // Touch/pointer up
310
+ this.on("inputup", (e) => {
311
+ const wasPressed = this.state === "pressed";
312
+
313
+ // Prevent default touch behaviors
314
+ if (e.touches || (e.nativeEvent && e.nativeEvent.type === 'touchend')) {
315
+ if (e.nativeEvent) {
316
+ e.nativeEvent.preventDefault();
317
+ }
318
+ this._isTouch = true;
319
+ }
320
+
321
+ // Verify pointer is still over button using hit test
322
+ const stillOver = this._hitTest && this._hitTest(e.x, e.y);
323
+
324
+ // Fire onClick if user was in "pressed" state and pointer is still over
325
+ if (wasPressed && stillOver && typeof onClick === "function") {
266
326
  onClick();
267
327
  }
268
- // Return to hover state if the pointer is still over the button
269
- this.setState("hover");
328
+
329
+ // Check if pointer is still over the button
330
+ // On touch devices, don't set hover state (no hover on touch)
331
+ if (stillOver && !this._isTouch) {
332
+ this.setState("hover");
333
+ } else {
334
+ this.setState("default");
335
+ // Reset touch flag after a delay to allow mouse hover to work
336
+ if (this._isTouch) {
337
+ setTimeout(() => {
338
+ this._isTouch = false;
339
+ }, 300);
340
+ }
341
+ }
342
+ });
343
+
344
+ // Touch move - update pointer position
345
+ this.on("inputmove", (e) => {
346
+ // Check if pointer is still over button
347
+ if (this._hitTest && this._hitTest(e.x, e.y)) {
348
+ this._pointerOver = true;
349
+ } else {
350
+ this._pointerOver = false;
351
+ if (this.state === "hover" && !this._isTouch) {
352
+ this.setState("default");
353
+ }
354
+ }
270
355
  });
271
356
  }
272
357
 
@@ -121,9 +121,3 @@ export default UI_THEME;
121
121
 
122
122
 
123
123
 
124
-
125
-
126
-
127
-
128
-
129
-
@@ -43,7 +43,13 @@ export class ToggleButton extends Button {
43
43
  }
44
44
 
45
45
  // Update our visual style for toggled vs. not
46
+ // Store current state before refresh
47
+ const currentState = this.state;
46
48
  this.refreshToggleVisual();
49
+ // Re-apply current state after refresh to ensure proper colors
50
+ if (currentState) {
51
+ this.setState(currentState);
52
+ }
47
53
  },
48
54
  });
49
55
  // Terminal × Vercel theme for toggled state
@@ -59,8 +65,13 @@ export class ToggleButton extends Button {
59
65
 
60
66
  toggle(v) {
61
67
  // Toggle the button state and refresh visuals
68
+ const currentState = this.state;
62
69
  this.toggled = v;
63
70
  this.refreshToggleVisual();
71
+ // Re-apply current state after refresh to ensure proper colors
72
+ if (currentState) {
73
+ this.setState(currentState);
74
+ }
64
75
  }
65
76
 
66
77
  /**
@@ -68,32 +79,32 @@ export class ToggleButton extends Button {
68
79
  */
69
80
  refreshToggleVisual() {
70
81
  if (this.toggled) {
71
- // E.g. "active" styling
72
- this.bg.fillColor = this.colorActiveBg;
73
- this.bg.strokeColor = this.colorActiveStroke;
82
+ // Active/toggled styling - use correct property names
83
+ this.bg.color = this.colorActiveBg;
84
+ this.bg.stroke = this.colorActiveStroke;
74
85
  this.label.color = this.colorActiveText;
75
86
  } else {
76
- // Revert to normal styling
77
- this.bg.fillColor = this.colors.default.bg;
78
- this.bg.strokeColor = this.colors.default.stroke;
87
+ // Revert to normal styling - use correct property names
88
+ this.bg.color = this.colors.default.bg;
89
+ this.bg.stroke = this.colors.default.stroke;
79
90
  this.label.color = this.colors.default.text;
80
91
  }
81
92
  }
82
93
 
83
94
  /**
84
- * If we want ephemeral states (hover, pressed) to remain visible briefly,
85
- * we can let parent setState run, then immediately re-apply toggled style
86
- * so we don't lose the "toggled" color. This is optional.
95
+ * Override setState to properly handle toggled state.
96
+ * When toggled, always use active colors. When not toggled, use normal button behavior.
87
97
  */
88
98
  setState(state) {
99
+ // Always call parent first to handle cursor and callbacks
89
100
  super.setState(state);
90
-
91
- // If we're toggled on, ensure it stays in the toggled visuals
92
- // after the parent sets hover/pressed colors.
101
+
102
+ // If toggled, override colors with active colors (ignore hover/pressed colors)
93
103
  if (this.toggled) {
94
- this.bg.fillColor = this.colorActiveBg;
95
- this.bg.strokeColor = this.colorActiveStroke;
104
+ this.bg.color = this.colorActiveBg;
105
+ this.bg.stroke = this.colorActiveStroke;
96
106
  this.label.color = this.colorActiveText;
97
107
  }
108
+ // If not toggled, parent's setState already set the correct colors
98
109
  }
99
110
  }
@@ -237,12 +237,20 @@ export class Tooltip extends GameObject {
237
237
  this.bg.height = textHeight + this.padding * 2;
238
238
 
239
239
  // Position each line inside bg
240
- const startX = -this.bg.width / 2 + this.padding;
241
- const startY = -this.bg.height / 2 + this.padding;
240
+ // TextShape now centers its bounding box at (x, y), so for left/top aligned text
241
+ // we need to position the CENTER where we want it, not the top-left corner.
242
+ // Text should START at the left edge + padding, so CENTER is at left edge + padding + textWidth/2
243
+ const bgLeft = -this.bg.width / 2;
242
244
 
243
245
  for (let i = 0; i < this.lineShapes.length; i++) {
244
- this.lineShapes[i].x = startX;
245
- this.lineShapes[i].y = startY + i * lineHeight;
246
+ const shape = this.lineShapes[i];
247
+ const textHalfWidth = (shape._width || 0) / 2;
248
+ const textHalfHeight = (shape._height || lineHeight) / 2;
249
+
250
+ // Text starts at left edge + padding, so center is at left edge + padding + halfWidth
251
+ shape.x = bgLeft + this.padding + textHalfWidth;
252
+ // Text starts at top edge + padding + i*lineHeight, so center is offset by halfHeight
253
+ shape.y = -this.bg.height / 2 + this.padding + i * lineHeight + textHalfHeight;
246
254
  }
247
255
  }
248
256
 
package/src/index.js CHANGED
@@ -21,5 +21,8 @@ export * from "./state";
21
21
  // Particle system
22
22
  export * from "./particle";
23
23
 
24
+ // Physics (for particle simulations)
25
+ export * from "./physics";
26
+
24
27
  // WebGL (optional, for shader effects)
25
28
  export * from "./webgl";