@stevejtrettel/shader-sandbox 0.1.3 → 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.
- package/README.md +220 -23
- package/bin/cli.js +106 -14
- package/dist-lib/app/App.d.ts +143 -15
- package/dist-lib/app/App.d.ts.map +1 -1
- package/dist-lib/app/App.js +1343 -108
- package/dist-lib/app/app.css +349 -24
- package/dist-lib/app/types.d.ts +48 -5
- package/dist-lib/app/types.d.ts.map +1 -1
- package/dist-lib/editor/EditorPanel.d.ts +2 -2
- package/dist-lib/editor/EditorPanel.d.ts.map +1 -1
- package/dist-lib/editor/EditorPanel.js +1 -1
- package/dist-lib/editor/editor-panel.css +55 -32
- package/dist-lib/editor/prism-editor.css +16 -16
- package/dist-lib/embed.js +1 -1
- package/dist-lib/engine/{ShadertoyEngine.d.ts → ShaderEngine.d.ts} +134 -10
- package/dist-lib/engine/ShaderEngine.d.ts.map +1 -0
- package/dist-lib/engine/ShaderEngine.js +1523 -0
- package/dist-lib/engine/glHelpers.d.ts +24 -0
- package/dist-lib/engine/glHelpers.d.ts.map +1 -1
- package/dist-lib/engine/glHelpers.js +88 -0
- package/dist-lib/engine/std140.d.ts +47 -0
- package/dist-lib/engine/std140.d.ts.map +1 -0
- package/dist-lib/engine/std140.js +119 -0
- package/dist-lib/engine/types.d.ts +55 -5
- package/dist-lib/engine/types.d.ts.map +1 -1
- package/dist-lib/engine/types.js +1 -1
- package/dist-lib/index.d.ts +4 -3
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +2 -1
- package/dist-lib/layouts/SplitLayout.d.ts +2 -1
- package/dist-lib/layouts/SplitLayout.d.ts.map +1 -1
- package/dist-lib/layouts/SplitLayout.js +3 -0
- package/dist-lib/layouts/TabbedLayout.d.ts.map +1 -1
- package/dist-lib/layouts/UILayout.d.ts +55 -0
- package/dist-lib/layouts/UILayout.d.ts.map +1 -0
- package/dist-lib/layouts/UILayout.js +147 -0
- package/dist-lib/layouts/default.css +2 -2
- package/dist-lib/layouts/index.d.ts +11 -1
- package/dist-lib/layouts/index.d.ts.map +1 -1
- package/dist-lib/layouts/index.js +17 -1
- package/dist-lib/layouts/split.css +33 -31
- package/dist-lib/layouts/tabbed.css +127 -74
- package/dist-lib/layouts/types.d.ts +14 -3
- package/dist-lib/layouts/types.d.ts.map +1 -1
- package/dist-lib/main.js +33 -0
- package/dist-lib/project/configHelpers.d.ts +45 -0
- package/dist-lib/project/configHelpers.d.ts.map +1 -0
- package/dist-lib/project/configHelpers.js +196 -0
- package/dist-lib/project/generatedLoader.d.ts +2 -2
- package/dist-lib/project/generatedLoader.d.ts.map +1 -1
- package/dist-lib/project/generatedLoader.js +23 -5
- package/dist-lib/project/loadProject.d.ts +6 -6
- package/dist-lib/project/loadProject.d.ts.map +1 -1
- package/dist-lib/project/loadProject.js +396 -144
- package/dist-lib/project/loaderHelper.d.ts +4 -4
- package/dist-lib/project/loaderHelper.d.ts.map +1 -1
- package/dist-lib/project/loaderHelper.js +278 -116
- package/dist-lib/project/types.d.ts +292 -13
- package/dist-lib/project/types.d.ts.map +1 -1
- package/dist-lib/project/types.js +13 -1
- package/dist-lib/styles/base.css +5 -1
- package/dist-lib/uniforms/UniformControls.d.ts +60 -0
- package/dist-lib/uniforms/UniformControls.d.ts.map +1 -0
- package/dist-lib/uniforms/UniformControls.js +518 -0
- package/dist-lib/uniforms/UniformStore.d.ts +74 -0
- package/dist-lib/uniforms/UniformStore.d.ts.map +1 -0
- package/dist-lib/uniforms/UniformStore.js +145 -0
- package/dist-lib/uniforms/UniformsPanel.d.ts +53 -0
- package/dist-lib/uniforms/UniformsPanel.d.ts.map +1 -0
- package/dist-lib/uniforms/UniformsPanel.js +124 -0
- package/dist-lib/uniforms/index.d.ts +11 -0
- package/dist-lib/uniforms/index.d.ts.map +1 -0
- package/dist-lib/uniforms/index.js +8 -0
- package/package.json +1 -1
- package/src/app/App.ts +1469 -126
- package/src/app/app.css +349 -24
- package/src/app/types.ts +53 -5
- package/src/editor/EditorPanel.ts +5 -5
- package/src/editor/editor-panel.css +55 -32
- package/src/editor/prism-editor.css +16 -16
- package/src/embed.ts +1 -1
- package/src/engine/ShaderEngine.ts +1934 -0
- package/src/engine/glHelpers.ts +117 -0
- package/src/engine/std140.ts +136 -0
- package/src/engine/types.ts +69 -5
- package/src/index.ts +4 -3
- package/src/layouts/SplitLayout.ts +8 -3
- package/src/layouts/TabbedLayout.ts +3 -3
- package/src/layouts/UILayout.ts +185 -0
- package/src/layouts/default.css +2 -2
- package/src/layouts/index.ts +20 -1
- package/src/layouts/split.css +33 -31
- package/src/layouts/tabbed.css +127 -74
- package/src/layouts/types.ts +19 -3
- package/src/layouts/ui.css +289 -0
- package/src/main.ts +39 -1
- package/src/project/configHelpers.ts +225 -0
- package/src/project/generatedLoader.ts +27 -6
- package/src/project/loadProject.ts +459 -173
- package/src/project/loaderHelper.ts +377 -130
- package/src/project/types.ts +360 -14
- package/src/styles/base.css +5 -1
- package/src/styles/theme.css +292 -0
- package/src/uniforms/UniformControls.ts +660 -0
- package/src/uniforms/UniformStore.ts +166 -0
- package/src/uniforms/UniformsPanel.ts +163 -0
- package/src/uniforms/index.ts +13 -0
- package/src/uniforms/uniform-controls.css +342 -0
- package/src/uniforms/uniforms-panel.css +277 -0
- package/templates/shaders/example-buffer/config.json +1 -0
- package/dist-lib/engine/ShadertoyEngine.d.ts.map +0 -1
- package/dist-lib/engine/ShadertoyEngine.js +0 -704
- package/src/engine/ShadertoyEngine.ts +0 -929
package/src/engine/glHelpers.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/engine/types.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Engine Layer - Type Definitions
|
|
3
3
|
*
|
|
4
|
-
* Internal types used by
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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 {
|
|
13
|
-
export
|
|
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 {
|
|
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:
|
|
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 {
|
|
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:
|
|
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
|
+
}
|
package/src/layouts/default.css
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
position: relative;
|
|
16
16
|
width: 800px;
|
|
17
17
|
height: 600px;
|
|
18
|
-
background:
|
|
18
|
+
background: var(--bg-canvas);
|
|
19
19
|
border-radius: 8px;
|
|
20
|
-
box-shadow:
|
|
20
|
+
box-shadow: var(--shadow-lg), var(--shadow-sm);
|
|
21
21
|
overflow: hidden;
|
|
22
22
|
}
|