@stevejtrettel/shader-sandbox 0.1.2 → 0.1.4

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 (113) hide show
  1. package/README.md +259 -235
  2. package/bin/cli.js +106 -14
  3. package/dist-lib/app/App.d.ts +143 -15
  4. package/dist-lib/app/App.d.ts.map +1 -1
  5. package/dist-lib/app/App.js +1343 -108
  6. package/dist-lib/app/app.css +349 -24
  7. package/dist-lib/app/types.d.ts +48 -5
  8. package/dist-lib/app/types.d.ts.map +1 -1
  9. package/dist-lib/editor/EditorPanel.d.ts +2 -2
  10. package/dist-lib/editor/EditorPanel.d.ts.map +1 -1
  11. package/dist-lib/editor/EditorPanel.js +1 -1
  12. package/dist-lib/editor/editor-panel.css +55 -32
  13. package/dist-lib/editor/prism-editor.css +16 -16
  14. package/dist-lib/embed.js +1 -1
  15. package/dist-lib/engine/{ShadertoyEngine.d.ts → ShaderEngine.d.ts} +134 -10
  16. package/dist-lib/engine/ShaderEngine.d.ts.map +1 -0
  17. package/dist-lib/engine/ShaderEngine.js +1523 -0
  18. package/dist-lib/engine/glHelpers.d.ts +24 -0
  19. package/dist-lib/engine/glHelpers.d.ts.map +1 -1
  20. package/dist-lib/engine/glHelpers.js +88 -0
  21. package/dist-lib/engine/std140.d.ts +47 -0
  22. package/dist-lib/engine/std140.d.ts.map +1 -0
  23. package/dist-lib/engine/std140.js +119 -0
  24. package/dist-lib/engine/types.d.ts +55 -5
  25. package/dist-lib/engine/types.d.ts.map +1 -1
  26. package/dist-lib/engine/types.js +1 -1
  27. package/dist-lib/index.d.ts +4 -3
  28. package/dist-lib/index.d.ts.map +1 -1
  29. package/dist-lib/index.js +2 -1
  30. package/dist-lib/layouts/SplitLayout.d.ts +2 -1
  31. package/dist-lib/layouts/SplitLayout.d.ts.map +1 -1
  32. package/dist-lib/layouts/SplitLayout.js +3 -0
  33. package/dist-lib/layouts/TabbedLayout.d.ts.map +1 -1
  34. package/dist-lib/layouts/UILayout.d.ts +55 -0
  35. package/dist-lib/layouts/UILayout.d.ts.map +1 -0
  36. package/dist-lib/layouts/UILayout.js +147 -0
  37. package/dist-lib/layouts/default.css +2 -2
  38. package/dist-lib/layouts/index.d.ts +11 -1
  39. package/dist-lib/layouts/index.d.ts.map +1 -1
  40. package/dist-lib/layouts/index.js +17 -1
  41. package/dist-lib/layouts/split.css +33 -31
  42. package/dist-lib/layouts/tabbed.css +127 -74
  43. package/dist-lib/layouts/types.d.ts +14 -3
  44. package/dist-lib/layouts/types.d.ts.map +1 -1
  45. package/dist-lib/main.js +33 -0
  46. package/dist-lib/project/configHelpers.d.ts +45 -0
  47. package/dist-lib/project/configHelpers.d.ts.map +1 -0
  48. package/dist-lib/project/configHelpers.js +196 -0
  49. package/dist-lib/project/generatedLoader.d.ts +2 -2
  50. package/dist-lib/project/generatedLoader.d.ts.map +1 -1
  51. package/dist-lib/project/generatedLoader.js +23 -5
  52. package/dist-lib/project/loadProject.d.ts +6 -6
  53. package/dist-lib/project/loadProject.d.ts.map +1 -1
  54. package/dist-lib/project/loadProject.js +396 -144
  55. package/dist-lib/project/loaderHelper.d.ts +4 -4
  56. package/dist-lib/project/loaderHelper.d.ts.map +1 -1
  57. package/dist-lib/project/loaderHelper.js +278 -116
  58. package/dist-lib/project/types.d.ts +292 -13
  59. package/dist-lib/project/types.d.ts.map +1 -1
  60. package/dist-lib/project/types.js +13 -1
  61. package/dist-lib/styles/base.css +5 -1
  62. package/dist-lib/uniforms/UniformControls.d.ts +60 -0
  63. package/dist-lib/uniforms/UniformControls.d.ts.map +1 -0
  64. package/dist-lib/uniforms/UniformControls.js +518 -0
  65. package/dist-lib/uniforms/UniformStore.d.ts +74 -0
  66. package/dist-lib/uniforms/UniformStore.d.ts.map +1 -0
  67. package/dist-lib/uniforms/UniformStore.js +145 -0
  68. package/dist-lib/uniforms/UniformsPanel.d.ts +53 -0
  69. package/dist-lib/uniforms/UniformsPanel.d.ts.map +1 -0
  70. package/dist-lib/uniforms/UniformsPanel.js +124 -0
  71. package/dist-lib/uniforms/index.d.ts +11 -0
  72. package/dist-lib/uniforms/index.d.ts.map +1 -0
  73. package/dist-lib/uniforms/index.js +8 -0
  74. package/package.json +16 -1
  75. package/src/app/App.ts +1469 -126
  76. package/src/app/app.css +349 -24
  77. package/src/app/types.ts +53 -5
  78. package/src/editor/EditorPanel.ts +5 -5
  79. package/src/editor/editor-panel.css +55 -32
  80. package/src/editor/prism-editor.css +16 -16
  81. package/src/embed.ts +1 -1
  82. package/src/engine/ShaderEngine.ts +1934 -0
  83. package/src/engine/glHelpers.ts +117 -0
  84. package/src/engine/std140.ts +136 -0
  85. package/src/engine/types.ts +69 -5
  86. package/src/index.ts +4 -3
  87. package/src/layouts/SplitLayout.ts +8 -3
  88. package/src/layouts/TabbedLayout.ts +3 -3
  89. package/src/layouts/UILayout.ts +185 -0
  90. package/src/layouts/default.css +2 -2
  91. package/src/layouts/index.ts +20 -1
  92. package/src/layouts/split.css +33 -31
  93. package/src/layouts/tabbed.css +127 -74
  94. package/src/layouts/types.ts +19 -3
  95. package/src/layouts/ui.css +289 -0
  96. package/src/main.ts +39 -1
  97. package/src/project/configHelpers.ts +225 -0
  98. package/src/project/generatedLoader.ts +27 -6
  99. package/src/project/loadProject.ts +459 -173
  100. package/src/project/loaderHelper.ts +377 -130
  101. package/src/project/types.ts +360 -14
  102. package/src/styles/base.css +5 -1
  103. package/src/styles/theme.css +292 -0
  104. package/src/uniforms/UniformControls.ts +660 -0
  105. package/src/uniforms/UniformStore.ts +166 -0
  106. package/src/uniforms/UniformsPanel.ts +163 -0
  107. package/src/uniforms/index.ts +13 -0
  108. package/src/uniforms/uniform-controls.css +342 -0
  109. package/src/uniforms/uniforms-panel.css +277 -0
  110. package/templates/shaders/example-buffer/config.json +1 -0
  111. package/dist-lib/engine/ShadertoyEngine.d.ts.map +0 -1
  112. package/dist-lib/engine/ShadertoyEngine.js +0 -704
  113. package/src/engine/ShadertoyEngine.ts +0 -929
