# Macro Clips & Scripts — Featureset Plan Interaktive Buttons plus eine kleine Script-Sprache, mit der Timelines gesteuert werden können. ## Getroffene Entscheidungen 1. Buttons sind **Definitionen + Instanzen** (wie normale Clips): einmal definieren, beliebig oft auf Clip-Tracks instanziieren. 2. `pressed` ist **rein visueller State**. Aktionen hängen an einem `onClick` pro Button-**Instanz** (nicht pro Definition), damit derselbe Button in unterschiedlichen Kontexten andere Dinge tun kann. 3. Script-Actions feuern **nur bei vorwärts laufendem Playback**, nicht beim Scrubben und nicht beim Rückwärtslaufen. ## Datenmodell ### Clip mit Varianten ```ts 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 // 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 ```ts 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 ```ts 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). ```ts 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 ```ts 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.trackOrder` kann `ScriptTrack`-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: ```ts interface ActionRuntime { fps: number playbackIn: number playbackOut: number duration: number markers: Record // 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 `playbackOut` zurück auf `playbackIn` springt — das Intervall wird als Reset behandelt) - Manuellen `setPlayhead` Aufrufen (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` über `duration * easing`, zwischen den beiden State-Stacks per layer-id matchen und blenden. Wenn kein Match: Cross-Fade zwischen den gesamten Stacks. - Kein Match zwischen `disabled` und irgendwas → instant switch (keine Transition für Disable-Toggle). ### Button-Pointer-Handling - Hit-Area: bbox aller sichtbaren Layer im `normal`-State (cached), überschreibbar per `ButtonClip.hitArea`. - Event-Logik (State-Machine): - `mouseenter` bei state=`normal` → `hover`. - `mouseleave` bei state=`hover` oder `pressed` → `normal` (drag-out cancelt den Click — Standard-HTML-Button-Verhalten). - `mousedown` bei state=`hover` → `pressed`. - `mouseup` bei state=`pressed` innerhalb der Hit-Area → `onClick` feuern, dann → `hover`. - state=`disabled` blockt 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 persistent `state=disabled` bis `enable`-Action oder manueller Reset. ## Editor UI ### Sidebar - „New Button" → Dialog, erstellt `ButtonClip` mit vier leeren State-Slots und einem Default-Rect im `normal`-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). - `onClick` Action-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 (`3` bei 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 (`Clip` als Union mit `kind: 'animation' | 'button'`, `ButtonClip`, `ButtonInstance`, `ScriptTrack`, `ScriptFrame`, `Marker`, `Action`). - `normalizeScene`: setzt `kind: '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`. - 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.ts` mit `evalAction(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 zu `exportPlayer.ts` und `server/render.html` (shared source via Template-Literal aus `actions.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. - `Action` bekommt `type: '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 1. **Mehrere Script-Tracks parallel?** Vermutlich ja, für Organisation. Default: einer wird automatisch angelegt wenn der User die erste Script-Aktion anlegt. 2. **Button-Hit-Area:** automatisch bbox des `normal` States oder muss explizit definiert werden? Vorschlag: bbox als Default, manuell überschreibbar im Button-Edit. 3. **Script-Aktionen bei Loop-Wrap:** entschieden → nein, Wrap = Reset, kein Firing. 4. **Keyboard-Shortcuts für Buttons:** nice-to-have, als eigene Action `triggerButton` plus Keymap? Später. 5. **Persistenz der Runtime-States** (welcher Button ist aktuell pressed): nein, nicht im Projektfile. 6. **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. 7. **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. 8. **Label-Uniqueness für Button-Instanzen:** Scene-weit unique oder nur pro ClipTrack? Vorschlag: Scene-weit, damit Scripts aus jedem Kontext referenzieren können. 9. **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. 10. **jumpTo im Animation-Clip-Context:** wenn ein Button in einem AnimationClip-Edit sitzt und `jumpTo` ausfü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), `m` shortcut. - **Slice 3:** Timeline-Rendering per Snapshot/DOM-Assertion. - **Slice 4:** Dialog-Flow-Tests (Action hinzufügen/entfernen, Typ ändern). - **Slice 5:** `evalAction` als 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.ts` und `server/render.html` werden deutlich komplexer, weil sie Pointer-Handling und einen Action-Interpreter kriegen. → Shared module in `src/lib/actions.ts` das 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 `normalizeScene` migrieren — wir bumpen `Scene.version` nicht, 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.