Canvas zoom + pan, crop / mask-preview / stage-preview / centre-
crosshair overlays, and timeline snap / show-locked / displayMode
/ px-per-sec / auto-fit now ride along in the saved project under
a new optional `editor` block. Reopening a project restores the
workbench exactly how the user left it — zoom level, pan offset,
HUD toggles, timeline density — instead of snapping back to
defaults.
Editor state lives on the store as a sibling of `this.project`
rather than inside it, so `JSON.stringify(store.project)` (the
undo/redo snapshot) still only captures content. The new
`projectForSave` getter stitches the two on the way to disk;
`loadProject` reads them back apart on the way in. Saves through
App.vue, the rename flow, and IDB recents all go through
`projectForSave`.
CanvasStage skips its first auto-fit when a non-default
canvas zoom/pan was restored so the user's exact view survives
mount. Frame-based export options pick up the same source of
truth for a consistent starting point.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First stable release. Highlights in this bump:
- Group context menu gains a proper danger "Delete group" entry
that tears down the group AND every track it contains (layers,
clip tracks, script tracks, audio tracks) along with their
dependent content. "Ungroup" keeps its old meaning as a separate
non-danger item, so the two operations stop being confusable.
New removeTrackGroupWithChildren store action clears any
selection pointing inside the group before dispatching per-kind
removes.
- APNG preview page now renders the export at its native pixel
size: the .stage + <img> use explicit width/height in pixels
instead of a max-960 aspect-ratio box with object-fit: contain.
A 4K export looks 4K; the body scrolls if the window is
smaller.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Optional centre crosshair: dashed horizontal + vertical lines
through the effective canvas centre, togglable from the HUD next
to the crop / mask-preview / stage-preview buttons. Scales with
zoom and stays visible inside clip-edit.
- Locked clip tracks now behave as if they weren't on the stage —
the ci-group is listening:false + draggable:false, so selection,
drag, and transform all pass through. deselectIfLocked also
clears a clip-instance selection that just got locked out
(directly or via a parent group).
- Eye toggle stays clickable on locked layer / clip-track rows —
previous pointer-events:none rule only whitelisted the lock
button. Hide-in-editor is orthogonal to lock.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Track groups get the same editor-hide affordance as layer and clip
tracks: new eye button on the group header, setGroupEditorHidden
action (clears selection pointing inside the group), and a cascade
through the Konva stackItems + render.ts so every child track in a
hidden group vanishes in the editor.
Track renaming via right-click context menu now covers layers and
clip tracks too — setLayerName / setClipTrackName actions, new i18n
strings, and end-to-end UI tests driving the menu → promptDialog →
rename flow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each clip instance now renders an invisible frame rect sized to the
union of its layers' footprints across every keyframe. The
transformer wraps that frame so its handles stay on the clip's own
outer constraints instead of chasing the currently-visible inner
shape, and the rect doubles as a hit region so empty-playhead
frames remain selectable. A plain click on a timeline clip-block
also selects the instance (drag still takes over past the 2px
dead-zone).
Clip tracks pick up the same editor-hide affordance as layer
tracks: new eye button on the track-name, setClipTrackEditorHidden
action, and filters in the Konva stackItems + render.ts so
hidden-track instances vanish in the editor. ExportDialog's
skip-hidden opt-in now also strips hidden clip tracks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Right-click an SVG-backed layer to split it into one layer per
top-level drawable. Each part keeps the original stage position
and ends up as a same-size standalone SVG.
Also: delete confirm now floats above the welcome dialog (topmost
z-index + scoped ESC/Tab via modal stack) and autofocuses its
primary button so Enter triggers confirm instead of bubbling back
to the underlying row.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Right-clicking the stage opens a small menu with Document settings
(pops the document drawer) and Resize stage. Closes on outside
click and ESC.
- Resize stage drops eight accent-coloured drag handles around the
canvas rect (N/NE/E/SE/S/SW/W/NW). East/south handles just grow or
shrink the canvas; west/north (and their corners) also shift every
scene-world coordinate in lock-step so content stays pinned to its
visible position while the canvas re-aligns around it.
- Enter forces stage-crop on so the user can see where the new edge
lands; exit restores the previous crop setting. Exit paths: Done
button in the floating banner, ESC, or the restored crop toggle.
- New store action shiftSceneWorldCoords(dx, dy) covers layer base +
main/position keyframes + bezier control points + clipInstance x/y
across every scene page. Clip-library layers, button assets, hit
shapes and polygon vertex clouds stay put — those live in local
coord systems, not scene-world space.
- Tests: store coverage (shift across pages, positionKeyframes,
ctrlX/Y, clipInstance, clip-library untouched, (0,0) no-op) plus UI
coverage (context-menu items, Resize stage enters mode + renders 8
handles, ESC and Done both exit, crop forced on and restored).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
addTrackGroup takes an optional anchorTrackId and inserts directly
above it in trackOrder; anchors nested inside another group resolve
to their parent's slot. The timeline's group-add button forwards
selectedLayerId so the new group lands where the user is working
instead of always jumping to the top. Covered by store + UI tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Scripts bumped from 26px to 30px so the five track kinds read as one
visual list; clip / script / audio tracks lose their kind-specific
gradient backgrounds now that they're no longer fixed-position
chrome.
- Audio tracks pick up the standard drag attributes + handlers; they
now participate in the same trackOrder flow as layer / clip / script
tracks and can be dropped into groups.
- normalizeScenePage accepts scripts + audio as group children (used
to silently strip them); the per-kind ordered appending is replaced
with a single tail loop so no kind is hardcoded to the bottom.
- moveTrackIntoGroup color cascade now covers all four groupable kinds
(scripts + audio inherit group colour like layers + clip tracks).
- Group expansion in Timeline emits scripts + audio as first-class
sub-track children, with the shared in-group / sub-track classes
and group-aware drag handlers.
- Tests: move-into-group + color cascade for scripts + audio, legacy
childIds roundtrip preserves both kinds, audio track is draggable,
script track is 30px.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Document drawer slides out from under the topbar (no tab, no hover
peek); opens via project-name button, closes on outside-click or ESC.
Layout: inputs row + divider + preset row, presets user-managed via
popup and persisted in localStorage.
- PWA manifest + minimal service worker so Chrome installs RUWIO as a
standalone app — suppresses the native fullscreen exit overlay.
- Clips and audio clips get right-click context menus (Duplicate /
Delete) via new `duplicateClipInstance` / `duplicateAudioClip` store
actions.
- Clip moves cascade non-overlap in both directions; `setAudioClipPosition`
now pushes backward as well as forward. New `setClipInstancePosition`
gives clip instances the same behaviour.
- Library drags carry timeline-shaped ghost previews (audio pill, clip
pill, button thumb) instead of the library row; clip lane renders a
dashed drop-target ghost and rejects window-overlap drops.
- Scene / Clipart drawer tabs disappear in fullscreen; the 24 px they
occupied is reclaimed for the stage. Play button sits on the exact
horizontal center via a 3-column grid in the transport cluster.
- Tests: context menu + push-cascade + duplicate action coverage for
both clip kinds; DocumentDrawer suite rewritten for the slide-out
model.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Multi-select
- New `selectedLayerIds: string[]` on the scene store — ordered by
click, anchor = last entry, always equal to `selectedLayerId`.
- `toggleLayerSelection(id)` shift-click behaviour: adds or removes,
anchor follows the last click, effectively-locked layers refuse.
- `selectLayer`, `addLayer`, and every selection-reset path keep
the set in lock-step with the anchor.
Timeline
- Shift-click on a layer row's name toggles that layer's
membership in the multi-selection. Every selected row highlights
via `store.selectedLayerIds.includes(track.layerId)`.
- Right-click on a track now preserves an existing multi-selection
if the clicked track is part of it; lands as a single-select
otherwise.
- Track context menu adds two entries when ≥1 layer is selected:
- "Group N selected layers" — calls the new store action and
drops the group into the topmost selected track's stack slot
(not at the very top as a naive `addTrackGroup` would).
- "Convert N selected to clip" — packs the layers into a fresh
clip with `duration = scene.duration`, deep-clones via
`structuredClone(toRaw(l))`, drops a single `ClipInstance` on
a new clipTrack at `startTime = 0`, and positions the
clipTrack at the topmost selected track's slot.
Fixes
- `removeLayer` no longer unconditionally clears
`selectedLayerIds` (bulk-replace artifact from the multi-select
work); the anchor-matched path clears the whole set, the
non-anchor path only splices out that one id.
- `deselectIfLocked` indentation normalised.
Regression tests
- Multi-select store API: selectLayer sync, toggleLayerSelection
add/remove/anchor, lock-filter, empty-selection guards.
- `groupSelectedLayers`: happy-path packing, empty-select null
return, lock-filter, stack-slot pinning.
- `convertSelectedLayersToClip`: packs layers + spawns clipTrack +
instance (deep-clone integrity pinned via unique per-layer names
so the cloned-layer lookup is unambiguous), drops the clipTrack
at the topmost slot, returns null on empty selection, skips
effectively-locked layers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Button-state transitions
- State flips on stage-preview buttons (normal / hover / pressed /
disabled) now ease over 50ms instead of snapping. New
`transitions` map + rAF ticker in useButtonRuntime drive a
continuous sample time between BUTTON_STATE_TIMES slots;
CanvasStage samples each button child at that interpolated time
so keyframes at the 4 states blend smoothly. Mid-flight flips
(hover → pressed while still in normal → hover) rebase from
the current live sample instead of snapping.
Locked groups revoke edits
- Locking a layer directly or its enclosing group deselects the
layer so the Properties panel can't keep editing it. Panel also
splits into three branches — editable / locked / no-layer —
with a dedicated locked-layer hint.
- Timeline keyframe interactions (drag, dblclick-to-remove,
context menu, dblclick-to-create on lane, add-kf toolbar)
now consult effective lock (direct + parent group) instead
of just layer.locked.
Editor polish
- Inactive tracks fade as a whole row (opacity 0.5) instead of
per-child, so keyframes, segments, and still-frame tails stay
visually proportional to their active counterparts.
- Dragging a layer onto a group's track-name label now drops
it INTO the group (was previously only the group-lane area).
- Button assets on the stage show a pointer cursor on hover so
it's obvious they're interactive.
- Global CSS forces `cursor: pointer !important` on every enabled
button / role="button" so scoped component styles can no longer
accidentally shadow the default.
- Timeline resize handle jumped above the scrub-bar (z-index 100)
so the full 14px grab zone is hit-testable again.
Regression tests
- setTrackLocked deselect paths: direct lock, group lock with
selected child, group lock with unrelated selection, unlock
doesn't re-select.
- duplicateKeyframeAt: props + easing + hold + bezier copied,
collision refusal, unknown-id guards, separated-track routing.
- Button transition runtime: no-transition baseline, mid-flight
bounds, mid-transition rebase, no-op setState.
- Shared composables tooltips + asset-thumbs: same ref across
calls, localStorage round-trip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tooltips
- New v-tooltip directive + TooltipHost floating panel replace the
native title attribute across 95+ sites (topbar, timeline, sidebar,
properties, stage overlays, fullscreen preview, export dialog,
clipart drawer, modals). Styled dark card with a configurable
label + optional help line for inline documentation; screen
readers still get an aria-label when the panel is disabled.
- Settings → Interface toggle (`ruwio:tooltipsEnabled`) hides the
visual panel for users who know the app. IconButton now routes
its title prop through v-tooltip so every existing call site is
tooltip-aware without churn.
Editor-hide + export filter
- New `Layer.editorHidden` flag, eye toggle per layer row, skipped
by the Konva stage and the mask-preview canvas path but honoured
by exports and fullscreen preview (runtime untouched).
- Export dialog gets a "Skip hidden tracks" switch that strips
editor-hidden layers from every format (scene JS, runtime,
example HTML, MP4, WebM) on opt-in. Disabled when the scene has
no hidden layers.
Button editor
- `+` buttons appear on empty state slots in button-timeline rows
and duplicate the nearest keyframe (prev by preference, next
otherwise) onto that slot. New `duplicateKeyframeAt` store
action carries props, easing, hold, and bezier handles across.
Stage / interaction
- Plain left-click on the empty content view pans the viewport,
but only when the pointer isn't over a Konva shape — transformer
handles, vertex dots, and selection boxes that extend past the
canvas edge keep their drag.
Settings reshuffle
- Timeline tab merged into Document (Duration + FPS sit under a
new "Timing" sub-heading). New Interface tab gathers three
global prefs: Show tooltips, Show locked tracks, Asset
thumbnails — driven by shared composables so the existing
toolbar toggles stay in sync. Interface-tab CSS defensively
resets the scoped label flex-column + uppercase rule that was
making the toggle row look like an accident.
Project identity + UX polish
- Favicon wired up (moved to public/favicon.ico, linked from
index.html); scripts/pack.sh packs the minimum Vue project
(src, index.html, configs, public) into a tarball with the
right version in the filename, and `npm run pack` drives it.
- About dialog now credits Torsten Hoeltge.
Test infra
- Progress bar turns red mid-run on the first failure: runAll
gets an onResult callback that TestOverlay uses to push each
result into the reactive array incrementally.
- mountComponent in the test harness registers v-tooltip on the
isolated app instance so IconButton's aria-label contract
actually fires under test.
Regression tests
- duplicateKeyframeAt: copy fidelity (props, easing, hold),
collision refusal, unknown-id guards, separated-track targeting.
- Shared composables: useTooltipPref and useAssetThumbsPref both
return the same ref across calls and persist to localStorage.
- Tooltip contract: IconButton ships `aria-label` (not title);
live-DOM smoke tests match on aria-label selectors now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Canvas stage: plain left-click in the empty content area (outside
the canvas rectangle) now starts a pan. Ctrl+left-click still pans
anywhere, middle-click unchanged. Only pure left-click qualifies
(any modifier leaves the old routing intact).
- Export rebrand Animatr → RUWIO for everything user-visible:
- Default filenames: projekt.animatr.json → projekt.ruwio.json;
animatr-runtime.js → ruwio-runtime.js; App's export fallback
base animatr → ruwio.
- Generated comment headers in runtime/scene/bundled scripts.
- Console warn prefix in the runtime and the scene store.
- Example HTML title, heading, and embed-usage snippet.
- parseProject error message.
- ExportDialog copy.
- RUNTIME_VERSION 14 → 15 so cached runtimes reload.
- On-disk magic stays `animatr-project` and window.AnimatrPlayer /
__animatrPending / localStorage & IndexedDB keys are untouched so
old project files and previously-exported HTML keep working.
Regression tests: legacy magic still parses; parseProject error
mentions RUWIO and not Animatr; runtime/scene/bundled scripts and
example HTML all carry the new brand; console prefix is [ruwio].
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Headline:
- Group colour cascades to every non-overridden child (layer or clip
track). New `colorOverridden` flag on Layer / ClipTrack / ScriptTrack
/ TrackGroup; setTrackColor on a non-group pins the track, clearing
re-inherits from the parent group. moveTrackIntoGroup also inherits.
normalizeScene migrates legacy scenes (existing colour ⇒ overridden).
- Keyframes inside a button-asset timeline snap to the 4 state slots
(normal/hover/pressed/disabled). upsertKeyframe / setKeyframeTime /
duplicateKeyframe all enforce it; colliding drags refuse the move,
duplicates walk to the nearest free slot.
- Backspace / Delete on a selected layer removes it (respecting the
layer's own lock and any enclosing group lock).
- "Still frame" allowed on the last keyframe: the layer stays on stage
past its final kf. Timeline draws a solid dim tail line from the
last kf to the end, and single-keyframe tracks now show a full-span
line so on-stage/off-stage reads at a glance. Height stays 3px.
- Project name moved next to the logo, turns into a rename button on
hover (pencil fades in). Rename syncs the stored record when the
project is already persisted.
- Shadow + blur keyframes animate in the preview and exported runtime:
the runtime's lerpProps used to drop them, so every preview rendered
shadow=0 between keyframes. RUNTIME_VERSION 11 → 13 (covers the
button-mask + shadow fixes). Held-last-kf also taught to the runtime.
- Button masks render across every path: edit-mode mask-preview,
nested buttons on the main stage, sidebar thumbnails, fullscreen
preview + exports. drawButtonFrame new in render.ts.
Smaller fixes:
- Dragging layer A on the stage with a stale selection on layer B used
to move B and snap A back. Now the shift-selected branch only runs
when the dragged layer is itself in the selection.
- Clip-track row was missing its icon — PhFilmStrip was referenced but
never imported. Added, plus dropped the bespoke `gap` on clip / script
name rows so their icon+label spacing matches layer rows.
- Clip-art drawer pre-lays-out its 400px grid behind `overflow: hidden`
so slide-in reveals a finished grid instead of reflowing auto-fill
columns over 220ms.
- Timeline clip-track dropzone hides until the library has a clip; its
"drop a clip here" hint stays centred in the visible ruler area.
- Keyframe context menu: check-mark and icons share a single 12px column
so the ✓ on an easing option lines up with PhPauseCircle / PhCopy /
PhTrash on the rows below. "Still frame"'s active indicator moved to
the trailing-check slot.
- Button editor's mask-preview now shows the button's own stage instead
of leaking through to the main scene. drawButtonFrame drives it.
Regression tests cover all of the above: group-color cascade, pin,
clear-re-inherit, migration, button-kf snap and collision rules,
backspace-deletes-selected-layer (incl. lock guards + editable-target
guard), still-frame-past-end visibility, clipart drawer rigid width,
mask rendering in drawButtonFrame / drawSceneFrame, shadow + blur
interpolation in both canvas and runtime paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Button assets
- Re-introduce button-asset type with 4 content states (normal/hover/
pressed/disabled) stored as keyframes at BUTTON_STATE_TIMES, plus a
separate hit-area editor mode.
- useButtonRuntime composable for per-instance state/disabled flags on
the main stage.
- Sidebar library lists button assets with live 80×80 thumbnails;
renderButtonThumbnail now accepts an images map so image-kind layers
inside a button render instead of producing a fully-transparent PNG.
- Compact (no-thumbnail) mode aligns button rows to the library-row
height.
Scope footer
- Button-edit mode now uses the same sidebar scope-footer pattern as
clip-edit (back button to "Main timeline" + active scope chip);
removed the inline exit button from the timeline scrub-bar.
Timeline
- Track-name column default bumped to 260px to match the assets sidebar
above it (storage key bumped to :v2 so stale values reset).
- scrub-bar z-index raised above sticky track-name columns so rows
scroll under it instead of over.
- scrub-handle flips z-index based on horizontal scroll: above
name-spacer when the playhead is in view (no half-clip at t=0),
below when the playhead has scrolled off the left edge (so the
toggle row cleanly covers it).
- name-spacer extends ±3/1px to fully cover the obscured handle.
- scrub-handle head pokes 3px above the ruler with rounded top corners
via clip-path path().
Track context menu
- Added missing i18n keys (renameScriptTrack / deleteScriptTrack /
renameScriptTrackDialog.{title,confirm}).
- Rename items show a visible pencil icon; empty .check slot removed
so items line up flush-left; toggle items place their active ✓ on
the right (kf-menu-trailing-check).
Dev HUD
- Test progress bar flips to --danger as soon as the first failure is
recorded, instead of only at the end of the run.
Regression tests
- renderButtonThumbnail: null on empty button, renders shape layers,
accepts custom images map.
- i18n: renameScriptTrack / deleteScriptTrack / dialog keys resolve.
- scrub-handle-obscured logic (visible at t=0, hidden when playhead is
scrolled off-screen, still visible when playhead is far to the
right of the scroll offset).
- Updated button state-scrubber expectations to match
BUTTON_STATE_TIMES (normal=1, hover=2, pressed=3, disabled=4) and
that hit-mode parks at normal=1 rather than 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The button authoring ended up too far from the intended model after
three correction passes. Pulling it out entirely so we can rebuild
from a clean spec. Markers, script tracks, script frames, the Action
editor, the Script DSL and the runtime for all of the above stay —
they work.
Removed:
- types: ButtonState, StateTransition, ButtonHitArea, ButtonClip,
ButtonInstance, setButtonState/setButtonDisabled Action variants,
Scene.buttonClips/buttonInstances
- store: activeButtonClip* getters, all add/remove/setButton* actions,
seedDefaultButtonClip, enterButtonEdit/setActiveButtonState/
exitButtonEdit, button-label resolution in actionRuntime
- useButtonRuntime composable, buttonBlend helper,
ButtonInstanceDialog, sidebar Buttons section, Timeline button
rendering + pointer handlers, CanvasStage button rendering +
pointer state machine, exportPlayer button runtime + hit-testing
- DSL commands state/enable/disable and their parse errors
- 35 button-related tests + helpers; stale testing doc; stale design
doc
Re-applied three uncommitted fixes that were unrelated to the button
work:
- clicking a marker no longer jumps the playhead (only drag / dblclick
act on it)
- script tracks get a right-click context menu (rename + delete,
color in the existing palette)
- onTracksPointerDown excludes .script-lane / .script-frame so
clicks on empty lanes or on frame dots reach the frame-open path
instead of being swallowed by box-select pointer capture
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>
- 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>
- 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>
- 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>