Easy 2D shader setup with quad-shader

The quad-shader JavaScript library helps you build WebGL animations easily. The library has zero dependencies and is extremely lightweight (2kB gzipped). WebGL runs on the GPU, allowing for smooth, high-frame-rate animations.

All animations on this page are generated on the fly with WebGL fragment shaders using quad-shader, Vite and TypeScript. See below for more animation examples.

With fragment shaders, you have control over each pixel's color, making it perfect for Creative Coding and generating textures and patterns procedurally. With a few lines of code, you can create intricate effects.

Getting started with quad-shader

We'll go through the steps required for displaying the following animation:

First install quad-shader:

npm install quad-shader

Then include the following code in your page:

import { animate, getComputedStylePropRGBA } from "quad-shader";
const qs = animate(
    canvas, /* the HTMLCanvasElement to draw on */
    frag, /* fragment shader, as a string */
);
/* register a callback that updates "uColor" on render */
/* 'getComputedStylePropRGBA' is a helper that returns the */
/*  given CSS property as a RGBA value */
qs.uniform4f("uColor", () => getComputedStylePropRGBA(canvas, "color"));

The canvas should be an HTMLCanvasElement. The frag should be a string with the following content:

precision lowp float;
varying vec2 vPosition; /* pixel position, X & Y in [-1, +1] */
uniform vec4 uColor; /* injected from JS */
uniform float uTime; /* time in seconds since canvas loaded */
// Animation code
void main() {
    float theta = atan(vPosition.y, vPosition.x);
    float rho = length(vPosition.xy);
    float v = mod(rho - uTime/10., .2);
    float alpha = smoothstep(.1, .2, v);
    alpha *= 1. - smoothstep(0., 1., rho);
    float fadeIn = smoothstep(0., 1., uTime);
    gl_FragColor = fadeIn * alpha * uColor;
}

You should now see the animation on your page! Keep reading for advanced usage or jump straight to the Examples section for inspiration.

Passing inputs as uniforms

While the snippet above is very concise, quad-shader includes some pretty powerful features out of the box. First, the position of the current pixel is provided as vPosition. Second, the value of the uTime uniform will be continuously updated — this is done for you by quad-shader's animate() function.

By calling the uniform4f method on the object returned by animate(), we could pass a static value to set the custom uColor uniform. Though we can also register a callback which will reflect the canvas' (inherited) CSS color property at any given time — open your devtools and have some fun modifying the properties on this page! The technique used is described in this blog post.

All uniform[1234][fi] methods from WebGL are mirrored, using quad-shader's simpler API.

The uniform setters are a great way to make dynamic animations — especially if you start reacting to browser events. Try clicking this next animation!

Finally, and probably least obvious: the animation is only rendered when the canvas element is on the screen! If you scroll past it and the element exits the viewport, no frames will be rendered, saving on GPU power. As soon as the element re-enters the viewport, animation will resume, as if it had been running in the background.

For more information check out the code for this page on GitHub and give it a star if you like it!


Examples

This section contains some example usages of quad-shader.

The source code for each example is included. You’re free to use or modify it in accordance with the MIT License. If you find the examples useful, consider leaving a star on the repository!

1. Plane Window

A slow-moving animation inspired by the view from an airplane window. The clouds are generated using sine waves with varying frequencies and horizontal speeds.

