snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-23 16:12:52 +05:00
commit a774ac673a
4541 changed files with 770499 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
import { useCallback } from 'react'
import { useValue } from 'tldraw'
import { ConfigPanel } from '../config-panel/ConfigPanel'
import { ConfigPanelBooleanControl } from '../config-panel/ConfigPanelBooleanControl'
import { ConfigPanelSlider } from '../config-panel/ConfigPanelSlider'
import { resetShaderConfig, shaderConfig } from './config'
const SLIDER_CONFIGS: Record<string, { min: number; max: number }> = {
quality: { min: 0.1, max: 1 },
stepSize: { min: 1, max: 50 },
steps: { min: 1, max: 100 },
}
export function RainbowConfigPanel() {
const config = useValue('config', () => shaderConfig.get(), [])
const handleChange = useCallback((prop: string, value: number | boolean) => {
shaderConfig.update((prev) => ({ ...prev, [prop]: value }))
}, [])
return (
<ConfigPanel onReset={resetShaderConfig}>
{Object.entries(config).map(([prop, value]) => {
if (typeof value === 'number') {
const sliderConfig = SLIDER_CONFIGS[prop] || { min: 0, max: 1 }
return (
<ConfigPanelSlider
key={prop}
prop={prop}
label={prop}
min={sliderConfig.min}
max={sliderConfig.max}
value={value}
type="float"
onChange={handleChange}
/>
)
}
if (typeof value === 'boolean') {
return (
<ConfigPanelBooleanControl
key={prop}
prop={prop}
label={prop}
value={value}
onChange={handleChange}
/>
)
}
return null
})}
</ConfigPanel>
)
}

View File

@@ -0,0 +1,25 @@
import { memo, useLayoutEffect, useRef } from 'react'
import { useEditor } from 'tldraw'
import { WebGLCanvas } from '../WebGLCanvas'
import { shaderConfig } from './config'
import { RainbowShaderManager } from './RainbowShaderManager'
export const RainbowRenderer = memo(() => {
const editor = useEditor()
const rCanvas = useRef<HTMLCanvasElement>(null)
useLayoutEffect(() => {
const manager = new RainbowShaderManager(editor, rCanvas.current!, shaderConfig)
const handlePointerMove = (e: PointerEvent) => manager.pointerMove(e.clientX, e.clientY)
window.addEventListener('pointermove', handlePointerMove)
return () => {
window.removeEventListener('pointermove', handlePointerMove)
manager.dispose()
}
}, [editor])
return <WebGLCanvas ref={rCanvas} config={shaderConfig} />
})

View File

