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:

  1. Shader-specific categories — custom params defined by the shader
  2. 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:

  1. If key exists in originalOverrides → compare against that value
  2. Otherwise → compare against paramDef.value (default)
  3. 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:

  1. Default — empty overrides (all params at definition defaults)
  2. Revert — original overrides from composition load (if any exist)
  3. Built-in — from contentDef.presets array in the content JSON
  4. User — stored in localStorage under fantasynth: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 requestAnimationFrame loop 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 uClipAge uniform)
  • Reset Anim — resets elapsed time (shader uTime uniform)
  • 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