function shader({
width = 640,
height = 480,
devicePixelRatio = window.devicePixelRatio,
invalidation,
visibility,
uniforms = {},
inputs = {},
iMouse = false,
iTime = false,
sources = [],
preserveDrawingBuffer = false
} = {}) {
uniforms = new Map(Object.entries(uniforms).map(([name, type]) => [name, {type}]));
inputs = new Map(Object.entries(inputs));
for (const {type} of uniforms.values()) if (type !== "float") throw new Error(`unknown type: ${type}`);
for (const name of inputs.keys()) if (!uniforms.has(name)) uniforms.set(name, {type: "float"});
if (iTime && !uniforms.has("iTime")) uniforms.set("iTime", {type: "float"});
if (visibility !== undefined && typeof visibility !== "function") throw new Error("invalid visibility");
return function() {
const source = String.raw.apply(String, arguments);
const canvas = document.createElement("canvas");
canvas.width = width * devicePixelRatio;
canvas.height = height * devicePixelRatio;
canvas.style = `max-width: 100%; width: ${width}px; height: auto;`;
const gl = canvas.getContext("webgl", {preserveDrawingBuffer});
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, `precision highp float;
${Array.from(uniforms, ([name, {type}]) => `uniform ${type} ${name};`).join("\n")}
const vec3 iResolution = vec3(
${(width * devicePixelRatio).toFixed(1)},
${(height * devicePixelRatio).toFixed(1)},
${(devicePixelRatio).toFixed(1)}
);
`, ...sources, source, `
void main() {
mainImage(gl_FragColor, gl_FragCoord.xy);
}
`);
const vertexShader = createShader(gl, gl.VERTEX_SHADER, `
attribute vec2 a_vertex;
void main() {
gl_Position = vec4(a_vertex, 0.0, 1.0);
}
`);
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, Float32Array.of(-1, -1, +1, -1, +1, +1, -1, +1), gl.STATIC_DRAW);
const program = createProgram(gl, vertexShader, fragmentShader);
gl.useProgram(program);
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
const a_vertex = gl.getAttribLocation(program, "a_vertex");
gl.enableVertexAttribArray(a_vertex);
gl.vertexAttribPointer(a_vertex, 2, gl.FLOAT, false, 0, 0);
for (const [name, u] of uniforms) u.location = gl.getUniformLocation(program, name);
const ondispose = invalidation === undefined ? Inputs.disposal(canvas) : invalidation;
let frame;
let disposed = false;
ondispose.then(() => disposed = true);
async function render() {
if (visibility !== undefined) await visibility();
frame = undefined;
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
}
Object.assign(canvas, {
update(values = {}) {
if (disposed) return false;
for (const name in values) {
const u = uniforms.get(name);
if (!u) throw new Error(`unknown uniform: ${name}`);
gl.uniform1f(u.location, values[name]);
}
frame || requestAnimationFrame(render);
return true;
}
});
for (const [name, input] of inputs) {
const u = uniforms.get(name);
if (!u) throw new Error(`unknown uniform: ${name}`);
gl.uniform1f(u.location, input.value);
const update = () => {
gl.uniform1f(u.location, input.value);
frame || requestAnimationFrame(render);
};
input.addEventListener("input", update);
ondispose.then(() => input.removeEventListener("input", update));
}
if (iTime) {
frame = true; // always rendering
const u_time = gl.getUniformLocation(program, "iTime");
let timeframe;
(async function tick() {
if (visibility !== undefined) await visibility();
gl.uniform1f(u_time, performance.now() / 1000);
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
return timeframe = requestAnimationFrame(tick);
})();
ondispose.then(() => cancelAnimationFrame(timeframe));
} else {
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
}
return canvas;
};
}