@@ -0,0 +1,299 @@
import { Atom, Box, Editor, Group2d, react, TLShape, Vec } from 'tldraw'
import { WebGLManager } from '../WebGLManager'
import { ShaderManagerConfig } from './config'
import FRAGMENT_SHADER from './fragment.glsl?raw'
import VERTEX_SHADER from './vertex.glsl?raw'
export type Geometry = Array<{ start: Vec; end: Vec }>
export class RainbowShaderManager extends WebGLManager<ShaderManagerConfig> {
private program: WebGLProgram | null = null
private positionBuffer: WebGLBuffer | null = null
private vao: WebGLVertexArrayObject | null = null
private u_resolution: WebGLUniformLocation | null = null
private u_darkMode: WebGLUniformLocation | null = null
private u_quality: WebGLUniformLocation | null = null
private u_zoom: WebGLUniformLocation | null = null
private u_stepSize: WebGLUniformLocation | null = null
private u_steps: WebGLUniformLocation | null = null
private u_offset: WebGLUniformLocation | null = null
private u_segments: WebGLUniformLocation | null = null
private u_segmentCount: WebGLUniformLocation | null = null
private u_pointerPos: WebGLUniformLocation | null = null
private geometries: Geometry[] = []
private maxSegments: number = 2000
private pointer: Vec = new Vec(0.5, 0.5)
constructor(
editor: Editor,
canvas: HTMLCanvasElement,
config: Atom<ShaderManagerConfig, unknown>
) {
super(editor, canvas, config)
this.initialize()
}
onInitialize = (): void => {
this.maxSegments = Math.floor(Math.min(512, 2000 / this.getConfig().quality))
if (!this.gl) {
console.error('No WebGL context available')
return
}
if (this.gl.isContextLost()) {
console.error('WebGL context is lost, cannot initialize')
return
}
const maxTextureSize = this.gl.getParameter(this.gl.MAX_TEXTURE_SIZE)
if (!maxTextureSize) {
console.error('WebGL context appears invalid (max texture size is null)')
return
}
const gl = this.gl
const compileShader = (type: number, source: string): WebGLShader | null => {
const shader = gl.createShader(type)
if (!shader) {
console.error('Failed to create shader - createShader returned null')
return null
}
gl.shaderSource(shader, source)
gl.compileShader(shader)
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS) !== true) {
const shaderType = type === gl.VERTEX_SHADER ? 'VERTEX' : 'FRAGMENT'
const log = gl.getShaderInfoLog(shader)
console.error(`${shaderType} shader compile error:`, log || 'No error log available')
console.error('Shader source:', source)
gl.deleteShader(shader)
return null
}
return shader
}
const vertexShader = compileShader(this.gl.VERTEX_SHADER, VERTEX_SHADER)
if (!vertexShader) {
console.error('Failed to compile vertex shader, aborting initialization')
return
}
const fragmentShader = compileShader(this.gl.FRAGMENT_SHADER, FRAGMENT_SHADER)
if (!fragmentShader) {
console.error('Failed to compile fragment shader, aborting initialization')
this.gl.deleteShader(vertexShader)
return
}
this.program = this.gl.createProgram()
if (!this.program) {
console.error('Failed to create program')
return
}
this.gl.attachShader(this.program, vertexShader)
this.gl.attachShader(this.program, fragmentShader)
this.gl.linkProgram(this.program)
if (!this.gl.getProgramParameter(this.program, this.gl.LINK_STATUS)) {
console.error('Program link error:', this.gl.getProgramInfoLog(this.program))
return
}
this.u_resolution = this.gl.getUniformLocation(this.program, 'u_resolution')
this.u_darkMode = this.gl.getUniformLocation(this.program, 'u_darkMode')
this.u_quality = this.gl.getUniformLocation(this.program, 'u_quality')
this.u_zoom = this.gl.getUniformLocation(this.program, 'u_zoom')
this.u_stepSize = this.gl.getUniformLocation(this.program, 'u_stepSize')
this.u_steps = this.gl.getUniformLocation(this.program, 'u_steps')
this.u_offset = this.gl.getUniformLocation(this.program, 'u_offset')
this.u_segments = this.gl.getUniformLocation(this.program, 'u_segments')
this.u_segmentCount = this.gl.getUniformLocation(this.program, 'u_segmentCount')
this.u_pointerPos = this.gl.getUniformLocation(this.program, 'u_pointerPos')
this.positionBuffer = this.gl.createBuffer()
this.vao = this.gl.createVertexArray()
this.gl.bindVertexArray(this.vao)
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer)
const positions = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1])
this.gl.bufferData(this.gl.ARRAY_BUFFER, positions, this.gl.STATIC_DRAW)
const a_position = this.gl.getAttribLocation(this.program, 'a_position')
this.gl.enableVertexAttribArray(a_position)
this.gl.vertexAttribPointer(a_position, 2, this.gl.FLOAT, false, 0, 0)
this.gl.bindVertexArray(null)
this.disposables.add(react('dependencies', this.tick))
this.tick()
}
onUpdate = (): void => {
const { editor } = this
const vsb = editor.getViewportScreenBounds()
const camera = editor.getCamera()
this.geometries = []
for (const shape of editor.getCurrentPageShapes()) {
try {
const geometry = this.extractGeometry(shape, camera, vsb)
if (geometry) {
this.geometries.push(geometry)
}
} catch (e) {
console.log(`Error extracting geometry for shape: ${shape.type}`, e)
}
}
}
onFirstRender = (): void => {
if (!this.gl || !this.program) return
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height)
this.gl.clearColor(0, 0, 0, 0)
this.gl.enable(this.gl.BLEND)
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA)
this.gl.useProgram(this.program)
}
onRender = (_deltaTime: number, _currentTime: number): void => {
if (!this.gl || !this.program) return
this.gl.clear(this.gl.COLOR_BUFFER_BIT)
const isDarkMode = this.editor.user.getIsDarkMode()
const { quality, stepSize, steps, offset } = this.getConfig()
if (this.u_resolution) {
this.gl.uniform2f(this.u_resolution, this.canvas.width, this.canvas.height)
}
if (this.u_darkMode) {
this.gl.uniform1f(this.u_darkMode, isDarkMode ? 1.0 : 0.0)
}
if (this.u_quality) {
this.gl.uniform1f(this.u_quality, quality)
}
if (this.u_zoom) {
this.gl.uniform1f(this.u_zoom, this.editor.getZoomLevel())
}
if (this.u_stepSize) {
this.gl.uniform1f(this.u_stepSize, stepSize)
}
if (this.u_steps) {
this.gl.uniform1i(this.u_steps, steps)
}
if (this.u_offset) {
this.gl.uniform1f(this.u_offset, offset)
}
if (this.u_pointerPos) {
this.gl.uniform2f(this.u_pointerPos, this.pointer.x, this.pointer.y)
}
const allSegments: number[] = []
for (const geometry of this.geometries) {
for (const segment of geometry) {
if (allSegments.length < this.maxSegments * 4) {
allSegments.push(segment.start.x, segment.start.y, segment.end.x, segment.end.y)
}
}
}
const farAway = 999999
while (allSegments.length < this.maxSegments * 4)
allSegments.push(farAway, farAway, farAway, farAway)
if (this.u_segments) {
this.gl.uniform4fv(this.u_segments, allSegments)
}
if (this.u_segmentCount) {
this.gl.uniform1f(this.u_segmentCount, Math.min(allSegments.length / 4, this.maxSegments))
}
this.gl.bindVertexArray(this.vao)
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4)
this.gl.bindVertexArray(null)
}
onDispose = (): void => {
if (this.gl) {
if (this.vao) {
this.gl.deleteVertexArray(this.vao)
this.vao = null
}
if (this.positionBuffer) {
this.gl.deleteBuffer(this.positionBuffer)
this.positionBuffer = null
}
if (this.program) {
this.gl.deleteProgram(this.program)
this.program = null
}
}
}
pointerMove = (x: number, y: number): void => {
const vsb = this.editor.getViewportScreenBounds()
this.pointer.x = (x - vsb.x) / vsb.width
this.pointer.y = 1.0 - (y - vsb.y) / vsb.height
this.tick()
}
private pageToCanvas = (
point: Vec,
camera: { x: number; y: number; z: number },
viewportScreenBounds: Box
): Vec => {
const canvasX = ((point.x + camera.x) * camera.z) / viewportScreenBounds.width
const canvasY = 1.0 - ((point.y + camera.y) * camera.z) / viewportScreenBounds.height
return new Vec(canvasX, canvasY)
}
private extractGeometry = (
shape: TLShape,
camera: { x: number; y: number; z: number },
viewportScreenBounds: Box
): Geometry | null => {
const geometry = this.editor.getShapeGeometry(shape)
const transform = this.editor.getShapePageTransform(shape)
if (!transform) return null
const canvasPoints = geometry.vertices.map((c) =>
this.pageToCanvas(transform.applyToPoint(c), camera, viewportScreenBounds)
)
const segments: Geometry = []
for (let i = 0; i < canvasPoints.length - 1; i++) {
segments.push({ start: canvasPoints[i], end: canvasPoints[i + 1] })
}
const isClosed =
geometry instanceof Group2d
? (geometry.children[0]?.isClosed ?? true)
: 'isClosed' in geometry
? geometry.isClosed
: true
if (isClosed && canvasPoints.length > 0) {
segments.push({
start: canvasPoints[canvasPoints.length - 1],
end: canvasPoints[0],
})
}
return segments
}
}