@@ -407,6 +407,123 @@ export function createTextureFromImage(
407
407
  return tex;
408
408
  }
409
409
 
410
+ // =============================================================================
411
+ // Audio Texture
412
+ // =============================================================================
413
+
414
+ /**
415
+ * Create a 512x2 audio texture (Shadertoy-compatible).
416
+ * Row 0: frequency spectrum (FFT), Row 1: waveform.
417
+ * Uses R8 format — data is in the red channel, sampled with texture().x
418
+ */
419
+ export function createAudioTexture(gl: WebGL2RenderingContext): WebGLTexture {
420
+ const tex = gl.createTexture();
421
+ if (!tex) throw new Error('Failed to create audio texture');
422
+
423
+ gl.bindTexture(gl.TEXTURE_2D, tex);
424
+
425
+ const width = 512;
426
+ const height = 2;
427
+ const data = new Uint8Array(width * height); // R8, all zeros
428
+
429
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.R8, width, height, 0, gl.RED, gl.UNSIGNED_BYTE, data);
430
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
431
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
432
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
433
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
434
+ gl.bindTexture(gl.TEXTURE_2D, null);
435
+
436
+ return tex;
437
+ }
438
+
439
+ /**
440
+ * Update audio texture with frequency and waveform data.
441
+ */
442
+ export function updateAudioTextureData(
443
+ gl: WebGL2RenderingContext,
444
+ texture: WebGLTexture,
445
+ frequencyData: Uint8Array,
446
+ waveformData: Uint8Array,
447
+ ): void {
448
+ gl.bindTexture(gl.TEXTURE_2D, texture);
449
+ // Row 0: frequency
450
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 512, 1, gl.RED, gl.UNSIGNED_BYTE, frequencyData);
451
+ // Row 1: waveform
452
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 1, 512, 1, gl.RED, gl.UNSIGNED_BYTE, waveformData);
453
+ gl.bindTexture(gl.TEXTURE_2D, null);
454
+ }
455
+
456
+ // =============================================================================
457
+ // Video/Webcam Texture
458
+ // =============================================================================
459
+
460
+ /**
461
+ * Create a placeholder texture for video/webcam (1x1 black, RGBA).
462
+ * Updated each frame with actual video data once ready.
463
+ */
464
+ export function createVideoPlaceholderTexture(gl: WebGL2RenderingContext): WebGLTexture {
465
+ const tex = gl.createTexture();
466
+ if (!tex) throw new Error('Failed to create video texture');
467
+
468
+ gl.bindTexture(gl.TEXTURE_2D, tex);
469
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 0, 255]));
470
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
471
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
472
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
473
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
474
+ gl.bindTexture(gl.TEXTURE_2D, null);
475
+
476
+ return tex;
477
+ }
478
+
479
+ /**
480
+ * Upload a video frame to a texture.
481
+ */
482
+ export function updateVideoTexture(
483
+ gl: WebGL2RenderingContext,
484
+ texture: WebGLTexture,
485
+ video: HTMLVideoElement,
486
+ ): void {
487
+ gl.bindTexture(gl.TEXTURE_2D, texture);
488
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video);
489
+ gl.bindTexture(gl.TEXTURE_2D, null);
490
+ }
491
+
492
+ // =============================================================================
493
+ // Script-Uploaded Texture
494
+ // =============================================================================
495
+
496
+ /**
497
+ * Create or update a script-uploaded texture.
498
+ * Detects data type: Uint8Array → RGBA8, Float32Array → RGBA32F.
499
+ */
500
+ export function createOrUpdateScriptTexture(
501
+ gl: WebGL2RenderingContext,
502
+ existing: WebGLTexture | null,
503
+ width: number,
504
+ height: number,
505
+ data: Uint8Array | Float32Array,
506
+ ): WebGLTexture {
507
+ const tex = existing ?? gl.createTexture();
508
+ if (!tex) throw new Error('Failed to create script texture');
509
+
510
+ gl.bindTexture(gl.TEXTURE_2D, tex);
511
+
512
+ if (data instanceof Float32Array) {
513
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, width, height, 0, gl.RGBA, gl.FLOAT, data);
514
+ } else {
515
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
516
+ }
517
+
518
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
519
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
520
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
521
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
522
+ gl.bindTexture(gl.TEXTURE_2D, null);
523
+
524
+ return tex;
525
+ }
526
+
410
527
  // =============================================================================
