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.
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.
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!
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!
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.
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"));
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();
}
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.)
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"));
#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;
}
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%;
}
}