@rosalana/sandbox 0.0.3 → 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;if(this.callback=t,this.running=!0,this.frame===0){const e=performance.now();this.startTime=e,this.lastTime=e}return 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 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
  /**
@@ -103,11 +106,9 @@ class R {
103
106
  */
104
107
  start(t) {
105
108
  if (this.running) return this;
106
- if (this.callback = t, this.running = !0, this.frame === 0) {
107
- const e = performance.now();
108
- this.startTime = e, this.lastTime = e;
109
- }
110
- return this.rafId = requestAnimationFrame(this.loop), this;
109
+ this.callback = t, this.running = !0;
110
+ const e = performance.now();
111
+ return this.frame === 0 ? this.startTime = e : this.startTime = e - this.time * 1e3, this.lastTime = e, this.rafId = requestAnimationFrame(this.loop), this;
111
112
  }
112
113
  /**
113
114
  * Stop the animation loop.
@@ -120,7 +121,7 @@ class R {
120
121
  * Reset clock to initial state.
121
122
  */
122
123
  reset() {
123
- 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;
124
125
  }
125
126
  /**
126
127
  * Get current clock state snapshot.
@@ -129,7 +130,9 @@ class R {
129
130
  return {
130
131
  time: this.time,
131
132
  delta: this.delta,
132
- frame: this.frame
133
+ frame: this.frame,
134
+ running: this.running,
135
+ fps: Math.round(this.fps)
133
136
  };
134
137
  }
135
138
  /**
@@ -149,27 +152,43 @@ class R {
149
152
  * Cleanup.
150
153
  */
151
154
  destroy() {
152
- this.stop(), this.callback = null;
155
+ this.reset(), this.callback = null;
156
+ }
157
+ /**
158
+ * Set maximum frames per second.
159
+ */
160
+ setMaxFps(t) {
161
+ return this.maxFps = t, this;
153
162
  }
154
163
  /**
155
164
  * Internal animation frame handler.
156
165
  */
157
166
  loop(t) {
158
- 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);
159
178
  }
160
179
  }
161
180
  class p {
162
181
  constructor(t) {
163
- i(this, "gl");
164
- i(this, "vao", null);
165
- i(this, "vbo", null);
166
- i(this, "ibo", null);
167
- i(this, "vertexCount", 0);
168
- i(this, "indexCount", 0);
169
- 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);
170
189
  // WebGL1 VAO extension (if available)
171
- i(this, "vaoExt", null);
172
- i(this, "isWebGL2");
190
+ r(this, "vaoExt", null);
191
+ r(this, "isWebGL2");
173
192
  this.gl = t, this.isWebGL2 = t instanceof WebGL2RenderingContext, this.isWebGL2 || (this.vaoExt = t.getExtension("OES_vertex_array_object"));
174
193
  }
175
194
  /**
@@ -177,7 +196,7 @@ class p {
177
196
  * This is the most common use case for shader effects.
178
197
  */
