README
------
Initial README.md at repo root with the existing src/assets/logo.png
header, the editor screenshot from src/assets/ruwio_screenshot.png,
a feature-highlights list, quickstart, stack, project layout, and
the in-app test runner shortcut (Ctrl+Shift+Alt+T).
Effects panel merge
-------------------
Wiggle and Kaleidoscope used to live in two separate collapsible
sections. Now they share one "Effects" section with a thin border
between the two sub-blocks (`.effect-block + .effect-block`),
keeping their independent toggles + controls. Same change for the
ClipInstance variant. Section preference key flows through one
`effects` slot instead of two.
Layer kaleido pivot
-------------------
Until now only ClipInstance kaleido had a separate pivot;
Layer kaleido was locked to the layer's own (x, y). Layers now
get optional `kaleidoPivotX/Y` too, mirroring the ClipInstance
contract.
- types/scene.ts: optional pivot fields on Layer.
- lib/kaleido.ts: layerKaleidoPivot + layerArmTransform helpers
computing per-arm world (x, y, rotation) around the override.
- lib/render.ts: drawLayers + drawMaskShape route through
layerArmTransform.
- lib/exportPlayer.ts: byte-for-byte JS mirror layerArmTf inside
the engine source. RUNTIME_VERSION 32 → 33.
- components/CanvasStage.vue: withK now takes the layer +
computes rotated x/y/rotation per arm; honours the
liveKaleidoPivot ref so a drag updates all copies in real
time. Generic kaleidoPivotHandle handles both clip-instance
and layer entities (KaleidoPivotEntity discriminated union);
manual window-level pointermove drives the drag because
Konva's `draggable: true` didn't fire dragmove for shapes
inside the clipFunc'd canvas group here.
- stores/scene.ts: setLayerKaleidoPivot(id, patch | null) with
null-clear semantics.
- components/PropertiesPanel.vue: two pivot X/Y inputs in a
`.row` inside the layer kaleido sub-block, defaulting to the
sampled position. All kaleido inputs (count, offset, pivot)
switched from @change to @input so the stage repaints live
while the user types or spins the value.
Test fixes
----------
- "PropertiesPanel kaleido offset writes back through the mode"
dispatched a synthetic 'change' event; the input now binds to
@input. Updated to dispatch 'input'.
- "PropertiesPanel: Wiggle section toggles the wiggle field" —
section open-state persists across tests via the panel's
localStorage pref. Test now resets the key before mount and
only clicks the header when the body isn't already shown,
then restores the prior value.
- "PropertiesPanel: layer kaleido pivot inputs render + write
back live" used a global value-50 filter that also matched the
Transform section's X/Y inputs at the sampled position (50, 50).
Scoped the search to the Effects section's body + its `.row`.
Tests added (6 new under "layer kaleido pivot — separate orbit centre"):
- layerArmTransform math: defaults vs explicit override.
- store roundtrip + null-clear of setLayerKaleidoPivot.
- drawSceneFrame integration: 4 cardinal pixel checks for a
20×20 red rect at (160, 100), pivot (100, 100), 4 arms —
sampled at the rotated body centres.
- PropertiesPanel: two pivot inputs render at the sampled
position; @input writes pivotX while pivotY stays default.
- CanvasStage smoke: pivot handle paths render when a kaleido-
on layer is selected (plus toggle-off cleanup).
- Engine source carries layerArmTf and bumps RUNTIME_VERSION
to '33'. Earlier '32' assertions in the kaleido + wiggle
suites also bumped to '33'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tiny progressive-enhancement feature using the browser's native
EyeDropper API (Chromium 95+). Adds a small pipette button next
to every colour swatch in the properties panel; clicking it opens
the OS picker and applies the chosen sRGBHex to the bound colour
property.
- src/composables/useEyeDropper.ts: returns { supported, pick }.
`supported` re-detects on every call (not a singleton) so a late-
loaded polyfill or a test-time monkey-patch is observed without
juggling Vite's module cache. `pick()` resolves to the chosen
hex string, returns null on cancel / abort / missing API.
- PropertiesPanel: PhEyedropper button on each of the three colour
rows (fill, stroke, shadow). v-if="eyeDropperSupported" hides
the button entirely on Safari + Firefox — clean PE. Fill and
stroke buttons disable when their "Transparent" toggle is on.
Picked colour writes through the same fillColor / strokeColor /
shadowColorValue computeds, so the keyframe path / asset writer
is identical to a color-input drag.
- properties.eyedropper i18n string.
Tests (7 new under "eyedropper — color sampling from anywhere on the page"):
- supported reflects window.EyeDropper presence at call time.
- pick() resolves with the sRGBHex from a stubbed EyeDropper.
- pick() returns null on AbortError.
- pick() returns null when the API is missing.
- PropertiesPanel renders eyedropper buttons when supported.
- PropertiesPanel hides them when EyeDropper is undefined.
- Clicking the fill eyedropper applies the picked colour through
the layer's keyframe / asset writer.
`claude.txt` backlog complete (wiggle / motion-paths / eyedropper
all shipped).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-layer modifier that adds noise to the sampled position and
rotation each frame. Pairs thematically with kaleido: both stack
on top of the keyframe sample, both per-layer, both deterministic
so editor and exported runtime render identically.
Math (src/lib/wiggle.ts):
- hash32 + layerSeeds(layerId): three uncorrelated 32-bit seeds per
layer, derived from the id so the same layer always wobbles the
same way.
- wiggleNoise(seed, t): sum-of-three-sines in [-1, 1]. Smooth (C∞),
cheap, no noise-lib dependency. Phase constants are irrational
ratios picked so the streams don't collapse into a low-period
pattern.
- applyWiggle(props, layer, time): adds amp * noise(seed, t*freq)
to x / y / rotation. Defaults: 8px position, 2° rotation, 2Hz.
Wire-in:
- src/lib/interpolate.ts sampleLayer: post-processes the keyframe
sample with applyWiggle. Every renderer goes through sampleLayer
(canvas2d, Konva, button-state, clip-internal), so the modifier
lights up everywhere from a single hook.
- src/lib/exportPlayer.ts engine: byte-for-byte JS mirror of
hash32 / layerSeeds / wiggleNoise / applyWiggle, plus the same
post-process branch in the engine's sample(). RUNTIME_VERSION
31 → 32.
Data + store:
- Layer.wiggle?: { amplitude?, rotation?, frequency? }. All sub-
fields optional with the documented defaults.
- store.setLayerWiggle(id, patch | null): patch object updates
individual fields with magnitude clamping (≥ 0); null deletes
the whole wiggle field.
UI (PropertiesPanel + i18n):
- New "Wiggle" section between Mask and Kaleidoscope. Toggle
(PhWaveSine icon) seeds the pleasant defaults; three RangeSliders
for position amplitude (0-64 px), rotation amplitude (0-45°),
and frequency (0-10 Hz). Hidden for button-asset layers
(consistent with Kaleido's hit-area constraint).
Tests (8 new under "wiggle — procedural shake modifier"):
- wiggleNoise determinism + [-1, 1] bounds across 4 seeds × ~100
time points.
- layerSeeds stability + the three seeds are pairwise distinct;
different ids yield different triples.
- applyWiggle on a layer without `wiggle` is a no-op (returns the
input reference unchanged).
- sampleLayer with wiggle: drift visible across a 1s window and
always within the configured amplitude bands.
- sampleLayer with wiggle is deterministic — same time, same drift.
- setLayerWiggle roundtrip + magnitude clamp + null-delete.
- PropertiesPanel Wiggle toggle writes the wiggle field with the
documented defaults.
- Exported runtime carries wiggleNoise / applyWiggle / wiggleSeeds
and bumps RUNTIME_VERSION to '32'.
Plan for the next two backlog items (motion-path overlay + canvas
eyedropper) is in claude.txt for later.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Timeline ruler's existing s/fr toggle (store.editor.timelineDisplayMode)
now drives every time display and time input across the editor. Mixing
"frames here, seconds there" was inconsistent — pick the unit once, see
it everywhere.
New composable (src/composables/useTimeFormat.ts):
- formatTime(seconds, opts) → "1.42s" or "42 fr" (short variant drops
trailing zeros for ruler-style ticks).
- timeToInputValue / inputValueToSeconds — paired conversions used at
every <input type="number"> bound to a time. Storage stays in seconds
(renderer + .ruw files unchanged); conversion happens at the UI edge.
- inputStep ('0.01' / '1') and unitLabel ('seconds' / 'frames') for
consistent input attributes / labels.
Surfaces switched over:
- App.vue topbar playhead + duration.
- PropertiesPanel keyframe-here label (`@ 1.50s` → `@ 45 fr`).
- FullscreenPreview time label.
- AssetSidebar clip + audio asset durations.
- Timeline audio clip + clip trim tooltips (route through formatKfTime,
which already honoured the toggle).
- DocumentDrawer scene duration input — step + min flip with the mode,
label gets a (seconds)/(frames) suffix.
- ScriptFrameDialog time input — same flip.
- ActionEditor jumpTo time input — same flip. (jumpToFrame stays as
frames since "frame" is its semantic, not a unit choice.)
- Kaleido offset on ClipInstance — now follows the global toggle too,
was hardcoded frames before. The frames-by-default UX comes from
the user defaulting to 'frames' on the ruler; switching to seconds
shows the same offset in seconds without losing precision.
Number-input audit (cleaning up "ewig viele Nachkommastellen"):
- ScriptFrameDialog time: rounded display to ≤3 decimals at the edge.
- ActionEditor jumpTo time: same.
- PropertiesPanel kaleido pivot X/Y (ClipInstance): rounded to int.
- RangeSlider strokeWidth format: snapped to 0.5 increments.
Number-input widths (capping the panel sprawl):
- PropertiesPanel inputs: max-width 96px + tabular-nums; .row inputs
(side-by-side pivot X/Y, transform X/Y) keep flex-share.
- ActionEditor .params input[type='number']: explicit 96px.
- ScriptFrameDialog .row input[type='number']: explicit 96px.
- DocumentDrawer + ExportDialog: already had explicit widths.
Tests (6 new under "time format — global seconds/frames toggle"):
- useTimeFormat composable roundtrips through both modes at fps=30.
- PropertiesPanel keyframe label flips on mode change.
- DocumentDrawer duration input flips step + value (2s ↔ 60fr).
- ActionEditor jumpTo flips step + value (1s ↔ 24fr at fps=24).
- Kaleido offset display switches between modes for the same stored
value (0.5s ↔ 15fr).
- Kaleido offset write-back: typing "30" in frames mode at fps=30
stores exactly 1.0s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New per-entity property that renders content N times around a pivot.
For clip instances each radial copy can also be staggered in time, so
arm i starts playing the clip i*offset frames later than the source.
Data model (src/types/scene.ts):
- Layer: kaleido?, kaleidoCount?
- ClipInstance: kaleido?, kaleidoCount?, kaleidoOffset?, kaleidoRotateCopies?,
kaleidoPivotX?, kaleidoPivotY?
All fields optional; legacy projects unchanged.
Math (src/lib/kaleido.ts):
- kaleidoCountFor / kaleidoAngleDegrees, clipKaleidoPivot, clipArmTranslate.
- clipArmLocalTime: returns the per-arm clip-local time. For arm i with
offset dt, the arm shows frame (offsetIntoBlock - i*dt) — visible only
when that falls inside [0, trimmedDuration]. Returns null otherwise so
the caller can hide pre-start and post-end arms.
Render parity:
- src/lib/render.ts: extracted renderClipInstance helper used by both the
top-level scene loop and drawTrackGroup. drawLayers + drawMaskShape now
loop the kaleido count and add the per-copy angle to the layer rotation
(Konva-style "rotate around (x,y)" comes for free).
- src/lib/exportPlayer.ts: mirror in JS (kalCount, kalAngle, clipKaleidoPivot,
clipArmLocalT, clipArmTrans, drawClipInstance). drawLayer + drawMaskShape
wrapped the same way. RUNTIME_VERSION 28 → 31 (3 fixes during landing).
- src/components/CanvasStage.vue: stackItems now emits one bundle per
kaleido arm with arm-specific items sampled at the arm's localT, so the
Konva live preview shows the staggered start in the editor — not just
in the fullscreen preview / export. Source arm (armIndex 0) keeps the
selection / drag / transform handlers; non-source arms are listening:
false. New clipInstanceKaleidoGroupConfig handles the per-arm transform
with rotateCopies on/off semantics. withK helper augments per-layer
configs with the kaleido rotation.
UI (src/components/PropertiesPanel.vue + i18n):
- New "Kaleidoscope" collapsible section. Layer mode: toggle + repeat.
Clip-instance mode (selected via canvas, panel banner explains the
partial editability): toggle, repeat, start offset (frames — converted
to seconds via scene.fps before storage), pivot X/Y, rotate-copies
toggle. Hidden for button-asset layers (hit-area routing breaks if
multiplied).
Store (src/stores/scene.ts):
- setLayerKaleido / setLayerKaleidoCount with [2, 64] integer clamp,
setClipInstanceKaleido with clear-semantics for offset=0 and
rotateCopies=true (deletes the field so JSON stays clean).
Tests (8 new under "kaleidoscope — store + render + engine"):
- Roundtrip + clamp for both Layer and ClipInstance setters.
- drawSceneFrame: 4 cardinal-pixel kaleido smoke for static layer.
- Button asset bypasses kaleido (hit-area routing intact).
- ClipInstance roundtrip + clear-semantics for offset=0.
- clipArmLocalTime: arm 0 honours bounds (returns null when source
itself is past), delayed arms outlive the source's window when
their shifted time is still in [0, dur].
- drawSceneFrame integration: 4-arm clip with 0.5s offset; sample at
t=0 / t=0.5 / t=1.5 verifies staircase visibility (1 → 2 → 3 arms).
- Engine source carries kalCount, kalAngle, drawClipInstance,
clipKaleidoPivot, clipArmLocalT; RUNTIME_VERSION '31'.
- layerActiveAt regression — kaleido fields don't break visibility gate.
Plus the prior export-runtime test updated to read drawClipInstance's
inst.{x,y,scaleX,scaleY,rotation} variables instead of the old inline
inst2.* names that the kaleido refactor removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the bare <select> with a custom combobox that fits the
properties panel's visual language:
- Trigger looks like the other inputs (var(--bg)/var(--border)/
var(--radius-sm)) and shows the selected font's name rendered in
that font itself, with a rotating caret.
- Options live in a teleported popover anchored beneath the trigger.
Each option renders its own label in its own typeface so the user
sees what they're picking before applying it. Active row uses
var(--accent-soft) to match the rest of the editor.
- Click-outside closes via window pointerdown; Escape closes and
returns focus to the trigger.
- Storage shape simplified: textFontModel uses '' for system-sans
instead of the literal 'system-sans' id, so the model field round-
trips through JSON without a placeholder string ever leaking.
Updated PropertiesPanel test to walk the new picker (click trigger,
read options from .font-popover, click an option) instead of
dispatching a select 'change' event. Picker label text doubles as
the test selector — System Sans / Inter / Permanent Marker confirm
the option list is wired up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New src/lib/fonts.ts: curated set (Inter, Roboto, Lora, Playfair
Display, Bebas Neue, Source Code Pro, Permanent Marker) shipped as
local .woff2 in public/fonts/. loadFonts() registers them via
FontFace at app start; fontsReady ref re-triggers Konva text configs
once fonts resolve so the first paint isn't stuck on the fallback.
- TextAsset gets optional fontFamily (id; undefined = system sans).
PropertiesPanel adds a font dropdown next to font size.
- All five render sites read the family: CanvasStage textConfig +
clipTextConfig (Konva), drawLayer + drawMaskShape (canvas2d),
exportPlayer engine (uses an embedded FONT_FAMILIES map and fontFam
helper; runtime version 27 → 28).
- Example HTML export now lists used bundled fonts and prints the
@font-face CSS the user has to paste alongside the .woff2 files —
fonts themselves are NOT bundled into the export to keep it small.
- Default text fill changed from yellow #f5d76e to black #000000 in
both the main sidebar and the SVG-scope add-text path.
- 8 new tests under "text font picker — model + render + export".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Click-and-drag on the empty stage starts a layer marquee with
touch (not enclose) semantics — every unlocked layer whose bbox
overlaps the drag rect joins the selection. Tiny clicks still
deselect; shift adds to the existing selection on both clicks
and marquees. Polygon-vertex lasso stays available when a single
polygon is the active selection.
- Konva transformer now wraps every selected layer in one shared
bounding box (polygons fold in for mixed selections; vertex
handles + the vertex lasso suppress to avoid fighting the box).
rotateEnabled flips off on multi to skip the ambiguous shared-
pivot rotation. Drag of any selected node mirrors the delta onto
every sibling Konva node so the whole group travels together;
transformend persists each layer's final pose, not just the
anchor's.
- liveLayerPose became a per-id map (liveLayerPoses) so the
reactive shape configs don't snap non-focused siblings back to
their sampled pre-transform pose mid-gesture.
- Plain click on a member of an existing multi-selection no longer
collapses to single-select before the drag begins.
- Arrow-key nudge + Timeline + keyframe button + Properties panel
all fan out across the multi-selection: every editable layer
gets the value, kind-specific panel sections (text, polygon,
fill, stroke, blur, shadow, mask, button) collapse to leave just
Transform + Opacity, and the trash button labels with the
selection count.
- Click on empty stage with a polygon selected now deselects the
layer too instead of leaving the polygon stuck in the panel.
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>
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>
- 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>
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>
- 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>