Pocket Shader

A pocket-sized javascript library for easy WebGL shader rendering.


npx
jsr add
@braebo / pocket-shader

Usage

Fullscreen

By default, the PocketShader class creates a canvas and appends it to the body with css position: fixed.

import { PocketShader } from 'pocket-shader'

new PocketShader()

Sizing

To use a specific container, pass an element or selector as the first argument.

import { PocketShader } from 'pocket-shader'

new PocketShader('#codeblock2')

The canvas will always grow to fill its container, remaining responsive and resizing automatically.

Shaders

Shaders can be passed into the fragment and/or vertex options.

import { PocketShader } from 'pocket-shader'

const ps = new PocketShader('#codeblock3', { 
  fragment: `
    uniform float u_time;
    uniform vec2 u_mouse;

    in vec2 vUv;
    out vec4 color;

    void main() {
      vec2 p = vUv - 0.5, m = u_mouse - 0.5;
      float a = atan(p.y, p.x) - atan(m.y, m.x);
      float c = 0.5 + 0.5 * cos(a);
      color = vec4(c, m.x + 0.5 * c, m.y + 0.5 * c, 1.0);
    }
  `
})

The default vertex and fragment shaders:


in vec4 a_position;
out vec2 vUv;

void main() {
    vUv = a_position.xy;
    gl_Position = a_position;
}
#version 300 es
precision mediump float;

in vec2 vUv;
out vec4 color;

uniform float u_time;

void main() {
    color = vec4(vUv, 0.5 + 0.5 * sin(u_time), 1.0);
}

note: #version and precision are automatically injected (if missing).

Raw Imports

Bundlers like vite make it easy to import your shaders directly from .glsl files.

import { PocketShader } from 'pocket-shader'
import fragment from './clouds.glsl?raw'

new PocketShader('#codeblock4', {
	autoStart: true,
	fragment 
})

Animation

Render Loop

The autoStart option will start a render loop as soon as the canvas has been appended to its container.

import { PocketShader } from 'pocket-shader'
import fragment from './bruh.frag?raw'

new PocketShader('#codeblock5', {
    fragment,
    autoStart: true
})

The on method allows you to listen for the 'render' event, which fires before each frame.

import { PocketShader } from 'pocket-shader'
import fragment from './kirby.frag?raw'

const ps = new PocketShader('#codeblock11', {
    fragment,
    autoStart: true,
    uniforms: { 
        u_jump: { type: 'float', value: 0 } 
    },
})

ps.on('render', ({ time }) => { 
    ps.uniforms.u_jump.value = Math.sin(time) 
}) 

Playback Controls

The animation loop can be controlled with methods like start() and stop().

import { PocketShader } from 'pocket-shader'

const ps = new PocketShader('#codeblock6')

ps.start() 
ps . state
ps . time

The speed can be passed as an option, or adjusted freely on each instance.

import { PocketShader } from 'pocket-shader'

new PocketShader('#codeblock7', {
    autoStart: true,
    speed: 2
})

2.00

Max Pixel Ratio

The maxPixelRatio will limit the pixel ratio of the canvas, which is determined by the device's pixel ratio by default.

Lowering this can increse performance significantly depending on the size of the canvas / DPI of the screen.

import myShader from './dyingUniverse.glsl?raw'
import { PocketShader } from 'pocket-shader'

new PocketShader('#codeblock8', {
    fragment: myShader,
    maxPixelRatio: 2
    autoStart: true,
})

0.1

Uniforms

Built-in Uniforms

The following built-in uniforms are always available:

uniform vec2  u_resolution;
uniform float u_time;
uniform vec2  u_mouse;

in vec2 vUv;

u_resolution

The canvas resolution is determined by the container size and the maxPixelRatio.

u_time

The time in seconds since the shader started. This can be controlled directly with ps.time. See Playback Controls.

u_mouse

The mouse position, normalized to vec2(0-1). See Mouse Position.

Custom Uniforms

The uniforms option accepts custom uniforms.

They can be accessed anytime with ps.uniforms and will auto-update in the shader when changed.

import { PocketShader } from 'pocket-shader'
import myShader from './glass.frag?raw'

const ps = new PocketShader('#codeblock9', {
    fragment: myShader,
    autoStart: true,
    uniforms: { 
        octave: { type: 'int',   value: 3 },
        zoom:   { type: 'float', value: 0 },
        sphere: { type: 'float', value: 1 },
        // etc...
    } 
})

// Update them later
ps.uniforms.octave.value = 4
ps.uniforms.zoom.value = 1
octave
3
zoom
2.00
sphere
1.00
refraction
0.90
U
1.00
V
0.50
W
1.00

For now, uniforms must be {type,value} objects.  In the future, it will accept primitive values and infer the type internally.
(just ran out of time for now...)

Mouse Position

The mouse position is available in the uniform u_mouse.

The position is normalized to vec2(0-1), where [0, 0] is the bottom left corner of the canvas (bottom is 0 in GLSL).

const ps = new PocketShader({ 
  fragment: `
    uniform vec2 u_resolution;
    uniform vec2 u_mouse;

    in vec2 vUv;
    out vec4 color;

    void main() {
      vec2 uv = vUv * u_resolution;
      float size = u_resolution.x * 0.05;

      vec2 mousePos = u_mouse * u_resolution;

      float dist = length(uv - mousePos);
      float circle = smoothstep(size, size - 0.1, dist);
      float glowEffect = exp(-dist * 0.1);

      color = vec4(vec3(circle * glowEffect), 1.0);
    }
  `,
}) 

It can be manually updated on the instance with ps.mouse.

/**
 * Always keep the mouse position updated,
 * even while outside the canvas bounds.
 */
window.addEventListener('mousemove', e => {
  // normalized to 0-1
  ps.setMousePosition(e)

  // or, raw dogged
  ps.mouse = {
    x: e.clientX
    y: e.clientY
  }
})

Mouse Smoothing

mouseSmoothing can be passed in as an option, and/or updated on an instance.

It ranges from 0-1, where 0 is no smoothing, and 1 is infinite smoothing.

const ps = new PocketShader('#codeblock10', {
  mouseSmoothing: 0.8,
  fragment: `...`
})

ps.mouseSmoothing = 0.95

0.95