View File

@@ -0,0 +1,38 @@
import { atom, react } from 'tldraw'
import { WebGLManagerConfig } from '../WebGLManager'
export interface ShaderManagerConfig extends WebGLManagerConfig {
stepSize: number
steps: number
offset: number
}
export const DEFAULT_CONFIG: ShaderManagerConfig = {
startPaused: true,
quality: 0.5,
stepSize: 10,
steps: 10,
offset: 0,
pixelate: true,
}
const STORAGE_KEY = 'shader-rainbow-config-1'
let initialValue = DEFAULT_CONFIG
try {
const value = localStorage.getItem(STORAGE_KEY)
if (value) initialValue = JSON.parse(value)
} catch {
// noop
}
export const shaderConfig = atom<ShaderManagerConfig>('shader-config', initialValue)
export function resetShaderConfig() {
shaderConfig.set(DEFAULT_CONFIG)
}
react('save to local storage', () => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(shaderConfig.get()))
})

View File

@@ -0,0 +1,172 @@
#version 300 es
precision mediump float;
precision mediump int;
in vec2 v_uv;
out vec4 fragColor;
uniform vec2 u_resolution;
uniform float u_darkMode;
uniform float u_quality;
uniform float u_zoom;
uniform float u_stepSize;
uniform int u_steps;
uniform float u_offset;
uniform vec2 u_pointerPos;
// Constants
#define MAX_SEGMENTS 512
const float HASH_SEED_X = 127.1;
const float HASH_SEED_Y = 311.7;
const float HASH_MULTIPLIER = 43758.5453;
const float HUE_RANGE = 6.0;
const float HUE_TRANSITION = 2.0;
const float SMOOTHING_FACTOR = 3.0;
const int FBM_OCTAVES = 4;
const float FBM_AMPLITUDE_START = 0.5;
const float FBM_AMPLITUDE_DECAY = 0.5;
const float FBM_FREQUENCY_START = 1.0;
const float FBM_FREQUENCY_GROWTH = 2.0;
const float CLAMP_MIN = 0.0;
const float CLAMP_MAX = 1.0;
const float DARK_MODE_BRIGHTNESS = 0.8;
const float LIGHT_MODE_DESATURATION = 0.1;
const vec3 LUMINANCE_WEIGHTS = vec3(0.299, 0.587, 0.114);
const float QUINTIC_POWER = 5.0;
const float MAX_INIT_DISTANCE = 1000.0;
// Geometry data
uniform vec4 u_segments[MAX_SEGMENTS]; // xy = start, zw = end
uniform float u_segmentCount;
// Simple noise function
float hash(vec2 p) {
return fract(sin(dot(p, vec2(HASH_SEED_X, HASH_SEED_Y))) * HASH_MULTIPLIER);
}
// Rotate through full rainbow spectrum based on step count
vec3 rotateHue(float step, float steps, float offset) {
float t = mod(step + offset * steps, steps) / steps;
float hue = t * HUE_RANGE; // 0-6 range for six color transitions
float x = 1.0 - abs(mod(hue, HUE_TRANSITION) - 1.0);
if (hue < 1.0) return vec3(1.0, x, 0.0); // Red to Yellow
else if (hue < 2.0) return vec3(x, 1.0, 0.0); // Yellow to Green
else if (hue < 3.0) return vec3(0.0, 1.0, x); // Green to Cyan
else if (hue < 4.0) return vec3(0.0, x, 1.0); // Cyan to Blue
else if (hue < 5.0) return vec3(x, 0.0, 1.0); // Blue to Magenta
else return vec3(1.0, 0.0, x); // Magenta to Red
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (SMOOTHING_FACTOR - 2.0 * f);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
// Fractal Brownian Motion for more organic noise
float fbm(vec2 p) {
float value = 0.0;
float amplitude = FBM_AMPLITUDE_START;
float frequency = FBM_FREQUENCY_START;
for (int i = 0; i < FBM_OCTAVES; i++) {
value += amplitude * noise(p * frequency);
amplitude *= FBM_AMPLITUDE_DECAY;
frequency *= FBM_FREQUENCY_GROWTH;
}
return value;
}
// Find closest point on line segment to point p
vec2 closestPointOnSegment(vec2 p, vec2 a, vec2 b) {
vec2 pa = p - a;
vec2 ba = b - a;
float h = clamp(dot(pa, ba) / dot(ba, ba), CLAMP_MIN, CLAMP_MAX);
return a + ba * h;
}
// Ease out quintic for smooth falloff
float easeOutQuint(float t) {
return 1.0 - pow(1.0 - t, QUINTIC_POWER);
}
void main() {
// Convert UV to pixel coordinates
vec2 pixelPos = v_uv * u_resolution;
float minDist = MAX_INIT_DISTANCE;
vec2 closestPoint = vec2(0.0);
// Find actual closest point on any segment
for (int i = 0; i < MAX_SEGMENTS; i++) {
if (float(i) >= u_segmentCount) break;
vec4 segment = u_segments[i];
vec2 start = segment.xy * u_resolution;
vec2 end = segment.zw * u_resolution;
vec2 pointOnSegment = closestPointOnSegment(pixelPos, start, end);
float dist = distance(pixelPos, pointOnSegment);
if (dist < minDist) {
minDist = dist;
closestPoint = pointOnSegment;
}
}
// Proximity-based rainbow
// Scale by quality and zoom to maintain consistent appearance
float maxDistance = u_stepSize * float(u_steps) * u_quality * u_zoom;
if (minDist > maxDistance) {
fragColor = vec4(0.0, 0.0, 0.0, 0.0);
return;
}
// Apply pointer erosion effect
vec2 pointerPixelPos = u_pointerPos * u_resolution;
float pointerDist = distance(pixelPos, pointerPixelPos);
float pointerRadius = 100.0 * u_zoom; // Radius of effect in pixels
// Only affect pixels within the pointer radius
if (pointerDist < pointerRadius) {
// Erosion strength falls off from center to edge
float erosionStrength = smoothstep(pointerRadius, 0.0, pointerDist);
// Subtract from geometry distance, creating erosion effect
minDist = max(0.0, minDist - erosionStrength * maxDistance * 0.3);
}
// Quantize distance into discrete bands
float steps = float(u_steps);
float bandIndex = floor(minDist / (maxDistance/steps));
minDist = bandIndex * (maxDistance/steps);
float proximity = smoothstep(maxDistance, 0.0, minDist);
// Generate rainbow color for this band using rotateHue
vec3 rainbowColor = rotateHue(bandIndex, steps, u_offset);
// Adjust colors for dark mode
if (u_darkMode > 0.5) {
// In dark mode, reduce brightness and increase saturation
rainbowColor = rainbowColor * DARK_MODE_BRIGHTNESS;
} else {
// In light mode, slightly desaturate for better visibility
rainbowColor = mix(rainbowColor, vec3(dot(rainbowColor, LUMINANCE_WEIGHTS)), LIGHT_MODE_DESATURATION);
}
// Opacity with ease out quintic for smooth falloff
float alpha = easeOutQuint(proximity);
fragColor = vec4(rainbowColor, alpha);
}

View File

@@ -0,0 +1,325 @@
# Rainbow Shader Example
A WebGL shader that renders colorful rainbow distance fields around tldraw shapes, creating discrete color bands that adapt to dark/light mode.
## What This Example Does
- Extracts geometry from all tldraw shapes in real-time
- Calculates distance fields from shape edges
- Renders rainbow color bands based on distance
- Adapts colors for dark/light mode
- Provides on-demand rendering for performance
## Key Files
- **[config.ts](config.ts)** - Configuration with localStorage persistence
- **[vertex.glsl](vertex.glsl)** - Standard full-screen quad vertex shader
- **[fragment.glsl](fragment.glsl)** - Distance field shader with rainbow color generation
- **[RainbowShaderManager.ts](RainbowShaderManager.ts)** - Shape geometry extraction and WebGL rendering
- **[RainbowRenderer.tsx](RainbowRenderer.tsx)** - React component integrating the shader manager
- **[RainbowConfigPanel.tsx](RainbowConfigPanel.tsx)** - UI panel for runtime configuration controls
## How It Works
### Shape Geometry Extraction
1. **Reactive Updates** (RainbowShaderManager.ts:150): Uses tldraw's `react()` to subscribe to shape changes, camera changes, and theme changes
2. **Extract All Shapes** (line 156): `onUpdate` iterates through all shapes on the current page
3. **Get Geometry** (line 290): Uses `editor.getShapeGeometry()` to get shape vertices
4. **Transform to Canvas Space** (lines 272-282, 294-295):
- Apply shape transform matrix to vertices
- Convert from page coordinates to screen coordinates
- Normalize to canvas UV space (0-1 range)
5. **Build Segments** (lines 298-318): Connect vertices into line segments, handling closed vs open shapes
### Distance Field Calculation
The fragment shader (fragment.glsl) calculates distance from each pixel to the nearest shape edge:
1. **Convert UV to Pixels** (line 103): `pixelPos = v_uv * u_resolution`
2. **Find Nearest Segment** (lines 109-123):
- Loop through all segments (up to `maxSegments`)
- Use `closestPointOnSegment` to find closest point on each segment
- Track minimum distance across all segments
3. **Quantize Distance** (lines 139-140): Floor distance into discrete bands for stepped effect
4. **Generate Color** (line 144): Use `rotateHue` function to get rainbow color for the band
5. **Apply Alpha** (line 156): Smooth falloff using ease-out quintic function
### Rainbow Color Generation
The `rotateHue` function (fragment.glsl:47-58) cycles through the full spectrum:
- Red → Yellow → Green → Cyan → Blue → Magenta → Red
- Each band gets a color based on its index
- `offset` parameter rotates colors along the spectrum
## Configuration
### Performance Settings
- **quality** (0.1-1.0): Resolution multiplier
- Affects `maxSegments` calculation: `Math.floor(Math.min(512, 2000 / quality))`
- Lower quality = supports more shapes
- Default: 0.5
### Visual Parameters
- **stepSize** (1-50): Distance between color bands in pixels
- Larger values = wider bands
- Default: 10
- **steps** (1-100): Number of color bands to render
- More steps = rainbow extends further from shapes
- Default: 10
- **offset** (0-1): Color rotation offset
- Animating this value rotates the rainbow
- Default: 0
### Rendering Options
- **pixelate**: Apply pixelated rendering style
- **startPaused**: Render on-demand instead of continuously (default: true)
## How to Modify This Example
### Changing Color Schemes
The `rotateHue` function (fragment.glsl:47-58) can be modified for different color schemes:
**Warm colors only**:
```glsl
vec3 rotateHue(float step, float steps, float offset) {
float t = mod(step + offset * steps, steps) / steps;
return mix(vec3(1.0, 0.0, 0.0), vec3(1.0, 1.0, 0.0), t); // Red to Yellow
}
```
**Custom gradient**:
```glsl
vec3 rotateHue(float step, float steps, float offset) {
float t = mod(step + offset * steps, steps) / steps;
vec3 color1 = vec3(0.2, 0.5, 1.0); // Light blue
vec3 color2 = vec3(1.0, 0.2, 0.5); // Pink
return mix(color1, color2, t);
}
```
### Adjusting Distance Bands
Modify the quantization (fragment.glsl:139-140):
**Smooth gradient instead of bands**:
```glsl
// Remove these lines:
// float bandIndex = floor(minDist / (maxDistance/steps));
// minDist = bandIndex * (maxDistance/steps);
// Use continuous distance:
float t = minDist / maxDistance;
vec3 rainbowColor = rotateHue(t * steps, steps, u_offset);
```
**Non-uniform band spacing**:
```glsl
float bandIndex = pow(minDist / maxDistance, 2.0) * steps; // Exponential spacing
```
### Adding Noise Effects
The shader includes `fbm` (Fractal Brownian Motion) function (lines 74-86):
```glsl
void main() {
// ... existing distance calculation ...
// Add noise to distance
float noiseValue = fbm(pixelPos * 0.01);
minDist += noiseValue * 5.0;
// ... rest of shader ...
}
```
### Animating the Effect
Use the time parameter in `onRender`:
```typescript
onRender = (deltaTime: number, currentTime: number): void => {
// ... existing code ...
// Auto-rotate colors
const animatedOffset = (currentTime * 0.1) % 1.0
if (this.u_offset) {
this.gl.uniform1f(this.u_offset, animatedOffset)
}
// Pulse effect
const pulseStepSize = 10 + Math.sin(currentTime * 2) * 5
if (this.u_stepSize) {
this.gl.uniform1f(this.u_stepSize, pulseStepSize)
}
}
```
### Filtering Shapes
Modify `onUpdate` to process only certain shapes (RainbowShaderManager.ts:155):
```typescript
onUpdate = (): void => {
const shapes = this.editor.getCurrentPageShapes()
this.geometries = []
for (const shape of shapes) {
// Filter by type
if (shape.type === 'geo' || shape.type === 'draw') {
const geometry = this.extractGeometry(shape, camera, vsb)
if (geometry) {
this.geometries.push(geometry)
}
}
}
}
```
### Custom Geometry Processing
Extend `extractGeometry` for shape-specific effects (line 284):
```typescript
private extractGeometry = (
shape: TLShape,
camera: { x: number; y: number; z: number },
viewportScreenBounds: Box
): Array<{ start: Vec; end: Vec }> | null => {
// Get base geometry
const segments = /* ... existing code ... */
// Add extra segments for specific shape types
if (shape.type === 'draw') {
// Sample more points from draw shapes for smoother curves
// ... custom sampling logic ...
}
return segments
}
```
### Adding New Uniforms
1. **In fragment.glsl**: Add uniform declaration
```glsl
uniform float u_brightness;
```
2. **In RainbowShaderManager.ts**:
- Add property (line ~25):
```typescript
private u_brightness: WebGLUniformLocation | null = null
```
- Get location in `onInitialize` (after line 127):
```typescript
this.u_brightness = this.gl.getUniformLocation(this.program, 'u_brightness')
```
- Set value in `onRender` (around line 213):
```typescript
if (this.u_brightness) {
this.gl.uniform1f(this.u_brightness, this.getConfig().brightness)
}
```
3. **In config.ts**: Add to interface and defaults (lines 4-17)
4. **In RainbowConfigPanel.tsx**: Add slider range (line 8)
## Performance Considerations
### Segment Limits
The shader has a maximum segment limit to stay within WebGL uniform limits:
```typescript
this.maxSegments = Math.floor(Math.min(512, 2000 / quality))
```
- Lower quality = more segments allowed
- Segments beyond limit are clipped (line 219)
- Consider simplifying geometry for complex shapes
### On-Demand Rendering
The example uses `startPaused: true` (config.ts:11) for better performance:
- Renders only when shapes change
- Set to `false` for continuous animation
- Call `tick()` manually to trigger renders
### Optimization Tips
- Reduce `quality` to process fewer pixels
- Lower `steps` to reduce color calculations
- Simplify `rotateHue` if using solid colors
- Cache geometry if shapes don't change frequently
## Common Patterns
### Conditional Rendering by Distance
```glsl
void main() {
// ... distance calculation ...
// Different effects based on distance
if (minDist < 10.0) {
fragColor = vec4(1.0, 1.0, 1.0, 1.0); // White core
} else if (minDist < 50.0) {
fragColor = vec4(rainbowColor, 1.0); // Solid rainbow
} else {
fragColor = vec4(rainbowColor, alpha); // Faded rainbow
}
}
```
### Shape-Specific Colors
Pass additional data through a separate uniform array:
```typescript
// In RainbowShaderManager.ts
const allColors: number[] = []
for (const geometry of this.geometries) {
// Extract color from shape
const color = getShapeColor(shape)
allColors.push(color.r, color.g, color.b)
}
this.gl.uniform3fv(this.u_colors, allColors)
```
### Debug Visualization
Visualize the distance field directly:
```glsl
void main() {
// ... distance calculation ...
// Grayscale visualization of distance
float visualDist = minDist / maxDistance;
fragColor = vec4(vec3(visualDist), 1.0);
}
```
## Next Steps
For other shader patterns, see:
- **minimal/** - Basic shader template with minimal features
- **fluid/** - Interactive fluid simulation responding to shape movements
- **shadow/** - Shape-aware shadows with soft falloff

View File

@@ -0,0 +1,15 @@
#version 300 es
precision mediump float;
precision mediump int;
// Constants
const float UV_SCALE = 0.5;
const float UV_OFFSET = 0.5;
in vec2 a_position;
out vec2 v_uv;
void main() {
v_uv = a_position * UV_SCALE + UV_OFFSET;
gl_Position = vec4(a_position, 0.0, 1.0);
}