@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.
Files changed (102) hide show
  1. package/demos/coordinates.html +698 -0
  2. package/demos/cube3d.html +23 -0
  3. package/demos/demos.css +17 -3
  4. package/demos/dino.html +42 -0
  5. package/demos/fluid-simple.html +22 -0
  6. package/demos/fluid.html +37 -0
  7. package/demos/gameobjects.html +626 -0
  8. package/demos/index.html +19 -7
  9. package/demos/js/blob.js +18 -5
  10. package/demos/js/coordinates.js +840 -0
  11. package/demos/js/cube3d.js +789 -0
  12. package/demos/js/dino.js +1420 -0
  13. package/demos/js/fluid-simple.js +253 -0
  14. package/demos/js/fluid.js +527 -0
  15. package/demos/js/gameobjects.js +176 -0
  16. package/demos/js/plane3d.js +256 -0
  17. package/demos/js/platformer.js +1579 -0
  18. package/demos/js/sphere3d.js +229 -0
  19. package/demos/js/sprite.js +473 -0
  20. package/demos/js/tde/accretiondisk.js +65 -12
  21. package/demos/js/tde/blackholescene.js +2 -2
  22. package/demos/js/tde/config.js +2 -2
  23. package/demos/js/tde/index.js +152 -27
  24. package/demos/js/tde/lensedstarfield.js +32 -25
  25. package/demos/js/tde/tdestar.js +78 -98
  26. package/demos/js/tde/tidalstream.js +24 -8
  27. package/demos/plane3d.html +24 -0
  28. package/demos/platformer.html +43 -0
  29. package/demos/sphere3d.html +24 -0
  30. package/demos/sprite.html +18 -0
  31. package/docs/README.md +230 -222
  32. package/docs/api/FluidSystem.md +173 -0
  33. package/docs/concepts/architecture-overview.md +204 -204
  34. package/docs/concepts/coordinate-system.md +384 -0
  35. package/docs/concepts/rendering-pipeline.md +279 -279
  36. package/docs/concepts/shapes-vs-gameobjects.md +187 -0
  37. package/docs/concepts/two-layer-architecture.md +229 -229
  38. package/docs/fluid-dynamics.md +99 -0
  39. package/docs/getting-started/first-game.md +354 -354
  40. package/docs/getting-started/installation.md +175 -157
  41. package/docs/modules/collision/README.md +2 -2
  42. package/docs/modules/fluent/README.md +6 -6
  43. package/docs/modules/game/README.md +303 -303
  44. package/docs/modules/isometric-camera.md +2 -2
  45. package/docs/modules/isometric.md +1 -1
  46. package/docs/modules/painter/README.md +328 -328
  47. package/docs/modules/particle/README.md +3 -3
  48. package/docs/modules/shapes/README.md +221 -221
  49. package/docs/modules/shapes/base/euclidian.md +123 -123
  50. package/docs/modules/shapes/base/shape.md +262 -262
  51. package/docs/modules/shapes/base/transformable.md +243 -243
  52. package/docs/modules/state/README.md +2 -2
  53. package/docs/modules/util/README.md +1 -1
  54. package/docs/modules/util/camera3d.md +3 -3
  55. package/docs/modules/util/scene3d.md +1 -1
  56. package/package.json +3 -1
  57. package/readme.md +19 -5
  58. package/src/collision/collision.js +75 -0
  59. package/src/game/game.js +11 -5
  60. package/src/game/index.js +2 -1
  61. package/src/game/objects/index.js +3 -0
  62. package/src/game/objects/platformer-scene.js +411 -0
  63. package/src/game/objects/scene.js +14 -0
  64. package/src/game/objects/sprite.js +529 -0
  65. package/src/game/pipeline.js +20 -16
  66. package/src/game/systems/FluidSystem.js +835 -0
  67. package/src/game/systems/index.js +11 -0
  68. package/src/game/ui/button.js +39 -18
  69. package/src/game/ui/cursor.js +14 -0
  70. package/src/game/ui/fps.js +12 -4
  71. package/src/game/ui/index.js +2 -0
  72. package/src/game/ui/stepper.js +549 -0
  73. package/src/game/ui/theme.js +123 -0
  74. package/src/game/ui/togglebutton.js +9 -3
  75. package/src/game/ui/tooltip.js +11 -4
  76. package/src/io/input.js +75 -45
  77. package/src/io/mouse.js +44 -19
  78. package/src/io/touch.js +35 -12
  79. package/src/math/fluid.js +507 -0
  80. package/src/math/index.js +2 -0
  81. package/src/mixins/anchor.js +17 -7
  82. package/src/motion/tweenetik.js +16 -0
  83. package/src/shapes/cube3d.js +599 -0
  84. package/src/shapes/index.js +3 -0
  85. package/src/shapes/plane3d.js +687 -0
  86. package/src/shapes/sphere3d.js +75 -6
  87. package/src/util/camera2d.js +315 -0
  88. package/src/util/camera3d.js +218 -12
  89. package/src/util/index.js +1 -0
  90. package/src/webgl/shaders/plane-shaders.js +332 -0
  91. package/src/webgl/shaders/sphere-shaders.js +4 -2
  92. package/types/fluent.d.ts +361 -0
  93. package/types/game.d.ts +303 -0
  94. package/types/index.d.ts +144 -5
  95. package/types/math.d.ts +361 -0
  96. package/types/motion.d.ts +271 -0
  97. package/types/particle.d.ts +373 -0
  98. package/types/shapes.d.ts +107 -9
  99. package/types/util.d.ts +353 -0
  100. package/types/webgl.d.ts +109 -0
  101. package/disk_example.png +0 -0
  102. package/tde.png +0 -0
@@ -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
+ }
@@ -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";
@@ -109,3 +111,4 @@ export { Hexagon } from "./hexagon.js";
109
111
  export { Heart } from "./heart.js";
110
112
  export { Cross } from "./cross.js";
111
113
  export * from "./text.js";
114
+ export { ImageShape } from "./image.js";