shaderpad 1.0.0-beta.29 → 1.0.0-beta.31

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/README.md CHANGED
@@ -178,7 +178,7 @@ shader.updateUniforms(
178
178
 
179
179
  #### `initializeTexture(name, source, options?)`
180
180
 
181
- Initialize a texture from an image, video, or canvas element.
181
+ Initialize a texture from an image, video, canvas element, or typed array.
182
182
 
183
183
  ```typescript
184
184
  // Initialize a texture from an image.
@@ -188,15 +188,49 @@ img.onload = () => {
188
188
  shader.initializeTexture('u_texture', img);
189
189
  };
190
190
 
191
+ // Initialize a texture from a typed array (Float32Array, Uint8Array, etc.).
192
+ const data = new Float32Array(width * height * 4); // RGBA data
193
+ shader.initializeTexture(
194
+ 'u_custom',
195
+ {
196
+ data,
197
+ width,
198
+ height,
199
+ },
200
+ {
201
+ internalFormat: gl.RGBA32F,
202
+ type: gl.FLOAT,
203
+ minFilter: gl.NEAREST,
204
+ magFilter: gl.NEAREST,
205
+ }
206
+ );
207
+
191
208
  // Initialize a texture with history (stores previous frames).
192
209
  shader.initializeTexture('u_webcam', videoElement, { history: 30 });
210
+
211
+ // Preserve Y orientation for DOM sources (don't flip vertically).
212
+ shader.initializeTexture('u_canvas', canvasElement, { preserveY: true });
193
213
  ```
194
214
 
195
215
  **Parameters:**
196
216
 
197
217
  - `name` (string): The name of the texture uniform as declared in your shader
198
- - `source` (HTMLImageElement | HTMLVideoElement | HTMLCanvasElement): The texture source
199
- - `options` (optional): `{ history?: number }` - Number of previous frames to store
218
+ - `source` (HTMLImageElement | HTMLVideoElement | HTMLCanvasElement | CustomTexture): The texture source
219
+ - `options` (optional): Texture options (see below)
220
+
221
+ **Texture Options:**
222
+
223
+ - `history?: number` - Number of previous frames to store (creates a `sampler2DArray`)
224
+ - `preserveY?: boolean` - For DOM sources only: if `true`, don't flip vertically (default: `false`, flips to match WebGL's bottom-up convention)
225
+ - `internalFormat?: number` - WebGL internal format (e.g., `gl.RGBA8`, `gl.RGBA32F`)
226
+ - `format?: number` - WebGL format (default: `gl.RGBA`)
227
+ - `type?: number` - WebGL data type (default: `gl.UNSIGNED_BYTE` for DOM sources, must be specified for typed arrays)
228
+ - `minFilter?: number` - Minification filter (default: `gl.LINEAR`)
229
+ - `magFilter?: number` - Magnification filter (default: `gl.LINEAR`)
230
+ - `wrapS?: number` - Wrap mode for S coordinate (default: `gl.CLAMP_TO_EDGE`)
231
+ - `wrapT?: number` - Wrap mode for T coordinate (default: `gl.CLAMP_TO_EDGE`)
232
+
233
+ **Note:** For typed array sources (`CustomTexture`), you must provide data in bottom-up orientation (WebGL convention). The `preserveY` option is ignored for typed arrays.
200
234
 
201
235
  #### `updateTextures(updates)`
202
236
 
@@ -206,12 +240,29 @@ Update one or more textures. Useful for updating video textures each frame.
206
240
  shader.updateTextures({
207
241
  u_webcam: videoElement,
208
242
  u_overlay: overlayCanvas,
243
+ u_custom: {
244
+ data: typedArray,
245
+ width,
246
+ height,
247
+ },
248
+ });
249
+
250
+ // Typed arrays can be partially updated.
251
+ shader.updateTextures({
252
+ u_custom: {
253
+ data: partialData,
254
+ width: regionWidth,
255
+ height: regionHeight,
256
+ isPartial: true,
257
+ x: offsetX,
258
+ y: offsetY,
259
+ },
209
260
  });
210
261
  ```
211
262
 
212
263
  **Parameters:**
213
264
 
214
- - `updates` (Record<string, TextureSource>): Object mapping texture names to their new sources
265
+ - `updates` (Record<string, TextureSource | PartialCustomTexture>): Object mapping texture names to their new sources
215
266
 
216
267
  ### Lifecycle methods
217
268
 
@@ -289,6 +340,14 @@ float zIndex = historyZ(u_webcam, u_webcamFrameOffset, nFramesAgo);
289
340
  vec4 historyColor = texture(u_webcam, vec3(v_uv, zIndex));
290
341
  ```
291
342
 
343
+ ### debug
344
+
345
+ The `debug` option controls whether debug logging is enabled. When enabled, ShaderPad will log warnings when uniforms or textures are not found in the shader. Defaults to `true` in development (when `process.env.NODE_ENV !== 'production'`) and `false` in production builds.
346
+
347
+ ```typescript
348
+ const shader = new ShaderPad(fragmentShaderSrc, { debug: true }); // Explicitly enable debug logging.
349
+ ```
350
+
292
351
  ### plugins
293
352
 
294
353
  ShaderPad supports plugins to add additional functionality. Plugins are imported from separate paths to keep bundle sizes small.
@@ -317,7 +376,7 @@ import ShaderPad from 'shaderpad';
317
376
  import save, { WithSave } from 'shaderpad/plugins/save';
318
377
 
319
378
  const shader = new ShaderPad(fragmentShaderSrc, { plugins: [save()] }) as WithSave<ShaderPad>;
320
- shader.save('my-frame');
379
+ shader.save('filename', 'Optional mobile share text');
321
380
  ```
322
381
 
323
382
  #### face
@@ -340,27 +399,53 @@ const shader = new ShaderPad(fragmentShaderSrc, {
340
399
 
341
400
  **Uniforms:**
342
401
 
343
- | Uniform | Type | Description |
344
- | ----------------- | -------------------- | ------------------------------------------------ |
345
- | `u_maxFaces` | int | Maximum number of faces to detect |
346
- | `u_nFaces` | int | Current number of detected faces |
347
- | `u_faceLandmarks` | vec2[maxFaces * 478] | Landmark positions in UV space |
348
- | `u_leftEye` | vec2[maxFaces] | Left eye positions |
349
- | `u_rightEye` | vec2[maxFaces] | Right eye positions |
350
- | `u_noseTip` | vec2[maxFaces] | Nose tip positions |
351
- | `u_mouth` | vec2[maxFaces] | Mouth center positions |
352
- | `u_faceMask` | sampler2D | Face mask texture (R: mouth, G: face, B: eyes) |
353
- | `u_faceCenter` | vec2[maxFaces] | Center positions of the face mask bounding boxes |
402
+ | Uniform | Type | Description |
403
+ | -------------------- | --------- | ---------------------------------------------------------- |
404
+ | `u_maxFaces` | int | Maximum number of faces to detect |
405
+ | `u_nFaces` | int | Current number of detected faces |
406
+ | `u_faceLandmarksTex` | sampler2D | Raw landmark data texture (use `faceLandmark()` to access) |
407
+ | `u_faceMask` | sampler2D | Face mask texture (R: mouth, G: face, B: eyes) |
408
+
409
+ **Helper functions:**
410
+
411
+ - `faceLandmark(int faceIndex, int landmarkIndex) -> vec4` - Returns landmark data as `vec4(x, y, z, visibility)`. Use `vec2(faceLandmark(...))` to get just the screen position.
412
+ - `inFace(vec2 pos) -> float` - Returns face mask value at position (green channel)
413
+ - `inEye(vec2 pos) -> float` - Returns eye mask value at position (blue channel)
414
+ - `inMouth(vec2 pos) -> float` - Returns mouth mask value at position (red channel)
354
415
 
355
- **Helper functions:** `faceLandmark(int faceIndex, int landmarkIndex)`, `getFace(vec2 pos)`, `getEye(vec2 pos)`, `getMouth(vec2 pos)`
416
+ **Constants:**
356
417
 
357
- Use `faceLandmark(int faceIndex, int landmarkIndex)` in GLSL to retrieve a specific point. [Landmark indices are documented here.](https://ai.google.dev/edge/mediapipe/solutions/vision/face_landmarker#face_landmarker_model)
418
+ - `FACE_LANDMARK_L_EYE_CENTER` - Left eye center landmark index
419
+ - `FACE_LANDMARK_R_EYE_CENTER` - Right eye center landmark index
420
+ - `FACE_LANDMARK_NOSE_TIP` - Nose tip landmark index
421
+ - `FACE_LANDMARK_FACE_CENTER` - Face center landmark index (custom, calculated from all landmarks)
422
+ - `FACE_LANDMARK_MOUTH_CENTER` - Mouth center landmark index (custom, calculated from inner lip landmarks)
423
+
424
+ **Example usage:**
425
+
426
+ ```glsl
427
+ // Get a specific landmark position.
428
+ vec2 nosePos = vec2(faceLandmark(0, FACE_LANDMARK_NOSE_TIP));
429
+
430
+ if (inMouth(v_uv) > 0.0) {
431
+ // Position is inside a mouth.
432
+ }
433
+
434
+ // Iterate through all faces and landmarks.
435
+ for (int i = 0; i < u_nFaces; ++i) {
436
+ vec4 leftEye = faceLandmark(i, FACE_LANDMARK_L_EYE_CENTER);
437
+ vec4 rightEye = faceLandmark(i, FACE_LANDMARK_R_EYE_CENTER);
438
+ // ...
439
+ }
440
+ ```
441
+
442
+ [Landmark indices are documented here.](https://ai.google.dev/edge/mediapipe/solutions/vision/face_landmarker#face_landmarker_model) This library adds two custom landmarks: `FACE_CENTER` and `MOUTH_CENTER`. This brings the total landmark count to 480.
358
443
 
359
444
  **Note:** The face plugin requires `@mediapipe/tasks-vision` as a peer dependency.
360
445
 
361
446
  #### pose
362
447
 
363
- The `pose` plugin uses [MediaPipe Pose Landmarker](https://ai.google.dev/edge/mediapipe/solutions/vision/pose_landmarker) to expose a flat array of 2D landmarks. Each pose contributes 33 landmarks, enumerated below.
448
+ The `pose` plugin uses [MediaPipe Pose Landmarker](https://ai.google.dev/edge/mediapipe/solutions/vision/pose_landmarker) to expose a flat array of 2D landmarks. Each pose contributes 39 landmarks (33 standard + 6 custom), enumerated below.
364
449
 
365
450
  ```typescript
