@rosalana/sandbox 0.0.4 → 0.0.5

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 CHANGED
@@ -146,7 +146,7 @@ Sandbox figures out which WebGL version you're using by looking at your shader c
146
146
  If WebGL2 isn't available, Sandbox falls back to WebGL1 automatically. You can always check what you're running:
147
147
 
148
148
  ```ts
149
- sandbox.webglVersion();
149
+ sandbox.version; // 1 or 2
150
150
  ```
151
151
 
152
152
  ## Uniforms
@@ -195,6 +195,8 @@ These uniforms are filled automatically every frame — no setup needed. Just de
195
195
 
196
196
  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.
197
197
 
198
+ The callback receives a `ClockState` object with `time`, `delta`, `frame`, `running`, and `fps` (smoothed).
199
+
198
200
  **Pre-compute values on the CPU** before they hit the shader:
199
201
 
200
202
  ```ts
@@ -327,6 +329,7 @@ interface SandboxOptions {
327
329
  autoplay?: boolean;
328
330
  pauseWhenHidden?: boolean;
329
331
  dpr?: number | "auto";
332
+ fps?: number;
330
333
  preserveDrawingBuffer?: boolean;
331
334
  antialias?: boolean;
332
335
  onError?: (error: SandboxError) => void;
@@ -337,20 +340,21 @@ interface SandboxOptions {
337
340
  }
338
341
  ```
339
342
 
340
- | Option | Default | Description |
341
- | ----------------------- | --------------- | ------------------------------- |
342
- | `vertex` | built-in | Custom vertex shader |
343
- | `fragment` | built-in | Fragment shader |
344
- | `autoplay` | `true` | Start rendering immediately |
345
- | `pauseWhenHidden` | `true` | Pause when scrolled out of view |
346
- | `dpr` | `"auto"` | Device pixel ratio |
347
- | `preserveDrawingBuffer` | `false` | Keep buffer for screenshots |
348
- | `antialias` | `true` | Enable antialiasing |
349
- | `onError` | `console.error` | Error callback |
350
- | `onLoad` | | Called when ready |
351
- | `onBeforeRender` | — | Hook before each frame |
352
- | `onAfterRender` | — | Hook after each frame |
353
- | `uniforms` | — | Initial uniform values |
343
+ | Option | Default | Description |
344
+ | ----------------------- | --------------- | ---------------------------------------------- |
345
+ | `vertex` | built-in | Custom vertex shader |
346
+ | `fragment` | built-in | Fragment shader |
347
+ | `autoplay` | `true` | Start rendering immediately |
348
+ | `pauseWhenHidden` | `true` | Pause when scrolled out of view |
349
+ | `dpr` | `"auto"` | Device pixel ratio |
350
+ | `fps` | `0` (unlimited) | Max frame rate (approximate due to rAF timing) |
351
+ | `preserveDrawingBuffer` | `false` | Keep buffer for screenshots |
352
+ | `antialias` | `true` | Enable antialiasing |
353
+ | `onError` | `console.error` | Error callback |
354
+ | `onLoad` | — | Called on each shader compilation |
355
+ | `onBeforeRender` | — | Hook before each frame |
356
+ | `onAfterRender` | — | Hook after each frame |
357
+ | `uniforms` | — | Initial uniform values |
354
358
 
355
359
  ## Limitations (by design)
356
360
 
