Clip Editor
Technical reference for the two-panel clip editing system. Covers parameter rendering, presets, previews, undo, and engine integration.
Architecture
The clip editor is a two-panel UI orchestrated by ClipEditPanel. It delegates to specialized managers that read and mutate a shared ClipEditPanelState container.
ClipEditPanel
│
├── ClipEditPanelState Shared state container
│
├── ClipMetadataRenderer Clip name, color, trigger, layer
├── VariantManager Variant accordion + CRUD
├── ContentManager Content list + add/remove/reorder
├── ParameterRenderer Dynamic parameter form generation
├── PresetManager Built-in + user preset data layer
├── PresetUIManager Preset dropdown UI + actions
├── InlinePreviewManager 128×128 Three.js shader preview
├── ParamControllerManager MIDI/dashboard controller binding
└── UndoManager Toggle-based parameter undo
Two-Panel Layout
Lifecycle
Opening
controlPanel.onClipEdit(layerId, clipId, clip)
→ Load content definitions for first enabled variant
→ Load model descriptors (await if needed)
→ Resolve track info (loop length, quantize)
→ editPanel.show(clip, contentDefs, layerId, layerIndex, layerCount, trackInfo)
show() stores clip data in state, auto-selects the first enabled variant, stores original overrides for all content entries, and calls render().
Closing
hide() disables fullscreen preview, disposes chaser preview, removes .visible, resets state, and fires onClose().
Track Editing
showTrack(track, trackId, loopLength, quantize) sets editMode = 'track' and renders track metadata (loop length, quantize, BPM) instead of clip metadata.
Parameter System
Input Types
Parameters are rendered by ParameterRenderer, which delegates to ParameterInputFactory for type-specific inputs.
| Type | Input Component | Notes |
|---|---|---|
| numeric | Text input + inline slider | Fill bar, drag-to-adjust, min/max/step |
| boolean | Icon toggle | Phosphor check/x icons |
| vec2/vec3/vec4 | Component sliders | Per-axis with x/y/z/w labels |
| color | HTML color picker | Converts [r,g,b] (0-1) ↔ hex |
| select | Dropdown or icon buttons | Icon mode if options have icon property |
| curve | CurveEditor | Interactive spline with presets, add/remove points |
| texture | Async dropdown | Loads from textures/index.json |
| font | Async dropdown | Loads from textures/fonts/index.json |
| model | Async dropdown | Loads from models/index.json |
| string | Text input | Free-form text, not a GLSL uniform |
| modifierStack | Stack editor | Collapsible blocks, drag reorder, type picker |
| modelStack | Stack editor | Model items with scale/rotation/pivot params |
| display | Read-only span | Shows live-computed value from engine |
Animatable Numeric Inputs
A numeric param with an ${key}Animation sibling select param renders as an animatable input:
[━━━━━━━●━━━] 2.5 [○ ∿ △ ⊿ □] ← slider + animation type icons
(none, sine, triangle, saw, square)
min: [━━●━━] 0.2 period: [━━━●] 4.0 ← sub-params (2×2 grid)
max: [━━━━●] 1.0 phase: [●━━━] 0.0 visible when animated
Sub-params follow the ${key}Min, ${key}Max, ${key}Period, ${key}Phase convention. Phase is only shown for sine animation.
Parameter Categories
Content definitions can organize params into named categories that render as collapsible sections. Two groups are separated visually:
- Shader-specific categories — custom params defined by the shader
- Feature categories — standard params from the feature system (Blending, Color, World Space, etc.)
Each category section has:
- Clickable header with expand/collapse arrow
- Optional header toggle (if first param is a boolean without
visibleWhen) - Optional icon (from
contentDef.categoryIcons) - Layout support for multi-column rows (via
contentDef.layouts)
Category expand/collapse state is cached per content ref across content switches within a session.
Parameter Visibility
Params can declare visibleWhen conditions that show/hide them based on other param values:
{ "visibleWhen": { "mode": "advanced" } }
{ "visibleWhen": { "intensity": [0, 1, 2] } }
{ "visibleWhen": { "scale": { "gte": 0.5, "lt": 10 } } }
- Multiple conditions use AND logic
- Supports equality, array-OR, and comparison operators (
gte,lte,gt,lt) - Re-evaluated after every parameter change
Modified Labels
Parameter labels show a visual indicator when their value differs from the composition baseline. Comparison logic:
- If key exists in
originalOverrides→ compare against that value - Otherwise → compare against
paramDef.value(default) - Deep equality via
JSON.stringify
Right-Click Revert
Right-clicking a parameter control reverts it to its composition baseline value:
- If the key exists in
originalOverrides, restores that value - Otherwise, deletes the override (falls back to
paramDef.value)
Right-clicking a parameter label opens the controller binding menu instead.
Live Parameters
Parameters with "live": true in their definition update the running content without full reactivation:
Param change
├─ live: true → onLiveParamChange(key, value, layerId, contentIndex)
│ → contentSystem.setSceneParam() — instant uniform update
└─ live: false → _notifyClipChange()
→ contentSystem.activateFromClip() — full reactivation
Non-live params on scene content show a warning icon indicating the scene will restart.
Variant Management
Variants are alternative parameter configurations for a clip. The left panel renders them as a draggable accordion.
Operations:
- Select — click variant header; loads content definitions and re-renders right panel
- Create — deep-copies current variant with new ID and name
- Delete — requires at least one variant to remain
- Reorder — drag-and-drop between accordion items
- Expand/Collapse — click header arrow; selected variant is always expanded
Each variant contains a content array — the list of content entries (shader, scene, postprocess, etc.) that activate together.
Content Management
The content list within each variant shows all content entries. Each item displays an enabled toggle, type icon, name, and delete button.
Operations:
- Select — click to select; right panel re-renders with selected content's parameters
- Add — content picker dropdown showing available content definitions
- Remove — delete button on each item
- Reorder — drag-and-drop within the list
- Copy/Paste — shift+right-click to copy, shift+click to paste after
Content ref changes (adding new content or changing a reference) trigger onContentRefChange, which loads the new content definition and reactivates the clip.
Preset System
Data Layer (PresetManager)
Presets are ordered:
- Default — empty overrides (all params at definition defaults)
- Revert — original overrides from composition load (if any exist)
- Built-in — from
contentDef.presetsarray in the content JSON - User — stored in
localStorageunderfantasynth:presets:${contentId}
UI Layer (PresetUIManager)
The preset row renders:
- Dropdown with all available presets (separators between sections)
- Add button — prompts for name, saves current values as user preset
- Delete button — only for user presets (and built-in presets in dev mode)
Applying a preset replaces all paramOverrides with the preset's params, captures an undo snapshot, updates all previews, and re-renders the parameter form.
Inline Preview
A pooled 128×128 Three.js renderer for live shader preview, managed by InlinePreviewManager.
Rendering:
- Orthographic camera, fullscreen plane with shader material
- Material built from content definition's GLSL source + current param uniforms
- Runs own
requestAnimationFrameloop independent of the main engine - Simulates time, beat, and clip age using configurable BPM (default 120)
Controls:
- Play/Pause — toggles animation loop, preserves accumulated time
- Reset Age — resets clip age timer (shader
uClipAgeuniform) - Reset Anim — resets elapsed time (shader
uTimeuniform) - Loop — wraps clip age for repeating lifecycle effects
Param sync: When a parameter changes in the editor, updateInlinePreviewParam(key, value) sets the corresponding uniform on the preview material.
Fullscreen Preview
Toggling fullscreen preview sends onPreviewToggle(enabled, contentDef, overrides) to the engine, which renders the shader at full viewport size. Parameter changes sync to both the inline and fullscreen previews simultaneously.
Chaser Preview
For content with a "Chaser" category, a ChaserGridPreview renders an isometric 3D grid visualization of the chaser activation pattern. It replicates the GLSL chaser logic in JavaScript and animates in sync with the beat clock.
Undo System
Design
Toggle-based snapshot — one level of undo/redo. Captures the full paramOverrides object before the first change in a session, then Ctrl+Z toggles between current and snapshot state.
This is intentionally lightweight for a VJ context where the primary need is quick "oops" recovery, not a full history tree.
Snapshot Capture
First parameter change after open/undo
→ captureSnapshot(contentIndex)
→ Deep clone content.paramOverrides via JSON.parse/stringify
→ Store as { variantIndex, contentIndex, overrides }
→ Set undoShouldCapture = false (don't re-capture until toggle)
The undoShouldCapture flag ensures only the first change in a session captures a snapshot. Subsequent changes accumulate without re-capturing — Ctrl+Z reverts to the state before the entire editing session.
Toggle (Ctrl+Z / Cmd+Z)
Ctrl+Z pressed (panel visible)
→ Verify snapshot variant/content indices match current selection
→ Clone current paramOverrides as swap state
→ Restore snapshot → content.paramOverrides = snapshot.overrides
→ Store swap state as new snapshot (for redo toggle)
→ Notify clip change (reactivates content with restored params)
→ Re-render parameter form with restored values
→ Set undoShouldCapture = true (next change starts new session)
Pressing Ctrl+Z again toggles back to the pre-undo state (redo).
Scope
Covered:
| Action | How it's captured |
|---|---|
| Parameter value change | Snapshot before first _handleParamChange |
| Modifier add/remove/reorder | Entire modifier array is a paramOverride |
| Model stack add/remove | Same mechanism as modifiers |
| Preset application | Captures snapshot before replacing overrides |
| Right-click revert | Captures snapshot, then sets undoShouldCapture = true |
Not covered:
| Action | Reason |
|---|---|
| Variant add/remove/reorder | Structural change, not a parameter override |
| Content add/remove/reorder | Structural change |
| Content ref change | Triggers content reload, not a param change |
| Clip metadata (name, color, trigger) | Not stored in paramOverrides |
Invalidation
- Switching content index or variant index does not clear the snapshot — it's preserved
- Ctrl+Z silently does nothing if current indices don't match the snapshot's
- Switching back to the original content/variant makes undo available again
- Opening a new clip (
resetForNewClip) clears the snapshot and resets the capture flag
Engine Integration
Callback API
ClipEditPanel exposes callbacks wired by EditPanelWiring.js:
Clip lifecycle:
| Callback | Purpose |
|---|---|
onClipChange(clip, overrides, layerId, variantIndex) |
Parameter mutated — reactivate clip |
onVariantChange(clip, variantIndex, layerId) |
Variant selected — load new content |
onContentRefChange(clip, variantIndex, contentRef, layerId) |
Content added/changed |
onClipLayerChange(clip, oldLayerId, newLayerIndex) |
Clip moved to different layer |
Live updates:
| Callback | Purpose |
|---|---|
onLiveParamChange(key, value, layerId, contentIndex) |
Update shader uniform directly |
onGetDisplayValue(layerId, contentIndex, key) |
Read live value from running content |
onSceneTextureChange(layerId, key, texture) |
Texture loaded for scene content |
onModelReload(contentIndex, paramKey, modelPath) |
Model selection changed |
onModelStackReload(contentIndex, paramKey, items) |
Model stack changed |