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
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:
62
templates/shader/src/config-panel/ConfigPanel.tsx
Normal file
62
templates/shader/src/config-panel/ConfigPanel.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { useCallback } from 'react'
|
||||
import { TldrawUiButton, TldrawUiIcon, useLocalStorageState } from 'tldraw'
|
||||
|
||||
export function ConfigPanel({
|
||||
children,
|
||||
onReset,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onReset: () => void
|
||||
}) {
|
||||
const [isExpanded, setIsExpanded] = useLocalStorageState('shader-config-panel-expanded', false)
|
||||
|
||||
const toggleExpanded = useCallback(() => {
|
||||
setIsExpanded((expanded) => !expanded)
|
||||
}, [setIsExpanded])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`tlui-style-panel__wrapper tlui-style-panel shader-config-panel ${
|
||||
isExpanded ? 'shader-config-panel--expanded' : 'shader-config-panel--collapsed'
|
||||
}`}
|
||||
onWheelCapture={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
{/* Header with collapse/expand button */}
|
||||
<div className="shader-config-panel__header">
|
||||
{isExpanded && (
|
||||
<TldrawUiButton
|
||||
type="normal"
|
||||
onClick={onReset}
|
||||
className="shader-config-panel__reset-button"
|
||||
>
|
||||
Reset
|
||||
</TldrawUiButton>
|
||||
)}
|
||||
<TldrawUiButton
|
||||
type="icon"
|
||||
onClick={toggleExpanded}
|
||||
className="shader-config-panel__toggle"
|
||||
>
|
||||
<TldrawUiIcon icon={<MixerIcon />} small label="Expand / collapse" />
|
||||
</TldrawUiButton>
|
||||
</div>
|
||||
|
||||
{isExpanded && <div className="shader-config-panel__content">{children}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MixerIcon() {
|
||||
return (
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M5.5 3C4.67157 3 4 3.67157 4 4.5C4 5.32843 4.67157 6 5.5 6C6.32843 6 7 5.32843 7 4.5C7 3.67157 6.32843 3 5.5 3ZM3 5C3.01671 5 3.03323 4.99918 3.04952 4.99758C3.28022 6.1399 4.28967 7 5.5 7C6.71033 7 7.71978 6.1399 7.95048 4.99758C7.96677 4.99918 7.98329 5 8 5H13.5C13.7761 5 14 4.77614 14 4.5C14 4.22386 13.7761 4 13.5 4H8C7.98329 4 7.96677 4.00082 7.95048 4.00242C7.71978 2.86009 6.71033 2 5.5 2C4.28967 2 3.28022 2.86009 3.04952 4.00242C3.03323 4.00082 3.01671 4 3 4H1.5C1.22386 4 1 4.22386 1 4.5C1 4.77614 1.22386 5 1.5 5H3ZM11.9505 10.9976C11.7198 12.1399 10.7103 13 9.5 13C8.28967 13 7.28022 12.1399 7.04952 10.9976C7.03323 10.9992 7.01671 11 7 11H1.5C1.22386 11 1 10.7761 1 10.5C1 10.2239 1.22386 10 1.5 10H7C7.01671 10 7.03323 10.0008 7.04952 10.0024C7.28022 8.8601 8.28967 8 9.5 8C10.7103 8 11.7198 8.8601 11.9505 10.0024C11.9668 10.0008 11.9833 10 12 10H13.5C13.7761 10 14 10.2239 14 10.5C14 10.7761 13.7761 11 13.5 11H12C11.9833 11 11.9668 10.9992 11.9505 10.9976ZM8 10.5C8 9.67157 8.67157 9 9.5 9C10.3284 9 11 9.67157 11 10.5C11 11.3284 10.3284 12 9.5 12C8.67157 12 8 11.3284 8 10.5Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export function ConfigPanelBooleanControl({
|
||||
prop,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
prop: string
|
||||
label: string
|
||||
value: boolean
|
||||
onChange: (prop: string, value: boolean) => void
|
||||
}) {
|
||||
return (
|
||||
<label className="shader-config-panel__control shader-config-panel__control--boolean">
|
||||
<span className="shader-config-panel__label">{label}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={value ?? false}
|
||||
onChange={(e) => onChange(prop, e.target.checked)}
|
||||
className="shader-config-panel__boolean-input"
|
||||
/>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
3
templates/shader/src/config-panel/ConfigPanelLabel.tsx
Normal file
3
templates/shader/src/config-panel/ConfigPanelLabel.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export function ConfigPanelLabel({ children }: { children: React.ReactNode }) {
|
||||
return <span className="shader-config-panel__label">{children}</span>
|
||||
}
|
||||
43
templates/shader/src/config-panel/ConfigPanelSlider.tsx
Normal file
43
templates/shader/src/config-panel/ConfigPanelSlider.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { TldrawUiSlider } from 'tldraw'
|
||||
import { ConfigPanelLabel } from './ConfigPanelLabel'
|
||||
|
||||
const STEPS = 100
|
||||
|
||||
export function ConfigPanelSlider({
|
||||
prop,
|
||||
label,
|
||||
min,
|
||||
max,
|
||||
value,
|
||||
onChange,
|
||||
type,
|
||||
}: {
|
||||
prop: string
|
||||
label: string
|
||||
min: number
|
||||
max: number
|
||||
value: number
|
||||
type: 'float' | 'int'
|
||||
onChange: (prop: string, value: number) => void
|
||||
}) {
|
||||
// Map actual value (min to max) to slider value (1 to 10)
|
||||
const sliderValue = value != null ? 1 + ((value - min) / (max - min)) * (STEPS - 1) : 1
|
||||
|
||||
return (
|
||||
<div className="shader-config-panel__control shader-config-panel__control--slider">
|
||||
<ConfigPanelLabel>{label}</ConfigPanelLabel>
|
||||
<TldrawUiSlider
|
||||
steps={STEPS}
|
||||
min={1}
|
||||
value={sliderValue}
|
||||
label={sliderValue.toFixed(0) + '%'}
|
||||
title={label}
|
||||
onValueChange={(sliderValue) => {
|
||||
// Map slider value (1 to 10) back to actual value (min to max)
|
||||
const actualValue = min + ((sliderValue - 1) / (STEPS - 1)) * (max - min)
|
||||
onChange(prop, type === 'float' ? actualValue : Math.round(actualValue))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
239
templates/shader/src/config-panel/config-panel.md
Normal file
239
templates/shader/src/config-panel/config-panel.md
Normal file
@@ -0,0 +1,239 @@
|
||||
# Config Panel
|
||||
|
||||
A collapsible, reusable configuration panel component for building interactive controls for shader templates. The config panel provides a consistent UI for adjusting shader parameters in real-time.
|
||||
|
||||
The config panel system consists of:
|
||||
|
||||
- **ConfigPanel**: Container component with collapse/expand functionality and reset button
|
||||
- **ConfigPanelSlider**: Slider control for numeric values (int or float)
|
||||
- **ConfigPanelBooleanControl**: Checkbox control for boolean values
|
||||
- **ConfigPanelLabel**: Text label component
|
||||
|
||||
All controls are styled to match the tldraw UI and automatically handle state persistence.
|
||||
|
||||
## ConfigPanel Component
|
||||
|
||||
The main container component that wraps all configuration controls.
|
||||
|
||||
### Props
|
||||
|
||||
- `children`: React nodes containing the control components
|
||||
- `onReset`: Callback function to reset configuration to defaults
|
||||
|
||||
### Features
|
||||
|
||||
- **Collapsible**: Click the mixer icon to expand/collapse the panel
|
||||
- **Persistent State**: Panel expanded/collapsed state is saved to localStorage
|
||||
- **Reset Button**: Visible when expanded, calls the `onReset` callback
|
||||
- **Scroll Prevention**: Prevents wheel events from affecting the canvas behind it
|
||||
|
||||
### Usage
|
||||
|
||||
```tsx
|
||||
import { ConfigPanel } from '../config-panel/ConfigPanel'
|
||||
import { resetMyConfig } from './config'
|
||||
|
||||
function MyConfigPanel() {
|
||||
return <ConfigPanel onReset={resetMyConfig}>{/* Add controls here */}</ConfigPanel>
|
||||
}
|
||||
```
|
||||
|
||||
## ConfigPanelSlider
|
||||
|
||||
A slider control for numeric values with automatic range mapping.
|
||||
|
||||
### Props
|
||||
|
||||
- `prop`: Property name (passed to onChange)
|
||||
- `label`: Display label for the slider
|
||||
- `min`: Minimum value
|
||||
- `max`: Maximum value
|
||||
- `value`: Current value
|
||||
- `type`: Either `'float'` or `'int'`
|
||||
- `onChange`: Callback `(prop: string, value: number) => void`
|
||||
|
||||
### Features
|
||||
|
||||
- Automatically maps between slider steps (1-100) and actual value range
|
||||
- Rounds to integers when `type='int'`
|
||||
- Uses tldraw's built-in `TldrawUiSlider` component
|
||||
|
||||
### Usage
|
||||
|
||||
```tsx
|
||||
<ConfigPanelSlider
|
||||
prop="splatRadius"
|
||||
label="Splat Radius"
|
||||
min={0.01}
|
||||
max={1}
|
||||
value={config.splatRadius}
|
||||
type="float"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
```
|
||||
|
||||
## ConfigPanelBooleanControl
|
||||
|
||||
A checkbox control for boolean values.
|
||||
|
||||
### Props
|
||||
|
||||
- `prop`: Property name (passed to onChange)
|
||||
- `label`: Display label for the checkbox
|
||||
- `value`: Current boolean value
|
||||
- `onChange`: Callback `(prop: string, value: boolean) => void`
|
||||
|
||||
### Usage
|
||||
|
||||
```tsx
|
||||
<ConfigPanelBooleanControl
|
||||
prop="paused"
|
||||
label="Paused"
|
||||
value={config.paused}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
```
|
||||
|
||||
## Reactive Configuration Pattern
|
||||
|
||||
The config panel works with tldraw's reactive `atom` system for automatic state management and persistence.
|
||||
|
||||
### Setting Up Config
|
||||
|
||||
```tsx
|
||||
import { atom, react } from 'tldraw'
|
||||
|
||||
// 1. Define default configuration
|
||||
const DEFAULT_CONFIG = {
|
||||
quality: 0.5,
|
||||
paused: false,
|
||||
// ... other properties
|
||||
}
|
||||
|
||||
// 2. Load from localStorage with fallback
|
||||
const STORAGE_KEY = 'my-shader-config'
|
||||
let initialValue = DEFAULT_CONFIG
|
||||
|
||||
try {
|
||||
const value = localStorage.getItem(STORAGE_KEY)
|
||||
if (value) initialValue = JSON.parse(value)
|
||||
} catch {
|
||||
// Use defaults if parse fails
|
||||
}
|
||||
|
||||
// 3. Create reactive atom
|
||||
export const myConfig = atom('my-config', initialValue)
|
||||
|
||||
// 4. Reset function
|
||||
export function resetMyConfig() {
|
||||
myConfig.set(DEFAULT_CONFIG)
|
||||
}
|
||||
|
||||
// 5. Auto-save to localStorage
|
||||
react('save to local storage', () => {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(myConfig.get()))
|
||||
})
|
||||
```
|
||||
|
||||
### Using Config in Component
|
||||
|
||||
```tsx
|
||||
import { useValue } from 'tldraw'
|
||||
import { myConfig } from './config'
|
||||
|
||||
export function MyConfigPanel() {
|
||||
// Subscribe to reactive config changes
|
||||
const config = useValue('config', () => myConfig.get(), [])
|
||||
|
||||
// Update config properties
|
||||
const handleChange = useCallback((prop: string, value: number | boolean) => {
|
||||
myConfig.update((prev) => ({ ...prev, [prop]: value }))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ConfigPanel onReset={resetMyConfig}>
|
||||
<ConfigPanelSlider
|
||||
prop="quality"
|
||||
label="Quality"
|
||||
min={0}
|
||||
max={1}
|
||||
value={config.quality}
|
||||
type="float"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<ConfigPanelBooleanControl
|
||||
prop="paused"
|
||||
label="Paused"
|
||||
value={config.paused}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</ConfigPanel>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
The config panel uses CSS classes that can be customized:
|
||||
|
||||
- `.shader-config-panel`: Main panel container
|
||||
- `.shader-config-panel--expanded`: Applied when expanded
|
||||
- `.shader-config-panel--collapsed`: Applied when collapsed
|
||||
- `.shader-config-panel__header`: Header with reset and toggle buttons
|
||||
- `.shader-config-panel__content`: Content area (only visible when expanded)
|
||||
- `.shader-config-panel__control`: Base class for individual controls
|
||||
- `.shader-config-panel__control--slider`: Modifier for slider controls
|
||||
- `.shader-config-panel__control--boolean`: Modifier for boolean controls
|
||||
- `.shader-config-panel__boolean-input`: Checkbox input element
|
||||
- `.shader-config-panel__label`: Label text for controls
|
||||
|
||||
## Integration with Shader Manager
|
||||
|
||||
The config panel is typically placed in the tldraw component tree and accessed by a shader manager:
|
||||
|
||||
```tsx
|
||||
function MyShaderRenderer({ editor }: { editor: Editor }) {
|
||||
const canvas = useRef<HTMLCanvasElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!canvas.current) return
|
||||
// Initialize shader with reactive config
|
||||
const manager = new MyShaderManager(canvas.current, editor, myConfig)
|
||||
return () => manager.dispose()
|
||||
}, [editor])
|
||||
|
||||
return <canvas ref={canvas} className="shader-app__canvas" />
|
||||
}
|
||||
|
||||
export default function ShaderExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw>
|
||||
<MyShaderRenderer />
|
||||
<MyConfigPanel />
|
||||
</Tldraw>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Group Related Controls**: Organize controls by category (e.g., "General Settings", "Simulation Settings", "Visual Effects")
|
||||
2. **Sensible Ranges**: Choose min/max values that provide useful adjustment without breaking the effect
|
||||
3. **Clear Labels**: Use concise, descriptive labels (abbreviate if needed to fit the UI)
|
||||
4. **Reasonable Defaults**: Set defaults that work well out of the box
|
||||
5. **Persist State**: Always use the reactive atom pattern with localStorage persistence
|
||||
6. **Type Safety**: Define TypeScript interfaces for your config objects
|
||||
7. **Reset Function**: Always provide a reset function for returning to defaults
|
||||
|
||||
## Example: Complete Config Panel Setup
|
||||
|
||||
See `templates/shader/src/fluid/` for a full example implementing:
|
||||
|
||||
- `config.ts`: Reactive atom with localStorage persistence
|
||||
- `FluidConfigPanel.tsx`: Complete configuration UI with 20+ controls
|
||||
- `FluidManager.ts`: Shader manager that reads from the config atom
|
||||
- Integration with WebGL fluid simulation
|
||||
|
||||
This demonstrates the full pattern for creating a configurable shader effect with a professional UI.
|
||||
Reference in New Issue
Block a user