411
528
  // Helper Utilities
412
529
  // =============================================================================
@@ -0,0 +1,136 @@
1
+ /**
2
+ * std140 Layout Packing Utilities
3
+ *
4
+ * Handles conversion from tightly-packed user data to std140 layout
5
+ * required by WebGL2 Uniform Buffer Objects.
6
+ *
7
+ * std140 array element rules:
8
+ * - Every array element is rounded up to a vec4 (16 bytes) stride
9
+ * - float[N]: 4 bytes data + 12 bytes padding per element
10
+ * - vec2[N]: 8 bytes data + 8 bytes padding per element
11
+ * - vec3[N]: 12 bytes data + 4 bytes padding per element
12
+ * - vec4[N]: 16 bytes, no padding
13
+ * - mat3[N]: 3 columns of vec4 (padded) = 48 bytes per element
14
+ * - mat4[N]: 4 columns of vec4 = 64 bytes, no padding needed
15
+ */
16
+
17
+ import { ArrayUniformType } from '../project/types';
18
+
19
+ /** Number of floats per element in tightly-packed user data */
20
+ const TIGHT_FLOATS: Record<ArrayUniformType, number> = {
21
+ float: 1,
22
+ vec2: 2,
23
+ vec3: 3,
24
+ vec4: 4,
25
+ mat3: 9,
26
+ mat4: 16,
27
+ };
28
+
29
+ /** Number of floats per array element in std140 layout */
30
+ const STD140_STRIDE_FLOATS: Record<ArrayUniformType, number> = {
31
+ float: 4, // 1 float + 3 padding
32
+ vec2: 4, // 2 floats + 2 padding
33
+ vec3: 4, // 3 floats + 1 padding
34
+ vec4: 4, // 4 floats, naturally aligned
35
+ mat3: 12, // 3 columns × 4 floats (vec3 padded to vec4)
36
+ mat4: 16, // 4 columns × 4 floats, no padding
37
+ };
38
+
39
+ /**
40
+ * Number of tightly-packed floats for a given type and count.
41
+ */
42
+ export function tightFloatCount(type: ArrayUniformType, count: number): number {
43
+ return TIGHT_FLOATS[type] * count;
44
+ }
45
+
46
+ /**
47
+ * Compute the total byte size of a std140 uniform block for an array.
48
+ */
49
+ export function std140ByteSize(type: ArrayUniformType, count: number): number {
50
+ return STD140_STRIDE_FLOATS[type] * count * 4; // 4 bytes per float
51
+ }
52
+
53
+ /**
54
+ * Compute the total number of floats in std140 layout for a given type and count.
55
+ */
56
+ export function std140FloatCount(type: ArrayUniformType, count: number): number {
57
+ return STD140_STRIDE_FLOATS[type] * count;
58
+ }
59
+
60
+ /**
61
+ * Pack tightly-packed user data into std140 layout.
62
+ *
63
+ * For mat4 and vec4, the tight layout is already std140-compatible,
64
+ * so this returns the input directly (no copy).
65
+ *
66
+ * For other types, allocates a new Float32Array with proper padding.
67
+ *
68
+ * @param type - The GLSL type of each array element
69
+ * @param count - Number of elements
70
+ * @param tightData - User-provided tightly-packed data
71
+ * @param out - Optional pre-allocated output buffer (reused across frames)
72
+ */
73
+ export function packStd140(
74
+ type: ArrayUniformType,
75
+ count: number,
76
+ tightData: Float32Array,
77
+ out?: Float32Array
78
+ ): Float32Array {
79
+ const tightPerElement = TIGHT_FLOATS[type];
80
+ const strideFloats = STD140_STRIDE_FLOATS[type];
81
+
82
+ // Fast path: mat4 and vec4 are already std140-compatible
83
+ if (tightPerElement === strideFloats) {
84
+ return tightData;
85
+ }
86
+
87
+ const totalFloats = strideFloats * count;
88
+ const result = out && out.length >= totalFloats ? out : new Float32Array(totalFloats);
89
+
90
+ if (type === 'mat3') {
91
+ // mat3: 9 tight floats → 3 columns of vec4 (12 floats)
92
+ // Column-major: tight = [c0r0, c0r1, c0r2, c1r0, c1r1, c1r2, c2r0, c2r1, c2r2]
93
+ // std140: [c0r0, c0r1, c0r2, 0, c1r0, c1r1, c1r2, 0, c2r0, c2r1, c2r2, 0]
94
+ for (let i = 0; i < count; i++) {
95
+ const srcOff = i * 9;
96
+ const dstOff = i * 12;
97
+ // Column 0
98
+ result[dstOff + 0] = tightData[srcOff + 0];
99
+ result[dstOff + 1] = tightData[srcOff + 1];
100
+ result[dstOff + 2] = tightData[srcOff + 2];
101
+ result[dstOff + 3] = 0;
102
+ // Column 1
103
+ result[dstOff + 4] = tightData[srcOff + 3];
104
+ result[dstOff + 5] = tightData[srcOff + 4];
105
+ result[dstOff + 6] = tightData[srcOff + 5];
106
+ result[dstOff + 7] = 0;
107
+ // Column 2
108
+ result[dstOff + 8] = tightData[srcOff + 6];
109
+ result[dstOff + 9] = tightData[srcOff + 7];
110
+ result[dstOff + 10] = tightData[srcOff + 8];
111
+ result[dstOff + 11] = 0;
112
+ }
113
+ } else {
114
+ // float, vec2, vec3: pad each element to 4 floats
115
+ for (let i = 0; i < count; i++) {
116
+ const srcOff = i * tightPerElement;
117
+ const dstOff = i * 4;
118
+ for (let j = 0; j < tightPerElement; j++) {
119
+ result[dstOff + j] = tightData[srcOff + j];
120
+ }
121
+ // Remaining floats stay 0 (from Float32Array initialization or previous clear)
122
+ for (let j = tightPerElement; j < 4; j++) {
123
+ result[dstOff + j] = 0;
124
+ }
125
+ }
126
+ }
127
+
128
+ return result;
129
+ }
130
+
131
+ /**
132
+ * Get the GLSL type string for use in uniform block declarations.
133
+ */
134
+ export function glslTypeName(type: ArrayUniformType): string {
135
+ return type; // float, vec2, vec3, vec4, mat3, mat4 are already valid GLSL
136
+ }
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * Engine Layer - Type Definitions
3
3
  *
