Displays GLSL fragment shaders as a website background. Supports Shadertoy shaders, multipass - ping-pong offscreen buffers, feedback loops, floating-point textures. Either with WebGL 1 or 2, will try to run wherever it’s technically possible.
Website/Demo: 🎆 https://xemantic.github.io/shader-web-background 🎆
I designed this library to use complex fragment shaders as part of my web design and development process. This is the tool which finally lets me embrace the web browser as a creative coding environment. If you are familiar with GLSL, then it might help you publish your work on web as well. If you are coming from a web development background, then you might want to learn a bit more about shaders first, for example from The Book of Shaders. I hope that examples presented in this documentation are self-explanatory. If you find it useful, then
TL;DR:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Minimal shader</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<script src="https://xemantic.github.io/shader-web-background/dist/shader-web-background.min.js"></script>
<script type="x-shader/x-fragment" id="image">
precision highp float;
uniform float iTime;
void main() {
gl_FragColor = vec4(
mod(gl_FragCoord.x / 256., 1.),
mod((gl_FragCoord.x + gl_FragCoord.y - iTime * 40.) / 256. , 1.),
mod(gl_FragCoord.y / 256., 1.),
1.
);
}
</script>
<script>
shaderWebBackground.shade({
shaders: {
image: {
uniforms: {
iTime: (gl, loc) => gl.uniform1f(loc, performance.now() / 1000)
}
}
}
});
</script>
<style>
.shader-web-background-fallback {
background: url("https://placekitten.com/666/666");
background-position: center;
background-size: cover;
background-attachment: fixed;
}
</style>
</head>
<body>
<h1>shader-web-background minimal example</h1>
</body>
</html>
ℹ️ If you prefer to learn by example, here is the list of demos displayed with their highlighted source code:
https://xemantic.github.io/shader-web-background/#demo
There are several ways of adjusting this library to your needs:
If you want your shaders to start rendering before any other resources are loaded, then go for this method. Just take the contents of:
https://xemantic.github.io/shader-web-background/dist/shader-web-background.min.js
and put it as <script>
in the <head>
of your HTML file.
See minimal demo for reference (live version).
Add this code to the <head>
of your HTML:
<script src="https://xemantic.github.io/shader-web-background/dist/shader-web-background.min.js"></script>
In the future I will publish shader-web-background
to npm. For now you can just download the latest minified distribution together with source map and sources.
You will need at least one fragment shader defined like this:
<script type="x-shader/x-fragment" id="image">
precision highp float;
void main() {
// ...
}
</script>
Put it in the <head>
of your HTML. The type
should be x-shader/x-fragment
and the id
attribute is arbitrary.
⚠️ Note: Remember to give unique id
to each of your shaders if you are defining more of them.
<script>
shaderWebBackground.shade({
shaders: {
image: {}
}
});
</script>
⚠️ Note: the shader name image
should match the one defined as shader source id
attribute.
ℹ️ This step is not necessary, however adding it will improve the experience for the small amount of users who still cannot run shaders on their devices.
Define fallback CSS style, for example a static screenshot of your shader frame:
<style>
.shader-web-background-fallback {
background: url("https://placekitten.com/666/666");
background-position: center;
background-size: cover;
background-attachment: fixed;
}
</style>
The shader-web-background-fallback
CSS class is applied to HTML document root and the canvas.
⚠️ Note that in case of any errors the default canvas will not be attached to HTML document at all. In case of shading a canvas which is already attached to HTML, it might be tempting to provide a fallback canvas background based on the shader-web-background-fallback
CSS class, however it might not work on some browsers. Custom error handler might be needed for cross compatibility.
See Handling errors section for details.
See the full shader-web-background API
The configuration object passed to the shaderWebBackground.shade(config) call in the example above will result in a minimal rendering pipeline consisting of one fragment shader named image
. A new static <canvas id="shader-web-background">
element covering the whole viewport will be added to the page with z-index: -9999
, to be displayed behind other page elements.
ℹ️ Note: the default <canvas>
element will be attached to document <body>
only when the whole DOM tree is constructed. Also the actual rendering of shader frames will not happen until the page is fully loaded, even though shaders are compiled immediately.
Uniforms provide shaders with the input from the world outside GPU. Describing this mechanism is out of scope of this documentation. I decided not to build abstraction over this part of WebGL, because it is already quite concise. See WebGLRenderingContext.uniform documentation.
Let’s assume that you want to provide your shader with a time value measured in seconds since the moment the page was loaded. First define a uniform in the image
shader:
uniform float iTime;
The iTime
name is arbitrary, but it should match with what you specify in the configuration:
shaderWebBackground.shade({
shaders: {
image: {
uniforms: {
iTime: (gl, loc) => gl.uniform1f(loc, performance.now() / 1000)
}
}
}
});
The (gl, loc) => gl.uniform1f(loc, performance.now() / 1000)
function will be invoked before rendering each shader frame. If you are not familiar with JavaScript arrow functions, it’s an equivalent of:
function(gl, loc) {
gl.uniform1f(loc, performance.now() / 1000)
}
ℹ️ Check documentation of the standard JavaScript performance.now() function which returns the number of milliseconds since the page was loaded. Dividing it by 1000
will result in floating-point value measured in seconds.
⚠️ During development check the console often. If you will forget to configure a uniform declared in the shader, then exception will be thrown (See error-no-configuration-of-shader-uniform test case). Also if you configure a uniform which does not exist in the shader, then a warning will pop up on console (see error-unnecessary-uniform-configured test case).
Summary: you can use this mechanism to adapt any API as an input of your shaders. Check project demos for examples how to integrate input like:
The declaration of “texture” uniform uses sampler2D
type:
uniform sampler2D iWebCam;
ℹ️ The uniform name is arbitrary. For example Shadertoy is binding textures under name iChannel0
, iChannel1
, etc. and this is the convention used mostly in this documentation.
Such a uniform can be set with:
shaderWebBackground.shade({
onInit: (ctx) => {
ctx.iWebCam = initializeTexture(ctx.gl);
},
shaders: {
image: {
uniforms: {
iWebCam: (gl, loc, ctx) => ctx.texture(loc, ctx.iWebCam);
}
}
}
});
⚠️ initializing textures is still not documented, but standard WebGL rules should apply.
ℹ️ the texture passed as a second argument to ctx.texture can be either an instance of WebGLTexture or a reference to the buffer of another shader in the pipeline. Check Complex config example section and API - Context: buffers.
All the shaders, except for the last one in the pipeline, will have associated textures to render to. By default these textures are initialized as RGBA HALF_FLOAT
(16bit) floating-point with linear interpolation and are clamped to the edge. The texture initialization can be customized. See API - Shader: texture documentation for details.
⚠️ Note: the default settings will work on all the platforms while customization can easily break the compatibility, especially on older iOS devices. Consult the API for remedies.
Here is a comprehensive example of a configuration object with comments. It is using Shadertoy conventions for naming buffers and uniforms but keep in mind that the naming is arbitrary and might be adjusted to the needs of your project.
// mouse coordinates taken from from the mousemove event expressed in "CSS pixels"
var mouseX;
var mouseY;
document.addEventListener("mousemove", (event) => {
mouseX = event.clientX;
mouseY = event.clientY;
});
shaderWebBackground.shade({
// supplied canvas to use for shading
canvas: document.getElementById("my-canvas"),
// called only once before the first run
onInit: (ctx) => {
// we can center the mouse even before any "mousemove" event occurs
// note, we are
mouseX = ctx.cssWidth / 2;
mouseY = ctx.cssHeight / 2;
// for convenience you can store your attributes on context
ctx.iFrame = 0;
},
onResize: (width, height, ctx) => {
ctx.iMinDimension = Math.min(width, height);
},
onBeforeFrame: (ctx) => {
ctx.shaderMouseX = ctx.toShaderX(mouseX);
ctx.shaderMouseY = ctx.toShaderY(mouseY);
},
shaders: {
// the first buffer to be rendered in the pipeline
BufferA: {
// uniform setters, attribute names should match with those defined in the shader
uniforms: {
// uniform value calculated in place
iTime: (gl, loc) => gl.uniform1f(loc, performance.now() / 1000),
// uniform values taken from context
iFrame: (gl, loc) => gl.uniform1i(loc, ctx.iFrame),
iMinDimension: (gl, loc, ctx) => gl.uniform1f(loc, ctx.iMinDimension),
iResolution: (gl, loc, ctx) => gl.uniform2f(loc, ctx.width, ctx.height),
iMouse: (gl, loc, ctx) => gl.uniform2f(loc, ctx.shaderMouseX, ctx.shaderMouseY),
// inputing the previous output of itself - feedback loop
iChannel0: (gl, loc, ctx) => ctx.texture(loc, ctx.buffers.BufferA)
// ... more uniforms
}
},
// ... more shaders
BufferD: {
// optional custom initializer of buffer's texture
texture: (gl, ctx) => {
// initializing floating-point texture in custom way for WebGL 1 and 2
ctx.initHalfFloatRGBATexture(ctx.width, ctx.height);
// standard WebGL texture parameters
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
},
uniforms: {
iChanel0: (gl, loc, ctx) => ctx.texture(loc, ctx.buffers.BufferA)
// ... more uniforms
}
},
// the last shader will render to screen
Image: {
uniforms: {
iChanel0: (gl, loc, ctx) => ctx.texture(loc, ctx.buffers.BufferD)
// ... more uniforms
}
}
},
onAfterFrame: (ctx) => {
ctx.iFrame++;
},
// custom error handler
onError: (error, canvas) => {
canvas.remove();
console.error(error);
document.documentElement.classList.add("my-fallback");
}
});
The API is intended to be self explanatory. Check API specification for details. There are several shaders defined in the example above. They will be processed in sequence called Multipass
in Shadertoy nomenclature. The last of defined shaders will render to screen. The output of previous shaders, including feedback loop of the previous frame rendered by the same shader, can be easily passed to uniforms.
Several validations are being performed on supplied configuration to avoid common problems which are usually hard to debug otherwise. The src/test/html/errors/ folder contains all the error test cases which can be also checked on the live demo of error handling.
All the errors and warnings will be visible on console.
See:
⚠️ This library relays on WebGL 1 as a common denominator, therefore even if it will use WebGL 2 whenever it is supported in runtime, the shader code should be still compatible with GLSL ES 1.00
// mouse coordinates taken from from the mousemove event
var mouseX;
var mouseY;
document.addEventListener("mousemove", (event) => {
mouseX = event.clientX;
mouseY = event.clientY;
});
// mouse coordinates relative to the shader, you can also store them on the context
var shaderMouseX;
var shaderMouseY;
shaderWebBackground.shade({
onInit: (ctx) => {
// screen center
mouseX = ctx.cssWidth / 2;
mouseY = ctx.cssHeight / 2;
},
onBeforeFrame: (ctx) => {
shaderMouseX = ctx.toShaderX(mouseX);
shaderMouseY = ctx.toShaderY(mouseY);
},
shaders: {
image: {
uniforms: {
iMouse: (gl, loc) => gl.uniform2f(loc, shaderMouseX, shaderMouseY)
}
}
}
});
ℹ️ Note: initial mouse coordinates are provided in onInit
function because the first mousemove
event can happen long after the shader is started. Shader coordinates start at the bottom-left corner of the canvas and are aligned with the middle of the pixel - (0.5, 0.5)
.
API reference:
Demos:
This library can utilize Shadertoy code with minimal effort - a simple shader wrapping:
<script type="x-shader/x-fragment" id="Image">
precision highp float;
uniform vec2 iResolution;
uniform float iTime;
// ... other needed uniforms
// -- Paste your Shadertoy code here:
// ...
// -- End of Shadertoy code
void main() {
mainImage(gl_FragColor, gl_FragCoord.xy);
}
</script>
The id
attribute of the <script>
is set to reflect Shadertoy tab called Image
. Most shaders will use at least these 2 uniforms, and it’s easy to provide their values in the configuration:
shaderWebBackground.shade({
shaders: {
Image: {
uniforms: {
iResolution: (gl, loc, ctx) => gl.uniform2f(loc, ctx.width, ctx.height),
iTime: (gl, loc) => gl.uniform1f(loc, performance.now() / 1000),
}
}
}
});
Shadertoy demos:
There is no automated solution for that. You will have to copy the Common
part directly into your shaders, just above the other Shadertoy code.
texture
function?In Shadertoy textures are accessed with the texture
function while in WebGL 1 it is texture2D
. Here is a simple workaround to be added before the original code:
#define texture texture2D
In Shadertoy each “Channel” binding a texture can have separate sampler parameters like interpolation or wrapping. This functionality cannot be easily ported to WebGL 1, but most shaders relaying on these features can be adjusted with code-based workarounds. For example if the texture is supposed to be repeated, then something like this might be a functional replacement of the texture
function in a given shader:
vec4 repeatedTexture(in sampler2D channel, in vec2 uv) {
return texture2D(channel, mod(uv, 1.));
}
⚠️ Mipmaps are not supported.
See also API - Shader: texture.
You can name your shaders according to Shadertoy buffer names:
BufferA
BufferB
BufferC
BufferD
Image
And then wire them together:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Multipass Shadertoy shader</title>
<script type="x-shader/x-fragment" id="BufferA">
precision highp float;
uniform sampler2D iChannel0;
// ... the code of BufferA tab with the uniforms and wrapping as above
</script>
<script type="x-shader/x-fragment" id="Image">
precision highp float;
uniform sampler2D iChannel0;
// ... the code of Image tab with the uniforms and wrapping as above
</script>
<script>
// ... your prefer method of loading shader-web-background as described above
</script>
<script>
shaderWebBackground.shade({
shaders: {
BufferA: {
uniforms: {
iChannel0: (gl, loc, ctx) => ctx.texture(loc, ctx.buffers.BufferA)
}
},
Image: {
uniforms: {
iChannel0: (gl, loc, ctx) => ctx.texture(loc, ctx.buffers.BufferA)
}
}
}
});
</script>
</head>
<body>
</body>
</html>
git clone https://github.com/xemantic/shader-web-background.git
cd shader-web-background
./gradlew compileJs
It will trigger Google Closure Compiler which will check sources using type information and transpile them into minified JavaScript files:
This project has been developed using IntelliJ IDEA with google-java-format plugin enabled. The most noticeable element of this style are 2 spaces instead of 4 for rendering tabs.
Either:
<section id="projects-using-shader-web-background">
Or send me a link with description.
Author: xemantic
Demo: https://xemantic.github.io/shader-web-background/
Source Code: https://github.com/xemantic/shader-web-background
#javascript