View js code
import { animate, getComputedStylePropRGBA } from "quad-shader";
const qs = animate(
    canvas, /* the HTMLCanvasElement to draw on */
    frag, /* fragment shader, as a string */
);
/* register a callback that updates "uColor" on render */
/* 'getComputedStylePropRGBA' is a helper that returns the */
/*  given CSS property as a RGBA value */
qs.uniform4f("uColor", () => getComputedStylePropRGBA(canvas, "color"));
View fragment code
precision lowp float;
varying vec2 vPosition;
uniform vec4 uColor;
uniform float uTime;
vec4 waves() {
    // The pixel value (starting out transparent)
    vec4 pixel = vec4(0., 0., 0., 0.);
    // ellipse equation; window is "1." inside the ellipse and "0." outside
    float window = step(1., vPosition.x*vPosition.x/(.9*.9) + vPosition.y*vPosition.y);
    // default "sky" color
    pixel = .08 * uColor;
    for (int i = 8; i >= 1; i-=1) {
        // "distance" to the window
        float d = float(i);
        // Equation for a wave as wave < A sin(wt + phi) + B
        float B = - .5 + 3./14.*sqrt(d - 1.);
        // amplitude of wave, slowly decreasing with distance
        float A = .15 / sqrt(2.*d - 1.);
        // the speed at which the wave ("cloud") moves
        float v = .3 * (1. + 3./sqrt(d - .5))/5.;
        // wave phase angle at any point is displacement (how far the "cloud"
        // has moved) plus some somewhat arbitrary shift
        float phi = v * uTime + (d - 1.) * (d - 1.) /4.;
        // the angular frequency (found empirically)
        float w = d * 2. / 5. + 13./5.;
        bool inWave = vPosition.y < B + A * sin( -1. * w * vPosition.x + phi);
        // fade as we get further away
        vec4 color = uColor * (1. - (d - 1.)/10.);
        pixel = inWave ? color : pixel;
    }
    pixel *= 1. - window; // transparent outside of window border
    return pixel;
}
void main() {
    float fadeIn = smoothstep(0., 1., uTime);
    gl_FragColor = fadeIn * waves();
}

2. Bubbly Button

A button that sparkles with bubbly joy. The rendered canvas element is nested inside the button element and slightly oversized so that the bubbles can escape the button’s bounds. (See the included CSS below.)

View js code
import { animate, getComputedStylePropRGBA } from "quad-shader";
const qs = animate(
    canvas, /* the HTMLCanvasElement to draw on */
    frag, /* fragment shader, as a string */
);
/* register a callback that updates "uColor" on render */
/* 'getComputedStylePropRGBA' is a helper that returns the */
/*  given CSS property as a RGBA value */
qs.uniform4f("uColor", () => getComputedStylePropRGBA(canvas, "color"));
View fragment code
#define TAU 6.28318530718
precision mediump float;
varying vec2 vPosition;
uniform vec4 uColor;
uniform float uTime;
const float n_slices = 25.; // how many radial slices to draw
const float R = .07; // Reference radius
float rnd(float x) {
  return mod(sin(x * 12.9898) * 43758.5453, 1.);
}
float get_opacity(vec2 uv) {
  float product = 1.;
  float transparency = 1.;
  for(int i = 0; i < int(n_slices); i ++) {
    float x = rnd(float(i)); // Randomness used for dot size, motion & opacity
    float r = R * (.1 + x/2.);
    float speed = .15 + .5 * rnd(x);
    float phase = x + float(i) / n_slices;
    float opacity = x;
    float rho = mod(uTime*speed + phase, 1.); // Repeat the animation
    float theta = float(i + i) * TAU /n_slices ;
    theta += uTime * 2. / TAU; // Add some rotation
    vec2 center = rho * vec2(cos(theta), sin(theta)); // dot center
    float d = 2.*r;
    float appear = d + .3*rnd(x); // distance where dot fades in
    float disappear = 1. - d - rnd(x)/5.; // distance where dot fades out
    opacity *= smoothstep(appear, appear+.2, rho) * (1. - smoothstep(disappear -.1, disappear, rho));
    product *= 1. - opacity * (1. - step(r, length(uv - center)));
  }
  return 1. - product;
}
void main() {
    gl_FragColor = get_opacity(vPosition) * uColor;
}
View CSS
button {
  position: relative;
  display: block;
  width: 20em;
  aspect-ratio: 4 / 3;
  margin: 10em auto;
  padding: 0;
  color: var(--col-pop);
  cursor: pointer;
  --stroke-width: 4px;
  stroke-width: calc(var(--stroke-width) / 3);
  border-width: 0;
  border-radius: 20px;
  outline-width: var(--stroke-width);
  outline-style: solid;
  background-color: var(--col-background);
  outline-color: var(--col-pop);
  &:hover {
    color: var(--color-primary);
    outline-width: 4px;
  }
  &:active {
    color: var(--col-background);
    background-color: var(--col-pop);
  }
  & canvas {
    color: var(--col-pop);
    position: absolute;
    left: -50%;
    top: -50%;
    width: 200%;
    height: 200%;
    pointer-events: none;
    transition: opacity 0.6s linear;
  }
  & svg {
    display: block;
    width: 100%;
    height: 50%;
  }
}