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.
- Creates a full-screen quad with a simple shader
- Adapts background color based on tldraw's dark/light mode
- Provides the minimal foundation for building custom shader effects
- config.ts - Configuration with localStorage persistence
- vertex.glsl - Basic vertex shader for full-screen quad
- fragment.glsl - Simple fragment shader outputting solid color
- MinimalShaderManager.ts - Core WebGL setup and render loop incorporating glsl files
- MinimalRenderer.tsx - React component integrating the shader manager
- MinimalConfigPanel.tsx - UI panel for runtime configuration controls
-
In fragment.glsl: Add uniform declaration
uniform float u_myValue;
-
In MinimalShaderManager.ts:
- Add property to store uniform location (line ~14):
private u_myValue: WebGLUniformLocation | null = null
- Get uniform location in
onInitializeafter line 103:this.u_myValue = this.gl.getUniformLocation(this.program, 'u_myValue')
- Set uniform value in
onRender(around line 144):if (this.u_myValue) { this.gl.uniform1f(this.u_myValue, this.config.get().myValue) }
- Add property to store uniform location (line ~14):
-
In config.ts:
- Add property to
ShaderManagerConfiginterface (line ~4):export interface ShaderManagerConfig extends WebGLManagerConfig { count: number myValue: number // Add here }
- Add default value in
DEFAULT_CONFIG(line ~8):myValue: 0.5,
- Add property to
-
In MinimalConfigPanel.tsx:
- Add slider range to
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 panel will automatically generate a slider for the new value
- Add slider range to
The fragment shader (fragment.glsl) is where visual effects happen:
- Input:
v_uv- normalized UV coordinates (0-1) from vertex shader - Output:
fragColor- RGBA color for the pixel - Current behavior: Outputs solid color from
u_bgColoruniform
Example 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:
- rainbow/ - Animated color effects with multiple uniforms
- fluid/ - Interactive fluid simulation with pointer tracking
- shadow/ - Shape-aware rendering using tldraw editor data