@guinetik/gcanvas 1.0.4 → 1.0.5

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 (193) hide show
  1. package/dist/CNAME +1 -0
  2. package/dist/animations.html +31 -0
  3. package/dist/basic.html +38 -0
  4. package/dist/baskara.html +31 -0
  5. package/dist/bezier.html +35 -0
  6. package/dist/beziersignature.html +29 -0
  7. package/dist/blackhole.html +28 -0
  8. package/dist/blob.html +35 -0
  9. package/dist/coordinates.html +698 -0
  10. package/dist/cube3d.html +23 -0
  11. package/dist/demos.css +303 -0
  12. package/dist/dino.html +42 -0
  13. package/dist/easing.html +28 -0
  14. package/dist/events.html +195 -0
  15. package/dist/fluent.html +647 -0
  16. package/dist/fluid-simple.html +22 -0
  17. package/dist/fluid.html +37 -0
  18. package/dist/fractals.html +36 -0
  19. package/dist/gameobjects.html +626 -0
  20. package/dist/gcanvas.es.js +517 -0
  21. package/dist/gcanvas.es.min.js +1 -1
  22. package/dist/gcanvas.umd.js +1 -1
  23. package/dist/gcanvas.umd.min.js +1 -1
  24. package/dist/genart.html +26 -0
  25. package/dist/gendream.html +26 -0
  26. package/dist/group.html +36 -0
  27. package/dist/home.html +587 -0
  28. package/dist/hyperbolic001.html +23 -0
  29. package/dist/hyperbolic002.html +23 -0
  30. package/dist/hyperbolic003.html +23 -0
  31. package/dist/hyperbolic004.html +23 -0
  32. package/dist/hyperbolic005.html +22 -0
  33. package/dist/index.html +398 -0
  34. package/dist/isometric.html +34 -0
  35. package/dist/js/animations.js +452 -0
  36. package/dist/js/basic.js +204 -0
  37. package/dist/js/baskara.js +751 -0
  38. package/dist/js/bezier.js +692 -0
  39. package/dist/js/beziersignature.js +241 -0
  40. package/dist/js/blackhole/accretiondisk.obj.js +379 -0
  41. package/dist/js/blackhole/blackhole.obj.js +318 -0
  42. package/dist/js/blackhole/index.js +409 -0
  43. package/dist/js/blackhole/particle.js +56 -0
  44. package/dist/js/blackhole/starfield.obj.js +218 -0
  45. package/dist/js/blob.js +2276 -0
  46. package/dist/js/coordinates.js +840 -0
  47. package/dist/js/cube3d.js +789 -0
  48. package/dist/js/dino.js +1420 -0
  49. package/dist/js/easing.js +477 -0
  50. package/dist/js/fluent.js +183 -0
  51. package/dist/js/fluid-simple.js +253 -0
  52. package/dist/js/fluid.js +527 -0
  53. package/dist/js/fractals.js +932 -0
  54. package/dist/js/fractalworker.js +93 -0
  55. package/dist/js/gameobjects.js +176 -0
  56. package/dist/js/genart.js +268 -0
  57. package/dist/js/gendream.js +209 -0
  58. package/dist/js/group.js +140 -0
  59. package/dist/js/hyperbolic001.js +310 -0
  60. package/dist/js/hyperbolic002.js +388 -0
  61. package/dist/js/hyperbolic003.js +319 -0
  62. package/dist/js/hyperbolic004.js +345 -0
  63. package/dist/js/hyperbolic005.js +340 -0
  64. package/dist/js/info-toggle.js +25 -0
  65. package/dist/js/isometric.js +863 -0
  66. package/dist/js/kerr.js +1547 -0
  67. package/dist/js/lavalamp.js +590 -0
  68. package/dist/js/layout.js +354 -0
  69. package/dist/js/mondrian.js +285 -0
  70. package/dist/js/opacity.js +275 -0
  71. package/dist/js/painter.js +484 -0
  72. package/dist/js/particles-showcase.js +514 -0
  73. package/dist/js/particles.js +299 -0
  74. package/dist/js/patterns.js +397 -0
  75. package/dist/js/penrose/artifact.js +69 -0
  76. package/dist/js/penrose/blackhole.js +121 -0
  77. package/dist/js/penrose/constants.js +73 -0
  78. package/dist/js/penrose/game.js +943 -0
  79. package/dist/js/penrose/lore.js +278 -0
  80. package/dist/js/penrose/penrosescene.js +892 -0
  81. package/dist/js/penrose/ship.js +216 -0
  82. package/dist/js/penrose/sounds.js +211 -0
  83. package/dist/js/penrose/voidparticle.js +55 -0
  84. package/dist/js/penrose/voidscene.js +258 -0
  85. package/dist/js/penrose/voidship.js +144 -0
  86. package/dist/js/penrose/wormhole.js +46 -0
  87. package/dist/js/pipeline.js +555 -0
  88. package/dist/js/plane3d.js +256 -0
  89. package/dist/js/platformer.js +1579 -0
  90. package/dist/js/scene.js +304 -0
  91. package/dist/js/scenes.js +320 -0
  92. package/dist/js/schrodinger.js +410 -0
  93. package/dist/js/schwarzschild.js +1015 -0
  94. package/dist/js/shapes.js +628 -0
  95. package/dist/js/space/alien.js +171 -0
  96. package/dist/js/space/boom.js +98 -0
  97. package/dist/js/space/boss.js +353 -0
  98. package/dist/js/space/buff.js +73 -0
  99. package/dist/js/space/bullet.js +102 -0
  100. package/dist/js/space/constants.js +85 -0
  101. package/dist/js/space/game.js +1884 -0
  102. package/dist/js/space/hud.js +112 -0
  103. package/dist/js/space/laserbeam.js +179 -0
  104. package/dist/js/space/lightning.js +277 -0
  105. package/dist/js/space/minion.js +192 -0
  106. package/dist/js/space/missile.js +212 -0
  107. package/dist/js/space/player.js +430 -0
  108. package/dist/js/space/powerup.js +90 -0
  109. package/dist/js/space/starfield.js +58 -0
  110. package/dist/js/space/starpower.js +90 -0
  111. package/dist/js/spacetime.js +559 -0
  112. package/dist/js/sphere3d.js +229 -0
  113. package/dist/js/sprite.js +473 -0
  114. package/dist/js/starfaux/config.js +118 -0
  115. package/dist/js/starfaux/enemy.js +353 -0
  116. package/dist/js/starfaux/hud.js +78 -0
  117. package/dist/js/starfaux/index.js +482 -0
  118. package/dist/js/starfaux/laser.js +182 -0
  119. package/dist/js/starfaux/player.js +468 -0
  120. package/dist/js/starfaux/terrain.js +560 -0
  121. package/dist/js/study001.js +275 -0
  122. package/dist/js/study002.js +366 -0
  123. package/dist/js/study003.js +331 -0
  124. package/dist/js/study004.js +389 -0
  125. package/dist/js/study005.js +209 -0
  126. package/dist/js/study006.js +194 -0
  127. package/dist/js/study007.js +192 -0
  128. package/dist/js/study008.js +413 -0
  129. package/dist/js/svgtween.js +204 -0
  130. package/dist/js/tde/accretiondisk.js +471 -0
  131. package/dist/js/tde/blackhole.js +219 -0
  132. package/dist/js/tde/blackholescene.js +209 -0
  133. package/dist/js/tde/config.js +59 -0
  134. package/dist/js/tde/index.js +820 -0
  135. package/dist/js/tde/jets.js +290 -0
  136. package/dist/js/tde/lensedstarfield.js +154 -0
  137. package/dist/js/tde/tdestar.js +297 -0
  138. package/dist/js/tde/tidalstream.js +372 -0
  139. package/dist/js/tde_old/blackhole.obj.js +354 -0
  140. package/dist/js/tde_old/debris.obj.js +791 -0
  141. package/dist/js/tde_old/flare.obj.js +239 -0
  142. package/dist/js/tde_old/index.js +448 -0
  143. package/dist/js/tde_old/star.obj.js +812 -0
  144. package/dist/js/tetris/config.js +157 -0
  145. package/dist/js/tetris/grid.js +286 -0
  146. package/dist/js/tetris/index.js +1195 -0
  147. package/dist/js/tetris/renderer.js +634 -0
  148. package/dist/js/tetris/tetrominos.js +280 -0
  149. package/dist/js/tiles.js +312 -0
  150. package/dist/js/tweendemo.js +79 -0
  151. package/dist/js/visibility.js +102 -0
  152. package/dist/kerr.html +28 -0
  153. package/dist/lavalamp.html +27 -0
  154. package/dist/layouts.html +37 -0
  155. package/dist/logo.svg +4 -0
  156. package/dist/loop.html +84 -0
  157. package/dist/mondrian.html +32 -0
  158. package/dist/og_image.png +0 -0
  159. package/dist/opacity.html +36 -0
  160. package/dist/painter.html +39 -0
  161. package/dist/particles-showcase.html +28 -0
  162. package/dist/particles.html +24 -0
  163. package/dist/patterns.html +33 -0
  164. package/dist/penrose-game.html +31 -0
  165. package/dist/pipeline.html +737 -0
  166. package/dist/plane3d.html +24 -0
  167. package/dist/platformer.html +43 -0
  168. package/dist/scene.html +33 -0
  169. package/dist/scenes.html +96 -0
  170. package/dist/schrodinger.html +27 -0
  171. package/dist/schwarzschild.html +27 -0
  172. package/dist/shapes.html +16 -0
  173. package/dist/space.html +85 -0
  174. package/dist/spacetime.html +27 -0
  175. package/dist/sphere3d.html +24 -0
  176. package/dist/sprite.html +18 -0
  177. package/dist/starfaux.html +22 -0
  178. package/dist/study001.html +23 -0
  179. package/dist/study002.html +23 -0
  180. package/dist/study003.html +23 -0
  181. package/dist/study004.html +23 -0
  182. package/dist/study005.html +22 -0
  183. package/dist/study006.html +24 -0
  184. package/dist/study007.html +24 -0
  185. package/dist/study008.html +22 -0
  186. package/dist/svgtween.html +29 -0
  187. package/dist/tde.html +28 -0
  188. package/dist/tetris3d.html +25 -0
  189. package/dist/tiles.html +28 -0
  190. package/dist/transforms.html +400 -0
  191. package/dist/tween.html +45 -0
  192. package/dist/visibility.html +33 -0
  193. package/package.json +1 -1
