- 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>
- 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.
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.