Cursor feedback

A shader that feeds back on itself through a 5x5 tiled history buffer.

View live demoRaw .ts
/**
 * History buffer demo. Flashes colors frame-by-frame, then displays
 * all previous frames side-by-side using helpers plugin.
 */
import ShaderPad from 'shaderpad';
import helpers from 'shaderpad/plugins/helpers';
import { createFullscreenCanvas } from 'shaderpad/util';

import type { ExampleContext } from '@/examples/runtime';

const gridLength = 5;
const gridSize = gridLength * gridLength;

const fragmentShaderSrc = `#version 300 es
precision highp float;

in vec2 v_uv;
out vec4 outColor;
uniform int u_frame;
uniform vec2 u_cursor;

uniform highp sampler2DArray u_history;
uniform int u_historyFrameOffset;

void main() {
	vec2 gridUV = fract(v_uv * ${gridLength}.0);
	vec2 gridPos = floor(v_uv * ${gridLength}.0);

	// Calculate which history frame to show based on grid position.
	int historyLength = textureSize(u_history, 0).z;
	int outputFrameIndex = u_frame % historyLength; // Index of the frame this full render will write to.
	int age = int(${gridLength}.0 - gridPos.x + gridPos.y * ${gridLength}.0); // 25 is top left, 1 is bottom right.

	// Sample from history texture; dim old frames.
	float z = historyZ(u_history, u_historyFrameOffset, age);
	vec3 historyColor = texture(u_history, vec3(gridUV, z)).rgb;
	float dim = 1.0 - float(age) / float(historyLength);
	historyColor *= dim;

	// Add cursor overlay.
	float cursorDist = distance(v_uv, u_cursor);
	float cursor = smoothstep(0.05, 0.02, cursorDist);
	vec3 cursorColor = vec3(cursor, 0.0, 0.0);

	vec3 finalColor = historyColor + cursorColor;
	outColor = vec4(finalColor, 1.0);
}`;

export async function init({ mount }: ExampleContext) {
	const canvas = createFullscreenCanvas(mount);
	canvas.width = 1000;
	canvas.height = 1000;

	const shader = new ShaderPad(fragmentShaderSrc, {
		canvas,
		history: gridSize,
		plugins: [helpers()],
		cursorTarget: window,
	});
	shader.play();

	return () => {
		shader.destroy();
		canvas.remove();
	};
}