@@ -0,0 +1,634 @@
1
+ /**
2
+ * Renderer - Handles 3D rendering of well, pieces, and grid
3
+ */
4
+
5
+ import { Cube3D, Tweenetik, Easing } from "/gcanvas.es.min.js";
6
+ import { CONFIG } from "./config.js";
7
+
8
+ /**
9
+ * Convert grid coordinates to world 3D coordinates
10
+ * Grid origin (0,0,0) is at front-top-left of well
11
+ * World origin is at center of well
12
+ *
13
+ * @param {number} gridX
14
+ * @param {number} gridY
15
+ * @param {number} gridZ
16
+ * @param {number} cubeSize - Dynamic cube size
17
+ * @param {number} cubeGap - Dynamic gap size
18
+ * @returns {{x: number, y: number, z: number}}
19
+ */
20
+ export function gridToWorld(gridX, gridY, gridZ, cubeSize, cubeGap) {
21
+ const { width, depth, height } = CONFIG.grid;
22
+ const size = cubeSize + cubeGap;
23
+
24
+ // Center the grid around origin
25
+ const halfWidth = (width * size) / 2;
26
+ const halfDepth = (depth * size) / 2;
27
+ const halfHeight = (height * size) / 2;
28
+
29
+ return {
30
+ x: gridX * size - halfWidth + size / 2,
31
+ y: gridY * size - halfHeight + size / 2,
32
+ z: gridZ * size - halfDepth + size / 2,
33
+ };
34
+ }
35
+
36
+ /**
37
+ * WellRenderer - Renders the wireframe well boundary
38
+ */
39
+ export class WellRenderer {
40
+ constructor(camera, cubeSize, cubeGap) {
41
+ this.camera = camera;
42
+ this.cubeSize = cubeSize;
43
+ this.cubeGap = cubeGap;
44
+
45
+ this._updateDimensions();
46
+ }
47
+
48
+ /**
49
+ * Update dimensions when size changes
50
+ * @param {number} cubeSize
51
+ * @param {number} cubeGap
52
+ */
53
+ updateSize(cubeSize, cubeGap) {
54
+ this.cubeSize = cubeSize;
55
+ this.cubeGap = cubeGap;
56
+ this._updateDimensions();
57
+ }
58
+
59
+ /**
60
+ * Recalculate dimensions
61
+ * @private
62
+ */
63
+ _updateDimensions() {
64
+ const { width, depth, height } = CONFIG.grid;
65
+ const size = this.cubeSize + this.cubeGap;
66
+
67
+ // Calculate well dimensions in world space
68
+ this.wellWidth = width * size;
69
+ this.wellDepth = depth * size;
70
+ this.wellHeight = height * size;
71
+
72
+ // Corner positions
73
+ this.corners = this._calculateCorners();
74
+ }
75
+
76
+ /**
77
+ * Calculate the 8 corners of the well
78
+ * @private
79
+ */
80
+ _calculateCorners() {
81
+ const hw = this.wellWidth / 2;
82
+ const hd = this.wellDepth / 2;
83
+ const hh = this.wellHeight / 2;
84
+
85
+ return {
86
+ // Top face corners
87
+ topFrontLeft: { x: -hw, y: -hh, z: -hd },
88
+ topFrontRight: { x: hw, y: -hh, z: -hd },
89
+ topBackLeft: { x: -hw, y: -hh, z: hd },
90
+ topBackRight: { x: hw, y: -hh, z: hd },
91
+ // Bottom face corners
92
+ bottomFrontLeft: { x: -hw, y: hh, z: -hd },
93
+ bottomFrontRight: { x: hw, y: hh, z: -hd },
94
+ bottomBackLeft: { x: -hw, y: hh, z: hd },
95
+ bottomBackRight: { x: hw, y: hh, z: hd },
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Draw a 3D line from p1 to p2
101
+ * @private
102
+ */
103
+ _drawLine3D(ctx, centerX, centerY, p1, p2, color, lineWidth) {
104
+ const proj1 = this.camera.project(p1.x, p1.y, p1.z);
105
+ const proj2 = this.camera.project(p2.x, p2.y, p2.z);
106
+
107
+ // Don't draw if either point is behind camera
108
+ if (
109
+ proj1.z < -this.camera.perspective + 50 ||
110
+ proj2.z < -this.camera.perspective + 50
111
+ ) {
112
+ return;
113
+ }
114
+
115
+ ctx.beginPath();
116
+ ctx.moveTo(centerX + proj1.x, centerY + proj1.y);
117
+ ctx.lineTo(centerX + proj2.x, centerY + proj2.y);
118
+ ctx.strokeStyle = color;
119
+ ctx.lineWidth = lineWidth;
120
+ ctx.stroke();
121
+ }
122
+
123
+ /**
124
+ * Render the well wireframe
125
+ * @param {CanvasRenderingContext2D} ctx
126
+ * @param {number} centerX - Screen center X
127
+ * @param {number} centerY - Screen center Y
128
+ */
129
+ render(ctx, centerX, centerY) {
130
+ const c = this.corners;
131
+ const color = CONFIG.visual.wellColor;
132
+ const lineWidth = CONFIG.visual.wellLineWidth;
133
+
134
+ // Draw all 12 edges of the wireframe box
135
+ // Vertical edges (4)
136
+ this._drawLine3D(
137
+ ctx,
138
+ centerX,
139
+ centerY,
140
+ c.topFrontLeft,
141
+ c.bottomFrontLeft,
142
+ color,
143
+ lineWidth
144
+ );
145
+ this._drawLine3D(
146
+ ctx,
147
+ centerX,
148
+ centerY,
149
+ c.topFrontRight,
150
+ c.bottomFrontRight,
151
+ color,
152
+ lineWidth
153
+ );
154
+ this._drawLine3D(
155
+ ctx,
156
+ centerX,
157
+ centerY,
158
+ c.topBackLeft,
159
+ c.bottomBackLeft,
160
+ color,
161
+ lineWidth
162
+ );
163
+ this._drawLine3D(
164
+ ctx,
165
+ centerX,
166
+ centerY,
167
+ c.topBackRight,
168
+ c.bottomBackRight,
169
+ color,
170
+ lineWidth
171
+ );
172
+
173
+ // Top edges (4)
174
+ this._drawLine3D(
175
+ ctx,
176
+ centerX,
177
+ centerY,
178
+ c.topFrontLeft,
179
+ c.topFrontRight,
180
+ color,
181
+ lineWidth
182
+ );
183
+ this._drawLine3D(
184
+ ctx,
185
+ centerX,
186
+ centerY,
187
+ c.topFrontRight,
188
+ c.topBackRight,
189
+ color,
190
+ lineWidth
191
+ );
192
+ this._drawLine3D(
193
+ ctx,
194
+ centerX,
195
+ centerY,
196
+ c.topBackRight,
197
+ c.topBackLeft,
198
+ color,
199
+ lineWidth
200
+ );
201
+ this._drawLine3D(
202
+ ctx,
203
+ centerX,
204
+ centerY,
205
+ c.topBackLeft,
206
+ c.topFrontLeft,
207
+ color,
208
+ lineWidth
209
+ );
210
+
211
+ // Bottom edges (4)
212
+ this._drawLine3D(
213
+ ctx,
214
+ centerX,
215
+ centerY,
216
+ c.bottomFrontLeft,
217
+ c.bottomFrontRight,
218
+ color,
219
+ lineWidth
220
+ );
221
+ this._drawLine3D(
222
+ ctx,
223
+ centerX,
224
+ centerY,
225
+ c.bottomFrontRight,
226
+ c.bottomBackRight,
227
+ color,
228
+ lineWidth
229
+ );
230
+ this._drawLine3D(
231
+ ctx,
232
+ centerX,
233
+ centerY,
234
+ c.bottomBackRight,
235
+ c.bottomBackLeft,
236
+ color,
237
+ lineWidth
238
+ );
239
+ this._drawLine3D(
240
+ ctx,
241
+ centerX,
242
+ centerY,
243
+ c.bottomBackLeft,
244
+ c.bottomFrontLeft,
245
+ color,
246
+ lineWidth
247
+ );
248
+
249
+ // Draw floor grid
250
+ this._drawFloorGrid(ctx, centerX, centerY);
251
+ }
252
+
253
+ /**
254
+ * Draw grid lines on the floor of the well
255
+ * @private
256
+ */
257
+ _drawFloorGrid(ctx, centerX, centerY) {
258
+ const { width, depth } = CONFIG.grid;
259
+ const size = CONFIG.visual.cubeSize + CONFIG.visual.cubeGap;
260
+ const hw = this.wellWidth / 2;
261
+ const hd = this.wellDepth / 2;
262
+ const hh = this.wellHeight / 2;
263
+ const floorY = hh;
264
+
265
+ const gridColor = "rgba(0, 255, 65, 0.2)";
266
+ const gridWidth = 0.5;
267
+
268
+ // X direction lines
269
+ for (let i = 0; i <= width; i++) {
270
+ const x = -hw + i * size;
271
+ this._drawLine3D(
272
+ ctx,
273
+ centerX,
274
+ centerY,
275
+ { x: x, y: floorY, z: -hd },
276
+ { x: x, y: floorY, z: hd },
277
+ gridColor,
278
+ gridWidth
279
+ );
280
+ }
281
+
282
+ // Z direction lines
283
+ for (let i = 0; i <= depth; i++) {
284
+ const z = -hd + i * size;
285
+ this._drawLine3D(
286
+ ctx,
287
+ centerX,
288
+ centerY,
289
+ { x: -hw, y: floorY, z: z },
290
+ { x: hw, y: floorY, z: z },
291
+ gridColor,
292
+ gridWidth
293
+ );
294
+ }
295
+ }
296
+ }
297
+
298
+ /**
299
+ * BlockRenderer - Renders cubes for pieces and grid blocks
300
+ * Uses a pool of Cube3D instances for efficiency
301
+ */
302
+ export class BlockRenderer {
303
+ constructor(camera, cubeSize, cubeGap) {
304
+ this.camera = camera;
305
+ this.cubeSize = cubeSize;
306
+ this.cubeGap = cubeGap;
307
+
308
+ // Cube pools - we reuse cubes to avoid constant allocation
309
+ this.pieceCubes = [];
310
+ this.ghostCubes = [];
311
+ this.gridCubes = [];
312
+ this.hintCubes = [];
313
+
314
+ // Track what we're rendering
315
+ this.pieceData = null;
316
+ this.ghostData = null;
317
+ this.gridData = [];
318
+ this.hintData = null;
319
+ }
320
+
321
+ /**
322
+ * Update size when screen changes
323
+ * @param {number} cubeSize
324
+ * @param {number} cubeGap
325
+ */
326
+ updateSize(cubeSize, cubeGap) {
327
+ this.cubeSize = cubeSize;
328
+ this.cubeGap = cubeGap;
329
+ }
330
+
331
+ /**
332
+ * Create a cube at the given grid position
333
+ * @param {number} gridX
334
+ * @param {number} gridY
335
+ * @param {number} gridZ
336
+ * @param {string} color
337
+ * @param {number} opacity
338
+ * @returns {Cube3D}
339
+ * @private
340
+ */
341
+ _createCube(gridX, gridY, gridZ, color, opacity = 1.0) {
342
+ const world = gridToWorld(gridX, gridY, gridZ, this.cubeSize, this.cubeGap);
343
+
344
+ const cube = new Cube3D(this.cubeSize, {
345
+ x: world.x,
346
+ y: world.y,
347
+ z: world.z,
348
+ camera: this.camera,
349
+ faceColors: {
350
+ front: color,
351
+ back: color,
352
+ top: color,
353
+ bottom: color,
354
+ left: color,
355
+ right: color,
356
+ },
357
+ stickerMode: CONFIG.visual.stickerMode,
358
+ stickerMargin: CONFIG.visual.stickerMargin,
359
+ stickerBackgroundColor: CONFIG.visual.stickerBackgroundColor,
360
+ stroke: CONFIG.visual.wellColor,
361
+ lineWidth: 0.5,
362
+ });
363
+
364
+ cube._opacity = opacity;
365
+
366
+ return cube;
367
+ }
368
+
369
+ /**
370
+ * Update cubes for the active piece
371
+ * @param {TetrisPiece|null} piece
372
+ */
373
+ updatePiece(piece) {
374
+ if (!piece) {
375
+ this.pieceData = null;
376
+ this.pieceCubes = [];
377
+ return;
378
+ }
379
+
380
+ this.pieceData = {
381
+ positions: piece.getWorldPositions(),
382
+ color: piece.color,
383
+ };
384
+
385
+ // Rebuild cubes
386
+ this.pieceCubes = this.pieceData.positions.map((pos) =>
387
+ this._createCube(pos.x, pos.y, pos.z, this.pieceData.color, 1.0)
388
+ );
389
+ }
390
+
391
+ /**
392
+ * Update the ghost piece (landing preview)
393
+ * @param {TetrisPiece|null} piece
394
+ * @param {number} landingY
395
+ */
396
+ updateGhost(piece, landingY) {
397
+ if (!piece || landingY === piece.y) {
398
+ this.ghostData = null;
399
+ this.ghostCubes = [];
400
+ return;
401
+ }
402
+
403
+ // Calculate ghost positions using voxels
404
+ const ghostPositions = piece.voxels.map((v) => ({
405
+ x: piece.x + v.x,
406
+ y: landingY + v.y,
407
+ z: piece.z + v.z,
408
+ }));
409
+
410
+ this.ghostData = {
411
+ positions: ghostPositions,
412
+ color: piece.color,
413
+ };
414
+
415
+ // Rebuild ghost cubes with transparency
416
+ this.ghostCubes = this.ghostData.positions.map((pos) =>
417
+ this._createCube(
418
+ pos.x,
419
+ pos.y,
420
+ pos.z,
421
+ this.ghostData.color,
422
+ CONFIG.visual.ghostAlpha
423
+ )
424
+ );
425
+ }
426
+
427
+ /**
428
+ * Update cubes for all locked grid blocks
429
+ * @param {Array<{x: number, y: number, z: number, color: string}>} filledCells
430
+ * @param {Array<{x: number, y: number, z: number}>} [newPositions] - Newly placed positions to animate
431
+ */
432
+ updateGrid(filledCells, newPositions = null) {
433
+ this.gridData = filledCells;
434
+
435
+ // Rebuild grid cubes
436
+ this.gridCubes = filledCells.map((cell) =>
437
+ this._createCube(cell.x, cell.y, cell.z, cell.color, 1.0)
438
+ );
439
+
440
+ // Animate newly placed cubes with bounce
441
+ if (newPositions && newPositions.length > 0) {
442
+ this._animateDropBounce(newPositions);
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Animate a bounce effect on newly placed cubes
448
+ * @param {Array<{x: number, y: number, z: number}>} positions
449
+ * @private
450
+ */
451
+ _animateDropBounce(positions) {
452
+ for (const pos of positions) {
453
+ // Find the cube at this position
454
+ const cube = this.gridCubes.find((c) => {
455
+ const world = gridToWorld(pos.x, pos.y, pos.z, this.cubeSize, this.cubeGap);
456
+ return (
457
+ Math.abs(c.x - world.x) < 0.1 &&
458
+ Math.abs(c.y - world.y) < 0.1 &&
459
+ Math.abs(c.z - world.z) < 0.1
460
+ );
461
+ });
462
+
463
+ if (cube) {
464
+ const finalY = cube.y;
465
+ cube.y = finalY - this.cubeSize;
466
+ Tweenetik.to(cube, { y: finalY }, 0.3, Easing.easeOutBounce);
467
+ }
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Update hint ghost cubes (optimal position preview)
473
+ * @param {Array<{x: number, y: number, z: number}>|null} positions
474
+ */
475
+ updateHint(positions) {
476
+ if (!positions || positions.length === 0) {
477
+ this.hintData = null;
478
+ this.hintCubes = [];
479
+ return;
480
+ }
481
+
482
+ this.hintData = { positions };
483
+
484
+ // Create hint cubes with distinct gold/yellow color
485
+ const hintColor = "#FFD700";
486
+ this.hintCubes = positions.map((pos) =>
487
+ this._createCube(pos.x, pos.y, pos.z, hintColor, 0.4)
488
+ );
489
+ }
490
+
491
+ /**
492
+ * Collect all cubes and sort by depth for proper rendering
493
+ * @returns {Cube3D[]}
494
+ */
495
+ getSortedCubes() {
496
+ const allCubes = [...this.gridCubes, ...this.hintCubes, ...this.ghostCubes, ...this.pieceCubes];
497
+
498
+ // Calculate depth for each cube
499
+ const cubesWithDepth = allCubes.map((cube) => {
500
+ const proj = this.camera.project(cube.x, cube.y, cube.z);
501
+ return { cube, depth: proj.z };
502
+ });
503
+
504
+ // Sort back to front (larger z = further away)
505
+ cubesWithDepth.sort((a, b) => b.depth - a.depth);
506
+
507
+ return cubesWithDepth.map((item) => item.cube);
508
+ }
509
+
510
+ /**
511
+ * Render all cubes
512
+ * @param {CanvasRenderingContext2D} ctx
513
+ * @param {number} centerX
514
+ * @param {number} centerY
515
+ */
516
+ render(ctx, centerX, centerY) {
517
+ const sortedCubes = this.getSortedCubes();
518
+
519
+ ctx.save();
520
+ ctx.translate(centerX, centerY);
521
+
522
+ for (const cube of sortedCubes) {
523
+ if (cube._opacity < 1.0) {
524
+ ctx.globalAlpha = cube._opacity;
525
+ }
526
+ cube.draw();
527
+ ctx.globalAlpha = 1.0;
528
+ }
529
+
530
+ ctx.restore();
531
+ }
532
+ }
533
+
534
+ /**
535
+ * NextPieceRenderer - Renders the preview of the next piece
536
+ */
537
+ export class NextPieceRenderer {
538
+ constructor(camera, cubeSize) {
539
+ // Create a separate camera for the preview
540
+ this.previewCamera = camera;
541
+ this.cubeSize = cubeSize;
542
+ this.cubes = [];
543
+ this.pieceType = null;
544
+ this.currentColor = null;
545
+ this.currentMatrix = null;
546
+ }
547
+
548
+ /**
549
+ * Update size
550
+ * @param {number} cubeSize
551
+ */
552
+ updateSize(cubeSize) {
553
+ this.cubeSize = cubeSize;
554
+ // Rebuild cubes with new size if we have piece data
555
+ if (this.pieceType && this.currentMatrix) {
556
+ this._rebuildCubes(this.currentColor, this.currentMatrix);
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Rebuild cubes with current settings
562
+ * @private
563
+ */
564
+ _rebuildCubes(color, matrix) {
565
+ this.cubes = [];
566
+ const size = this.cubeSize * 0.6;
567
+ const rows = matrix.length;
568
+ const cols = matrix[0].length;
569
+
570
+ // Center the preview piece
571
+ const offsetX = (cols * size) / 2;
572
+ const offsetZ = (rows * size) / 2;
573
+
574
+ for (let z = 0; z < rows; z++) {
575
+ for (let x = 0; x < cols; x++) {
576
+ if (matrix[z][x]) {
577
+ const cube = new Cube3D(size, {
578
+ x: x * size - offsetX + size / 2,
579
+ y: 0,
580
+ z: z * size - offsetZ + size / 2,
581
+ camera: this.previewCamera,
582
+ faceColors: {
583
+ front: color,
584
+ back: color,
585
+ top: color,
586
+ bottom: color,
587
+ left: color,
588
+ right: color,
589
+ },
590
+ stickerMode: true,
591
+ stickerMargin: CONFIG.visual.stickerMargin,
592
+ stickerBackgroundColor: CONFIG.visual.stickerBackgroundColor,
593
+ stroke: CONFIG.visual.wellColor,
594
+ lineWidth: 0.5,
595
+ });
596
+ this.cubes.push(cube);
597
+ }
598
+ }
599
+ }
600
+ }
601
+
602
+ /**
603
+ * Update the preview piece
604
+ * @param {string} pieceType
605
+ * @param {string} color
606
+ * @param {number[][]} matrix
607
+ */
608
+ update(pieceType, color, matrix) {
609
+ if (this.pieceType === pieceType) return;
610
+
611
+ this.pieceType = pieceType;
612
+ this.currentColor = color;
613
+ this.currentMatrix = matrix;
614
+
615
+ this._rebuildCubes(color, matrix);
616
+ }
617
+
618
+ /**
619
+ * Render the preview
620
+ * @param {CanvasRenderingContext2D} ctx
621
+ * @param {number} x - Screen X position
622
+ * @param {number} y - Screen Y position
623
+ */
624
+ render(ctx, x, y) {
625
+ ctx.save();
626
+ ctx.translate(x, y);
627
+
628
+ for (const cube of this.cubes) {
629
+ cube.draw();
630
+ }
631
+
632
+ ctx.restore();
633
+ }
634
+ }