@guinetik/gcanvas 1.0.0 → 1.0.2
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.
- package/demos/coordinates.html +698 -0
- package/demos/cube3d.html +23 -0
- package/demos/demos.css +17 -3
- package/demos/dino.html +42 -0
- package/demos/fluid-simple.html +22 -0
- package/demos/fluid.html +37 -0
- package/demos/gameobjects.html +626 -0
- package/demos/index.html +19 -7
- package/demos/js/blob.js +18 -5
- package/demos/js/coordinates.js +840 -0
- package/demos/js/cube3d.js +789 -0
- package/demos/js/dino.js +1420 -0
- package/demos/js/fluid-simple.js +253 -0
- package/demos/js/fluid.js +527 -0
- package/demos/js/gameobjects.js +176 -0
- package/demos/js/plane3d.js +256 -0
- package/demos/js/platformer.js +1579 -0
- package/demos/js/sphere3d.js +229 -0
- package/demos/js/sprite.js +473 -0
- package/demos/js/tde/accretiondisk.js +65 -12
- package/demos/js/tde/blackholescene.js +2 -2
- package/demos/js/tde/config.js +2 -2
- package/demos/js/tde/index.js +152 -27
- package/demos/js/tde/lensedstarfield.js +32 -25
- package/demos/js/tde/tdestar.js +78 -98
- package/demos/js/tde/tidalstream.js +24 -8
- package/demos/plane3d.html +24 -0
- package/demos/platformer.html +43 -0
- package/demos/sphere3d.html +24 -0
- package/demos/sprite.html +18 -0
- package/docs/README.md +230 -222
- package/docs/api/FluidSystem.md +173 -0
- package/docs/concepts/architecture-overview.md +204 -204
- package/docs/concepts/coordinate-system.md +384 -0
- package/docs/concepts/rendering-pipeline.md +279 -279
- package/docs/concepts/shapes-vs-gameobjects.md +187 -0
- package/docs/concepts/two-layer-architecture.md +229 -229
- package/docs/fluid-dynamics.md +99 -0
- package/docs/getting-started/first-game.md +354 -354
- package/docs/getting-started/installation.md +175 -157
- package/docs/modules/collision/README.md +2 -2
- package/docs/modules/fluent/README.md +6 -6
- package/docs/modules/game/README.md +303 -303
- package/docs/modules/isometric-camera.md +2 -2
- package/docs/modules/isometric.md +1 -1
- package/docs/modules/painter/README.md +328 -328
- package/docs/modules/particle/README.md +3 -3
- package/docs/modules/shapes/README.md +221 -221
- package/docs/modules/shapes/base/euclidian.md +123 -123
- package/docs/modules/shapes/base/shape.md +262 -262
- package/docs/modules/shapes/base/transformable.md +243 -243
- package/docs/modules/state/README.md +2 -2
- package/docs/modules/util/README.md +1 -1
- package/docs/modules/util/camera3d.md +3 -3
- package/docs/modules/util/scene3d.md +1 -1
- package/package.json +3 -1
- package/readme.md +19 -5
- package/src/collision/collision.js +75 -0
- package/src/game/game.js +11 -5
- package/src/game/index.js +2 -1
- package/src/game/objects/index.js +3 -0
- package/src/game/objects/platformer-scene.js +411 -0
- package/src/game/objects/scene.js +14 -0
- package/src/game/objects/sprite.js +529 -0
- package/src/game/pipeline.js +20 -16
- package/src/game/systems/FluidSystem.js +835 -0
- package/src/game/systems/index.js +11 -0
- package/src/game/ui/button.js +39 -18
- package/src/game/ui/cursor.js +14 -0
- package/src/game/ui/fps.js +12 -4
- package/src/game/ui/index.js +2 -0
- package/src/game/ui/stepper.js +549 -0
- package/src/game/ui/theme.js +123 -0
- package/src/game/ui/togglebutton.js +9 -3
- package/src/game/ui/tooltip.js +11 -4
- package/src/io/input.js +75 -45
- package/src/io/mouse.js +44 -19
- package/src/io/touch.js +35 -12
- package/src/math/fluid.js +507 -0
- package/src/math/index.js +2 -0
- package/src/mixins/anchor.js +17 -7
- package/src/motion/tweenetik.js +16 -0
- package/src/shapes/cube3d.js +599 -0
- package/src/shapes/index.js +3 -0
- package/src/shapes/plane3d.js +687 -0
- package/src/shapes/sphere3d.js +75 -6
- package/src/util/camera2d.js +315 -0
- package/src/util/camera3d.js +218 -12
- package/src/util/index.js +1 -0
- package/src/webgl/shaders/plane-shaders.js +332 -0
- package/src/webgl/shaders/sphere-shaders.js +4 -2
- package/types/fluent.d.ts +361 -0
- package/types/game.d.ts +303 -0
- package/types/index.d.ts +144 -5
- package/types/math.d.ts +361 -0
- package/types/motion.d.ts +271 -0
- package/types/particle.d.ts +373 -0
- package/types/shapes.d.ts +107 -9
- package/types/util.d.ts +353 -0
- package/types/webgl.d.ts +109 -0
- package/disk_example.png +0 -0
- package/tde.png +0 -0
|
@@ -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 "../../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
|
+
});
|