@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,863 @@
1
+ /**
2
+ * IsometricGame Demo
3
+ *
4
+ * Demonstrates the IsometricScene class for creating isometric tile-based games.
5
+ * Features a bouncing ball that can be controlled with WASD and Space to jump.
6
+ */
7
+ import {
8
+ Game,
9
+ GameObject,
10
+ IsometricScene,
11
+ IsometricCamera,
12
+ Button,
13
+ TextShape,
14
+ Painter,
15
+ Keys
16
+ } from "../../src/index";
17
+
18
+ /**
19
+ * Configuration for the isometric demo
20
+ */
21
+ const CONFIG = {
22
+ gridSize: 10,
23
+ tileWidth: 64,
24
+ tileHeight: 32,
25
+ elevationScale: 1.0,
26
+ ball: {
27
+ baseRadius: 8,
28
+ color: "#3498db",
29
+ strokeColor: "#2980b9",
30
+ jumpPower: 4,
31
+ gravity: 0.25,
32
+ acceleration: 0.8, // grid units per second squared
33
+ maxVelocity: 0.12, // max grid units per frame
34
+ friction: 0.90,
35
+ bounceFactorWall: 0.5,
36
+ bounceFactorGround: 0.2,
37
+ },
38
+ grid: {
39
+ lineColor: "#ccc",
40
+ originColor: "#FF0000",
41
+ originRadius: 5,
42
+ },
43
+ // Platform layout: [x, y, width, depth, height, color]
44
+ platforms: [
45
+ // Starting platform (center) - big and low
46
+ { x: -2, y: -2, w: 4, d: 4, h: 20, color: "#8B4513" },
47
+
48
+ // Ramp going up-right (stepping stones)
49
+ { x: 2, y: -2, w: 2, d: 2, h: 35, color: "#A0522D" },
50
+ { x: 4, y: -2, w: 2, d: 2, h: 50, color: "#A0522D" },
51
+ { x: 6, y: -2, w: 3, d: 3, h: 65, color: "#CD853F" },
52
+
53
+ // High platform (top right)
54
+ { x: 6, y: -6, w: 3, d: 3, h: 80, color: "#DEB887" },
55
+
56
+ // Bridge/ramp going back
57
+ { x: 4, y: -6, w: 2, d: 2, h: 65, color: "#A0522D" },
58
+ { x: 2, y: -6, w: 2, d: 2, h: 50, color: "#A0522D" },
59
+ { x: 0, y: -6, w: 2, d: 2, h: 35, color: "#8B4513" },
60
+
61
+ // Side platform (green area)
62
+ { x: -6, y: 0, w: 3, d: 3, h: 40, color: "#6B8E23" },
63
+ { x: -6, y: 3, w: 2, d: 2, h: 25, color: "#556B2F" },
64
+
65
+ // Lower platform (blue)
66
+ { x: 3, y: 3, w: 3, d: 3, h: 30, color: "#4682B4" },
67
+ ],
68
+ };
69
+
70
+ /**
71
+ * An isometric 3D box/platform that the ball can stand on.
72
+ */
73
+ class IsometricBox extends GameObject {
74
+ /**
75
+ * @param {Game} game - Game instance
76
+ * @param {IsometricScene} isoScene - The parent isometric scene
77
+ * @param {Object} options - Box configuration
78
+ * @param {number} options.x - Grid X position
79
+ * @param {number} options.y - Grid Y position
80
+ * @param {number} options.w - Width in grid units
81
+ * @param {number} options.d - Depth in grid units
82
+ * @param {number} options.h - Height in pixels
83
+ * @param {string} options.color - Base color
84
+ */
85
+ constructor(game, isoScene, options) {
86
+ super(game);
87
+ this.isoScene = isoScene;
88
+ this.x = options.x;
89
+ this.y = options.y;
90
+ this.w = options.w;
91
+ this.d = options.d;
92
+ this.h = options.h;
93
+ this.baseColor = options.color;
94
+
95
+ // Calculate colors for shading
96
+ this.topColor = options.color;
97
+ this.leftColor = this.shadeColor(options.color, -30);
98
+ this.rightColor = this.shadeColor(options.color, -50);
99
+ }
100
+
101
+ /**
102
+ * Custom depth value for sorting - uses front corner for proper overlap
103
+ */
104
+ get isoDepth() {
105
+ // Use the front-most corner (x+w, y+d) plus height
106
+ // Height factor matches ball's z factor for consistent sorting
107
+ return (this.x + this.w) + (this.y + this.d) + this.h * 0.5;
108
+ }
109
+
110
+ /**
111
+ * Shade a hex color by a percentage
112
+ * @param {string} color - Hex color
113
+ * @param {number} percent - Percentage to lighten/darken
114
+ * @returns {string} Shaded hex color
115
+ */
116
+ shadeColor(color, percent) {
117
+ const num = parseInt(color.replace("#", ""), 16);
118
+ const amt = Math.round(2.55 * percent);
119
+ const R = Math.max(0, Math.min(255, (num >> 16) + amt));
120
+ const G = Math.max(0, Math.min(255, ((num >> 8) & 0x00FF) + amt));
121
+ const B = Math.max(0, Math.min(255, (num & 0x0000FF) + amt));
122
+ return `#${(0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1)}`;
123
+ }
124
+
125
+ /**
126
+ * Check if a point is inside this box's X/Y bounds
127
+ * @param {number} px - Point X in grid
128
+ * @param {number} py - Point Y in grid
129
+ * @param {number} margin - Optional margin for collision (default 0.1)
130
+ * @returns {boolean}
131
+ */
132
+ containsPoint(px, py, margin = 0.1) {
133
+ return px >= this.x - margin && px < this.x + this.w + margin &&
134
+ py >= this.y - margin && py < this.y + this.d + margin;
135
+ }
136
+
137
+ /**
138
+ * Get the surface height for landing.
139
+ * Always returns the platform height - collision detection handles whether to use it.
140
+ * @returns {number} Platform height
141
+ */
142
+ getSurfaceHeight() {
143
+ return this.h;
144
+ }
145
+
146
+ /**
147
+ * Renders the isometric box with all visible faces.
148
+ * Draws back faces first, front faces last, based on camera angle.
149
+ */
150
+ render() {
151
+ const scene = this.isoScene;
152
+
153
+ // Get camera angle (direction camera is looking FROM)
154
+ const cameraAngle = scene.camera ? scene.camera.angle : 0;
155
+
156
+ // Camera view direction (where camera is looking TOWARD)
157
+ // In isometric, default view looks toward +X +Y direction (angle π/4 from +X axis)
158
+ // Camera rotation rotates around Z axis
159
+ const viewDirection = Math.PI / 4 + cameraAngle;
160
+
161
+ // Get all 8 corners of the box
162
+ const topNW = scene.toIsometric(this.x, this.y, this.h);
163
+ const topNE = scene.toIsometric(this.x + this.w, this.y, this.h);
164
+ const topSE = scene.toIsometric(this.x + this.w, this.y + this.d, this.h);
165
+ const topSW = scene.toIsometric(this.x, this.y + this.d, this.h);
166
+
167
+ const botNW = scene.toIsometric(this.x, this.y, 0);
168
+ const botNE = scene.toIsometric(this.x + this.w, this.y, 0);
169
+ const botSE = scene.toIsometric(this.x + this.w, this.y + this.d, 0);
170
+ const botSW = scene.toIsometric(this.x, this.y + this.d, 0);
171
+
172
+ // Light direction (fixed in world space - from upper left)
173
+ const lightAngle = -Math.PI / 4;
174
+
175
+ // Define the 4 side faces with their world-space normal directions
176
+ const faces = [
177
+ { // North face (Y-): normal points toward -Y
178
+ verts: [topNW, topNE, botNE, botNW],
179
+ normalAngle: -Math.PI / 2,
180
+ },
181
+ { // East face (X+): normal points toward +X
182
+ verts: [topNE, topSE, botSE, botNE],
183
+ normalAngle: 0,
184
+ },
185
+ { // South face (Y+): normal points toward +Y
186
+ verts: [topSE, topSW, botSW, botSE],
187
+ normalAngle: Math.PI / 2,
188
+ },
189
+ { // West face (X-): normal points toward -X
190
+ verts: [topSW, topNW, botNW, botSW],
191
+ normalAngle: Math.PI,
192
+ }
193
+ ];
194
+
195
+ // Calculate shading and visibility for each face
196
+ for (const face of faces) {
197
+ // Rotate the face normal by camera angle
198
+ const rotatedNormal = face.normalAngle + cameraAngle;
199
+
200
+ // Face visibility: a face is visible if its rotated normal
201
+ // has a component pointing toward the camera (away from view direction)
202
+ // In isometric, visible faces are those facing generally toward -Y screen direction
203
+ const normalToView = rotatedNormal - viewDirection;
204
+ face.facingCamera = Math.cos(normalToView) < 0;
205
+
206
+ // For depth sorting: faces with normals pointing more toward +Y screen
207
+ // (into the screen in isometric) should be drawn first
208
+ face.depth = Math.sin(rotatedNormal);
209
+
210
+ // Lighting: based on angle between world-space normal and light
211
+ const lightDiff = face.normalAngle - lightAngle;
212
+ const lightFactor = (Math.cos(lightDiff) + 1) / 2; // 0 to 1
213
+ const shadeFactor = -50 + lightFactor * 60; // Range from -50 to +10
214
+ face.color = this.shadeColor(this.baseColor, shadeFactor);
215
+ }
216
+
217
+ // Sort faces: draw back-facing first, then front-facing
218
+ // Within each group, sort by depth
219
+ faces.sort((a, b) => {
220
+ // Back-facing faces drawn first
221
+ if (a.facingCamera !== b.facingCamera) {
222
+ return a.facingCamera ? 1 : -1;
223
+ }
224
+ // Then by depth (lower depth = further back = draw first)
225
+ return a.depth - b.depth;
226
+ });
227
+
228
+ // Draw faces in order: back faces first (with strokes), then front faces (fill covers back strokes)
229
+ Painter.useCtx((ctx) => {
230
+ ctx.strokeStyle = "rgba(0,0,0,0.4)";
231
+ ctx.lineWidth = 1;
232
+
233
+ // Draw each face with fill AND stroke, in depth order
234
+ // Back faces are drawn first - their strokes will show at the back edges
235
+ // Front faces are drawn last - their fills will cover internal strokes
236
+ for (const face of faces) {
237
+ ctx.beginPath();
238
+ ctx.moveTo(face.verts[0].x, face.verts[0].y);
239
+ ctx.lineTo(face.verts[1].x, face.verts[1].y);
240
+ ctx.lineTo(face.verts[2].x, face.verts[2].y);
241
+ ctx.lineTo(face.verts[3].x, face.verts[3].y);
242
+ ctx.closePath();
243
+ ctx.fillStyle = face.color;
244
+ ctx.fill();
245
+ ctx.stroke();
246
+ }
247
+
248
+ // Draw top face last (always on top)
249
+ ctx.beginPath();
250
+ ctx.moveTo(topNW.x, topNW.y);
251
+ ctx.lineTo(topNE.x, topNE.y);
252
+ ctx.lineTo(topSE.x, topSE.y);
253
+ ctx.lineTo(topSW.x, topSW.y);
254
+ ctx.closePath();
255
+ ctx.fillStyle = this.topColor;
256
+ ctx.fill();
257
+ ctx.stroke();
258
+ });
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Renders the isometric grid lines and origin marker.
264
+ *
265
+ * Uses the parent IsometricScene's toIsometric() method for projection.
266
+ * Should be added with zIndex = -1 to render behind other objects.
267
+ */
268
+ class IsometricGrid extends GameObject {
269
+ /**
270
+ * @param {Game} game - Game instance
271
+ * @param {IsometricScene} isoScene - The parent isometric scene for projection
272
+ */
273
+ constructor(game, isoScene) {
274
+ super(game);
275
+ this.isoScene = isoScene;
276
+ this.zIndex = -1; // Render behind other objects
277
+ }
278
+
279
+ /**
280
+ * Renders the grid lines and origin marker
281
+ */
282
+ render() {
283
+ const gridSize = this.isoScene.gridSize;
284
+
285
+ // Set up line style
286
+ Painter.colors.setStrokeColor(CONFIG.grid.lineColor);
287
+ Painter.lines.setLineWidth(1);
288
+
289
+ // Draw vertical grid lines (along X axis)
290
+ for (let x = -gridSize; x <= gridSize; x++) {
291
+ const start = this.isoScene.toIsometric(x, -gridSize);
292
+ const end = this.isoScene.toIsometric(x, gridSize);
293
+ Painter.lines.line(start.x, start.y, end.x, end.y);
294
+ }
295
+
296
+ // Draw horizontal grid lines (along Y axis)
297
+ for (let y = -gridSize; y <= gridSize; y++) {
298
+ const start = this.isoScene.toIsometric(-gridSize, y);
299
+ const end = this.isoScene.toIsometric(gridSize, y);
300
+ Painter.lines.line(start.x, start.y, end.x, end.y);
301
+ }
302
+
303
+ // Draw the origin marker in red for reference
304
+ const origin = this.isoScene.toIsometric(0, 0);
305
+ Painter.shapes.fillCircle(origin.x, origin.y, CONFIG.grid.originRadius, CONFIG.grid.originColor);
306
+ }
307
+ }
308
+
309
+ /**
310
+ * A bouncing ball that can be controlled with WASD + Space.
311
+ *
312
+ * Uses grid coordinates (x, y) for position and z for height above ground.
313
+ * The IsometricScene projects the position automatically; this class handles
314
+ * shadow rendering and visual effects relative to the projected position.
315
+ */
316
+ class Ball extends GameObject {
317
+ /**
318
+ * @param {Game} game - Game instance
319
+ * @param {IsometricScene} isoScene - The parent isometric scene for projection
320
+ * @param {IsometricBox[]} platforms - Array of platforms to collide with
321
+ */
322
+ constructor(game, isoScene, platforms = []) {
323
+ super(game);
324
+ this.isoScene = isoScene;
325
+ this.platforms = platforms;
326
+
327
+ // Grid position (x, y) and height (z)
328
+ this.x = 0;
329
+ this.y = 0;
330
+ this.z = 30; // Start above the starting platform
331
+
332
+ // Visual properties
333
+ this.baseRadius = CONFIG.ball.baseRadius;
334
+ this.color = CONFIG.ball.color;
335
+ this.strokeColor = CONFIG.ball.strokeColor;
336
+
337
+ // Physics properties
338
+ this.jumpPower = CONFIG.ball.jumpPower;
339
+ this.gravity = CONFIG.ball.gravity;
340
+ this.speed = CONFIG.ball.speed;
341
+ this.friction = CONFIG.ball.friction;
342
+ this.bounceFactorWall = CONFIG.ball.bounceFactorWall;
343
+ this.bounceFactorGround = CONFIG.ball.bounceFactorGround;
344
+
345
+ // Velocity
346
+ this.velocityX = 0;
347
+ this.velocityY = 0; // Vertical velocity (for jumping)
348
+ this.velocityZ = 0; // Grid Y velocity (confusingly named in original)
349
+
350
+ this.isJumping = false;
351
+ this.groundHeight = 0; // Current ground level (0 or platform height)
352
+
353
+ // Rotation for soccer ball effect (radians)
354
+ this.rotationX = 0; // Rotation around X axis (from moving in Y)
355
+ this.rotationY = 0; // Rotation around Y axis (from moving in X)
356
+ }
357
+
358
+ /**
359
+ * Set the platforms array for collision detection
360
+ * @param {IsometricBox[]} platforms
361
+ */
362
+ setPlatforms(platforms) {
363
+ this.platforms = platforms;
364
+ }
365
+
366
+ /**
367
+ * Custom depth value for sorting - ensures ball renders on top of platforms
368
+ */
369
+ get isoDepth() {
370
+ // Find the platform we're over (if any) and use its front corner as base
371
+ let baseDepth = this.x + this.y;
372
+
373
+ for (const platform of this.platforms) {
374
+ if (platform.containsPoint(this.x, this.y, 0)) {
375
+ // Use the platform's front corner as our base depth
376
+ const platformFront = (platform.x + platform.w) + (platform.y + platform.d);
377
+ if (platformFront > baseDepth) {
378
+ baseDepth = platformFront;
379
+ }
380
+ }
381
+ }
382
+
383
+ // Add height plus small offset to ensure we render on top of platforms
384
+ return baseDepth + this.z * 0.5 + 1;
385
+ }
386
+
387
+ /**
388
+ * Resets the ball to the starting platform
389
+ */
390
+ resetPosition() {
391
+ // Start on the center platform
392
+ this.x = 0;
393
+ this.y = 0;
394
+ this.z = 30; // Above the starting platform (which is at height 20)
395
+ this.velocityX = 0;
396
+ this.velocityZ = 0;
397
+ this.velocityY = 0;
398
+ this.isJumping = false;
399
+ this.groundHeight = 0;
400
+ this.rotationX = 0;
401
+ this.rotationY = 0;
402
+ }
403
+
404
+ /**
405
+ * Updates ball physics and handles input
406
+ * @param {number} dt - Delta time in seconds
407
+ */
408
+ update(dt) {
409
+ // Use config values for physics
410
+ const acceleration = CONFIG.ball.acceleration;
411
+ const maxVelocity = CONFIG.ball.maxVelocity;
412
+
413
+ // Frame-rate independent friction: friction^(dt*60) normalizes to 60 FPS feel
414
+ const frictionFactor = Math.pow(this.friction, dt * 60);
415
+ this.velocityX *= frictionFactor;
416
+ this.velocityZ *= frictionFactor;
417
+
418
+ // Handle movement input (WASD) - can move diagonally
419
+ if (Keys.isDown(Keys.W)) {
420
+ this.velocityZ -= acceleration * dt;
421
+ }
422
+ if (Keys.isDown(Keys.S)) {
423
+ this.velocityZ += acceleration * dt;
424
+ }
425
+ if (Keys.isDown(Keys.A)) {
426
+ this.velocityX -= acceleration * dt;
427
+ }
428
+ if (Keys.isDown(Keys.D)) {
429
+ this.velocityX += acceleration * dt;
430
+ }
431
+
432
+ // Clamp velocity to prevent overshooting
433
+ this.velocityX = Math.max(-maxVelocity, Math.min(maxVelocity, this.velocityX));
434
+ this.velocityZ = Math.max(-maxVelocity, Math.min(maxVelocity, this.velocityZ));
435
+
436
+ // Calculate desired new position
437
+ let newX = this.x + this.velocityX;
438
+ let newY = this.y + this.velocityZ;
439
+
440
+ // --- HORIZONTAL COLLISION DETECTION ---
441
+ // Simple approach: check if new position would be inside any platform we can't climb
442
+
443
+ // Ball collision radius in grid units (generous to prevent visual clipping)
444
+ const ballRadius = 0.62;
445
+ const platformBounce = 1.2; // Bouncy! >1 means it bounces back harder
446
+
447
+ // --- COLLISION RESOLUTION ---
448
+ // For each platform, check collision and resolve with bounce
449
+
450
+ for (const platform of this.platforms) {
451
+ // Skip if we're high enough to be on this platform
452
+ if (this.z >= platform.h) continue;
453
+
454
+ // Platform bounds expanded by ball radius
455
+ const pLeft = platform.x - ballRadius;
456
+ const pRight = platform.x + platform.w + ballRadius;
457
+ const pTop = platform.y - ballRadius;
458
+ const pBottom = platform.y + platform.d + ballRadius;
459
+
460
+ // Check if new position would be inside this platform
461
+ const insideX = newX > pLeft && newX < pRight;
462
+ const insideY = newY > pTop && newY < pBottom;
463
+
464
+ if (insideX && insideY) {
465
+ // Calculate overlap on each axis
466
+ const overlapLeft = newX - pLeft;
467
+ const overlapRight = pRight - newX;
468
+ const overlapTop = newY - pTop;
469
+ const overlapBottom = pBottom - newY;
470
+
471
+ // Find minimum overlap (shortest way out)
472
+ const minOverlap = Math.min(overlapLeft, overlapRight, overlapTop, overlapBottom);
473
+
474
+ // Push out and bounce in the direction of minimum overlap
475
+ if (minOverlap === overlapLeft) {
476
+ newX = pLeft;
477
+ this.velocityX = -Math.abs(this.velocityX) * platformBounce;
478
+ } else if (minOverlap === overlapRight) {
479
+ newX = pRight;
480
+ this.velocityX = Math.abs(this.velocityX) * platformBounce;
481
+ } else if (minOverlap === overlapTop) {
482
+ newY = pTop;
483
+ this.velocityZ = -Math.abs(this.velocityZ) * platformBounce;
484
+ } else if (minOverlap === overlapBottom) {
485
+ newY = pBottom;
486
+ this.velocityZ = Math.abs(this.velocityZ) * platformBounce;
487
+ }
488
+ }
489
+ }
490
+
491
+ // Apply the resolved position
492
+ this.x = newX;
493
+ this.y = newY;
494
+
495
+ // Update ball rotation based on movement (rolling effect)
496
+ // Physics: rotation = distance / radius
497
+ // For a ball of visual radius ~0.5 grid units, rolling 1 unit = 2 full rotations
498
+ const visualRadius = 0.5; // Grid units for rotation calculation
499
+ const distanceX = this.velocityX; // Distance moved this frame
500
+ const distanceY = this.velocityZ;
501
+
502
+ // Rotation in radians = distance / radius
503
+ this.rotationY += distanceX / visualRadius; // Moving in X rotates around Y axis
504
+ this.rotationX -= distanceY / visualRadius; // Moving in Y rotates around X axis
505
+
506
+ // Handle jump input
507
+ if (Keys.isDown(Keys.SPACE) && !this.isJumping) {
508
+ this.velocityY = this.jumpPower;
509
+ this.isJumping = true;
510
+ }
511
+
512
+ // Apply gravity (frame-rate independent)
513
+ this.velocityY -= this.gravity * dt * 60;
514
+ this.z += this.velocityY;
515
+
516
+ // --- VERTICAL COLLISION DETECTION ---
517
+ // Find what platforms we're currently over and can land on
518
+ this.groundHeight = 0;
519
+
520
+ for (const platform of this.platforms) {
521
+ // Check if we're within the platform's X/Y bounds
522
+ if (platform.containsPoint(this.x, this.y, 0)) {
523
+ // Get this platform's surface height
524
+ const surfaceHeight = platform.getSurfaceHeight();
525
+ if (surfaceHeight > this.groundHeight) {
526
+ this.groundHeight = surfaceHeight;
527
+ }
528
+ }
529
+ }
530
+
531
+ // Ground/platform collision - bounce based on impact speed
532
+ if (this.z < this.groundHeight) {
533
+ this.z = this.groundHeight;
534
+
535
+ // Calculate bounce factor based on impact velocity
536
+ // Faster falls = bouncier landing
537
+ const impactSpeed = Math.abs(this.velocityY);
538
+ const minBounce = 0.3;
539
+ const maxBounce = 0.8;
540
+ const bounceFactor = Math.min(maxBounce, minBounce + impactSpeed * 0.05);
541
+
542
+ this.velocityY *= -bounceFactor;
543
+
544
+ // Only stop bouncing if really slow
545
+ if (Math.abs(this.velocityY) < 0.3) {
546
+ this.velocityY = 0;
547
+ this.isJumping = false;
548
+ }
549
+ }
550
+
551
+ // Fall through floor (no platform and below ground) - reset
552
+ if (this.z < 0 && this.groundHeight === 0) {
553
+ // Check if we're over any platform at all
554
+ let overAnyPlatform = false;
555
+ for (const platform of this.platforms) {
556
+ if (platform.containsPoint(this.x, this.y, 0)) {
557
+ overAnyPlatform = true;
558
+ break;
559
+ }
560
+ }
561
+ // If not over any platform, we fell off - reset
562
+ if (!overAnyPlatform && this.z < -30) {
563
+ this.resetPosition();
564
+ }
565
+ }
566
+
567
+ // Boundary collisions with grid edges - bouncy walls!
568
+ const gridSize = CONFIG.gridSize;
569
+ const effectiveBoundary = gridSize - ballRadius;
570
+ const gridBounce = 1.5; // Very bouncy grid walls!
571
+
572
+ // X boundary - clamp and bounce
573
+ if (this.x < -effectiveBoundary) {
574
+ this.x = -effectiveBoundary;
575
+ this.velocityX = Math.abs(this.velocityX) * gridBounce;
576
+ } else if (this.x > effectiveBoundary) {
577
+ this.x = effectiveBoundary;
578
+ this.velocityX = -Math.abs(this.velocityX) * gridBounce;
579
+ }
580
+
581
+ // Y boundary - clamp and bounce
582
+ if (this.y < -effectiveBoundary) {
583
+ this.y = -effectiveBoundary;
584
+ this.velocityZ = Math.abs(this.velocityZ) * gridBounce;
585
+ } else if (this.y > effectiveBoundary) {
586
+ this.y = effectiveBoundary;
587
+ this.velocityZ = -Math.abs(this.velocityZ) * gridBounce;
588
+ }
589
+ }
590
+
591
+ /**
592
+ * Renders the ball as a gradient sphere with rotating stripe.
593
+ */
594
+ render() {
595
+ const ctx = Painter.ctx;
596
+
597
+ // Get projected position at ground height (for shadow)
598
+ const shadowPos = this.isoScene.toIsometric(this.x, this.y, this.groundHeight);
599
+ // Get projected position at ball height
600
+ const ballPos = this.isoScene.toIsometric(this.x, this.y, this.z);
601
+
602
+ // Calculate perspective scaling based on distance from center
603
+ const distanceFromCenter = Math.abs(this.y);
604
+ const maxDistance = this.isoScene.gridSize;
605
+ const depthScale = 0.7 + (distanceFromCenter / maxDistance) * 0.6;
606
+
607
+ // Height factor - higher objects appear slightly smaller
608
+ const heightAboveGround = this.z - this.groundHeight;
609
+ const heightFactor = Math.max(0.7, 1 - Math.abs(heightAboveGround) / 200);
610
+
611
+ // Calculate final radius with perspective
612
+ const radius = (this.baseRadius * this.isoScene.gridSize) / 4 * heightFactor * depthScale;
613
+
614
+ // Shadow properties - shrinks and fades as ball rises above ground
615
+ const shadowScale = Math.max(0.2, 1 - Math.abs(heightAboveGround) / 100);
616
+ const shadowAlpha = Math.max(0.1, 0.3 - Math.abs(heightAboveGround) / 300);
617
+
618
+ // Draw shadow at ground level
619
+ Painter.shapes.fillEllipse(
620
+ shadowPos.x,
621
+ shadowPos.y + radius / 2,
622
+ radius * shadowScale,
623
+ (radius / 2) * shadowScale,
624
+ 0,
625
+ `rgba(0, 0, 0, ${shadowAlpha})`
626
+ );
627
+
628
+ const cx = ballPos.x;
629
+ const cy = ballPos.y;
630
+
631
+ // Draw simple blue marble with light-aware gradient
632
+ ctx.save();
633
+
634
+ // Light direction based on ball rotation (simulates light from top-left)
635
+ // As ball rotates, the lit side shifts
636
+ const lightOffsetX = -0.3 + Math.sin(this.rotationY) * 0.15;
637
+ const lightOffsetY = -0.3 + Math.sin(this.rotationX) * 0.15;
638
+
639
+ // Main gradient - shifts with rotation for lighting effect
640
+ const gradient = ctx.createRadialGradient(
641
+ cx + lightOffsetX * radius, cy + lightOffsetY * radius, 0,
642
+ cx, cy, radius
643
+ );
644
+ gradient.addColorStop(0, "#7ec8e3"); // Bright highlight
645
+ gradient.addColorStop(0.25, "#4a9fd4"); // Light blue
646
+ gradient.addColorStop(0.5, "#2d7ab8"); // Mid blue
647
+ gradient.addColorStop(0.75, "#1a5a8c"); // Darker
648
+ gradient.addColorStop(1, "#0d3a5c"); // Shadow edge
649
+
650
+ ctx.beginPath();
651
+ ctx.arc(cx, cy, radius, 0, Math.PI * 2);
652
+ ctx.fillStyle = gradient;
653
+ ctx.fill();
654
+
655
+ // Specular highlight (small, fixed position for glass look)
656
+ ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
657
+ ctx.beginPath();
658
+ ctx.arc(cx - radius * 0.3, cy - radius * 0.3, radius * 0.1, 0, Math.PI * 2);
659
+ ctx.fill();
660
+
661
+ ctx.restore();
662
+
663
+ // Subtle outline
664
+ ctx.beginPath();
665
+ ctx.arc(cx, cy, radius, 0, Math.PI * 2);
666
+ ctx.strokeStyle = "rgba(0, 40, 80, 0.3)";
667
+ ctx.lineWidth = 1;
668
+ ctx.stroke();
669
+ }
670
+ }
671
+
672
+ /**
673
+ * Main game class demonstrating IsometricScene usage.
674
+ *
675
+ * Creates an isometric scene with a grid and controllable ball.
676
+ * Use WASD to move, Space to jump.
677
+ */
678
+ export class IsometricGame extends Game {
679
+ constructor(canvas) {
680
+ super(canvas);
681
+ this.enableFluidSize();
682
+ this.backgroundColor = "#ecf0f1";
683
+ }
684
+
685
+ /**
686
+ * Initialize the game with isometric scene and objects
687
+ */
688
+ init() {
689
+ super.init();
690
+
691
+ // Create the isometric camera for view rotation
692
+ // Use 90° steps for proper isometric look (45° causes visual flattening)
693
+ this.isoCamera = new IsometricCamera({
694
+ rotationStep: Math.PI / 2, // 90 degrees
695
+ animationDuration: 0.5,
696
+ easing: 'easeOutCubic',
697
+ });
698
+
699
+ // Create the isometric scene centered on the canvas
700
+ this.isoScene = new IsometricScene(this, {
701
+ x: this.width / 2,
702
+ y: this.height / 2,
703
+ tileWidth: CONFIG.tileWidth,
704
+ tileHeight: CONFIG.tileHeight,
705
+ gridSize: CONFIG.gridSize,
706
+ elevationScale: CONFIG.elevationScale,
707
+ depthSort: true,
708
+ camera: this.isoCamera,
709
+ });
710
+
711
+ // Create and add the grid (renders behind everything)
712
+ const grid = new IsometricGrid(this, this.isoScene);
713
+ this.isoScene.add(grid);
714
+
715
+ // Create platforms from config
716
+ this.platforms = [];
717
+ for (const p of CONFIG.platforms) {
718
+ const platform = new IsometricBox(this, this.isoScene, {
719
+ x: p.x,
720
+ y: p.y,
721
+ w: p.w,
722
+ d: p.d,
723
+ h: p.h,
724
+ color: p.color,
725
+ });
726
+ this.platforms.push(platform);
727
+ this.isoScene.add(platform);
728
+ }
729
+
730
+ // Create and add the ball (with platform references for collision)
731
+ this.ball = new Ball(this, this.isoScene, this.platforms);
732
+ this.isoScene.add(this.ball);
733
+
734
+ // Add the scene to the pipeline
735
+ this.pipeline.add(this.isoScene);
736
+
737
+ // Create rotation buttons and keyboard controls
738
+ this.createRotationButtons();
739
+ this.setupKeyboardControls();
740
+ }
741
+
742
+ /**
743
+ * Create arrow buttons for rotating the isometric view
744
+ */
745
+ createRotationButtons() {
746
+ const buttonSize = 50;
747
+ const margin = 20;
748
+
749
+ // Left rotation button (counter-clockwise)
750
+ this.rotateLeftBtn = new Button(this, {
751
+ x: margin + buttonSize / 2,
752
+ y: this.height - margin - buttonSize / 2,
753
+ width: buttonSize,
754
+ height: buttonSize,
755
+ text: "◀",
756
+ font: "24px sans-serif",
757
+ colorDefaultBg: "#2c3e50",
758
+ colorDefaultStroke: "#34495e",
759
+ colorDefaultText: "#ecf0f1",
760
+ colorHoverBg: "#34495e",
761
+ colorHoverStroke: "#3498db",
762
+ colorHoverText: "#3498db",
763
+ colorPressedBg: "#1a252f",
764
+ colorPressedStroke: "#2980b9",
765
+ colorPressedText: "#2980b9",
766
+ onClick: () => this.isoCamera.rotateLeft(),
767
+ });
768
+ this.pipeline.add(this.rotateLeftBtn);
769
+
770
+ // Right rotation button (clockwise)
771
+ this.rotateRightBtn = new Button(this, {
772
+ x: margin + buttonSize * 1.5 + 10,
773
+ y: this.height - margin - buttonSize / 2,
774
+ width: buttonSize,
775
+ height: buttonSize,
776
+ text: "▶",
777
+ font: "24px sans-serif",
778
+ colorDefaultBg: "#2c3e50",
779
+ colorDefaultStroke: "#34495e",
780
+ colorDefaultText: "#ecf0f1",
781
+ colorHoverBg: "#34495e",
782
+ colorHoverStroke: "#3498db",
783
+ colorHoverText: "#3498db",
784
+ colorPressedBg: "#1a252f",
785
+ colorPressedStroke: "#2980b9",
786
+ colorPressedText: "#2980b9",
787
+ onClick: () => this.isoCamera.rotateRight(),
788
+ });
789
+ this.pipeline.add(this.rotateRightBtn);
790
+
791
+ // Angle display text
792
+ this.angleText = new TextShape("0°", {
793
+ font: "bold 18px monospace",
794
+ color: "#2c3e50",
795
+ align: "left",
796
+ baseline: "middle",
797
+ });
798
+ this.angleTextX = margin + buttonSize * 2 + 25;
799
+ this.angleTextY = this.height - margin - buttonSize / 2;
800
+ }
801
+
802
+ /**
803
+ * Render the angle display (called after pipeline)
804
+ */
805
+ render() {
806
+ super.render();
807
+
808
+ // Update and render angle text
809
+ if (this.angleText && this.isoCamera) {
810
+ const degrees = Math.round(this.isoCamera.getAngleDegrees());
811
+ this.angleText.text = `${degrees}°`;
812
+
813
+ Painter.save();
814
+ Painter.translateTo(this.angleTextX, this.angleTextY);
815
+ this.angleText.render();
816
+ Painter.restore();
817
+ }
818
+ }
819
+
820
+ /**
821
+ * Set up keyboard event listeners for camera rotation
822
+ */
823
+ setupKeyboardControls() {
824
+ // Q to rotate left
825
+ this.events.on(Keys.Q, () => {
826
+ this.isoCamera.rotateLeft();
827
+ });
828
+
829
+ // E to rotate right
830
+ this.events.on(Keys.E, () => {
831
+ this.isoCamera.rotateRight();
832
+ });
833
+ }
834
+
835
+ /**
836
+ * Handle window resize to keep scene centered and buttons positioned
837
+ */
838
+ onResize() {
839
+ if (this.isoScene) {
840
+ this.isoScene.x = this.width / 2;
841
+ this.isoScene.y = this.height / 2;
842
+ }
843
+
844
+ // Reposition buttons and angle text
845
+ const buttonSize = 50;
846
+ const margin = 20;
847
+
848
+ if (this.rotateLeftBtn) {
849
+ this.rotateLeftBtn.x = margin + buttonSize / 2;
850
+ this.rotateLeftBtn.y = this.height - margin - buttonSize / 2;
851
+ }
852
+ if (this.rotateRightBtn) {
853
+ this.rotateRightBtn.x = margin + buttonSize * 1.5 + 10;
854
+ this.rotateRightBtn.y = this.height - margin - buttonSize / 2;
855
+ }
856
+
857
+ // Reposition angle text
858
+ this.angleTextX = margin + buttonSize * 2 + 25;
859
+ this.angleTextY = this.height - margin - buttonSize / 2;
860
+ }
861
+ }
862
+
863
+ export default IsometricGame;