@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,226 @@
1
+ import { Shape } from "./shape.js";
2
+ import { Painter } from "../painter/painter.js";
3
+
4
+ /**
5
+ * Prism - A 3D-looking isometric triangular prism with rotation support.
6
+ *
7
+ * Supports:
8
+ * - Individual face colors
9
+ * - Full bounding box
10
+ * - Face visibility control (top, bottom, left, right, front, back)
11
+ * - Rotation around X, Y, and Z axes
12
+ *
13
+ * Note: This is a 2.5D visual illusion — not actual 3D rendering.
14
+ */
15
+ export class Prism extends Shape {
16
+ /**
17
+ * Create a triangular prism
18
+ * @param {number} x - X position (center of the prism)
19
+ * @param {number} y - Y position (center of the prism)
20
+ * @param {number} width - Width of the prism
21
+ * @param {number} height - Height of the triangular face
22
+ * @param {number} depth - Depth/length of the prism
23
+ * @param {object} options - Customization options
24
+ * @param {string} [options.faceTopColor] - Color of the top face
25
+ * @param {string} [options.faceBottomColor] - Color of the bottom face
26
+ * @param {string} [options.faceLeftColor] - Color of the left face
27
+ * @param {string} [options.faceRightColor] - Color of the right face
28
+ * @param {string} [options.faceFrontColor] - Color of the front face
29
+ * @param {string} [options.faceBackColor] - Color of the back face
30
+ * @param {Array<string>} [options.visibleFaces] - Array of face keys to render
31
+ * @param {string} [options.stroke] - Optional stroke around each face
32
+ * @param {number} [options.lineWidth] - Stroke width
33
+ * @param {number} [options.rotationX] - Rotation around X axis in radians
34
+ * @param {number} [options.rotationY] - Rotation around Y axis in radians
35
+ * @param {number} [options.rotationZ] - Rotation around Z axis in radians
36
+ */
37
+ constructor(depth = 100, options = {}) {
38
+ super(options);
39
+ this.depth = depth;
40
+
41
+ this.faceTopColor = options.faceTopColor || "#eee";
42
+ this.faceBottomColor = options.faceBottomColor || "#ccc";
43
+ this.faceLeftColor = options.faceLeftColor || "#aaa";
44
+ this.faceRightColor = options.faceRightColor || "#888";
45
+ this.faceFrontColor = options.faceFrontColor || "#666";
46
+ this.faceBackColor = options.faceBackColor || "#444";
47
+
48
+ this.stroke = options.stroke || null;
49
+ this.lineWidth = options.lineWidth || 1;
50
+
51
+ // Rotation angles (in radians)
52
+ this.rotationX = options.rotationX || 0;
53
+ this.rotationY = options.rotationY || 0;
54
+ this.rotationZ = options.rotationZ || 0;
55
+
56
+ /** @type {Array<'top'|'bottom'|'left'|'right'|'front'|'back'>} */
57
+ this.visibleFaces = options.visibleFaces || [
58
+ "top",
59
+ "left",
60
+ "right",
61
+ "front",
62
+ "back",
63
+ "bottom",
64
+ ];
65
+ }
66
+
67
+ /**
68
+ * Set rotation angles
69
+ * @param {number} x - Rotation around X axis in radians
70
+ * @param {number} y - Rotation around Y axis in radians
71
+ * @param {number} z - Rotation around Z axis in radians
72
+ */
73
+ setRotation(x, y, z) {
74
+ this.rotationX = x;
75
+ this.rotationY = y;
76
+ this.rotationZ = z;
77
+ return this; // Enable method chaining
78
+ }
79
+
80
+ /**
81
+ * Rotate the prism incrementally
82
+ * @param {number} x - Increment for X rotation in radians
83
+ * @param {number} y - Increment for Y rotation in radians
84
+ * @param {number} z - Increment for Z rotation in radians
85
+ */
86
+ rotate(x, y, z) {
87
+ this.rotationX += x;
88
+ this.rotationY += y;
89
+ this.rotationZ += z;
90
+ return this; // Enable method chaining
91
+ }
92
+
93
+ /**
94
+ * Internal draw logic
95
+ */
96
+ draw() {
97
+ super.draw();
98
+ const w = this.width / 2; // Half width for positioning
99
+ const h = this.height / 2; // Half height for positioning
100
+ const d = this.depth / 2; // Half depth for positioning
101
+
102
+ /**
103
+ * Apply 3D rotation to a point
104
+ * @param {number} x
105
+ * @param {number} y
106
+ * @param {number} z
107
+ * @returns {{x: number, y: number, z: number}}
108
+ */
109
+ const rotate3D = (x, y, z) => {
110
+ // Apply X-axis rotation
111
+ let y1 = y;
112
+ let z1 = z;
113
+ y = y1 * Math.cos(this.rotationX) - z1 * Math.sin(this.rotationX);
114
+ z = y1 * Math.sin(this.rotationX) + z1 * Math.cos(this.rotationX);
115
+
116
+ // Apply Y-axis rotation
117
+ let x1 = x;
118
+ z1 = z;
119
+ x = x1 * Math.cos(this.rotationY) + z1 * Math.sin(this.rotationY);
120
+ z = -x1 * Math.sin(this.rotationY) + z1 * Math.cos(this.rotationY);
121
+
122
+ // Apply Z-axis rotation
123
+ x1 = x;
124
+ y1 = y;
125
+ x = x1 * Math.cos(this.rotationZ) - y1 * Math.sin(this.rotationZ);
126
+ y = x1 * Math.sin(this.rotationZ) + y1 * Math.cos(this.rotationZ);
127
+
128
+ return { x, y, z };
129
+ };
130
+
131
+ /**
132
+ * Isometric projection of 3D point
133
+ * @param {number} x
134
+ * @param {number} y
135
+ * @param {number} z
136
+ * @returns {{x: number, y: number}}
137
+ */
138
+ const iso = (x, y, z) => {
139
+ // Apply rotations first
140
+ const rotated = rotate3D(x, y, z);
141
+
142
+ // Then apply isometric projection
143
+ const isoX = (rotated.x - rotated.y) * Math.cos(Math.PI / 6);
144
+ const isoY = (rotated.x + rotated.y) * Math.sin(Math.PI / 6) - rotated.z;
145
+ return { x: isoX, y: isoY, z: rotated.z }; // Include z for depth sorting
146
+ };
147
+
148
+ // Define vertices for the triangular prism
149
+ const p = {
150
+ // Front triangular face
151
+ p0: iso(-w, -d, -h), // Front bottom left
152
+ p1: iso(w, -d, -h), // Front bottom right
153
+ p2: iso(0, -d, h), // Front top center
154
+
155
+ // Back triangular face
156
+ p3: iso(-w, d, -h), // Back bottom left
157
+ p4: iso(w, d, -h), // Back bottom right
158
+ p5: iso(0, d, h), // Back top center
159
+ };
160
+
161
+ // Faces mapped to corner points
162
+ const faces = {
163
+ // Triangular front and back faces
164
+ front: { points: [p.p0, p.p1, p.p2], color: this.faceFrontColor },
165
+ back: { points: [p.p3, p.p4, p.p5], color: this.faceBackColor },
166
+
167
+ // Rectangular side faces
168
+ bottom: { points: [p.p0, p.p1, p.p4, p.p3], color: this.faceBottomColor },
169
+ right: { points: [p.p1, p.p2, p.p5, p.p4], color: this.faceRightColor },
170
+ left: { points: [p.p0, p.p2, p.p5, p.p3], color: this.faceLeftColor },
171
+ };
172
+
173
+ // Calculate visibility based on depth sorting
174
+ const visibleFacesWithDepth = this.visibleFaces
175
+ .filter((key) => faces[key])
176
+ .map((key) => {
177
+ const face = faces[key];
178
+
179
+ // Calculate face center for z-ordering
180
+ const centerX =
181
+ face.points.reduce((sum, pt) => sum + pt.x, 0) / face.points.length;
182
+ const centerY =
183
+ face.points.reduce((sum, pt) => sum + pt.y, 0) / face.points.length;
184
+ const centerZ =
185
+ face.points.reduce((sum, pt) => sum + (pt.z || 0), 0) /
186
+ face.points.length;
187
+
188
+ // Calculate approximate depth
189
+ // Higher value = farther back
190
+ const depth = centerX * centerX + centerY * centerY + centerZ * centerZ;
191
+
192
+ return { key, face, depth };
193
+ })
194
+ .sort((a, b) => b.depth - a.depth); // Sort by depth (back to front)
195
+
196
+ // Draw faces in depth order
197
+ visibleFacesWithDepth.forEach(({ key, face }) => {
198
+ if (face?.color) {
199
+ Painter.shapes.polygon(
200
+ face.points,
201
+ face.color,
202
+ this.stroke,
203
+ this.lineWidth
204
+ );
205
+ }
206
+ });
207
+ }
208
+
209
+ /**
210
+ * Compute bounding box for interactivity and layout
211
+ * @returns {{x: number, y: number, width: number, height: number}}
212
+ */
213
+ getBounds() {
214
+ // Calculate actual bounds based on isometric projection
215
+ const projectionFactor = 1.5; // Approximation for isometric projection
216
+ const maxDimension = Math.max(this.width, this.height, this.depth);
217
+ const adjustedSize = maxDimension * projectionFactor;
218
+
219
+ return {
220
+ x: this.x - adjustedSize / 2,
221
+ y: this.y - adjustedSize / 2,
222
+ width: adjustedSize,
223
+ height: adjustedSize,
224
+ };
225
+ }
226
+ }
@@ -0,0 +1,35 @@
1
+ import { Shape } from "./shape.js";
2
+ import { Painter } from "../painter/painter.js";
3
+
4
+ /**
5
+ * Rectangle - A drawable centered rectangle using the canvas.
6
+ *
7
+ * Draws a rectangle from its center (not top-left) using Painter.
8
+ */
9
+ export class Rectangle extends Shape {
10
+ constructor(options = {}) {
11
+ super(options);
12
+ }
13
+
14
+ /**
15
+ * Renders the rectangle using Painter.
16
+ */
17
+ draw() {
18
+ super.draw(); // Apply constraints from Shape
19
+ this.drawRect();
20
+ }
21
+
22
+ drawRect() {
23
+ // When in a group context, use relative coordinates
24
+ const x = -this.width / 2;
25
+ const y = -this.height / 2;
26
+
27
+ if (this.color) {
28
+ Painter.shapes.rect(x, y, this.width, this.height, this.color);
29
+ }
30
+
31
+ if (this.stroke) {
32
+ Painter.shapes.outlineRect(x, y, this.width, this.height, this.stroke, this.lineWidth);
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,333 @@
1
+ import { Geometry2d } from "./geometry.js";
2
+ import { Painter } from "../painter/painter.js";
3
+ import { Traceable } from "./traceable.js";
4
+
5
+ /**
6
+ * Renderable
7
+ * ----------
8
+ *
9
+ * A render-capable spatial object in the gcanvas engine.
10
+ *
11
+ * This class introduces the core rendering lifecycle — it knows when to draw,
12
+ * how to draw, and how to **not draw** when conditions say so (invisible, opacity = 0).
13
+ *
14
+ * ### Architectural Role
15
+ *
16
+ * - Adds **rendering lifecycle control** (`render()`)
17
+ * - Supports **canvas state management** (save/restore)
18
+ * - Exposes **visual props** like opacity, visibility, and debug
19
+ * - Optional **shadow styling** support
20
+ *
21
+ * `Renderable` should be extended by anything that draws visuals:
22
+ * - Shapes (e.g., `Rectangle`, `Text`)
23
+ * - Sprites
24
+ * - UI elements
25
+ *
26
+ * @abstract
27
+ * @extends Geometry2d
28
+ */
29
+ export class Renderable extends Traceable {
30
+ /**
31
+ * @param {Object} [options={}]
32
+ * @param {boolean} [options.visible=true] - Whether this object should be drawn
33
+ * @param {number} [options.opacity=1] - Alpha transparency (0–1)
34
+ * @param {boolean} [options.active=true] - Whether this object receives updates
35
+ * @param {string} [options.blendMode="source-over"] - Optional blend mode
36
+ * @param {string} [options.shadowColor] - Optional shadow color
37
+ * @param {number} [options.shadowBlur=0] - Shadow blur radius
38
+ * @param {number} [options.shadowOffsetX=0] - Shadow X offset
39
+ * @param {number} [options.shadowOffsetY=0] - Shadow Y offset
40
+ * @param {number} [options.zIndex=0] - Z-index for stacking order
41
+ */
42
+ constructor(options = {}) {
43
+ super(options);
44
+ this._visible = options.visible !== false;
45
+ this._opacity = typeof options.opacity === "number" ? options.opacity : 1;
46
+ this._active = options.active !== false;
47
+ this.zIndex = options.zIndex ?? 0;
48
+
49
+ this._shadowColor = options.shadowColor ?? undefined;
50
+ this._shadowBlur = options.shadowBlur ?? 0;
51
+ this._shadowOffsetX = options.shadowOffsetX ?? 0;
52
+ this._shadowOffsetY = options.shadowOffsetY ?? 0;
53
+
54
+ // Render caching - when enabled, renders to an offscreen canvas once
55
+ // and blits the cached bitmap on subsequent frames for better performance.
56
+ this._cacheRendering = options.cacheRendering ?? false;
57
+ this._cacheCanvas = null;
58
+ this._cacheDirty = true;
59
+ this._cachePadding = options.cachePadding ?? 2; // Extra padding for anti-aliasing/strokes
60
+
61
+ this._tick = 0;
62
+ this.logger.log("Renderable", this.x, this.y, this.width, this.height);
63
+ }
64
+
65
+ /**
66
+ * Main render method.
67
+ * Handles visibility, translation, and calls draw() in the transformed context.
68
+ * If cacheRendering is enabled, renders to an offscreen canvas once and blits it.
69
+ */
70
+ render() {
71
+ if (!this._visible || this._opacity <= 0) return;
72
+
73
+ Painter.save();
74
+ Painter.effects.setBlendMode(this._blendMode);
75
+
76
+ if (this.crisp) {
77
+ Painter.translateTo(Math.round(this.x), Math.round(this.y));
78
+ } else {
79
+ Painter.translateTo(this.x, this.y);
80
+ }
81
+
82
+ this.applyShadow(Painter.ctx);
83
+
84
+ // If caching IS NOT enabled or we're the base class, render normally
85
+ if (!this._cacheRendering || this.constructor.name === "Renderable") {
86
+ Painter.opacity.pushOpacity(this._opacity);
87
+ this.draw();
88
+ Painter.opacity.popOpacity();
89
+ } else {
90
+ // Caching logic - cache the IDENTITY shape (untransformed)
91
+ const rawWidth = typeof this.width === "number" ? this.width : 0;
92
+ const rawHeight = typeof this.height === "number" ? this.height : 0;
93
+ const padding = this._cachePadding * 2;
94
+ const cacheWidth = Math.ceil(rawWidth + padding) || 1;
95
+ const cacheHeight = Math.ceil(rawHeight + padding) || 1;
96
+
97
+ // Create or resize cache canvas if needed
98
+ if (
99
+ !this._cacheCanvas ||
100
+ this._cacheCanvas.width !== cacheWidth ||
101
+ this._cacheCanvas.height !== cacheHeight
102
+ ) {
103
+ this._cacheCanvas = document.createElement("canvas");
104
+ this._cacheCanvas.width = cacheWidth;
105
+ this._cacheCanvas.height = cacheHeight;
106
+ this._cacheDirty = true;
107
+ }
108
+
109
+ // Re-render to cache if dirty
110
+ if (this._cacheDirty) {
111
+ this._renderToCache(cacheWidth, cacheHeight);
112
+ this._cacheDirty = false;
113
+ }
114
+
115
+ // Blit cached canvas with current opacity AND transforms
116
+ // This allows efficient rotation/scaling of the cached bitmap
117
+ Painter.opacity.pushOpacity(this._opacity);
118
+
119
+ // Extract transform properties if they exist
120
+ const rotation = this.rotation ?? 0;
121
+ const scaleX = this.scaleX ?? 1;
122
+ const scaleY = this.scaleY ?? 1;
123
+
124
+ Painter.img.draw(this._cacheCanvas, 0, 0, {
125
+ width: cacheWidth,
126
+ height: cacheHeight,
127
+ rotation: rotation,
128
+ scaleX: scaleX,
129
+ scaleY: scaleY,
130
+ anchor: "center",
131
+ });
132
+
133
+ Painter.opacity.popOpacity();
134
+ }
135
+
136
+ Painter.restore();
137
+ }
138
+
139
+ /**
140
+ * Internal method to render the object to the offscreen cache canvas.
141
+ * @param {number} width - Cache width
142
+ * @param {number} height - Cache height
143
+ * @protected
144
+ */
145
+ _renderToCache(width, height) {
146
+ const cacheCtx = this._cacheCanvas.getContext("2d");
147
+ cacheCtx.clearRect(0, 0, width, height);
148
+
149
+ // Swap to cache context
150
+ const mainCtx = Painter.ctx;
151
+ Painter.ctx = cacheCtx;
152
+
153
+ // Signal subclasses to skip transforms if they are transform-aware
154
+ this._isCaching = true;
155
+
156
+ cacheCtx.save();
157
+ // Translate to center of cache so (0,0) draws correctly
158
+ cacheCtx.translate(width / 2, height / 2);
159
+
160
+ // Call draw() to render to the cache at full opacity and identity transform
161
+ this.draw();
162
+
163
+ cacheCtx.restore();
164
+
165
+ this._isCaching = false;
166
+
167
+ // Restore main context
168
+ Painter.ctx = mainCtx;
169
+ }
170
+
171
+ /**
172
+ * Mark the render cache as needing refresh.
173
+ */
174
+ invalidateCache() {
175
+ this._cacheDirty = true;
176
+ }
177
+
178
+ draw() {
179
+ this.drawDebug();
180
+ }
181
+
182
+ /**
183
+ * Called once per frame if the object is active.
184
+ * @param {number} dt - Time delta since last frame (seconds)
185
+ */
186
+ update(dt) {
187
+ this.trace("Renderable.update");
188
+ this._tick += dt;
189
+ super.update(dt);
190
+ }
191
+
192
+ /**
193
+ * Apply shadow styles to the current canvas context.
194
+ * Only called if `shadowColor` is defined.
195
+ *
196
+ * @param {CanvasRenderingContext2D} ctx
197
+ */
198
+ applyShadow(ctx) {
199
+ if (!this._shadowColor) return;
200
+ ctx.shadowColor = this._shadowColor;
201
+ ctx.shadowBlur = this._shadowBlur;
202
+ ctx.shadowOffsetX = this._shadowOffsetX;
203
+ ctx.shadowOffsetY = this._shadowOffsetY;
204
+ }
205
+
206
+ /**
207
+ * Gets whether the object is visible (drawn during render).
208
+ * @type {boolean}
209
+ */
210
+ get visible() {
211
+ return this._visible;
212
+ }
213
+
214
+ set visible(v) {
215
+ this._visible = Boolean(v);
216
+ }
217
+
218
+ get width() {
219
+ return super.width;
220
+ }
221
+
222
+ set width(v) {
223
+ super.width = v;
224
+ this.invalidateCache();
225
+ }
226
+
227
+ get height() {
228
+ return super.height;
229
+ }
230
+
231
+ set height(v) {
232
+ super.height = v;
233
+ this.invalidateCache();
234
+ }
235
+
236
+ /**
237
+ * Gets whether the object is active (updated during game loop).
238
+ * @type {boolean}
239
+ */
240
+ get active() {
241
+ return this._active;
242
+ }
243
+
244
+ set active(v) {
245
+ this._active = Boolean(v);
246
+ }
247
+
248
+ /**
249
+ * Gets the object's opacity (0–1).
250
+ * @type {number}
251
+ */
252
+ get opacity() {
253
+ return this._opacity;
254
+ }
255
+
256
+ set opacity(v) {
257
+ this._opacity = Math.min(1, Math.max(0, typeof v === "number" ? v : 1));
258
+ }
259
+
260
+ /**
261
+ * Gets the current shadow color (if any).
262
+ * @type {string|undefined}
263
+ */
264
+ get shadowColor() {
265
+ return this._shadowColor;
266
+ }
267
+
268
+ set shadowColor(v) {
269
+ this._shadowColor = v;
270
+ this.invalidateCache();
271
+ }
272
+
273
+ /**
274
+ * Gets the blur radius for the drop shadow.
275
+ * @type {number}
276
+ */
277
+ get shadowBlur() {
278
+ return this._shadowBlur;
279
+ }
280
+
281
+ set shadowBlur(v) {
282
+ this._shadowBlur = v;
283
+ this.invalidateCache();
284
+ }
285
+
286
+ /**
287
+ * Gets the horizontal offset of the drop shadow.
288
+ * @type {number}
289
+ */
290
+ get shadowOffsetX() {
291
+ return this._shadowOffsetX;
292
+ }
293
+
294
+ set shadowOffsetX(v) {
295
+ this._shadowOffsetX = v;
296
+ this.invalidateCache();
297
+ }
298
+
299
+ /**
300
+ * Gets the vertical offset of the drop shadow.
301
+ * @type {number}
302
+ */
303
+ get shadowOffsetY() {
304
+ return this._shadowOffsetY;
305
+ }
306
+
307
+ set shadowOffsetY(v) {
308
+ this._shadowOffsetY = v;
309
+ this.invalidateCache();
310
+ }
311
+
312
+ /**
313
+ * Total elapsed time this object has been alive (updated).
314
+ * @type {number}
315
+ * @readonly
316
+ */
317
+ get tick() {
318
+ return this._tick;
319
+ }
320
+
321
+ /**
322
+ * Whether render caching is enabled for this object.
323
+ * @type {boolean}
324
+ */
325
+ get cacheRendering() {
326
+ return this._cacheRendering;
327
+ }
328
+
329
+ set cacheRendering(v) {
330
+ this._cacheRendering = Boolean(v);
331
+ if (v) this.invalidateCache();
332
+ }
333
+ }
@@ -0,0 +1,26 @@
1
+ import { Shape } from "./shape.js";
2
+ import { Painter } from "../painter/painter.js";
3
+
4
+ export class Ring extends Shape {
5
+ constructor(outerRadius, innerRadius, options = {}) {
6
+ super(options);
7
+ this.outerRadius = outerRadius;
8
+ this.innerRadius = innerRadius;
9
+ }
10
+
11
+ draw() {
12
+ super.draw();
13
+ Painter.lines.beginPath();
14
+ Painter.shapes.arc(0, 0, this.outerRadius, 0, Math.PI * 2);
15
+ Painter.shapes.arc(0, 0, this.innerRadius, 0, Math.PI * 2, true);
16
+ Painter.lines.closePath();
17
+
18
+ if (this.color) {
19
+ Painter.colors.fill(this.color);
20
+ }
21
+
22
+ if (this.stroke) {
23
+ Painter.colors.stroke(this.stroke, this.lineWidth);
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,95 @@
1
+ import { Shape } from "./shape.js";
2
+ import { Painter } from "../painter/painter.js";
3
+
4
+ /**
5
+ * RoundedRectangle - A drawable centered rectangle with rounded corners.
6
+ *
7
+ * Draws a rounded rectangle from its center using Painter.
8
+ */
9
+ export class RoundedRectangle extends Shape {
10
+ /**
11
+ * @param {number} x - Center X
12
+ * @param {number} y - Center Y
13
+ * @param {number} width - Rectangle width
14
+ * @param {number} height - Rectangle height
15
+ * @param {number|number[]} radii - Corner radius or array of radii for each corner
16
+ * @param {Object} [options] - Shape rendering options
17
+ */
18
+ constructor(radii = 0, options = {}) {
19
+ super(options);
20
+ // Handle radius either as a single value or array of 4 values
21
+ if (typeof radii === "number") {
22
+ this.radii = [radii, radii, radii, radii]; // [topLeft, topRight, bottomRight, bottomLeft]
23
+ } else if (Array.isArray(radii)) {
24
+ // Ensure we have exactly 4 values
25
+ this.radii =
26
+ radii.length === 4
27
+ ? radii
28
+ : [
29
+ radii[0] || 0,
30
+ radii[1] || radii[0] || 0,
31
+ radii[2] || radii[0] || 0,
32
+ radii[3] || radii[1] || radii[0] || 0,
33
+ ];
34
+ } else {
35
+ this.radii = [0, 0, 0, 0];
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Renders the rounded rectangle using Painter's roundRect method.
41
+ */
42
+ draw() {
43
+ super.draw();
44
+ const x = -this.width / 2;
45
+ const y = -this.height / 2;
46
+ // Use the Painter's roundRect utility methods
47
+ if (this.color && this.stroke) {
48
+ // If both fill and stroke are needed
49
+ Painter.shapes.roundRect(
50
+ x,
51
+ y,
52
+ this.width,
53
+ this.height,
54
+ this.radii,
55
+ this.color,
56
+ this.stroke,
57
+ this.lineWidth
58
+ );
59
+ } else if (this.color) {
60
+ // If only fill is needed
61
+ Painter.shapes.fillRoundRect(
62
+ x,
63
+ y,
64
+ this.width,
65
+ this.height,
66
+ this.radii,
67
+ this.color
68
+ );
69
+ } else if (this.stroke) {
70
+ // If only stroke is needed
71
+ Painter.shapes.strokeRoundRect(
72
+ x,
73
+ y,
74
+ this.width,
75
+ this.height,
76
+ this.radii,
77
+ this.stroke,
78
+ this.lineWidth
79
+ );
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Returns the bounding box for the rounded rectangle
85
+ * @returns {{x: number, y: number, width: number, height: number}}
86
+ */
87
+ getBounds() {
88
+ return {
89
+ x: this.x,
90
+ y: this.y,
91
+ width: this.width,
92
+ height: this.height,
93
+ };
94
+ }
95
+ }