@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,789 @@
1
+ import {
2
+ Game,
3
+ Cube3D,
4
+ FPSCounter,
5
+ Button,
6
+ Scene,
7
+ Text,
8
+ Position,
9
+ verticalLayout,
10
+ applyLayout,
11
+ } from "/gcanvas.es.min.js";
12
+ import { Camera3D } from "/gcanvas.es.min.js";
13
+
14
+ /**
15
+ * Configuration for the Rubik's Cube demo
16
+ * Sizes are calculated dynamically based on screen size
17
+ */
18
+ const CONFIG = {
19
+ // Base sizes (will be scaled)
20
+ baseCubeletSize: 0.07, // fraction of min(width, height)
21
+ baseGap: 0.004, // fraction of min(width, height)
22
+
23
+ camera: {
24
+ perspective: 800,
25
+ rotationX: 0.4,
26
+ rotationY: -0.5,
27
+ inertia: true,
28
+ friction: 0.95,
29
+ clampX: false, // Allow full rotation
30
+ },
31
+
32
+ selfRotation: {
33
+ speed: 0.25, // radians per second (gentle rotation)
34
+ pauseDuringAnimation: true, // pause global rotation during layer animation
35
+ },
36
+
37
+ // Standard Rubik's cube colors
38
+ colors: {
39
+ white: "#FFFFFF",
40
+ yellow: "#FFD500",
41
+ red: "#B71234",
42
+ orange: "#FF5800",
43
+ blue: "#0046AD",
44
+ green: "#009B48",
45
+ black: "#0A0A0A", // interior faces
46
+ },
47
+
48
+ // Terminal aesthetic - sticker mode with green grid
49
+ sticker: {
50
+ enabled: true,
51
+ margin: 0.12, // Sticker inset as fraction of face
52
+ backgroundColor: "#0A0A0A", // Black plastic
53
+ strokeColor: "#00FF41", // Terminal green
54
+ lineWidth: 1.5,
55
+ },
56
+
57
+ // Layer rotation animation
58
+ layerAnimation: {
59
+ duration: 0.3, // seconds per layer rotation
60
+ shuffleMoves: 20, // number of random moves in shuffle
61
+ shuffleDelay: 0.05, // delay between shuffle moves (seconds)
62
+ },
63
+ };
64
+
65
+ /**
66
+ * CubeletData - Holds cubelet and its grid position + face colors
67
+ */
68
+ class CubeletData {
69
+ constructor(cubelet, gridX, gridY, gridZ, faceColors) {
70
+ this.cubelet = cubelet;
71
+ this.gridX = gridX;
72
+ this.gridY = gridY;
73
+ this.gridZ = gridZ;
74
+ // Store face colors separately for rotation tracking
75
+ this.faceColors = { ...faceColors };
76
+ // Extra rotation for layer animation
77
+ this.layerRotationX = 0;
78
+ this.layerRotationY = 0;
79
+ this.layerRotationZ = 0;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Layer rotation move definition
85
+ */
86
+ class LayerMove {
87
+ constructor(axis, layer, direction) {
88
+ this.axis = axis; // 'x', 'y', or 'z'
89
+ this.layer = layer; // -1, 0, or 1
90
+ this.direction = direction; // 1 (CW) or -1 (CCW)
91
+ }
92
+ }
93
+
94
+ /**
95
+ * RubiksCubeDemo - Showcases Cube3D with a 3x3x3 Rubik's cube
96
+ * Features layer rotation and shuffle animation
97
+ */
98
+ class RubiksCubeDemo extends Game {
99
+ constructor(canvas) {
100
+ super(canvas);
101
+ this.backgroundColor = "#000000";
102
+ this.enableFluidSize();
103
+ }
104
+
105
+ init() {
106
+ super.init();
107
+
108
+ // Create camera with mouse controls
109
+ this.camera = new Camera3D({
110
+ perspective: CONFIG.camera.perspective,
111
+ rotationX: CONFIG.camera.rotationX,
112
+ rotationY: CONFIG.camera.rotationY,
113
+ inertia: CONFIG.camera.inertia,
114
+ friction: CONFIG.camera.friction,
115
+ clampX: CONFIG.camera.clampX,
116
+ });
117
+ this.camera.enableMouseControl(this.canvas);
118
+
119
+ // Calculate initial sizes based on screen
120
+ this._updateSizes();
121
+
122
+ // Create 3x3x3 grid of cubelets
123
+ this._createCubelets();
124
+
125
+ // Global self-rotation angle (shared by all cubelets)
126
+ this.globalRotationY = 0;
127
+
128
+ // Layer animation state
129
+ this.animatingLayer = null; // Current LayerMove being animated
130
+ this.animationProgress = 0; // 0 to 1
131
+ this.animationQueue = []; // Queue of moves to perform
132
+ this.moveHistory = []; // Track all moves for solving
133
+ this.isSolving = false; // Don't track moves during solve
134
+
135
+ // Create buttons
136
+ this.shuffleButton = new Button(this, {
137
+ text: "SHUFFLE",
138
+ width: 100,
139
+ height: 40,
140
+ font: "bold 14px monospace",
141
+ onClick: () => this._startShuffle(),
142
+ });
143
+
144
+ this.solveButton = new Button(this, {
145
+ text: "SOLVE",
146
+ width: 100,
147
+ height: 40,
148
+ font: "bold 14px monospace",
149
+ onClick: () => this._startSolve(),
150
+ });
151
+
152
+ // Layout buttons vertically and add to anchored scene
153
+ const buttons = [this.shuffleButton, this.solveButton];
154
+ const layout = verticalLayout(buttons, { spacing: 10 });
155
+ applyLayout(buttons, layout.positions);
156
+
157
+ this.buttonPanel = new Scene(this, {
158
+ anchor: "bottom-left",
159
+ width: 100,
160
+ height: 100,
161
+ anchorOffsetY: -50,
162
+ });
163
+ buttons.forEach(btn => this.buttonPanel.add(btn));
164
+ this.pipeline.add(this.buttonPanel);
165
+
166
+ // Add FPS counter
167
+ this.pipeline.add(
168
+ new FPSCounter(this, {
169
+ anchor: "bottom-right",
170
+ })
171
+ );
172
+
173
+ // Camera rotation text
174
+ this.cameraText = new Text(this, "", {
175
+ font: "12px monospace",
176
+ anchor: Position.BOTTOM_CENTER,
177
+ color: "#00FF41",
178
+ });
179
+ this.pipeline.add(this.cameraText);
180
+ }
181
+
182
+ /**
183
+ * Calculate sizes based on current screen dimensions
184
+ */
185
+ _updateSizes() {
186
+ const minDim = Math.min(this.width, this.height);
187
+ this.cubeletSize = minDim * CONFIG.baseCubeletSize;
188
+ this.gap = minDim * CONFIG.baseGap;
189
+ }
190
+
191
+ /**
192
+ * Create or recreate the 3x3x3 grid of cubelets
193
+ */
194
+ _createCubelets() {
195
+ this.cubelets = [];
196
+
197
+ for (let gx = -1; gx <= 1; gx++) {
198
+ for (let gy = -1; gy <= 1; gy++) {
199
+ for (let gz = -1; gz <= 1; gz++) {
200
+ const faceColors = this._getFaceColors(gx, gy, gz);
201
+ const cubelet = this._createCubelet(gx, gy, gz, faceColors);
202
+ this.cubelets.push(new CubeletData(cubelet, gx, gy, gz, faceColors));
203
+ }
204
+ }
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Get face colors based on grid position
210
+ */
211
+ _getFaceColors(gridX, gridY, gridZ) {
212
+ return {
213
+ front: gridZ === -1 ? CONFIG.colors.red : CONFIG.colors.black,
214
+ back: gridZ === 1 ? CONFIG.colors.orange : CONFIG.colors.black,
215
+ top: gridY === -1 ? CONFIG.colors.white : CONFIG.colors.black,
216
+ bottom: gridY === 1 ? CONFIG.colors.yellow : CONFIG.colors.black,
217
+ left: gridX === -1 ? CONFIG.colors.green : CONFIG.colors.black,
218
+ right: gridX === 1 ? CONFIG.colors.blue : CONFIG.colors.black,
219
+ };
220
+ }
221
+
222
+ /**
223
+ * Handle window resize
224
+ */
225
+ onResize() {
226
+ this._updateSizes();
227
+
228
+ // Recreate cubelets with new sizes but preserve colors
229
+ if (this.cubelets) {
230
+ const currentRotation = this.globalRotationY;
231
+ const savedColors = this.cubelets.map(d => ({
232
+ gridX: d.gridX,
233
+ gridY: d.gridY,
234
+ gridZ: d.gridZ,
235
+ faceColors: { ...d.faceColors }
236
+ }));
237
+
238
+ this.cubelets = [];
239
+ for (const saved of savedColors) {
240
+ const cubelet = this._createCubelet(
241
+ saved.gridX, saved.gridY, saved.gridZ, saved.faceColors
242
+ );
243
+ this.cubelets.push(new CubeletData(
244
+ cubelet, saved.gridX, saved.gridY, saved.gridZ, saved.faceColors
245
+ ));
246
+ }
247
+
248
+ // Restore rotation
249
+ for (const data of this.cubelets) {
250
+ data.cubelet.selfRotationY = currentRotation;
251
+ }
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Create a single cubelet at the given grid position with specified colors
257
+ */
258
+ _createCubelet(gridX, gridY, gridZ, faceColors) {
259
+ const offset = this.cubeletSize + this.gap;
260
+ const x = gridX * offset;
261
+ const y = gridY * offset;
262
+ const z = gridZ * offset;
263
+
264
+ return new Cube3D(this.cubeletSize, {
265
+ x,
266
+ y,
267
+ z,
268
+ camera: this.camera,
269
+ faceColors,
270
+ stickerMode: CONFIG.sticker.enabled,
271
+ stickerMargin: CONFIG.sticker.margin,
272
+ stickerBackgroundColor: CONFIG.sticker.backgroundColor,
273
+ stroke: CONFIG.sticker.strokeColor,
274
+ lineWidth: CONFIG.sticker.lineWidth,
275
+ });
276
+ }
277
+
278
+ /**
279
+ * Start shuffle animation
280
+ */
281
+ _startShuffle() {
282
+ if (this.animatingLayer || this.animationQueue.length > 0) {
283
+ return; // Already animating
284
+ }
285
+
286
+ this.isSolving = false; // Track moves during shuffle
287
+
288
+ // Generate random moves
289
+ const axes = ['x', 'y', 'z'];
290
+ const layers = [-1, 0, 1];
291
+ const directions = [1, -1];
292
+
293
+ for (let i = 0; i < CONFIG.layerAnimation.shuffleMoves; i++) {
294
+ const axis = axes[Math.floor(Math.random() * axes.length)];
295
+ const layer = layers[Math.floor(Math.random() * layers.length)];
296
+ const direction = directions[Math.floor(Math.random() * directions.length)];
297
+ this.animationQueue.push(new LayerMove(axis, layer, direction));
298
+ }
299
+
300
+ // Start first move
301
+ this._startNextMove();
302
+ }
303
+
304
+ /**
305
+ * Start solve animation - reverse all moves in history
306
+ */
307
+ _startSolve() {
308
+ if (this.animatingLayer || this.animationQueue.length > 0) {
309
+ return; // Already animating
310
+ }
311
+
312
+ if (this.moveHistory.length === 0) {
313
+ return; // Already solved
314
+ }
315
+
316
+ this.isSolving = true; // Don't track moves during solve
317
+
318
+ // Reverse all moves in history (LIFO order, opposite direction)
319
+ for (let i = this.moveHistory.length - 1; i >= 0; i--) {
320
+ const move = this.moveHistory[i];
321
+ // Reverse the direction
322
+ this.animationQueue.push(new LayerMove(move.axis, move.layer, -move.direction));
323
+ }
324
+
325
+ // Clear history since we're solving
326
+ this.moveHistory = [];
327
+
328
+ // Start first move
329
+ this._startNextMove();
330
+ }
331
+
332
+ /**
333
+ * Start the next move in the queue
334
+ */
335
+ _startNextMove() {
336
+ if (this.animationQueue.length === 0) {
337
+ this.animatingLayer = null;
338
+ return;
339
+ }
340
+
341
+ this.animatingLayer = this.animationQueue.shift();
342
+ this.animationProgress = 0;
343
+ }
344
+
345
+ /**
346
+ * Get cubelets in a specific layer
347
+ */
348
+ _getCubeletsInLayer(axis, layer) {
349
+ return this.cubelets.filter(data => {
350
+ switch (axis) {
351
+ case 'x': return data.gridX === layer;
352
+ case 'y': return data.gridY === layer;
353
+ case 'z': return data.gridZ === layer;
354
+ }
355
+ return false;
356
+ });
357
+ }
358
+
359
+ /**
360
+ * Apply layer rotation completion - update grid positions and face colors
361
+ */
362
+ _completeLayerRotation(move) {
363
+ // Track this move for solving (only if not currently solving)
364
+ if (!this.isSolving) {
365
+ this.moveHistory.push(new LayerMove(move.axis, move.layer, move.direction));
366
+ }
367
+
368
+ const layerCubelets = this._getCubeletsInLayer(move.axis, move.layer);
369
+
370
+ for (const data of layerCubelets) {
371
+ // Reset layer rotation
372
+ data.layerRotationX = 0;
373
+ data.layerRotationY = 0;
374
+ data.layerRotationZ = 0;
375
+
376
+ // Update grid positions based on rotation
377
+ const { gridX, gridY, gridZ } = data;
378
+ let newX = gridX, newY = gridY, newZ = gridZ;
379
+
380
+ // Rotate grid position 90 degrees around axis
381
+ switch (move.axis) {
382
+ case 'x':
383
+ // Rotate around X: Y and Z swap
384
+ newY = move.direction * gridZ;
385
+ newZ = -move.direction * gridY;
386
+ break;
387
+ case 'y':
388
+ // Rotate around Y: X and Z swap
389
+ newX = -move.direction * gridZ;
390
+ newZ = move.direction * gridX;
391
+ break;
392
+ case 'z':
393
+ // Rotate around Z: X and Y swap
394
+ newX = move.direction * gridY;
395
+ newY = -move.direction * gridX;
396
+ break;
397
+ }
398
+
399
+ data.gridX = newX;
400
+ data.gridY = newY;
401
+ data.gridZ = newZ;
402
+
403
+ // Rotate face colors to match grid rotation direction
404
+ // The face colors must cycle in the same direction as the grid positions
405
+ const oldColors = { ...data.faceColors };
406
+ switch (move.axis) {
407
+ case 'x':
408
+ // X-axis: rotates Y and Z, keeping X faces (left/right) fixed
409
+ // Grid: top→back→bottom→front (dir=1) or reverse (dir=-1)
410
+ if (move.direction === 1) {
411
+ data.faceColors.back = oldColors.top;
412
+ data.faceColors.bottom = oldColors.back;
413
+ data.faceColors.front = oldColors.bottom;
414
+ data.faceColors.top = oldColors.front;
415
+ } else {
416
+ data.faceColors.front = oldColors.top;
417
+ data.faceColors.bottom = oldColors.front;
418
+ data.faceColors.back = oldColors.bottom;
419
+ data.faceColors.top = oldColors.back;
420
+ }
421
+ break;
422
+ case 'y':
423
+ // Y-axis: rotates X and Z, keeping Y faces (top/bottom) fixed
424
+ // Grid: front→right→back→left (dir=1) or reverse (dir=-1)
425
+ if (move.direction === 1) {
426
+ data.faceColors.right = oldColors.front;
427
+ data.faceColors.back = oldColors.right;
428
+ data.faceColors.left = oldColors.back;
429
+ data.faceColors.front = oldColors.left;
430
+ } else {
431
+ data.faceColors.left = oldColors.front;
432
+ data.faceColors.back = oldColors.left;
433
+ data.faceColors.right = oldColors.back;
434
+ data.faceColors.front = oldColors.right;
435
+ }
436
+ break;
437
+ case 'z':
438
+ // Z-axis: rotates X and Y, keeping Z faces (front/back) fixed
439
+ // Grid: left→bottom→right→top (dir=1) or reverse (dir=-1)
440
+ if (move.direction === 1) {
441
+ data.faceColors.bottom = oldColors.left;
442
+ data.faceColors.right = oldColors.bottom;
443
+ data.faceColors.top = oldColors.right;
444
+ data.faceColors.left = oldColors.top;
445
+ } else {
446
+ data.faceColors.top = oldColors.left;
447
+ data.faceColors.right = oldColors.top;
448
+ data.faceColors.bottom = oldColors.right;
449
+ data.faceColors.left = oldColors.bottom;
450
+ }
451
+ break;
452
+ }
453
+
454
+ // Update cubelet position and colors
455
+ const offset = this.cubeletSize + this.gap;
456
+ data.cubelet.x = data.gridX * offset;
457
+ data.cubelet.y = data.gridY * offset;
458
+ data.cubelet.z = data.gridZ * offset;
459
+ data.cubelet.setFaceColors(data.faceColors);
460
+ }
461
+ }
462
+
463
+ update(dt) {
464
+ super.update(dt);
465
+
466
+ // Update camera (for inertia)
467
+ this.camera.update(dt);
468
+
469
+ // Handle layer animation
470
+ if (this.animatingLayer) {
471
+ this.animationProgress += dt / CONFIG.layerAnimation.duration;
472
+
473
+ if (this.animationProgress >= 1) {
474
+ // Complete this move
475
+ this._completeLayerRotation(this.animatingLayer);
476
+ this._startNextMove();
477
+ } else {
478
+ // Animate layer rotation
479
+ const angle = (Math.PI / 2) * this.animationProgress * this.animatingLayer.direction;
480
+ const layerCubelets = this._getCubeletsInLayer(
481
+ this.animatingLayer.axis,
482
+ this.animatingLayer.layer
483
+ );
484
+
485
+ for (const data of layerCubelets) {
486
+ switch (this.animatingLayer.axis) {
487
+ case 'x':
488
+ data.layerRotationX = angle;
489
+ break;
490
+ case 'y':
491
+ data.layerRotationY = angle;
492
+ break;
493
+ case 'z':
494
+ data.layerRotationZ = angle;
495
+ break;
496
+ }
497
+ }
498
+ }
499
+ }
500
+
501
+ // Update global rotation (pause during animation if configured)
502
+ if (!this.animatingLayer || !CONFIG.selfRotation.pauseDuringAnimation) {
503
+ this.globalRotationY += CONFIG.selfRotation.speed * dt;
504
+ }
505
+
506
+ // Apply same base rotation to all cubelets
507
+ for (const data of this.cubelets) {
508
+ data.cubelet.selfRotationY = this.globalRotationY;
509
+ }
510
+
511
+ // Update camera rotation text
512
+ this.cameraText.text = `Camera: X:${this.camera.rotationX.toFixed(2)} Y:${this.camera.rotationY.toFixed(2)}`;
513
+ }
514
+
515
+ render() {
516
+ super.render();
517
+
518
+ const ctx = this.ctx;
519
+ const centerX = this.width / 2;
520
+ const centerY = this.height / 2;
521
+ const hs = this.cubeletSize / 2;
522
+
523
+ // Collect ALL faces from ALL cubelets for global depth sorting
524
+ const allFaces = [];
525
+
526
+ // Face definitions: local corners relative to cubelet center
527
+ const faceDefinitions = {
528
+ front: { corners: [[-1,-1,-1], [1,-1,-1], [1,1,-1], [-1,1,-1]], normal: [0,0,-1] },
529
+ back: { corners: [[1,-1,1], [-1,-1,1], [-1,1,1], [1,1,1]], normal: [0,0,1] },
530
+ top: { corners: [[-1,-1,1], [1,-1,1], [1,-1,-1], [-1,-1,-1]], normal: [0,-1,0] },
531
+ bottom: { corners: [[-1,1,-1], [1,1,-1], [1,1,1], [-1,1,1]], normal: [0,1,0] },
532
+ left: { corners: [[-1,-1,1], [-1,-1,-1], [-1,1,-1], [-1,1,1]], normal: [-1,0,0] },
533
+ right: { corners: [[1,-1,-1], [1,-1,1], [1,1,1], [1,1,-1]], normal: [1,0,0] },
534
+ };
535
+
536
+ for (const data of this.cubelets) {
537
+ const cubelet = data.cubelet;
538
+
539
+ // Layer rotations (in cube's local space)
540
+ const layerX = data.layerRotationX;
541
+ const layerY = data.layerRotationY;
542
+ const layerZ = data.layerRotationZ;
543
+ // Global rotation (applied after layer rotation)
544
+ const globalY = this.globalRotationY;
545
+
546
+ // Process each face
547
+ for (const [faceName, faceDef] of Object.entries(faceDefinitions)) {
548
+ const color = data.faceColors[faceName];
549
+
550
+ // Transform corners: LAYER rotation first, then GLOBAL rotation
551
+ const worldCorners = faceDef.corners.map(([lx, ly, lz]) => {
552
+ // Scale to cubelet size
553
+ let x = lx * hs;
554
+ let y = ly * hs;
555
+ let z = lz * hs;
556
+
557
+ // Also transform cubelet position
558
+ let px = cubelet.x, py = cubelet.y, pz = cubelet.z;
559
+
560
+ // 1. Apply LAYER rotation first (in cube's local space)
561
+ // X-axis layer rotation
562
+ if (layerX !== 0) {
563
+ const cosX = Math.cos(layerX), sinX = Math.sin(layerX);
564
+ // Rotate face vertex
565
+ let y1 = y * cosX - z * sinX;
566
+ let z1 = y * sinX + z * cosX;
567
+ y = y1; z = z1;
568
+ // Rotate position
569
+ let py1 = py * cosX - pz * sinX;
570
+ let pz1 = py * sinX + pz * cosX;
571
+ py = py1; pz = pz1;
572
+ }
573
+
574
+ // Y-axis layer rotation
575
+ if (layerY !== 0) {
576
+ const cosLY = Math.cos(layerY), sinLY = Math.sin(layerY);
577
+ let x1 = x * cosLY - z * sinLY;
578
+ let z1 = x * sinLY + z * cosLY;
579
+ x = x1; z = z1;
580
+ let px1 = px * cosLY - pz * sinLY;
581
+ let pz1 = px * sinLY + pz * cosLY;
582
+ px = px1; pz = pz1;
583
+ }
584
+
585
+ // Z-axis layer rotation
586
+ if (layerZ !== 0) {
587
+ const cosZ = Math.cos(layerZ), sinZ = Math.sin(layerZ);
588
+ let x1 = x * cosZ - y * sinZ;
589
+ let y1 = x * sinZ + y * cosZ;
590
+ x = x1; y = y1;
591
+ let px1 = px * cosZ - py * sinZ;
592
+ let py1 = px * sinZ + py * cosZ;
593
+ px = px1; py = py1;
594
+ }
595
+
596
+ // 2. Apply GLOBAL rotation (whole cube spin)
597
+ const cosGY = Math.cos(globalY), sinGY = Math.sin(globalY);
598
+ let xg = x * cosGY - z * sinGY;
599
+ let zg = x * sinGY + z * cosGY;
600
+ x = xg; z = zg;
601
+ let pxg = px * cosGY - pz * sinGY;
602
+ let pzg = px * sinGY + pz * cosGY;
603
+ px = pxg; pz = pzg;
604
+
605
+ return { x: x + px, y: y + py, z: z + pz };
606
+ });
607
+
608
+ // Transform normal: same order - layer first, then global
609
+ let [nx, ny, nz] = faceDef.normal;
610
+
611
+ // Layer rotations
612
+ if (layerX !== 0) {
613
+ const cosX = Math.cos(layerX), sinX = Math.sin(layerX);
614
+ let ny1 = ny * cosX - nz * sinX;
615
+ let nz1 = ny * sinX + nz * cosX;
616
+ ny = ny1; nz = nz1;
617
+ }
618
+ if (layerY !== 0) {
619
+ const cosLY = Math.cos(layerY), sinLY = Math.sin(layerY);
620
+ let nx1 = nx * cosLY - nz * sinLY;
621
+ let nz1 = nx * sinLY + nz * cosLY;
622
+ nx = nx1; nz = nz1;
623
+ }
624
+ if (layerZ !== 0) {
625
+ const cosZ = Math.cos(layerZ), sinZ = Math.sin(layerZ);
626
+ let nx1 = nx * cosZ - ny * sinZ;
627
+ let ny1 = nx * sinZ + ny * cosZ;
628
+ nx = nx1; ny = ny1;
629
+ }
630
+
631
+ // Global Y rotation
632
+ const cosGY = Math.cos(globalY), sinGY = Math.sin(globalY);
633
+ let nxg = nx * cosGY - nz * sinGY;
634
+ let nzg = nx * sinGY + nz * cosGY;
635
+ nx = nxg; nz = nzg;
636
+
637
+ // Apply camera rotation to normal for backface culling
638
+ let vnx = nx, vny = ny, vnz = nz;
639
+ const camCosY = Math.cos(this.camera.rotationY);
640
+ const camSinY = Math.sin(this.camera.rotationY);
641
+ let vnx1 = vnx * camCosY - vnz * camSinY;
642
+ let vnz1 = vnx * camSinY + vnz * camCosY;
643
+ vnx = vnx1; vnz = vnz1;
644
+
645
+ const camCosX = Math.cos(this.camera.rotationX);
646
+ const camSinX = Math.sin(this.camera.rotationX);
647
+ let vny1 = vny * camCosX - vnz * camSinX;
648
+ let vnz2 = vny * camSinX + vnz * camCosX;
649
+ vny = vny1; vnz = vnz2;
650
+
651
+ // Backface culling
652
+ if (vnz > 0.01) continue;
653
+
654
+ // Project corners
655
+ const projectedCorners = worldCorners.map(c => this.camera.project(c.x, c.y, c.z));
656
+
657
+ // Calculate average depth for sorting
658
+ const avgDepth = projectedCorners.reduce((sum, p) => sum + p.z, 0) / 4;
659
+
660
+ // Calculate lighting
661
+ const intensity = this._calculateLighting(vnx, vny, vnz);
662
+
663
+ allFaces.push({
664
+ corners: projectedCorners,
665
+ color,
666
+ depth: avgDepth,
667
+ intensity,
668
+ isSticker: cubelet.stickerMode,
669
+ stickerMargin: cubelet.stickerMargin,
670
+ stickerBg: cubelet.stickerBackgroundColor,
671
+ stroke: cubelet.stroke,
672
+ lineWidth: cubelet.lineWidth,
673
+ });
674
+ }
675
+ }
676
+
677
+ // Sort all faces by depth (back to front)
678
+ allFaces.sort((a, b) => b.depth - a.depth);
679
+
680
+ // Render all faces
681
+ for (const face of allFaces) {
682
+ const corners = face.corners;
683
+
684
+ ctx.beginPath();
685
+ ctx.moveTo(centerX + corners[0].x, centerY + corners[0].y);
686
+ for (let i = 1; i < corners.length; i++) {
687
+ ctx.lineTo(centerX + corners[i].x, centerY + corners[i].y);
688
+ }
689
+ ctx.closePath();
690
+
691
+ if (face.isSticker) {
692
+ // Sticker mode: black background + colored sticker
693
+ const bgColor = this._applyLightingToColor(face.stickerBg, face.intensity);
694
+ ctx.fillStyle = bgColor;
695
+ ctx.fill();
696
+
697
+ if (face.stroke) {
698
+ ctx.strokeStyle = face.stroke;
699
+ ctx.lineWidth = face.lineWidth;
700
+ ctx.stroke();
701
+ }
702
+
703
+ // Draw inset sticker
704
+ const stickerCorners = this._getInsetCorners(corners, face.stickerMargin, centerX, centerY);
705
+ ctx.beginPath();
706
+ ctx.moveTo(stickerCorners[0].x, stickerCorners[0].y);
707
+ for (let i = 1; i < stickerCorners.length; i++) {
708
+ ctx.lineTo(stickerCorners[i].x, stickerCorners[i].y);
709
+ }
710
+ ctx.closePath();
711
+ ctx.fillStyle = this._applyLightingToColor(face.color, face.intensity);
712
+ ctx.fill();
713
+ } else {
714
+ ctx.fillStyle = this._applyLightingToColor(face.color, face.intensity);
715
+ ctx.fill();
716
+
717
+ if (face.stroke) {
718
+ ctx.strokeStyle = face.stroke;
719
+ ctx.lineWidth = face.lineWidth;
720
+ ctx.stroke();
721
+ }
722
+ }
723
+ }
724
+ }
725
+
726
+ /**
727
+ * Calculate inset corners for sticker rendering
728
+ */
729
+ _getInsetCorners(corners, margin, centerX, centerY) {
730
+ // Calculate center of the face
731
+ let cx = 0, cy = 0;
732
+ for (const c of corners) {
733
+ cx += centerX + c.x;
734
+ cy += centerY + c.y;
735
+ }
736
+ cx /= corners.length;
737
+ cy /= corners.length;
738
+
739
+ // Inset each corner towards center
740
+ const insetFactor = 1 - margin * 2;
741
+ return corners.map(c => ({
742
+ x: cx + (centerX + c.x - cx) * insetFactor,
743
+ y: cy + (centerY + c.y - cy) * insetFactor,
744
+ }));
745
+ }
746
+
747
+ /**
748
+ * Calculate lighting intensity
749
+ */
750
+ _calculateLighting(nx, ny, nz) {
751
+ const lightX = 0.5, lightY = -0.7, lightZ = -0.5;
752
+ const len = Math.sqrt(lightX*lightX + lightY*lightY + lightZ*lightZ);
753
+ const lx = lightX/len, ly = lightY/len, lz = lightZ/len;
754
+ let intensity = -(nx*lx + ny*ly + nz*lz);
755
+ return Math.max(0, intensity) * 0.6 + 0.4;
756
+ }
757
+
758
+ /**
759
+ * Apply lighting to a hex color
760
+ */
761
+ _applyLightingToColor(color, intensity) {
762
+ if (!color || !color.startsWith('#')) return color;
763
+ const hex = color.replace('#', '');
764
+ const r = Math.round(parseInt(hex.substring(0,2), 16) * intensity);
765
+ const g = Math.round(parseInt(hex.substring(2,4), 16) * intensity);
766
+ const b = Math.round(parseInt(hex.substring(4,6), 16) * intensity);
767
+ return `rgb(${r},${g},${b})`;
768
+ }
769
+
770
+ /**
771
+ * Apply Y-axis rotation to a point
772
+ */
773
+ _applyRotation(x, y, z, angle) {
774
+ const cosY = Math.cos(angle);
775
+ const sinY = Math.sin(angle);
776
+ return {
777
+ x: x * cosY - z * sinY,
778
+ y: y,
779
+ z: x * sinY + z * cosY,
780
+ };
781
+ }
782
+ }
783
+
784
+ // Start the demo
785
+ window.addEventListener("load", () => {
786
+ const canvas = document.getElementById("game");
787
+ const demo = new RubiksCubeDemo(canvas);
788
+ demo.start();
789
+ });