@rosalana/sandbox 0.0.1
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/LICENCE +21 -0
- package/README.md +361 -0
- package/dist/errors.d.ts +32 -0
- package/dist/index.cjs.js +57 -0
- package/dist/index.d.ts +156 -0
- package/dist/index.es.js +1107 -0
- package/dist/tools/clock.d.ts +54 -0
- package/dist/tools/geometry.d.ts +62 -0
- package/dist/tools/hooks.d.ts +12 -0
- package/dist/tools/listener.d.ts +11 -0
- package/dist/tools/program.d.ts +58 -0
- package/dist/tools/uniform.d.ts +42 -0
- package/dist/tools/uniforms.d.ts +65 -0
- package/dist/tools/web_gl.d.ts +93 -0
- package/dist/types.d.ts +111 -0
- package/package.json +54 -0
package/LICENCE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rosalana
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
[](https://github.com/rosalana)
|
|
2
|
+
|
|
3
|
+
**Rosalana Sandbox** is a lightweight WebGL wrapper for **simple, beautiful shader effects**. It focuses on a clean API, type safety, and fast setup so you can go from idea to a shader in minutes.
|
|
4
|
+
|
|
5
|
+
It's **DX‑friendly**, small, and intentionally minimal — perfect for gradients, ambient backgrounds, and animated GLSL experiments. If you're not building a full 3D engine, Sandbox is a delightful alternative to larger libraries like three.js or p5.js.
|
|
6
|
+
|
|
7
|
+
### Bundle size comparison
|
|
8
|
+
|
|
9
|
+
| Library | Minified | Gzipped |
|
|
10
|
+
| ------------- | -------- | -------- |
|
|
11
|
+
| **Sandbox** | 31 KB | **8 KB** |
|
|
12
|
+
| three.js | 694 KB | 175 KB |
|
|
13
|
+
| p5.js | 1.1 MB | 351 KB |
|
|
14
|
+
|
|
15
|
+
Sandbox is **~22x smaller** than three.js and **~44x smaller** than p5.js.
|
|
16
|
+
|
|
17
|
+
It works in both **WebGL1 and WebGL2** contexts, with automatic fallback and detection.
|
|
18
|
+
|
|
19
|
+
## Table of Contents
|
|
20
|
+
|
|
21
|
+
- [Installation](#installation)
|
|
22
|
+
- [Quick setup](#quick-setup)
|
|
23
|
+
- [Playback control](#playback-control)
|
|
24
|
+
- [Time control](#time-control)
|
|
25
|
+
- [Static rendering](#static-rendering)
|
|
26
|
+
- [Shaders](#shaders)
|
|
27
|
+
- [WebGL version detection](#webgl-version-detection)
|
|
28
|
+
- [Uniforms](#uniforms)
|
|
29
|
+
- [Built‑in uniforms](#built-in-uniforms)
|
|
30
|
+
- [Hooks](#hooks)
|
|
31
|
+
- [Self-removing hooks](#self-removing-hooks)
|
|
32
|
+
- [Chaining](#chaining)
|
|
33
|
+
- [Error handling](#error-handling)
|
|
34
|
+
- [Vue integration](#vue-integration)
|
|
35
|
+
- [Cleanup](#cleanup)
|
|
36
|
+
- [Options](#options)
|
|
37
|
+
- [Limitations (by design)](#limitations-by-design)
|
|
38
|
+
- [License](#license)
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install @rosalana/sandbox
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick setup
|
|
47
|
+
|
|
48
|
+
Sandbox is designed to get you up and running with minimal effort. It ships with sensible defaults, so in most cases it only takes a few lines of code to get started.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { Sandbox } from "@rosalana/sandbox";
|
|
52
|
+
|
|
53
|
+
const sandbox = Sandbox.create(canvas, {
|
|
54
|
+
fragment: fragSource,
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
That's it. You get a running render loop and a fullscreen quad. No WebGL ceremony, no boilerplate — just your shader doing its thing.
|
|
59
|
+
|
|
60
|
+
## Playback control
|
|
61
|
+
|
|
62
|
+
Autoplay is enabled by default, so your shader starts rendering immediately. But you're in full control — pause, play, scrub through time, whatever you need.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
sandbox.play();
|
|
66
|
+
sandbox.pause();
|
|
67
|
+
sandbox.toggle();
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Want to know if it's running?
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
sandbox.isPlaying();
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Time control
|
|
77
|
+
|
|
78
|
+
This is where it gets fun. You can jump to any point in time, which is perfect for debugging or creating deterministic renders.
|
|
79
|
+
|
|
80
|
+
Start playing from a specific moment:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
sandbox.playAt(2.5);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Or set up an auto-pause — great for intro animations that should stop after a few seconds:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
sandbox.pauseAt(10);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Static rendering
|
|
93
|
+
|
|
94
|
+
Sometimes you don't need animation at all. Maybe you're generating a gradient thumbnail or rendering a single frame for export.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const sandbox = Sandbox.create(canvas, {
|
|
98
|
+
fragment: fragSource,
|
|
99
|
+
autoplay: false,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
sandbox.render();
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Or render at a specific time — perfect for deterministic, reproducible output:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
sandbox.renderAt(1.5);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Shaders
|
|
112
|
+
|
|
113
|
+
Shaders are the only thing you need to provide. Sandbox comes with a default fullscreen vertex shader, so you usually don't need to write one yourself — and it automatically switches between WebGL1 and WebGL2 versions depending on your fragment shader.
|
|
114
|
+
|
|
115
|
+
Update your fragment shader on the fly:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
sandbox.setFragment(fragmentSource);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
When you need full control over both vertex and fragment:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
sandbox.setShader(vertexSource, fragmentSource);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### WebGL version detection
|
|
128
|
+
|
|
129
|
+
Sandbox figures out which WebGL version you're using by looking at your shader code:
|
|
130
|
+
|
|
131
|
+
- `#version 300 es` → WebGL2
|
|
132
|
+
- no version directive → WebGL1
|
|
133
|
+
|
|
134
|
+
If WebGL2 isn't available, Sandbox falls back to WebGL1 automatically. You can always check what you're running:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
sandbox.webglVersion();
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Uniforms
|
|
141
|
+
|
|
142
|
+
Uniforms are how you feed data into your shader. Sandbox makes them **type‑safe** and **chainable** — no more guessing what went wrong.
|
|
143
|
+
|
|
144
|
+
Set a single uniform:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
sandbox.setUniform<number>("u_intensity", 0.8);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Set multiple uniforms at once:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
sandbox.setUniforms<{
|
|
154
|
+
u_intensity: number;
|
|
155
|
+
u_color: Vec3;
|
|
156
|
+
}>({
|
|
157
|
+
u_intensity: 0.75,
|
|
158
|
+
u_color: [1, 0.2, 0.3],
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Read back a uniform value:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
const intensity = sandbox.getUniform<number>("u_intensity");
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
All numeric values are treated as floats. This keeps the API simple and predictable.
|
|
169
|
+
|
|
170
|
+
## Built‑in uniforms
|
|
171
|
+
|
|
172
|
+
These uniforms are filled automatically every frame — no setup needed. Just declare them in your shader and they work:
|
|
173
|
+
|
|
174
|
+
| Uniform | Type | Description |
|
|
175
|
+
| -------------- | ----- | --------------------------- |
|
|
176
|
+
| `u_resolution` | vec2 | Canvas size in pixels |
|
|
177
|
+
| `u_time` | float | Elapsed time (seconds) |
|
|
178
|
+
| `u_delta` | float | Delta time since last frame |
|
|
179
|
+
| `u_mouse` | vec2 | Mouse position on canvas |
|
|
180
|
+
| `u_frame` | int | Frame counter |
|
|
181
|
+
|
|
182
|
+
## Hooks
|
|
183
|
+
|
|
184
|
+
Hooks are one of the most powerful features in Sandbox. They let you run logic every frame — before or after render — which opens up a world of possibilities.
|
|
185
|
+
|
|
186
|
+
**Pre-compute values on the CPU** before they hit the shader:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
sandbox.hook(({ time }) => {
|
|
190
|
+
const intensity = (Math.sin(time) + 1) / 2;
|
|
191
|
+
sandbox.setUniform("u_intensity", intensity);
|
|
192
|
+
}, "before");
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Sync state with reactive frameworks** like Vue or React:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
const playing = ref(false);
|
|
199
|
+
|
|
200
|
+
sandbox.hook(() => {
|
|
201
|
+
playing.value = sandbox.isPlaying();
|
|
202
|
+
}, "after");
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The hook returns a removal function, so you can clean up whenever you want:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
const remove = sandbox.hook(({ time }) => {
|
|
209
|
+
console.log(time);
|
|
210
|
+
}, "after");
|
|
211
|
+
|
|
212
|
+
remove();
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Self-removing hooks
|
|
216
|
+
|
|
217
|
+
Sometimes you need a hook that runs only until a condition is met. Just return `false` and the hook removes itself:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
sandbox.hook(({ time }) => {
|
|
221
|
+
if (time > 5) return false;
|
|
222
|
+
}, "after");
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
This is how `pauseAt()` works internally — it's hooks all the way down.
|
|
226
|
+
|
|
227
|
+
## Chaining
|
|
228
|
+
|
|
229
|
+
Every method returns `this`, so you can chain calls for clean, expressive code:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
sandbox
|
|
233
|
+
.setUniforms({ u_color: [1, 0, 0] })
|
|
234
|
+
.time(2.5)
|
|
235
|
+
.render();
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Error handling
|
|
239
|
+
|
|
240
|
+
Shader errors happen — typos, syntax mistakes, driver quirks. Sandbox handles them gracefully and reports them via a single callback. No try/catch needed.
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
Sandbox.create(canvas, {
|
|
244
|
+
fragment: shader,
|
|
245
|
+
onError: (error) => {
|
|
246
|
+
console.error(error.message);
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The error object includes useful details:
|
|
252
|
+
|
|
253
|
+
- `error.code` — error type (`SHADER_COMPILATION_FAILED`, `PROGRAM_LINK_FAILED`, etc.)
|
|
254
|
+
- `error.lines` — line numbers where errors occurred (for compilation errors)
|
|
255
|
+
- `error.shaderType` — which shader failed (`vertex` or `fragment`)
|
|
256
|
+
|
|
257
|
+
Error codes: `WEBGL_NOT_SUPPORTED`, `SHADER_COMPILATION_FAILED`, `PROGRAM_LINK_FAILED`, `SHADER_VERSION_MISMATCH`
|
|
258
|
+
|
|
259
|
+
## Vue integration
|
|
260
|
+
|
|
261
|
+
Here's a complete example showing how to use Sandbox with Vue's reactivity system:
|
|
262
|
+
|
|
263
|
+
```vue
|
|
264
|
+
<script setup lang="ts">
|
|
265
|
+
import { shallowRef, ref, onMounted, onUnmounted } from "vue";
|
|
266
|
+
import { Sandbox } from "@rosalana/sandbox";
|
|
267
|
+
|
|
268
|
+
const canvasRef = ref<HTMLCanvasElement>();
|
|
269
|
+
const sandbox = shallowRef<Sandbox | null>(null);
|
|
270
|
+
const isPlaying = ref(false);
|
|
271
|
+
|
|
272
|
+
onMounted(() => {
|
|
273
|
+
sandbox.value = Sandbox.create(canvasRef.value!, {
|
|
274
|
+
onAfterRender: () => {
|
|
275
|
+
isPlaying.value = sandbox.value?.isPlaying() ?? false;
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
onUnmounted(() => {
|
|
281
|
+
sandbox.value?.destroy();
|
|
282
|
+
});
|
|
283
|
+
</script>
|
|
284
|
+
|
|
285
|
+
<template>
|
|
286
|
+
{{ isPlaying ? "Playing" : "Paused" }}
|
|
287
|
+
<canvas ref="canvasRef" />
|
|
288
|
+
</template>
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Use `shallowRef` for the Sandbox instance — you don't want Vue making the WebGL context reactive.
|
|
292
|
+
|
|
293
|
+
## Cleanup
|
|
294
|
+
|
|
295
|
+
Always destroy when you're done. This releases all WebGL resources and removes event listeners:
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
sandbox.destroy();
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
In frameworks like Vue:
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
onUnmounted(() => {
|
|
305
|
+
sandbox.destroy();
|
|
306
|
+
});
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Options
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
interface SandboxOptions {
|
|
313
|
+
vertex?: string;
|
|
314
|
+
fragment?: string;
|
|
315
|
+
autoplay?: boolean;
|
|
316
|
+
pauseWhenHidden?: boolean;
|
|
317
|
+
dpr?: number | "auto";
|
|
318
|
+
preserveDrawingBuffer?: boolean;
|
|
319
|
+
antialias?: boolean;
|
|
320
|
+
onError?: (error: SandboxError) => void;
|
|
321
|
+
onLoad?: () => void;
|
|
322
|
+
onBeforeRender?: HookCallback | null;
|
|
323
|
+
onAfterRender?: HookCallback | null;
|
|
324
|
+
uniforms?: UniformSchema;
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
| Option | Default | Description |
|
|
329
|
+
| ----------------------- | ------------- | ------------------------------- |
|
|
330
|
+
| `vertex` | built-in | Custom vertex shader |
|
|
331
|
+
| `fragment` | built-in | Fragment shader |
|
|
332
|
+
| `autoplay` | `true` | Start rendering immediately |
|
|
333
|
+
| `pauseWhenHidden` | `true` | Pause when scrolled out of view |
|
|
334
|
+
| `dpr` | `"auto"` | Device pixel ratio |
|
|
335
|
+
| `preserveDrawingBuffer` | `false` | Keep buffer for screenshots |
|
|
336
|
+
| `antialias` | `true` | Enable antialiasing |
|
|
337
|
+
| `onError` | `console.error` | Error callback |
|
|
338
|
+
| `onLoad` | — | Called when ready |
|
|
339
|
+
| `onBeforeRender` | — | Hook before each frame |
|
|
340
|
+
| `onAfterRender` | — | Hook after each frame |
|
|
341
|
+
| `uniforms` | — | Initial uniform values |
|
|
342
|
+
|
|
343
|
+
## Limitations (by design)
|
|
344
|
+
|
|
345
|
+
- No textures (planned for future)
|
|
346
|
+
- No multi‑pass rendering
|
|
347
|
+
- No 3D scene graph
|
|
348
|
+
|
|
349
|
+
If you need a full engine, reach for three.js. For clean shader‑only effects, Sandbox is a joy to use.
|
|
350
|
+
|
|
351
|
+
## License
|
|
352
|
+
|
|
353
|
+
Rosalana Sandbox is open-source under the [MIT license](/LICENCE), allowing you to freely use, modify, and distribute it with minimal restrictions.
|
|
354
|
+
|
|
355
|
+
You may not be able to use our systems but you can use our code to build your own.
|
|
356
|
+
|
|
357
|
+
For details on how to contribute or how the Rosalana ecosystem is maintained, please refer to each repository's individual guidelines.
|
|
358
|
+
|
|
359
|
+
**Questions or feedback?**
|
|
360
|
+
|
|
361
|
+
Feel free to open an issue or contribute with a pull request. Happy coding with Rosalana!
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Error codes for Sandbox errors */
|
|
2
|
+
export type SandboxErrorCode = "WEBGL_NOT_SUPPORTED" | "CONTEXT_CREATION_FAILED" | "SHADER_COMPILATION_FAILED" | "PROGRAM_LINK_FAILED" | "SHADER_VERSION_MISMATCH" | "UNKNOWN_ERROR";
|
|
3
|
+
/** Base error class for all Sandbox errors */
|
|
4
|
+
export declare class SandboxError extends Error {
|
|
5
|
+
readonly code: SandboxErrorCode;
|
|
6
|
+
constructor(message: string, code: SandboxErrorCode);
|
|
7
|
+
}
|
|
8
|
+
/** WebGL context creation failure */
|
|
9
|
+
export declare class SandboxContextError extends SandboxError {
|
|
10
|
+
constructor(reason: "not_supported" | "creation_failed");
|
|
11
|
+
}
|
|
12
|
+
export declare class SandboxShaderVersionMismatchError extends SandboxError {
|
|
13
|
+
readonly vertexVersion: number;
|
|
14
|
+
readonly fragmentVersion: number;
|
|
15
|
+
constructor(vertexVersion: number, fragmentVersion: number);
|
|
16
|
+
}
|
|
17
|
+
/** Shader compilation failure with line info extraction */
|
|
18
|
+
export declare class SandboxShaderCompilationError extends SandboxError {
|
|
19
|
+
readonly shaderType: "vertex" | "fragment";
|
|
20
|
+
readonly source: string;
|
|
21
|
+
readonly infoLog: string;
|
|
22
|
+
/** Line numbers where errors occurred */
|
|
23
|
+
readonly lines: number[];
|
|
24
|
+
constructor(shaderType: "vertex" | "fragment", source: string, infoLog: string);
|
|
25
|
+
/** Parse error log to extract line numbers */
|
|
26
|
+
private static parseErrorLines;
|
|
27
|
+
}
|
|
28
|
+
/** Shader program linking failure */
|
|
29
|
+
export declare class SandboxProgramError extends SandboxError {
|
|
30
|
+
readonly infoLog: string;
|
|
31
|
+
constructor(infoLog: string);
|
|
32
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";var w=Object.defineProperty;var k=(o,t,e)=>t in o?w(o,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):o[t]=e;var i=(o,t,e)=>k(o,typeof t!="symbol"?t+"":t,e);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class u{constructor(t,e,r,s){this.target=t,this.type=e,this.listener=r,this.options=s,this.target.addEventListener(this.type,this.listener,this.options)}remove(){this.target.removeEventListener(this.type,this.listener,this.options)}static on(t,e,r,s){return t.addEventListener(e,r,s),()=>t.removeEventListener(e,r,s)}}class l extends Error{constructor(t,e){super(t),this.code=e,this.name="SandboxError"}}class S extends l{constructor(t){const e=t==="not_supported"?"WebGL is not supported in this browser.":"Failed to create WebGL context. The GPU may be unavailable.";super(e,t==="not_supported"?"WEBGL_NOT_SUPPORTED":"CONTEXT_CREATION_FAILED"),this.name="SandboxContextError"}}class L extends l{constructor(t,e){super(`Vertex and fragment shader WebGL versions do not match (${t} vs ${e})`,"SHADER_VERSION_MISMATCH"),this.vertexVersion=t,this.fragmentVersion=e,this.name="SandboxShaderVersionMismatchError"}}class c extends l{constructor(e,r,s){const n=c.parseErrorLines(s),a=n.length>0?` at line(s): ${n.join(", ")}`:"";super(`${e} shader compilation failed${a}
|
|
2
|
+
|
|
3
|
+
${s}`,"SHADER_COMPILATION_FAILED");i(this,"lines");this.shaderType=e,this.source=r,this.infoLog=s,this.name="SandboxShaderCompilationError",this.lines=n}static parseErrorLines(e){const r=[/ERROR:\s*\d*:(\d+)/g,/(\d+):(\d+)\(\d+\):/g,/^(\d+):/gm],s=new Set;for(const n of r){let a;for(;(a=n.exec(e))!==null;){const f=parseInt(a[1],10);f>0&&s.add(f)}}return[...s].sort((n,a)=>n-a)}}class d extends l{constructor(t){super(`Shader program linking failed
|
|
4
|
+
|
|
5
|
+
${t}`,"PROGRAM_LINK_FAILED"),this.infoLog=t,this.name="SandboxProgramError"}}class R{constructor(){i(this,"time",0);i(this,"delta",0);i(this,"frame",0);i(this,"running",!1);i(this,"startTime",0);i(this,"lastTime",0);i(this,"rafId",null);i(this,"callback",null);this.loop=this.loop.bind(this)}start(t){if(this.running)return this;this.callback=t,this.running=!0;const e=performance.now();return this.startTime=e,this.lastTime=e,this.rafId=requestAnimationFrame(this.loop),this}stop(){return this.running?(this.running=!1,this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null),this):this}reset(){return this.stop(),this.time=0,this.delta=0,this.frame=0,this}getState(){return{time:this.time,delta:this.delta,frame:this.frame}}tick(t=0){return this.delta=t,this.time+=t,this.frame++,this.callback&&this.callback(this.getState()),this}setTime(t){return this.time=t,this}destroy(){this.stop(),this.callback=null}loop(t){this.running&&(this.delta=(t-this.lastTime)/1e3,this.lastTime=t,this.time=(t-this.startTime)/1e3,this.frame++,this.callback&&this.callback(this.getState()),this.rafId=requestAnimationFrame(this.loop))}}class p{constructor(t){i(this,"gl");i(this,"vao",null);i(this,"vbo",null);i(this,"ibo",null);i(this,"vertexCount",0);i(this,"indexCount",0);i(this,"useIndices",!1);i(this,"vaoExt",null);i(this,"isWebGL2");this.gl=t,this.isWebGL2=t instanceof WebGL2RenderingContext,this.isWebGL2||(this.vaoExt=t.getExtension("OES_vertex_array_object"))}static fullscreenQuad(t){const e=new p(t),r=new Float32Array([-1,-1,0,0,1,-1,1,0,-1,1,0,1,1,1,1,1]),s=new Uint16Array([0,1,2,2,1,3]);return e.setup(r,s),e}setup(t,e){const r=this.gl;return this.createVAO(),this.bindVAO(),this.vbo=r.createBuffer(),r.bindBuffer(r.ARRAY_BUFFER,this.vbo),r.bufferData(r.ARRAY_BUFFER,t,r.STATIC_DRAW),this.vertexCount=t.length/4,e&&(this.ibo=r.createBuffer(),r.bindBuffer(r.ELEMENT_ARRAY_BUFFER,this.ibo),r.bufferData(r.ELEMENT_ARRAY_BUFFER,e,r.STATIC_DRAW),this.indexCount=e.length,this.useIndices=!0),this.unbindVAO(),this}linkAttributes(t){const e=this.gl;this.bindVAO(),e.bindBuffer(e.ARRAY_BUFFER,this.vbo);const r=4*Float32Array.BYTES_PER_ELEMENT,s=this.getPositionLocation(t);s>=0&&(e.enableVertexAttribArray(s),e.vertexAttribPointer(s,2,e.FLOAT,!1,r,0));const n=this.getTexcoordLocation(t);return n>=0&&(e.enableVertexAttribArray(n),e.vertexAttribPointer(n,2,e.FLOAT,!1,r,2*Float32Array.BYTES_PER_ELEMENT)),this.useIndices&&e.bindBuffer(e.ELEMENT_ARRAY_BUFFER,this.ibo),this.unbindVAO(),this}bind(){return this.bindVAO(),this}unbind(){return this.unbindVAO(),this}draw(){const t=this.gl;return this.bindVAO(),this.useIndices?t.drawElements(t.TRIANGLES,this.indexCount,t.UNSIGNED_SHORT,0):t.drawArrays(t.TRIANGLE_STRIP,0,this.vertexCount),this}destroy(){const t=this.gl;this.deleteVAO(),this.vbo&&(t.deleteBuffer(this.vbo),this.vbo=null),this.ibo&&(t.deleteBuffer(this.ibo),this.ibo=null)}getPositionLocation(t){let e=t.getAttribLocation("a_position");return e>=0||(e=t.getAttribLocation("aPosition"),e>=0)||(e=t.getAttribLocation("position"),e>=0)?e:-1}getTexcoordLocation(t){let e=t.getAttribLocation("a_texcoord");return e>=0||(e=t.getAttribLocation("aTexCoord"),e>=0)||(e=t.getAttribLocation("texcoord"),e>=0)||(e=t.getAttribLocation("a_uv"),e>=0)?e:-1}createVAO(){this.isWebGL2?this.vao=this.gl.createVertexArray():this.vaoExt&&(this.vao=this.vaoExt.createVertexArrayOES())}bindVAO(){this.vao&&(this.isWebGL2?this.gl.bindVertexArray(this.vao):this.vaoExt&&this.vaoExt.bindVertexArrayOES(this.vao))}unbindVAO(){this.isWebGL2?this.gl.bindVertexArray(null):this.vaoExt&&this.vaoExt.bindVertexArrayOES(null)}deleteVAO(){this.vao&&(this.isWebGL2?this.gl.deleteVertexArray(this.vao):this.vaoExt&&this.vaoExt.deleteVertexArrayOES(this.vao),this.vao=null)}}class h{constructor(t){i(this,"gl");i(this,"program",null);i(this,"vertexShader",null);i(this,"fragmentShader",null);i(this,"version",1);this.gl=t}static detectVersion(t){return/^\s*#version\s+300\s+es/m.test(t)?2:1}compile(t,e){this.destroy();const r=h.detectVersion(t),s=h.detectVersion(e);if(r!=s)throw new L(r,s);return this.version=Math.max(r,s),this.vertexShader=this.compileShader("vertex",t),this.fragmentShader=this.compileShader("fragment",e),this.linkProgram(),this}use(){return this.program&&this.gl.useProgram(this.program),this}getProgram(){return this.program}getVersion(){return this.version}getAttribLocation(t){return this.program?this.gl.getAttribLocation(this.program,t):-1}getUniformLocation(t){return this.program?this.gl.getUniformLocation(this.program,t):null}destroy(){const t=this.gl;this.program&&(this.vertexShader&&t.detachShader(this.program,this.vertexShader),this.fragmentShader&&t.detachShader(this.program,this.fragmentShader),t.deleteProgram(this.program),this.program=null),this.vertexShader&&(t.deleteShader(this.vertexShader),this.vertexShader=null),this.fragmentShader&&(t.deleteShader(this.fragmentShader),this.fragmentShader=null)}compileShader(t,e){const r=this.gl,s=t==="vertex"?r.VERTEX_SHADER:r.FRAGMENT_SHADER,n=r.createShader(s);if(!n)throw new c(t,e,"Failed to create shader object");if(r.shaderSource(n,e),r.compileShader(n),!r.getShaderParameter(n,r.COMPILE_STATUS)){const f=r.getShaderInfoLog(n)||"Unknown error";throw r.deleteShader(n),new c(t,e,f)}return n}linkProgram(){const t=this.gl;if(!this.vertexShader||!this.fragmentShader)throw new d("Shaders not compiled");const e=t.createProgram();if(!e)throw new d("Failed to create program object");if(t.attachShader(e,this.vertexShader),t.attachShader(e,this.fragmentShader),t.linkProgram(e),!t.getProgramParameter(e,t.LINK_STATUS)){const s=t.getProgramInfoLog(e)||"Unknown error";throw t.deleteProgram(e),new d(s)}this.program=e}}class x{constructor(t,e){i(this,"name");i(this,"method");i(this,"isArray");i(this,"isMatrix");i(this,"location",null);i(this,"locationResolved",!1);i(this,"value");this.name=t,this.value=e;const r=x.inferMethodInfo(e);this.method=r.method,this.isArray=r.isArray,this.isMatrix=r.isMatrix}static inferMethodInfo(t){if(typeof t=="boolean")return{method:"uniform1i",isArray:!1,isMatrix:!1};if(typeof t=="number")return{method:"uniform1f",isArray:!1,isMatrix:!1};if(!Array.isArray(t))return{method:"uniform1f",isArray:!1,isMatrix:!1};const e=t.length,r=t[0];if(Array.isArray(r))switch(r.length){case 2:return{method:"uniform2fv",isArray:!0,isMatrix:!1};case 3:return{method:"uniform3fv",isArray:!0,isMatrix:!1};case 4:return{method:"uniform4fv",isArray:!0,isMatrix:!1};default:return{method:"uniform1fv",isArray:!0,isMatrix:!1}}switch(e){case 2:return{method:"uniform2fv",isArray:!1,isMatrix:!1};case 3:return{method:"uniform3fv",isArray:!1,isMatrix:!1};case 4:return{method:"uniform4fv",isArray:!1,isMatrix:!1};case 9:return{method:"uniformMatrix3fv",isArray:!1,isMatrix:!0};case 16:return{method:"uniformMatrix4fv",isArray:!1,isMatrix:!0};default:return{method:"uniform1fv",isArray:!0,isMatrix:!1}}}resolveLocation(t,e){return this.locationResolved||(this.location=t.getUniformLocation(e,this.name),this.locationResolved=!0),this.location}invalidateLocation(){this.location=null,this.locationResolved=!1}setValue(t){this.value=t}getValue(){return this.value}upload(t,e){const r=this.resolveLocation(t,e);if(r===null)return;const s=this.value;let n;switch(typeof s=="boolean"?n=s?1:0:typeof s=="number"?n=s:this.isArray&&Array.isArray(s[0])?n=new Float32Array(s.flat()):n=new Float32Array(s),this.method){case"uniform1f":t.uniform1f(r,n);break;case"uniform1i":t.uniform1i(r,n);break;case"uniform1fv":t.uniform1fv(r,n);break;case"uniform2fv":t.uniform2fv(r,n);break;case"uniform3fv":t.uniform3fv(r,n);break;case"uniform4fv":t.uniform4fv(r,n);break;case"uniformMatrix2fv":t.uniformMatrix2fv(r,!1,n);break;case"uniformMatrix3fv":t.uniformMatrix3fv(r,!1,n);break;case"uniformMatrix4fv":t.uniformMatrix4fv(r,!1,n);break}}}const m=class m{constructor(t){i(this,"gl");i(this,"program",null);i(this,"uniforms",new Map);this.gl=t}attachProgram(t){this.program=t;for(const e of this.uniforms.values())e.invalidateLocation();return this}set(t,e){const r=this.uniforms.get(t);return r?r.setValue(e):this.uniforms.set(t,new x(t,e)),this}setMany(t){for(const[e,r]of Object.entries(t))this.set(e,r);return this}get(t){var e;return(e=this.uniforms.get(t))==null?void 0:e.getValue()}has(t){return this.uniforms.has(t)}delete(t){return this.uniforms.delete(t)}uploadAll(){if(!this.program)return this;for(const t of this.uniforms.values())t.upload(this.gl,this.program);return this}uploadBuiltIns(t,e,r){if(this.set("u_resolution",e),this.set("u_time",t.time),this.set("u_delta",t.delta),this.set("u_mouse",r),this.set("u_frame",t.frame),!this.program)return this;for(const s of m.BUILT_INS){const n=this.uniforms.get(s);n&&n.upload(this.gl,this.program)}return this}clear(){this.uniforms.clear()}destroy(){this.uniforms.clear(),this.program=null}keys(){return this.uniforms.keys()}get size(){return this.uniforms.size}};i(m,"BUILT_INS",new Set(["u_resolution","u_time","u_delta","u_mouse","u_frame"]));let v=m;class A{constructor(){i(this,"hooks",new Map)}id(){return Math.random().toString(36).substring(2,10)}add(t){const e=this.id();return this.hooks.set(e,t),()=>this.remove(e)}remove(t){this.hooks.delete(t)}run(t){for(const[e,r]of this.hooks)r(t)===!1&&this.remove(e)}destroy(){this.hooks.clear()}}class b{constructor(t,e){i(this,"canvas");i(this,"gl");i(this,"options");i(this,"onBeforeHooks",new A);i(this,"onAfterHooks",new A);i(this,"_program");i(this,"_geometry");i(this,"_uniforms");i(this,"_clock");i(this,"_resolution",[1,1]);i(this,"_mouse",[0,0]);i(this,"_version",1);i(this,"playing",!1);this.canvas=t,this.options=e,this.gl=this.initContext(),this.enableExtensions(),this._program=new h(this.gl),this._geometry=p.fullscreenQuad(this.gl),this._uniforms=new v(this.gl),this._clock=new R,this.options.onBeforeRender&&this.onBeforeHooks.add(this.options.onBeforeRender),this.options.onAfterRender&&this.onAfterHooks.add(this.options.onAfterRender),this.onRender=this.onRender.bind(this)}static setup(t,e){const r=new b(t,e);return e.vertex&&e.fragment&&r.shader(e.vertex,e.fragment),e.uniforms&&r._uniforms.setMany(e.uniforms),r}initContext(){const t={antialias:this.options.antialias,preserveDrawingBuffer:this.options.preserveDrawingBuffer,alpha:!0,depth:!1,stencil:!1},e=this.canvas.getContext("webgl2",t);if(e)return this._version=2,e;const r=this.canvas.getContext("webgl",t);if(r)return this._version=1,r;const s=new S("not_supported");throw this.options.onError(s),s}enableExtensions(){this.gl.getExtension("OES_standard_derivatives"),this.gl.getExtension("OES_texture_float"),this.gl.getExtension("OES_texture_float_linear"),this._version===1&&this.gl.getExtension("OES_vertex_array_object")}viewport(t,e,r,s){return this.canvas.width=r,this.canvas.height=s,this.gl.viewport(t,e,r,s),this._resolution=[r,s],this}clock(t){return this._clock.setTime(t),this}mouse(t,e){return this._mouse=[t,e],this}uniform(t,e){return this._uniforms.set(t,e),this}uniforms(t){return this._uniforms.setMany(t),this}getUniform(t){return this._uniforms.get(t)}shader(t,e){try{this._program.compile(t,e),this._version=this._program.getVersion(),this._geometry.linkAttributes(this._program);const r=this._program.getProgram();r&&this._uniforms.attachProgram(r)}catch(r){r instanceof l&&this.options.onError(r)}return this}play(){return this.playing?this:(this.playing=!0,this._clock.start(this.onRender),this)}pause(){if(!this.playing)return this;const t=this._clock.getState();return this.onBeforeHooks.run(t),this.playing=!1,this._clock.stop(),this.onAfterHooks.run(t),this}render(){return this.onRender(this._clock.getState()),this}getContext(){return this.gl}getVersion(){return this._version}destroy(){this.pause(),this._clock.destroy(),this._geometry.destroy(),this._program.destroy(),this._uniforms.destroy(),this.onAfterHooks.destroy(),this.onBeforeHooks.destroy()}onRender(t){const e=this.gl;this.onBeforeHooks.run(t),e.clearColor(0,0,0,0),e.clear(e.COLOR_BUFFER_BIT),this._program.use(),this._uniforms.uploadBuiltIns(t,this._resolution,this._mouse),this._uniforms.uploadAll(),this._geometry.bind(),this._geometry.draw(),this.onAfterHooks.run(t)}}const g=`#ifdef GL_ES
|
|
6
|
+
precision mediump float;
|
|
7
|
+
#endif
|
|
8
|
+
|
|
9
|
+
attribute vec2 a_position;
|
|
10
|
+
attribute vec2 a_texcoord;
|
|
11
|
+
|
|
12
|
+
varying vec2 v_texcoord;
|
|
13
|
+
|
|
14
|
+
void main() {
|
|
15
|
+
v_texcoord = a_texcoord;
|
|
16
|
+
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
17
|
+
}
|
|
18
|
+
`,E=`#ifdef GL_ES
|
|
19
|
+
precision mediump float;
|
|
20
|
+
#endif
|
|
21
|
+
|
|
22
|
+
uniform vec2 u_resolution;
|
|
23
|
+
uniform float u_time;
|
|
24
|
+
|
|
25
|
+
varying vec2 v_texcoord;
|
|
26
|
+
|
|
27
|
+
void main() {
|
|
28
|
+
vec2 uv = v_texcoord;
|
|
29
|
+
vec3 color = vec3(uv.x, uv.y, 0.5 + 0.5 * sin(u_time));
|
|
30
|
+
gl_FragColor = vec4(color, 1.0);
|
|
31
|
+
}
|
|
32
|
+
`,y=`#version 300 es
|
|
33
|
+
|
|
34
|
+
in vec2 a_position;
|
|
35
|
+
in vec2 a_texcoord;
|
|
36
|
+
|
|
37
|
+
out vec2 v_texcoord;
|
|
38
|
+
|
|
39
|
+
void main() {
|
|
40
|
+
v_texcoord = a_texcoord;
|
|
41
|
+
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
42
|
+
}`,M=`#version 300 es
|
|
43
|
+
precision highp float;
|
|
44
|
+
|
|
45
|
+
uniform vec2 u_resolution;
|
|
46
|
+
uniform float u_time;
|
|
47
|
+
uniform vec2 u_mouse;
|
|
48
|
+
|
|
49
|
+
in vec2 v_texcoord;
|
|
50
|
+
out vec4 fragColor;
|
|
51
|
+
|
|
52
|
+
void main() {
|
|
53
|
+
vec2 uv = gl_FragCoord.xy / u_resolution;
|
|
54
|
+
vec3 color = vec3(uv.x, uv.y, 0.5 + 0.5 * sin(u_time));
|
|
55
|
+
fragColor = vec4(color, 1.0);
|
|
56
|
+
}`;class _{constructor(t,e){i(this,"listeners",[]);i(this,"canvas");i(this,"options");i(this,"engine");this.canvas=t,this.options=this.resolveOptions(e),this.engine=b.setup(this.canvas,this.options),this.setupListeners(),this.setViewport(),this.options.onLoad(),this.options.autoplay&&this.play()}static create(t,e){return new _(t,e)}resolveOptions(t){const e={vertex:g,fragment:E,autoplay:!0,pauseWhenHidden:!0,dpr:"auto",preserveDrawingBuffer:!1,antialias:!0,onError:r=>{console.error("Oops!",r,`
|
|
57
|
+
You can handle errors programmatically by providing an onError callback to suppress this log and implement custom fallback behavior.`)},onLoad:()=>{},onBeforeRender:null,onAfterRender:null,uniforms:{}};if(t!=null&&t.vertex&&!(t!=null&&t.fragment)){const r=h.detectVersion(t.vertex);e.vertex=t.vertex,e.fragment=r===2?M:E}if(t!=null&&t.fragment&&!(t!=null&&t.vertex)){const r=h.detectVersion(t.fragment);e.fragment=t.fragment,e.vertex=r===2?y:g}return{...e,...t}}setupListeners(){this.listeners.push(u.on(window,"resize",()=>{this.setViewport()}),u.on(this.canvas,"resize",()=>{this.setViewport()}),u.on(document,"scroll",()=>{this.options.pauseWhenHidden&&(this.isInViewport()?this.play():this.pause())}),u.on(document,"mousemove",t=>{this.setMouse(t.clientX||t.pageX,t.clientY||t.pageY)}),u.on(document,"touchmove",t=>{t.touches.length>0&&this.setMouse(t.touches[0].clientX,t.touches[0].clientY)}))}destroyListeners(){this.listeners.forEach(t=>t()),this.listeners=[]}setViewport(){const t=this.options.dpr==="auto"?Math.min(2,window.devicePixelRatio||1):this.options.dpr,e=this.canvas.clientWidth||this.canvas.width||1,r=this.canvas.clientHeight||this.canvas.height||1;this.engine.viewport(0,0,Math.max(1,Math.floor(e*t)),Math.max(1,Math.floor(r*t)))}isInViewport(){const t=this.canvas.getBoundingClientRect();return t.bottom>=0&&t.right>=0&&t.top<=(window.innerHeight||document.documentElement.clientHeight)&&t.left<=(window.innerWidth||document.documentElement.clientWidth)}setMouse(t,e){const r=this.canvas.getBoundingClientRect();t>=r.left&&t<=r.right&&e>=r.top&&e<=r.bottom&&this.engine.mouse(t-r.left,e-r.top)}setUniform(t,e){return this.engine.uniform(t,e),this}setUniforms(t){return this.engine.uniforms(t),this}getUniform(t){return this.engine.getUniform(t)}setShader(t,e){return this.engine.shader(t,e),this}setFragment(t){const r=this.webglVersion()===1?g:y;return this.engine.shader(r,t),this}hook(t,e="before"){return e==="before"?this.engine.onBeforeHooks.add(t):this.engine.onAfterHooks.add(t)}play(){return this.engine.play(),this}playAt(t){return this.engine.clock(t),this.engine.play(),this}pause(){return this.engine.pause(),this}pauseAt(t){const e=this.hook(r=>{r.time>=t&&(e(),this.pause())},"after");return this}toggle(){return this.engine.playing?this.pause():this.play(),this}time(t){return this.engine.clock(t),this}render(){return this.engine.render(),this}renderAt(t){return this.engine.clock(t),this.engine.render(),this}isPlaying(){return this.engine.playing}webglVersion(){return this.engine.getVersion()}canvasElement(){return this.canvas}destroy(){this.destroyListeners(),this.engine.destroy()}}exports.Sandbox=_;exports.SandboxContextError=S;exports.SandboxError=l;exports.SandboxProgramError=d;exports.SandboxShaderCompilationError=c;exports.SandboxShaderVersionMismatchError=L;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { AnyUniformValue, HookCallback, SandboxOptions, UniformSchema, WebGLVersion } from "./types";
|
|
2
|
+
export * from "./types";
|
|
3
|
+
export * from "./errors";
|
|
4
|
+
/**
|
|
5
|
+
* Sandbox - A lightweight WebGL wrapper for shader effects.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // Static rendering
|
|
9
|
+
* const sandbox = new Sandbox(canvas, {
|
|
10
|
+
* fragment: myShader,
|
|
11
|
+
* autoplay: false,
|
|
12
|
+
* });
|
|
13
|
+
* sandbox.setUniforms({ u_time: 1.5 }).render();
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // Animation loop
|
|
17
|
+
* const sandbox = new Sandbox(canvas, {
|
|
18
|
+
* fragment: myShader,
|
|
19
|
+
* autoplay: true,
|
|
20
|
+
* });
|
|
21
|
+
*/
|
|
22
|
+
export declare class Sandbox {
|
|
23
|
+
/** Active event listeners */
|
|
24
|
+
private listeners;
|
|
25
|
+
/** HTML canvas element */
|
|
26
|
+
private canvas;
|
|
27
|
+
/** Resolved options */
|
|
28
|
+
private options;
|
|
29
|
+
/** WebGL engine */
|
|
30
|
+
private engine;
|
|
31
|
+
constructor(canvas: HTMLCanvasElement, options?: SandboxOptions);
|
|
32
|
+
/**
|
|
33
|
+
* Sandbox - A lightweight WebGL wrapper for shader effects.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // Static rendering
|
|
37
|
+
* const sandbox = Sandbox.create(canvas, {
|
|
38
|
+
* fragment: myShader,
|
|
39
|
+
* autoplay: false,
|
|
40
|
+
* });
|
|
41
|
+
* sandbox.setUniforms({ u_time: 1.5 }).render();
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* // Animation loop
|
|
45
|
+
* const sandbox = Sandbox.create(canvas, {
|
|
46
|
+
* fragment: myShader,
|
|
47
|
+
* autoplay: true,
|
|
48
|
+
* });
|
|
49
|
+
*/
|
|
50
|
+
static create(canvas: HTMLCanvasElement, options?: SandboxOptions): Sandbox;
|
|
51
|
+
private resolveOptions;
|
|
52
|
+
private setupListeners;
|
|
53
|
+
private destroyListeners;
|
|
54
|
+
private setViewport;
|
|
55
|
+
private isInViewport;
|
|
56
|
+
private setMouse;
|
|
57
|
+
/**
|
|
58
|
+
* Set a single uniform value with type checking.
|
|
59
|
+
* @example
|
|
60
|
+
* sandbox.setUniform<Vec3>("u_color", [1, 0, 0]);
|
|
61
|
+
* sandbox.setUniform<number>("u_time", 1.5);
|
|
62
|
+
* sandbox.setUniform<Vec3[]>("u_colors", [[1, 0, 0], [0, 1, 0]]);
|
|
63
|
+
*/
|
|
64
|
+
setUniform<T extends AnyUniformValue>(name: string, value: T): this;
|
|
65
|
+
/**
|
|
66
|
+
* Set multiple uniforms at once with type checking.
|
|
67
|
+
* @example
|
|
68
|
+
* interface MyUniforms extends UniformSchema {
|
|
69
|
+
* u_time: number;
|
|
70
|
+
* u_resolution: Vec2;
|
|
71
|
+
* u_colors: Vec3[];
|
|
72
|
+
* }
|
|
73
|
+
* sandbox.setUniforms<MyUniforms>({
|
|
74
|
+
* u_time: 1.5,
|
|
75
|
+
* u_resolution: [800, 600],
|
|
76
|
+
* u_colors: [[1, 0, 0], [0, 1, 0]],
|
|
77
|
+
* });
|
|
78
|
+
*/
|
|
79
|
+
setUniforms<T extends UniformSchema>(uniforms: T): this;
|
|
80
|
+
/**
|
|
81
|
+
* Get current uniform value.
|
|
82
|
+
*/
|
|
83
|
+
getUniform<T extends AnyUniformValue>(name: string): T | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* Update shaders.
|
|
86
|
+
* @example
|
|
87
|
+
* sandbox.setShader(vertexSource, fragmentSource);
|
|
88
|
+
*/
|
|
89
|
+
setShader(vertex: string, fragment: string): this;
|
|
90
|
+
/**
|
|
91
|
+
* Update only fragment shader (uses default vertex).
|
|
92
|
+
* @example
|
|
93
|
+
* sandbox.setFragment(fragmentSource);
|
|
94
|
+
*/
|
|
95
|
+
setFragment(fragment: string): this;
|
|
96
|
+
/**
|
|
97
|
+
* Add a runtime render hook.
|
|
98
|
+
*/
|
|
99
|
+
hook(callback: HookCallback, when?: "before" | "after"): () => void;
|
|
100
|
+
/**
|
|
101
|
+
* Start animation loop.
|
|
102
|
+
*/
|
|
103
|
+
play(): this;
|
|
104
|
+
/**
|
|
105
|
+
* Start animation loop at specific time (in seconds).
|
|
106
|
+
*/
|
|
107
|
+
playAt(time: number): this;
|
|
108
|
+
/**
|
|
109
|
+
* Stop animation loop.
|
|
110
|
+
*/
|
|
111
|
+
pause(): this;
|
|
112
|
+
/**
|
|
113
|
+
* Pause animation loop at specific time (in seconds).
|
|
114
|
+
*/
|
|
115
|
+
pauseAt(time: number): this;
|
|
116
|
+
/**
|
|
117
|
+
* Toggle play/pause state.
|
|
118
|
+
*/
|
|
119
|
+
toggle(): this;
|
|
120
|
+
/**
|
|
121
|
+
* Set current time (in seconds).
|
|
122
|
+
*/
|
|
123
|
+
time(time: number): this;
|
|
124
|
+
/**
|
|
125
|
+
* Render a single frame (for static rendering).
|
|
126
|
+
* @example
|
|
127
|
+
* sandbox.time(1.4).render();
|
|
128
|
+
*/
|
|
129
|
+
render(): this;
|
|
130
|
+
/**
|
|
131
|
+
* Render at specific time (for deterministic output).
|
|
132
|
+
* @example
|
|
133
|
+
* sandbox.renderAt(2.5); // Render as if 2.5 seconds elapsed
|
|
134
|
+
*/
|
|
135
|
+
renderAt(time: number): this;
|
|
136
|
+
/**
|
|
137
|
+
* Check if currently playing.
|
|
138
|
+
*/
|
|
139
|
+
isPlaying(): boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Get WebGL version using (1 or 2).
|
|
142
|
+
*/
|
|
143
|
+
webglVersion(): WebGLVersion;
|
|
144
|
+
/**
|
|
145
|
+
* Get canvas element.
|
|
146
|
+
*/
|
|
147
|
+
canvasElement(): HTMLCanvasElement;
|
|
148
|
+
/**
|
|
149
|
+
* Destroy sandbox and release all resources.
|
|
150
|
+
* @example
|
|
151
|
+
* onUnmounted(() => {
|
|
152
|
+
* sandbox.destroy();
|
|
153
|
+
* });
|
|
154
|
+
*/
|
|
155
|
+
destroy(): void;
|
|
156
|
+
}
|