@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
@@ -32,6 +32,9 @@ export class Camera3D {
32
32
  * @param {number} [options.rotationX=0] - Initial X rotation (tilt up/down) in radians
33
33
  * @param {number} [options.rotationY=0] - Initial Y rotation (spin left/right) in radians
34
34
  * @param {number} [options.rotationZ=0] - Initial Z rotation (roll) in radians
35
+ * @param {number} [options.x=0] - Initial X position in world space
36
+ * @param {number} [options.y=0] - Initial Y position in world space
37
+ * @param {number} [options.z=0] - Initial Z position in world space
35
38
  * @param {number} [options.perspective=800] - Perspective distance (higher = less distortion)
36
39
  * @param {number} [options.sensitivity=0.005] - Mouse drag sensitivity
37
40
  * @param {number} [options.minRotationX=-1.5] - Minimum X rotation limit
@@ -49,10 +52,18 @@ export class Camera3D {
49
52
  this.rotationY = options.rotationY ?? 0;
50
53
  this.rotationZ = options.rotationZ ?? 0;
51
54
 
55
+ // Position state (camera location in world space)
56
+ this.x = options.x ?? 0;
57
+ this.y = options.y ?? 0;
58
+ this.z = options.z ?? 0;
59
+
52
60
  // Store initial values for reset
53
61
  this._initialRotationX = this.rotationX;
54
62
  this._initialRotationY = this.rotationY;
55
63
  this._initialRotationZ = this.rotationZ;
64
+ this._initialX = this.x;
65
+ this._initialY = this.y;
66
+ this._initialZ = this.z;
56
67
 
57
68
  // Perspective
58
69
  this.perspective = options.perspective ?? 800;
@@ -86,6 +97,20 @@ export class Camera3D {
86
97
  this._lastMouseY = 0;
87
98
  this._canvas = null;
88
99
  this._boundHandlers = null;
100
+
101
+ // Follow target state
102
+ this._followTarget = null;
103
+ this._followOffset = { x: 0, y: 0, z: 0 };
104
+ this._followLookAt = true; // Auto-orient to look at target's forward direction
105
+ this._followLerp = 0.1; // Interpolation speed (0-1, higher = snappier)
106
+
107
+ // Position animation state
108
+ this._targetX = null;
109
+ this._targetY = null;
110
+ this._targetZ = null;
111
+ this._targetRotationX = null;
112
+ this._targetRotationY = null;
113
+ this._positionLerp = 0.05; // Default smooth movement speed
89
114
  }
90
115
 
91
116
  /**
@@ -96,6 +121,12 @@ export class Camera3D {
96
121
  * @returns {{x: number, y: number, z: number, scale: number}} Projected 2D coordinates and depth info
97
122
  */
98
123
  project(x, y, z) {
124
+ // Translate world relative to camera position
125
+ // (Moving camera right = moving world left)
126
+ x -= this.x;
127
+ y -= this.y;
128
+ z -= this.z;
129
+
99
130
  // Rotate around Z axis (roll)
100
131
  if (this.rotationZ !== 0) {
101
132
  const cosZ = Math.cos(this.rotationZ);
@@ -141,12 +172,78 @@ export class Camera3D {
141
172
  }
142
173
 
143
174
  /**
144
- * Update camera for auto-rotation and inertia (call in update loop)
175
+ * Update camera for auto-rotation, inertia, and follow mode (call in update loop)
145
176
  * @param {number} dt - Delta time in seconds
146
177
  */
147
178
  update(dt) {
148
- // Apply inertia when not dragging
149
- if (this.inertia && !this._isDragging) {
179
+ // Follow target mode - position camera relative to target
180
+ if (this._followTarget) {
181
+ const target = this._followTarget;
182
+ const targetX = (target.x ?? 0) + this._followOffset.x;
183
+ const targetY = (target.y ?? 0) + this._followOffset.y;
184
+ const targetZ = (target.z ?? 0) + this._followOffset.z;
185
+
186
+ // Smooth interpolation to target position
187
+ this.x += (targetX - this.x) * this._followLerp;
188
+ this.y += (targetY - this.y) * this._followLerp;
189
+ this.z += (targetZ - this.z) * this._followLerp;
190
+
191
+ // Auto-orient to look toward origin (or specified look target)
192
+ if (this._followLookAt) {
193
+ const lookX = this._followLookAtTarget?.x ?? 0;
194
+ const lookY = this._followLookAtTarget?.y ?? 0;
195
+ const lookZ = this._followLookAtTarget?.z ?? 0;
196
+
197
+ // Calculate direction from camera to look target
198
+ const dx = lookX - this.x;
199
+ const dy = lookY - this.y;
200
+ const dz = lookZ - this.z;
201
+ const distXZ = Math.sqrt(dx * dx + dz * dz);
202
+
203
+ // Target rotation to face the look target
204
+ const targetRotY = Math.atan2(dx, dz);
205
+ const targetRotX = Math.atan2(-dy, distXZ);
206
+
207
+ // Smooth rotation interpolation
208
+ this.rotationY += this._angleDiff(this.rotationY, targetRotY) * this._followLerp;
209
+ this.rotationX += (targetRotX - this.rotationX) * this._followLerp;
210
+ }
211
+ }
212
+ // Animated position movement (moveTo)
213
+ else if (this._targetX !== null) {
214
+ const lerp = this._positionLerp;
215
+
216
+ // Interpolate position
217
+ this.x += (this._targetX - this.x) * lerp;
218
+ this.y += (this._targetY - this.y) * lerp;
219
+ this.z += (this._targetZ - this.z) * lerp;
220
+
221
+ // Interpolate rotation if targets set
222
+ if (this._targetRotationX !== null) {
223
+ this.rotationX += (this._targetRotationX - this.rotationX) * lerp;
224
+ }
225
+ if (this._targetRotationY !== null) {
226
+ this.rotationY += this._angleDiff(this.rotationY, this._targetRotationY) * lerp;
227
+ }
228
+
229
+ // Check if we've arrived (close enough)
230
+ const posDist = Math.abs(this._targetX - this.x) +
231
+ Math.abs(this._targetY - this.y) +
232
+ Math.abs(this._targetZ - this.z);
233
+ if (posDist < 0.1) {
234
+ this.x = this._targetX;
235
+ this.y = this._targetY;
236
+ this.z = this._targetZ;
237
+ this._targetX = null;
238
+ this._targetY = null;
239
+ this._targetZ = null;
240
+ this._targetRotationX = null;
241
+ this._targetRotationY = null;
242
+ }
243
+ }
244
+
245
+ // Apply inertia when not dragging and not following
246
+ if (this.inertia && !this._isDragging && !this._followTarget) {
150
247
  // Apply velocity to rotation
151
248
  if (Math.abs(this._velocityX) > 0.0001 || Math.abs(this._velocityY) > 0.0001) {
152
249
  this.rotationY += this._velocityY;
@@ -167,8 +264,8 @@ export class Camera3D {
167
264
  }
168
265
  }
169
266
 
170
- // Auto-rotate when not dragging and no significant velocity
171
- if (this.autoRotate && !this._isDragging) {
267
+ // Auto-rotate when not dragging, not following, and no significant velocity
268
+ if (this.autoRotate && !this._isDragging && !this._followTarget) {
172
269
  const hasVelocity = Math.abs(this._velocityX) > 0.001 || Math.abs(this._velocityY) > 0.001;
173
270
  if (!hasVelocity) {
174
271
  const delta = this.autoRotateSpeed * dt;
@@ -187,6 +284,17 @@ export class Camera3D {
187
284
  }
188
285
  }
189
286
 
287
+ /**
288
+ * Calculate shortest angle difference (handles wraparound)
289
+ * @private
290
+ */
291
+ _angleDiff(from, to) {
292
+ let diff = to - from;
293
+ while (diff > Math.PI) diff -= Math.PI * 2;
294
+ while (diff < -Math.PI) diff += Math.PI * 2;
295
+ return diff;
296
+ }
297
+
190
298
  /**
191
299
  * Enable mouse/touch drag rotation on a canvas
192
300
  * @param {HTMLCanvasElement} canvas - The canvas element to attach controls to
@@ -360,15 +468,22 @@ export class Camera3D {
360
468
  }
361
469
 
362
470
  /**
363
- * Reset rotation to initial values and stop inertia
471
+ * Reset rotation and position to initial values, stop inertia and following
364
472
  * @returns {Camera3D} Returns this for chaining
365
473
  */
366
474
  reset() {
367
475
  this.rotationX = this._initialRotationX;
368
476
  this.rotationY = this._initialRotationY;
369
477
  this.rotationZ = this._initialRotationZ;
478
+ this.x = this._initialX;
479
+ this.y = this._initialY;
480
+ this.z = this._initialZ;
370
481
  this._velocityX = 0;
371
482
  this._velocityY = 0;
483
+ this._followTarget = null;
484
+ this._targetX = null;
485
+ this._targetY = null;
486
+ this._targetZ = null;
372
487
  return this;
373
488
  }
374
489
 
@@ -382,6 +497,91 @@ export class Camera3D {
382
497
  return this;
383
498
  }
384
499
 
500
+ /**
501
+ * Set camera position in world space
502
+ * @param {number} x - X position
503
+ * @param {number} y - Y position
504
+ * @param {number} z - Z position
505
+ * @returns {Camera3D} Returns this for chaining
506
+ */
507
+ setPosition(x, y, z) {
508
+ this.x = x;
509
+ this.y = y;
510
+ this.z = z;
511
+ return this;
512
+ }
513
+
514
+ /**
515
+ * Animate camera to a new position (and optionally rotation)
516
+ * @param {number} x - Target X position
517
+ * @param {number} y - Target Y position
518
+ * @param {number} z - Target Z position
519
+ * @param {object} [options] - Animation options
520
+ * @param {number} [options.rotationX] - Target X rotation
521
+ * @param {number} [options.rotationY] - Target Y rotation
522
+ * @param {number} [options.lerp=0.05] - Interpolation speed (0-1)
523
+ * @returns {Camera3D} Returns this for chaining
524
+ */
525
+ moveTo(x, y, z, options = {}) {
526
+ this._targetX = x;
527
+ this._targetY = y;
528
+ this._targetZ = z;
529
+ this._targetRotationX = options.rotationX ?? null;
530
+ this._targetRotationY = options.rotationY ?? null;
531
+ this._positionLerp = options.lerp ?? 0.05;
532
+ return this;
533
+ }
534
+
535
+ /**
536
+ * Follow a target object (position camera relative to target)
537
+ * @param {object} target - Object with x, y, z properties to follow
538
+ * @param {object} [options] - Follow options
539
+ * @param {number} [options.offsetX=0] - X offset from target
540
+ * @param {number} [options.offsetY=0] - Y offset from target
541
+ * @param {number} [options.offsetZ=0] - Z offset from target
542
+ * @param {boolean} [options.lookAt=true] - Auto-orient to look at lookAtTarget
543
+ * @param {object} [options.lookAtTarget=null] - Point to look at (default: origin)
544
+ * @param {number} [options.lerp=0.1] - Interpolation speed (0-1, higher = snappier)
545
+ * @returns {Camera3D} Returns this for chaining
546
+ */
547
+ follow(target, options = {}) {
548
+ this._followTarget = target;
549
+ this._followOffset = {
550
+ x: options.offsetX ?? 0,
551
+ y: options.offsetY ?? 0,
552
+ z: options.offsetZ ?? 0,
553
+ };
554
+ this._followLookAt = options.lookAt ?? true;
555
+ this._followLookAtTarget = options.lookAtTarget ?? null;
556
+ this._followLerp = options.lerp ?? 0.1;
557
+ return this;
558
+ }
559
+
560
+ /**
561
+ * Stop following target and optionally reset to initial position
562
+ * @param {boolean} [resetPosition=false] - Whether to animate back to initial position
563
+ * @returns {Camera3D} Returns this for chaining
564
+ */
565
+ unfollow(resetPosition = false) {
566
+ this._followTarget = null;
567
+ if (resetPosition) {
568
+ this.moveTo(this._initialX, this._initialY, this._initialZ, {
569
+ rotationX: this._initialRotationX,
570
+ rotationY: this._initialRotationY,
571
+ lerp: 0.05,
572
+ });
573
+ }
574
+ return this;
575
+ }
576
+
577
+ /**
578
+ * Check if camera is currently following a target
579
+ * @returns {boolean} True if following a target
580
+ */
581
+ isFollowing() {
582
+ return this._followTarget !== null;
583
+ }
584
+
385
585
  /**
386
586
  * Set rotation angles
387
587
  * @param {number} x - X rotation in radians
@@ -424,15 +624,21 @@ export class Camera3D {
424
624
  }
425
625
 
426
626
  /**
427
- * Look at a specific point (sets rotation to face that direction)
428
- * @param {number} x - Target X
429
- * @param {number} y - Target Y
430
- * @param {number} z - Target Z
627
+ * Look at a specific point (sets rotation to face that direction from current position)
628
+ * @param {number} x - Target X in world space
629
+ * @param {number} y - Target Y in world space
630
+ * @param {number} z - Target Z in world space
431
631
  * @returns {Camera3D} Returns this for chaining
432
632
  */
433
633
  lookAt(x, y, z) {
434
- this.rotationY = Math.atan2(x, z);
435
- this.rotationX = Math.atan2(y, Math.sqrt(x * x + z * z));
634
+ // Calculate direction from camera position to target
635
+ const dx = x - this.x;
636
+ const dy = y - this.y;
637
+ const dz = z - this.z;
638
+ const distXZ = Math.sqrt(dx * dx + dz * dz);
639
+
640
+ this.rotationY = Math.atan2(dx, dz);
641
+ this.rotationX = Math.atan2(-dy, distXZ);
436
642
  return this;
437
643
  }
438
644
  }
package/src/util/index.js CHANGED
@@ -3,4 +3,5 @@ export * from "./position";
3
3
  export * from "./layout";
4
4
  export { TaskManager } from "./tasks";
5
5
  export { Camera3D } from "./camera3d";
6
+ export { Camera2D } from "./camera2d";
6
7
  export { IsometricCamera } from "./isometric-camera";
@@ -0,0 +1,332 @@
1
+ /**
2
+ * Plane Shaders for WebGL Rendering
3
+ *
4
+ * These shaders render 2D patterns onto a plane quad.
5
+ * Unlike sphere shaders which use ray-sphere intersection,
6
+ * these shaders work directly with UV coordinates.
7
+ */
8
+
9
+ // =============================================================================
10
+ // VERTEX SHADER
11
+ // =============================================================================
12
+
13
+ /**
14
+ * Standard vertex shader for plane rendering
15
+ * Simply passes through position and UV coordinates
16
+ */
17
+ export const PLANE_VERTEX = `
18
+ precision highp float;
19
+
20
+ attribute vec2 aPosition;
21
+ attribute vec2 aUv;
22
+
23
+ varying vec2 vUv;
24
+
25
+ void main() {
26
+ vUv = aUv;
27
+ gl_Position = vec4(aPosition, 0.0, 1.0);
28
+ }
29
+ `;
30
+
31
+ // =============================================================================
32
+ // COMMON SHADER FUNCTIONS
33
+ // =============================================================================
34
+
35
+ /**
36
+ * Common functions included in all plane fragment shaders
37
+ */
38
+ export const PLANE_COMMON = `
39
+ precision highp float;
40
+
41
+ varying vec2 vUv;
42
+
43
+ uniform float uTime;
44
+ uniform vec2 uResolution;
45
+
46
+ // =============================================================================
47
+ // NOISE FUNCTIONS
48
+ // =============================================================================
49
+
50
+ float hash(float n) {
51
+ return fract(sin(n) * 43758.5453123);
52
+ }
53
+
54
+ float hash2(vec2 p) {
55
+ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
56
+ }
57
+
58
+ // 2D Value noise
59
+ float noise2D(vec2 x) {
60
+ vec2 i = floor(x);
61
+ vec2 f = fract(x);
62
+ f = f * f * (3.0 - 2.0 * f);
63
+
64
+ float a = hash2(i);
65
+ float b = hash2(i + vec2(1.0, 0.0));
66
+ float c = hash2(i + vec2(0.0, 1.0));
67
+ float d = hash2(i + vec2(1.0, 1.0));
68
+
69
+ return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
70
+ }
71
+
72
+ // FBM (Fractional Brownian Motion)
73
+ float fbm2D(vec2 p, int octaves) {
74
+ float value = 0.0;
75
+ float amplitude = 0.5;
76
+ float frequency = 1.0;
77
+
78
+ for (int i = 0; i < 8; i++) {
79
+ if (i >= octaves) break;
80
+ value += amplitude * noise2D(p * frequency);
81
+ frequency *= 2.0;
82
+ amplitude *= 0.5;
83
+ }
84
+
85
+ return value;
86
+ }
87
+ `;
88
+
89
+ // =============================================================================
90
+ // GRADIENT SHADER
91
+ // =============================================================================
92
+
93
+ /**
94
+ * Linear gradient shader
95
+ * Uniforms:
96
+ * - uColor1: Start color [r, g, b] (0-1)
97
+ * - uColor2: End color [r, g, b] (0-1)
98
+ * - uAngle: Gradient angle in radians (0 = horizontal, PI/2 = vertical)
99
+ */
100
+ export const GRADIENT_FRAGMENT = `
101
+ ${PLANE_COMMON}
102
+
103
+ uniform vec3 uColor1;
104
+ uniform vec3 uColor2;
105
+ uniform float uAngle;
106
+
107
+ void main() {
108
+ vec2 uv = vUv;
109
+
110
+ // Calculate gradient direction from angle
111
+ vec2 dir = vec2(cos(uAngle), sin(uAngle));
112
+
113
+ // Project UV onto gradient direction
114
+ // Center at 0.5 so gradient goes through middle
115
+ float t = dot(uv - 0.5, dir) + 0.5;
116
+ t = clamp(t, 0.0, 1.0);
117
+
118
+ // Interpolate colors
119
+ vec3 color = mix(uColor1, uColor2, t);
120
+
121
+ gl_FragColor = vec4(color, 1.0);
122
+ }
123
+ `;
124
+
125
+ // =============================================================================
126
+ // GRID SHADER
127
+ // =============================================================================
128
+
129
+ /**
130
+ * Grid pattern shader
131
+ * Uniforms:
132
+ * - uLineColor: Grid line color [r, g, b] (0-1)
133
+ * - uBackgroundColor: Background color [r, g, b] (0-1)
134
+ * - uGridSize: Number of grid cells (e.g., 8.0 = 8x8 grid)
135
+ * - uLineWidth: Line width as fraction of cell (e.g., 0.05)
136
+ */
137
+ export const GRID_FRAGMENT = `
138
+ ${PLANE_COMMON}
139
+
140
+ uniform vec3 uLineColor;
141
+ uniform vec3 uBackgroundColor;
142
+ uniform float uGridSize;
143
+ uniform float uLineWidth;
144
+
145
+ void main() {
146
+ vec2 uv = vUv;
147
+
148
+ // Scale UV to grid space
149
+ vec2 grid = fract(uv * uGridSize);
150
+
151
+ // Calculate distance to nearest grid line
152
+ float lineX = min(grid.x, 1.0 - grid.x);
153
+ float lineY = min(grid.y, 1.0 - grid.y);
154
+
155
+ // Smoothstep for anti-aliased lines
156
+ float halfWidth = uLineWidth * 0.5;
157
+ float edgeSmooth = 0.01;
158
+
159
+ float lineAlphaX = 1.0 - smoothstep(halfWidth - edgeSmooth, halfWidth + edgeSmooth, lineX);
160
+ float lineAlphaY = 1.0 - smoothstep(halfWidth - edgeSmooth, halfWidth + edgeSmooth, lineY);
161
+ float lineAlpha = max(lineAlphaX, lineAlphaY);
162
+
163
+ // Mix colors
164
+ vec3 color = mix(uBackgroundColor, uLineColor, lineAlpha);
165
+
166
+ gl_FragColor = vec4(color, 1.0);
167
+ }
168
+ `;
169
+
170
+ // =============================================================================
171
+ // CHECKERBOARD SHADER
172
+ // =============================================================================
173
+
174
+ /**
175
+ * Checkerboard pattern shader
176
+ * Uniforms:
177
+ * - uColor1: First square color [r, g, b] (0-1)
178
+ * - uColor2: Second square color [r, g, b] (0-1)
179
+ * - uSize: Number of squares per side (e.g., 8.0 = 8x8)
180
+ */
181
+ export const CHECKERBOARD_FRAGMENT = `
182
+ ${PLANE_COMMON}
183
+
184
+ uniform vec3 uColor1;
185
+ uniform vec3 uColor2;
186
+ uniform float uSize;
187
+
188
+ void main() {
189
+ vec2 uv = vUv;
190
+
191
+ // Calculate which square we're in
192
+ vec2 cell = floor(uv * uSize);
193
+
194
+ // Alternating pattern based on cell coordinates
195
+ float checker = mod(cell.x + cell.y, 2.0);
196
+
197
+ // Select color
198
+ vec3 color = mix(uColor1, uColor2, checker);
199
+
200
+ gl_FragColor = vec4(color, 1.0);
201
+ }
202
+ `;
203
+
204
+ // =============================================================================
205
+ // NOISE SHADER
206
+ // =============================================================================
207
+
208
+ /**
209
+ * Animated noise pattern shader
210
+ * Uniforms:
211
+ * - uColor1: Base color [r, g, b] (0-1)
212
+ * - uColor2: Secondary color [r, g, b] (0-1)
213
+ * - uNoiseScale: Scale of the noise pattern (e.g., 4.0)
214
+ * - uAnimSpeed: Animation speed (e.g., 0.5)
215
+ */
216
+ export const NOISE_FRAGMENT = `
217
+ ${PLANE_COMMON}
218
+
219
+ uniform vec3 uColor1;
220
+ uniform vec3 uColor2;
221
+ uniform float uNoiseScale;
222
+ uniform float uAnimSpeed;
223
+
224
+ void main() {
225
+ vec2 uv = vUv;
226
+
227
+ // Animated noise
228
+ float n = fbm2D(uv * uNoiseScale + uTime * uAnimSpeed, 4);
229
+
230
+ // Map noise to colors
231
+ vec3 color = mix(uColor1, uColor2, n);
232
+
233
+ gl_FragColor = vec4(color, 1.0);
234
+ }
235
+ `;
236
+
237
+ // =============================================================================
238
+ // RADIAL GRADIENT SHADER
239
+ // =============================================================================
240
+
241
+ /**
242
+ * Radial gradient shader
243
+ * Uniforms:
244
+ * - uColor1: Center color [r, g, b] (0-1)
245
+ * - uColor2: Edge color [r, g, b] (0-1)
246
+ * - uCenterX: Center X position (0-1, default 0.5)
247
+ * - uCenterY: Center Y position (0-1, default 0.5)
248
+ * - uRadius: Gradient radius (default 0.5)
249
+ */
250
+ export const RADIAL_GRADIENT_FRAGMENT = `
251
+ ${PLANE_COMMON}
252
+
253
+ uniform vec3 uColor1;
254
+ uniform vec3 uColor2;
255
+ uniform float uCenterX;
256
+ uniform float uCenterY;
257
+ uniform float uRadius;
258
+
259
+ void main() {
260
+ vec2 uv = vUv;
261
+ vec2 center = vec2(uCenterX, uCenterY);
262
+
263
+ // Calculate distance from center
264
+ float dist = length(uv - center);
265
+
266
+ // Normalize by radius
267
+ float t = clamp(dist / uRadius, 0.0, 1.0);
268
+
269
+ // Interpolate colors
270
+ vec3 color = mix(uColor1, uColor2, t);
271
+
272
+ gl_FragColor = vec4(color, 1.0);
273
+ }
274
+ `;
275
+
276
+ // =============================================================================
277
+ // STRIPES SHADER
278
+ // =============================================================================
279
+
280
+ /**
281
+ * Stripe pattern shader
282
+ * Uniforms:
283
+ * - uColor1: First stripe color [r, g, b] (0-1)
284
+ * - uColor2: Second stripe color [r, g, b] (0-1)
285
+ * - uStripeCount: Number of stripes
286
+ * - uAngle: Stripe angle in radians
287
+ */
288
+ export const STRIPES_FRAGMENT = `
289
+ ${PLANE_COMMON}
290
+
291
+ uniform vec3 uColor1;
292
+ uniform vec3 uColor2;
293
+ uniform float uStripeCount;
294
+ uniform float uAngle;
295
+
296
+ void main() {
297
+ vec2 uv = vUv;
298
+
299
+ // Rotate UV by angle
300
+ vec2 center = vec2(0.5);
301
+ vec2 rotated = uv - center;
302
+ float c = cos(uAngle);
303
+ float s = sin(uAngle);
304
+ rotated = vec2(rotated.x * c - rotated.y * s, rotated.x * s + rotated.y * c);
305
+ rotated += center;
306
+
307
+ // Create stripes along one axis
308
+ float stripe = floor(mod(rotated.x * uStripeCount, 2.0));
309
+
310
+ // Select color
311
+ vec3 color = mix(uColor1, uColor2, stripe);
312
+
313
+ gl_FragColor = vec4(color, 1.0);
314
+ }
315
+ `;
316
+
317
+ // =============================================================================
318
+ // SHADER EXPORT
319
+ // =============================================================================
320
+
321
+ export const PLANE_SHADERS = {
322
+ vertex: PLANE_VERTEX,
323
+ common: PLANE_COMMON,
324
+ gradient: GRADIENT_FRAGMENT,
325
+ grid: GRID_FRAGMENT,
326
+ checkerboard: CHECKERBOARD_FRAGMENT,
327
+ noise: NOISE_FRAGMENT,
328
+ radialGradient: RADIAL_GRADIENT_FRAGMENT,
329
+ stripes: STRIPES_FRAGMENT,
330
+ };
331
+
332
+ export default PLANE_SHADERS;
@@ -904,6 +904,7 @@ ${SPHERE_COMMON}
904
904
  uniform vec3 uBaseColor;
905
905
  uniform float uSeed;
906
906
  uniform float uStormIntensity; // 0-1
907
+ uniform float uRotationSpeed; // rotation speed multiplier (default ~0.1)
907
908
 
908
909
  void main() {
909
910
  // Setup ray - camera looking at sphere from fixed position
@@ -930,8 +931,9 @@ void main() {
930
931
  float latitude = asin(rotatedNormal.y); // -PI/2 to PI/2
931
932
  float longitude = atan(rotatedNormal.z, rotatedNormal.x); // -PI to PI
932
933
 
933
- // Animated rotation
934
- float time = uTime * 0.1;
934
+ // Animated rotation (use uRotationSpeed, default to 0.1 if not set)
935
+ float rotSpeed = uRotationSpeed > 0.0 ? uRotationSpeed : 0.1;
936
+ float time = uTime * rotSpeed;
935
937
 
936
938
  // Create bands based on latitude
937
939
  float bands = sin(latitude * 15.0 + time) * 0.5 + 0.5;