4
- * Internal types used by ShadertoyEngine for managing WebGL resources.
4
+ * Internal types used by ShaderEngine for managing WebGL resources.
5
5
  * Based on docs/engine-spec.md
6
6
  */
7
7
 
8
8
  import type {
9
- ShadertoyProject,
9
+ ShaderProject,
10
10
  PassName,
11
11
  Channels,
12
+ ChannelSource,
12
13
  } from '../project/types';
13
14
 
14
15
  // =============================================================================
@@ -16,14 +17,14 @@ import type {
16
17
  // =============================================================================
17
18
 
18
19
  /**
19
- * Options for constructing a ShadertoyEngine.
20
+ * Options for constructing a ShaderEngine.
20
21
  *
21
22
  * The App is responsible for creating the WebGL2RenderingContext
22
23
  * and passing it in.
23
24
  */
24
25
  export interface EngineOptions {
25
26
  gl: WebGL2RenderingContext;
26
- project: ShadertoyProject;
27
+ project: ShaderProject;
27
28
  }
28
29
 
29
30
  // =============================================================================
@@ -45,9 +46,29 @@ export interface PassUniformLocations {
45
46
  iTimeDelta: WebGLUniformLocation | null;
46
47
  iFrame: WebGLUniformLocation | null;
47
48
  iMouse: WebGLUniformLocation | null;
49
+ iMousePressed: WebGLUniformLocation | null;
50
+ iDate: WebGLUniformLocation | null;
51
+ iFrameRate: WebGLUniformLocation | null;
48
52
 
49
53
  // iChannel0..3
50
54
  iChannel: (WebGLUniformLocation | null)[];
55
+
56
+ // iChannelResolution[4] - resolution of each channel texture
57
+ iChannelResolution: (WebGLUniformLocation | null)[];
58
+
59
+ // Touch uniforms (Shader Sandbox extensions)
60
+ iTouchCount: WebGLUniformLocation | null;
61
+ iTouch: (WebGLUniformLocation | null)[]; // iTouch0, iTouch1, iTouch2
62
+ iPinch: WebGLUniformLocation | null;
63
+ iPinchDelta: WebGLUniformLocation | null;
64
+ iPinchCenter: WebGLUniformLocation | null;
65
+
66
+ // Custom uniforms (from project config)
67
+ custom: Map<string, WebGLUniformLocation | null>;
68
+
69
+ // Named samplers (standard mode)
70
+ namedSamplers: Map<string, WebGLUniformLocation | null>;
71
+ namedSamplerResolutions: Map<string, WebGLUniformLocation | null>;
51
72
  }
52
73
 
53
74
  // =============================================================================
@@ -74,6 +95,9 @@ export interface RuntimePass {
74
95
  // - previous: where we read "previous" from when needed
75
96
  currentTexture: WebGLTexture;
76
97
  previousTexture: WebGLTexture;
98
+
99
+ // Named samplers (standard mode) - maps sampler name → channel source
100
+ namedSamplers?: Map<string, ChannelSource>;
77
101
  }
78
102
 
79
103
  // =============================================================================
@@ -82,7 +106,7 @@ export interface RuntimePass {
82
106
 
83
107
  /**
84
108
  * Runtime representation of an external 2D texture.
85
- * This corresponds 1:1 to ShadertoyTexture2D from the project.
109
+ * This corresponds 1:1 to ShaderTexture2D from the project.
86
110
  */
87
111
  export interface RuntimeTexture2D {
88
112
  name: string; // e.g. "tex0" (same as project texture name)
@@ -101,6 +125,46 @@ export interface RuntimeKeyboardTexture {
101
125
  height: number;
102
126
  }
103
127
 
128
+ /**
129
+ * Runtime audio texture (microphone FFT + waveform).
130
+ * 512x2, R8 format: row 0 = frequency, row 1 = waveform.
131
+ */
132
+ export interface RuntimeAudioTexture {
133
+ texture: WebGLTexture;
134
+ audioContext: AudioContext | null;
135
+ analyser: AnalyserNode | null;
136
+ stream: MediaStream | null;
137
+ frequencyData: Uint8Array<ArrayBuffer>; // 512 bytes
138
+ waveformData: Uint8Array<ArrayBuffer>; // 512 bytes
139
+ width: number; // 512
140
+ height: number; // 2
141
+ initialized: boolean;
142
+ }
143
+
144
+ /**
145
+ * Runtime video texture (webcam or video file).
146
+ */
147
+ export interface RuntimeVideoTexture {
148
+ texture: WebGLTexture;
149
+ video: HTMLVideoElement | null;
150
+ stream: MediaStream | null; // Only for webcam
151
+ width: number;
152
+ height: number;
153
+ ready: boolean;
154
+ kind: 'webcam' | 'video';
155
+ src?: string; // Only for video files
156
+ }
157
+
158
+ /**
159
+ * Runtime script-uploaded texture.
160
+ */
161
+ export interface RuntimeScriptTexture {
162
+ texture: WebGLTexture;
163
+ width: number;
164
+ height: number;
165
+ isFloat: boolean; // Float32Array → RGBA32F, Uint8Array → RGBA8
166
+ }
167
+
104
168
  // =============================================================================
105
169
  // Engine Stats
106
170
  // =============================================================================
package/src/index.ts CHANGED
@@ -7,7 +7,8 @@
7
7
  import './styles/base.css';
8
8
 
9
9
  export { App } from './app/App';
10
- export { createLayout } from './layouts';
10
+ export { createLayout, applyTheme } from './layouts';
11
11
  export { loadDemo } from './project/loaderHelper';
12
- export type { ShadertoyProject, ShadertoyConfig, PassName } from './project/types';
13
- export type { RecompileResult, BaseLayout, LayoutMode, LayoutOptions, RecompileHandler } from './layouts/types';
12
+ export type { ShaderProject, ProjectConfig, PassName, ThemeMode, DemoScriptHooks, ScriptEngineAPI, ArrayUniformDefinition } from './project/types';
13
+ export { isArrayUniform } from './project/types';
14
+ export type { RecompileResult, BaseLayout, LayoutMode, LayoutOptions, RecompileHandler, UniformChangeHandler } from './layouts/types';
@@ -7,12 +7,12 @@
7
7
 
8
8
  import './split.css';
9
9
 
10
- import { BaseLayout, LayoutOptions, RecompileHandler } from './types';
11
- import { ShadertoyProject } from '../project/types';
10
+ import { BaseLayout, LayoutOptions, RecompileHandler, UniformChangeHandler } from './types';
11
+ import { ShaderProject } from '../project/types';
12
12
 
13
13
  export class SplitLayout implements BaseLayout {
14
14
  private container: HTMLElement;
15
- private project: ShadertoyProject;
15
+ private project: ShaderProject;
16
16
  private root: HTMLElement;
17
17
  private canvasContainer: HTMLElement;
18
18
  private codePanel: HTMLElement;
@@ -20,6 +20,7 @@ export class SplitLayout implements BaseLayout {
20
20
  private editorPanel: any = null;
21
21
  private recompileHandler: RecompileHandler | null = null;
22
22
 
23
+
23
24
  constructor(opts: LayoutOptions) {
24
25
  this.container = opts.container;
25
26
  this.project = opts.project;
@@ -56,6 +57,10 @@ export class SplitLayout implements BaseLayout {
56
57
  }
57
58
  }
58
59
 
60
+ setUniformHandler(_handler: UniformChangeHandler): void {
61
+ // TODO: wire up uniform change handler to editor panel
62
+ }
63
+
59
64
  dispose(): void {
60
65
  if (this.editorPanel) {
61
66
  this.editorPanel.dispose();
@@ -8,7 +8,7 @@
8
8
  import './tabbed.css';
9
9
 
10
10
  import { BaseLayout, LayoutOptions, RecompileHandler } from './types';
11
- import { ShadertoyProject, PassName } from '../project/types';
11
+ import { ShaderProject, PassName } from '../project/types';
12
12
 
13
13
  type ShaderTab = { kind: 'shader'; name: string };
14
14
  type CodeTab = { kind: 'code'; name: string; passName: 'common' | PassName; source: string };
@@ -17,7 +17,7 @@ type Tab = ShaderTab | CodeTab | ImageTab;
17
17
 
18
18
  export class TabbedLayout implements BaseLayout {
19
19
  private container: HTMLElement;
20
- private project: ShadertoyProject;
20
+ private project: ShaderProject;
21
21
  private root: HTMLElement;
22
22
  private canvasContainer: HTMLElement;
23
23
  private contentArea: HTMLElement;
@@ -337,7 +337,7 @@ export class TabbedLayout implements BaseLayout {
337
337
  this.imageViewer.style.visibility = 'visible';
338
338
 
339
339
  const img = document.createElement('img');
340
- img.src = tab.url;
340
+ img.src = (tab as ImageTab).url;
341
341
  img.alt = tab.name;
342
342
 
343
343
  this.imageViewer.innerHTML = '';
@@ -0,0 +1,185 @@
1
+ /**
2
+ * UI Layout
3
+ *
4
+ * Shader on left, uniform controls panel on right (~200px wide).
5
+ * Playback controls (play/pause, reset, screenshot) at bottom of UI panel.
6
+ * Responsive: stacks vertically on small screens (<600px).
7
+ */
8
+
9
+ import './ui.css';
10
+
11
+ import { BaseLayout, LayoutOptions } from './types';
12
+ import { ShaderProject, UniformValue } from '../project/types';
13
+ import type { UniformControls as UniformControlsType } from '../uniforms/UniformControls';
14
+
15
+ export class UILayout implements BaseLayout {
16
+ private container: HTMLElement;
17
+ private project: ShaderProject;
18
+ private root: HTMLElement;
19
+ private canvasContainer: HTMLElement;
20
+ private uiPanel: HTMLElement;
21
+
22
+ // Uniform controls
23
+ private uniformsContainer: HTMLElement;
24
+ private uniformControls: UniformControlsType | null = null;
25
+
26
+ // Playback controls
27
+ private playbackContainer: HTMLElement;
28
+ private playPauseButton: HTMLElement | null = null;
29
+
30
+ // Callbacks (set by App)
31
+ private onPlayPause: (() => void) | null = null;
32
+ private onReset: (() => void) | null = null;
33
+ private onScreenshot: (() => void) | null = null;
34
+ private onUniformChange: ((name: string, value: UniformValue) => void) | null = null;
35
+
36
+ constructor(opts: LayoutOptions) {
37
+ this.container = opts.container;
38
+ this.project = opts.project;
39
+
40
+ // Create root layout container
41
+ this.root = document.createElement('div');
42
+ this.root.className = 'layout-ui';
43
+
44
+ // Create canvas container (left side)
45
+ this.canvasContainer = document.createElement('div');
46
+ this.canvasContainer.className = 'ui-canvas-container';
47
+
48
+ // Create UI panel (right side)
49
+ this.uiPanel = document.createElement('div');
50
+ this.uiPanel.className = 'ui-panel';
51
+
52
+ // Create uniforms container (scrollable, vertically centered)
53
+ this.uniformsContainer = document.createElement('div');
54
+ this.uniformsContainer.className = 'ui-uniforms-container';
55
+ this.uiPanel.appendChild(this.uniformsContainer);
56
+
57
+ // Create playback controls container (fixed at bottom)
58
+ this.playbackContainer = document.createElement('div');
59
+ this.playbackContainer.className = 'ui-playback-container';
60
+ this.buildPlaybackControls();
61
+ this.uiPanel.appendChild(this.playbackContainer);
62
+
63
+ // Load uniform controls
64
+ this.loadUniformControls();
65
+
66
+ // Assemble and append to DOM
67
+ this.root.appendChild(this.canvasContainer);
68
+ this.root.appendChild(this.uiPanel);
69
+ this.container.appendChild(this.root);
70
+ }
71
+
72
+ getCanvasContainer(): HTMLElement {
73
+ return this.canvasContainer;
74
+ }
75
+
76
+ /**
77
+ * Set callbacks for playback controls.
78
+ * Called by App after initialization.
79
+ */
80
+ setPlaybackCallbacks(callbacks: {
81
+ onPlayPause: () => void;
82
+ onReset: () => void;
83
+ onScreenshot: () => void;
84
+ }): void {
85
+ this.onPlayPause = callbacks.onPlayPause;
86
+ this.onReset = callbacks.onReset;
87
+ this.onScreenshot = callbacks.onScreenshot;
88
+ }
89
+
90
+ /**
91
+ * Set callback for uniform changes.
92
+ * Called by App after initialization.
93
+ */
94
+ setUniformCallback(callback: (name: string, value: UniformValue) => void): void {
95
+ this.onUniformChange = callback;
96
+ }
97
+
98
+ /**
99
+ * Update play/pause button state.
100
+ */
101
+ setPaused(paused: boolean): void {
102
+ if (this.playPauseButton) {
103
+ this.playPauseButton.innerHTML = paused
104
+ ? `<svg viewBox="0 0 16 16"><path d="M4 3v10l8-5-8-5z"/></svg>`
105
+ : `<svg viewBox="0 0 16 16"><path d="M5 3h2v10H5V3zm4 0h2v10H9V3z"/></svg>`;
106
+ this.playPauseButton.title = paused ? 'Play' : 'Pause';
107
+ }
108
+ }
109
+
110
+ dispose(): void {
111
+ if (this.uniformControls) {
112
+ this.uniformControls.destroy();
113
+ this.uniformControls = null;
114
+ }
115
+ this.container.innerHTML = '';
116
+ }
117
+
118
+ /**
119
+ * Build playback control buttons.
120
+ */
121
+ private buildPlaybackControls(): void {
122
+ // Play/Pause button
123
+ this.playPauseButton = document.createElement('button');
124
+ this.playPauseButton.className = 'ui-control-button';
125
+ this.playPauseButton.title = 'Pause';
126
+ this.playPauseButton.innerHTML = `<svg viewBox="0 0 16 16"><path d="M5 3h2v10H5V3zm4 0h2v10H9V3z"/></svg>`;
127
+ this.playPauseButton.addEventListener('click', () => {
128
+ if (this.onPlayPause) this.onPlayPause();
129
+ });
130
+
131
+ // Reset button
132
+ const resetButton = document.createElement('button');
133
+ resetButton.className = 'ui-control-button';
134
+ resetButton.title = 'Reset';
135
+ resetButton.innerHTML = `<svg viewBox="0 0 16 16"><path d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/><path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/></svg>`;
136
+ resetButton.addEventListener('click', () => {
137
+ if (this.onReset) this.onReset();
138
+ });
139
+
140
+ // Screenshot button
141
+ const screenshotButton = document.createElement('button');
142
+ screenshotButton.className = 'ui-control-button';
143
+ screenshotButton.title = 'Screenshot';
144
+ screenshotButton.innerHTML = `<svg viewBox="0 0 16 16"><path d="M10.5 8.5a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z"/><path d="M2 4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-1.172a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 9.172 2H6.828a2 2 0 0 0-1.414.586l-.828.828A2 2 0 0 1 3.172 4H2zm.5 2a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm9 2.5a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0z"/></svg>`;
145
+ screenshotButton.addEventListener('click', () => {
146
+ if (this.onScreenshot) this.onScreenshot();
147
+ });
148
+
149
+ this.playbackContainer.appendChild(this.playPauseButton);
150
+ this.playbackContainer.appendChild(resetButton);
151
+ this.playbackContainer.appendChild(screenshotButton);
152
+ }
153
+
154
+ /**
155
+ * Load uniform controls dynamically.
156
+ */
157
+ private async loadUniformControls(): Promise<void> {
158
+ const uniforms = this.project.uniforms;
159
+
160
+ // If no uniforms, show empty state
161
+ if (!uniforms || Object.keys(uniforms).length === 0) {
162
+ const emptyState = document.createElement('div');
163
+ emptyState.className = 'ui-empty-state';
164
+ emptyState.textContent = 'No uniforms';
165
+ this.uniformsContainer.appendChild(emptyState);
166
+ return;
167
+ }
168
+
169
+ try {
170
+ const { UniformControls } = await import('../uniforms/UniformControls');
171
+ this.uniformControls = new UniformControls({
172
+ container: this.uniformsContainer,
173
+ uniforms: uniforms,
174
+ onChange: (name: string, value: UniformValue) => {
175
+ if (this.onUniformChange) {
176
+ this.onUniformChange(name, value);
177
+ }
178
+ },
179
+ });
180
+ } catch (err) {
181
+ console.error('Failed to load uniform controls:', err);
182
+ this.uniformsContainer.textContent = 'Failed to load controls';
183
+ }
184
+ }
185
+ }
@@ -15,8 +15,8 @@
15
15
  position: relative;
16
16
  width: 800px;
17
17
  height: 600px;
18
- background: #000;
18
+ background: var(--bg-canvas);
19
19
  border-radius: 8px;
20
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25), 0 5px 15px rgba(0, 0, 0, 0.15);
20
+ box-shadow: var(--shadow-lg), var(--shadow-sm);
21
21
  overflow: hidden;
22
22
  }