@guinetik/gcanvas 1.0.1 → 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/gameobjects.html +626 -0
- package/demos/index.html +17 -7
- package/demos/js/coordinates.js +840 -0
- package/demos/js/cube3d.js +789 -0
- package/demos/js/dino.js +1420 -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 +3 -3
- package/demos/js/tde/tidalstream.js +2 -2
- 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/concepts/coordinate-system.md +384 -0
- package/docs/concepts/shapes-vs-gameobjects.md +187 -0
- package/docs/fluid-dynamics.md +99 -97
- package/package.json +1 -1
- package/src/game/game.js +11 -5
- 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/ui/theme.js +123 -121
- package/src/io/input.js +75 -45
- package/src/io/mouse.js +44 -19
- package/src/io/touch.js +35 -12
- package/src/shapes/cube3d.js +599 -0
- package/src/shapes/index.js +2 -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/index.js +1 -0
- package/src/webgl/shaders/plane-shaders.js +332 -0
- package/src/webgl/shaders/sphere-shaders.js +4 -2
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
import { Shape } from "./shape.js";
|
|
2
|
+
import { Plane3D } from "./plane3d.js";
|
|
3
|
+
import { Painter } from "../painter/painter.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Cube3D - A 3D cube composed of 6 Plane3D faces
|
|
7
|
+
*
|
|
8
|
+
* This cube integrates with Camera3D for proper 3D projection and rendering.
|
|
9
|
+
* Each face is a Plane3D that can have its own color, texture, or shader.
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - Composed of 6 Plane3D faces for flexibility
|
|
13
|
+
* - Integrates with Camera3D for rotation and perspective
|
|
14
|
+
* - Self-rotation around any axis
|
|
15
|
+
* - Individual face colors
|
|
16
|
+
* - Automatic depth sorting and backface culling
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // Basic cube with solid colors
|
|
20
|
+
* const cube = new Cube3D(100, {
|
|
21
|
+
* camera: this.camera,
|
|
22
|
+
* faceColors: {
|
|
23
|
+
* front: "#FF0000",
|
|
24
|
+
* back: "#00FF00",
|
|
25
|
+
* top: "#FFFFFF",
|
|
26
|
+
* bottom: "#FFFF00",
|
|
27
|
+
* left: "#0000FF",
|
|
28
|
+
* right: "#FFA500",
|
|
29
|
+
* },
|
|
30
|
+
* });
|
|
31
|
+
*/
|
|
32
|
+
export class Cube3D extends Shape {
|
|
33
|
+
/**
|
|
34
|
+
* Create a 3D cube
|
|
35
|
+
* @param {number} size - Cube edge length
|
|
36
|
+
* @param {object} options - Configuration options
|
|
37
|
+
* @param {number} [options.x=0] - X position in 3D space
|
|
38
|
+
* @param {number} [options.y=0] - Y position in 3D space
|
|
39
|
+
* @param {number} [options.z=0] - Z position in 3D space
|
|
40
|
+
* @param {Camera3D} [options.camera] - Camera for projection
|
|
41
|
+
* @param {boolean} [options.debug=false] - Show wireframe
|
|
42
|
+
* @param {string} [options.stroke] - Wireframe line color
|
|
43
|
+
* @param {number} [options.lineWidth=1] - Wireframe line width
|
|
44
|
+
* @param {number} [options.selfRotationX=0] - Self-rotation around X axis (radians)
|
|
45
|
+
* @param {number} [options.selfRotationY=0] - Self-rotation around Y axis (radians)
|
|
46
|
+
* @param {number} [options.selfRotationZ=0] - Self-rotation around Z axis (radians)
|
|
47
|
+
* @param {Object} [options.faceColors] - Colors for each face
|
|
48
|
+
* @param {string} [options.faceColors.front="#FF0000"] - Front face color
|
|
49
|
+
* @param {string} [options.faceColors.back="#FFA500"] - Back face color
|
|
50
|
+
* @param {string} [options.faceColors.top="#FFFFFF"] - Top face color
|
|
51
|
+
* @param {string} [options.faceColors.bottom="#FFFF00"] - Bottom face color
|
|
52
|
+
* @param {string} [options.faceColors.left="#00FF00"] - Left face color
|
|
53
|
+
* @param {string} [options.faceColors.right="#0000FF"] - Right face color
|
|
54
|
+
*/
|
|
55
|
+
constructor(size, options = {}) {
|
|
56
|
+
super(options);
|
|
57
|
+
|
|
58
|
+
this.size = size;
|
|
59
|
+
|
|
60
|
+
// 3D position
|
|
61
|
+
this.x = options.x ?? 0;
|
|
62
|
+
this.y = options.y ?? 0;
|
|
63
|
+
this.z = options.z ?? 0;
|
|
64
|
+
|
|
65
|
+
// Camera reference
|
|
66
|
+
this.camera = options.camera ?? null;
|
|
67
|
+
|
|
68
|
+
// Rendering options
|
|
69
|
+
this.debug = options.debug ?? false;
|
|
70
|
+
|
|
71
|
+
// Self-rotation (radians)
|
|
72
|
+
this.selfRotationX = options.selfRotationX ?? 0;
|
|
73
|
+
this.selfRotationY = options.selfRotationY ?? 0;
|
|
74
|
+
this.selfRotationZ = options.selfRotationZ ?? 0;
|
|
75
|
+
|
|
76
|
+
// Face colors (default: Rubik's cube colors)
|
|
77
|
+
this.faceColors = {
|
|
78
|
+
front: options.faceColors?.front ?? "#B71234", // Red
|
|
79
|
+
back: options.faceColors?.back ?? "#FF5800", // Orange
|
|
80
|
+
top: options.faceColors?.top ?? "#FFFFFF", // White
|
|
81
|
+
bottom: options.faceColors?.bottom ?? "#FFD500", // Yellow
|
|
82
|
+
left: options.faceColors?.left ?? "#009B48", // Green
|
|
83
|
+
right: options.faceColors?.right ?? "#0046AD", // Blue
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Edge stroke styling
|
|
87
|
+
this.stroke = options.stroke ?? null;
|
|
88
|
+
this.lineWidth = options.lineWidth ?? 1;
|
|
89
|
+
|
|
90
|
+
// Sticker mode - renders colored sticker inset from face edge
|
|
91
|
+
this.stickerMode = options.stickerMode ?? false;
|
|
92
|
+
this.stickerMargin = options.stickerMargin ?? 0.15; // Margin as fraction of face size
|
|
93
|
+
this.stickerBackgroundColor = options.stickerBackgroundColor ?? "#0A0A0A"; // Black plastic
|
|
94
|
+
|
|
95
|
+
// Face configurations (relative position and orientation)
|
|
96
|
+
// Each face is defined by:
|
|
97
|
+
// - localPos: position relative to cube center
|
|
98
|
+
// - faceRotX, faceRotY: rotation to orient the face correctly
|
|
99
|
+
this._faceConfigs = this._createFaceConfigs();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Create face configuration data
|
|
104
|
+
* Defines the 6 faces with their local positions and orientations
|
|
105
|
+
* @private
|
|
106
|
+
*/
|
|
107
|
+
_createFaceConfigs() {
|
|
108
|
+
const hs = this.size / 2;
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
// Front face (z = -hs, facing -Z / towards camera)
|
|
112
|
+
front: {
|
|
113
|
+
localPos: { x: 0, y: 0, z: -hs },
|
|
114
|
+
faceRotX: 0,
|
|
115
|
+
faceRotY: 0,
|
|
116
|
+
color: this.faceColors.front,
|
|
117
|
+
},
|
|
118
|
+
// Back face (z = +hs, facing +Z / away from camera)
|
|
119
|
+
back: {
|
|
120
|
+
localPos: { x: 0, y: 0, z: hs },
|
|
121
|
+
faceRotX: 0,
|
|
122
|
+
faceRotY: Math.PI,
|
|
123
|
+
color: this.faceColors.back,
|
|
124
|
+
},
|
|
125
|
+
// Top face (y = -hs, facing -Y / up)
|
|
126
|
+
top: {
|
|
127
|
+
localPos: { x: 0, y: -hs, z: 0 },
|
|
128
|
+
faceRotX: -Math.PI / 2,
|
|
129
|
+
faceRotY: 0,
|
|
130
|
+
color: this.faceColors.top,
|
|
131
|
+
},
|
|
132
|
+
// Bottom face (y = +hs, facing +Y / down)
|
|
133
|
+
bottom: {
|
|
134
|
+
localPos: { x: 0, y: hs, z: 0 },
|
|
135
|
+
faceRotX: Math.PI / 2,
|
|
136
|
+
faceRotY: 0,
|
|
137
|
+
color: this.faceColors.bottom,
|
|
138
|
+
},
|
|
139
|
+
// Left face (x = -hs, facing -X / left)
|
|
140
|
+
left: {
|
|
141
|
+
localPos: { x: -hs, y: 0, z: 0 },
|
|
142
|
+
faceRotX: 0,
|
|
143
|
+
faceRotY: Math.PI / 2,
|
|
144
|
+
color: this.faceColors.left,
|
|
145
|
+
},
|
|
146
|
+
// Right face (x = +hs, facing +X / right)
|
|
147
|
+
right: {
|
|
148
|
+
localPos: { x: hs, y: 0, z: 0 },
|
|
149
|
+
faceRotX: 0,
|
|
150
|
+
faceRotY: -Math.PI / 2,
|
|
151
|
+
color: this.faceColors.right,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Set or update the camera reference
|
|
158
|
+
* @param {Camera3D} camera - Camera instance
|
|
159
|
+
* @returns {Cube3D} this for chaining
|
|
160
|
+
*/
|
|
161
|
+
setCamera(camera) {
|
|
162
|
+
this.camera = camera;
|
|
163
|
+
return this;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Update face colors
|
|
168
|
+
* @param {Object} colors - Color object with face names as keys
|
|
169
|
+
* @returns {Cube3D} this for chaining
|
|
170
|
+
*/
|
|
171
|
+
setFaceColors(colors) {
|
|
172
|
+
Object.assign(this.faceColors, colors);
|
|
173
|
+
// Update face configs
|
|
174
|
+
for (const [name, config] of Object.entries(this._faceConfigs)) {
|
|
175
|
+
if (colors[name]) {
|
|
176
|
+
config.color = colors[name];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return this;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Apply self-rotation to a point
|
|
184
|
+
* @param {number} x - X component
|
|
185
|
+
* @param {number} y - Y component
|
|
186
|
+
* @param {number} z - Z component
|
|
187
|
+
* @returns {{x: number, y: number, z: number}} Rotated point
|
|
188
|
+
* @private
|
|
189
|
+
*/
|
|
190
|
+
_applySelfRotation(x, y, z) {
|
|
191
|
+
// Rotate around Y axis first
|
|
192
|
+
if (this.selfRotationY !== 0) {
|
|
193
|
+
const cosY = Math.cos(this.selfRotationY);
|
|
194
|
+
const sinY = Math.sin(this.selfRotationY);
|
|
195
|
+
const x1 = x * cosY - z * sinY;
|
|
196
|
+
const z1 = x * sinY + z * cosY;
|
|
197
|
+
x = x1;
|
|
198
|
+
z = z1;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Rotate around X axis
|
|
202
|
+
if (this.selfRotationX !== 0) {
|
|
203
|
+
const cosX = Math.cos(this.selfRotationX);
|
|
204
|
+
const sinX = Math.sin(this.selfRotationX);
|
|
205
|
+
const y1 = y * cosX - z * sinX;
|
|
206
|
+
const z1 = y * sinX + z * cosX;
|
|
207
|
+
y = y1;
|
|
208
|
+
z = z1;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Rotate around Z axis
|
|
212
|
+
if (this.selfRotationZ !== 0) {
|
|
213
|
+
const cosZ = Math.cos(this.selfRotationZ);
|
|
214
|
+
const sinZ = Math.sin(this.selfRotationZ);
|
|
215
|
+
const x1 = x * cosZ - y * sinZ;
|
|
216
|
+
const y1 = x * sinZ + y * cosZ;
|
|
217
|
+
x = x1;
|
|
218
|
+
y = y1;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { x, y, z };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Calculate lighting intensity based on surface normal
|
|
226
|
+
* @param {number} nx - Normal x component
|
|
227
|
+
* @param {number} ny - Normal y component
|
|
228
|
+
* @param {number} nz - Normal z component
|
|
229
|
+
* @returns {number} Intensity 0-1
|
|
230
|
+
* @private
|
|
231
|
+
*/
|
|
232
|
+
_calculateLighting(nx, ny, nz) {
|
|
233
|
+
// Simple directional light from top-right-front
|
|
234
|
+
const lightX = 0.5;
|
|
235
|
+
const lightY = 0.7;
|
|
236
|
+
const lightZ = 0.5;
|
|
237
|
+
const lightLen = Math.sqrt(
|
|
238
|
+
lightX * lightX + lightY * lightY + lightZ * lightZ
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const lx = lightX / lightLen;
|
|
242
|
+
const ly = lightY / lightLen;
|
|
243
|
+
const lz = lightZ / lightLen;
|
|
244
|
+
|
|
245
|
+
let intensity = nx * lx + ny * ly + nz * lz;
|
|
246
|
+
intensity = Math.max(0, intensity) * 0.7 + 0.3;
|
|
247
|
+
|
|
248
|
+
return intensity;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Apply lighting to a color
|
|
253
|
+
* @param {string} color - Base color (hex format)
|
|
254
|
+
* @param {number} intensity - Light intensity 0-1
|
|
255
|
+
* @returns {string} RGB color string
|
|
256
|
+
* @private
|
|
257
|
+
*/
|
|
258
|
+
_applyLighting(color, intensity) {
|
|
259
|
+
if (!color || typeof color !== "string" || !color.startsWith("#")) {
|
|
260
|
+
return color;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const hex = color.replace("#", "");
|
|
264
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
265
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
266
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
267
|
+
|
|
268
|
+
const lr = Math.round(r * intensity);
|
|
269
|
+
const lg = Math.round(g * intensity);
|
|
270
|
+
const lb = Math.round(b * intensity);
|
|
271
|
+
|
|
272
|
+
return `rgb(${lr}, ${lg}, ${lb})`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Main render method
|
|
277
|
+
*/
|
|
278
|
+
draw() {
|
|
279
|
+
super.draw();
|
|
280
|
+
|
|
281
|
+
if (!this.camera) {
|
|
282
|
+
// Fallback: draw a simple square if no camera
|
|
283
|
+
const hs = this.size / 2;
|
|
284
|
+
if (this.faceColors.front) {
|
|
285
|
+
Painter.shapes.fillRect(-hs, -hs, this.size, this.size, this.faceColors.front);
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const ctx = Painter.ctx;
|
|
291
|
+
const hs = this.size / 2;
|
|
292
|
+
|
|
293
|
+
// Check if any self-rotation is applied
|
|
294
|
+
const hasSelfRotation =
|
|
295
|
+
this.selfRotationX !== 0 ||
|
|
296
|
+
this.selfRotationY !== 0 ||
|
|
297
|
+
this.selfRotationZ !== 0;
|
|
298
|
+
|
|
299
|
+
// Build list of faces with their transformed data
|
|
300
|
+
const facesToRender = [];
|
|
301
|
+
|
|
302
|
+
for (const [name, config] of Object.entries(this._faceConfigs)) {
|
|
303
|
+
// Get face local position
|
|
304
|
+
let { x: lx, y: ly, z: lz } = config.localPos;
|
|
305
|
+
|
|
306
|
+
// Apply cube's self-rotation to face position
|
|
307
|
+
if (hasSelfRotation) {
|
|
308
|
+
const rotated = this._applySelfRotation(lx, ly, lz);
|
|
309
|
+
lx = rotated.x;
|
|
310
|
+
ly = rotated.y;
|
|
311
|
+
lz = rotated.z;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Calculate world position (cube position + rotated face offset)
|
|
315
|
+
const worldX = this.x + lx;
|
|
316
|
+
const worldY = this.y + ly;
|
|
317
|
+
const worldZ = this.z + lz;
|
|
318
|
+
|
|
319
|
+
// Calculate face normal (initially pointing outward from face center)
|
|
320
|
+
// Start with the normal pointing in the direction the face is facing
|
|
321
|
+
let nx = 0, ny = 0, nz = 0;
|
|
322
|
+
if (name === "front") { nx = 0; ny = 0; nz = -1; }
|
|
323
|
+
else if (name === "back") { nx = 0; ny = 0; nz = 1; }
|
|
324
|
+
else if (name === "top") { nx = 0; ny = -1; nz = 0; }
|
|
325
|
+
else if (name === "bottom") { nx = 0; ny = 1; nz = 0; }
|
|
326
|
+
else if (name === "left") { nx = -1; ny = 0; nz = 0; }
|
|
327
|
+
else if (name === "right") { nx = 1; ny = 0; nz = 0; }
|
|
328
|
+
|
|
329
|
+
// Apply cube's self-rotation to normal
|
|
330
|
+
if (hasSelfRotation) {
|
|
331
|
+
const rotatedN = this._applySelfRotation(nx, ny, nz);
|
|
332
|
+
nx = rotatedN.x;
|
|
333
|
+
ny = rotatedN.y;
|
|
334
|
+
nz = rotatedN.z;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Apply camera rotation to normal for view-space culling
|
|
338
|
+
let vnx = nx, vny = ny, vnz = nz;
|
|
339
|
+
|
|
340
|
+
// Z axis rotation
|
|
341
|
+
if (this.camera.rotationZ !== 0) {
|
|
342
|
+
const cosZ = Math.cos(this.camera.rotationZ);
|
|
343
|
+
const sinZ = Math.sin(this.camera.rotationZ);
|
|
344
|
+
const nx0 = vnx;
|
|
345
|
+
const ny0 = vny;
|
|
346
|
+
vnx = nx0 * cosZ - ny0 * sinZ;
|
|
347
|
+
vny = nx0 * sinZ + ny0 * cosZ;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Y axis rotation
|
|
351
|
+
const cosY = Math.cos(this.camera.rotationY);
|
|
352
|
+
const sinY = Math.sin(this.camera.rotationY);
|
|
353
|
+
const nx1 = vnx * cosY - vnz * sinY;
|
|
354
|
+
const nz1 = vnx * sinY + vnz * cosY;
|
|
355
|
+
|
|
356
|
+
// X axis rotation
|
|
357
|
+
const cosX = Math.cos(this.camera.rotationX);
|
|
358
|
+
const sinX = Math.sin(this.camera.rotationX);
|
|
359
|
+
const ny1 = vny * cosX - nz1 * sinX;
|
|
360
|
+
const nz2 = vny * sinX + nz1 * cosX;
|
|
361
|
+
|
|
362
|
+
// Backface culling: skip faces pointing away from camera
|
|
363
|
+
if (nz2 > 0.01) {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Project face center
|
|
368
|
+
const projected = this.camera.project(worldX, worldY, worldZ);
|
|
369
|
+
|
|
370
|
+
// Skip if behind camera
|
|
371
|
+
if (projected.z < -this.camera.perspective + 10) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Calculate lighting based on rotated normal
|
|
376
|
+
const intensity = this._calculateLighting(nx1, ny1, nz2);
|
|
377
|
+
|
|
378
|
+
// Calculate face vertices (quad corners)
|
|
379
|
+
const faceVertices = this._getFaceVertices(name, config, hasSelfRotation);
|
|
380
|
+
|
|
381
|
+
facesToRender.push({
|
|
382
|
+
name,
|
|
383
|
+
config,
|
|
384
|
+
projected,
|
|
385
|
+
vertices: faceVertices,
|
|
386
|
+
depth: projected.z,
|
|
387
|
+
intensity,
|
|
388
|
+
nx: nx1,
|
|
389
|
+
ny: ny1,
|
|
390
|
+
nz: nz2,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Sort faces by depth (back to front)
|
|
395
|
+
facesToRender.sort((a, b) => b.depth - a.depth);
|
|
396
|
+
|
|
397
|
+
// Render each visible face
|
|
398
|
+
for (const face of facesToRender) {
|
|
399
|
+
this._renderFace(ctx, face);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Get the 4 corner vertices of a face in world space
|
|
405
|
+
* @param {string} name - Face name
|
|
406
|
+
* @param {Object} config - Face configuration
|
|
407
|
+
* @param {boolean} hasSelfRotation - Whether self-rotation is applied
|
|
408
|
+
* @returns {Array} Array of 4 projected vertices
|
|
409
|
+
* @private
|
|
410
|
+
*/
|
|
411
|
+
_getFaceVertices(name, config, hasSelfRotation) {
|
|
412
|
+
const hs = this.size / 2;
|
|
413
|
+
|
|
414
|
+
// Define corner offsets based on face orientation
|
|
415
|
+
let corners;
|
|
416
|
+
|
|
417
|
+
switch (name) {
|
|
418
|
+
case "front":
|
|
419
|
+
corners = [
|
|
420
|
+
{ x: -hs, y: -hs, z: -hs },
|
|
421
|
+
{ x: hs, y: -hs, z: -hs },
|
|
422
|
+
{ x: hs, y: hs, z: -hs },
|
|
423
|
+
{ x: -hs, y: hs, z: -hs },
|
|
424
|
+
];
|
|
425
|
+
break;
|
|
426
|
+
case "back":
|
|
427
|
+
corners = [
|
|
428
|
+
{ x: hs, y: -hs, z: hs },
|
|
429
|
+
{ x: -hs, y: -hs, z: hs },
|
|
430
|
+
{ x: -hs, y: hs, z: hs },
|
|
431
|
+
{ x: hs, y: hs, z: hs },
|
|
432
|
+
];
|
|
433
|
+
break;
|
|
434
|
+
case "top":
|
|
435
|
+
corners = [
|
|
436
|
+
{ x: -hs, y: -hs, z: hs },
|
|
437
|
+
{ x: hs, y: -hs, z: hs },
|
|
438
|
+
{ x: hs, y: -hs, z: -hs },
|
|
439
|
+
{ x: -hs, y: -hs, z: -hs },
|
|
440
|
+
];
|
|
441
|
+
break;
|
|
442
|
+
case "bottom":
|
|
443
|
+
corners = [
|
|
444
|
+
{ x: -hs, y: hs, z: -hs },
|
|
445
|
+
{ x: hs, y: hs, z: -hs },
|
|
446
|
+
{ x: hs, y: hs, z: hs },
|
|
447
|
+
{ x: -hs, y: hs, z: hs },
|
|
448
|
+
];
|
|
449
|
+
break;
|
|
450
|
+
case "left":
|
|
451
|
+
corners = [
|
|
452
|
+
{ x: -hs, y: -hs, z: hs },
|
|
453
|
+
{ x: -hs, y: -hs, z: -hs },
|
|
454
|
+
{ x: -hs, y: hs, z: -hs },
|
|
455
|
+
{ x: -hs, y: hs, z: hs },
|
|
456
|
+
];
|
|
457
|
+
break;
|
|
458
|
+
case "right":
|
|
459
|
+
corners = [
|
|
460
|
+
{ x: hs, y: -hs, z: -hs },
|
|
461
|
+
{ x: hs, y: -hs, z: hs },
|
|
462
|
+
{ x: hs, y: hs, z: hs },
|
|
463
|
+
{ x: hs, y: hs, z: -hs },
|
|
464
|
+
];
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Apply self-rotation and projection
|
|
469
|
+
return corners.map((corner) => {
|
|
470
|
+
let { x, y, z } = corner;
|
|
471
|
+
|
|
472
|
+
if (hasSelfRotation) {
|
|
473
|
+
const rotated = this._applySelfRotation(x, y, z);
|
|
474
|
+
x = rotated.x;
|
|
475
|
+
y = rotated.y;
|
|
476
|
+
z = rotated.z;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Add cube position
|
|
480
|
+
x += this.x;
|
|
481
|
+
y += this.y;
|
|
482
|
+
z += this.z;
|
|
483
|
+
|
|
484
|
+
// Project through camera
|
|
485
|
+
return this.camera.project(x, y, z);
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Render a single face
|
|
491
|
+
* @param {CanvasRenderingContext2D} ctx - Canvas context
|
|
492
|
+
* @param {Object} face - Face data
|
|
493
|
+
* @private
|
|
494
|
+
*/
|
|
495
|
+
_renderFace(ctx, face) {
|
|
496
|
+
const { vertices, config, intensity } = face;
|
|
497
|
+
|
|
498
|
+
// Apply lighting to face color
|
|
499
|
+
const faceColor = this._applyLighting(config.color, intensity);
|
|
500
|
+
|
|
501
|
+
// Draw quad as path
|
|
502
|
+
ctx.beginPath();
|
|
503
|
+
ctx.moveTo(vertices[0].x, vertices[0].y);
|
|
504
|
+
for (let i = 1; i < vertices.length; i++) {
|
|
505
|
+
ctx.lineTo(vertices[i].x, vertices[i].y);
|
|
506
|
+
}
|
|
507
|
+
ctx.closePath();
|
|
508
|
+
|
|
509
|
+
if (this.debug) {
|
|
510
|
+
// Wireframe mode
|
|
511
|
+
ctx.strokeStyle = this.stroke || "#fff";
|
|
512
|
+
ctx.lineWidth = this.lineWidth ?? 1;
|
|
513
|
+
ctx.stroke();
|
|
514
|
+
} else if (this.stickerMode) {
|
|
515
|
+
// Sticker mode: black plastic background with inset colored sticker
|
|
516
|
+
const bgColor = this._applyLighting(this.stickerBackgroundColor, intensity);
|
|
517
|
+
ctx.fillStyle = bgColor;
|
|
518
|
+
ctx.fill();
|
|
519
|
+
|
|
520
|
+
// Draw sticker stroke (grid lines on the plastic)
|
|
521
|
+
if (this.stroke) {
|
|
522
|
+
ctx.strokeStyle = this.stroke;
|
|
523
|
+
ctx.lineWidth = this.lineWidth ?? 1;
|
|
524
|
+
ctx.stroke();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Calculate inset sticker vertices
|
|
528
|
+
const stickerVerts = this._getInsetVertices(vertices, this.stickerMargin);
|
|
529
|
+
|
|
530
|
+
// Draw the colored sticker
|
|
531
|
+
ctx.beginPath();
|
|
532
|
+
ctx.moveTo(stickerVerts[0].x, stickerVerts[0].y);
|
|
533
|
+
for (let i = 1; i < stickerVerts.length; i++) {
|
|
534
|
+
ctx.lineTo(stickerVerts[i].x, stickerVerts[i].y);
|
|
535
|
+
}
|
|
536
|
+
ctx.closePath();
|
|
537
|
+
ctx.fillStyle = faceColor;
|
|
538
|
+
ctx.fill();
|
|
539
|
+
} else {
|
|
540
|
+
// Standard filled mode
|
|
541
|
+
ctx.fillStyle = faceColor;
|
|
542
|
+
ctx.fill();
|
|
543
|
+
|
|
544
|
+
// Optional stroke
|
|
545
|
+
if (this.stroke) {
|
|
546
|
+
ctx.strokeStyle = this.stroke;
|
|
547
|
+
ctx.lineWidth = this.lineWidth ?? 1;
|
|
548
|
+
ctx.stroke();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Calculate inset vertices for sticker rendering
|
|
555
|
+
* @param {Array} vertices - Original quad vertices
|
|
556
|
+
* @param {number} margin - Margin as fraction (0-0.5)
|
|
557
|
+
* @returns {Array} Inset vertices
|
|
558
|
+
* @private
|
|
559
|
+
*/
|
|
560
|
+
_getInsetVertices(vertices, margin) {
|
|
561
|
+
// Calculate center of the quad
|
|
562
|
+
let cx = 0, cy = 0;
|
|
563
|
+
for (const v of vertices) {
|
|
564
|
+
cx += v.x;
|
|
565
|
+
cy += v.y;
|
|
566
|
+
}
|
|
567
|
+
cx /= vertices.length;
|
|
568
|
+
cy /= vertices.length;
|
|
569
|
+
|
|
570
|
+
// Inset each vertex towards the center
|
|
571
|
+
const insetFactor = 1 - margin * 2; // e.g., margin=0.15 -> factor=0.7
|
|
572
|
+
return vertices.map(v => ({
|
|
573
|
+
x: cx + (v.x - cx) * insetFactor,
|
|
574
|
+
y: cy + (v.y - cy) * insetFactor,
|
|
575
|
+
}));
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Get the center point in world coordinates
|
|
580
|
+
* @returns {{x: number, y: number, z: number}}
|
|
581
|
+
*/
|
|
582
|
+
getCenter() {
|
|
583
|
+
return { x: this.x, y: this.y, z: this.z };
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Get bounding box for the cube
|
|
588
|
+
* @returns {{x: number, y: number, width: number, height: number}}
|
|
589
|
+
*/
|
|
590
|
+
getBounds() {
|
|
591
|
+
const hs = this.size / 2;
|
|
592
|
+
return {
|
|
593
|
+
x: this.x - hs,
|
|
594
|
+
y: this.y - hs,
|
|
595
|
+
width: this.size,
|
|
596
|
+
height: this.size,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
}
|
package/src/shapes/index.js
CHANGED
|
@@ -98,6 +98,8 @@ export { Triangle } from "./triangle.js";
|
|
|
98
98
|
export { Star } from "./star.js";
|
|
99
99
|
export { Sphere } from "./sphere.js";
|
|
100
100
|
export { Sphere3D } from "./sphere3d.js";
|
|
101
|
+
export { Plane3D } from "./plane3d.js";
|
|
102
|
+
export { Cube3D } from "./cube3d.js";
|
|
101
103
|
export { SVGShape } from "./svg.js";
|
|
102
104
|
export { StickFigure } from "./figure.js";
|
|
103
105
|
export { Ring } from "./ring.js";
|