package/dist/index.cjs.js CHANGED
@@ -1,8 +1,8 @@
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 f extends l{constructor(e,r,s){const n=f.parseErrorLines(s),a=n.length>0?` at line(s): ${n.join(", ")}`:"";super(`${e} shader compilation failed${a}
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 r=(o,t,e)=>k(o,typeof t!="symbol"?t+"":t,e);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class u{constructor(t,e,i,s){this.target=t,this.type=e,this.listener=i,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,i,s){return t.addEventListener(e,i,s),()=>t.removeEventListener(e,i,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 f extends l{constructor(e,i,s){const n=f.parseErrorLines(s),h=n.length>0?` at line(s): ${n.join(", ")}`:"";super(`${e} shader compilation failed${h}
2
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 c=parseInt(a[1],10);c>0&&s.add(c)}}return[...s].sort((n,a)=>n-a)}}class d extends l{constructor(t){super(`Shader program linking failed
3
+ ${s}`,"SHADER_COMPILATION_FAILED");r(this,"lines");this.shaderType=e,this.source=i,this.infoLog=s,this.name="SandboxShaderCompilationError",this.lines=n}static parseErrorLines(e){const i=[/ERROR:\s*\d*:(\d+)/g,/(\d+):(\d+)\(\d+\):/g,/^(\d+):/gm],s=new Set;for(const n of i){let h;for(;(h=n.exec(e))!==null;){const c=parseInt(h[1],10);c>0&&s.add(c)}}return[...s].sort((n,h)=>n-h)}}class d extends l{constructor(t){super(`Shader program linking failed
4
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.frame===0?this.startTime=e:this.startTime=e-this.time*1e3,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.reset(),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 f(t,e,"Failed to create shader object");if(r.shaderSource(n,e),r.compileShader(n),!r.getShaderParameter(n,r.COMPILE_STATUS)){const c=r.getShaderInfoLog(n)||"Unknown error";throw r.deleteShader(n),new f(t,e,c)}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
5
+ ${t}`,"PROGRAM_LINK_FAILED"),this.infoLog=t,this.name="SandboxProgramError"}}class R{constructor(){r(this,"time",0);r(this,"delta",0);r(this,"frame",0);r(this,"running",!1);r(this,"fps",0);r(this,"startTime",0);r(this,"lastTime",0);r(this,"rafId",null);r(this,"callback",null);r(this,"maxFps",0);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.frame===0?this.startTime=e:this.startTime=e-this.time*1e3,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.fps=0,this}getState(){return{time:this.time,delta:this.delta,frame:this.frame,running:this.running,fps:Math.round(this.fps)}}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.reset(),this.callback=null}setMaxFps(t){return this.maxFps=t,this}loop(t){if(!this.running)return;if(this.maxFps>0){const i=1e3/this.maxFps;if(t-this.lastTime<i){this.rafId=requestAnimationFrame(this.loop);return}}this.delta=(t-this.lastTime)/1e3,this.lastTime=t;const e=this.delta>0?1/this.delta:0;this.fps=this.fps*.95+e*.05,this.time=(t-this.startTime)/1e3,this.frame++,this.callback&&this.callback(this.getState()),this.rafId=requestAnimationFrame(this.loop)}}class p{constructor(t){r(this,"gl");r(this,"vao",null);r(this,"vbo",null);r(this,"ibo",null);r(this,"vertexCount",0);r(this,"indexCount",0);r(this,"useIndices",!1);r(this,"vaoExt",null);r(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),i=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(i,s),e}setup(t,e){const i=this.gl;return this.createVAO(),this.bindVAO(),this.vbo=i.createBuffer(),i.bindBuffer(i.ARRAY_BUFFER,this.vbo),i.bufferData(i.ARRAY_BUFFER,t,i.STATIC_DRAW),this.vertexCount=t.length/4,e&&(this.ibo=i.createBuffer(),i.bindBuffer(i.ELEMENT_ARRAY_BUFFER,this.ibo),i.bufferData(i.ELEMENT_ARRAY_BUFFER,e,i.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 i=4*Float32Array.BYTES_PER_ELEMENT,s=this.getPositionLocation(t);s>=0&&(e.enableVertexAttribArray(s),e.vertexAttribPointer(s,2,e.FLOAT,!1,i,0));const n=this.getTexcoordLocation(t);return n>=0&&(e.enableVertexAttribArray(n),e.vertexAttribPointer(n,2,e.FLOAT,!1,i,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 a{constructor(t){r(this,"gl");r(this,"program",null);r(this,"vertexShader",null);r(this,"fragmentShader",null);r(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 i=a.detectVersion(t),s=a.detectVersion(e);if(i!=s)throw new L(i,s);return this.version=Math.max(i,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 i=this.gl,s=t==="vertex"?i.VERTEX_SHADER:i.FRAGMENT_SHADER,n=i.createShader(s);if(!n)throw new f(t,e,"Failed to create shader object");if(i.shaderSource(n,e),i.compileShader(n),!i.getShaderParameter(n,i.COMPILE_STATUS)){const c=i.getShaderInfoLog(n)||"Unknown error";throw i.deleteShader(n),new f(t,e,c)}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){r(this,"name");r(this,"method");r(this,"isArray");r(this,"isMatrix");r(this,"location",null);r(this,"locationResolved",!1);r(this,"value");this.name=t,this.value=e;const i=x.inferMethodInfo(e);this.method=i.method,this.isArray=i.isArray,this.isMatrix=i.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,i=t[0];if(Array.isArray(i))switch(i.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 i=this.resolveLocation(t,e);if(i===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(i,n);break;case"uniform1i":t.uniform1i(i,n);break;case"uniform1fv":t.uniform1fv(i,n);break;case"uniform2fv":t.uniform2fv(i,n);break;case"uniform3fv":t.uniform3fv(i,n);break;case"uniform4fv":t.uniform4fv(i,n);break;case"uniformMatrix2fv":t.uniformMatrix2fv(i,!1,n);break;case"uniformMatrix3fv":t.uniformMatrix3fv(i,!1,n);break;case"uniformMatrix4fv":t.uniformMatrix4fv(i,!1,n);break}}}const m=class m{constructor(t){r(this,"gl");r(this,"program",null);r(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 i=this.uniforms.get(t);return i?i.setValue(e):this.uniforms.set(t,new x(t,e)),this}setMany(t){for(const[e,i]of Object.entries(t))this.set(e,i);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,i){if(this.set("u_resolution",e),this.set("u_time",t.time),this.set("u_delta",t.delta),this.set("u_mouse",i),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}};r(m,"BUILT_INS",new Set(["u_resolution","u_time","u_delta","u_mouse","u_frame"]));let v=m;class A{constructor(){r(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,i]of this.hooks)i(t)===!1&&this.remove(e)}destroy(){this.hooks.clear()}}class _{constructor(t,e){r(this,"canvas");r(this,"gl");r(this,"options");r(this,"onBeforeHooks",new A);r(this,"onAfterHooks",new A);r(this,"_program");r(this,"_geometry");r(this,"_uniforms");r(this,"_clock");r(this,"_resolution",[1,1]);r(this,"_mouse",[0,0]);r(this,"_version",1);r(this,"playing",!1);this.canvas=t,this.options=e,this.gl=this.initContext(),this.enableExtensions(),this._program=new a(this.gl),this._geometry=p.fullscreenQuad(this.gl),this._uniforms=new v(this.gl),this._clock=new R,this.options.fps&&this._clock.setMaxFps(this.options.fps),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 i=new _(t,e);return e.vertex&&e.fragment&&i.shader(e.vertex,e.fragment),e.uniforms&&i._uniforms.setMany(e.uniforms),i}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 i=this.canvas.getContext("webgl",t);if(i)return this._version=1,i;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,i,s){return this.canvas.width=i,this.canvas.height=s,this.gl.viewport(t,e,i,s),this._resolution=[i,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 i=this._program.getProgram();i&&this._uniforms.attachProgram(i),this.options.onLoad()}catch(i){i instanceof l&&this.options.onError(i)}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}getClock(){return this._clock}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
6
  precision mediump float;
7
7
  #endif
8
8
 
@@ -15,7 +15,7 @@ void main() {
15
15
  v_texcoord = a_texcoord;
16
16
  gl_Position = vec4(a_position, 0.0, 1.0);
17
17
  }
18
- `,y=`#ifdef GL_ES
18
+ `,E=`#ifdef GL_ES
19
19
  precision mediump float;
20
20
  #endif
21
21
 
@@ -29,7 +29,7 @@ void main() {
29
29
  vec3 color = vec3(uv.x, uv.y, 0.5 + 0.5 * sin(u_time));
30
30
  gl_FragColor = vec4(color, 1.0);
31
31
  }
32
- `,E=`#version 300 es
32
+ `,y=`#version 300 es
33
33
 
34
34
  in vec2 a_position;
35
35
  in vec2 a_texcoord;
@@ -39,7 +39,7 @@ out vec2 v_texcoord;
39
39
  void main() {
40
40
  v_texcoord = a_texcoord;
41
41
  gl_Position = vec4(a_position, 0.0, 1.0);
42
- }`,M=`#version 300 es
42
+ }`,V=`#version 300 es
43
43
  precision highp float;
44
44
 
45
45
  uniform vec2 u_resolution;
@@ -53,5 +53,5 @@ void main() {
53
53
  vec2 uv = gl_FragCoord.xy / u_resolution;
54
54
  vec3 color = vec3(uv.x, uv.y, 0.5 + 0.5 * sin(u_time));
55
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:y,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:y}if(t!=null&&t.fragment&&!(t!=null&&t.vertex)){const r=h.detectVersion(t.fragment);e.fragment=t.fragment,e.vertex=r===2?E:g}return{...e,...t}}setupListeners(){this.listeners.push(u.on(window,"resize",()=>{this.setViewport()}),u.on(this.canvas,"resize",()=>{this.setViewport()}),(()=>{let t=!1;return u.on(document,"scroll",e=>{this.options.pauseWhenHidden&&(this.isInViewport()?t&&!this.isPlaying()&&(this.play(),t=!1):this.isPlaying()&&(this.pause(),t=!0))})})(),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:E;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=f;exports.SandboxShaderVersionMismatchError=L;
56
+ }`;class b{constructor(t,e){r(this,"listeners",[]);r(this,"canvasEl");r(this,"options");r(this,"engine");r(this,"usingCustomVertex",!1);this.canvasEl=t,this.options=this.resolveOptions(e),this.engine=_.setup(this.canvasEl,this.options),this.setupListeners(),this.setViewport(),this.options.autoplay&&this.play()}static create(t,e){return new b(t,e)}resolveOptions(t){const e={vertex:g,fragment:E,autoplay:!0,pauseWhenHidden:!0,dpr:"auto",fps:0,preserveDrawingBuffer:!1,antialias:!0,onError:i=>{console.error("Oops!",i,`
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&&(this.usingCustomVertex=!0),t!=null&&t.vertex&&!(t!=null&&t.fragment)){const i=a.detectVersion(t.vertex);e.vertex=t.vertex,e.fragment=i===2?V:E}if(t!=null&&t.fragment&&!(t!=null&&t.vertex)){const i=a.detectVersion(t.fragment);e.fragment=t.fragment,e.vertex=i===2?y:g}return{...e,...t}}setupListeners(){this.listeners.push(u.on(window,"resize",()=>{this.setViewport()}),u.on(this.canvasEl,"resize",()=>{this.setViewport()}),(()=>{let t=!1;return u.on(document,"scroll",e=>{this.options.pauseWhenHidden&&(this.isInViewport()?t&&!this.isPlaying()&&(this.play(),t=!1):this.isPlaying()&&(this.pause(),t=!0))})})(),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.canvasEl.clientWidth||this.canvasEl.width||1,i=this.canvasEl.clientHeight||this.canvasEl.height||1;this.engine.viewport(0,0,Math.max(1,Math.floor(e*t)),Math.max(1,Math.floor(i*t)))}isInViewport(){const t=this.canvasEl.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 i=this.canvasEl.getBoundingClientRect();t>=i.left&&t<=i.right&&e>=i.top&&e<=i.bottom&&this.engine.mouse(t-i.left,e-i.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.options.vertex=t,this.options.fragment=e,this.usingCustomVertex=!0,this.engine.shader(this.options.vertex,this.options.fragment),this}setFragment(t){const e=a.detectVersion(t),i=a.detectVersion(this.options.vertex);return this.options.fragment=t,e!==i&&(this.usingCustomVertex||(this.options.vertex=e===2?y:g)),this.engine.shader(this.options.vertex,this.options.fragment),this}setFps(t){return this.engine.getClock().setMaxFps(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(i=>{i.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}get version(){return this.engine.getVersion()}get canvas(){return this.canvasEl}destroy(){this.destroyListeners(),this.engine.destroy()}}exports.Sandbox=b;exports.SandboxContextError=S;exports.SandboxError=l;exports.SandboxProgramError=d;exports.SandboxShaderCompilationError=f;exports.SandboxShaderVersionMismatchError=L;
package/dist/index.d.ts CHANGED
@@ -23,11 +23,13 @@ export declare class Sandbox {
23
23
  /** Active event listeners */
24
24
  private listeners;
25
25
  /** HTML canvas element */
26
- private canvas;
26
+ private canvasEl;
27
27
  /** Resolved options */
28
28
  private options;
29
29
  /** WebGL engine */
30
30
  private engine;
31
+ /** User sets custom vertex shader */
32
+ private usingCustomVertex;
31
33
  constructor(canvas: HTMLCanvasElement, options?: SandboxOptions);
32
34
  /**
33
35
  * Sandbox - A lightweight WebGL wrapper for shader effects.
@@ -93,6 +95,14 @@ export declare class Sandbox {
93
95
  * sandbox.setFragment(fragmentSource);
94
96
  */
95
97
  setFragment(fragment: string): this;
98
+ /**
99
+ * Set the max frame rate runtime
100
+ *
101
+ * @example
102
+ * sandbox.setFps(30); // Limit to 30 FPS
103
+ * sandbox.setFps(0); // Unlimited FPS
104
+ */
105
+ setFps(fps: number): this;
96
106
  /**
97
107
  * Add a runtime render hook.
98
108
  */
@@ -140,11 +150,11 @@ export declare class Sandbox {
140
150
  /**
141
151
  * Get WebGL version using (1 or 2).
142
152
  */
143
- webglVersion(): WebGLVersion;
153
+ get version(): WebGLVersion;
144
154
  /**
145
155
  * Get canvas element.
146
156
  */
147
- canvasElement(): HTMLCanvasElement;
157
+ get canvas(): HTMLCanvasElement;
148
158
  /**
149
159
  * Destroy sandbox and release all resources.
150
160
  * @example
package/dist/index.es.js CHANGED
@@ -1,9 +1,9 @@
1
1
  var S = Object.defineProperty;
2
2
  var L = (o, t, e) => t in o ? S(o, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : o[t] = e;
3
- var i = (o, t, e) => L(o, typeof t != "symbol" ? t + "" : t, e);
3
+ var r = (o, t, e) => L(o, typeof t != "symbol" ? t + "" : t, e);
4
4
  class l {
5
- constructor(t, e, r, s) {
6
- this.target = t, this.type = e, this.listener = r, this.options = s, this.target.addEventListener(
5
+ constructor(t, e, i, s) {
6
+ this.target = t, this.type = e, this.listener = i, this.options = s, this.target.addEventListener(
7
7
  this.type,
8
8
  this.listener,
9
9
  this.options
@@ -16,8 +16,8 @@ class l {
16
16
  this.options
17
17
  );
18
18
  }
19
- static on(t, e, r, s) {
20
- return t.addEventListener(e, r, s), () => t.removeEventListener(e, r, s);
19
+ static on(t, e, i, s) {
20
+ return t.addEventListener(e, i, s), () => t.removeEventListener(e, i, s);
21
21
  }
22
22
  }
23
23
  class u extends Error {
@@ -43,21 +43,21 @@ class k extends u {
43
43
  }
44
44
  }
45
45
  class c extends u {
46
- constructor(e, r, s) {
47
- const n = c.parseErrorLines(s), a = n.length > 0 ? ` at line(s): ${n.join(", ")}` : "";
46
+ constructor(e, i, s) {
47
+ const n = c.parseErrorLines(s), h = n.length > 0 ? ` at line(s): ${n.join(", ")}` : "";
48
48
  super(
49
- `${e} shader compilation failed${a}
49
+ `${e} shader compilation failed${h}
50
50
 
51
51
  ${s}`,
52
52
  "SHADER_COMPILATION_FAILED"
53
53
  );
54
54
  /** Line numbers where errors occurred */
55
- i(this, "lines");
56
- this.shaderType = e, this.source = r, this.infoLog = s, this.name = "SandboxShaderCompilationError", this.lines = n;
55
+ r(this, "lines");
56
+ this.shaderType = e, this.source = i, this.infoLog = s, this.name = "SandboxShaderCompilationError", this.lines = n;
57
57
  }
58
58
  /** Parse error log to extract line numbers */
59
59
  static parseErrorLines(e) {
60
- const r = [
60
+ const i = [
61
61
  /ERROR:\s*\d*:(\d+)/g,
62
62
  // Chrome/ANGLE: ERROR: 0:15
63
63
  /(\d+):(\d+)\(\d+\):/g,
@@ -65,14 +65,14 @@ ${s}`,
65
65
  /^(\d+):/gm
66
66
  // Simple: 15:
67
67
  ], s = /* @__PURE__ */ new Set();
68
- for (const n of r) {
69
- let a;
70
- for (; (a = n.exec(e)) !== null; ) {
71
- const f = parseInt(a[1], 10);
68
+ for (const n of i) {
69
+ let h;
70
+ for (; (h = n.exec(e)) !== null; ) {
71
+ const f = parseInt(h[1], 10);
72
72
  f > 0 && s.add(f);
73
73
  }
74
74
  }
75
- return [...s].sort((n, a) => n - a);
75
+ return [...s].sort((n, h) => n - h);
76
76
  }
77
77
  }
78
78
  class m extends u {
@@ -85,17 +85,20 @@ ${t}`, "PROGRAM_LINK_FAILED"), this.infoLog = t, this.name = "SandboxProgramErro
85
85
  class R {
86
86
  constructor() {
87
87
  /** Total elapsed time in seconds */
88
- i(this, "time", 0);
88
+ r(this, "time", 0);
89
89
  /** Delta time since last frame in seconds */
90
- i(this, "delta", 0);
90
+ r(this, "delta", 0);
91
91
  /** Frame counter */
92
- i(this, "frame", 0);
92
+ r(this, "frame", 0);
93
93
  /** Is clock running */
94
- i(this, "running", !1);
95
- i(this, "startTime", 0);
96
- i(this, "lastTime", 0);
97
- i(this, "rafId", null);
98
- i(this, "callback", null);
94
+ r(this, "running", !1);
95
+ /** Smoothed frames per second */
96
+ r(this, "fps", 0);
97
+ r(this, "startTime", 0);
98
+ r(this, "lastTime", 0);
99
+ r(this, "rafId", null);
100
+ r(this, "callback", null);
101
+ r(this, "maxFps", 0);
99
102
  this.loop = this.loop.bind(this);
100
103
  }
101
104
  /**
@@ -118,7 +121,7 @@ class R {
118
121
  * Reset clock to initial state.
119
122
  */
120
123
  reset() {
121
- return this.stop(), this.time = 0, this.delta = 0, this.frame = 0, this;
124
+ return this.stop(), this.time = 0, this.delta = 0, this.frame = 0, this.fps = 0, this;
122
125
  }
123
126
  /**
124
127
  * Get current clock state snapshot.
@@ -127,7 +130,9 @@ class R {
127
130
  return {
128
131
  time: this.time,
129
132
  delta: this.delta,
130
- frame: this.frame
133
+ frame: this.frame,
134
+ running: this.running,
135
+ fps: Math.round(this.fps)
131
136
  };
132
137
  }
133
138
  /**
@@ -149,25 +154,41 @@ class R {
149
154
  destroy() {
150
155
  this.reset(), this.callback = null;
151
156
  }
157
+ /**
158
+ * Set maximum frames per second.
159
+ */
160
+ setMaxFps(t) {
161
+ return this.maxFps = t, this;
162
+ }
152
163
  /**
153
164
  * Internal animation frame handler.
154
165
  */
155
166
  loop(t) {
156
- 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));
167
+ if (!this.running) return;
168
+ if (this.maxFps > 0) {
169
+ const i = 1e3 / this.maxFps;
170
+ if (t - this.lastTime < i) {
171
+ this.rafId = requestAnimationFrame(this.loop);
172
+ return;
173
+ }
174
+ }
175
+ this.delta = (t - this.lastTime) / 1e3, this.lastTime = t;
176
+ const e = this.delta > 0 ? 1 / this.delta : 0;
177
+ this.fps = this.fps * 0.95 + e * 0.05, this.time = (t - this.startTime) / 1e3, this.frame++, this.callback && this.callback(this.getState()), this.rafId = requestAnimationFrame(this.loop);
157
178
  }
158
179
  }
159
180
  class p {
160
181
  constructor(t) {
161
- i(this, "gl");
162
- i(this, "vao", null);
163
- i(this, "vbo", null);
164
- i(this, "ibo", null);
165
- i(this, "vertexCount", 0);
166
- i(this, "indexCount", 0);
167
- i(this, "useIndices", !1);
182
+ r(this, "gl");
183
+ r(this, "vao", null);
184
+ r(this, "vbo", null);
185
+ r(this, "ibo", null);
186
+ r(this, "vertexCount", 0);
187
+ r(this, "indexCount", 0);
188
+ r(this, "useIndices", !1);
168
189
  // WebGL1 VAO extension (if available)
169
- i(this, "vaoExt", null);
170
- i(this, "isWebGL2");
190
+ r(this, "vaoExt", null);
191
+ r(this, "isWebGL2");
171
192
  this.gl = t, this.isWebGL2 = t instanceof WebGL2RenderingContext, this.isWebGL2 || (this.vaoExt = t.getExtension("OES_vertex_array_object"));
172
193
  }
173
194
  /**
@@ -175,7 +196,7 @@ class p {
175
196
  * This is the most common use case for shader effects.
176
197
  */
177
198
  static fullscreenQuad(t) {
178
- const e = new p(t), r = new Float32Array([
199
+ const e = new p(t), i = new Float32Array([
179
200
  // position texcoord
180
201
  -1,
181
202
  -1,
@@ -207,14 +228,14 @@ class p {
207
228
  3
208
229
  // second triangle
209
230
  ]);
210
- return e.setup(r, s), e;
231
+ return e.setup(i, s), e;
211
232
  }
212
233
  /**
213
234
  * Setup geometry from vertex and index data.
214
235
  */
215
236
  setup(t, e) {
216
- const r = this.gl;
217
- 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;
237
+ const i = this.gl;
238
+ return this.createVAO(), this.bindVAO(), this.vbo = i.createBuffer(), i.bindBuffer(i.ARRAY_BUFFER, this.vbo), i.bufferData(i.ARRAY_BUFFER, t, i.STATIC_DRAW), this.vertexCount = t.length / 4, e && (this.ibo = i.createBuffer(), i.bindBuffer(i.ELEMENT_ARRAY_BUFFER, this.ibo), i.bufferData(i.ELEMENT_ARRAY_BUFFER, e, i.STATIC_DRAW), this.indexCount = e.length, this.useIndices = !0), this.unbindVAO(), this;
218
239
  }
219
240
  /**
220
241
  * Link vertex attributes to shader program.
@@ -223,15 +244,15 @@ class p {
223
244
  linkAttributes(t) {
224
245
  const e = this.gl;
225
246
  this.bindVAO(), e.bindBuffer(e.ARRAY_BUFFER, this.vbo);
226
- const r = 4 * Float32Array.BYTES_PER_ELEMENT, s = this.getPositionLocation(t);
227
- s >= 0 && (e.enableVertexAttribArray(s), e.vertexAttribPointer(s, 2, e.FLOAT, !1, r, 0));
247
+ const i = 4 * Float32Array.BYTES_PER_ELEMENT, s = this.getPositionLocation(t);
248
+ s >= 0 && (e.enableVertexAttribArray(s), e.vertexAttribPointer(s, 2, e.FLOAT, !1, i, 0));
228
249
  const n = this.getTexcoordLocation(t);
229
250
  return n >= 0 && (e.enableVertexAttribArray(n), e.vertexAttribPointer(
230
251
  n,
231
252
  2,
232
253
  e.FLOAT,
233
254
  !1,
234
- r,
255
+ i,
235
256
  2 * Float32Array.BYTES_PER_ELEMENT
236
257
  )), this.useIndices && e.bindBuffer(e.ELEMENT_ARRAY_BUFFER, this.ibo), this.unbindVAO(), this;
237
258
  }
@@ -293,13 +314,13 @@ class p {
293
314
  this.vao && (this.isWebGL2 ? this.gl.deleteVertexArray(this.vao) : this.vaoExt && this.vaoExt.deleteVertexArrayOES(this.vao), this.vao = null);
294
315
  }
295
316
  }
296
- class h {
317
+ class a {
297
318
  constructor(t) {
298
- i(this, "gl");
299
- i(this, "program", null);
300
- i(this, "vertexShader", null);
301
- i(this, "fragmentShader", null);
302
- i(this, "version", 1);
319
+ r(this, "gl");
320
+ r(this, "program", null);
321
+ r(this, "vertexShader", null);
322
+ r(this, "fragmentShader", null);
323
+ r(this, "version", 1);
303
324
  this.gl = t;
304
325
  }
305
326
  /**
@@ -316,10 +337,10 @@ class h {
316
337
  */
317
338
  compile(t, e) {
318
339
  this.destroy();
319
- const r = h.detectVersion(t), s = h.detectVersion(e);
320
- if (r != s)
321
- throw new k(r, s);
322
- return this.version = Math.max(r, s), this.vertexShader = this.compileShader("vertex", t), this.fragmentShader = this.compileShader("fragment", e), this.linkProgram(), this;
340
+ const i = a.detectVersion(t), s = a.detectVersion(e);
341
+ if (i != s)
342
+ throw new k(i, s);
343
+ return this.version = Math.max(i, s), this.vertexShader = this.compileShader("vertex", t), this.fragmentShader = this.compileShader("fragment", e), this.linkProgram(), this;
323
344
  }
324
345
  /**
325
346
  * Bind this program for rendering.
@@ -363,16 +384,16 @@ class h {
363
384
  * @throws ShaderCompilationError if compilation fails
364
385
  */
365
386
  compileShader(t, e) {
366
- const r = this.gl, s = t === "vertex" ? r.VERTEX_SHADER : r.FRAGMENT_SHADER, n = r.createShader(s);
387
+ const i = this.gl, s = t === "vertex" ? i.VERTEX_SHADER : i.FRAGMENT_SHADER, n = i.createShader(s);
367
388
  if (!n)
368
389
  throw new c(
369
390
  t,
370
391
  e,
371
392
  "Failed to create shader object"
372
393
  );
373
- if (r.shaderSource(n, e), r.compileShader(n), !r.getShaderParameter(n, r.COMPILE_STATUS)) {
374
- const f = r.getShaderInfoLog(n) || "Unknown error";
375
- throw r.deleteShader(n), new c(t, e, f);
394
+ if (i.shaderSource(n, e), i.compileShader(n), !i.getShaderParameter(n, i.COMPILE_STATUS)) {
395
+ const f = i.getShaderInfoLog(n) || "Unknown error";
396
+ throw i.deleteShader(n), new c(t, e, f);
376
397
  }
377
398
  return n;
378
399
  }
@@ -396,16 +417,16 @@ class h {
396
417
  }
397
418
  class x {
398
419
  constructor(t, e) {
399
- i(this, "name");
400
- i(this, "method");
401
- i(this, "isArray");
402
- i(this, "isMatrix");
403
- i(this, "location", null);
404
- i(this, "locationResolved", !1);
405
- i(this, "value");
420
+ r(this, "name");
421
+ r(this, "method");
422
+ r(this, "isArray");
423
+ r(this, "isMatrix");
424
+ r(this, "location", null);
425
+ r(this, "locationResolved", !1);
426
+ r(this, "value");
406
427
  this.name = t, this.value = e;
407
- const r = x.inferMethodInfo(e);
408
- this.method = r.method, this.isArray = r.isArray, this.isMatrix = r.isMatrix;
428
+ const i = x.inferMethodInfo(e);
429
+ this.method = i.method, this.isArray = i.isArray, this.isMatrix = i.isMatrix;
409
430
  }
410
431
  /**
411
432
  * Infer WebGL method and metadata from value type.
@@ -417,9 +438,9 @@ class x {
417
438
  return { method: "uniform1f", isArray: !1, isMatrix: !1 };
418
439
  if (!Array.isArray(t))
419
440
  return { method: "uniform1f", isArray: !1, isMatrix: !1 };
420
- const e = t.length, r = t[0];
421
- if (Array.isArray(r))
422
- switch (r.length) {
441
+ const e = t.length, i = t[0];
442
+ if (Array.isArray(i))
443
+ switch (i.length) {
423
444
  case 2:
424
445
  return { method: "uniform2fv", isArray: !0, isMatrix: !1 };
425
446
  case 3:
@@ -475,8 +496,8 @@ class x {
475
496
  * @param program - Current WebGL program (for location resolution)
476
497
  */
477
498
  upload(t, e) {
478
- const r = this.resolveLocation(t, e);
479
- if (r === null)
499
+ const i = this.resolveLocation(t, e);
500
+ if (i === null)
480
501
  return;
481
502
  const s = this.value;
482
503
  let n;
@@ -484,40 +505,40 @@ class x {
484
505
  s.flat()
485
506
  ) : n = new Float32Array(s), this.method) {
486
507
  case "uniform1f":
487
- t.uniform1f(r, n);
508
+ t.uniform1f(i, n);
488
509
  break;
489
510
  case "uniform1i":
490
- t.uniform1i(r, n);
511
+ t.uniform1i(i, n);
491
512
  break;
492
513
  case "uniform1fv":
493
- t.uniform1fv(r, n);
514
+ t.uniform1fv(i, n);
494
515
  break;
495
516
  case "uniform2fv":
496
- t.uniform2fv(r, n);
517
+ t.uniform2fv(i, n);
497
518
  break;
498
519
  case "uniform3fv":
499
- t.uniform3fv(r, n);
520
+ t.uniform3fv(i, n);
500
521
  break;
501
522
  case "uniform4fv":
502
- t.uniform4fv(r, n);
523
+ t.uniform4fv(i, n);
503
524
  break;
504
525
  case "uniformMatrix2fv":
505
- t.uniformMatrix2fv(r, !1, n);
526
+ t.uniformMatrix2fv(i, !1, n);
506
527
  break;
507
528
  case "uniformMatrix3fv":
508
- t.uniformMatrix3fv(r, !1, n);
529
+ t.uniformMatrix3fv(i, !1, n);
509
530
  break;
510
531
  case "uniformMatrix4fv":
511
- t.uniformMatrix4fv(r, !1, n);
532
+ t.uniformMatrix4fv(i, !1, n);
512
533
  break;
513
534
  }
514
535
  }
515
536
  }
516
537
  const d = class d {
517
538
  constructor(t) {
518
- i(this, "gl");
519
- i(this, "program", null);
520
- i(this, "uniforms", /* @__PURE__ */ new Map());
539
+ r(this, "gl");
540
+ r(this, "program", null);
541
+ r(this, "uniforms", /* @__PURE__ */ new Map());
521
542
  this.gl = t;
522
543
  }
523
544
  /**
@@ -535,15 +556,15 @@ const d = class d {
535
556
  * Creates the uniform if it doesn't exist, updates if it does.
536
557
  */
537
558
  set(t, e) {
538
- const r = this.uniforms.get(t);
539
- return r ? r.setValue(e) : this.uniforms.set(t, new x(t, e)), this;
559
+ const i = this.uniforms.get(t);
560
+ return i ? i.setValue(e) : this.uniforms.set(t, new x(t, e)), this;
540
561
  }
541
562
  /**
542
563
  * Set multiple uniforms at once.
543
564
  */
544
565
  setMany(t) {
545
- for (const [e, r] of Object.entries(t))
546
- this.set(e, r);
566
+ for (const [e, i] of Object.entries(t))
567
+ this.set(e, i);
547
568
  return this;
548
569
  }
549
570
  /**
@@ -580,8 +601,8 @@ const d = class d {
580
601
  * Upload only built-in uniforms (u_resolution, u_time, u_delta, u_mouse, u_frame).
581
602
  * Call this every frame with current values.
582
603
  */
583
- uploadBuiltIns(t, e, r) {
584
- 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)
604
+ uploadBuiltIns(t, e, i) {
605
+ if (this.set("u_resolution", e), this.set("u_time", t.time), this.set("u_delta", t.delta), this.set("u_mouse", i), this.set("u_frame", t.frame), !this.program)
585
606
  return this;
586
607
  for (const s of d.BUILT_INS) {
587
608
  const n = this.uniforms.get(s);
@@ -615,7 +636,7 @@ const d = class d {
615
636
  }
616
637
  };
617
638
  /** Built-in uniform names that are handled automatically */
618
- i(d, "BUILT_INS", /* @__PURE__ */ new Set([
639
+ r(d, "BUILT_INS", /* @__PURE__ */ new Set([
619
640
  "u_resolution",
620
641
  "u_time",
621
642
  "u_delta",
@@ -625,7 +646,7 @@ i(d, "BUILT_INS", /* @__PURE__ */ new Set([
625
646
  let v = d;
626
647
  class b {
627
648
  constructor() {
628
- i(this, "hooks", /* @__PURE__ */ new Map());
649
+ r(this, "hooks", /* @__PURE__ */ new Map());
629
650
  }
630
651
  id() {
631
652
  return Math.random().toString(36).substring(2, 10);
@@ -641,8 +662,8 @@ class b {
641
662
  }
642
663
  /** Run all hooks with the given state */
643
664
  run(t) {
644
- for (const [e, r] of this.hooks)
645
- r(t) === !1 && this.remove(e);
665
+ for (const [e, i] of this.hooks)
666
+ i(t) === !1 && this.remove(e);
646
667
  }
647
668
  destroy() {
648
669
  this.hooks.clear();
@@ -650,27 +671,27 @@ class b {
650
671
  }
651
672
  class _ {
652
673
  constructor(t, e) {
653
- i(this, "canvas");
654
- i(this, "gl");
655
- i(this, "options");
656
- i(this, "onBeforeHooks", new b());
657
- i(this, "onAfterHooks", new b());
658
- i(this, "_program");
659
- i(this, "_geometry");
660
- i(this, "_uniforms");
661
- i(this, "_clock");
662
- i(this, "_resolution", [1, 1]);
663
- i(this, "_mouse", [0, 0]);
664
- i(this, "_version", 1);
665
- i(this, "playing", !1);
666
- 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);
674
+ r(this, "canvas");
675
+ r(this, "gl");
676
+ r(this, "options");
677
+ r(this, "onBeforeHooks", new b());
678
+ r(this, "onAfterHooks", new b());
679
+ r(this, "_program");
680
+ r(this, "_geometry");
681
+ r(this, "_uniforms");
682
+ r(this, "_clock");
683
+ r(this, "_resolution", [1, 1]);
684
+ r(this, "_mouse", [0, 0]);
685
+ r(this, "_version", 1);
686
+ r(this, "playing", !1);
687
+ this.canvas = t, this.options = e, this.gl = this.initContext(), this.enableExtensions(), this._program = new a(this.gl), this._geometry = p.fullscreenQuad(this.gl), this._uniforms = new v(this.gl), this._clock = new R(), this.options.fps && this._clock.setMaxFps(this.options.fps), this.options.onBeforeRender && this.onBeforeHooks.add(this.options.onBeforeRender), this.options.onAfterRender && this.onAfterHooks.add(this.options.onAfterRender), this.onRender = this.onRender.bind(this);
667
688
  }
668
689
  /**
669
690
  * Factory method to create and setup WebGL instance.
670
691
  */
671
692
  static setup(t, e) {
672
- const r = new _(t, e);
673
- return e.vertex && e.fragment && r.shader(e.vertex, e.fragment), e.uniforms && r._uniforms.setMany(e.uniforms), r;
693
+ const i = new _(t, e);
694
+ return e.vertex && e.fragment && i.shader(e.vertex, e.fragment), e.uniforms && i._uniforms.setMany(e.uniforms), i;
674
695
  }
675
696
  /**
676
697
  * Initialize WebGL context.
@@ -687,9 +708,9 @@ class _ {
687
708
  }, e = this.canvas.getContext("webgl2", t);
688
709
  if (e)
689
710
  return this._version = 2, e;
690
- const r = this.canvas.getContext("webgl", t);
691
- if (r)
692
- return this._version = 1, r;
711
+ const i = this.canvas.getContext("webgl", t);
712
+ if (i)
713
+ return this._version = 1, i;
693
714
  const s = new w("not_supported");
694
715
  throw this.options.onError(s), s;
695
716
  }
@@ -702,8 +723,8 @@ class _ {
702
723
  /**
703
724
  * Set viewport dimensions.
704
725
  */
705
- viewport(t, e, r, s) {
706
- return this.canvas.width = r, this.canvas.height = s, this.gl.viewport(t, e, r, s), this._resolution = [r, s], this;
726
+ viewport(t, e, i, s) {
727
+ return this.canvas.width = i, this.canvas.height = s, this.gl.viewport(t, e, i, s), this._resolution = [i, s], this;
707
728
  }
708
729
  /**
709
730
  * Set the clock time
@@ -742,10 +763,10 @@ class _ {
742
763
  shader(t, e) {
743
764
  try {
744
765
  this._program.compile(t, e), this._version = this._program.getVersion(), this._geometry.linkAttributes(this._program);
745
- const r = this._program.getProgram();
746
- r && this._uniforms.attachProgram(r);
747
- } catch (r) {
748
- r instanceof u && this.options.onError(r);
766
+ const i = this._program.getProgram();
767
+ i && this._uniforms.attachProgram(i), this.options.onLoad();
768
+ } catch (i) {
769
+ i instanceof u && this.options.onError(i);
749
770
  }
750
771
  return this;
751
772
  }
@@ -781,6 +802,12 @@ class _ {
781
802
  getVersion() {
782
803
  return this._version;
783
804
  }
805
+ /**
806
+ * Get the clock instance
807
+ */
808
+ getClock() {
809
+ return this._clock;
810
+ }
784
811
  /**
785
812
  * Cleanup all resources.
786
813
  */
@@ -822,7 +849,7 @@ void main() {
822
849
  vec3 color = vec3(uv.x, uv.y, 0.5 + 0.5 * sin(u_time));
823
850
  gl_FragColor = vec4(color, 1.0);
824
851
  }
825
- `, y = `#version 300 es
852
+ `, E = `#version 300 es
826
853
 
827
854
  in vec2 a_position;
828
855
  in vec2 a_texcoord;
@@ -832,7 +859,7 @@ out vec2 v_texcoord;
832
859
  void main() {
833
860
  v_texcoord = a_texcoord;
834
861
  gl_Position = vec4(a_position, 0.0, 1.0);
835
- }`, M = `#version 300 es
862
+ }`, V = `#version 300 es
836
863
  precision highp float;
837
864
 
838
865
  uniform vec2 u_resolution;
@@ -847,17 +874,19 @@ void main() {
847
874
  vec3 color = vec3(uv.x, uv.y, 0.5 + 0.5 * sin(u_time));
848
875
  fragColor = vec4(color, 1.0);
849
876
  }`;
850
- class E {
877
+ class y {
851
878
  constructor(t, e) {
852
879
  /** Active event listeners */
853
- i(this, "listeners", []);
880
+ r(this, "listeners", []);
854
881
  /** HTML canvas element */
855
- i(this, "canvas");
882
+ r(this, "canvasEl");
856
883
  /** Resolved options */
857
- i(this, "options");
884
+ r(this, "options");
858
885
  /** WebGL engine */
859
- i(this, "engine");
860
- this.canvas = t, this.options = this.resolveOptions(e), this.engine = _.setup(this.canvas, this.options), this.setupListeners(), this.setViewport(), this.options.onLoad(), this.options.autoplay && this.play();
886
+ r(this, "engine");
887
+ /** User sets custom vertex shader */
888
+ r(this, "usingCustomVertex", !1);
889
+ this.canvasEl = t, this.options = this.resolveOptions(e), this.engine = _.setup(this.canvasEl, this.options), this.setupListeners(), this.setViewport(), this.options.autoplay && this.play();
861
890
  }
862
891
  /**
863
892
  * Sandbox - A lightweight WebGL wrapper for shader effects.
@@ -878,7 +907,7 @@ class E {
878
907
  * });
879
908
  */
880
909
  static create(t, e) {
881
- return new E(t, e);
910
+ return new y(t, e);
882
911
  }
883
912
  resolveOptions(t) {
884
913
  const e = {
@@ -887,12 +916,13 @@ class E {
887
916
  autoplay: !0,
888
917
  pauseWhenHidden: !0,
889
918
  dpr: "auto",
919
+ fps: 0,
890
920
  preserveDrawingBuffer: !1,
891
921
  antialias: !0,
892
- onError: (r) => {
922
+ onError: (i) => {
893
923
  console.error(
894
924
  "Oops!",
895
- r,
925
+ i,
896
926
  `
897
927
  You can handle errors programmatically by providing an onError callback to suppress this log and implement custom fallback behavior.`
898
928
  );
@@ -903,13 +933,13 @@ You can handle errors programmatically by providing an onError callback to suppr
903
933
  onAfterRender: null,
904
934
  uniforms: {}
905
935
  };
906
- if (t != null && t.vertex && !(t != null && t.fragment)) {
907
- const r = h.detectVersion(t.vertex);
908
- e.vertex = t.vertex, e.fragment = r === 2 ? M : A;
936
+ if (t != null && t.vertex && (this.usingCustomVertex = !0), t != null && t.vertex && !(t != null && t.fragment)) {
937
+ const i = a.detectVersion(t.vertex);
938
+ e.vertex = t.vertex, e.fragment = i === 2 ? V : A;
909
939
  }
910
940
  if (t != null && t.fragment && !(t != null && t.vertex)) {
911
- const r = h.detectVersion(t.fragment);
912
- e.fragment = t.fragment, e.vertex = r === 2 ? y : g;
941
+ const i = a.detectVersion(t.fragment);
942
+ e.fragment = t.fragment, e.vertex = i === 2 ? E : g;
913
943
  }
914
944
  return { ...e, ...t };
915
945
  }
@@ -920,7 +950,7 @@ You can handle errors programmatically by providing an onError callback to suppr
920
950
  this.setViewport();
921
951
  }),
922
952
  // Canvas resize
923
- l.on(this.canvas, "resize", () => {
953
+ l.on(this.canvasEl, "resize", () => {
924
954
  this.setViewport();
925
955
  }),
926
956
  // Visibility check on scroll
@@ -944,21 +974,21 @@ You can handle errors programmatically by providing an onError callback to suppr
944
974
  this.listeners.forEach((t) => t()), this.listeners = [];
945
975
  }
946
976
  setViewport() {
947
- 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;
977
+ const t = this.options.dpr === "auto" ? Math.min(2, window.devicePixelRatio || 1) : this.options.dpr, e = this.canvasEl.clientWidth || this.canvasEl.width || 1, i = this.canvasEl.clientHeight || this.canvasEl.height || 1;
948
978
  this.engine.viewport(
949
979
  0,
950
980
  0,
951
981
  Math.max(1, Math.floor(e * t)),
952
- Math.max(1, Math.floor(r * t))
982
+ Math.max(1, Math.floor(i * t))
953
983
  );
954
984
  }
955
985
  isInViewport() {
956
- const t = this.canvas.getBoundingClientRect();
986
+ const t = this.canvasEl.getBoundingClientRect();
957
987
  return t.bottom >= 0 && t.right >= 0 && t.top <= (window.innerHeight || document.documentElement.clientHeight) && t.left <= (window.innerWidth || document.documentElement.clientWidth);
958
988
  }
959
989
  setMouse(t, e) {
960
- const r = this.canvas.getBoundingClientRect();
961
- t >= r.left && t <= r.right && e >= r.top && e <= r.bottom && this.engine.mouse(t - r.left, e - r.top);
990
+ const i = this.canvasEl.getBoundingClientRect();
991
+ t >= i.left && t <= i.right && e >= i.top && e <= i.bottom && this.engine.mouse(t - i.left, e - i.top);
962
992
  }
963
993
  /**
964
994
  * Set a single uniform value with type checking.
@@ -999,7 +1029,7 @@ You can handle errors programmatically by providing an onError callback to suppr
999
1029
  * sandbox.setShader(vertexSource, fragmentSource);
1000
1030
  */
1001
1031
  setShader(t, e) {
1002
- return this.engine.shader(t, e), this;
1032
+ return this.options.vertex = t, this.options.fragment = e, this.usingCustomVertex = !0, this.engine.shader(this.options.vertex, this.options.fragment), this;
1003
1033
  }
1004
1034
  /**
1005
1035
  * Update only fragment shader (uses default vertex).
@@ -1007,8 +1037,18 @@ You can handle errors programmatically by providing an onError callback to suppr
1007
1037
  * sandbox.setFragment(fragmentSource);
1008
1038
  */
1009
1039
  setFragment(t) {
1010
- const r = this.webglVersion() === 1 ? g : y;
1011
- return this.engine.shader(r, t), this;
1040
+ const e = a.detectVersion(t), i = a.detectVersion(this.options.vertex);
1041
+ return this.options.fragment = t, e !== i && (this.usingCustomVertex || (this.options.vertex = e === 2 ? E : g)), this.engine.shader(this.options.vertex, this.options.fragment), this;
1042
+ }
1043
+ /**
1044
+ * Set the max frame rate runtime
1045
+ *
1046
+ * @example
1047
+ * sandbox.setFps(30); // Limit to 30 FPS
1048
+ * sandbox.setFps(0); // Unlimited FPS
1049
+ */
1050
+ setFps(t) {
1051
+ return this.engine.getClock().setMaxFps(t), this;
1012
1052
  }
1013
1053
  /**
1014
1054
  * Add a runtime render hook.
@@ -1038,8 +1078,8 @@ You can handle errors programmatically by providing an onError callback to suppr
1038
1078
  * Pause animation loop at specific time (in seconds).
1039
1079
  */
1040
1080
  pauseAt(t) {
1041
- const e = this.hook((r) => {
1042
- r.time >= t && (e(), this.pause());
1081
+ const e = this.hook((i) => {
1082
+ i.time >= t && (e(), this.pause());
1043
1083
  }, "after");
1044
1084
  return this;
1045
1085
  }
@@ -1080,14 +1120,14 @@ You can handle errors programmatically by providing an onError callback to suppr
1080
1120
  /**
1081
1121
  * Get WebGL version using (1 or 2).
1082
1122
  */
1083
- webglVersion() {
1123
+ get version() {
1084
1124
  return this.engine.getVersion();
1085
1125
  }
1086
1126
  /**
1087
1127
  * Get canvas element.
1088
1128
  */
1089
- canvasElement() {
1090
- return this.canvas;
1129
+ get canvas() {
1130
+ return this.canvasEl;
1091
1131
  }
1092
1132
  /**
1093
1133
  * Destroy sandbox and release all resources.
@@ -1101,7 +1141,7 @@ You can handle errors programmatically by providing an onError callback to suppr
1101
1141
  }
1102
1142
  }
1103
1143
  export {
1104
- E as Sandbox,
1144
+ y as Sandbox,
1105
1145
  w as SandboxContextError,
1106
1146
  u as SandboxError,
1107
1147
  m as SandboxProgramError,
@@ -12,10 +12,13 @@ export default class Clock {
12
12
  frame: number;
13
13
  /** Is clock running */
14
14
  running: boolean;
15
+ /** Smoothed frames per second */
16
+ fps: number;
15
17
  private startTime;
16
18
  private lastTime;
17
19
  private rafId;
18
20
  private callback;
21
+ private maxFps;
19
22
  constructor();
20
23
  /**
21
24
  * Start the animation loop with a render callback.
@@ -47,6 +50,10 @@ export default class Clock {
47
50
  * Cleanup.
48
51
  */
49
52
  destroy(): void;
53
+ /**
54
+ * Set maximum frames per second.
55
+ */
56
+ setMaxFps(fps: number): this;
50
57
  /**
51
58
  * Internal animation frame handler.
52
59
  */
@@ -1,4 +1,5 @@
1
1
  import type { AnyUniformValue, ResolvedSandboxOptions, UniformSchema, WebGLContext, WebGLVersion } from "../types";
2
+ import Clock from "./clock";
2
3
  import Hooks from "./hooks";
3
4
  /**
4
5
  * Main WebGL orchestrator.
@@ -82,6 +83,10 @@ export default class WebGL {
82
83
  * Get detected WebGL version.
83
84
  */
84
85
  getVersion(): WebGLVersion;
86
+ /**
87
+ * Get the clock instance
88
+ */
89
+ getClock(): Clock;
85
90
  /**
86
91
  * Cleanup all resources.
87
92
  */
package/dist/types.d.ts CHANGED
@@ -11,6 +11,8 @@ export interface SandboxOptions {
11
11
  pauseWhenHidden?: boolean;
12
12
  /** Device pixel ratio - "auto" uses window.devicePixelRatio (default: "auto") */
13
13
  dpr?: number | "auto";
14
+ /** Max Frame rate (default: 0 as unlimited) */
15
+ fps?: number;
14
16
  /** Preserve drawing buffer for screenshots (default: false) */
15
17
  preserveDrawingBuffer?: boolean;
16
18
  /** Enable antialiasing (default: true) */
@@ -95,6 +97,10 @@ export interface ClockState {
95
97
  delta: number;
96
98
  /** Frame counter */
97
99
  frame: number;
100
+ /** Is the clock currently running */
101
+ running: boolean;
102
+ /** Frame rate in frames per second */
103
+ fps: number;
98
104
  }
99
105
  /** Internal uniform entry for caching */
100
106
  export interface UniformEntry {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rosalana/sandbox",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Lightweight WebGL wrapper for simple, beautiful shader effects",
5
5
  "keywords": [
6
6
  "webgl",