179
198
  static fullscreenQuad(t) {
180
- const e = new p(t), r = new Float32Array([
199
+ const e = new p(t), i = new Float32Array([
181
200
  // position texcoord
182
201
  -1,
183
202
  -1,
@@ -209,14 +228,14 @@ class p {
209
228
  3
210
229
  // second triangle
211
230
  ]);
212
- return e.setup(r, s), e;
231
+ return e.setup(i, s), e;
213
232
  }
214
233
  /**
215
234
  * Setup geometry from vertex and index data.
216
235
  */
217
236
  setup(t, e) {
218
- const r = this.gl;
219
- 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;
220
239
  }
221
240
  /**
222
241
  * Link vertex attributes to shader program.
@@ -225,15 +244,15 @@ class p {
225
244
  linkAttributes(t) {
226
245
  const e = this.gl;
227
246
  this.bindVAO(), e.bindBuffer(e.ARRAY_BUFFER, this.vbo);
228
- const r = 4 * Float32Array.BYTES_PER_ELEMENT, s = this.getPositionLocation(t);
229
- 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));
230
249
  const n = this.getTexcoordLocation(t);
231
250
  return n >= 0 && (e.enableVertexAttribArray(n), e.vertexAttribPointer(
232
251
  n,
233
252
  2,
234
253
  e.FLOAT,
235
254
  !1,
236
- r,
255
+ i,
237
256
  2 * Float32Array.BYTES_PER_ELEMENT
238
257
  )), this.useIndices && e.bindBuffer(e.ELEMENT_ARRAY_BUFFER, this.ibo), this.unbindVAO(), this;
239
258
  }
@@ -295,13 +314,13 @@ class p {
295
314
  this.vao && (this.isWebGL2 ? this.gl.deleteVertexArray(this.vao) : this.vaoExt && this.vaoExt.deleteVertexArrayOES(this.vao), this.vao = null);
296
315
  }
297
316
  }
298
- class h {
317
+ class a {
299
318
  constructor(t) {
300
- i(this, "gl");
301
- i(this, "program", null);
302
- i(this, "vertexShader", null);
303
- i(this, "fragmentShader", null);
304
- 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);
305
324
  this.gl = t;
306
325
  }
307
326
  /**
@@ -318,10 +337,10 @@ class h {
318
337
  */
319
338
  compile(t, e) {
320
339
  this.destroy();
321
- const r = h.detectVersion(t), s = h.detectVersion(e);
322
- if (r != s)
323
- throw new k(r, s);
324
- 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;
325
344
  }
326
345
  /**
327
346
  * Bind this program for rendering.
@@ -365,16 +384,16 @@ class h {
365
384
  * @throws ShaderCompilationError if compilation fails
366
385
  */
367
386
  compileShader(t, e) {
368
- 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);
369
388
  if (!n)
370
389
  throw new c(
371
390
  t,
372
391
  e,
373
392
  "Failed to create shader object"
374
393
  );
375
- if (r.shaderSource(n, e), r.compileShader(n), !r.getShaderParameter(n, r.COMPILE_STATUS)) {
376
- const f = r.getShaderInfoLog(n) || "Unknown error";
377
- 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);
378
397
  }
379
398
  return n;
380
399
  }
@@ -398,16 +417,16 @@ class h {
398
417
  }
399
418
  class x {
400
419
  constructor(t, e) {
401
- i(this, "name");
402
- i(this, "method");
403
- i(this, "isArray");
404
- i(this, "isMatrix");
405
- i(this, "location", null);
406
- i(this, "locationResolved", !1);
407
- 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");
408
427
  this.name = t, this.value = e;
409
- const r = x.inferMethodInfo(e);
410
- 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;
411
430
  }
412
431
  /**
413
432
  * Infer WebGL method and metadata from value type.
@@ -419,9 +438,9 @@ class x {
419
438
  return { method: "uniform1f", isArray: !1, isMatrix: !1 };
420
439
  if (!Array.isArray(t))
421
440
  return { method: "uniform1f", isArray: !1, isMatrix: !1 };
422
- const e = t.length, r = t[0];
423
- if (Array.isArray(r))
424
- switch (r.length) {
441
+ const e = t.length, i = t[0];
442
+ if (Array.isArray(i))
443
+ switch (i.length) {
425
444
  case 2:
426
445
  return { method: "uniform2fv", isArray: !0, isMatrix: !1 };
427
446
  case 3:
@@ -477,8 +496,8 @@ class x {
477
496
  * @param program - Current WebGL program (for location resolution)
478
497
  */
479
498
  upload(t, e) {
480
- const r = this.resolveLocation(t, e);
481
- if (r === null)
499
+ const i = this.resolveLocation(t, e);
500
+ if (i === null)
482
501
  return;
483
502
  const s = this.value;
484
503
  let n;
@@ -486,40 +505,40 @@ class x {
486
505
  s.flat()
487
506
  ) : n = new Float32Array(s), this.method) {
488
507
  case "uniform1f":
489
- t.uniform1f(r, n);
508
+ t.uniform1f(i, n);
490
509
  break;
491
510
  case "uniform1i":
492
- t.uniform1i(r, n);
511
+ t.uniform1i(i, n);
493
512
  break;
494
513
  case "uniform1fv":
495
- t.uniform1fv(r, n);
514
+ t.uniform1fv(i, n);
496
515
  break;
497
516
  case "uniform2fv":
498
- t.uniform2fv(r, n);
517
+ t.uniform2fv(i, n);
499
518
  break;
500
519
  case "uniform3fv":
501
- t.uniform3fv(r, n);
520
+ t.uniform3fv(i, n);
502
521
  break;
503
522
  case "uniform4fv":
504
- t.uniform4fv(r, n);
523
+ t.uniform4fv(i, n);
505
524
  break;
506
525
  case "uniformMatrix2fv":
507
- t.uniformMatrix2fv(r, !1, n);
526
+ t.uniformMatrix2fv(i, !1, n);
508
527
  break;
509
528
  case "uniformMatrix3fv":
510
- t.uniformMatrix3fv(r, !1, n);
529
+ t.uniformMatrix3fv(i, !1, n);
511
530
  break;
512
531
  case "uniformMatrix4fv":
513
- t.uniformMatrix4fv(r, !1, n);
532
+ t.uniformMatrix4fv(i, !1, n);
514
533
  break;
515
534
  }
516
535
  }
517
536
  }
