@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
@@ -78,6 +78,9 @@ export class Sphere3D extends Shape {
78
78
  * @param {boolean} [options.useShader=false] - Use WebGL shader rendering
79
79
  * @param {string} [options.shaderType='star'] - Shader type: 'star', 'blackHole', 'rockyPlanet', 'gasGiant'
80
80
  * @param {Object} [options.shaderUniforms={}] - Custom shader uniforms
81
+ * @param {number} [options.selfRotationX=0] - Self-rotation around X axis (radians)
82
+ * @param {number} [options.selfRotationY=0] - Self-rotation around Y axis (radians)
83
+ * @param {number} [options.selfRotationZ=0] - Self-rotation around Z axis (radians)
81
84
  */
82
85
  constructor(radius, options = {}) {
83
86
  super(options);
@@ -93,6 +96,11 @@ export class Sphere3D extends Shape {
93
96
  this.shaderUniforms = options.shaderUniforms ?? {};
94
97
  this._shaderInitialized = false;
95
98
 
99
+ // Self-rotation (for Canvas 2D mode - shader uses uRotationSpeed)
100
+ this.selfRotationX = options.selfRotationX ?? 0;
101
+ this.selfRotationY = options.selfRotationY ?? 0;
102
+ this.selfRotationZ = options.selfRotationZ ?? 0;
103
+
96
104
  // Generate sphere geometry (for Canvas 2D fallback)
97
105
  this._generateGeometry();
98
106
  }
@@ -282,6 +290,48 @@ export class Sphere3D extends Shape {
282
290
  }
283
291
  }
284
292
 
293
+ /**
294
+ * Apply self-rotation to a point (vertex or normal)
295
+ * @param {number} x - X component
296
+ * @param {number} y - Y component
297
+ * @param {number} z - Z component
298
+ * @returns {{x: number, y: number, z: number}} Rotated point
299
+ * @private
300
+ */
301
+ _applySelfRotation(x, y, z) {
302
+ // Rotate around Y axis first (most common for spinning objects)
303
+ if (this.selfRotationY !== 0) {
304
+ const cosY = Math.cos(this.selfRotationY);
305
+ const sinY = Math.sin(this.selfRotationY);
306
+ const x1 = x * cosY - z * sinY;
307
+ const z1 = x * sinY + z * cosY;
308
+ x = x1;
309
+ z = z1;
310
+ }
311
+
312
+ // Rotate around X axis
313
+ if (this.selfRotationX !== 0) {
314
+ const cosX = Math.cos(this.selfRotationX);
315
+ const sinX = Math.sin(this.selfRotationX);
316
+ const y1 = y * cosX - z * sinX;
317
+ const z1 = y * sinX + z * cosX;
318
+ y = y1;
319
+ z = z1;
320
+ }
321
+
322
+ // Rotate around Z axis
323
+ if (this.selfRotationZ !== 0) {
324
+ const cosZ = Math.cos(this.selfRotationZ);
325
+ const sinZ = Math.sin(this.selfRotationZ);
326
+ const x1 = x * cosZ - y * sinZ;
327
+ const y1 = x * sinZ + y * cosZ;
328
+ x = x1;
329
+ y = y1;
330
+ }
331
+
332
+ return { x, y, z };
333
+ }
334
+
285
335
  /**
286
336
  * Calculate lighting intensity based on surface normal
287
337
  * @param {number} nx - Normal x component
@@ -398,18 +448,37 @@ export class Sphere3D extends Shape {
398
448
 
399
449
  // Project all vertices and normals through the camera
400
450
  // Add position offset so sphere appears at correct world position
451
+ const hasSelfRotation = this.selfRotationX !== 0 || this.selfRotationY !== 0 || this.selfRotationZ !== 0;
452
+
401
453
  const projectedVertices = this.vertices.map((v) => {
454
+ // Apply self-rotation to vertex position
455
+ let vx = v.x;
456
+ let vy = v.y;
457
+ let vz = v.z;
458
+ let nx = v.nx;
459
+ let ny = v.ny;
460
+ let nz = v.nz;
461
+
462
+ if (hasSelfRotation) {
463
+ const rotatedPos = this._applySelfRotation(vx, vy, vz);
464
+ vx = rotatedPos.x;
465
+ vy = rotatedPos.y;
466
+ vz = rotatedPos.z;
467
+
468
+ const rotatedNormal = this._applySelfRotation(nx, ny, nz);
469
+ nx = rotatedNormal.x;
470
+ ny = rotatedNormal.y;
471
+ nz = rotatedNormal.z;
472
+ }
473
+
402
474
  const projected = this.camera.project(
403
- v.x + (this.x || 0),
404
- v.y + (this.y || 0),
405
- v.z + (this.z || 0)
475
+ vx + (this.x || 0),
476
+ vy + (this.y || 0),
477
+ vz + (this.z || 0)
406
478
  );
407
479
 
408
480
  // Rotate normals using the same rotation sequence as Camera3D.project
409
481
  // (Z, then Y, then X)
410
- let nx = v.nx;
411
- let ny = v.ny;
412
- let nz = v.nz;
413
482
 
414
483
  // Rotate around Z axis (roll)
415
484
  if (this.camera.rotationZ !== 0) {
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Camera2D - 2D camera with smooth following, deadzone, zoom, and bounds
3
+ *
4
+ * Provides viewport management for platformer-style games with smooth
5
+ * target following, configurable deadzone, zoom support, and screen shake.
6
+ *
7
+ * @example
8
+ * // Basic usage with target following
9
+ * const camera = new Camera2D({
10
+ * target: player,
11
+ * viewportWidth: 800,
12
+ * viewportHeight: 600,
13
+ * lerp: 0.1,
14
+ * });
15
+ *
16
+ * // With deadzone (player can move freely inside without camera moving)
17
+ * const camera = new Camera2D({
18
+ * target: player,
19
+ * deadzone: { width: 100, height: 50 },
20
+ * bounds: { minX: 0, maxX: 2000, minY: 0, maxY: 600 },
21
+ * });
22
+ *
23
+ * // In update loop
24
+ * camera.update(dt);
25
+ * const offset = camera.getOffset();
26
+ * // Use offset.x, offset.y for rendering
27
+ */
28
+ export class Camera2D {
29
+ /**
30
+ * Create a new Camera2D instance
31
+ * @param {Object} options - Configuration options
32
+ * @param {Object} [options.target=null] - Target object with x, y to follow
33
+ * @param {Object} [options.deadzone=null] - Deadzone { width, height } where target can move freely
34
+ * @param {number} [options.lerp=0.1] - Smooth follow speed (0-1, higher = snappier)
35
+ * @param {number} [options.zoom=1] - Zoom scale factor
36
+ * @param {Object} [options.bounds=null] - Scroll bounds { minX, maxX, minY, maxY }
37
+ * @param {number} [options.viewportWidth=800] - Viewport width in pixels
38
+ * @param {number} [options.viewportHeight=600] - Viewport height in pixels
39
+ * @param {number} [options.offsetX=0] - Fixed X offset from target
40
+ * @param {number} [options.offsetY=0] - Fixed Y offset from target
41
+ */
42
+ constructor(options = {}) {
43
+ /** @type {number} Current camera X position (top-left of viewport in world space) */
44
+ this.x = 0;
45
+
46
+ /** @type {number} Current camera Y position (top-left of viewport in world space) */
47
+ this.y = 0;
48
+
49
+ /** @type {Object|null} Target object with x, y properties to follow */
50
+ this.target = options.target ?? null;
51
+
52
+ /** @type {Object|null} Deadzone dimensions { width, height } */
53
+ this.deadzone = options.deadzone ?? null;
54
+
55
+ /** @type {number} Smooth follow interpolation speed (0-1) */
56
+ this.lerp = options.lerp ?? 0.1;
57
+
58
+ /** @type {number} Zoom scale factor */
59
+ this.zoom = options.zoom ?? 1;
60
+
61
+ /** @type {Object|null} Scroll bounds { minX, maxX, minY, maxY } */
62
+ this.bounds = options.bounds ?? null;
63
+
64
+ /** @type {number} Viewport width in pixels */
65
+ this.viewportWidth = options.viewportWidth ?? 800;
66
+
67
+ /** @type {number} Viewport height in pixels */
68
+ this.viewportHeight = options.viewportHeight ?? 600;
69
+
70
+ /** @type {number} Fixed X offset from target */
71
+ this.offsetX = options.offsetX ?? 0;
72
+
73
+ /** @type {number} Fixed Y offset from target */
74
+ this.offsetY = options.offsetY ?? 0;
75
+
76
+ // Shake state
77
+ /** @private */
78
+ this._shakeIntensity = 0;
79
+ /** @private */
80
+ this._shakeDuration = 0;
81
+ /** @private */
82
+ this._shakeTime = 0;
83
+ /** @private */
84
+ this._shakeOffsetX = 0;
85
+ /** @private */
86
+ this._shakeOffsetY = 0;
87
+
88
+ // Store initial values for reset
89
+ this._initialX = this.x;
90
+ this._initialY = this.y;
91
+ }
92
+
93
+ /**
94
+ * Update camera position to follow target
95
+ * @param {number} dt - Delta time in seconds
96
+ */
97
+ update(dt) {
98
+ if (!this.target) return;
99
+
100
+ // Target center position in world space
101
+ const targetX = this.target.x + this.offsetX;
102
+ const targetY = this.target.y + this.offsetY;
103
+
104
+ // Current camera center position
105
+ const cameraCenterX = this.x + this.viewportWidth / 2;
106
+ const cameraCenterY = this.y + this.viewportHeight / 2;
107
+
108
+ // Calculate desired camera position
109
+ let desiredX = this.x;
110
+ let desiredY = this.y;
111
+
112
+ if (this.deadzone) {
113
+ // Only move camera if target leaves deadzone
114
+ const halfDeadW = this.deadzone.width / 2;
115
+ const halfDeadH = this.deadzone.height / 2;
116
+
117
+ // Check horizontal deadzone
118
+ if (targetX < cameraCenterX - halfDeadW) {
119
+ desiredX = targetX + halfDeadW - this.viewportWidth / 2;
120
+ } else if (targetX > cameraCenterX + halfDeadW) {
121
+ desiredX = targetX - halfDeadW - this.viewportWidth / 2;
122
+ }
123
+
124
+ // Check vertical deadzone
125
+ if (targetY < cameraCenterY - halfDeadH) {
126
+ desiredY = targetY + halfDeadH - this.viewportHeight / 2;
127
+ } else if (targetY > cameraCenterY + halfDeadH) {
128
+ desiredY = targetY - halfDeadH - this.viewportHeight / 2;
129
+ }
130
+ } else {
131
+ // Center on target
132
+ desiredX = targetX - this.viewportWidth / 2;
133
+ desiredY = targetY - this.viewportHeight / 2;
134
+ }
135
+
136
+ // Smooth lerp toward desired position
137
+ this.x += (desiredX - this.x) * this.lerp;
138
+ this.y += (desiredY - this.y) * this.lerp;
139
+
140
+ // Apply bounds constraints
141
+ if (this.bounds) {
142
+ const maxScrollX = this.bounds.maxX - this.viewportWidth;
143
+ const maxScrollY = this.bounds.maxY - this.viewportHeight;
144
+
145
+ this.x = Math.max(this.bounds.minX, Math.min(this.x, maxScrollX));
146
+ this.y = Math.max(this.bounds.minY, Math.min(this.y, maxScrollY));
147
+ }
148
+
149
+ // Update shake effect
150
+ this._updateShake(dt);
151
+ }
152
+
153
+ /**
154
+ * Update shake effect
155
+ * @private
156
+ * @param {number} dt - Delta time in seconds
157
+ */
158
+ _updateShake(dt) {
159
+ if (this._shakeDuration > 0 && this._shakeTime < this._shakeDuration) {
160
+ this._shakeTime += dt;
161
+ const progress = this._shakeTime / this._shakeDuration;
162
+ const decay = 1 - progress; // Linear decay
163
+
164
+ this._shakeOffsetX = (Math.random() - 0.5) * 2 * this._shakeIntensity * decay;
165
+ this._shakeOffsetY = (Math.random() - 0.5) * 2 * this._shakeIntensity * decay;
166
+
167
+ if (this._shakeTime >= this._shakeDuration) {
168
+ this._shakeOffsetX = 0;
169
+ this._shakeOffsetY = 0;
170
+ this._shakeDuration = 0;
171
+ this._shakeTime = 0;
172
+ }
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Get the camera offset for rendering (includes shake)
178
+ * Override this in subclasses for custom scroll behavior
179
+ * @returns {{x: number, y: number}} Camera offset
180
+ */
181
+ getOffset() {
182
+ return {
183
+ x: this.x + this._shakeOffsetX,
184
+ y: this.y + this._shakeOffsetY,
185
+ };
186
+ }
187
+
188
+ /**
189
+ * Set camera position directly (bypasses follow)
190
+ * @param {number} x - X position
191
+ * @param {number} y - Y position
192
+ * @returns {Camera2D} this for chaining
193
+ */
194
+ setPosition(x, y) {
195
+ this.x = x;
196
+ this.y = y;
197
+ return this;
198
+ }
199
+
200
+ /**
201
+ * Set new target to follow
202
+ * @param {Object} target - Object with x, y properties
203
+ * @returns {Camera2D} this for chaining
204
+ */
205
+ setTarget(target) {
206
+ this.target = target;
207
+ return this;
208
+ }
209
+
210
+ /**
211
+ * Apply camera shake effect
212
+ * @param {number} intensity - Shake amount in pixels
213
+ * @param {number} duration - Shake duration in seconds
214
+ * @returns {Camera2D} this for chaining
215
+ */
216
+ shake(intensity, duration) {
217
+ this._shakeIntensity = intensity;
218
+ this._shakeDuration = duration;
219
+ this._shakeTime = 0;
220
+ return this;
221
+ }
222
+
223
+ /**
224
+ * Stop any active shake immediately
225
+ * @returns {Camera2D} this for chaining
226
+ */
227
+ stopShake() {
228
+ this._shakeIntensity = 0;
229
+ this._shakeDuration = 0;
230
+ this._shakeTime = 0;
231
+ this._shakeOffsetX = 0;
232
+ this._shakeOffsetY = 0;
233
+ return this;
234
+ }
235
+
236
+ /**
237
+ * Check if a point/bounds is visible in the viewport
238
+ * @param {Object} bounds - Object with x, y, width, height (world coordinates)
239
+ * @returns {boolean} True if any part of bounds is visible
240
+ */
241
+ isVisible(bounds) {
242
+ if (!bounds) return false;
243
+
244
+ const cameraRight = this.x + this.viewportWidth;
245
+ const cameraBottom = this.y + this.viewportHeight;
246
+ const boundsRight = bounds.x + (bounds.width || 0);
247
+ const boundsBottom = bounds.y + (bounds.height || 0);
248
+
249
+ // AABB intersection test
250
+ return !(
251
+ bounds.x > cameraRight ||
252
+ boundsRight < this.x ||
253
+ bounds.y > cameraBottom ||
254
+ boundsBottom < this.y
255
+ );
256
+ }
257
+
258
+ /**
259
+ * Convert screen coordinates to world coordinates
260
+ * @param {number} screenX - Screen X position
261
+ * @param {number} screenY - Screen Y position
262
+ * @returns {{x: number, y: number}} World coordinates
263
+ */
264
+ screenToWorld(screenX, screenY) {
265
+ return {
266
+ x: screenX / this.zoom + this.x,
267
+ y: screenY / this.zoom + this.y,
268
+ };
269
+ }
270
+
271
+ /**
272
+ * Convert world coordinates to screen coordinates
273
+ * @param {number} worldX - World X position
274
+ * @param {number} worldY - World Y position
275
+ * @returns {{x: number, y: number}} Screen coordinates
276
+ */
277
+ worldToScreen(worldX, worldY) {
278
+ return {
279
+ x: (worldX - this.x) * this.zoom,
280
+ y: (worldY - this.y) * this.zoom,
281
+ };
282
+ }
283
+
284
+ /**
285
+ * Get the camera's world bounds (what's currently visible)
286
+ * @returns {{x: number, y: number, width: number, height: number}}
287
+ */
288
+ getWorldBounds() {
289
+ return {
290
+ x: this.x,
291
+ y: this.y,
292
+ width: this.viewportWidth / this.zoom,
293
+ height: this.viewportHeight / this.zoom,
294
+ };
295
+ }
296
+
297
+ /**
298
+ * Reset camera to initial position and clear shake
299
+ * @returns {Camera2D} this for chaining
300
+ */
301
+ reset() {
302
+ this.x = this._initialX;
303
+ this.y = this._initialY;
304
+ this.stopShake();
305
+ return this;
306
+ }
307
+
308
+ /**
309
+ * Check if camera is currently shaking
310
+ * @returns {boolean}
311
+ */
312
+ isShaking() {
313
+ return this._shakeDuration > 0 && this._shakeTime < this._shakeDuration;
314
+ }
315
+ }