- ButtonClip/ButtonInstance, ScriptTrack/ScriptFrame, Marker, Action union - new optional scene collections (buttonClips, buttonInstances, scriptTracks, scriptFrames, markers); normalizeScene initializes them on load - store actions for all new entities, marker name + button label uniqueness, time clamping, cascading deletes (script frames with their track, button instances with their clip), script tracks join trackOrder + setTrackLocked - design doc docs/macro-clips-and-scripts.md - tests cover add/remove/rename/retime paths, uniqueness, sort order, label collisions, and normalization Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
16 KiB
Macro Clips & Scripts — Featureset Plan
Interaktive Buttons plus eine kleine Script-Sprache, mit der Timelines gesteuert werden können.
Getroffene Entscheidungen
- Buttons sind Definitionen + Instanzen (wie normale Clips): einmal definieren, beliebig oft auf Clip-Tracks instanziieren.
pressedist rein visueller State. Aktionen hängen an einemonClickpro Button-Instanz (nicht pro Definition), damit derselbe Button in unterschiedlichen Kontexten andere Dinge tun kann.- Script-Actions feuern nur bei vorwärts laufendem Playback, nicht beim Scrubben und nicht beim Rückwärtslaufen.
Datenmodell
Clip mit Varianten
type Clip = AnimationClip | ButtonClip
interface AnimationClip {
kind: 'animation' // default, bestehendes Verhalten
id: string
name: string
duration: number
layers: Layer[]
}
type ButtonState = 'normal' | 'hover' | 'pressed' | 'disabled'
interface ButtonClip {
kind: 'button'
id: string
name: string
states: Record<ButtonState, Layer[]> // 4 Zellen, jeweils ein Stack
transitions?: StateTransition[]
hitArea?: { width: number; height: number; offsetX?: number; offsetY?: number }
}
interface StateTransition {
from: ButtonState
to: ButtonState
duration: number
easing: Easing
}
Instanz-Erweiterung
interface ButtonInstance extends ClipInstance {
// clipId zeigt auf einen ButtonClip; Trim ist irrelevant
onClick?: Action[]
disabled?: boolean
// runtime-only state, nicht persistiert: hover / pressed / disabled flags
}
Script-Track
interface ScriptTrack {
id: string
name?: string
color?: string | null
locked?: boolean
}
interface ScriptFrame {
id: string
trackId: string
time: number
actions: Action[]
// optional label zum Anzeigen im Timeline-Badge
label?: string
}
Marker
Marker sind benannte Zeit-Bookmarks auf der Haupt-Timeline, die als
Sprungziele dienen — sowohl in der DSL (jump @intro) als auch über
die UI (Doppelklick auf Marker springt dort hin).
interface Marker {
id: string
name: string // muss unique innerhalb der Szene sein (validate!)
time: number
color?: string | null
}
Scoping: Marker leben auf Szenen-Ebene. Innerhalb eines
AnimationClip-Edits sind sie nicht sichtbar/nutzbar (Animation-Clips
haben lokale Zeit). Später evtl. clip.markers ergänzen.
Rendering: Marker erscheinen auf dem Scrub-Ruler oberhalb der Keyframes als kleine Fahnen mit Label. Per Default in der Akzentfarbe, optional individuell einfärbbar. Klick setzt Playhead dorthin, Rechtsklick öffnet Menü (umbenennen, färben, löschen).
Namensregeln: [a-zA-Z_][a-zA-Z0-9_-]*, case-sensitive, unique.
Bei Konflikt zeigt UI einen Fehler, DSL-Parser löst den Namen zur
Laufzeit auf — fehlender Name = Action wird silent no-op (mit Warning
im Dev-Overlay).
Action-Union
type Action =
| { type: 'play' }
| { type: 'pause' }
| { type: 'rewindToLoop' } // Playhead → playbackIn
| { type: 'jumpTo'; time: number } // absolute Zeit
| { type: 'jumpToFrame'; frame: number } // frame = time * fps
| { type: 'jumpToMarker'; name: string } // Marker-Name → time
| { type: 'setButtonState'; instanceId: string; state: ButtonState }
| { type: 'setButtonDisabled'; instanceId: string; disabled: boolean }
| { type: 'script'; source: string } // DSL, Phase 7
Button-Instanz-Referenzen via instanceId (opak) funktionieren im
Code, aber für den User unhandlich. Jede ButtonInstance bekommt
deshalb zusätzlich ein optionales label: string (unique-scoped),
und Action-Editor-Dropdowns zeigen Labels statt IDs. DSL referenziert
btn:label.
Szene-Erweiterung:
scene.scriptTracks?: ScriptTrack[]scene.scriptFrames?: ScriptFrame[]scene.markers?: Marker[]scene.trackOrderkannScriptTrack-IDs enthalten.
Script-Sprache (Phase 7)
Zeilenbasierte Mini-DSL, parsed zu Action[]. Comments beginnen mit
//. Argumente space-separated. Tokens:
| Token | Bedeutung |
|---|---|
1.5 |
Zeit in Sekunden |
#135 |
Frame-Nummer |
@intro |
Marker-Name |
btn:label |
Button-Instanz (über ihr Label) |
play/pause |
Bare-Command |
MVP-Beispiele:
play
pause
rewind // → rewindToLoop
jump 2.5 // Sekunden
jump #135 // Frame
jump @intro // Marker
state btn:muteToggle pressed
enable btn:playButton
disable btn:playButton
// kommentar, wird ignoriert
Erweiterung später (außerhalb MVP): if state(btn:mute) == pressed then ... else ..., Variablen, lokale Labels im Script. Fürs erste
flache Action-Liste.
Parse-Fehler werden pro Frame/Button gesammelt und im Properties-Panel als roter Hinweis angezeigt; Runtime skippt fehlerhafte Frames/Buttons ohne zu crashen.
Runtime
evalAction
Zentrale reine Funktion evalAction(action, runtime), shared zwischen
Editor und Export-Runtime. runtime-Interface:
interface ActionRuntime {
fps: number
playbackIn: number
playbackOut: number
duration: number
markers: Record<string, number> // name → time, vorauflösend
setPlayhead(t: number): void
play(): void
pause(): void
setButtonState(instanceId: string, state: ButtonState): void
setButtonDisabled(instanceId: string, disabled: boolean): void
warn(msg: string): void // für devtools / console
}
evalAction macht Range-Clamping (Zeit in [0, duration]),
Marker-Lookup, und dispatcht auf runtime.*.
Script-Track-Evaluation
Im tick: Frames deren time im Interval (prevPlayhead, nextPlayhead] liegen feuern einmalig, sortiert nach time. Kein
Firing bei:
- Rückwärts-Scrubbing (
nextPlayhead < prevPlayhead) - Loop-Wrap (wenn Playhead von
playbackOutzurück aufplaybackInspringt — das Intervall wird als Reset behandelt) - Manuellen
setPlayheadAufrufen (scrub, keyboard-jump, Marker-Klick)
Das heißt: Firing nur bei natürlichem Vorwärts-tick. lastTickTime
speichert den vorherigen Playhead im Store.
Button-Rendering
- Render den
states[currentState]Layer-Stack. - State-Wechsel → passende Transition suchen (matching
from/to). Wenn vorhanden:interpProgress = 0..1überduration * easing, zwischen den beiden State-Stacks per layer-id matchen und blenden. Wenn kein Match: Cross-Fade zwischen den gesamten Stacks. - Kein Match zwischen
disabledund irgendwas → instant switch (keine Transition für Disable-Toggle).
Button-Pointer-Handling
- Hit-Area: bbox aller sichtbaren Layer im
normal-State (cached), überschreibbar perButtonClip.hitArea. - Event-Logik (State-Machine):
mouseenterbei state=normal→hover.mouseleavebei state=hoveroderpressed→normal(drag-out cancelt den Click — Standard-HTML-Button-Verhalten).mousedownbei state=hover→pressed.mouseupbei state=pressedinnerhalb der Hit-Area →onClickfeuern, dann →hover.- state=
disabledblockt alle Pointer-Events.
setButtonState-Action überschreibt die state-machine (für programmatische Zustandswechsel). Nächster natürlicher Pointer-Event kann wieder übernehmen.disabled-Action setzt persistentstate=disabledbisenable-Action oder manueller Reset.
Editor UI
Sidebar
- „New Button" → Dialog, erstellt
ButtonClipmit vier leeren State-Slots und einem Default-Rect imnormal-State. - Button-Definitionen erscheinen wie normale Clips in der Clipart-Liste, mit kleinem Badge-Icon zur Unterscheidung.
Button-Edit-Modus
- Wie
enterClip, aber mit State-Tabs oben:N / H / P / D. Jeder Tab editiert den eigenen Layer-Stack. Layer-Namen sollten zwischen States matchen, damit Transitions sie paaren können (Info-Tooltip bei mismatch). - Unter den Tabs: Liste der definierten Transitions mit Add/Remove.
- Hit-Area-Overlay als Gizmo (draggable rect) wenn im Button-Edit aktiv.
Properties-Panel — ButtonInstance
- Label-Feld (unique).
onClickAction-Editor: Liste mit Add/Up/Down/Remove, pro Action eine type-spezifische Param-Form.- Disabled-Checkbox (initial state).
Timeline
Script-Track-Row:
- Kleiner als ClipTrack (kein großer Header), Frames als orange Dots
mit Action-Count-Badge (
3bei drei Actions). - Klick auf Frame → Action-Dialog.
- „Add Script Track" im Timeline-Add-Menü.
ButtonClip-Instanz:
- Vier nebeneinanderliegende Zellen (statt einer Leiste), jede ~2×
so breit wie ein normaler ClipBlock-Frame, beschriftet mit
N / H / P / D. - Active-state (zur Laufzeit) wird visuell hervorgehoben.
- Drag/Drop funktioniert auf ganze Instanz — einzelne Zellen sind nicht per Drag verschiebbar.
Marker:
- Scrub-Ruler bekommt einen zweiten Layer über den Ticks für Marker-Fahnen (Dreieck/Flag mit Namen rechts daneben).
- Rechtsklick auf leere Ruler-Fläche → „Add marker here".
- Shortcut
m→ Marker am Playhead hinzufügen, direkt mit Rename-Prompt. - Doppelklick auf Marker = rename, Drag = move, Delete/Backspace (Marker-selektiert) = remove.
- Marker-Klick (einfach) springt Playhead dorthin.
Keyboard-Shortcuts
m— Marker am Playhead.- (bestehend:
i,o,r,space,shift-space.)
Slicing (Abarbeitungs-Reihenfolge)
Slice 1 — Datenmodell + Store
- Types einführen (
Clipals Union mitkind: 'animation' | 'button',ButtonClip,ButtonInstance,ScriptTrack,ScriptFrame,Marker,Action). normalizeScene: setztkind: 'animation'auf bestehende Clips, initialisiert neue Arrays (scriptTracks,scriptFrames,markers), validiert Marker-Namen-Uniqueness.- Store-Actions:
- Button:
addButtonClip,updateButtonClip,addButtonInstance,setButtonInstanceLabel,setButtonOnClick,setButtonDisabled,removeButtonClip. - Script:
addScriptTrack,removeScriptTrack,addScriptFrame,removeScriptFrame,setScriptFrameActions,setScriptFrameTime. - Marker:
addMarker,removeMarker,renameMarker,setMarkerTime,setMarkerColor.
- Button:
- Tests für alle Store-Actions und normalizeScene-Migration.
Slice 2 — Marker-UI
- Scrub-Ruler bekommt Marker-Layer mit Flags + Labels.
- Shortcut
m, Rechtsklick-Menü, Drag/Doppelklick/Delete. - Marker-Panel im Document-Settings (Liste mit Edit).
- Marker sind reine Navigations-Hilfen — Actions kommen erst später, aber das Datenmodell ist bereit.
Slice 3 — Timeline-Rendering (Buttons + Scripts)
- Script-Track-Row (analog zu ClipTrack-Row, eigener Renderer).
- ButtonClip-Instanzen: 4-Zellen-Darstellung.
- Drag/Drop von ButtonClip-Definition auf Clip-Tracks.
- Keine Edit-/Runtime-Features — nur Sichtbarkeit.
Slice 4 — Editor-Dialoge
- Button-Definition-Dialog + Enter/Exit-Button-Edit-Modus mit State-Tabs.
- Hit-Area-Gizmo im Button-Edit.
- Properties-Panel für ButtonInstance (Label, onClick-Editor, Disabled).
- Script-Frame-Dialog (Action-Liste + type-specific Forms).
- Action-Typ-Dropdowns zeigen Marker-Namen + Button-Labels statt IDs.
Slice 5 — Runtime (Editor)
src/lib/actions.tsmitevalAction(action, runtime).useSceneStore.tick: ScriptFrames feuern nach Forward-Only-Regel.- CanvasStage: Pointer-Handler und State-Machine für
Button-Instanzen, onClick-Dispatch via
evalAction. - Buttons rendern den aktuellen State-Stack (ohne Transitions).
Slice 6 — Runtime (Export)
- Port
evalAction+ Button/Script-Handling zuexportPlayer.tsundserver/render.html(shared source via Template-Literal ausactions.ts). - RUNTIME_VERSION bump.
- Export-Tests (headless Eval mit mock runtime).
Slice 7 — Transitions
- Transition-Definitions-UI im Button-Edit.
- Layer-Matching per id/name; Interpolations-Logik in Renderer und Export-Runtime.
- Crossfade als Fallback.
Slice 8 — Script-DSL
- Parser (
src/lib/scriptDsl.ts), klein, selbst-geschrieben, mit Error-Reporting. Actionbekommttype: 'script'→ zur Compile-Zeit (beim Save) in Actions expandiert, Source bleibt für Edit erhalten.- Properties-Panel kriegt Code-Editor (Textarea + Parse-Errors).
- DSL-Tests (Parser + Roundtrip).
Offene Fragen
- Mehrere Script-Tracks parallel? Vermutlich ja, für Organisation. Default: einer wird automatisch angelegt wenn der User die erste Script-Aktion anlegt.
- Button-Hit-Area: automatisch bbox des
normalStates oder muss explizit definiert werden? Vorschlag: bbox als Default, manuell überschreibbar im Button-Edit. - Script-Aktionen bei Loop-Wrap: entschieden → nein, Wrap = Reset, kein Firing.
- Keyboard-Shortcuts für Buttons: nice-to-have, als eigene Action
triggerButtonplus Keymap? Später. - Persistenz der Runtime-States (welcher Button ist aktuell pressed): nein, nicht im Projektfile.
- Button-Toggle-Verhalten: momentary vs toggle? Vorschlag: erstmal
nur momentary, Toggle baut der User per Script
(click → if state==normal then setState pressed else setState normal).
Später evtl. dediziertes
behavior: 'momentary' | 'toggle'Feld. - Marker im Export: sollen Marker in der exportierten Runtime sichtbar sein (z.B. als Overlay im Dev-Mode)? Standardmäßig nein — Marker sind Authoring-Tool, für Runtime nur als Sprungziele relevant.
- Label-Uniqueness für Button-Instanzen: Scene-weit unique oder nur pro ClipTrack? Vorschlag: Scene-weit, damit Scripts aus jedem Kontext referenzieren können.
- Marker als Script-Frame-Ersatz? Ein Marker mit Actions wäre quasi ein Script-Frame. Vorschlag: nein, separate Konzepte — Marker sind passive Sprungziele, Script-Frames sind aktive Aktionen. Beide benutzen aber die gleiche Action-Union.
- jumpTo im Animation-Clip-Context: wenn ein Button in einem
AnimationClip-Edit sitzt und
jumpToausführt — meint das die Haupt-Timeline oder die Clip-Timeline? Vorschlag: immer die Haupt-Timeline (dahin scrollt der Playhead sowieso). Clip-interne Sprünge wären separate Actions.
Testing-Strategie
Jede Slice landet mit Tests:
- Slice 1: Store-Actions komplett abgedeckt, normalizeScene-
Migration (alte Clips →
kind:'animation'), Marker-Uniqueness- Validierung. - Slice 2: Marker-Scrub-Ruler-Interaktion (add/move/delete),
mshortcut. - Slice 3: Timeline-Rendering per Snapshot/DOM-Assertion.
- Slice 4: Dialog-Flow-Tests (Action hinzufügen/entfernen, Typ ändern).
- Slice 5:
evalActionals reine Funktion mit Mock-Runtime — alle Action-Typen abgedeckt; ScriptFrame-Firing-Regeln (Forward/Loop-Wrap/Scrub). - Slice 6: Export-Runtime testet die gleichen Fälle wie Slice 5, aber über generierten JS-Code (Fixture-Based).
- Slice 7: Transition-Interpolation an konkreten Beispielen (layer-match vs crossfade).
- Slice 8: DSL-Parser-Tests (gültige/ungültige Zeilen, Roundtrip
source → actions → source).
Abhängigkeiten & Risiken
exportPlayer.tsundserver/render.htmlwerden deutlich komplexer, weil sie Pointer-Handling und einen Action-Interpreter kriegen. → Shared module insrc/lib/actions.tsdas beide Varianten konsumieren können (Template für Export-Runtime).- Der DSL-Parser muss klein bleiben, sonst frisst das Budget.
- Transitions zwischen States brauchen ein Layer-Matching (id/name) — wenn der User States mit unterschiedlichen Layer-Sets baut, gibt's nichts sauberes zum interpolieren → Fallback crossfade.
- Scene-Schema wächst merklich (Clips als Union, neue Arrays). Alte
Projektdateien müssen via
normalizeScenemigrieren — wir bumpenScene.versionnicht, solange wir defaults korrekt befüllen. Wenn Breaking-Changes nötig werden, dann Version-Bump + explizite Migration. - ButtonClip-States sind im MVP statische Posen (Layer-Stack zu t=0). Intern-animierte States (z.B. pulsierender Hover) sind bewusst raus — könnten später als „State ist selbst ein Mini-Clip mit eigener Duration" ergänzt werden, braucht dann aber Zustands-Scheduling.