366
451
  import ShaderPad from 'shaderpad';
@@ -373,49 +458,75 @@ const shader = new ShaderPad(fragmentShaderSrc, {
373
458
 
374
459
  **Uniforms:**
375
460
 
376
- | Uniform | Type | Description |
377
- | ----------------- | ------------------- | ------------------------------------------------ |
378
- | `u_maxPoses` | int | Maximum number of poses to track |
379
- | `u_nPoses` | int | Current number of detected poses |
380
- | `u_poseLandmarks` | vec2[maxPoses * 33] | Landmark positions in UV space |
381
- | `u_poseMask` | sampler2D | Pose mask texture (G: body, B: skeleton) |
382
- | `u_poseCenter` | vec2[maxPoses] | Center positions of the pose mask bounding boxes |
383
-
384
- **Helper functions:** `poseLandmark(int poseIndex, int landmarkIndex)`, `getBody(vec2 pos)`, `getSkeleton(vec2 pos)`
461
+ | Uniform | Type | Description |
462
+ | -------------------- | --------- | ----------------------------------------------------- |
463
+ | `u_maxPoses` | int | Maximum number of poses to track |
464
+ | `u_nPoses` | int | Current number of detected poses |
465
+ | `u_poseLandmarksTex` | sampler2D | Raw landmark data texture (RGBA: x, y, z, visibility) |
466
+ | `u_poseMask` | sampler2D | Pose mask texture (G: body) |
467
+
468
+ **Helper functions:**
469
+
470
+ - `poseLandmark(int poseIndex, int landmarkIndex) -> vec4` - Returns landmark data as `vec4(x, y, z, visibility)`. Use `vec2(poseLandmark(...))` to get just the screen position.
471
+ - `inBody(vec2 pos) -> float` - Returns body mask value at position (green channel)
472
+
473
+ **Constants:**
474
+
475
+ - `POSE_LANDMARK_LEFT_EYE` - Left eye landmark index (2)
476
+ - `POSE_LANDMARK_RIGHT_EYE` - Right eye landmark index (5)
477
+ - `POSE_LANDMARK_LEFT_SHOULDER` - Left shoulder landmark index (11)
478
+ - `POSE_LANDMARK_RIGHT_SHOULDER` - Right shoulder landmark index (12)
479
+ - `POSE_LANDMARK_LEFT_ELBOW` - Left elbow landmark index (13)
480
+ - `POSE_LANDMARK_RIGHT_ELBOW` - Right elbow landmark index (14)
481
+ - `POSE_LANDMARK_LEFT_HIP` - Left hip landmark index (23)
482
+ - `POSE_LANDMARK_RIGHT_HIP` - Right hip landmark index (24)
483
+ - `POSE_LANDMARK_LEFT_KNEE` - Left knee landmark index (25)
484
+ - `POSE_LANDMARK_RIGHT_KNEE` - Right knee landmark index (26)
485
+ - `POSE_LANDMARK_BODY_CENTER` - Body center landmark index (33, custom, calculated from all landmarks)
486
+ - `POSE_LANDMARK_LEFT_HAND_CENTER` - Left hand center landmark index (34, custom, calculated from pinky, thumb, wrist, index)
487
+ - `POSE_LANDMARK_RIGHT_HAND_CENTER` - Right hand center landmark index (35, custom, calculated from pinky, thumb, wrist, index)
488
+ - `POSE_LANDMARK_LEFT_FOOT_CENTER` - Left foot center landmark index (36, custom, calculated from ankle, heel, foot index)
489
+ - `POSE_LANDMARK_RIGHT_FOOT_CENTER` - Right foot center landmark index (37, custom, calculated from ankle, heel, foot index)
490
+ - `POSE_LANDMARK_TORSO_CENTER` - Torso center landmark index (38, custom, calculated from shoulders and hips)
491
+
492
+ **Note:** For connecting pose landmarks (e.g., drawing skeleton lines), `PoseLandmarker.POSE_CONNECTIONS` from `@mediapipe/tasks-vision` provides an array of `{ start, end }` pairs that define which landmarks should be connected.
385
493
 
386
494
  Use `poseLandmark(int poseIndex, int landmarkIndex)` in GLSL to retrieve a specific point. Landmark indices are:
387
495
 
388
- | Index | Landmark | Index | Landmark |
389
- | ----- | ----------------- | ----- | ---------------- |
390
- | 0 | nose | 17 | left pinky |
391
- | 1 | left eye (inner) | 18 | right pinky |
392
- | 2 | left eye | 19 | left index |
393
- | 3 | left eye (outer) | 20 | right index |
394
- | 4 | right eye (inner) | 21 | left thumb |
395
- | 5 | right eye | 22 | right thumb |
396
- | 6 | right eye (outer) | 23 | left hip |
397
- | 7 | left ear | 24 | right hip |
398
- | 8 | right ear | 25 | left knee |
399
- | 9 | mouth (left) | 26 | right knee |
400
- | 10 | mouth (right) | 27 | left ankle |
401
- | 11 | left shoulder | 28 | right ankle |
402
- | 12 | right shoulder | 29 | left heel |
403
- | 13 | left elbow | 30 | right heel |
404
- | 14 | right elbow | 31 | left foot index |
405
- | 15 | left wrist | 32 | right foot index |
406
- | 16 | right wrist | | |
496
+ | Index | Landmark | Index | Landmark |
497
+ | ----- | ----------------- | ----- | -------------------------- |
498
+ | 0 | nose | 20 | right index |
499
+ | 1 | left eye (inner) | 21 | left thumb |
500
+ | 2 | left eye | 22 | right thumb |
501
+ | 3 | left eye (outer) | 23 | left hip |
502
+ | 4 | right eye (inner) | 24 | right hip |
503
+ | 5 | right eye | 25 | left knee |
504
+ | 6 | right eye (outer) | 26 | right knee |
505
+ | 7 | left ear | 27 | left ankle |
506
+ | 8 | right ear | 28 | right ankle |
507
+ | 9 | mouth (left) | 29 | left heel |
508
+ | 10 | mouth (right) | 30 | right heel |
509
+ | 11 | left shoulder | 31 | left foot index |
510
+ | 12 | right shoulder | 32 | right foot index |
511
+ | 13 | left elbow | 33 | body center (custom) |
512
+ | 14 | right elbow | 34 | left hand center (custom) |
513
+ | 15 | left wrist | 35 | right hand center (custom) |
514
+ | 16 | right wrist | 36 | left foot center (custom) |
515
+ | 17 | left pinky | 37 | right foot center (custom) |
516
+ | 18 | right pinky | 38 | torso center (custom) |
517
+ | 19 | left index | | |
407
518
 
408
519
  [Source](https://ai.google.dev/edge/mediapipe/solutions/vision/pose_landmarker#pose_landmarker_model)
409
520
 
521
+ [Landmark indices are documented here.](https://ai.google.dev/edge/mediapipe/solutions/vision/pose_landmarker#pose_landmarker_model) This library adds six custom landmarks: `BODY_CENTER`, `LEFT_HAND_CENTER`, `RIGHT_HAND_CENTER`, `LEFT_FOOT_CENTER`, `RIGHT_FOOT_CENTER`, and `TORSO_CENTER`. This brings the total landmark count to 39.
522
+
410
523
  A minimal fragment shader loop looks like:
411
524
 
412
525
  ```glsl
413
- int POSE_LANDMARK_LEFT_HIP = 23;
414
- int POSE_LANDMARK_RIGHT_HIP = 24;
415
526
  for (int i = 0; i < u_maxPoses; ++i) {
416
527
  if (i >= u_nPoses) break;
417
- vec2 leftHip = poseLandmark(i, POSE_LANDMARK_LEFT_HIP);
418
- vec2 rightHip = poseLandmark(i, POSE_LANDMARK_RIGHT_HIP);
528
+ vec2 leftHip = vec2(poseLandmark(i, POSE_LANDMARK_LEFT_HIP));
529
+ vec2 rightHip = vec2(poseLandmark(i, POSE_LANDMARK_RIGHT_HIP));
419
530
  // …
420
531
  }
421
532
  ```
@@ -435,13 +546,15 @@ const shader = new ShaderPad(fragmentShaderSrc, {
435
546
 
436
547
  **Uniforms:**
437
548
 
438
- | Uniform | Type | Description |
439
- | ----------------- | ------------------- | -------------------------------- |
440
- | `u_maxHands` | int | Maximum number of hands to track |
441
- | `u_nHands` | int | Current number of detected hands |
442
- | `u_handLandmarks` | vec2[maxHands * 22] | Landmark positions in UV space |
549
+ | Uniform | Type | Description |
550
+ | -------------------- | --------- | ----------------------------------------------------- |
551
+ | `u_maxHands` | int | Maximum number of hands to track |
552
+ | `u_nHands` | int | Current number of detected hands |
553
+ | `u_handLandmarksTex` | sampler2D | Raw landmark data texture (RGBA: x, y, z, visibility) |
554
+
555
+ **Helper functions:**
443
556
 
444
- **Helper functions:** `handLandmark(int handIndex, int landmarkIndex)`
557
+ - `handLandmark(int handIndex, int landmarkIndex) -> vec4` - Returns landmark data as `vec4(x, y, z, visibility)`. Use `vec2(handLandmark(...))` to get just the screen position.
445
558
 
446
559
  Use `handLandmark(int handIndex, int landmarkIndex)` in GLSL to retrieve a specific point. Landmark indices are:
447
560
 
@@ -464,14 +577,16 @@ Use `handLandmark(int handIndex, int landmarkIndex)` in GLSL to retrieve a speci
464
577
  A minimal fragment shader loop looks like:
465
578
 
466
579
  ```glsl
467
- int HAND_LANDMARK_WRIST = 0;
468
- int HAND_LANDMARK_THUMB_TIP = 4;
469
- int HAND_LANDMARK_INDEX_TIP = 8;
580
+ #define HAND_LANDMARK_WRIST 0
581
+ #define HAND_LANDMARK_THUMB_TIP 4
582
+ #define HAND_LANDMARK_INDEX_TIP 8
583
+ #define HAND_LANDMARK_HAND_CENTER 21
470
584
  for (int i = 0; i < u_maxHands; ++i) {
471
585
  if (i >= u_nHands) break;
472
- vec2 wrist = handLandmark(i, HAND_LANDMARK_WRIST);
473
- vec2 thumbTip = handLandmark(i, HAND_LANDMARK_THUMB_TIP);
474
- vec2 indexTip = handLandmark(i, HAND_LANDMARK_INDEX_TIP);
586
+ vec2 wrist = vec2(handLandmark(i, HAND_LANDMARK_WRIST));
587
+ vec2 thumbTip = vec2(handLandmark(i, HAND_LANDMARK_THUMB_TIP));
588
+ vec2 indexTip = vec2(handLandmark(i, HAND_LANDMARK_INDEX_TIP));
589
+ vec2 handCenter = vec2(handLandmark(i, HAND_LANDMARK_HAND_CENTER));
475
590
  // …
476
591
  }
477
592
  ```
package/dist/index.d.mts CHANGED
@@ -4,6 +4,16 @@ interface Uniform {
4
4
  location: WebGLUniformLocation;
5
5
  arrayLength?: number;
6
6
  }
7
+ interface TextureOptions {
8
+ internalFormat?: number;
9
+ format?: number;
10
+ type?: number;
11
+ minFilter?: number;
12
+ magFilter?: number;
13
+ wrapS?: number;
14
+ wrapT?: number;
15
+ preserveY?: boolean;
16
+ }
7
17
  interface Texture {
8
18
  texture: WebGLTexture;
9
19
  unitIndex: number;
@@ -13,8 +23,20 @@ interface Texture {
13
23
  depth: number;
14
24
  writeIndex: number;
15
25
  };
26
+ options?: TextureOptions;
27
+ }
28
+ interface CustomTexture {
29
+ data: ArrayBufferView | null;
30
+ width: number;
31
+ height: number;
32
+ }
33
+ interface PartialCustomTexture extends CustomTexture {
34
+ isPartial?: boolean;
35
+ x?: number;
36
+ y?: number;
16
37
  }
17
- type TextureSource = HTMLImageElement | HTMLVideoElement | HTMLCanvasElement;
38
+ type TextureSource = HTMLImageElement | HTMLVideoElement | HTMLCanvasElement | CustomTexture;
39
+ type UpdateTextureSource = Exclude<TextureSource, CustomTexture> | PartialCustomTexture;
18
40
  interface PluginContext {
19
41
  gl: WebGL2RenderingContext;
20
42
  uniforms: Map<string, Uniform>;
@@ -31,6 +53,7 @@ interface Options {
31
53
  canvas?: HTMLCanvasElement | null;
32
54
  plugins?: Plugin[];
33
55
  history?: number;
56
+ debug?: boolean;
34
57
  }
35
58
  declare class ShaderPad {
36
59
  private isInternalCanvas;
@@ -57,6 +80,7 @@ declare class ShaderPad {
57
80
  onResize?: (width: number, height: number) => void;
58
81
  private hooks;
59
82
  private historyDepth;
83
+ private debug;
60
84
  constructor(fragmentShaderSrc: string, options?: Options);
61
85
  registerHook(name: LifecycleMethod, fn: Function): void;
62
86
  private init;
@@ -72,15 +96,16 @@ declare class ShaderPad {
72
96
  initializeUniform(name: string, type: 'float' | 'int', value: number | number[] | (number | number[])[], options?: {
73
97
  arrayLength?: number;
74
98
  }): void;
99
+ private log;
75
100
  updateUniforms(updates: Record<string, number | number[] | (number | number[])[]>, options?: {
76
101
  startIndex?: number;
77
102
  }): void;
78
103
  private createTexture;
79
104
  private _initializeTexture;
80
- initializeTexture(name: string, source: TextureSource, options?: {
105
+ initializeTexture(name: string, source: TextureSource, options?: TextureOptions & {
81
106
  history?: number;
82
107
  }): void;
83
- updateTextures(updates: Record<string, TextureSource>): void;
108
+ updateTextures(updates: Record<string, UpdateTextureSource>): void;
84
109
  private updateTexture;
85
110
  draw(): void;
86
111
  step(time: number): void;
@@ -90,4 +115,4 @@ declare class ShaderPad {
90
115
  destroy(): void;
91
116
  }
92
117
 
93
- export { type Options, type PluginContext, type TextureSource, ShaderPad as default };
118
+ export { type CustomTexture, type Options, type PartialCustomTexture, type PluginContext, type TextureOptions, type TextureSource, ShaderPad as default };
package/dist/index.d.ts CHANGED
@@ -4,6 +4,16 @@ interface Uniform {
4
4
  location: WebGLUniformLocation;
5
5
  arrayLength?: number;
6
6
  }
7
+ interface TextureOptions {
8
+ internalFormat?: number;
9
+ format?: number;
10
+ type?: number;
11
+ minFilter?: number;
12
+ magFilter?: number;
13
+ wrapS?: number;
14
+ wrapT?: number;
15
+ preserveY?: boolean;
16
+ }
7
17
  interface Texture {
8
18
  texture: WebGLTexture;
9
19
  unitIndex: number;
@@ -13,8 +23,20 @@ interface Texture {
13
23
  depth: number;
14
24
  writeIndex: number;
15
25
  };
26
+ options?: TextureOptions;
27
+ }
28
+ interface CustomTexture {
29
+ data: ArrayBufferView | null;
30
+ width: number;
31
+ height: number;
32
+ }
33
+ interface PartialCustomTexture extends CustomTexture {
34
+ isPartial?: boolean;
35
+ x?: number;
36
+ y?: number;
16
37
  }
17
- type TextureSource = HTMLImageElement | HTMLVideoElement | HTMLCanvasElement;
38
+ type TextureSource = HTMLImageElement | HTMLVideoElement | HTMLCanvasElement | CustomTexture;
39
+ type UpdateTextureSource = Exclude<TextureSource, CustomTexture> | PartialCustomTexture;
18
40
  interface PluginContext {
19
41
  gl: WebGL2RenderingContext;
20
42
  uniforms: Map<string, Uniform>;
@@ -31,6 +53,7 @@ interface Options {
31
53
  canvas?: HTMLCanvasElement | null;
32
54
  plugins?: Plugin[];
33
55
  history?: number;
56
+ debug?: boolean;
34
57
  }
35
58
  declare class ShaderPad {
36
59
  private isInternalCanvas;
@@ -57,6 +80,7 @@ declare class ShaderPad {
57
80
  onResize?: (width: number, height: number) => void;
58
81
  private hooks;
59
82
  private historyDepth;
83
+ private debug;
60
84
  constructor(fragmentShaderSrc: string, options?: Options);
61
85
  registerHook(name: LifecycleMethod, fn: Function): void;
62
86
  private init;
@@ -72,15 +96,16 @@ declare class ShaderPad {
72
96
  initializeUniform(name: string, type: 'float' | 'int', value: number | number[] | (number | number[])[], options?: {
73
97
  arrayLength?: number;
74
98
  }): void;
99
+ private log;
75
100
  updateUniforms(updates: Record<string, number | number[] | (number | number[])[]>, options?: {
76
101
  startIndex?: number;
77
102
  }): void;
78
103
  private createTexture;
79
104
  private _initializeTexture;
80
- initializeTexture(name: string, source: TextureSource, options?: {
105
+ initializeTexture(name: string, source: TextureSource, options?: TextureOptions & {
81
106
  history?: number;
82
107
  }): void;
83
- updateTextures(updates: Record<string, TextureSource>): void;
108
+ updateTextures(updates: Record<string, UpdateTextureSource>): void;
84
109
  private updateTexture;
85
110
  draw(): void;
86
111
  step(time: number): void;
@@ -90,4 +115,4 @@ declare class ShaderPad {
90
115
  destroy(): void;
91
116
  }
92
117
 
93
- export { type Options, type PluginContext, type TextureSource, ShaderPad as default };
118
+ export { type CustomTexture, type Options, type PartialCustomTexture, type PluginContext, type TextureOptions, type TextureSource, ShaderPad as default };
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
- "use strict";var f=Object.defineProperty;var T=Object.getOwnPropertyDescriptor;var v=Object.getOwnPropertyNames;var x=Object.prototype.hasOwnProperty;var E=(n,t)=>{for(var i in t)f(n,i,{get:t[i],enumerable:!0})},b=(n,t,i,e)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of v(t))!x.call(n,r)&&r!==i&&f(n,r,{get:()=>t[r],enumerable:!(e=T(t,r))||e.enumerable});return n};var y=n=>b(f({},"__esModule",{value:!0}),n);var U={};E(U,{default:()=>_});module.exports=y(U);var w=`#version 300 es
1
+ "use strict";var f=Object.defineProperty;var T=Object.getOwnPropertyDescriptor;var v=Object.getOwnPropertyNames;var b=Object.prototype.hasOwnProperty;var E=(n,t)=>{for(var i in t)f(n,i,{get:t[i],enumerable:!0})},y=(n,t,i,e)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of v(t))!b.call(n,r)&&r!==i&&f(n,r,{get:()=>t[r],enumerable:!(e=T(t,r))||e.enumerable});return n};var w=n=>y(f({},"__esModule",{value:!0}),n);var A={};E(A,{default:()=>_});module.exports=w(A);var R=`#version 300 es
2
2
  in vec2 aPosition;
3
3
  out vec2 v_uv;
4
4
  void main() {
5
5
  v_uv = aPosition * 0.5 + 0.5;
6
6
  gl_Position = vec4(aPosition, 0.0, 1.0);
7
7
  }
8
- `,R=33.333333333333336,c=Symbol("u_history");function L(n,t){if(!t?.length)return n;let i=n.split(`
8
+ `,U=33.333333333333336,d=Symbol("u_history");function L(n,t){if(!t?.length)return n;let i=n.split(`
9
9
  `),e=i.findLastIndex(r=>{let s=r.trimStart();return s.startsWith("precision ")||s.startsWith("#version ")})+1;return i.splice(e,0,...t),i.join(`
10
- `)}function p(n){if(n instanceof HTMLVideoElement)return{width:n.videoWidth,height:n.videoHeight};if(n instanceof HTMLCanvasElement){let t=n.getContext("webgl2");return t?{width:t.drawingBufferWidth,height:t.drawingBufferHeight}:{width:n.width,height:n.height}}return{width:n.naturalWidth??n.width,height:n.naturalHeight??n.height}}function g(n){return typeof n=="symbol"?n.description??"":n}var m=class{isInternalCanvas=!1;isTouchDevice=!1;gl;fragmentShaderSrc;uniforms=new Map;textures=new Map;textureUnitPool;buffer=null;program=null;animationFrameId;resolutionObserver;resizeObserver;resizeTimeout=null;lastResizeTime=-1/0;eventListeners=new Map;frame=0;startTime=0;cursorPosition=[.5,.5];clickPosition=[.5,.5];isMouseDown=!1;canvas;onResize;hooks=new Map;historyDepth;constructor(t,i={}){if(this.canvas=i.canvas||document.createElement("canvas"),i.canvas||(this.isInternalCanvas=!0,this.canvas.style.position="fixed",this.canvas.style.inset="0",this.canvas.style.height="100dvh",this.canvas.style.width="100dvw",document.body.appendChild(this.canvas)),this.gl=this.canvas.getContext("webgl2",{antialias:!1}),!this.gl)throw new Error("WebGL2 not supported. Please use a browser that supports WebGL2.");this.textureUnitPool={free:[],next:0,max:this.gl.getParameter(this.gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS)},this.historyDepth=i.history??0,this.animationFrameId=null,this.resolutionObserver=new MutationObserver(()=>this.updateResolution()),this.resizeObserver=new ResizeObserver(()=>this.throttledHandleResize());let e=[];if(i.plugins){let r={gl:this.gl,uniforms:this.uniforms,textures:this.textures,canvas:this.canvas,reserveTextureUnit:this.reserveTextureUnit.bind(this),releaseTextureUnit:this.releaseTextureUnit.bind(this),injectGLSL:s=>{e.push(s)}};Object.defineProperty(r,"program",{get:()=>this.program,enumerable:!0,configurable:!0}),i.plugins.forEach(s=>s(this,r))}this.fragmentShaderSrc=L(t,e),this.init(),this.addEventListeners()}registerHook(t,i){this.hooks.has(t)||this.hooks.set(t,[]),this.hooks.get(t).push(i)}init(){let t=w;if(this.program=this.gl.createProgram(),!this.program)throw new Error("Failed to create WebGL program");let i=this.createShader(this.gl.VERTEX_SHADER,t),e=this.createShader(this.gl.FRAGMENT_SHADER,this.fragmentShaderSrc);if(this.gl.attachShader(this.program,i),this.gl.attachShader(this.program,e),this.gl.linkProgram(this.program),this.gl.deleteShader(i),this.gl.deleteShader(e),!this.gl.getProgramParameter(this.program,this.gl.LINK_STATUS))throw console.error("Program link error:",this.gl.getProgramInfoLog(this.program)),this.gl.deleteProgram(this.program),new Error("Failed to link WebGL program");let r=this.gl.getAttribLocation(this.program,"aPosition");this.setupBuffer(r),this.gl.useProgram(this.program),this.resolutionObserver.observe(this.canvas,{attributes:!0,attributeFilter:["width","height"]}),this.resizeObserver.observe(this.canvas),this.isInternalCanvas||this.updateResolution(),this.initializeUniform("u_cursor","float",this.cursorPosition),this.initializeUniform("u_click","float",[...this.clickPosition,this.isMouseDown?1:0]),this.initializeUniform("u_time","float",0),this.initializeUniform("u_frame","int",0),this.historyDepth>0&&this._initializeTexture(c,this.canvas,{history:this.historyDepth}),this.hooks.get("init")?.forEach(s=>s.call(this))}createShader(t,i){let e=this.gl.createShader(t);if(this.gl.shaderSource(e,i),this.gl.compileShader(e),!this.gl.getShaderParameter(e,this.gl.COMPILE_STATUS))throw console.error("Shader compilation failed:",i),console.error(this.gl.getShaderInfoLog(e)),this.gl.deleteShader(e),new Error("Shader compilation failed");return e}setupBuffer(t){let i=new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]);this.buffer=this.gl.createBuffer(),this.gl.bindBuffer(this.gl.ARRAY_BUFFER,this.buffer),this.gl.bufferData(this.gl.ARRAY_BUFFER,i,this.gl.STATIC_DRAW),this.gl.viewport(0,0,this.gl.drawingBufferWidth,this.gl.drawingBufferHeight),this.gl.enableVertexAttribArray(t),this.gl.vertexAttribPointer(t,2,this.gl.FLOAT,!1,0,0)}throttledHandleResize(){clearTimeout(this.resizeTimeout);let t=performance.now(),i=this.lastResizeTime+R-t;i<=0?(this.lastResizeTime=t,this.handleResize()):this.resizeTimeout=setTimeout(()=>this.throttledHandleResize(),i)}handleResize(){let t=window.devicePixelRatio||1,i=this.canvas.clientWidth*t,e=this.canvas.clientHeight*t;this.isInternalCanvas&&(this.canvas.width!==i||this.canvas.height!==e)&&(this.canvas.width=i,this.canvas.height=e),this.onResize?.(i,e)}addEventListeners(){let t=(e,r)=>{if(!this.uniforms.has("u_cursor"))return;let s=this.canvas.getBoundingClientRect();this.cursorPosition[0]=(e-s.left)/s.width,this.cursorPosition[1]=1-(r-s.top)/s.height,this.updateUniforms({u_cursor:this.cursorPosition})},i=(e,r,s)=>{if(this.uniforms.has("u_click")){if(this.isMouseDown=e,e){let o=this.canvas.getBoundingClientRect(),h=r,l=s;this.clickPosition[0]=(h-o.left)/o.width,this.clickPosition[1]=1-(l-o.top)/o.height}this.updateUniforms({u_click:[...this.clickPosition,this.isMouseDown?1:0]})}};this.eventListeners.set("mousemove",e=>{let r=e;this.isTouchDevice||t(r.clientX,r.clientY)}),this.eventListeners.set("mousedown",e=>{let r=e;this.isTouchDevice||r.button===0&&(this.isMouseDown=!0,i(!0,r.clientX,r.clientY))}),this.eventListeners.set("mouseup",e=>{let r=e;this.isTouchDevice||r.button===0&&i(!1)}),this.eventListeners.set("touchmove",e=>{let r=e;r.touches.length>0&&t(r.touches[0].clientX,r.touches[0].clientY)}),this.eventListeners.set("touchstart",e=>{let r=e;this.isTouchDevice=!0,r.touches.length>0&&(t(r.touches[0].clientX,r.touches[0].clientY),i(!0,r.touches[0].clientX,r.touches[0].clientY))}),this.eventListeners.set("touchend",e=>{e.touches.length===0&&i(!1)}),this.eventListeners.forEach((e,r)=>{this.canvas.addEventListener(r,e)})}updateResolution(){let t=[this.gl.drawingBufferWidth,this.gl.drawingBufferHeight];this.gl.viewport(0,0,...t),this.uniforms.has("u_resolution")?this.updateUniforms({u_resolution:t}):this.initializeUniform("u_resolution","float",t),this.hooks.get("updateResolution")?.forEach(i=>i.call(this))}reserveTextureUnit(t){let i=this.textures.get(t);if(i)return i.unitIndex;if(this.textureUnitPool.free.length>0)return this.textureUnitPool.free.pop();if(this.textureUnitPool.next>=this.textureUnitPool.max)throw new Error("Exceeded the available texture units for this device.");return this.textureUnitPool.next++}releaseTextureUnit(t){let i=this.textures.get(t);i&&this.textureUnitPool.free.push(i.unitIndex)}clearHistoryTextureLayers(t){if(!t.history)return;let i=new Uint8Array(t.width*t.height*4);this.gl.activeTexture(this.gl.TEXTURE0+t.unitIndex),this.gl.bindTexture(this.gl.TEXTURE_2D_ARRAY,t.texture),this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL,!1);for(let e=0;e<t.history.depth;++e)this.gl.texSubImage3D(this.gl.TEXTURE_2D_ARRAY,0,0,0,e,t.width,t.height,1,this.gl.RGBA,this.gl.UNSIGNED_BYTE,i);this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL,!0)}initializeUniform(t,i,e,r){let s=r?.arrayLength;if(this.uniforms.has(t))throw new Error(`${t} is already initialized.`);if(i!=="float"&&i!=="int")throw new Error(`Invalid uniform type: ${i}. Expected 'float' or 'int'.`);if(s&&!(Array.isArray(e)&&e.length===s))throw new Error(`${t} array length mismatch: must initialize with ${s} elements.`);let o=this.gl.getUniformLocation(this.program,t);if(!o&&s&&(o=this.gl.getUniformLocation(this.program,`${t}[0]`)),!o){console.debug(`${t} not found in fragment shader. Skipping initialization.`);return}let h=s?e[0]:e,l=Array.isArray(h)?h.length:1;this.uniforms.set(t,{type:i,length:l,location:o,arrayLength:s});try{this.updateUniforms({[t]:e})}catch(a){throw this.uniforms.delete(t),a}this.hooks.get("initializeUniform")?.forEach(a=>a.call(this,...arguments))}updateUniforms(t,i){Object.entries(t).forEach(([e,r])=>{let s=this.uniforms.get(e);if(!s){console.debug(`${e} not found in fragment shader. Skipping update.`);return}let o=`uniform${s.length}${s.type.charAt(0)}`;if(s.arrayLength){if(!Array.isArray(r))throw new Error(`${e} is an array, but the value passed to updateUniforms is not an array.`);let h=r.length;if(!h)return;if(h>s.arrayLength)throw new Error(`${e} received ${h} values, but maximum length is ${s.arrayLength}.`);if(r.some(u=>(Array.isArray(u)?u.length:1)!==s.length))throw new Error(`Tried to update ${e} with some elements that are not length ${s.length}.`);let l=new(s.type==="float"?Float32Array:Int32Array)(r.flat()),a=s.location;if(i?.startIndex){let u=this.gl.getUniformLocation(this.program,`${e}[${i.startIndex}]`);if(!u)throw new Error(`${e}[${i.startIndex}] not found in fragment shader. Did you pass an invalid startIndex?`);a=u}this.gl[o+"v"](s.location,l)}else{if(Array.isArray(r)||(r=[r]),r.length!==s.length)throw new Error(`Invalid uniform value length: ${r.length}. Expected ${s.length}.`);this.gl[o](s.location,...r)}}),this.hooks.get("updateUniforms")?.forEach(e=>e.call(this,...arguments))}createTexture(t,i,e){let{width:r,height:s}=i,o=i.history?.depth??0,h=this.gl.createTexture();if(!h)throw new Error("Failed to create texture");if(typeof e!="number")try{e=this.reserveTextureUnit(t)}catch(u){throw this.gl.deleteTexture(h),u}let l=o>0,a=l?this.gl.TEXTURE_2D_ARRAY:this.gl.TEXTURE_2D;return this.gl.activeTexture(this.gl.TEXTURE0+e),this.gl.bindTexture(a,h),this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL,!0),this.gl.texParameteri(a,this.gl.TEXTURE_WRAP_S,this.gl.CLAMP_TO_EDGE),this.gl.texParameteri(a,this.gl.TEXTURE_WRAP_T,this.gl.CLAMP_TO_EDGE),this.gl.texParameteri(a,this.gl.TEXTURE_MIN_FILTER,this.gl.LINEAR),this.gl.texParameteri(a,this.gl.TEXTURE_MAG_FILTER,this.gl.LINEAR),l&&this.gl.texStorage3D(a,1,this.gl.RGBA8,r,s,o),{texture:h,unitIndex:e}}_initializeTexture(t,i,e){if(this.textures.has(t))throw new Error(`Texture '${g(t)}' is already initialized.`);let r=e?.history??0,{width:s,height:o}=p(i);if(!s||!o)throw new Error("Texture source must have valid dimensions");let h={width:s,height:o};r>0&&(h.history={depth:r,writeIndex:0});let{texture:l,unitIndex:a}=this.createTexture(t,h),u={texture:l,unitIndex:a,...h};r>0&&(this.initializeUniform(`${g(t)}FrameOffset`,"int",0),this.clearHistoryTextureLayers(u)),this.textures.set(t,u),this.updateTexture(t,i);let d=this.gl.getUniformLocation(this.program,g(t));d&&this.gl.uniform1i(d,a)}initializeTexture(t,i,e){this._initializeTexture(t,i,e),this.hooks.get("initializeTexture")?.forEach(r=>r.call(this,...arguments))}updateTextures(t){this.hooks.get("updateTextures")?.forEach(i=>i.call(this,...arguments)),Object.entries(t).forEach(([i,e])=>{this.updateTexture(i,e)})}updateTexture(t,i){let e=this.textures.get(t);if(!e)throw new Error(`Texture '${g(t)}' is not initialized.`);let{width:r,height:s}=p(i);if(e.width!==r||e.height!==s){this.gl.deleteTexture(e.texture),e.width=r,e.height=s;let{texture:o}=this.createTexture(t,e,e.unitIndex);e.texture=o,e.history&&(e.history.writeIndex=0,this.clearHistoryTextureLayers(e))}if(e.history){let o=t===c;this.gl.activeTexture(this.gl.TEXTURE0+e.unitIndex),this.gl.bindTexture(this.gl.TEXTURE_2D_ARRAY,e.texture),o?(this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL,!1),this.gl.copyTexSubImage3D(this.gl.TEXTURE_2D_ARRAY,0,0,0,e.history.writeIndex,0,0,r,s),this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL,!0)):this.gl.texSubImage3D(this.gl.TEXTURE_2D_ARRAY,0,0,0,e.history.writeIndex,r,s,1,this.gl.RGBA,this.gl.UNSIGNED_BYTE,i);let h=`${g(t)}FrameOffset`;this.updateUniforms({[h]:e.history.writeIndex}),e.history.writeIndex=(e.history.writeIndex+1)%e.history.depth}else this.gl.activeTexture(this.gl.TEXTURE0+e.unitIndex),this.gl.bindTexture(this.gl.TEXTURE_2D,e.texture),this.gl.texImage2D(this.gl.TEXTURE_2D,0,this.gl.RGBA,this.gl.RGBA,this.gl.UNSIGNED_BYTE,i)}draw(){let t=this.gl;t.clear(t.COLOR_BUFFER_BIT),t.drawArrays(t.TRIANGLES,0,6)}step(t){this.uniforms.has("u_time")&&this.updateUniforms({u_time:t}),this.uniforms.has("u_frame")&&this.updateUniforms({u_frame:this.frame}),this.draw(),this.textures.get(c)&&this.updateTexture(c,this.canvas),this.hooks.get("step")?.forEach(i=>i.call(this,t,this.frame)),++this.frame}play(t){this.pause();let i=e=>{e=(e-this.startTime)/1e3,this.step(e),this.animationFrameId=requestAnimationFrame(i),t&&t(e,this.frame)};this.animationFrameId=requestAnimationFrame(i)}pause(){this.animationFrameId&&(cancelAnimationFrame(this.animationFrameId),this.animationFrameId=null)}reset(){this.frame=0,this.startTime=performance.now(),this.textures.forEach(t=>{t.history&&(t.history.writeIndex=0,this.clearHistoryTextureLayers(t))}),this.hooks.get("reset")?.forEach(t=>t.call(this))}destroy(){this.animationFrameId&&(cancelAnimationFrame(this.animationFrameId),this.animationFrameId=null),this.resolutionObserver.disconnect(),this.resizeObserver.disconnect(),this.eventListeners.forEach((t,i)=>{this.canvas.removeEventListener(i,t)}),this.program&&this.gl.deleteProgram(this.program),this.textures.forEach(t=>{this.gl.deleteTexture(t.texture)}),this.textureUnitPool.free=[],this.textureUnitPool.next=0,this.buffer&&(this.gl.deleteBuffer(this.buffer),this.buffer=null),this.hooks.get("destroy")?.forEach(t=>t.call(this)),this.isInternalCanvas&&this.canvas.remove()}},_=m;
10
+ `)}function x(n){if("data"in n)return{width:n.width,height:n.height};if(n instanceof HTMLVideoElement)return{width:n.videoWidth,height:n.videoHeight};if(n instanceof HTMLCanvasElement){let t=n.getContext("webgl2");return t?{width:t.drawingBufferWidth,height:t.drawingBufferHeight}:{width:n.width,height:n.height}}return{width:n.naturalWidth??n.width,height:n.naturalHeight??n.height}}function c(n){return typeof n=="symbol"?n.description??"":n}var p=class{isInternalCanvas=!1;isTouchDevice=!1;gl;fragmentShaderSrc;uniforms=new Map;textures=new Map;textureUnitPool;buffer=null;program=null;animationFrameId;resolutionObserver;resizeObserver;resizeTimeout=null;lastResizeTime=-1/0;eventListeners=new Map;frame=0;startTime=0;cursorPosition=[.5,.5];clickPosition=[.5,.5];isMouseDown=!1;canvas;onResize;hooks=new Map;historyDepth;debug;constructor(t,i={}){if(this.canvas=i.canvas||document.createElement("canvas"),i.canvas||(this.isInternalCanvas=!0,this.canvas.style.position="fixed",this.canvas.style.inset="0",this.canvas.style.height="100dvh",this.canvas.style.width="100dvw",document.body.appendChild(this.canvas)),this.gl=this.canvas.getContext("webgl2",{antialias:!1}),!this.gl)throw new Error("WebGL2 not supported. Please use a browser that supports WebGL2.");this.textureUnitPool={free:[],next:0,max:this.gl.getParameter(this.gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS)},this.historyDepth=i.history??0,this.debug=i.debug??(typeof process<"u"&&!1),this.animationFrameId=null,this.resolutionObserver=new MutationObserver(()=>this.updateResolution()),this.resizeObserver=new ResizeObserver(()=>this.throttledHandleResize());let e=[];if(i.plugins){let r={gl:this.gl,uniforms:this.uniforms,textures:this.textures,canvas:this.canvas,reserveTextureUnit:this.reserveTextureUnit.bind(this),releaseTextureUnit:this.releaseTextureUnit.bind(this),injectGLSL:s=>{e.push(s)}};Object.defineProperty(r,"program",{get:()=>this.program,enumerable:!0,configurable:!0}),i.plugins.forEach(s=>s(this,r))}this.fragmentShaderSrc=L(t,e),this.init(),this.addEventListeners()}registerHook(t,i){this.hooks.has(t)||this.hooks.set(t,[]),this.hooks.get(t).push(i)}init(){let t=R;if(this.program=this.gl.createProgram(),!this.program)throw new Error("Failed to create WebGL program");let i=this.createShader(this.gl.VERTEX_SHADER,t),e=this.createShader(this.gl.FRAGMENT_SHADER,this.fragmentShaderSrc);if(this.gl.attachShader(this.program,i),this.gl.attachShader(this.program,e),this.gl.linkProgram(this.program),this.gl.deleteShader(i),this.gl.deleteShader(e),!this.gl.getProgramParameter(this.program,this.gl.LINK_STATUS))throw console.error("Program link error:",this.gl.getProgramInfoLog(this.program)),this.gl.deleteProgram(this.program),new Error("Failed to link WebGL program");let r=this.gl.getAttribLocation(this.program,"aPosition");this.setupBuffer(r),this.gl.useProgram(this.program),this.resolutionObserver.observe(this.canvas,{attributes:!0,attributeFilter:["width","height"]}),this.resizeObserver.observe(this.canvas),this.isInternalCanvas||this.updateResolution(),this.initializeUniform("u_cursor","float",this.cursorPosition),this.initializeUniform("u_click","float",[...this.clickPosition,this.isMouseDown?1:0]),this.initializeUniform("u_time","float",0),this.initializeUniform("u_frame","int",0),this.historyDepth>0&&this._initializeTexture(d,this.canvas,{history:this.historyDepth}),this.hooks.get("init")?.forEach(s=>s.call(this))}createShader(t,i){let e=this.gl.createShader(t);if(this.gl.shaderSource(e,i),this.gl.compileShader(e),!this.gl.getShaderParameter(e,this.gl.COMPILE_STATUS))throw console.error("Shader compilation failed:",i),console.error(this.gl.getShaderInfoLog(e)),this.gl.deleteShader(e),new Error("Shader compilation failed");return e}setupBuffer(t){let i=new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]);this.buffer=this.gl.createBuffer(),this.gl.bindBuffer(this.gl.ARRAY_BUFFER,this.buffer),this.gl.bufferData(this.gl.ARRAY_BUFFER,i,this.gl.STATIC_DRAW),this.gl.viewport(0,0,this.gl.drawingBufferWidth,this.gl.drawingBufferHeight),this.gl.enableVertexAttribArray(t),this.gl.vertexAttribPointer(t,2,this.gl.FLOAT,!1,0,0)}throttledHandleResize(){clearTimeout(this.resizeTimeout);let t=performance.now(),i=this.lastResizeTime+U-t;i<=0?(this.lastResizeTime=t,this.handleResize()):this.resizeTimeout=setTimeout(()=>this.throttledHandleResize(),i)}handleResize(){let t=window.devicePixelRatio||1,i=this.canvas.clientWidth*t,e=this.canvas.clientHeight*t;this.isInternalCanvas&&(this.canvas.width!==i||this.canvas.height!==e)&&(this.canvas.width=i,this.canvas.height=e),this.onResize?.(i,e)}addEventListeners(){let t=(e,r)=>{if(!this.uniforms.has("u_cursor"))return;let s=this.canvas.getBoundingClientRect();this.cursorPosition[0]=(e-s.left)/s.width,this.cursorPosition[1]=1-(r-s.top)/s.height,this.updateUniforms({u_cursor:this.cursorPosition})},i=(e,r,s)=>{if(this.uniforms.has("u_click")){if(this.isMouseDown=e,e){let o=this.canvas.getBoundingClientRect(),h=r,a=s;this.clickPosition[0]=(h-o.left)/o.width,this.clickPosition[1]=1-(a-o.top)/o.height}this.updateUniforms({u_click:[...this.clickPosition,this.isMouseDown?1:0]})}};this.eventListeners.set("mousemove",e=>{let r=e;this.isTouchDevice||t(r.clientX,r.clientY)}),this.eventListeners.set("mousedown",e=>{let r=e;this.isTouchDevice||r.button===0&&(this.isMouseDown=!0,i(!0,r.clientX,r.clientY))}),this.eventListeners.set("mouseup",e=>{let r=e;this.isTouchDevice||r.button===0&&i(!1)}),this.eventListeners.set("touchmove",e=>{let r=e;r.touches.length>0&&t(r.touches[0].clientX,r.touches[0].clientY)}),this.eventListeners.set("touchstart",e=>{let r=e;this.isTouchDevice=!0,r.touches.length>0&&(t(r.touches[0].clientX,r.touches[0].clientY),i(!0,r.touches[0].clientX,r.touches[0].clientY))}),this.eventListeners.set("touchend",e=>{e.touches.length===0&&i(!1)}),this.eventListeners.forEach((e,r)=>{this.canvas.addEventListener(r,e)})}updateResolution(){let t=[this.gl.drawingBufferWidth,this.gl.drawingBufferHeight];this.gl.viewport(0,0,...t),this.uniforms.has("u_resolution")?this.updateUniforms({u_resolution:t}):this.initializeUniform("u_resolution","float",t),this.hooks.get("updateResolution")?.forEach(i=>i.call(this))}reserveTextureUnit(t){let i=this.textures.get(t);if(i)return i.unitIndex;if(this.textureUnitPool.free.length>0)return this.textureUnitPool.free.pop();if(this.textureUnitPool.next>=this.textureUnitPool.max)throw new Error("Exceeded the available texture units for this device.");return this.textureUnitPool.next++}releaseTextureUnit(t){let i=this.textures.get(t);i&&this.textureUnitPool.free.push(i.unitIndex)}clearHistoryTextureLayers(t){if(!t.history)return;let i=t.options?.type??this.gl.UNSIGNED_BYTE,e=i===this.gl.FLOAT?new Float32Array(t.width*t.height*4):new Uint8Array(t.width*t.height*4);this.gl.activeTexture(this.gl.TEXTURE0+t.unitIndex),this.gl.bindTexture(this.gl.TEXTURE_2D_ARRAY,t.texture);for(let r=0;r<t.history.depth;++r)this.gl.texSubImage3D(this.gl.TEXTURE_2D_ARRAY,0,0,0,r,t.width,t.height,1,t.options?.format??this.gl.RGBA,i,e)}initializeUniform(t,i,e,r){let s=r?.arrayLength;if(this.uniforms.has(t))throw new Error(`${t} is already initialized.`);if(i!=="float"&&i!=="int")throw new Error(`Invalid uniform type: ${i}. Expected 'float' or 'int'.`);if(s&&!(Array.isArray(e)&&e.length===s))throw new Error(`${t} array length mismatch: must initialize with ${s} elements.`);let o=this.gl.getUniformLocation(this.program,t);if(!o&&s&&(o=this.gl.getUniformLocation(this.program,`${t}[0]`)),!o){this.log(`${t} not found in fragment shader. Skipping initialization.`);return}let h=s?e[0]:e,a=Array.isArray(h)?h.length:1;this.uniforms.set(t,{type:i,length:a,location:o,arrayLength:s});try{this.updateUniforms({[t]:e})}catch(u){throw this.uniforms.delete(t),u}this.hooks.get("initializeUniform")?.forEach(u=>u.call(this,...arguments))}log(...t){this.debug&&console.debug(...t)}updateUniforms(t,i){Object.entries(t).forEach(([e,r])=>{let s=this.uniforms.get(e);if(!s){this.log(`${e} not found in fragment shader. Skipping update.`);return}let o=`uniform${s.length}${s.type.charAt(0)}`;if(s.arrayLength){if(!Array.isArray(r))throw new Error(`${e} is an array, but the value passed to updateUniforms is not an array.`);let h=r.length;if(!h)return;if(h>s.arrayLength)throw new Error(`${e} received ${h} values, but maximum length is ${s.arrayLength}.`);if(r.some(l=>(Array.isArray(l)?l.length:1)!==s.length))throw new Error(`Tried to update ${e} with some elements that are not length ${s.length}.`);let a=new(s.type==="float"?Float32Array:Int32Array)(r.flat()),u=s.location;if(i?.startIndex){let l=this.gl.getUniformLocation(this.program,`${e}[${i.startIndex}]`);if(!l)throw new Error(`${e}[${i.startIndex}] not found in fragment shader. Did you pass an invalid startIndex?`);u=l}this.gl[o+"v"](s.location,a)}else{if(Array.isArray(r)||(r=[r]),r.length!==s.length)throw new Error(`Invalid uniform value length: ${r.length}. Expected ${s.length}.`);this.gl[o](s.location,...r)}}),this.hooks.get("updateUniforms")?.forEach(e=>e.call(this,...arguments))}createTexture(t,i,e){let{width:r,height:s}=i,o=i.history?.depth??0,h=this.gl.createTexture();if(!h)throw new Error("Failed to create texture");let a=e?.unitIndex;if(typeof a!="number")try{a=this.reserveTextureUnit(t)}catch(g){throw this.gl.deleteTexture(h),g}let u=o>0,l=u?this.gl.TEXTURE_2D_ARRAY:this.gl.TEXTURE_2D;if(this.gl.activeTexture(this.gl.TEXTURE0+a),this.gl.bindTexture(l,h),this.gl.texParameteri(l,this.gl.TEXTURE_WRAP_S,e?.wrapS??this.gl.CLAMP_TO_EDGE),this.gl.texParameteri(l,this.gl.TEXTURE_WRAP_T,e?.wrapT??this.gl.CLAMP_TO_EDGE),this.gl.texParameteri(l,this.gl.TEXTURE_MIN_FILTER,e?.minFilter??this.gl.LINEAR),this.gl.texParameteri(l,this.gl.TEXTURE_MAG_FILTER,e?.magFilter??this.gl.LINEAR),u){let g=e?.type??this.gl.UNSIGNED_BYTE,m=e?.internalFormat??(g===this.gl.FLOAT?this.gl.RGBA32F:this.gl.RGBA8);this.gl.texStorage3D(l,1,m,r,s,o)}return{texture:h,unitIndex:a}}_initializeTexture(t,i,e){if(this.textures.has(t))throw new Error(`Texture '${c(t)}' is already initialized.`);let{history:r=0,...s}=e??{},{width:o,height:h}=x(i);if(!o||!h)throw new Error("Texture source must have valid dimensions");let a={width:o,height:h};r>0&&(a.history={depth:r,writeIndex:0});let{texture:u,unitIndex:l}=this.createTexture(t,a,s),g={texture:u,unitIndex:l,...a,options:s};r>0&&(this.initializeUniform(`${c(t)}FrameOffset`,"int",0),this.clearHistoryTextureLayers(g)),this.textures.set(t,g),this.updateTexture(t,i);let m=this.gl.getUniformLocation(this.program,c(t));m&&this.gl.uniform1i(m,l)}initializeTexture(t,i,e){this._initializeTexture(t,i,e),this.hooks.get("initializeTexture")?.forEach(r=>r.call(this,...arguments))}updateTextures(t){this.hooks.get("updateTextures")?.forEach(i=>i.call(this,...arguments)),Object.entries(t).forEach(([i,e])=>{this.updateTexture(i,e)})}updateTexture(t,i){let e=this.textures.get(t);if(!e)throw new Error(`Texture '${c(t)}' is not initialized.`);let{width:r,height:s}=x(i);if(!r||!s)return;let o="isPartial"in i&&i.isPartial;if(!o&&(e.width!==r||e.height!==s)){this.gl.deleteTexture(e.texture),e.width=r,e.height=s;let{texture:a}=this.createTexture(t,e,{...e.options,unitIndex:e.unitIndex});e.texture=a,e.history&&(e.history.writeIndex=0,this.clearHistoryTextureLayers(e))}let h=!e.options?.preserveY;if(e.history){let a=t===d;this.gl.activeTexture(this.gl.TEXTURE0+e.unitIndex),this.gl.bindTexture(this.gl.TEXTURE_2D_ARRAY,e.texture),a?(this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL,!1),this.gl.copyTexSubImage3D(this.gl.TEXTURE_2D_ARRAY,0,0,0,e.history.writeIndex,0,0,r,s)):(this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL,h),this.gl.texSubImage3D(this.gl.TEXTURE_2D_ARRAY,0,0,0,e.history.writeIndex,r,s,1,e.options?.format??this.gl.RGBA,e.options?.type??this.gl.UNSIGNED_BYTE,i.data??i));let u=`${c(t)}FrameOffset`;this.updateUniforms({[u]:e.history.writeIndex}),e.history.writeIndex=(e.history.writeIndex+1)%e.history.depth}else{this.gl.activeTexture(this.gl.TEXTURE0+e.unitIndex),this.gl.bindTexture(this.gl.TEXTURE_2D,e.texture),this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL,h);let a=e.options?.format??this.gl.RGBA,u=e.options?.type??this.gl.UNSIGNED_BYTE;if(o)this.gl.texSubImage2D(this.gl.TEXTURE_2D,0,i.x??0,i.y??0,r,s,a,u,i.data);else{let l="data"in i&&i.data,g=e.options?.internalFormat??(l?u===this.gl.FLOAT?this.gl.RGBA32F:this.gl.RGBA8:this.gl.RGBA);this.gl.texImage2D(this.gl.TEXTURE_2D,0,g,r,s,0,a,u,i.data??i)}}}draw(){let t=this.gl;t.clear(t.COLOR_BUFFER_BIT),t.drawArrays(t.TRIANGLES,0,6)}step(t){this.uniforms.has("u_time")&&this.updateUniforms({u_time:t}),this.uniforms.has("u_frame")&&this.updateUniforms({u_frame:this.frame}),this.draw(),this.textures.get(d)&&this.updateTexture(d,this.canvas),this.hooks.get("step")?.forEach(i=>i.call(this,t,this.frame)),++this.frame}play(t){this.pause();let i=e=>{e=(e-this.startTime)/1e3,this.step(e),this.animationFrameId=requestAnimationFrame(i),t&&t(e,this.frame)};this.animationFrameId=requestAnimationFrame(i)}pause(){this.animationFrameId&&(cancelAnimationFrame(this.animationFrameId),this.animationFrameId=null)}reset(){this.frame=0,this.startTime=performance.now(),this.textures.forEach(t=>{t.history&&(t.history.writeIndex=0,this.clearHistoryTextureLayers(t))}),this.hooks.get("reset")?.forEach(t=>t.call(this))}destroy(){this.animationFrameId&&(cancelAnimationFrame(this.animationFrameId),this.animationFrameId=null),this.resolutionObserver.disconnect(),this.resizeObserver.disconnect(),this.eventListeners.forEach((t,i)=>{this.canvas.removeEventListener(i,t)}),this.program&&this.gl.deleteProgram(this.program),this.textures.forEach(t=>{this.gl.deleteTexture(t.texture)}),this.textureUnitPool.free=[],this.textureUnitPool.next=0,this.buffer&&(this.gl.deleteBuffer(this.buffer),this.buffer=null),this.hooks.get("destroy")?.forEach(t=>t.call(this)),this.isInternalCanvas&&this.canvas.remove()}},_=p;
11
11
  //# sourceMappingURL=index.js.map