Commit Graph
26 Commits
Author SHA1 Message Date
th 9b314ad817 Unify switch controls 2026-06-08 10:57:00 +02:00
th b9a4b53109 Add camera resize and keyframe controls 2026-06-08 00:33:57 +02:00
th 23a66c6fdf Add output camera framing 2026-06-07 23:12:46 +02:00
th 70cf131c47 Make kaleido pivot keyframable 2026-06-05 14:31:31 +02:00
thandClaude Opus 4.7 42c5f68182 1.2.13: README + Effects merge + draggable kaleido pivot for layers
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>
2026-04-26 23:26:56 +02:00
thandClaude Opus 4.7 184842b2e0 1.2.12: eyedropper — sample colours from anywhere on the page
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>
2026-04-26 21:21:14 +02:00
thandClaude Opus 4.7 9e25cf155a 1.2.10: wiggle — deterministic procedural shake modifier per layer
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>
2026-04-26 20:24:23 +02:00
thandClaude Opus 4.7 7d81a50e74 1.2.9: global seconds/frames toggle + sane number-input widths/rounding
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>
2026-04-26 20:01:54 +02:00
thandClaude Opus 4.7 80e1510b53 1.2.8: kaleidoscope — radial repeat for layers + clips with delayed-start arms
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>
2026-04-26 19:18:16 +02:00
thandClaude Opus 4.7 8c4f744afc 1.2.6: dedicated FontPicker component with in-line type previews
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>
2026-04-26 10:27:19 +02:00
thandClaude Opus 4.7 4c2b7c7c35 1.2.5: text font picker + 7 bundled Google fonts + default text fill black
- 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>
2026-04-26 00:32:42 +02:00
thandClaude Opus 4.7 1f2812c64d 1.1.3: multi-select on the stage — marquee, group transform, panel
- 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>
2026-04-25 08:41:26 +02:00
thandClaude Opus 4.7 af5f99380c 0.8.4: button transitions, locked groups revoke edits, UX polish
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>
2026-04-22 04:31:08 +02:00
thandClaude Opus 4.7 1d84c34c15 0.8.3: custom tooltips, editor-hide, pack script, settings reshuffle
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>
2026-04-21 23:57:35 +02:00
thandClaude Opus 4.7 c3764201b8 feat: 5-frame button assets + timeline/sidebar polish
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>
2026-04-21 19:19:39 +02:00
thandClaude Opus 4.7 d4c4830fe2 0.6.0: background video, in/out range, top-level masks, track icons
- 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>
2026-04-20 20:19:34 +02:00
thandClaude Opus 4.7 cbc309bc8c 0.5.1: replace properties checkboxes with icon toggles
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>
2026-04-20 12:18:47 +02:00
thandClaude Opus 4.7 ddea7d348f 0.5.0: RUWIO rebrand, vertex bezier, animation paths, welcome focus
- 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>
2026-04-20 12:12:38 +02:00
thandClaude Opus 4.7 276d621d20 0.3.0: freeform drawing, stroke properties, polygon edit gestures
- 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>
2026-04-20 09:53:01 +02:00
thandClaude Opus 4.7 a05396e734 0.2.0: polygon tool, mask editor preview, collapsible properties
- 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>
2026-04-20 01:29:27 +02:00
th 03b1147a93 bak 2026-04-20 01:05:27 +02:00
th 3ef5fb5a2d bak 2026-04-20 00:07:08 +02:00
th 6b6157d786 bak 2026-04-19 22:32:06 +02:00
th b977ee35a8 bak 2026-04-19 21:13:53 +02:00
th cc6b11e61d bak 2026-04-19 20:31:53 +02:00
th c6863f2bfc init 2026-04-19 17:53:36 +02:00