@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,553 @@
1
+ import { Shape } from "./shape.js";
2
+ import { Painter } from "../painter/painter.js";
3
+ /**
4
+ * TextShape - A drawable text shape that supports rotation, scaling, and grouping.
5
+ * Intended for use inside a Group.
6
+ *
7
+ * @extends Shape
8
+ */
9
+ export class TextShape extends Shape {
10
+ /**
11
+ * Create a text shape
12
+ *
13
+ * @param {string} text - The text content
14
+ * @param {Object} [options={}] - Configuration options
15
+ * @param {string} [options.font="12px monospace"] - CSS font string
16
+ * @param {string} [options.color="#000"] - Text color
17
+ * @param {string} [options.align="center"] - Text alignment (left, center, right)
18
+ * @param {string} [options.baseline="middle"] - Text baseline (top, middle, bottom)
19
+ */
20
+ constructor(text, options = {}) {
21
+ super(options);
22
+ this._text = text;
23
+ this._font = options.font || "12px monospace";
24
+ this._color = options.color || "yellow";
25
+ this._align = options.align || "center";
26
+ this._baseline = options.baseline || "middle";
27
+
28
+ // Calculate initial bounds
29
+ this._calculateBounds();
30
+ this._calculateAlignmentOffsets();
31
+ }
32
+
33
+ /**
34
+ * Draw the text using Painter
35
+ */
36
+ draw() {
37
+ super.draw();
38
+ this.logger.log("draw", this.font, this.color, this.opacity);
39
+ Painter.text.setFont(this.font);
40
+ Painter.text.setTextAlign(this.align);
41
+ Painter.text.setTextBaseline(this.baseline);
42
+ Painter.text.fillText(this.text, 0, 0, this.color);
43
+ }
44
+
45
+ _calculateAlignmentOffsets() {
46
+ // Save current canvas context
47
+ if (!Painter.text) return;
48
+ // Measure text dimensions
49
+ const metrics = Painter.text.measureTextDimensions(this.text, this.font);
50
+ // Calculate horizontal center point offset
51
+ switch (this._align) {
52
+ case "left":
53
+ this._centerOffsetX = metrics.width / 2;
54
+ break;
55
+ case "center":
56
+ this._centerOffsetX = 0;
57
+ break;
58
+ case "right":
59
+ this._centerOffsetX = -metrics.width / 2 - 5;
60
+ break;
61
+ }
62
+ // Calculate vertical center point offset
63
+ switch (this._baseline) {
64
+ case "top":
65
+ this._centerOffsetY = metrics.height/4;
66
+ break;
67
+ case "middle":
68
+ this._centerOffsetY = -2;
69
+ break;
70
+ case "bottom":
71
+ this._centerOffsetY = -metrics.height;
72
+ break;
73
+ }
74
+ //console.log("calculateAlignmentOffsets", this._centerOffsetY, this._centerOffsetX);
75
+ }
76
+
77
+ getTextBounds() {
78
+ if (Painter.text) {
79
+ // Measure the text dimensions
80
+ const metrics = Painter.text.measureTextDimensions(this.text, this.font);
81
+ // Add padding
82
+ const padding = 2;
83
+ return {
84
+ x: this._centerOffsetX - metrics.width / 2,
85
+ y: this._centerOffsetY - metrics.height / 2,
86
+ width: metrics.width + padding * 2,
87
+ height: metrics.height + padding * 2,
88
+ };
89
+ }
90
+ // Fallback
91
+ return {
92
+ x: this._centerOffsetX,
93
+ y: this._centerOffsetY,
94
+ width: this._width,
95
+ height: this._height,
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Overridden _calculateBounds to include alignment offsets
101
+ * @private
102
+ */
103
+ _calculateBounds() {
104
+ if (Painter.text) {
105
+ // Measure the text dimensions
106
+ const metrics = Painter.text.measureTextDimensions(this.text, this.font);
107
+
108
+ // Set dimensions based on measurements
109
+ this._width = metrics.width;
110
+ this._height = metrics.height;
111
+
112
+ // Calculate alignment offsets
113
+ this._calculateAlignmentOffsets();
114
+ } else {
115
+ // Fallback if Painter not available
116
+ this._width = this.text ? this.text.length * 8 : 0;
117
+ this._height = 16;
118
+ }
119
+ this.trace(
120
+ "TextShape.calculateBounds: " + this._width + "x" + this._height
121
+ );
122
+ }
123
+
124
+ /**
125
+ * Debug bounds should match text bounds
126
+ * @returns {Object} Debug bounds with width and height
127
+ */
128
+ getDebugBounds() {
129
+ const textBounds = this.getTextBounds();
130
+ return {
131
+ x: textBounds.x,
132
+ y: textBounds.y,
133
+ width: textBounds.width,
134
+ height: textBounds.height,
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Check if a property has changed and update bounds if needed
140
+ * @param {*} value - New value
141
+ * @param {*} oldValue - Previous value
142
+ * @private
143
+ */
144
+ checkDirty(value, oldValue) {
145
+ if (value !== oldValue) {
146
+ this._boundsDirty = true;
147
+ this._calculateBounds();
148
+ }
149
+ }
150
+
151
+ // Getters and setters
152
+
153
+ get text() {
154
+ return this._text;
155
+ }
156
+
157
+ set text(value) {
158
+ this.checkDirty(value, this._text);
159
+ this._text = value;
160
+ }
161
+
162
+ get font() {
163
+ return this._font;
164
+ }
165
+
166
+ set font(value) {
167
+ this.checkDirty(value, this._font);
168
+ this._font = value;
169
+ }
170
+
171
+ get color() {
172
+ return this._color;
173
+ }
174
+
175
+ set color(value) {
176
+ this._color = value;
177
+ }
178
+
179
+ get align() {
180
+ return this._align;
181
+ }
182
+
183
+ set align(value) {
184
+ this.checkDirty(value, this._align);
185
+ this._align = value;
186
+ }
187
+
188
+ get baseline() {
189
+ return this._baseline;
190
+ }
191
+
192
+ set baseline(value) {
193
+ this.checkDirty(value, this._baseline);
194
+ this._baseline = value;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * OutlinedText - A text shape with a stroke outline.
200
+ *
201
+ * Draws text with both fill and stroke for an outlined effect.
202
+ */
203
+ export class OutlinedText extends Shape {
204
+ /**
205
+ * @param {number} x - X coordinate (or center X if centered)
206
+ * @param {number} y - Y coordinate (or center Y if centered)
207
+ * @param {string} text - Text content
208
+ * @param {Object} [options] - Shape rendering options
209
+ * @param {boolean} [options.centered=false] - Whether the text is positioned from its center
210
+ * @param {string} [options.color='#000000'] - Text fill color
211
+ * @param {string} [options.stroke='#FFFFFF'] - Text stroke color
212
+ * @param {number} [options.lineWidth=1] - Width of the text outline
213
+ * @param {string} [options.font] - Font specification
214
+ * @param {string} [options.align='left'] - Text alignment ('left', 'center', 'right')
215
+ * @param {string} [options.baseline='alphabetic'] - Text baseline
216
+ */
217
+ constructor(x, y, text, options = {}) {
218
+ super(x, y, options);
219
+ this.text = text;
220
+ // Text-specific options
221
+ this.centered = options.centered || false;
222
+ this.color = options.color || "#000000";
223
+ this.stroke = options.stroke || "#FFFFFF";
224
+ this.lineWidth = options.lineWidth || 1;
225
+ this.font = options.font || null;
226
+ this.align = options.align || "left";
227
+ this.baseline = options.baseline || "alphabetic";
228
+ // Calculate text dimensions
229
+ this.calculateDimensions();
230
+ }
231
+
232
+ /**
233
+ * Calculate the dimensions of the text
234
+ * @private
235
+ */
236
+ calculateDimensions() {
237
+ if (!Painter.ctx) {
238
+ console.warn(
239
+ "Painter context not initialized. Cannot calculate text dimensions."
240
+ );
241
+ this.width = 0;
242
+ this.height = 0;
243
+ return;
244
+ }
245
+
246
+ // Save current context settings
247
+ const currentFont = Painter.text.font();
248
+
249
+ // Apply font if provided
250
+ if (this.font) Painter.text.setFont(this.font);
251
+
252
+ // Measure the text
253
+ const metrics = Painter.text.measureText(this.text);
254
+
255
+ // Set dimensions
256
+ this.width = metrics.width;
257
+
258
+ // Approximate height from font size if available
259
+ if (this.font) {
260
+ const fontSize = parseInt(this.font);
261
+ this.height = isNaN(fontSize) ? 20 : fontSize; // Default to 20 if parsing fails
262
+ } else {
263
+ // Try to get height from metrics (newer browsers) or estimate
264
+ this.height =
265
+ metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent ||
266
+ 20;
267
+ }
268
+
269
+ // Add a bit of padding for the stroke
270
+ this.width += this.lineWidth * 2;
271
+ this.height += this.lineWidth * 2;
272
+
273
+ // Restore font
274
+ Painter.text.setFont(currentFont);
275
+ }
276
+
277
+ /**
278
+ * Update the text content
279
+ * @param {string} text - New text content
280
+ */
281
+ setText(text) {
282
+ this.text = text;
283
+ this.calculateDimensions();
284
+ }
285
+
286
+ /**
287
+ * Renders the outlined text
288
+ */
289
+ draw() {
290
+ super.draw();
291
+
292
+ // Handle case where Painter context isn't initialized yet
293
+ if (!Painter.ctx) {
294
+ console.warn("Painter context not initialized. Cannot draw text.");
295
+ return;
296
+ }
297
+
298
+ // Calculate the starting position
299
+ let xPos = 0;
300
+ let yPos = 0;
301
+
302
+ // Apply text settings
303
+ if (this.font) Painter.text.setFont(this.font);
304
+ Painter.text.setTextAlign(this.align);
305
+ Painter.text.setTextBaseline(this.baseline);
306
+
307
+ // Adjust for centered positioning
308
+ if (this.centered) {
309
+ // No adjustment needed as textAlign will handle this
310
+ if (this.baseline === "middle" || this.baseline === "alphabetic") {
311
+ yPos = 0;
312
+ } else if (this.baseline === "top") {
313
+ yPos = this.height / 2;
314
+ } else if (this.baseline === "bottom") {
315
+ yPos = -this.height / 2;
316
+ }
317
+ }
318
+
319
+ // Draw the outlined text using Painter's outlinedText method
320
+ Painter.outlinedText(
321
+ this.text,
322
+ xPos,
323
+ yPos,
324
+ this.color,
325
+ this.stroke,
326
+ this.lineWidth,
327
+ this.font
328
+ );
329
+ }
330
+
331
+ getBounds() {
332
+ // Use the canvas context directly for precision
333
+ if (!Painter.ctx) {
334
+ return super.getBounds(); // fallback
335
+ }
336
+
337
+ // Save current font and apply the text style
338
+ const prevFont = Painter.text.font();
339
+ Painter.text.setFont(this.font);
340
+
341
+ const metrics = Painter.text.measureText(this.text);
342
+ const width = metrics.width;
343
+ const height =
344
+ metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent ||
345
+ parseInt(this.font) ||
346
+ 20;
347
+
348
+ Painter.text.setFont(prevFont);
349
+
350
+ this.width = width;
351
+ this.height = height;
352
+
353
+ return {
354
+ x: this.x,
355
+ y: this.y,
356
+ width,
357
+ height,
358
+ };
359
+ }
360
+ }
361
+
362
+ /**
363
+ * WrappedText - A text shape that automatically wraps to fit within a specified width.
364
+ *
365
+ * Draws text that wraps to new lines when it exceeds a maximum width.
366
+ */
367
+ export class WrappedText extends Shape {
368
+ /**
369
+ * @param {number} x - Left X coordinate (or center if centered=true)
370
+ * @param {number} y - Top Y coordinate (or center if centered=true)
371
+ * @param {string} text - Text content
372
+ * @param {number} maxWidth - Maximum width before wrapping
373
+ * @param {number} [lineHeight=20] - Line height for wrapped text
374
+ * @param {Object} [options] - Shape rendering options
375
+ * @param {boolean} [options.centered=false] - Whether the text is positioned from its center
376
+ * @param {string} [options.color='#000000'] - Text fill color
377
+ * @param {string} [options.font] - Font specification
378
+ * @param {string} [options.align='left'] - Text alignment ('left', 'center', 'right')
379
+ * @param {string} [options.baseline='top'] - Text baseline
380
+ */
381
+ constructor(x, y, text, maxWidth, lineHeight = 20, options = {}) {
382
+ super(x, y, options);
383
+
384
+ this.text = text;
385
+ this.maxWidth = maxWidth;
386
+ this.lineHeight = lineHeight;
387
+
388
+ // Text-specific options
389
+ this.centered = options.centered || false;
390
+ this.color = options.color || "#000000";
391
+ this.font = options.font || null;
392
+ this.align = options.align || "left";
393
+ this.baseline = options.baseline || "top";
394
+
395
+ // For outlined text
396
+ this.outlineColor = options.outlineColor || null;
397
+ this.outlineWidth = options.outlineWidth || 1;
398
+
399
+ // Calculate wrapped text dimensions
400
+ this.calculateDimensions();
401
+ }
402
+
403
+ /**
404
+ * Calculate the dimensions of the wrapped text
405
+ * @private
406
+ */
407
+ calculateDimensions() {
408
+ if (!Painter.ctx) {
409
+ console.warn(
410
+ "Painter context not initialized. Cannot calculate text dimensions."
411
+ );
412
+ this.width = this.maxWidth;
413
+ this.height = this.lineHeight;
414
+ this.lines = [this.text];
415
+ return;
416
+ }
417
+
418
+ // Save current context settings
419
+ const currentFont = Painter.text.font();
420
+ const currentAlign = Painter.text.textAlign();
421
+ const currentBaseline = Painter.text.textBaseline();
422
+
423
+ // Apply text settings
424
+ if (this.font) Painter.text.setFont(this.font);
425
+ Painter.text.setTextAlign("left"); // Always left-align for measurement
426
+ Painter.text.setTextBaseline("top");
427
+
428
+ // Calculate wrapped lines and dimensions
429
+ const words = this.text.split(" ");
430
+ let line = "";
431
+ let testLine = "";
432
+ this.lines = [];
433
+ this.width = 0;
434
+
435
+ for (let i = 0; i < words.length; i++) {
436
+ testLine = line + words[i] + " ";
437
+ const metrics = Painter.text.measureText(testLine);
438
+ const testWidth = metrics.width;
439
+
440
+ if (testWidth > this.maxWidth && i > 0) {
441
+ this.lines.push(line);
442
+ this.width = Math.max(this.width, Painter.text.measureText(line).width);
443
+ line = words[i] + " ";
444
+ } else {
445
+ line = testLine;
446
+ }
447
+ }
448
+
449
+ // Add the last line
450
+ this.lines.push(line);
451
+ this.width = Math.max(this.width, Painter.text.measureText(line).width);
452
+ this.height = this.lines.length * this.lineHeight;
453
+
454
+ // Restore context settings
455
+ Painter.text.setFont(currentFont);
456
+ Painter.text.setTextAlign(currentAlign);
457
+ Painter.text.setTextBaseline(currentBaseline);
458
+ }
459
+
460
+ /**
461
+ * Update the text content
462
+ * @param {string} text - New text content
463
+ */
464
+ setText(text) {
465
+ this.text = text;
466
+ this.calculateDimensions();
467
+ }
468
+
469
+ /**
470
+ * Renders the wrapped text
471
+ */
472
+ draw() {
473
+ super.draw();
474
+
475
+ // Handle case where Painter context isn't initialized yet
476
+ if (!Painter.ctx) {
477
+ console.warn("Painter context not initialized. Cannot draw text.");
478
+ return;
479
+ }
480
+
481
+ // Calculate the starting position
482
+ let xPos = 0;
483
+ let yPos = 0;
484
+
485
+ // Adjust for centered positioning
486
+ if (this.centered) {
487
+ xPos = -this.width / 2;
488
+ yPos = -this.height / 2;
489
+ }
490
+
491
+ // Apply text settings
492
+ if (this.font) Painter.text.setFont(this.font);
493
+ Painter.text.setTextAlign(this.align);
494
+ Painter.text.setTextBaseline(this.baseline);
495
+
496
+ // Adjust x based on alignment
497
+ let alignmentX = xPos;
498
+ if (this.align === "center") {
499
+ alignmentX = 0;
500
+ } else if (this.align === "right") {
501
+ alignmentX = xPos + this.width;
502
+ }
503
+
504
+ // Draw each line
505
+ for (let i = 0; i < this.lines.length; i++) {
506
+ const lineY = yPos + i * this.lineHeight;
507
+
508
+ if (this.outlineColor) {
509
+ // Draw outlined text
510
+ Painter.outlinedText(
511
+ this.lines[i],
512
+ alignmentX,
513
+ lineY,
514
+ this.color,
515
+ this.outlineColor,
516
+ this.outlineWidth,
517
+ this.font
518
+ );
519
+ } else {
520
+ // Draw regular text
521
+ Painter.text.fillText(
522
+ this.lines[i],
523
+ alignmentX,
524
+ lineY,
525
+ this.color,
526
+ this.font
527
+ );
528
+ }
529
+ }
530
+ }
531
+
532
+ /**
533
+ * Returns the bounding box
534
+ * @returns {{x: number, y: number, width: number, height: number}}
535
+ */
536
+ getBounds() {
537
+ if (this.centered) {
538
+ return {
539
+ x: this.x,
540
+ y: this.y,
541
+ width: this.width,
542
+ height: this.height,
543
+ };
544
+ } else {
545
+ return {
546
+ x: this.x + this.width / 2,
547
+ y: this.y + this.height / 2,
548
+ width: this.width,
549
+ height: this.height,
550
+ };
551
+ }
552
+ }
553
+ }
@@ -0,0 +1,83 @@
1
+ import { Painter } from "../painter/painter";
2
+ import { Geometry2d } from "./geometry.js";
3
+
4
+ export class Traceable extends Geometry2d {
5
+ constructor(options = {}) {
6
+ super(options);
7
+ this._debug = Boolean(options.debug);
8
+ this._debugColor =
9
+ typeof options.debugColor === "string" ? options.debugColor : "#0f0";
10
+ this.logger.log("Traceable", this.x, this.y, this.width, this.height);
11
+ }
12
+
13
+ /**
14
+ * Draws debug bounding box in local space (after translation and transforms).
15
+ * Should be called from within the transformed context.
16
+ */
17
+ drawDebug() {
18
+ if (!this._debug) return;
19
+
20
+ // Get the debug bounds in local space
21
+ const debugBounds = this.getDebugBounds();
22
+ this.logger.log(
23
+ this.constructor.name,
24
+ "drawDebug",
25
+ debugBounds.x,
26
+ debugBounds.y,
27
+ debugBounds.width,
28
+ debugBounds.height
29
+ );
30
+
31
+ // Draw debug rectangle in local space (already translated and transformed)
32
+ Painter.shapes.outlineRect(
33
+ debugBounds.x,
34
+ debugBounds.y,
35
+ debugBounds.width,
36
+ debugBounds.height,
37
+ this._debugColor,
38
+ 2
39
+ );
40
+ }
41
+
42
+ /**
43
+ * Returns debug bounds in local space (centered at origin).
44
+ * Override in subclasses for custom debug bounds.
45
+ * @returns {{x: number, y: number, width: number, height: number}}
46
+ */
47
+ getDebugBounds() {
48
+ // Return bounds centered at local origin (0, 0)
49
+ return {
50
+ width: this.width,
51
+ height: this.height,
52
+ x: -this.width / 2,
53
+ y: -this.height / 2,
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Logs the object's render state (debug only).
59
+ * @param {string} [msg]
60
+ */
61
+ trace(msg = "render") {
62
+ this.logger.log(
63
+ this.name == null ? this.constructor.name : this.name,
64
+ msg,
65
+ "x",
66
+ this.x,
67
+ "y",
68
+ this.y,
69
+ "w",
70
+ this.width,
71
+ "h",
72
+ this.height,
73
+ "opacity",
74
+ this._opacity,
75
+ "visible",
76
+ this._visible,
77
+ "active",
78
+ this._active,
79
+ "debug",
80
+ this.debug
81
+ );
82
+ }
83
+ }