Refactor + new tests triggered by an explicit coverage audit of the
macro-clips feature set.
- extracted blendButtonStates into src/lib/buttonBlend.ts so it can be
tested without mounting CanvasStage; the component now delegates to
it. Behaviour unchanged.
- store coverage: removeMarker / setMarkerTime / setMarkerColor on
unknown ids are no-ops, addMarker accepts an initial color,
removeScriptFrame / setScriptFrameActions / setScriptFrameTime on
unknown ids are no-ops, removeButtonInstance keeps the clip,
addButtonInstance defaults x/y to stage center,
setButtonInstancePosition writes through, setButtonInstanceLabel('')
clears, setButtonClipTransitions stores + deletes.
- button runtime: applyButtonStateChange is a no-op when the target
equals the current state, honors the persisted disabled flag as the
from-state, reset() clears transitions alongside state/disabled.
store.actionRuntime warns on unknown button refs.
- blendButtonStates: matched layer lerps props (incl. fill color),
unmatched from-layer fades out, unmatched to-layer fades in,
u=0/u=1 boundary conditions, missing asset id resolves are skipped.
- DSL edge cases: empty source, comments-only, trailing blank lines,
wrong state ref prefix, disable without arg, jump @ without name.
- tick firing robustness: multi-track frames fire in time order,
actions within a frame fire in list order, scriptFramesInRange
open-closed boundary is exact.
Macro-clips suites now total 76 tests across 11 groups; grand total
277 tests in the runner.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers the end-to-end flows per slice: markers (m shortcut + scrub-
ruler interactions), script tracks/frames (open dialog, action types,
forward-only firing rules), button clip creation and drag-to-track,
ButtonInstance dialog (label, onClick, disabled, transitions), canvas
runtime (pointer state machine, drag-out cancel behavior, scripted
state flips), transitions (matched layer blend vs crossfade, easing),
the DSL language reference with examples, a quick sanity pass for the
HTML export runtime, and which in-app test suites to look for.
Also documents the two known limitations (no in-app button state
authoring, scripts not baked into the server PNG/MP4 render) and a
small devtools cheat sheet for poking the pinia store directly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Headline features across 11 slices (see individual commits for details):
- Markers on the scrub ruler (name, color, drag, m shortcut, used as
jump targets in scripts/actions)
- Script tracks with action frames, full action authoring dialog with
a reusable ActionEditor component
- Button clips (definitions + placed instances), 4-cell timeline
rendering, sidebar authoring, drag-to-track, per-instance label and
onClick editing
- Button canvas runtime: hover/pressed/disabled state machine, onClick
execution via evalAction, visual state changes and state-to-state
transitions with per-transition duration + easing
- Action union: play/pause/rewindToLoop/jumpTo/jumpToFrame/
jumpToMarker/setButtonState/setButtonDisabled/script
- Script DSL parser for the `script` action: play/pause/rewind,
jump to seconds/#frame/@marker, state btn:label <state>,
enable/disable btn:label, // comments
- Forward-only script frame firing tied to natural playback tick (no
firing on scrub or loop wrap)
- Export runtime (exportPlayer.ts + server renderer) mirrors the
editor: evalAction, button rendering with canvas-space hit testing,
transitions, DSL parsing. RUNTIME_VERSION bumped from 1 → 6.
Known gap: buttons can be created and dragged onto tracks but their
per-state visuals can't be customized in-app yet (the seed gives a
four-colour rect per state). State-authoring mode is a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ButtonClip transitions are now editable in ButtonInstanceDialog
(from/to/duration/easing) and persisted on the clip via the new
store.setButtonClipTransitions action; transitions are shared by
every instance of the button (clip-scoped, not instance-scoped).
- lerpProps exported from interpolate.ts so callers outside the
sampler can blend two poses.
- useButtonRuntime tracks an active transition per instance plus a
reactive transitionTick that pokes Vue computeds each frame.
A self-driving RAF loop runs only while some transition is active
(idles when none), so animations work paused or playing.
- applyButtonStateChange(id, to, transitions, persistedDisabled)
is the one entry point for state changes — it looks up a matching
from→to transition and starts it, or flips state instantly. Pointer
handlers in CanvasStage and store.actionRuntime.setButtonState both
go through it.
- CanvasStage blends the two state stacks via sampleStateLayers +
blendStates: layers matched by name interpolate their sampled props
with lerpProps; unmatched layers fade in/out. Renderer uses the
blended layer list in place of the pure state stack during transit.
- Export runtime mirrors the same flow: applyButtonChange starts
transitions, sampleButtonTransition drives per-frame progress, and
drawButtonStatesBlended composites via drawLayer(layer, 0, override).
drawLayer gained an optional propsOverride param for this.
RUNTIME_VERSION bumped to 6.
- Tests cover applyButtonStateChange matching logic and
sampleTransition's time → u mapping plus done flag.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ButtonInstance gains optional x/y; new instances default to stage
center and setButtonInstancePosition updates them
- useButtonRuntime composable holds per-instance state/disabled
overrides (runtime-only, not persisted). getState layers the
runtime disabled override on top of the persisted initial flag.
- store.actionRuntime's setButtonState/setButtonDisabled now drive
useButtonRuntime instead of mutating the scene, so script-triggered
state changes stay ephemeral
- CanvasStage renders each visible (playhead >= startTime) button
instance inside a <v-group> at (bi.x, bi.y), drawing the current
state's layer stack with draggable/listening disabled plus a
transparent hit-rect sized to the clip's hitArea or the bbox of
the first rect/circle layer. The group wires mouseenter/leave/
down/up into the state machine and fires onClick via evalAction.
- exportPlayer runtime mirrors the same thing: getEffectiveButtonState,
per-frame buttonHits array, canvas-level mousemove/down/up handlers
do their own hit testing in scene space. RUNTIME_VERSION bumped to 5.
- tests cover useButtonRuntime defaults/overrides and the
store.actionRuntime → runtime-state roundtrip via label resolution.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- store.seedDefaultButtonClip(name) adds a button clip with a single
shared rect asset and four pre-populated state stacks in blue shades
(normal/hover/pressed/disabled). Layer name 'Face' is consistent so
transitions can later pair layers by name.
- AssetSidebar gets a Buttons section below Clips: create via prompt,
rename, delete, drag-to-track. Drag uses a new dataTransfer type
'application/x-animatr-button-clip' to keep it distinct from
animation-clip drops.
- Timeline clip-lane drop accepts both types and dispatches
addClipInstance / addButtonInstance accordingly; the new-track
drop zone handles both too, so dragging a button to an empty area
creates a fresh clip track + instance in one step.
- i18n for the sidebar strings and three dialogs.
- test covers seedDefaultButtonClip: four populated states, shared
asset, consistent layer name.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- src/lib/scriptDsl.ts: small line-based parser producing Action[] plus
a list of ParseError{line,message}. Handles: play/pause/rewind,
jump <sec> | #<frame> | @<marker>, state btn:<label> <state>,
enable/disable btn:<label>, // comments, skips blanks
- evalAction for type:'script' parses the source and recursively
dispatches sub-actions; errors surface via rt.warn
- ActionRuntime.setButtonState/setButtonDisabled are documented as
taking an instance id OR a label; the store's actionRuntime builds
a label→id map and resolves refs before applying
- exportPlayer template gets a matching parseScriptSource plus
resolveButtonRef; evalAction-in-engine mirrors the TS version
- RUNTIME_VERSION bumped to '4'
- tests: parser covers commands, comments, jump variants, state/enable
errors, unknown commands; two integration tests verify that script
actions dispatch via evalAction and that parse errors don't block
valid actions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ButtonInstanceDialog.vue edits the three instance-scope fields:
label (with uniqueness error surface), disabled flag, onClick
actions via the shared ActionEditor
- commit waits on a successful label set so collisions show inline
instead of silently dropping the rename
- Timeline wires click on a button-instance block to open the dialog
Creation/placement UX (sidebar entry + drag-to-track) and the per-state
layer editor are still deferred — this slice only makes existing
instances editable once something (devtools, later slice) has put them
on a clip track.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- exportPlayer engine refactored from elapsed-since-start to an explicit
currentT / lastNow / paused model so actions can jump, pause, and
resume cleanly
- every frame: advance currentT by dt, fire script frames whose time
sits in (prev, next], sorted; on loop wrap, fire frames up to rangeOut
then snap to rangeIn without firing across the wrap
- evalAction mirrors src/lib/actions.ts (play/pause/rewindToLoop/
jumpTo/jumpToFrame/jumpToMarker/setButtonState/setButtonDisabled/
script); marker map built from scene.markers; unknown markers warn
via window.console
- RUNTIME_VERSION bumped to '3' so old cached engines don't kick in
Server render.html (PNG-sequence path) stays untouched — scripted
interactivity is a runtime-only concern, the non-interactive server
export should not bake it in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- new src/lib/actions.ts:
- ActionRuntime interface (fps, range, markers, setPlayhead/play/pause,
setButtonState/setButtonDisabled, warn)
- evalAction dispatches each action kind, with clamping to [0, duration]
for time-based jumps and a warning path for unknown markers and for
script actions (DSL lands in slice 8)
- scriptFramesInRange helper returns frames in the open-closed interval
(prev, next], sorted
- store wires it up:
- tick captures prevPlayhead, advances, then fires script frames that
fall in (prev, landed]. Scrubs via setPlayhead never touch this path
so the forward-only guarantee falls out of the architecture.
- actionRuntime() builds an ActionRuntime bound to this store
- setButtonState is a no-op in the editor for now (waits for the
button-runtime slice); setButtonDisabled goes to the store action,
rewindToLoop/play/pause/jumps mutate the store directly
- tests: 10 evaluator tests against a mock ActionRuntime covering all
action kinds + marker map + scriptFramesInRange; 3 integration tests
for tick firing (ordering, no-fire-on-scrub, firing-at-playbackOut)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ActionEditor.vue — reusable list editor for Action[]: add/remove/reorder,
per-type parameter forms (jump to time/frame/marker, set button
state/disabled, script source). Marker and button dropdowns pull from
the store's current scene.
- ScriptFrameDialog.vue — modal wrapping ActionEditor plus time and label
inputs; commits on close, supports deletion.
- Timeline wires the dialog: clicking a script frame opens it for edit,
clicking the empty script lane creates a new frame at that time and
opens the editor.
- i18n keys for all action types, parameters, and dialog labels.
Button-authoring UI (definitions + instance placement + properties panel)
is its own slice (4b) — this commit ships script authoring only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- TimelineScriptTrack added to the track union; tracks computed emits one
per ScriptTrack in trackOrder, with the show-locked filter honoring its
locked flag
- script track row: drag handle + PhCode name badge + lock button; lane
renders each ScriptFrame as an orange dot with optional action-count
badge
- button instances render inside clip-track lanes as 4 cells labeled
N/H/P/D, colored per state, with a striped pattern on disabled and a
fixed visual width per cell
- new scrub-bar button to add a script track (PhCode icon)
- type narrowing tightened so inGroup checks skip script/group tracks
explicitly rather than relying on the negation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- marker flags render on the scrub ruler with color, label, and a dimmed
vertical guideline; z-index sits below brackets and playhead
- click marker to jump the playhead, drag to retime (snaps), double-click
to rename via prompt dialog, 'm' adds a marker at the playhead and
opens the rename prompt
- rename collision shows a dialog and loops back into the prompt
- selectedMarkerId tracks the active marker; Delete/Backspace removes it,
Escape clears the selection
- document keydown now gates m/Delete/Backspace on editable targets but
keeps Escape universal
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
- background video layer (document-level), syncs to playhead in editor and fullscreen
- playback in/out brackets on the scrub ruler; i/o set them, r rewinds to in-point
- togglePlay, tick, fullscreen loop, WebM + server FFmpeg export all honor [in, out]
- top-level mask layers cascade over everything below until a group boundary
- fullscreen preview slaves localTime to the bg video so it stops stuttering
- welcome "new project" now actually loads a blank scene (was a no-op)
- default canvas background hardened to white via normalizeScene
- new keyframe button (+) in the name-spacer, backspace/delete removes selected kfs
- drag-end auto-selects the created/shifted keyframe so the path is visible
- animation path updates live during drag and spans every selected keyframe
- track rows show an icon per asset kind (rect/circle/image/text/polygon/freeform)
- default mask mode is negative, buttons read − then +
- sticky track-name columns raised above playhead/brackets; orange bracket styling
- tests for playback range, blank scene, mask defaults, cascading top-level masks
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Timeline: new show/hide toggle next to snap; locked tracks (including
layer, clip, and the new group lock) are filtered from the timeline
when the toggle is off. Preference lives in localStorage and is
shared across components via a module-level ref.
- Groups become lockable via the same inline lock toggle. A locked
group propagates to its children: they behave as effectively locked
in the timeline and on the stage. Collapse/expand still works on a
locked group.
- Canvas: layers in locked+hidden tracks render as a 1px outline in
the layer (or fill) color instead of their full fill, and are
draggable:false / listening:false. Mask layers keep their dashed
ghost styling; masks still apply in the preview overlay regardless
of lock or show state.
- drawSceneFrame gains an optional `{ crop }` option; the mask-preview
overlay passes the editor's crop toggle through, so the two controls
are fully independent. Crop changes now also trigger a preview
redraw via a watcher dep.
- Tests: new suites for show-locked persistence, group lock store
action, crop option on drawSceneFrame (including mask+crop combos).
Properties transparent-toggle tests now reset the section prefs
localStorage so the Appearance section is guaranteed open when the
panel mounts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Track lock becomes a small inline icon toggle sitting next to the
track label, replacing the static badge and removing the context
menu's lock/unlock entry.
- Locked tracks are fully inert: the lane and name have pointer-events
disabled, only the lock button itself stays clickable. The pile of
hover-neutralizing CSS rules is gone.
- Lock button is icon-only (no background/border box) and swaps to the
opposite icon on hover (filled unlock when hovering a locked track,
so the click affordance is obvious).
- Locked tracks in groups keep their indentation — the drag-handle
switches from display:none to visibility:hidden so the reserved
space still aligns with unlocked sibling layers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pentagon clip-path on the scrub handle head (flat top, straight sides
down to 55% height, triangle point at the bottom center) so the head
visually funnels into the 1px playhead needle below. box-shadow
replaced with a drop-shadow filter that respects the clip-path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Track-name resize strip starts below the scrub bar and sits on the
name-column side of the boundary, so scrubbing at time=0 is no
longer intercepted by the resize hit area.
- Playhead (lane) and scrub handle get z-index 20 so they stay on top
of the sticky track-name column (z-index 5) and the resize strip.
The playhead stays fully visible at time=0 and when the lane is
scrolled under the sticky column.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Clipboard paste creates an image layer: a document-level paste
listener detects image items in the clipboard (Strg+V after a
screenshot, e.g.) and imports them via the shared helper. Skipped
when focus is in a real text input / textarea / contenteditable.
- Shared importImageFile() helper in fileLoader.ts replaces the
duplicated image-import logic in AssetSidebar and CanvasStage's
clipart-drop path. Optional position argument for drop-at-point.
- Clipart folder persistence hardened for reloads:
- initialize() is now safely re-runnable (no-ops only once a handle
is actually loaded, tolerates refresh failures so the folder name
survives even if permission isn't auto-granted)
- App.vue's onMounted kicks off initialize() so it runs regardless
of whether the drawer component has mounted
- DocumentSettings re-invokes initialize() each time the modal
opens — covers app-start races and gives the user gesture needed
for FS Access permission prompts
- Opening the drawer refreshes thumbnails when handle is set but
items are empty, so the user-gesture context restores permission
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All checkboxes and radio buttons in the Properties panel are now
icon-toggle buttons:
- Polygon "Closed path" → PhPolygon button (fill weight when closed)
- Fill/Stroke transparent → PhEye / PhEyeSlash toggle next to the swatch
- Mask use → PhMaskHappy toggle
- Mask mode → segmented control with PhPlus / PhMinus (negative is red)
Shared .icon-toggle style: 26×26, panel background, accent when active,
optional .danger variant. Tests updated to click buttons instead of
checkboxes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Rename the app to RUWIO: title, APP_NAME, topbar logo (40px PNG),
welcome dialog header and about dialog switch from the PhSparkle
glyph to the real logo. Redundant name text next to the logo is
removed across the board.
- About dialog: logo spans the full dialog width, the bottom Close
button is gone (close via header X stays).
- Vertex bezier for polygons: each vertex can be Corner / Curve / Split
curve via right-click. Curve mirrors tangent handles symmetrically,
split curve keeps them independent. Tangents are fully animatable
per keyframe — lerp mirrored across all four render pipelines.
- Stage path overlay: selecting a keyframe draws the tween arc between
the previous and selected keyframe with draggable per-frame dots that
curve the path via quadratic bezier control points. Other segments
on the same track render as dashed ghost lines so the full curve is
visible. Path dots grow + show grab cursor on hover.
- Vertex box-select: click-drag on empty stage while a polygon is
selected boxes vertices. Shift-click toggles individual vertices
into/out of the selection. Multi-vertex drag moves all selected at
once; right-click on a selected vertex applies Corner/Curve/Split
to all. Tangent handles only render when exactly one vertex is
selected.
- Welcome dialog focus: opens with the most recent project focused.
Enter opens it; Tab cycles only through the dialog's real buttons
(delete icons pulled out of tab order, scrollable body removed from
tab order). Export-current removed from welcome (not useful on
startup).
- Modal focus trap: all modals now keep Tab inside the dialog.
- Fill / stroke transparency toggle restores the last real color when
flipped back on, with a layer-base / asset fallback chain so there's
always a visible color to restore to. A watcher keeps the restore
cache in sync with the sampled value.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Clipart library: new left-edge drawer (5px idle, expands on hover,
400px when open). User picks a local folder via the File System Access
API in settings; directory handle is persisted in IndexedDB so the
library survives reloads. Dragging a thumbnail onto the stage creates
an image asset + layer at the drop point.
- Animation paths on the stage: selecting a keyframe draws the tween
path from the previous keyframe to the selected one as a solid
polyline with per-frame draggable dots. All other segments on the
same position-track render as dashed ghost lines so the full curve
progression is visible.
- Path dots grow on hover (2.5 → 6 px), show a grab cursor, and have a
wider hit area so they're easier to grab.
- Bezier-curved position interpolation: keyframes gain optional
ctrlX/ctrlY fields. When set, x/y tween along a quadratic bezier
through the control point instead of a straight line. Dragging a
path dot solves for the control so the curve passes through the
drag point at its frame progress. Mirrored across all four render
pipelines.
- Tests: new suites for polygon actions, freeform drawing lifecycle,
polygon + stroke rendering (15 new tests). Fixed the mask-render
tests that relied on a default-transparent canvas background.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Freeform drawing tool: new sidebar button enters a click-to-draw mode
that appends points, shows a live ghost preview line, and exits via
Enter or double-click (near the start point closes the path). Works
end-to-end through all four render pipelines.
- Polygon authoring gestures: double-click on a path inserts a vertex on
the nearest edge; double-click on an open polygon's endpoint resumes
the drawing session from that end, reversing point order when needed.
- Stroke + closed-path properties: all shape types (rect, circle, text,
polygon) expose stroke color, stroke width, and (for polygons) a
closed toggle. Fill and stroke each support a "transparent" switch so
shapes can render as outline-only or fill-only.
- Mask preview no longer inherits the user's stroke — the dashed ghost
keeps its own outline regardless of stroke-width changes.
- Playhead no longer dims on inactive timeline tracks; only keyframes,
segments and clip blocks fade.
- Properties panel point-count control for polygons, plus per-keyframe
points animation that correctly interpolates once a baseline exists.
- Pivot drag hides the rotation handle until release.
- Transformer attach retries on a short timer to avoid races where the
Konva node hasn't mounted yet when selection changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Polygon asset type with per-vertex animation: new sidebar button inserts a
hexagon; vertex handles in the editor write per-keyframe `points` arrays
that interpolate between keyframes (requires matching point counts).
- Canvas mask preview: toggle in the zoom toolbar switches the stage
between the editable ghost view and a rendered view that applies the
actual masking via the shared canvas 2D path.
- Properties panel is grouped into collapsible sections (Transform, Text,
Appearance, Shadow, Mask) with per-section open/closed state persisted
to localStorage.
- Sidebar shape buttons are icon-only to reclaim width for the Polygon
entry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>