518
537
  const d = class d {
519
538
  constructor(t) {
520
- i(this, "gl");
521
- i(this, "program", null);
522
- i(this, "uniforms", /* @__PURE__ */ new Map());
539
+ r(this, "gl");
540
+ r(this, "program", null);
541
+ r(this, "uniforms", /* @__PURE__ */ new Map());
523
542
  this.gl = t;
524
543
  }
525
544
  /**
@@ -537,15 +556,15 @@ const d = class d {
537
556
  * Creates the uniform if it doesn't exist, updates if it does.
538
557
  */
539
558
  set(t, e) {
540
- const r = this.uniforms.get(t);
541
- 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;
542
561
  }
543
562
  /**
544
563
  * Set multiple uniforms at once.
545
564
  */
546
565
  setMany(t) {
547
- for (const [e, r] of Object.entries(t))
548
- this.set(e, r);
566
+ for (const [e, i] of Object.entries(t))
567
+ this.set(e, i);
549
568
  return this;
550
569
  }
551
570
  /**
@@ -582,8 +601,8 @@ const d = class d {
582
601
  * Upload only built-in uniforms (u_resolution, u_time, u_delta, u_mouse, u_frame).
583
602
  * Call this every frame with current values.
584
603
  */
585
- uploadBuiltIns(t, e, r) {
586
- 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)
587
606
  return this;
588
607
  for (const s of d.BUILT_INS) {
589
608
  const n = this.uniforms.get(s);
@@ -617,7 +636,7 @@ const d = class d {
617
636
  }
618
637
  };
619
638
  /** Built-in uniform names that are handled automatically */
620
- i(d, "BUILT_INS", /* @__PURE__ */ new Set([
639
+ r(d, "BUILT_INS", /* @__PURE__ */ new Set([
621
640
  "u_resolution",
622
641
  "u_time",
623
642
  "u_delta",
@@ -627,7 +646,7 @@ i(d, "BUILT_INS", /* @__PURE__ */ new Set([
627
646
  let v = d;
628
647
  class b {
629
648
  constructor() {
630
- i(this, "hooks", /* @__PURE__ */ new Map());
649
+ r(this, "hooks", /* @__PURE__ */ new Map());
631
650
  }
632
651
  id() {
633
652
  return Math.random().toString(36).substring(2, 10);
@@ -643,8 +662,8 @@ class b {
643
662
  }
644
663
  /** Run all hooks with the given state */
645
664
  run(t) {
646
- for (const [e, r] of this.hooks)
647
- r(t) === !1 && this.remove(e);
665
+ for (const [e, i] of this.hooks)
666
+ i(t) === !1 && this.remove(e);
648
667
  }
649
668
  destroy() {
650
669
  this.hooks.clear();
@@ -652,27 +671,27 @@ class b {
652
671
  }
653
672
  class _ {
654
673
  constructor(t, e) {
655
- i(this, "canvas");
656
- i(this, "gl");
657
- i(this, "options");
658
- i(this, "onBeforeHooks", new b());
659
- i(this, "onAfterHooks", new b());
660
- i(this, "_program");
661
- i(this, "_geometry");
662
- i(this, "_uniforms");
663
- i(this, "_clock");
664
- i(this, "_resolution", [1, 1]);
665
- i(this, "_mouse", [0, 0]);
666
- i(this, "_version", 1);
667
- i(this, "playing", !1);
668
- 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);
669
688
  }
670
689
  /**
671
690
  * Factory method to create and setup WebGL instance.
672
691
  */
673
692
  static setup(t, e) {
674
- const r = new _(t, e);
675
- 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;
676
695
  }
677
696
  /**
678
697
  * Initialize WebGL context.
@@ -689,9 +708,9 @@ class _ {
689
708
  }, e = this.canvas.getContext("webgl2", t);
690
709
  if (e)
691
710
  return this._version = 2, e;
692
- const r = this.canvas.getContext("webgl", t);
693
- if (r)
694
- return this._version = 1, r;
711
+ const i = this.canvas.getContext("webgl", t);
712
+ if (i)
713
+ return this._version = 1, i;
695
714
  const s = new w("not_supported");
696
715
  throw this.options.onError(s), s;
697
716
  }
@@ -704,8 +723,8 @@ class _ {
704
723
  /**
705
724
  * Set viewport dimensions.
706
725
  */
707
- viewport(t, e, r, s) {
708
- 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;
709
728
  }
710
729
  /**
711
730
  * Set the clock time
@@ -744,10 +763,10 @@ class _ {
744
763
  shader(t, e) {
745
764
  try {
746
765
  this._program.compile(t, e), this._version = this._program.getVersion(), this._geometry.linkAttributes(this._program);
747
- const r = this._program.getProgram();
748
- r && this._uniforms.attachProgram(r);
749
- } catch (r) {
750
- 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);
751
770
  }
752
771
  return this;
753
772
  }
@@ -783,6 +802,12 @@ class _ {
783
802
  getVersion() {
784
803
  return this._version;
785
804
  }
805
+ /**
806
+ * Get the clock instance
807
+ */
808
+ getClock() {
809
+ return this._clock;
810
+ }
786
811
  /**
787
812
  * Cleanup all resources.
788
813
  */
@@ -824,7 +849,7 @@ void main() {
824
849
  vec3 color = vec3(uv.x, uv.y, 0.5 + 0.5 * sin(u_time));
825
850
  gl_FragColor = vec4(color, 1.0);
826
851
  }
827
- `, y = `#version 300 es
852
+ `, E = `#version 300 es
828
853
 
829
854
  in vec2 a_position;
830
855
  in vec2 a_texcoord;
@@ -834,7 +859,7 @@ out vec2 v_texcoord;
834
859
  void main() {
835
860
  v_texcoord = a_texcoord;
836
861
  gl_Position = vec4(a_position, 0.0, 1.0);
837
- }`, M = `#version 300 es
862
+ }`, V = `#version 300 es
838
863
  precision highp float;
839
864
 
840
865
  uniform vec2 u_resolution;
@@ -849,17 +874,19 @@ void main() {
849
874
  vec3 color = vec3(uv.x, uv.y, 0.5 + 0.5 * sin(u_time));
850
875
  fragColor = vec4(color, 1.0);
851
876
  }`;
852
- class E {
877
+ class y {
853
878
  constructor(t, e) {
854
879
  /** Active event listeners */
855
- i(this, "listeners", []);
880
+ r(this, "listeners", []);
856
881
  /** HTML canvas element */
857
- i(this, "canvas");
882
+ r(this, "canvasEl");
858
883
  /** Resolved options */
859
- i(this, "options");
884
+ r(this, "options");
860
885
  /** WebGL engine */
861
- i(this, "engine");
862
- 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();
863
890
  }
864
891
  /**
865
892
  * Sandbox - A lightweight WebGL wrapper for shader effects.
@@ -880,7 +907,7 @@ class E {
880
907
  * });
881
908
  */
882
909
  static create(t, e) {
883
- return new E(t, e);
910
+ return new y(t, e);
884
911
  }
885
912
  resolveOptions(t) {
886
913
  const e = {
@@ -889,12 +916,13 @@ class E {
889
916
  autoplay: !0,
890
917
  pauseWhenHidden: !0,
891
918
  dpr: "auto",
919
+ fps: 0,
892
920
  preserveDrawingBuffer: !1,
893
921
  antialias: !0,
894
- onError: (r) => {
922
+ onError: (i) => {
895
923
  console.error(
896
924
  "Oops!",
897
- r,
925
+ i,
898
926
  `
899
927
  You can handle errors programmatically by providing an onError callback to suppress this log and implement custom fallback behavior.`
900
928
  );
@@ -905,13 +933,13 @@ You can handle errors programmatically by providing an onError callback to suppr
905
933
  onAfterRender: null,
906
934
  uniforms: {}
907
935
  };
908
- if (t != null && t.vertex && !(t != null && t.fragment)) {
909
- const r = h.detectVersion(t.vertex);
910
- 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;
911
939
  }
912
940
  if (t != null && t.fragment && !(t != null && t.vertex)) {
913
- const r = h.detectVersion(t.fragment);
914
- 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;
915
943
  }
916
944
  return { ...e, ...t };
917
945
  }
@@ -922,7 +950,7 @@ You can handle errors programmatically by providing an onError callback to suppr
922
950
  this.setViewport();
923
951
  }),
924
952
  // Canvas resize
925
- l.on(this.canvas, "resize", () => {
953
+ l.on(this.canvasEl, "resize", () => {
926
954
  this.setViewport();
927
955
  }),
928
956
  // Visibility check on scroll
@@ -946,21 +974,21 @@ You can handle errors programmatically by providing an onError callback to suppr
946
974
  this.listeners.forEach((t) => t()), this.listeners = [];
947
975
  }
948
976
  setViewport() {
949
- 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;
950
978
  this.engine.viewport(
951
979
  0,
952
980
  0,
953
981
  Math.max(1, Math.floor(e * t)),
954
- Math.max(1, Math.floor(r * t))
982
+ Math.max(1, Math.floor(i * t))
955
983
  );
956
984
  }
957
985
  isInViewport() {
958
- const t = this.canvas.getBoundingClientRect();
986
+ const t = this.canvasEl.getBoundingClientRect();
959
987
  return t.bottom >= 0 && t.right >= 0 && t.top <= (window.innerHeight || document.documentElement.clientHeight) && t.left <= (window.innerWidth || document.documentElement.clientWidth);
960
988
  }
961
989
  setMouse(t, e) {
962
- const r = this.canvas.getBoundingClientRect();
963
- 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);
964
992
  }
965
993
  /**
966
994
  * Set a single uniform value with type checking.
@@ -1001,7 +1029,7 @@ You can handle errors programmatically by providing an onError callback to suppr
1001
1029
  * sandbox.setShader(vertexSource, fragmentSource);
1002
1030
  */
1003
1031
  setShader(t, e) {
1004
- 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;
1005
1033
  }
1006
1034
  /**
1007
1035
  * Update only fragment shader (uses default vertex).
@@ -1009,8 +1037,18 @@ You can handle errors programmatically by providing an onError callback to suppr
1009
1037
  * sandbox.setFragment(fragmentSource);
1010
1038
  */
1011
1039
  setFragment(t) {
1012
- const r = this.webglVersion() === 1 ? g : y;
1013
- 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;
1014
1052
  }
1015
1053
  /**
1016
1054
  * Add a runtime render hook.
@@ -1040,8 +1078,8 @@ You can handle errors programmatically by providing an onError callback to suppr
1040
1078
  * Pause animation loop at specific time (in seconds).
1041
1079
  */
1042
1080
  pauseAt(t) {
1043
- const e = this.hook((r) => {
1044
- r.time >= t && (e(), this.pause());
1081
+ const e = this.hook((i) => {
1082
+ i.time >= t && (e(), this.pause());
1045
1083
  }, "after");
1046
1084
  return this;
1047
1085
  }
@@ -1082,14 +1120,14 @@ You can handle errors programmatically by providing an onError callback to suppr
1082
1120
  /**
1083
1121
  * Get WebGL version using (1 or 2).
1084
1122
  */
1085
- webglVersion() {
1123
+ get version() {
1086
1124
  return this.engine.getVersion();
1087
1125
  }
1088
1126
  /**
1089
1127
  * Get canvas element.
1090
1128
  */
1091
- canvasElement() {
1092
- return this.canvas;
1129
+ get canvas() {
1130
+ return this.canvasEl;
1093
1131
  }
1094
1132
  /**
1095
1133
  * Destroy sandbox and release all resources.
@@ -1103,7 +1141,7 @@ You can handle errors programmatically by providing an onError callback to suppr
1103
1141
  }
1104
1142
  }
1105
1143
  export {
1106
- E as Sandbox,
1144
+ y as Sandbox,
1107
1145
  w as SandboxContextError,
1108
1146
  u as SandboxError,
1109
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.3",
3
+ "version": "0.0.5",
4
4
  "description": "Lightweight WebGL wrapper for simple, beautiful shader effects",
5
5
  "keywords": [
6
6
  "webgl",