@rosalana/sandbox
v0.2.1
Published
Lightweight WebGL wrapper for simple, beautiful shader effects
Downloads
94
Maintainers
Readme
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.
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.
Bundle size comparison
| Library | Minified | Gzipped | | ----------- | -------- | --------- | | Sandbox | 85 KB | 23 KB | | three.js | 694 KB | 175 KB | | p5.js | 1.1 MB | 351 KB |
Sandbox is ~8x smaller than three.js and ~15x smaller than p5.js.
It works in both WebGL1 and WebGL2 contexts, with automatic fallback and detection.
Table of Contents
- Installation
- Quick setup
- Playback control
- Sandbox Shaders
- Hooks
- Textures
- Export
- Chaining
- Error handling
- Vue integration
- Cleanup
- Options
- Limitations (by design)
- License
Installation
npm install @rosalana/sandboxQuick setup
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.
import { Sandbox } from "@rosalana/sandbox";
const sandbox = Sandbox.create(canvas, {
fragment: fragSource,
});That's it. You get a running render loop and a fullscreen quad. No WebGL ceremony, no boilerplate — just your shader doing its thing.
Playback control
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.
sandbox.play();
sandbox.pause();
sandbox.toggle();Want to know if it's running?
sandbox.isPlaying();Time control
This is where it gets fun. You can jump to any point in time, which is perfect for debugging or creating deterministic renders.
Start playing from a specific moment:
sandbox.playAt(2.5);Or set up an auto-pause — great for intro animations that should stop after a few seconds:
sandbox.pauseAt(10);Static rendering
Sometimes you don't need animation at all. Maybe you're generating a gradient thumbnail or rendering a single frame for export.
const sandbox = Sandbox.create(canvas, {
fragment: fragSource,
autoplay: false,
});
sandbox.render();Or render at a specific time — perfect for deterministic, reproducible output:
sandbox.renderAt(1.5);Sandbox Shaders
Writing GLSL from scratch means a lot of ceremony — uniform declarations, copy-pasting utility functions, wiring everything together. Sandbox takes care of the boring parts so you can focus on the shader itself.
The idea is simple: define reusable GLSL snippets as modules, then #import them with a single line. Sandbox resolves dependencies, declares uniforms, and injects everything into the final shader automatically.
Writing shaders
You only need to provide a fragment shader. Sandbox ships with a default fullscreen vertex shader and automatically matches WebGL versions — so you can focus on the fun part.
sandbox.setFragment(fragmentSource);Need full control over both shaders? No problem:
sandbox.setShader(vertexSource, fragmentSource);Sandbox detects WebGL version from your code (#version 300 es → WebGL2, no directive → WebGL1) and falls back gracefully. You can always check what you're running:
sandbox.version; // 1 or 2Built-in uniforms
These uniforms are populated automatically every frame. Just use them in your shader — no declaration or setup needed:
| Uniform | Type | Description |
| -------------- | ----- | --------------------------- |
| u_resolution | vec2 | Canvas size in pixels |
| u_time | float | Elapsed time (seconds) |
| u_delta | float | Delta time since last frame |
| u_mouse | vec2 | Mouse position on canvas |
| u_frame | int | Frame counter |
Built-in uniforms are globally available — even inside imported module functions. They're never namespaced, so u_time is always just u_time, everywhere.
Custom uniforms
Need your own data in the shader? Declare the uniform in GLSL, then set it from JavaScript:
sandbox.setUniform<number>("u_intensity", 0.8);
sandbox.setUniforms({
u_intensity: 0.75,
u_color: [1, 0.2, 0.3],
});Read a value back:
const intensity = sandbox.getUniform<number>("u_intensity");Everything is type-safe and chainable. All numeric values are treated as floats — simple and predictable.
Modules
[!IMPORTANT] Sandbox's built-in GLSL modules are still in beta and may change at any time. We published them early to get feedback. If you have ideas for improvements, please open an issue or a PR — we'd love your input.
Modules are reusable GLSL snippets that you can import into any shader. Sandbox ships with a built-in "sandbox" module, and you can define your own:
Sandbox.defineModule("my_effects", myGLSLSource);Then import any function from it:
#import bloom from "my_effects"
#import hex from "sandbox"
void main() {
vec3 color = hex(0xFF5733);
fragColor = vec4(bloom(color), 1.0);
}When you import a function, Sandbox pulls in everything it needs — the function body, any helper functions it calls, and any required uniforms. Each import is fully isolated, so importing the same function twice won't cause conflicts.
[!NOTE] All imported code is namespaced automatically to avoid naming collisions.
Module names starting with "sandbox" are reserved for built-in modules. Each module can only be defined once — this prevents accidental overwrites.
Want to see what's available? Inspect all registered modules at any time:
Sandbox.availableModules();You can also preview how your shader will look after processing:
Sandbox.compile(shaderSource);This is useful for debugging or precompiling shaders before deploying.
Module options
Imported functions can expose configurable options — friendly names that map to GLSL uniforms under the hood. You control them from JavaScript using sandbox.module():
sandbox.module("effect", {
intensity: 0.8,
});This is a powerful way to customize imported effects without touching any GLSL code.
Hardcoded vs. dynamic values
By default, a module's uniforms are only included in the final shader when you actually reference them. This gives you a choice — hardcode a value directly, or use the @ syntax to wire it up as a configurable uniform:
#import effect from "my_module"
void main() {
vec3 a = effect(v_texcoord, 2.0); // hardcoded value
vec3 b = effect(v_texcoord, @effect.intensity); // dynamic — set via sandbox.module()
fragColor = vec4(a + b, 1.0);
}The @effect.intensity syntax tells Sandbox to inject the uniform and keep it in sync with whatever you set in JavaScript.
Defining options
When defining a module, you declare which options each function supports:
Sandbox.defineModule("my_gradient", gradientSource, {
myFunc: {
colors: {
uniform: "u_colors",
default: [
[1, 0, 0],
[0, 0, 1],
],
},
speed: { uniform: "u_speed", default: 1.0 },
},
});Each option has a uniform (the GLSL name it maps to) and an optional default value applied automatically on import.
If all functions in your module share the same options, use the default key to avoid repetition:
Sandbox.defineModule("my_module", source, {
default: {
colors: {
uniform: "u_colors",
default: [
[1, 0, 0],
[0, 0, 1],
],
},
speed: { uniform: "u_speed", default: 1.0 },
},
specialFunc: {
speed: { uniform: "u_speed", default: 2.0 },
// "colors" is inherited from default
},
});Per-function options always take priority over default when both define the same key.
Hooks
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.
The callback receives a ClockState object with time, delta, frame, running, and fps (smoothed).
Pre-compute values on the CPU before they hit the shader:
sandbox.hook(({ time }) => {
const intensity = (Math.sin(time) + 1) / 2;
sandbox.setUniform("u_intensity", intensity);
}, "before");Sync state with reactive frameworks like Vue or React:
const playing = ref(false);
sandbox.hook(() => {
playing.value = sandbox.isPlaying();
}, "after");The hook returns a removal function, so you can clean up whenever you want:
const remove = sandbox.hook(({ time }) => {
console.log(time);
}, "after");
remove();Self-removing hooks
Sometimes you need a hook that runs only until a condition is met. Just return false and the hook removes itself:
sandbox.hook(({ time }) => {
if (time > 5) return false;
}, "after");This is how pauseAt() works internally — it's hooks all the way down.
Textures
Sandbox supports textures as sampler2D uniforms. Pass any image, canvas, or video element and Sandbox takes care of the WebGL plumbing — creating the texture, binding it to a texture unit, and setting the sampler uniform.
const img = new Image();
img.src = "photo.jpg";
img.onload = () => {
sandbox.setTexture("u_texture", img);
};Then sample it in your shader:
uniform sampler2D u_texture;
void main() {
vec4 color = texture2D(u_texture, v_texcoord);
gl_FragColor = color;
}Multiple textures work the same way — each gets its own texture unit automatically:
sandbox.setTexture("u_photo", photoImg);
sandbox.setTexture("u_mask", maskImg);You can also set textures upfront via options:
Sandbox.create(canvas, {
fragment: shader,
textures: {
u_photo: photoImg,
u_mask: { source: maskImg, wrap: "repeat" },
},
});Texture options
Each texture accepts optional configuration for wrapping, filtering, and orientation:
sandbox.setTexture("u_texture", img, {
wrap: "repeat", // both axes (default: "clamp")
minFilter: "nearest", // pixelated look (default: "linear")
flipY: false, // disable vertical flip (default: true)
});| Option | Values | Default |
| ----------- | ---------------------------------- | ---------- |
| wrap | "clamp", "repeat", "mirror" | "clamp" |
| wrapS | same (overrides wrap for S axis) | wrap |
| wrapT | same (overrides wrap for T axis) | wrap |
| minFilter | "nearest", "linear" | "linear" |
| magFilter | "nearest", "linear" | "linear" |
| flipY | boolean | true |
| dynamic | boolean | auto |
Dynamic textures
When you pass a video element, Sandbox automatically re-uploads pixels every frame so the texture stays in sync with playback. This also works for animated canvases — just set dynamic: true:
// Video — dynamic by default
sandbox.setTexture("u_video", videoElement);
// Animated canvas — opt in
sandbox.setTexture("u_canvas", canvasElement, { dynamic: true });To remove a texture and free its GPU memory:
sandbox.removeTexture("u_texture");Export
Sandbox can export the current frame as an image or blob — useful for saving screenshots, generating thumbnails, or uploading processed images to a server.
// Data URL (synchronous)
const url = sandbox.renderAt(1.5).exportAsURL("image/png");
// Blob (async) — perfect for server uploads
const blob = await sandbox.renderAt(1.5).exportAsBlob("image/jpeg", 0.9);
await fetch("/upload", { method: "POST", body: blob });
// HTMLImageElement
const img = sandbox.renderAt(1.5).exportAsImage("image/png");
document.body.appendChild(img);[!NOTE] Export methods work reliably after
render()orrenderAt(). If you need to capture frames during an active render loop, setpreserveDrawingBuffer: truein options.
Streaming
For real-time use cases like video calls or recording, Sandbox can expose the canvas as a MediaStream:
// WebRTC — send shader output to a video call
const stream = sandbox.stream(30);
peerConnection.addTrack(stream.getVideoTracks()[0], stream);
// Recording — save as video file
const recorder = new MediaRecorder(sandbox.stream(30));
recorder.start();This opens up workflows like webcam → texture → shader effect → video call with just a few lines of code.
Chaining
Every method returns this, so you can chain calls for clean, expressive code:
sandbox
.setUniforms({ u_color: [1, 0, 0] })
.time(2.5)
.render();Error handling
Shader errors happen — typos, syntax mistakes, driver quirks. Sandbox handles them gracefully and reports them via a single callback. No try/catch needed.
Sandbox.create(canvas, {
fragment: shader,
onError: (error) => {
console.error(error.message);
},
});The error object includes useful details:
error.code— error category (see table below)error.lines— line numbers where errors occurred (for shader compilation errors)error.shaderType— which shader failed (vertexorfragment)
| Code | When |
| ------------------ | ------------------------------------------------------------------------------------- |
| CONTEXT_ERROR | WebGL not supported or context creation failed |
| SHADER_ERROR | Shader compilation failed, version mismatch, import syntax error, or missing function |
| PROGRAM_ERROR | Shader program linking failed |
| VALIDATION_ERROR | Vertex/fragment shader version mismatch |
| MODULE_ERROR | Module not found, method not found, forbidden name, or duplicate definition |
| TEXTURE_ERROR | Texture creation failed or texture unit limit exceeded |
| UNKNOWN_ERROR | Unexpected error in callbacks (onLoad, hooks) |
Vue integration
Here's a complete example showing how to use Sandbox with Vue's reactivity system:
<script setup lang="ts">
import { shallowRef, ref, onMounted, onUnmounted } from "vue";
import { Sandbox } from "@rosalana/sandbox";
const canvasRef = ref<HTMLCanvasElement>();
const sandbox = shallowRef<Sandbox | null>(null);
const isPlaying = ref(false);
onMounted(() => {
sandbox.value = Sandbox.create(canvasRef.value!, {
onAfterRender: () => {
isPlaying.value = sandbox.value?.isPlaying() ?? false;
},
});
});
onUnmounted(() => {
sandbox.value?.destroy();
});
</script>
<template>
{{ isPlaying ? "Playing" : "Paused" }}
<canvas ref="canvasRef" />
</template>Use shallowRef for the Sandbox instance — you don't want Vue making the WebGL context reactive.
Cleanup
Always destroy when you're done. This releases all WebGL resources and removes event listeners:
sandbox.destroy();In frameworks like Vue:
onUnmounted(() => {
sandbox.destroy();
});Options
interface SandboxOptions {
vertex?: string;
fragment?: string;
autoplay?: boolean;
pauseWhenHidden?: boolean;
dpr?: number | "auto";
fps?: number;
preserveDrawingBuffer?: boolean;
antialias?: boolean;
onError?: (error: SandboxError) => void;
onLoad?: () => void;
onBeforeRender?: HookCallback | null;
onAfterRender?: HookCallback | null;
uniforms?: UniformSchema;
modules?: Record<string, Record<string, AnyUniformValue>>;
textures?: TextureSchema;
}| Option | Default | Description |
| ----------------------- | --------------- | ---------------------------------------------- |
| vertex | built-in | Custom vertex shader |
| fragment | built-in | Fragment shader |
| autoplay | true | Start rendering immediately |
| pauseWhenHidden | true | Pause when scrolled out of view |
| dpr | "auto" | Device pixel ratio |
| fps | 0 (unlimited) | Max frame rate (approximate due to rAF timing) |
| preserveDrawingBuffer | false | Keep buffer for screenshots |
| antialias | true | Enable antialiasing |
| onError | console.error | Error callback |
| onLoad | — | Called on each shader compilation |
| onBeforeRender | — | Hook before each frame |
| onAfterRender | — | Hook after each frame |
| uniforms | — | Initial uniform values |
| modules | — | Configure module options per imported function |
| textures | — | Initial textures to bind to sampler uniforms |
Limitations (by design)
- No multi‑pass rendering
- No 3D scene graph
- No custom geometry (fullscreen quad only)
If you need a full engine, reach for three.js. For clean shader‑only effects, Sandbox is a joy to use.
License
Rosalana Sandbox is open-source under the MIT license, allowing you to freely use, modify, and distribute it with minimal restrictions.
You may not be able to use our systems but you can use our code to build your own.
For details on how to contribute or how the Rosalana ecosystem is maintained, please refer to each repository's individual guidelines.
Questions or feedback?
Feel free to open an issue or contribute with a pull request. Happy coding with Rosalana!

