Commit Graph
100 Commits
Author SHA1 Message Date
th b88d174bcb cleanup 2026-04-26 21:39:11 +02:00
th 367dd68e49 readme 2026-04-26 21:36:20 +02:00
th e47a982492 readme 2026-04-26 21:33:40 +02:00
th ceaf1db434 readme 2026-04-26 21:25:12 +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 a2fdd1684a 1.2.11: motion-path overlay (toggle in stage view-options) + type-check cleanup
Motion-path overlay
-------------------
A toggleable always-on visualization of the selected layer's
position-keyframe trajectory. Pairs with the existing kf-selection-
driven path that only appears when a keyframe is actively selected.

- src/composables/useShowMotionPaths.ts: shared ref + localStorage
  ('animatr:showMotionPaths'), default off.
- CanvasStage.vue: new motionPathOverlay computed builds dashed
  segments + a dot per keyframe for store.selectedLayerId, but only
  when the toggle is on AND no kf is explicitly selected (the kf
  view wins — it's richer with drag handles). Color: lilac
  (#a47bff) so it doesn't collide with the blue kf-selection path.
- Click a dot → seek the playhead to that keyframe's time. Cheap
  navigation aid for layers with many keyframes.
- Toggle button placed in the stage's existing zoom/view-controls
  toolbar (alongside crop / mask preview / centre crosshair / stage
  preview), not the app topbar — that's where the canvas-scoped
  view modifiers already live.

Tests (4 new under "motion-path overlay — toggle + selected-layer rendering"):
- showMotionPaths persists to localStorage.
- Toggle on with two position kfs renders the overlay; toggling
  off returns to a clean canvas.
- kf-selection path takes precedence over the toggle (mount smoke
  with both paths active simultaneously).
- The motion-paths button in .zoom-controls flips the ref.

Type-check cleanup
------------------
The 7 pre-existing TS errors that piled up over the last few patch
bumps. None affected runtime, but vue-tsc was complaining:

- src/lib/clipBounds.ts: assetFootprint read width/height directly
  off ButtonAsset, which actually has canvas.width/canvas.height.
  Split the branch and added the same handling for SvgAsset.
- src/lib/render.ts: instancesByTrack was typed via
  `typeof scene.clipInstances` (which is optional) → values were
  ClipInstance[] | undefined. Wrapped in NonNullable.
- src/lib/ruwFile.ts: BackgroundVideo has no `name` field. Synthesised
  one from page.id so the extracted file gets a readable filename
  (`scene-bg-${page.id}`).
- src/devtools/tests.ts (2 sites): polygon test fixtures missed the
  required `fill` field — added `fill: '#0f0'`.
- src/devtools/tests.ts (1 site): TS 5 widens
  `Uint8Array<ArrayBufferLike>` past BlobPart's union; cast to
  unknown as BlobPart at the new File call.

`npm run type-check` is now clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 21:08:49 +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 27d6a47b72 1.2.7: 6 more bundled fonts + custom-font upload + layerActiveAt parity
Built-in additions: Montserrat, Nunito, Figtree, Gabarito (sans),
Archivo Black (heavy display), Press Start 2P (pixel/retro).

Custom font upload (lib/fonts.ts):
- uploadCustomFont(file) reads .woff2/.woff/.ttf/.otf via FileReader,
  registers a FontFace early so a corrupt file fails before storage,
  then persists {id, label, dataUrl} to localStorage under
  'animatr:customFonts'. Quota errors surface a friendly message.
- removeCustomFont(id) drops the FontFace + storage entry. If the
  asset still pointed at the deleted font, the picker resets it to
  System Sans on the calling side.
- AVAILABLE_FONTS becomes a ComputedRef combining BUILTIN_FONTS with
  the reactive customFontsState. fontFamilyMapSnapshot() rebuilds the
  {id → CSS family} map per export so user-uploaded fonts make it
  into the exported runtime.

FontPicker UI:
- Two sections divided by a thin rule — built-ins on top, "Your fonts"
  below — plus an "Upload font…" entry at the bottom that opens a
  hidden file input. Custom rows show a trash button on hover.
- Auto-selects a freshly uploaded font.

Engine source becomes a function (engineSource()) instead of a const,
so each export materializes the latest FONT_FAMILY map. ENGINE_TEMPLATE
holds the static skeleton with a __FONT_FAMILY_MAP__ placeholder.

Example HTML hint:
- Built-in fonts list their .woff2 path; custom fonts get a
  "locally uploaded; @font-face below has the file inlined as a data
  URL" line. The fontFaceCss snippet inlines data URLs for custom
  entries so it's self-contained.

Bug fixes (player layerActiveAt):
- Mirrors interpolate.ts isLayerActiveAt now. The previous
  `if(total<=1) return true` short-circuit made layers with a single
  future keyframe visible at t=0; the lastHold→Infinity heuristic
  ignored end keyframes entirely. Both regressions affected the
  fullscreen preview and standalone exports. Replaced with a strict
  scan: min = earliest non-end kf, max = earliest 'end' kf or +Inf.
  Static layers (no normal kfs) stay always-visible.

Tests (5 new under "text font picker — model + render + export"):
- 6 new built-in ids in FONT_FAMILIES map.
- uploadCustomFont registers + persists + appears in AVAILABLE_FONTS;
  removeCustomFont reverses it. Stubs FontFace and document.fonts.add
  for the test environment via a stubFontFaceForTest() helper.
- uploadCustomFont rejects unsupported extensions.
- Exported runtime FONT_FAMILIES includes uploaded custom font ids.
- FontPicker popover shows the Upload font… entry.
- New "exported runtime layerActiveAt mirrors editor isLayerActiveAt"
  extracts the function from engineSource() and exercises five
  scenarios (single future kf, two normal kfs no end, end kf cuts
  off, static layer always-visible, end kf wins over separated tracks).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 13:40:26 +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 b32b8a6a3d 1.2.4: mask cascade strict bottom-up + clip-internal masks
Audited masking against the four-scope spec (groups, main timeline,
clips, SVG-internal). Rule 2 was already correct; rules 1, 3, 4 needed
fixes:

- drawMaskedGroup now walks children bottom-up and applies each mask
  to the offscreen buffer immediately, so a mask only affects layers
  below it in the stack — layers above the mask render unmasked on top.
- drawTrackGroup rewritten with an Item discriminated union collected
  bottom-up, then a single z-order loop renders/applies, replacing the
  old "collect all masks → apply at end" pattern.
- Both clip-rendering sites (drawSceneFrame top-level clip path and
  drawTrackGroup's clip-child path) switched from drawLayers to
  drawMaskedGroup — previously a mask layer inside a clip was rendered
  as a regular shape instead of masking.
- 5 new tests under "mask layers — rendering" cover the four-rule
  spec (group below-only, group negative below-only, clip-scoped mask,
  SVG-asset mask scoped to interior, clip-internal layers above mask
  unmasked).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 00:05:40 +02:00
th 127812d3bf 1.2.3: track-name shift/ctrl select, polygon level-1/level-2, cross-scope polygon ops
- Track-name click semantics in the Timeline:
  - plain → single-select that layer
  - shift → range-select every parent layer between anchor and click
    (locked + locked-group children are filtered out)
  - ctrl/cmd → toggle the clicked layer in/out of the selection
  Anchor moves to the just-clicked id like a file manager.
- Polygon / freeform selection now lands at "level 1" — the regular
  size + rotation transformer attaches like any other shape. A
  double-click promotes to level 2 (vertex handles + edge-insert
  dbl-click + tangent handles); selecting another layer drops back
  to level 1. Same UX inside the SVG sub-editor.
- New layersUsingAsset(project, assetId) helper walks every layer
  collection in the project (scenes, clips, button assets, svg
  assets). All six polygon-mutation actions (addPolygonPoint,
  removePolygonPoint, setPolygonPointCount, reversePolygonPoints,
  movePolygonVertex, insertPolygonPointOnEdge) now use it, so
  changes to the asset's points propagate to kf.props.points on
  layers in any scope. Fixes "no node added on dblclick of a
  freeform line inside an SVG asset".
- SvgLayerPanel buttons reset padding:0 — the global button reset
  was eating the 22×22 row-button area with box-sizing:border-box
  and clipping the Phosphor icons out of view.
2026-04-25 23:39:19 +02:00
th c17e2e8a9f 1.2.2: timeline duration handle + frame-snapped script frames + cleaner time display
- Duration grip at the right edge of the scrub ruler. Drag to
  resize the active timeline (clip duration in clip-edit, scene
  duration otherwise); maps clientX → seconds and snaps to the
  scene's frame grid before calling setDuration. Hidden in button-
  asset edit (state grid is fixed).
- Script-frame times go through a quantizeToFrame helper at the
  store level (addScriptFrame / setScriptFrameTime / duplicateScriptFrame),
  so drag input no longer produces values like 1.4181818… and the
  playhead lands exactly on the frame during tick().
- Header time and DocumentDrawer duration input now show the
  duration rounded to 2 decimals — frame-aligned floats like
  4.733333… read as 4.73. Setter still writes the user-typed full-
  precision value through unchanged.
2026-04-25 23:00:13 +02:00
th 3bc329813c 1.2.1: strokes scale with the layer (incl. inside SVG-asset placements)
- strokeCfg now defaults to strokeScaleEnabled:true so stroked rect /
  circle / text outlines grow proportionally when the layer is scaled
  (Konva default; previously hard-pinned to false).
- polygonConfig and clipPolygonConfig had inline stroke blocks that
  also forced strokeScaleEnabled:false. Flipped both to true so
  freeform / polygon outlines scale alongside the shape.
- SVG-asset placements use a dedicated svgChildConfig wrapper +
  pre-multiplied strokeWidth in svgStackForAsset: each child's stroke
  is bumped by the placement's geometric-mean scale and rendered with
  strokeScaleEnabled:false (Konva resets the ctx to identity before
  stroking) so the on-screen width lands at the right pixel count
  regardless of how Konva propagates parent transforms to nested
  stroke rendering. Konva-mounted tests cover outer-scale 1×, 3× and
  non-uniform 4×/1×.

UI chrome (transformer borders, vertex / tangent handles, hit-shape
outlines, animation-path ghosts, ghost-locked layer indicators) keeps
strokeScaleEnabled:false intentionally — those should stay at constant
pixel width regardless of zoom.
2026-04-25 22:45:01 +02:00
th d03fbe4ecb 1.2.0: SVG asset editor + end keyframe type + UX bundle
This release bundles three independently-useful pieces of work that
overlap heavily in shared files (types, store, i18n, App, CanvasStage,
tests), so they ship together.

SVG asset editor
- New asset kind 'svg' (SvgAsset { canvas, layers }), authored from
  inside the existing shape primitives (rect / circle / text / polygon /
  freeform). "+" button under the Images sidebar header; opens a
  sub-editor that swaps the library sidebar for a dedicated layer
  panel, hides the timeline, and uses the same canvas-resize flow
  ("Edit canvas size" via right-click → no Document settings entry
  while in scope). Children carry a single t=0 keyframe each.
- Multi-select rotation in SVG scope only: a temporary group pivot
  appears at the selection centroid (draggable), rotation around it
  shifts each layer's x/y/rotation. Pivot follows the centroid drift
  on Konva-transformer scale/move. Outside SVG, transformer
  rotateEnabled stays off (group-rotate write-back is unreliable
  per prior feedback).
- Authored SVGs are placed as layers in the main scene and rendered
  via drawMaskedGroup at t=0; thumbnails surface in the sidebar via
  renderSvgThumbnail. Drop / click / drag / dbl-click-to-edit work
  via a transparent svg-hit catcher rect routed in the stage handler.
- Freeform default stroke is red (#e23b3b) so the open path is
  visible immediately. addDrawingPoint resolves the layer through
  activeLayers so freeform draws correctly inside SVG and clip
  scopes too.

End keyframe type
- Keyframe.kind: 'normal' | 'end'. End kfs carry no props, mark the
  visibility cap. layerActiveRange / isLayerActiveAt / sampleKeyframeSet
  ignore them for interpolation and use them only as the visible-from
  / visible-until bounds.
- One-time migration in normalizeProject synthesizes an end kf at the
  time of the last normal kf for layers that relied on the legacy
  "last kf = disappears" semantics; layers using hold:true to extend
  visibility past the end skip the migration. Idempotent.
- Timeline UI: new "+end" toolbar button (multi-select fan-out, toggle),
  "Convert to end" / "Back to normal" entries in the kf context menu
  (easing / still-frame / duplicate hidden for end kfs), end kfs
  render as a flat axis-aligned square (vs. the diamond-rotated normal
  kf). Render filter drops end kfs that collide with a normal kf at
  the same time so legacy migrated projects don't read as four-point
  stars.

UX polish
- TAB-collapse stage anchoring: the canvas content stays put when the
  asset / properties asides toggle (CanvasStage's ResizeObserver
  compensates viewX/Y by the container's bounding-rect drift).
- Topbar clipboard button copies the current frame to the system
  clipboard as a PNG via navigator.clipboard.write([ClipboardItem]).
- chromeClick predicate adds .timeline to the editing-panels bail so
  the synthetic click after a Timeline box-select drag no longer
  wipes the just-committed kf selection.
- Right-click on a polygon vertex stops propagation so the vertex
  context menu wins instead of the generic stage menu opening on top.
2026-04-25 21:45:58 +02:00
th 492f25f629 1.1.7: TAB hides the asset + properties asides for a full-width stage
Press TAB to slide the asset sidebar and the properties aside off-screen
and let the stage absorb their grid columns; press again to bring them
back. Skipped while typing in inputs so form-field focus traversal
keeps working.
2026-04-25 09:55:16 +02:00
th b5c5270256 1.1.6: still image export — PNG / JPG snapshot of the current frame
Adds a "Still image" subgroup under the project column in the export
dialog. Both options render the current playhead frame via the same
drawSceneFrame path used by GIF/APNG, so the still is pixel-identical
to the matching animated frame. JPG fills transparent scenes white;
PNG keeps alpha. Honours the per-export target size for higher-res
poster frames.
2026-04-25 09:49:32 +02:00
th b0813a1d50 1.1.5: settings/media folder details + empty-click deselects everything
- Settings → Media tab now shows per-folder image/audio counts, skipped
  subfolders, skipped files, and a rescan button. listClipart hard-skips
  subdirectories and unsupported file extensions, returning a skip
  report the panel renders.
- Empty-click anywhere — chrome, bare stage, empty timeline tracks —
  clears the full selection (layers, clip instances, keyframes) via a
  shared useDeselectAll helper. shouldChromeClickDeselect() splits
  exclusions into interactive elements, overlays, editing panels and
  timeline selection elements so empty timeline space deselects too.
2026-04-25 09:42:36 +02:00
thandClaude Opus 4.7 3005d9310d 1.1.4: 2-column export + welcome rework with auto-load + archive
- Export dialog goes wide (xl modal) and splits into two columns:
  Project & code on the left (.ruw, runtime.js, animation.js,
  example.html), Media on the right with the per-export size + fps
  inputs followed by MP4 / WebM / GIF / APNG / APNG preview. Skip-
  hidden toggle still spans the full width up top. Single-column
  fallback below 720px.

- Welcome dialog reworked:
  - Always auto-loads the last edited project on boot. The welcome
    dialog opens on top only when the new "Show on start"
    preference is enabled, or unconditionally when there are no
    recents (first-run flow).
  - Two-column body: Recent (max 5 thumbnail tiles, was 8) on the
    left, Archive (compact list view, no thumbnails) on the right.
  - Footer toggle binds to the new useShowWelcomeOnStart shared
    ref so the user can opt out of the boot dialog inline.
- Document settings → Interface tab grows a matching "Show welcome
  screen on start" toggle so users who turned it off can flip it
  back on without diving into localStorage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 09:02:52 +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 289fa770b0 1.1.2: arrow-key nudge for the selected stage object
Arrow keys now move the currently-selected object by 1 scene unit;
Shift bumps the step to 10. Works for both regular layers
(upsertKeyframe at the playhead — same write path as a stage drag)
and clip instances (setClipInstanceTransform). Locked targets and
events originating in text inputs are passed through untouched, so
typing in the document drawer / rename dialogs isn't stolen.

Also drops the flaky show-locked persistence test. It assumed the
in-memory default (true) differed from the stored value; on a
fresh dev-server restart with empty localStorage the first toggle
was a no-op, the watch never fired, and the assertion failed.
Test now flips the ref away first so every subsequent transition
is guaranteed to write.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 01:07:48 +02:00
th 01c13981c4 1.1.1: fullbak 2026-04-25 00:40:53 +02:00
th d59201100b document name input focus 2026-04-25 00:08:29 +02:00
thandClaude Opus 4.7 92e9f1619f 1.1.0: .ruw project bundle + per-export target FPS
- New .ruw file format: a STORE-only zip wrapping project.json +
  an assets/ folder. packProject extracts every data-URL asset
  (images, audio, background video) into its own file and
  rewrites the in-JSON src fields to relative paths;
  unpackProject rehydrates back to data URLs on read. Zero
  dependencies — we ship a minimal in-house zip codec.
- ExportDialog surfaces the new format as a top-row
  "RUWIO project (.ruw)" option that round-trips through the
  welcome-dialog file picker. Legacy .json imports still work
  via byte-sniffing the PK header in readAnyProjectFile. Dead
  App.vue `exportProjectFile` helper removed.
- Frame-based exports (GIF, APNG, APNG preview) take an optional
  targetFps override so a 60fps project doesn't balloon the file
  — APNG / GIF size scales ~linearly with frame count. The
  ExportDialog grows a second input next to target size;
  defaults inherit the scene's fps and reset on each dialog
  open.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 20:50:21 +02:00
thandClaude Opus 4.7 647f09b287 1.0.2: persist editor-session UI state with the project
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>
2026-04-24 19:47:44 +02:00
thandClaude Opus 4.7 cd9cf91ef0 1.0.1: target size for GIF / APNG / APNG preview
Frame-based exports (GIF, APNG, APNG-preview) accept an optional
target output size. ExportDialog exposes it as a single width
input above the three image-export rows; height is derived live
from the scene's aspect ratio so the user can't squash the output.
The preview HTML mirrors the target dims so a downsized export
previews at its real pixel size.

renderFrames takes targetWidth / targetHeight, sizes its canvas
accordingly and scales the ctx once; drawSceneFrame's own
save/restore preserves the transform across frames, including the
top-level-mask offscreen compositing path.

Target width on dialog open is inherited from the current
scene.canvas — watched via modelValue so resizing the stage and
reopening the dialog picks up the new size (instead of the stale
value from whenever ExportDialog first instantiated).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 17:50:35 +02:00
thandClaude Opus 4.7 c2b81b0fb4 1.0.0: delete-group cascade + native-size APNG preview
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>
2026-04-24 16:40:36 +02:00
thandClaude Opus 4.7 56cecabaae 0.18.3: grouped clip tracks render, playhead is a snap magnet
- Fix mask-preview / frame-export dropping any group whose children
  are clip tracks. render.ts's old group branch only pulled Layer
  children out of group.childIds; new drawTrackGroup walks both
  layer AND clip-track ids and still applies group-local masks to
  the composite. Same fix mirrored into the HTML export runtime in
  exportPlayer.ts.
- Snapping magnet for the playhead: snapTime now includes
  store.playhead in its candidate list by default. Clip drags, trim
  handles, keyframe drags, marker drags and in/out brackets stick
  to the needle when snap is on. Scrub handlers pass
  ignorePlayhead:true so the needle doesn't stick to its own prior
  position during a scrub.
- Clip trim handles derive the snapped edge first and back out to
  trimStart/trimEnd, so the edge lands exactly on the magnet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:20:18 +02:00
thandClaude Opus 4.7 21cf6e9a12 0.18.2: centre crosshair + locked-track selection + eye-on-lock
- 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>
2026-04-24 15:23:13 +02:00
thandClaude Opus 4.7 5af0cdb3d4 0.18.1: editor-hide cascades to groups + rename for layers & clip tracks
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>
2026-04-24 14:55:22 +02:00
thandClaude Opus 4.7 6784b76607 0.18.0: clip instances get stable transform frame + track hide
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>
2026-04-24 14:11:43 +02:00
thandClaude Opus 4.7 a858f94bb7 0.17.0: split SVG layer into per-path layers
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>
2026-04-24 12:26:57 +02:00
thandClaude Opus 4.7 baf775c8de 0.16.1: welcome screen arrow-key navigation
Up/Down arrow keys now move focus through the recents list in the
welcome dialog; Home/End jump to the first/last entry. Tab / Enter
/ Space behaviour is untouched — arrows are additive. Covered by
two UI tests (arrow roundtrip + Home/End) that poll for the async
IDB load instead of a fixed delay.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 10:29:07 +02:00
thandClaude Opus 4.7 fcfd21428d 0.16.0: animated GIF / APNG export + transparent canvas
- New frame-by-frame export pipeline in src/lib/frameExport.ts: loads
  scene images, walks rangeIn..rangeOut at scene.fps, drawSceneFrame
  to an offscreen canvas, awaits a per-frame callback, yields every
  4 frames to keep progress responsive, honours AbortSignal.
- Animated GIF via gifenc (per-frame quantize/palette, ~5 KB lib).
- Animated PNG via UPNG.js + pako (lossless RGBA, alpha-aware, ~30 KB).
- drawSceneFrame now clearRects first and skips the background fill
  when scene.canvas.background === 'transparent' — prerequisite for
  the alpha to reach the APNG output.
- DocumentDrawer: new checkerboard toggle next to the colour picker
  flips the stage between its last-picked colour and transparent;
  disabled swatch + 'Transparent' hex label make the state obvious.
- ExportDialog: three new option rows — Animated GIF, Animated PNG,
  and an APNG preview page that bundles the rendered APNG into a
  standalone HTML with a checkerboard backdrop + dark/light toggles
  for verifying transparency at a glance.
- Tests: renderFrames frame count + abort, GIF header magic, APNG
  acTL presence, transparent round-trip (decoded alpha == 0 on
  un-painted pixels), preview HTML structure, and the two UI tests
  covering ExportDialog's new rows and the progress-view transition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 10:21:30 +02:00
thandClaude Opus 4.7 827d80bfd0 0.15.0: stage context menu + drag-to-resize stage
- 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>
2026-04-24 09:10:54 +02:00
thandClaude Opus 4.7 9fe86b62b9 0.14.0: multi-folder media drawer
- Clipart store holds an array of root folders instead of a single
  handle, plus an activeHandleIndex for the in-folder view. Getters:
  activeHandle, inOverview, hasAnyFolder, canGoBack, folderName.
- Actions: addFolder (appends a root), enterFolder, exitFolder,
  removeFolder. Single-folder installs auto-drill into the sole root
  and behave exactly like the pre-refactor drawer; adding a 2nd+
  folder pops back to the root overview.
- IDB persistence migrated from the 'clipartDir' single-handle key to
  a 'clipartDirs' array. Legacy records are migrated on first load.
  saveClipartFolders unwraps Pinia proxies (toRaw per element) so
  IDB's structured clone accepts the payload.
- ClipartItem union gained a 'folder' kind so root tiles render as
  first-class media items. listClipart emits nested directories too,
  but the drawer keeps drill-down one level deep for now.
- ClipartDrawer header: three equally-sized .header-btn chips —
  back (only when canGoBack), refresh (when viewing a folder), and a
  new `+` (when any folder is attached). No more big 'Choose folder'
  button — empty state is a centred 96px dashed + with a wrapped hint.
- Folder tiles carry a hover `×` to remove a root from the library.
- DocumentSettings media tab lists all configured folders with a per-
  folder remove action; primary button is now 'Add folder'.
- Pinia store now calls acceptHMRUpdate so Vite HMR actually swaps
  the store definition between reloads instead of leaving the first-
  load state shape behind.
- Tests: store actions (addFolder / enterFolder / exitFolder / remove
  / IDB round-trip), drawer UI (centred + empty state, overview with
  folder tiles + click-to-enter, back visibility, single-root hides
  back, header + button triggers addFolder). Tests isolated with
  try/finally so a failing store assertion can't leak error state
  into subsequent drawer tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 08:33:20 +02:00
thandClaude Opus 4.7 8aa91fc27b 0.13.1: new track-group lands above the active layer
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>
2026-04-24 08:08:23 +02:00
thandClaude Opus 4.7 86ec7aa835 0.13.0: unify all track kinds — same height, same drag, same grouping
- 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>
2026-04-24 08:00:02 +02:00
thandClaude Opus 4.7 354dd8a4f0 0.12.0: document drawer redesign, PWA install, clip/audio context menus + push-cascade
- 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>
2026-04-24 07:41:26 +02:00
th fa66ba69bb 0.11.1: drawer mouse trigger and clip track / audio behavior 2026-04-23 20:31:41 +02:00
th 8d987d1526 Help Modal 2026-04-23 19:33:22 +02:00
th 8e8aa5f27c minor cosmetics 2026-04-22 19:58:30 +02:00
thandClaude Opus 4.7 34d1a89d56 0.10.7: fire scripts at play-start + right-click duplicate script frame
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 19:39:26 +02:00
thandClaude Opus 4.7 0bbef66192 0.10.6: setButtonEnabled action — disable buttons from the timeline
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 19:17:48 +02:00
thandClaude Opus 4.7 d3c5b8b640 0.10.5: clicking + on an empty button slot moves the editor to that state
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 18:55:11 +02:00
thandClaude Opus 4.7 8895fc76ef 0.10.4: button still-frames render only on their own state
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 18:32:18 +02:00
thandClaude Opus 4.7 fcb8edbb88 0.10.3: preloader + multi-scene export parity, jumpToScene rewinds
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:43:44 +02:00
thandClaude Opus 4.7 87de1344c4 0.10.2: audio waveforms + sidebar section polish
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 13:45:34 +02:00
thandClaude Opus 4.7 c5b1dd566a 0.10.1: audio clip push-cascade + track-swap ghost, Media drawer rename
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 12:52:47 +02:00
thandClaude Opus 4.7 04f867547d 0.10.0: audio tracks — assets, per-scene lanes, editor + export playback
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 11:53:41 +02:00
thandClaude Opus 4.7 b9f2ba9e47 0.9.1: jumpToScene action + auto-play on scene switch
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 07:44:43 +02:00
thandClaude Opus 4.7 b57994c754 0.9.0: scenes — multi-page projects with right-side drawer
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 07:17:06 +02:00
thandClaude Opus 4.7 517705bf55 0.8.7: duplicate track via right-click (layer / clip / script)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 06:22:32 +02:00
thandClaude Opus 4.7 d60684edd1 0.8.6: clip instances get stage transforms (x/y/scale/rotation)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 06:13:09 +02:00
thandClaude Opus 4.7 53c0410824 0.8.5: multi-select tracks, group + convert-to-clip
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>
2026-04-22 05:19:09 +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 a2c8e5998a 0.8.2: pan outside canvas + finish RUWIO rename of exports
- 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>
2026-04-21 22:17:10 +02:00
thandClaude Opus 4.7 82ee5c98d0 0.8.1: dirty-state + Cmd/Ctrl+S, fullscreen plays to OUT, UX fixes
- Unsaved-changes indicator: a small accent dot appears on the save
  button whenever the scene has changed since the last save/load.
  Save/load/new all mark the project clean on next tick.
- Cmd/Ctrl+S saves the project (reuses the same flow as the button;
  prompts for a name on first save). Tooltip now mentions the
  shortcut.
- Fullscreen preview plays to the OUT marker (or scene.duration when
  no OUT is set) instead of looping at the last keyframe. Dropped
  the runtime's effectiveEnd helper; RUNTIME_VERSION 13 → 14.
- "Still frame" keyframes no longer disable easing. A held kf's
  easing still drives the segment INTO it from the previous kf, so
  you can ease into a pause. Only the first kf disables easing
  (nothing precedes it). Dropped `timeline.holdDisabledHint`.
- Settings dialog: hint text 11px → 13px (and dropped italic) so
  the inline help is readable; section headings 10px → 13px.
- Canvas stage shadow: `shadowCfg` always emits explicit values plus
  a `shadowEnabled` flag instead of returning `{}`. Konva's default
  shadowOpacity is 1, so bailing with `{}` could leave stale shadow
  attrs on the shape. Preview and editor now gate on the same
  condition deterministically.
- Sidebar empty-state copy: "No clips" → "No clips yet" to match
  "No buttons yet".

Regression tests: runtime rangeOut fallback is scene.duration (not
last kf); sidebar empty-states both contain "yet"; the
holdDisabledHint key is gone; saveProject tooltips mention the
shortcut.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:00:35 +02:00
thandClaude Opus 4.7 1e89ea86e0 0.8.0: group colour cascade, still-frame past end, project rename, UX polish
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>
2026-04-21 21:26:06 +02:00
thandClaude Opus 4.7 ac89f6762d tests: flip the dropzone smoke test to assert the new gated behavior
The old test expected `.clip-track-dropzone` to always be present.
The dropzone is now hidden until the clip library has at least one
clip, so the blank smoke scene must not contain it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 19:47:33 +02:00
thandClaude Opus 4.7 f29dc448fb fix: masks inside buttons render across all paths
Buttons used to ignore `isMask` on every render path — the mask
shape drew as normal content instead of clipping the button's
other layers. Every entry point now routes button children through
`drawMaskedGroup`:

- `drawLayers` nested-button branch
- new `drawButtonFrame` (used by the mask-preview overlay in
  button-edit mode so the preview reflects the button's own
  stage instead of the main scene)
- `renderButtonThumbnail` so sidebar previews match the stage
- exported-runtime button branch mirrors the split; bumped
  `RUNTIME_VERSION` to '11' so cached players reload

Also:

- Timeline clip-track dropzone hides until the library has at
  least one clip, and the hint stays centred in the visible
  ruler area instead of drifting with horizontal scroll.
- Regression tests pin positive/negative button masks in
  `drawButtonFrame` and verify `drawSceneFrame` applies
  button-internal masks when the button sits on the main stage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 19:43:23 +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 31604052ca revert macro-clips button feature (keep markers + scripts)
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>
2026-04-20 23:37:49 +02:00
thandClaude Opus 4.7 616973b0fa feat: click-outside deselects a selected marker
Esc was already wired but nobody stumbles onto it. A capture-phase
pointerdown listener is installed while selectedMarkerId is set; any
click that doesn't land on a .timeline-marker (or on an open
dialog/menu) clears the selection. Listener is torn down automatically
when selection clears or the component unmounts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 22:20:58 +02:00
thandClaude Opus 4.7 4cb3a74dfb style: selected marker turns orange
Bold label + ring alone weren't enough to distinguish a selected
marker from the default red flags. The .selected rule now overrides
--marker-color to #f59e0b (amber) with !important so per-marker color
overrides also yield to selection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 22:18:22 +02:00
thandClaude Opus 4.7 fe1a7f9b3b fix: marker template was overriding the red default with --accent
The inline style wrote --marker-color: var(--accent) whenever
marker.color wasn't set, which shadowed the CSS default. Only emit
the inline override when a per-marker color is explicitly set so the
stylesheet's red (#ef4444) actually applies.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 22:16:29 +02:00
thandClaude Opus 4.7 28e6e5bcfc style: marker now uses pointer cursor, bold-not-underline on select, red by default
- cursor: pointer signals the click-to-jump affordance more clearly
  than ew-resize (which suggested "drag only")
- selected marker switches to font-weight 700 on the label instead of
  underlining it
- default --marker-color is now a classic red (#ef4444). User can
  still override per-marker via setMarkerColor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 22:14:56 +02:00
thandClaude Opus 4.7 06ca0b4d0e fix: marker double-click blocked by scrub handle overlap
After a single click the playhead snapped to the marker's time, which
moved the scrub handle's hit area directly on top of the marker flag.
The second click then landed on the handle (z-index 20) instead of
the marker (z-index 18), so dblclick never fired. Raised marker
z-index to 21 so marker-hits win at overlapping positions — still
below modals/popups (900+).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 22:12:36 +02:00
thandClaude Opus 4.7 198e1c7837 style: hide the default white scrollbar corner
::-webkit-scrollbar-corner was unstyled so the browser filled the
intersection of horizontal + vertical scrollbars with its default
color, which read as a ~10×10 white square in the dark theme.
Transparent matches the already-transparent track.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 22:10:35 +02:00
thandClaude Opus 4.7 d0ea7e54ee tests: replace the stale DSL-not-implemented-yet warn test
The test was written in slice 5 when evalAction for type:'script' was
a warn stub. Slice 8 wired it up to the actual parser, so the original
assertion (1 warning) no longer holds. Replaces it with two tests:
valid DSL source fires the parsed sub-actions without warnings, and
invalid lines surface a warning without blocking the valid ones.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 22:04:52 +02:00
thandClaude Opus 4.7 22b553e931 tests: fill macro-clips coverage gaps (+35 tests)
Refactor + new tests triggered by an explicit coverage audit of the
macro-clips feature set.

- extracted blendButtonStates into src/lib/buttonBlend.ts so it can be
  tested without mounting CanvasStage; the component now delegates to
  it. Behaviour unchanged.
- store coverage: removeMarker / setMarkerTime / setMarkerColor on
  unknown ids are no-ops, addMarker accepts an initial color,
  removeScriptFrame / setScriptFrameActions / setScriptFrameTime on
  unknown ids are no-ops, removeButtonInstance keeps the clip,
  addButtonInstance defaults x/y to stage center,
  setButtonInstancePosition writes through, setButtonInstanceLabel('')
  clears, setButtonClipTransitions stores + deletes.
- button runtime: applyButtonStateChange is a no-op when the target
  equals the current state, honors the persisted disabled flag as the
  from-state, reset() clears transitions alongside state/disabled.
  store.actionRuntime warns on unknown button refs.
- blendButtonStates: matched layer lerps props (incl. fill color),
  unmatched from-layer fades out, unmatched to-layer fades in,
  u=0/u=1 boundary conditions, missing asset id resolves are skipped.
- DSL edge cases: empty source, comments-only, trailing blank lines,
  wrong state ref prefix, disable without arg, jump @ without name.
- tick firing robustness: multi-track frames fire in time order,
  actions within a frame fire in list order, scriptFramesInRange
  open-closed boundary is exact.

Macro-clips suites now total 76 tests across 11 groups; grand total
277 tests in the runner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 22:00:58 +02:00
thandClaude Opus 4.7 6471c446df docs: testing walkthrough for the macro-clips feature set
Covers the end-to-end flows per slice: markers (m shortcut + scrub-
ruler interactions), script tracks/frames (open dialog, action types,
forward-only firing rules), button clip creation and drag-to-track,
ButtonInstance dialog (label, onClick, disabled, transitions), canvas
runtime (pointer state machine, drag-out cancel behavior, scripted
state flips), transitions (matched layer blend vs crossfade, easing),
the DSL language reference with examples, a quick sanity pass for the
HTML export runtime, and which in-app test suites to look for.

Also documents the two known limitations (no in-app button state
authoring, scripts not baked into the server PNG/MP4 render) and a
small devtools cheat sheet for poking the pinia store directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 21:52:12 +02:00
thandClaude Opus 4.7 1fc9eb72be 0.7.0: macro clips, scripts, markers, button state transitions
Headline features across 11 slices (see individual commits for details):

- Markers on the scrub ruler (name, color, drag, m shortcut, used as
  jump targets in scripts/actions)
- Script tracks with action frames, full action authoring dialog with
  a reusable ActionEditor component
- Button clips (definitions + placed instances), 4-cell timeline
  rendering, sidebar authoring, drag-to-track, per-instance label and
  onClick editing
- Button canvas runtime: hover/pressed/disabled state machine, onClick
  execution via evalAction, visual state changes and state-to-state
  transitions with per-transition duration + easing
- Action union: play/pause/rewindToLoop/jumpTo/jumpToFrame/
  jumpToMarker/setButtonState/setButtonDisabled/script
- Script DSL parser for the `script` action: play/pause/rewind,
  jump to seconds/#frame/@marker, state btn:label <state>,
  enable/disable btn:label, // comments
- Forward-only script frame firing tied to natural playback tick (no
  firing on scrub or loop wrap)
- Export runtime (exportPlayer.ts + server renderer) mirrors the
  editor: evalAction, button rendering with canvas-space hit testing,
  transitions, DSL parsing. RUNTIME_VERSION bumped from 1 → 6.

Known gap: buttons can be created and dragged onto tracks but their
per-state visuals can't be customized in-app yet (the seed gives a
four-colour rect per state). State-authoring mode is a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 21:49:53 +02:00
thandClaude Opus 4.7 68ea8576b6 macro-clips slice 7: button state transitions
- 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>
2026-04-20 21:48:50 +02:00
thandClaude Opus 4.7 f61f90d613 macro-clips slice 4d: button canvas runtime + pointer handling
- 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>
2026-04-20 21:40:02 +02:00
thandClaude Opus 4.7 024883943b macro-clips slice 4c: button creation in sidebar + drag to clip tracks
- 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>
2026-04-20 21:31:41 +02:00
thandClaude Opus 4.7 fb58cd18f7 macro-clips slice 8: script DSL parser + runtime label resolution
- 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>
2026-04-20 21:20:15 +02:00
thandClaude Opus 4.7 94ebbac238 macro-clips slice 4b: button instance dialog
- ButtonInstanceDialog.vue edits the three instance-scope fields:
  label (with uniqueness error surface), disabled flag, onClick
  actions via the shared ActionEditor
- commit waits on a successful label set so collisions show inline
  instead of silently dropping the rename
- Timeline wires click on a button-instance block to open the dialog

Creation/placement UX (sidebar entry + drag-to-track) and the per-state
layer editor are still deferred — this slice only makes existing
instances editable once something (devtools, later slice) has put them
on a clip track.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 21:16:25 +02:00
thandClaude Opus 4.7 ec8d25a351 macro-clips slice 6: export runtime fires script frames
- exportPlayer engine refactored from elapsed-since-start to an explicit
  currentT / lastNow / paused model so actions can jump, pause, and
  resume cleanly
- every frame: advance currentT by dt, fire script frames whose time
  sits in (prev, next], sorted; on loop wrap, fire frames up to rangeOut
  then snap to rangeIn without firing across the wrap
- evalAction mirrors src/lib/actions.ts (play/pause/rewindToLoop/
  jumpTo/jumpToFrame/jumpToMarker/setButtonState/setButtonDisabled/
  script); marker map built from scene.markers; unknown markers warn
  via window.console
- RUNTIME_VERSION bumped to '3' so old cached engines don't kick in

Server render.html (PNG-sequence path) stays untouched — scripted
interactivity is a runtime-only concern, the non-interactive server
export should not bake it in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 21:10:18 +02:00
thandClaude Opus 4.7 8817a73c3d macro-clips slice 5: action evaluator + forward-only script frame firing
- 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>
2026-04-20 21:00:59 +02:00
thandClaude Opus 4.7 b9d1ca16f7 macro-clips slice 4a: script frame dialog + reusable action editor
- ActionEditor.vue — reusable list editor for Action[]: add/remove/reorder,
  per-type parameter forms (jump to time/frame/marker, set button
  state/disabled, script source). Marker and button dropdowns pull from
  the store's current scene.
- ScriptFrameDialog.vue — modal wrapping ActionEditor plus time and label
  inputs; commits on close, supports deletion.
- Timeline wires the dialog: clicking a script frame opens it for edit,
  clicking the empty script lane creates a new frame at that time and
  opens the editor.
- i18n keys for all action types, parameters, and dialog labels.

Button-authoring UI (definitions + instance placement + properties panel)
is its own slice (4b) — this commit ships script authoring only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:54:24 +02:00
thandClaude Opus 4.7 92d58937e3 macro-clips slice 3: timeline rendering for scripts and button instances
- TimelineScriptTrack added to the track union; tracks computed emits one
  per ScriptTrack in trackOrder, with the show-locked filter honoring its
  locked flag
- script track row: drag handle + PhCode name badge + lock button; lane
  renders each ScriptFrame as an orange dot with optional action-count
  badge
- button instances render inside clip-track lanes as 4 cells labeled
  N/H/P/D, colored per state, with a striped pattern on disabled and a
  fixed visual width per cell
- new scrub-bar button to add a script track (PhCode icon)
- type narrowing tightened so inGroup checks skip script/group tracks
  explicitly rather than relying on the negation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:49:59 +02:00
thandClaude Opus 4.7 373a835576 macro-clips slice 2: marker UI on the scrub ruler
- marker flags render on the scrub ruler with color, label, and a dimmed
  vertical guideline; z-index sits below brackets and playhead
- click marker to jump the playhead, drag to retime (snaps), double-click
  to rename via prompt dialog, 'm' adds a marker at the playhead and
  opens the rename prompt
- rename collision shows a dialog and loops back into the prompt
- selectedMarkerId tracks the active marker; Delete/Backspace removes it,
  Escape clears the selection
- document keydown now gates m/Delete/Backspace on editable targets but
  keeps Escape universal

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:42:45 +02:00
thandClaude Opus 4.7 a75e9151ed macro-clips slice 1: types + store for buttons, scripts, markers
- 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>
2026-04-20 20:34:44 +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 69de48ea8d 0.5.6: show/hide locked tracks + group lock + crop/mask-preview split
- 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>
2026-04-20 16:42:39 +02:00
thandClaude Opus 4.7 63042b284d 0.5.5: inline lock toggle, proper lock indentation
- Track lock becomes a small inline icon toggle sitting next to the
  track label, replacing the static badge and removing the context
  menu's lock/unlock entry.
- Locked tracks are fully inert: the lane and name have pointer-events
  disabled, only the lock button itself stays clickable. The pile of
  hover-neutralizing CSS rules is gone.
- Lock button is icon-only (no background/border box) and swaps to the
  opposite icon on hover (filled unlock when hovering a locked track,
  so the click affordance is obvious).
- Locked tracks in groups keep their indentation — the drag-handle
  switches from display:none to visibility:hidden so the reserved
  space still aligns with unlocked sibling layers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:37:40 +02:00
thandClaude Opus 4.7 6786ad739a 0.5.4: scrub head tapers into the playhead needle
Pentagon clip-path on the scrub handle head (flat top, straight sides
down to 55% height, triangle point at the bottom center) so the head
visually funnels into the 1px playhead needle below. box-shadow
replaced with a drop-shadow filter that respects the clip-path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:19:27 +02:00
thandClaude Opus 4.7 48f92015e4 0.5.3: timeline name-col resize + playhead layering
- Track-name resize strip starts below the scrub bar and sits on the
  name-column side of the boundary, so scrubbing at time=0 is no
  longer intercepted by the resize hit area.
- Playhead (lane) and scrub handle get z-index 20 so they stay on top
  of the sticky track-name column (z-index 5) and the resize strip.
  The playhead stays fully visible at time=0 and when the lane is
  scrolled under the sticky column.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:13:33 +02:00
thandClaude Opus 4.7 07b0ef2659 0.5.2: paste images, shared import helper, clipart persistence
- Clipboard paste creates an image layer: a document-level paste
  listener detects image items in the clipboard (Strg+V after a
  screenshot, e.g.) and imports them via the shared helper. Skipped
  when focus is in a real text input / textarea / contenteditable.
- Shared importImageFile() helper in fileLoader.ts replaces the
  duplicated image-import logic in AssetSidebar and CanvasStage's
  clipart-drop path. Optional position argument for drop-at-point.
- Clipart folder persistence hardened for reloads:
  - initialize() is now safely re-runnable (no-ops only once a handle
    is actually loaded, tolerates refresh failures so the folder name
    survives even if permission isn't auto-granted)
  - App.vue's onMounted kicks off initialize() so it runs regardless
    of whether the drawer component has mounted
  - DocumentSettings re-invokes initialize() each time the modal
    opens — covers app-start races and gives the user gesture needed
    for FS Access permission prompts
  - Opening the drawer refreshes thumbnails when handle is set but
    items are empty, so the user-gesture context restores permission

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:05:48 +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 869b67458a 0.4.0: clipart drawer, animation paths, bezier keyframes
- 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>
2026-04-20 10:35:43 +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 43933f93d1 bak 2026-04-20 00:29:12 +02:00