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>
This commit is contained in:
1 parent
31604052ca
commit
c3764201b8
16 files changed
+2560
-349
No files matched your search
@@ -128,6 +128,39 @@ function sampleKeyframeSet(
|
||||
return { ...base }
|
||||
}
|
||||
|
||||
/**
|
||||
* Union range of all keyframe times on a layer (main + position + rotation
|
||||
* tracks). A layer is considered "alive" only within this range.
|
||||
*
|
||||
* Returns null (→ always visible) when the layer has zero or one total
|
||||
* keyframes — a single keyframe is a static layer with no animation range.
|
||||
*/
|
||||
export function layerActiveRange(
|
||||
layer: Layer,
|
||||
): { min: number; max: number } | null {
|
||||
const main = layer.keyframes || []
|
||||
const pos = layer.positionKeyframes || []
|
||||
const rot = layer.rotationKeyframes || []
|
||||
const total = main.length + pos.length + rot.length
|
||||
if (total <= 1) return null
|
||||
let min = Infinity
|
||||
let max = -Infinity
|
||||
for (const arr of [main, pos, rot]) {
|
||||
for (const k of arr) {
|
||||
if (k.time < min) min = k.time
|
||||
if (k.time > max) max = k.time
|
||||
}
|
||||
}
|
||||
return { min, max }
|
||||
}
|
||||
|
||||
export function isLayerActiveAt(layer: Layer, time: number): boolean {
|
||||
const range = layerActiveRange(layer)
|
||||
if (!range) return true
|
||||
const EPSILON = 1e-6
|
||||
return time >= range.min - EPSILON && time <= range.max + EPSILON
|
||||
}
|
||||
|
||||
export function sampleLayer(layer: Layer, time: number): AnimatableProps {
|
||||
const main = sampleKeyframeSet(layer.keyframes, layer.base, time)
|
||||
if (!layer.separated) return main
|
||||
|
||||
Reference in new issue
Block a user