@guinetik/gcanvas 1.0.4 → 2.0.0

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