@stevejtrettel/shader-sandbox 0.1.0
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 +391 -0
- package/bin/cli.js +389 -0
- package/dist-lib/app/App.d.ts +134 -0
- package/dist-lib/app/App.d.ts.map +1 -0
- package/dist-lib/app/App.js +570 -0
- package/dist-lib/app/types.d.ts +32 -0
- package/dist-lib/app/types.d.ts.map +1 -0
- package/dist-lib/app/types.js +6 -0
- package/dist-lib/editor/EditorPanel.d.ts +39 -0
- package/dist-lib/editor/EditorPanel.d.ts.map +1 -0
- package/dist-lib/editor/EditorPanel.js +274 -0
- package/dist-lib/editor/prism-editor.css +99 -0
- package/dist-lib/editor/prism-editor.d.ts +19 -0
- package/dist-lib/editor/prism-editor.d.ts.map +1 -0
- package/dist-lib/editor/prism-editor.js +96 -0
- package/dist-lib/embed.d.ts +17 -0
- package/dist-lib/embed.d.ts.map +1 -0
- package/dist-lib/embed.js +35 -0
- package/dist-lib/engine/ShadertoyEngine.d.ts +160 -0
- package/dist-lib/engine/ShadertoyEngine.d.ts.map +1 -0
- package/dist-lib/engine/ShadertoyEngine.js +704 -0
- package/dist-lib/engine/glHelpers.d.ts +79 -0
- package/dist-lib/engine/glHelpers.d.ts.map +1 -0
- package/dist-lib/engine/glHelpers.js +298 -0
- package/dist-lib/engine/types.d.ts +77 -0
- package/dist-lib/engine/types.d.ts.map +1 -0
- package/dist-lib/engine/types.js +7 -0
- package/dist-lib/index.d.ts +12 -0
- package/dist-lib/index.d.ts.map +1 -0
- package/dist-lib/index.js +9 -0
- package/dist-lib/layouts/DefaultLayout.d.ts +17 -0
- package/dist-lib/layouts/DefaultLayout.d.ts.map +1 -0
- package/dist-lib/layouts/DefaultLayout.js +27 -0
- package/dist-lib/layouts/FullscreenLayout.d.ts +17 -0
- package/dist-lib/layouts/FullscreenLayout.d.ts.map +1 -0
- package/dist-lib/layouts/FullscreenLayout.js +27 -0
- package/dist-lib/layouts/SplitLayout.d.ts +26 -0
- package/dist-lib/layouts/SplitLayout.d.ts.map +1 -0
- package/dist-lib/layouts/SplitLayout.js +61 -0
- package/dist-lib/layouts/TabbedLayout.d.ts +38 -0
- package/dist-lib/layouts/TabbedLayout.d.ts.map +1 -0
- package/dist-lib/layouts/TabbedLayout.js +305 -0
- package/dist-lib/layouts/index.d.ts +24 -0
- package/dist-lib/layouts/index.d.ts.map +1 -0
- package/dist-lib/layouts/index.js +36 -0
- package/dist-lib/layouts/split.css +196 -0
- package/dist-lib/layouts/tabbed.css +345 -0
- package/dist-lib/layouts/types.d.ts +48 -0
- package/dist-lib/layouts/types.d.ts.map +1 -0
- package/dist-lib/layouts/types.js +4 -0
- package/dist-lib/main.d.ts +15 -0
- package/dist-lib/main.d.ts.map +1 -0
- package/dist-lib/main.js +102 -0
- package/dist-lib/project/generatedLoader.d.ts +3 -0
- package/dist-lib/project/generatedLoader.d.ts.map +1 -0
- package/dist-lib/project/generatedLoader.js +17 -0
- package/dist-lib/project/loadProject.d.ts +22 -0
- package/dist-lib/project/loadProject.d.ts.map +1 -0
- package/dist-lib/project/loadProject.js +350 -0
- package/dist-lib/project/loaderHelper.d.ts +7 -0
- package/dist-lib/project/loaderHelper.d.ts.map +1 -0
- package/dist-lib/project/loaderHelper.js +240 -0
- package/dist-lib/project/types.d.ts +192 -0
- package/dist-lib/project/types.d.ts.map +1 -0
- package/dist-lib/project/types.js +7 -0
- package/dist-lib/styles/base.css +29 -0
- package/package.json +48 -0
- package/src/app/App.ts +699 -0
- package/src/app/app.css +208 -0
- package/src/app/types.ts +36 -0
- package/src/editor/EditorPanel.ts +340 -0
- package/src/editor/editor-panel.css +175 -0
- package/src/editor/prism-editor.css +99 -0
- package/src/editor/prism-editor.ts +124 -0
- package/src/embed.ts +55 -0
- package/src/engine/ShadertoyEngine.ts +929 -0
- package/src/engine/glHelpers.ts +432 -0
- package/src/engine/types.ts +118 -0
- package/src/index.ts +13 -0
- package/src/layouts/DefaultLayout.ts +40 -0
- package/src/layouts/FullscreenLayout.ts +40 -0
- package/src/layouts/SplitLayout.ts +81 -0
- package/src/layouts/TabbedLayout.ts +371 -0
- package/src/layouts/default.css +22 -0
- package/src/layouts/fullscreen.css +15 -0
- package/src/layouts/index.ts +44 -0
- package/src/layouts/split.css +196 -0
- package/src/layouts/tabbed.css +345 -0
- package/src/layouts/types.ts +58 -0
- package/src/main.ts +114 -0
- package/src/project/generatedLoader.ts +23 -0
- package/src/project/loadProject.ts +421 -0
- package/src/project/loaderHelper.ts +300 -0
- package/src/project/types.ts +243 -0
- package/src/styles/base.css +29 -0
- package/src/styles/embed.css +14 -0
- package/src/vite-env.d.ts +1 -0
- package/templates/index.html +28 -0
- package/templates/main.ts +126 -0
- package/templates/package.json +12 -0
- package/templates/shaders/example-buffer/bufferA.glsl +14 -0
- package/templates/shaders/example-buffer/config.json +10 -0
- package/templates/shaders/example-buffer/image.glsl +5 -0
- package/templates/shaders/example-gradient/config.json +4 -0
- package/templates/shaders/example-gradient/image.glsl +7 -0
- package/templates/vite.config.js +35 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine Layer - WebGL Helper Functions
|
|
3
|
+
*
|
|
4
|
+
* Low-level WebGL utilities for shader compilation, texture creation,
|
|
5
|
+
* framebuffer management, and VAO setup.
|
|
6
|
+
*
|
|
7
|
+
* Based on docs/engine-spec.md
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Shader Compilation
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Compile a shader of given type from source. Throws on error.
|
|
16
|
+
*/
|
|
17
|
+
export function createShader(
|
|
18
|
+
gl: WebGL2RenderingContext,
|
|
19
|
+
type: GLenum,
|
|
20
|
+
source: string
|
|
21
|
+
): WebGLShader {
|
|
22
|
+
const shader = gl.createShader(type);
|
|
23
|
+
if (!shader) {
|
|
24
|
+
throw new Error('Failed to create shader object');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
gl.shaderSource(shader, source);
|
|
28
|
+
gl.compileShader(shader);
|
|
29
|
+
|
|
30
|
+
const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
|
|
31
|
+
if (!success) {
|
|
32
|
+
const infoLog = gl.getShaderInfoLog(shader);
|
|
33
|
+
gl.deleteShader(shader);
|
|
34
|
+
throw new Error(`Shader compilation failed:\n${infoLog}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return shader;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Link a program from vertex + fragment source strings.
|
|
42
|
+
* Throws with a descriptive error if link fails.
|
|
43
|
+
*/
|
|
44
|
+
export function createProgramFromSources(
|
|
45
|
+
gl: WebGL2RenderingContext,
|
|
46
|
+
vertexSource: string,
|
|
47
|
+
fragmentSource: string
|
|
48
|
+
): WebGLProgram {
|
|
49
|
+
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);
|
|
50
|
+
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
|
|
51
|
+
|
|
52
|
+
const program = gl.createProgram();
|
|
53
|
+
if (!program) {
|
|
54
|
+
throw new Error('Failed to create program object');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
gl.attachShader(program, vertexShader);
|
|
58
|
+
gl.attachShader(program, fragmentShader);
|
|
59
|
+
gl.linkProgram(program);
|
|
60
|
+
|
|
61
|
+
const success = gl.getProgramParameter(program, gl.LINK_STATUS);
|
|
62
|
+
if (!success) {
|
|
63
|
+
const infoLog = gl.getProgramInfoLog(program);
|
|
64
|
+
gl.deleteProgram(program);
|
|
65
|
+
gl.deleteShader(vertexShader);
|
|
66
|
+
gl.deleteShader(fragmentShader);
|
|
67
|
+
throw new Error(`Program linking failed:\n${infoLog}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Clean up shaders (no longer needed after linking)
|
|
71
|
+
gl.detachShader(program, vertexShader);
|
|
72
|
+
gl.detachShader(program, fragmentShader);
|
|
73
|
+
gl.deleteShader(vertexShader);
|
|
74
|
+
gl.deleteShader(fragmentShader);
|
|
75
|
+
|
|
76
|
+
return program;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// =============================================================================
|
|
80
|
+
// Fullscreen Triangle VAO
|
|
81
|
+
// =============================================================================
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create a full-screen triangle VAO.
|
|
85
|
+
*
|
|
86
|
+
* Attribute layout:
|
|
87
|
+
* - location 0: vec2 position in clip space.
|
|
88
|
+
*
|
|
89
|
+
* The triangle covers the entire screen using three vertices:
|
|
90
|
+
* (-1, -1), (3, -1), (-1, 3)
|
|
91
|
+
* This ensures all pixels are covered without needing two triangles.
|
|
92
|
+
*/
|
|
93
|
+
export function createFullscreenTriangleVAO(gl: WebGL2RenderingContext): WebGLVertexArrayObject {
|
|
94
|
+
const vao = gl.createVertexArray();
|
|
95
|
+
if (!vao) {
|
|
96
|
+
throw new Error('Failed to create VAO');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
gl.bindVertexArray(vao);
|
|
100
|
+
|
|
101
|
+
// Create VBO with triangle vertices
|
|
102
|
+
const positions = new Float32Array([
|
|
103
|
+
-1, -1, // Bottom-left
|
|
104
|
+
3, -1, // Bottom-right (extends beyond viewport)
|
|
105
|
+
-1, 3, // Top-left (extends beyond viewport)
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
const vbo = gl.createBuffer();
|
|
109
|
+
if (!vbo) {
|
|
110
|
+
throw new Error('Failed to create VBO');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
|
|
114
|
+
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
|
|
115
|
+
|
|
116
|
+
// Enable and configure vertex attribute at location 0
|
|
117
|
+
gl.enableVertexAttribArray(0);
|
|
118
|
+
gl.vertexAttribPointer(
|
|
119
|
+
0, // attribute location
|
|
120
|
+
2, // size (vec2)
|
|
121
|
+
gl.FLOAT, // type
|
|
122
|
+
false, // normalized
|
|
123
|
+
0, // stride
|
|
124
|
+
0 // offset
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Unbind
|
|
128
|
+
gl.bindVertexArray(null);
|
|
129
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
|
130
|
+
|
|
131
|
+
return vao;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// =============================================================================
|
|
135
|
+
// Texture Creation
|
|
136
|
+
// =============================================================================
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Create a float RGBA texture (no data) for use as a render target.
|
|
140
|
+
* This MUST use an internal format compatible with EXT_color_buffer_float.
|
|
141
|
+
*
|
|
142
|
+
* Per spec: ALL render targets MUST be RGBA32F.
|
|
143
|
+
*/
|
|
144
|
+
export function createRenderTargetTexture(
|
|
145
|
+
gl: WebGL2RenderingContext,
|
|
146
|
+
width: number,
|
|
147
|
+
height: number
|
|
148
|
+
): WebGLTexture {
|
|
149
|
+
const tex = gl.createTexture();
|
|
150
|
+
if (!tex) {
|
|
151
|
+
throw new Error('Failed to create texture');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
155
|
+
|
|
156
|
+
// Allocate RGBA32F texture (float format required by Shadertoy spec)
|
|
157
|
+
gl.texImage2D(
|
|
158
|
+
gl.TEXTURE_2D,
|
|
159
|
+
0, // mip level
|
|
160
|
+
gl.RGBA32F, // internal format (32-bit float per channel)
|
|
161
|
+
width,
|
|
162
|
+
height,
|
|
163
|
+
0, // border (must be 0)
|
|
164
|
+
gl.RGBA, // format
|
|
165
|
+
gl.FLOAT, // type
|
|
166
|
+
null // no data (allocate only)
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Set filtering to NEAREST (common for simulation/compute shaders)
|
|
170
|
+
// This prevents interpolation artifacts in PDE/physics shaders
|
|
171
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
172
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
173
|
+
|
|
174
|
+
// Set wrap mode to CLAMP_TO_EDGE (prevent wrap-around at boundaries)
|
|
175
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
176
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
177
|
+
|
|
178
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
179
|
+
|
|
180
|
+
return tex;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Create a framebuffer with a single color attachment at COLOR_ATTACHMENT0.
|
|
185
|
+
* Throws if framebuffer is not complete.
|
|
186
|
+
*/
|
|
187
|
+
export function createFramebufferWithColorAttachment(
|
|
188
|
+
gl: WebGL2RenderingContext,
|
|
189
|
+
texture: WebGLTexture
|
|
190
|
+
): WebGLFramebuffer {
|
|
191
|
+
const fbo = gl.createFramebuffer();
|
|
192
|
+
if (!fbo) {
|
|
193
|
+
throw new Error('Failed to create framebuffer');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
|
197
|
+
|
|
198
|
+
// Attach texture to color attachment 0
|
|
199
|
+
gl.framebufferTexture2D(
|
|
200
|
+
gl.FRAMEBUFFER,
|
|
201
|
+
gl.COLOR_ATTACHMENT0,
|
|
202
|
+
gl.TEXTURE_2D,
|
|
203
|
+
texture,
|
|
204
|
+
0 // mip level
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// Check framebuffer completeness
|
|
208
|
+
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
|
|
209
|
+
if (status !== gl.FRAMEBUFFER_COMPLETE) {
|
|
210
|
+
gl.deleteFramebuffer(fbo);
|
|
211
|
+
throw new Error(`Framebuffer incomplete: ${getFramebufferStatusString(gl, status)}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
215
|
+
|
|
216
|
+
return fbo;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Create a 1x1 black float texture for unused channels.
|
|
221
|
+
*
|
|
222
|
+
* Per spec: UNUSED CHANNELS MUST STILL BIND A 1×1 BLACK FLOAT TEXTURE.
|
|
223
|
+
* This prevents NaN/undefined behavior when sampling unused channels.
|
|
224
|
+
*/
|
|
225
|
+
export function createBlackTexture(gl: WebGL2RenderingContext): WebGLTexture {
|
|
226
|
+
const tex = gl.createTexture();
|
|
227
|
+
if (!tex) {
|
|
228
|
+
throw new Error('Failed to create black texture');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
232
|
+
|
|
233
|
+
// 1x1 black pixel [0, 0, 0, 1] as float
|
|
234
|
+
const blackPixel = new Float32Array([0, 0, 0, 1]);
|
|
235
|
+
|
|
236
|
+
gl.texImage2D(
|
|
237
|
+
gl.TEXTURE_2D,
|
|
238
|
+
0,
|
|
239
|
+
gl.RGBA32F,
|
|
240
|
+
1,
|
|
241
|
+
1,
|
|
242
|
+
0,
|
|
243
|
+
gl.RGBA,
|
|
244
|
+
gl.FLOAT,
|
|
245
|
+
blackPixel
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
// Set filtering
|
|
249
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
250
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
251
|
+
|
|
252
|
+
// Set wrap mode
|
|
253
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
254
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
255
|
+
|
|
256
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
257
|
+
|
|
258
|
+
return tex;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Create a 256x3 keyboard state texture.
|
|
263
|
+
*
|
|
264
|
+
* Shadertoy keyboard texture format:
|
|
265
|
+
* - Width: 256 (one column per ASCII keycode)
|
|
266
|
+
* - Height: 3 (3 rows for different data)
|
|
267
|
+
* - Row 0 (sample at y=0.25): Current key state (0.0 = up, 1.0 = down)
|
|
268
|
+
* - Row 1: Unused
|
|
269
|
+
* - Row 2 (sample at y=0.75): Toggle state (flips between 0.0 and 1.0 on each press)
|
|
270
|
+
*
|
|
271
|
+
* Returns the WebGLTexture. Data is initialized to all zeros.
|
|
272
|
+
* Use updateKeyboardTexture() to update the state.
|
|
273
|
+
*/
|
|
274
|
+
export function createKeyboardTexture(gl: WebGL2RenderingContext): WebGLTexture {
|
|
275
|
+
const tex = gl.createTexture();
|
|
276
|
+
if (!tex) {
|
|
277
|
+
throw new Error('Failed to create keyboard texture');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
281
|
+
|
|
282
|
+
// 256x3 texture, all zeros initially
|
|
283
|
+
const width = 256;
|
|
284
|
+
const height = 3;
|
|
285
|
+
const data = new Float32Array(width * height * 4); // RGBA, all zeros
|
|
286
|
+
|
|
287
|
+
gl.texImage2D(
|
|
288
|
+
gl.TEXTURE_2D,
|
|
289
|
+
0,
|
|
290
|
+
gl.RGBA32F,
|
|
291
|
+
width,
|
|
292
|
+
height,
|
|
293
|
+
0,
|
|
294
|
+
gl.RGBA,
|
|
295
|
+
gl.FLOAT,
|
|
296
|
+
data
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
// NEAREST filtering - no interpolation between keys!
|
|
300
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
301
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
302
|
+
|
|
303
|
+
// CLAMP to edge
|
|
304
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
305
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
306
|
+
|
|
307
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
308
|
+
|
|
309
|
+
return tex;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Update keyboard texture with current key states.
|
|
314
|
+
*
|
|
315
|
+
* @param gl WebGL context
|
|
316
|
+
* @param texture The keyboard texture to update
|
|
317
|
+
* @param keyStates Map of keycode -> current state (true = down, false = up)
|
|
318
|
+
* @param toggleStates Map of keycode -> toggle state (0.0 or 1.0)
|
|
319
|
+
*/
|
|
320
|
+
export function updateKeyboardTexture(
|
|
321
|
+
gl: WebGL2RenderingContext,
|
|
322
|
+
texture: WebGLTexture,
|
|
323
|
+
keyStates: Map<number, boolean>,
|
|
324
|
+
toggleStates: Map<number, number>
|
|
325
|
+
): void {
|
|
326
|
+
const width = 256;
|
|
327
|
+
const height = 3;
|
|
328
|
+
const data = new Float32Array(width * height * 4);
|
|
329
|
+
|
|
330
|
+
// Fill in key states
|
|
331
|
+
for (let keycode = 0; keycode < 256; keycode++) {
|
|
332
|
+
const isDown = keyStates.get(keycode) || false;
|
|
333
|
+
const toggleValue = toggleStates.get(keycode) || 0.0;
|
|
334
|
+
|
|
335
|
+
// Row 0 (y=0): Current key state
|
|
336
|
+
const row0Index = (0 * width + keycode) * 4;
|
|
337
|
+
data[row0Index + 0] = isDown ? 1.0 : 0.0; // R channel
|
|
338
|
+
data[row0Index + 1] = isDown ? 1.0 : 0.0; // G channel (redundant but matches Shadertoy)
|
|
339
|
+
data[row0Index + 2] = isDown ? 1.0 : 0.0; // B channel
|
|
340
|
+
data[row0Index + 3] = 1.0; // A channel
|
|
341
|
+
|
|
342
|
+
// Row 1 (y=1): Unused (keep as zeros)
|
|
343
|
+
|
|
344
|
+
// Row 2 (y=2): Toggle state
|
|
345
|
+
const row2Index = (2 * width + keycode) * 4;
|
|
346
|
+
data[row2Index + 0] = toggleValue;
|
|
347
|
+
data[row2Index + 1] = toggleValue;
|
|
348
|
+
data[row2Index + 2] = toggleValue;
|
|
349
|
+
data[row2Index + 3] = 1.0;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
353
|
+
gl.texSubImage2D(
|
|
354
|
+
gl.TEXTURE_2D,
|
|
355
|
+
0,
|
|
356
|
+
0, 0, // x, y offset
|
|
357
|
+
width, height,
|
|
358
|
+
gl.RGBA,
|
|
359
|
+
gl.FLOAT,
|
|
360
|
+
data
|
|
361
|
+
);
|
|
362
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Create a 2D texture from an HTMLImageElement (or ImageBitmap).
|
|
367
|
+
* This is used for project textures (dog.png, noise.png, etc.)
|
|
368
|
+
*
|
|
369
|
+
* NOTE: actual image loading is done by the App; engine just gets an
|
|
370
|
+
* already-loaded image object.
|
|
371
|
+
*/
|
|
372
|
+
export function createTextureFromImage(
|
|
373
|
+
gl: WebGL2RenderingContext,
|
|
374
|
+
image: HTMLImageElement | ImageBitmap,
|
|
375
|
+
filter: 'nearest' | 'linear',
|
|
376
|
+
wrap: 'clamp' | 'repeat'
|
|
377
|
+
): WebGLTexture {
|
|
378
|
+
const tex = gl.createTexture();
|
|
379
|
+
if (!tex) {
|
|
380
|
+
throw new Error('Failed to create texture');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
384
|
+
|
|
385
|
+
// Upload image data
|
|
386
|
+
gl.texImage2D(
|
|
387
|
+
gl.TEXTURE_2D,
|
|
388
|
+
0,
|
|
389
|
+
gl.RGBA,
|
|
390
|
+
gl.RGBA,
|
|
391
|
+
gl.UNSIGNED_BYTE,
|
|
392
|
+
image
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
// Set filtering
|
|
396
|
+
const glFilter = filter === 'nearest' ? gl.NEAREST : gl.LINEAR;
|
|
397
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, glFilter);
|
|
398
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, glFilter);
|
|
399
|
+
|
|
400
|
+
// Set wrap mode
|
|
401
|
+
const glWrap = wrap === 'clamp' ? gl.CLAMP_TO_EDGE : gl.REPEAT;
|
|
402
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, glWrap);
|
|
403
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, glWrap);
|
|
404
|
+
|
|
405
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
406
|
+
|
|
407
|
+
return tex;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// =============================================================================
|
|
411
|
+
// Helper Utilities
|
|
412
|
+
// =============================================================================
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Get a human-readable string for framebuffer status.
|
|
416
|
+
*/
|
|
417
|
+
function getFramebufferStatusString(gl: WebGL2RenderingContext, status: GLenum): string {
|
|
418
|
+
switch (status) {
|
|
419
|
+
case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
|
|
420
|
+
return 'FRAMEBUFFER_INCOMPLETE_ATTACHMENT';
|
|
421
|
+
case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
|
|
422
|
+
return 'FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT';
|
|
423
|
+
case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS:
|
|
424
|
+
return 'FRAMEBUFFER_INCOMPLETE_DIMENSIONS';
|
|
425
|
+
case gl.FRAMEBUFFER_UNSUPPORTED:
|
|
426
|
+
return 'FRAMEBUFFER_UNSUPPORTED';
|
|
427
|
+
case gl.FRAMEBUFFER_INCOMPLETE_MULTISAMPLE:
|
|
428
|
+
return 'FRAMEBUFFER_INCOMPLETE_MULTISAMPLE';
|
|
429
|
+
default:
|
|
430
|
+
return `Unknown status: ${status}`;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine Layer - Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* Internal types used by ShadertoyEngine for managing WebGL resources.
|
|
5
|
+
* Based on docs/engine-spec.md
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
ShadertoyProject,
|
|
10
|
+
PassName,
|
|
11
|
+
Channels,
|
|
12
|
+
} from '../project/types';
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Engine Options
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Options for constructing a ShadertoyEngine.
|
|
20
|
+
*
|
|
21
|
+
* The App is responsible for creating the WebGL2RenderingContext
|
|
22
|
+
* and passing it in.
|
|
23
|
+
*/
|
|
24
|
+
export interface EngineOptions {
|
|
25
|
+
gl: WebGL2RenderingContext;
|
|
26
|
+
project: ShadertoyProject;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// Per-Pass Uniform Locations
|
|
31
|
+
// =============================================================================
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Per-pass uniform locations and metadata.
|
|
35
|
+
*
|
|
36
|
+
* NOTE: This is separate from RuntimePass so that we can keep
|
|
37
|
+
* the GL program + locations together.
|
|
38
|
+
*/
|
|
39
|
+
export interface PassUniformLocations {
|
|
40
|
+
program: WebGLProgram;
|
|
41
|
+
|
|
42
|
+
// Core Shadertoy uniforms
|
|
43
|
+
iResolution: WebGLUniformLocation | null;
|
|
44
|
+
iTime: WebGLUniformLocation | null;
|
|
45
|
+
iTimeDelta: WebGLUniformLocation | null;
|
|
46
|
+
iFrame: WebGLUniformLocation | null;
|
|
47
|
+
iMouse: WebGLUniformLocation | null;
|
|
48
|
+
|
|
49
|
+
// iChannel0..3
|
|
50
|
+
iChannel: (WebGLUniformLocation | null)[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// =============================================================================
|
|
54
|
+
// Runtime Pass Representation
|
|
55
|
+
// =============================================================================
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A runtime representation of a pass:
|
|
59
|
+
* - knows which project pass it corresponds to
|
|
60
|
+
* - owns two textures (current + previous) for ping-pong
|
|
61
|
+
* - owns a framebuffer and VAO for drawing
|
|
62
|
+
*/
|
|
63
|
+
export interface RuntimePass {
|
|
64
|
+
name: PassName;
|
|
65
|
+
projectChannels: Channels;
|
|
66
|
+
|
|
67
|
+
vao: WebGLVertexArrayObject;
|
|
68
|
+
uniforms: PassUniformLocations;
|
|
69
|
+
|
|
70
|
+
framebuffer: WebGLFramebuffer;
|
|
71
|
+
|
|
72
|
+
// Ping-pong textures:
|
|
73
|
+
// - current: where we write this frame
|
|
74
|
+
// - previous: where we read "previous" from when needed
|
|
75
|
+
currentTexture: WebGLTexture;
|
|
76
|
+
previousTexture: WebGLTexture;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// =============================================================================
|
|
80
|
+
// Runtime Texture Representation
|
|
81
|
+
// =============================================================================
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Runtime representation of an external 2D texture.
|
|
85
|
+
* This corresponds 1:1 to ShadertoyTexture2D from the project.
|
|
86
|
+
*/
|
|
87
|
+
export interface RuntimeTexture2D {
|
|
88
|
+
name: string; // e.g. "tex0" (same as project texture name)
|
|
89
|
+
texture: WebGLTexture;
|
|
90
|
+
width: number;
|
|
91
|
+
height: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Keyboard texture representation.
|
|
96
|
+
* For v1, you can leave this unimplemented or just stub it out.
|
|
97
|
+
*/
|
|
98
|
+
export interface RuntimeKeyboardTexture {
|
|
99
|
+
texture: WebGLTexture;
|
|
100
|
+
width: number;
|
|
101
|
+
height: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// =============================================================================
|
|
105
|
+
// Engine Stats
|
|
106
|
+
// =============================================================================
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Engine stats (for optional overlay / debugging).
|
|
110
|
+
*/
|
|
111
|
+
export interface EngineStats {
|
|
112
|
+
frame: number; // iFrame
|
|
113
|
+
time: number; // total time in seconds (iTime)
|
|
114
|
+
deltaTime: number; // last frame delta in seconds (iTimeDelta)
|
|
115
|
+
width: number;
|
|
116
|
+
height: number;
|
|
117
|
+
}
|
|
118
|
+
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shadertoy Runner - Public API
|
|
3
|
+
*
|
|
4
|
+
* This module exports everything needed to create a shader playground.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import './styles/base.css';
|
|
8
|
+
|
|
9
|
+
export { App } from './app/App';
|
|
10
|
+
export { createLayout } from './layouts';
|
|
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';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Layout
|
|
3
|
+
*
|
|
4
|
+
* Centered canvas with rounded corners and drop shadow.
|
|
5
|
+
* Default layout mode for a polished presentation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import './default.css';
|
|
9
|
+
|
|
10
|
+
import { BaseLayout, LayoutOptions } from './types';
|
|
11
|
+
|
|
12
|
+
export class DefaultLayout implements BaseLayout {
|
|
13
|
+
private container: HTMLElement;
|
|
14
|
+
private root: HTMLElement;
|
|
15
|
+
private canvasContainer: HTMLElement;
|
|
16
|
+
|
|
17
|
+
constructor(opts: LayoutOptions) {
|
|
18
|
+
this.container = opts.container;
|
|
19
|
+
|
|
20
|
+
// Create root layout container
|
|
21
|
+
this.root = document.createElement('div');
|
|
22
|
+
this.root.className = 'layout-default';
|
|
23
|
+
|
|
24
|
+
// Create canvas container
|
|
25
|
+
this.canvasContainer = document.createElement('div');
|
|
26
|
+
this.canvasContainer.className = 'canvas-container';
|
|
27
|
+
|
|
28
|
+
// Assemble and append to DOM
|
|
29
|
+
this.root.appendChild(this.canvasContainer);
|
|
30
|
+
this.container.appendChild(this.root);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getCanvasContainer(): HTMLElement {
|
|
34
|
+
return this.canvasContainer;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
dispose(): void {
|
|
38
|
+
this.container.innerHTML = '';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fullscreen Layout
|
|
3
|
+
*
|
|
4
|
+
* Canvas fills entire viewport, no padding or styling.
|
|
5
|
+
* Used for immersive shader experiences.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import './fullscreen.css';
|
|
9
|
+
|
|
10
|
+
import { BaseLayout, LayoutOptions } from './types';
|
|
11
|
+
|
|
12
|
+
export class FullscreenLayout implements BaseLayout {
|
|
13
|
+
private container: HTMLElement;
|
|
14
|
+
private root: HTMLElement;
|
|
15
|
+
private canvasContainer: HTMLElement;
|
|
16
|
+
|
|
17
|
+
constructor(opts: LayoutOptions) {
|
|
18
|
+
this.container = opts.container;
|
|
19
|
+
|
|
20
|
+
// Create root layout container
|
|
21
|
+
this.root = document.createElement('div');
|
|
22
|
+
this.root.className = 'layout-fullscreen';
|
|
23
|
+
|
|
24
|
+
// Create canvas container
|
|
25
|
+
this.canvasContainer = document.createElement('div');
|
|
26
|
+
this.canvasContainer.className = 'canvas-container';
|
|
27
|
+
|
|
28
|
+
// Assemble and append to DOM
|
|
29
|
+
this.root.appendChild(this.canvasContainer);
|
|
30
|
+
this.container.appendChild(this.root);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getCanvasContainer(): HTMLElement {
|
|
34
|
+
return this.canvasContainer;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
dispose(): void {
|
|
38
|
+
this.container.innerHTML = '';
|
|
39
|
+
}
|
|
40
|
+
}
|