@guinetik/gcanvas 1.0.2 → 1.0.4

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 (217) hide show
  1. package/dist/gcanvas.es.js +25656 -0
  2. package/dist/gcanvas.es.min.js +1 -0
  3. package/dist/gcanvas.umd.js +1 -0
  4. package/dist/gcanvas.umd.min.js +1 -0
  5. package/package.json +23 -6
  6. package/src/game/objects/index.js +1 -0
  7. package/src/game/objects/spritesheet.js +260 -0
  8. package/src/game/ui/theme.js +6 -0
  9. package/src/io/keys.js +9 -1
  10. package/src/math/boolean.js +481 -0
  11. package/src/math/index.js +1 -0
  12. package/.github/workflows/release.yaml +0 -70
  13. package/.jshintrc +0 -4
  14. package/.vscode/settings.json +0 -22
  15. package/CLAUDE.md +0 -310
  16. package/blackhole.jpg +0 -0
  17. package/demo.png +0 -0
  18. package/demos/CNAME +0 -1
  19. package/demos/animations.html +0 -31
  20. package/demos/basic.html +0 -38
  21. package/demos/baskara.html +0 -31
  22. package/demos/bezier.html +0 -35
  23. package/demos/beziersignature.html +0 -29
  24. package/demos/blackhole.html +0 -28
  25. package/demos/blob.html +0 -35
  26. package/demos/coordinates.html +0 -698
  27. package/demos/cube3d.html +0 -23
  28. package/demos/demos.css +0 -303
  29. package/demos/dino.html +0 -42
  30. package/demos/easing.html +0 -28
  31. package/demos/events.html +0 -195
  32. package/demos/fluent.html +0 -647
  33. package/demos/fluid-simple.html +0 -22
  34. package/demos/fluid.html +0 -37
  35. package/demos/fractals.html +0 -36
  36. package/demos/gameobjects.html +0 -626
  37. package/demos/genart.html +0 -26
  38. package/demos/gendream.html +0 -26
  39. package/demos/group.html +0 -36
  40. package/demos/home.html +0 -587
  41. package/demos/index.html +0 -376
  42. package/demos/isometric.html +0 -34
  43. package/demos/js/animations.js +0 -452
  44. package/demos/js/basic.js +0 -204
  45. package/demos/js/baskara.js +0 -751
  46. package/demos/js/bezier.js +0 -692
  47. package/demos/js/beziersignature.js +0 -241
  48. package/demos/js/blackhole/accretiondisk.obj.js +0 -379
  49. package/demos/js/blackhole/blackhole.obj.js +0 -318
  50. package/demos/js/blackhole/index.js +0 -409
  51. package/demos/js/blackhole/particle.js +0 -56
  52. package/demos/js/blackhole/starfield.obj.js +0 -218
  53. package/demos/js/blob.js +0 -2276
  54. package/demos/js/coordinates.js +0 -840
  55. package/demos/js/cube3d.js +0 -789
  56. package/demos/js/dino.js +0 -1420
  57. package/demos/js/easing.js +0 -477
  58. package/demos/js/fluent.js +0 -183
  59. package/demos/js/fluid-simple.js +0 -253
  60. package/demos/js/fluid.js +0 -527
  61. package/demos/js/fractals.js +0 -931
  62. package/demos/js/fractalworker.js +0 -93
  63. package/demos/js/gameobjects.js +0 -176
  64. package/demos/js/genart.js +0 -268
  65. package/demos/js/gendream.js +0 -209
  66. package/demos/js/group.js +0 -140
  67. package/demos/js/info-toggle.js +0 -25
  68. package/demos/js/isometric.js +0 -863
  69. package/demos/js/kerr.js +0 -1556
  70. package/demos/js/lavalamp.js +0 -590
  71. package/demos/js/layout.js +0 -354
  72. package/demos/js/mondrian.js +0 -285
  73. package/demos/js/opacity.js +0 -275
  74. package/demos/js/painter.js +0 -484
  75. package/demos/js/particles-showcase.js +0 -514
  76. package/demos/js/particles.js +0 -299
  77. package/demos/js/patterns.js +0 -397
  78. package/demos/js/penrose/artifact.js +0 -69
  79. package/demos/js/penrose/blackhole.js +0 -121
  80. package/demos/js/penrose/constants.js +0 -73
  81. package/demos/js/penrose/game.js +0 -943
  82. package/demos/js/penrose/lore.js +0 -278
  83. package/demos/js/penrose/penrosescene.js +0 -892
  84. package/demos/js/penrose/ship.js +0 -216
  85. package/demos/js/penrose/sounds.js +0 -211
  86. package/demos/js/penrose/voidparticle.js +0 -55
  87. package/demos/js/penrose/voidscene.js +0 -258
  88. package/demos/js/penrose/voidship.js +0 -144
  89. package/demos/js/penrose/wormhole.js +0 -46
  90. package/demos/js/pipeline.js +0 -555
  91. package/demos/js/plane3d.js +0 -256
  92. package/demos/js/platformer.js +0 -1579
  93. package/demos/js/scene.js +0 -304
  94. package/demos/js/scenes.js +0 -320
  95. package/demos/js/schrodinger.js +0 -410
  96. package/demos/js/schwarzschild.js +0 -1023
  97. package/demos/js/shapes.js +0 -628
  98. package/demos/js/space/alien.js +0 -171
  99. package/demos/js/space/boom.js +0 -98
  100. package/demos/js/space/boss.js +0 -353
  101. package/demos/js/space/buff.js +0 -73
  102. package/demos/js/space/bullet.js +0 -102
  103. package/demos/js/space/constants.js +0 -85
  104. package/demos/js/space/game.js +0 -1884
  105. package/demos/js/space/hud.js +0 -112
  106. package/demos/js/space/laserbeam.js +0 -179
  107. package/demos/js/space/lightning.js +0 -277
  108. package/demos/js/space/minion.js +0 -192
  109. package/demos/js/space/missile.js +0 -212
  110. package/demos/js/space/player.js +0 -430
  111. package/demos/js/space/powerup.js +0 -90
  112. package/demos/js/space/starfield.js +0 -58
  113. package/demos/js/space/starpower.js +0 -90
  114. package/demos/js/spacetime.js +0 -559
  115. package/demos/js/sphere3d.js +0 -229
  116. package/demos/js/sprite.js +0 -473
  117. package/demos/js/svgtween.js +0 -204
  118. package/demos/js/tde/accretiondisk.js +0 -471
  119. package/demos/js/tde/blackhole.js +0 -219
  120. package/demos/js/tde/blackholescene.js +0 -209
  121. package/demos/js/tde/config.js +0 -59
  122. package/demos/js/tde/index.js +0 -820
  123. package/demos/js/tde/jets.js +0 -290
  124. package/demos/js/tde/lensedstarfield.js +0 -154
  125. package/demos/js/tde/tdestar.js +0 -297
  126. package/demos/js/tde/tidalstream.js +0 -372
  127. package/demos/js/tde_old/blackhole.obj.js +0 -354
  128. package/demos/js/tde_old/debris.obj.js +0 -791
  129. package/demos/js/tde_old/flare.obj.js +0 -239
  130. package/demos/js/tde_old/index.js +0 -448
  131. package/demos/js/tde_old/star.obj.js +0 -812
  132. package/demos/js/tiles.js +0 -312
  133. package/demos/js/tweendemo.js +0 -79
  134. package/demos/js/visibility.js +0 -102
  135. package/demos/kerr.html +0 -28
  136. package/demos/lavalamp.html +0 -27
  137. package/demos/layouts.html +0 -37
  138. package/demos/logo.svg +0 -4
  139. package/demos/loop.html +0 -84
  140. package/demos/mondrian.html +0 -32
  141. package/demos/og_image.png +0 -0
  142. package/demos/opacity.html +0 -36
  143. package/demos/painter.html +0 -39
  144. package/demos/particles-showcase.html +0 -28
  145. package/demos/particles.html +0 -24
  146. package/demos/patterns.html +0 -33
  147. package/demos/penrose-game.html +0 -31
  148. package/demos/pipeline.html +0 -737
  149. package/demos/plane3d.html +0 -24
  150. package/demos/platformer.html +0 -43
  151. package/demos/scene.html +0 -33
  152. package/demos/scenes.html +0 -96
  153. package/demos/schrodinger.html +0 -27
  154. package/demos/schwarzschild.html +0 -27
  155. package/demos/shapes.html +0 -16
  156. package/demos/space.html +0 -85
  157. package/demos/spacetime.html +0 -27
  158. package/demos/sphere3d.html +0 -24
  159. package/demos/sprite.html +0 -18
  160. package/demos/svgtween.html +0 -29
  161. package/demos/tde.html +0 -28
  162. package/demos/tiles.html +0 -28
  163. package/demos/transforms.html +0 -400
  164. package/demos/tween.html +0 -45
  165. package/demos/visibility.html +0 -33
  166. package/docs/README.md +0 -230
  167. package/docs/api/FluidSystem.md +0 -173
  168. package/docs/concepts/architecture-overview.md +0 -204
  169. package/docs/concepts/coordinate-system.md +0 -384
  170. package/docs/concepts/lifecycle.md +0 -255
  171. package/docs/concepts/rendering-pipeline.md +0 -279
  172. package/docs/concepts/shapes-vs-gameobjects.md +0 -187
  173. package/docs/concepts/tde-zorder.md +0 -106
  174. package/docs/concepts/two-layer-architecture.md +0 -229
  175. package/docs/fluid-dynamics.md +0 -99
  176. package/docs/getting-started/first-game.md +0 -354
  177. package/docs/getting-started/hello-world.md +0 -269
  178. package/docs/getting-started/installation.md +0 -175
  179. package/docs/modules/collision/README.md +0 -453
  180. package/docs/modules/fluent/README.md +0 -1075
  181. package/docs/modules/game/README.md +0 -303
  182. package/docs/modules/isometric-camera.md +0 -210
  183. package/docs/modules/isometric.md +0 -275
  184. package/docs/modules/painter/README.md +0 -328
  185. package/docs/modules/particle/README.md +0 -559
  186. package/docs/modules/shapes/README.md +0 -221
  187. package/docs/modules/shapes/base/euclidian.md +0 -123
  188. package/docs/modules/shapes/base/geometry2d.md +0 -204
  189. package/docs/modules/shapes/base/renderable.md +0 -215
  190. package/docs/modules/shapes/base/shape.md +0 -262
  191. package/docs/modules/shapes/base/transformable.md +0 -243
  192. package/docs/modules/shapes/hierarchy.md +0 -218
  193. package/docs/modules/state/README.md +0 -577
  194. package/docs/modules/util/README.md +0 -99
  195. package/docs/modules/util/camera3d.md +0 -412
  196. package/docs/modules/util/scene3d.md +0 -395
  197. package/index.html +0 -17
  198. package/jsdoc.json +0 -50
  199. package/scripts/build-demo.js +0 -69
  200. package/scripts/bundle4llm.js +0 -276
  201. package/scripts/clearconsole.js +0 -48
  202. package/test/math/orbital.test.js +0 -61
  203. package/test/math/tensor.test.js +0 -114
  204. package/test/particle/emitter.test.js +0 -204
  205. package/test/particle/particle-system.test.js +0 -310
  206. package/test/particle/particle.test.js +0 -116
  207. package/test/particle/updaters.test.js +0 -386
  208. package/test/setup.js +0 -120
  209. package/test/shapes/euclidian.test.js +0 -44
  210. package/test/shapes/geometry.test.js +0 -86
  211. package/test/shapes/group.test.js +0 -86
  212. package/test/shapes/rectangle.test.js +0 -64
  213. package/test/shapes/transform.test.js +0 -379
  214. package/test/util/camera3d.test.js +0 -428
  215. package/test/util/scene3d.test.js +0 -352
  216. package/vite.config.js +0 -50
  217. package/vitest.config.js +0 -13
@@ -1,789 +0,0 @@
1
- import {
2
- Game,
3
- Cube3D,
4
- FPSCounter,
5
- Button,
6
- Scene,
7
- Text,
8
- Position,
9
- verticalLayout,
10
- applyLayout,
11
- } from "../../src/index.js";
12
- import { Camera3D } from "../../src/util/camera3d.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
- });