templates/shader/src/minimal/minimal-example.md
A bare-bones WebGL shader template that renders a solid color background responding to tldraw's dark mode. Use this as the starting point for a new shader.
In fragment.glsl: Add uniform declaration
uniform float u_myValue;
In MinimalShaderManager.ts:
private u_myValue: WebGLUniformLocation | null = null
onInitialize after line 103:
this.u_myValue = this.gl.getUniformLocation(this.program, 'u_myValue')
onRender (around line 144):
if (this.u_myValue) {
this.gl.uniform1f(this.u_myValue, this.config.get().myValue)
}
In config.ts:
ShaderManagerConfig interface (line ~4):
export interface ShaderManagerConfig extends WebGLManagerConfig {
count: number
myValue: number // Add here
}
DEFAULT_CONFIG (line ~8):
myValue: 0.5,
In MinimalConfigPanel.tsx:
SLIDER_CONFIGS (line ~8):
const SLIDER_CONFIGS: Record<string, { min: number; max: number }> = {
count: { min: 0, max: 100 },
myValue: { min: 0, max: 1 }, // Add here
}
The fragment shader (fragment.glsl) is where visual effects happen:
v_uv - normalized UV coordinates (0-1) from vertex shaderfragColor - RGBA color for the pixelu_bgColor uniformExample modifications:
Gradient effect:
void main() {
vec3 color = mix(vec3(1.0, 0.0, 0.0), vec3(0.0, 0.0, 1.0), v_uv.x);
fragColor = vec4(color, 1.0);
}
Animated pattern with time:
uniform float u_time;
void main() {
float pattern = sin(v_uv.x * 10.0 + u_time) * cos(v_uv.y * 10.0 + u_time);
fragColor = vec4(vec3(pattern), 1.0);
}
The shader manager tracks pointer position (MinimalShaderManager.ts:184). To use it:
Add uniform to fragment.glsl:
uniform vec2 u_pointer;
Get and set in MinimalShaderManager.ts:
// In onInitialize:
this.u_pointer = this.gl.getUniformLocation(this.program, 'u_pointer')
// In onRender:
if (this.u_pointer) {
this.gl.uniform2f(this.u_pointer, this.pointer.x, this.pointer.y)
}
Time values are available in onRender:
onRender = (deltaTime: number, currentTime: number): void => {
// deltaTime: time since last frame (seconds)
// currentTime: total elapsed time (seconds)
if (this.u_time) {
this.gl.uniform1f(this.u_time, currentTime)
}
}
The shader manager has access to the editor instance. To react to shapes:
onUpdate = (): void => {
const shapes = this.editor.getCurrentPageShapes()
// Process shapes and update shader uniforms
}
Use the pageToCanvas helper (line 199) to convert shape coordinates to shader UV space.
Group related uniforms into arrays for efficiency:
// Instead of separate calls:
this.gl.uniform1f(this.u_value1, val1)
this.gl.uniform1f(this.u_value2, val2)
// Use vectors:
this.gl.uniform2f(this.u_values, val1, val2)
Toggle effects based on config:
uniform bool u_enableEffect;
void main() {
vec3 color = u_bgColor;
if (u_enableEffect) {
color = applyEffect(color);
}
fragColor = vec4(color, 1.0);
}
Visualize UV coordinates or other values:
void main() {
// Red-green gradient showing UV space
fragColor = vec4(v_uv.x, v_uv.y, 0.0, 1.0);
}
For more complex examples, see: