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>
This commit is contained in:
1 parent
ac89f6762d
commit
1e89ea86e0
15 files changed
+849
-74
No files matched your search
+25
-3
@@ -1,6 +1,6 @@
|
||||
import type { Scene } from '@/types/scene'
|
||||
|
||||
const RUNTIME_VERSION = '11'
|
||||
const RUNTIME_VERSION = '13'
|
||||
|
||||
const ENGINE_SOURCE = `(function(){
|
||||
if (window.AnimatrPlayer && window.AnimatrPlayer.__v === '${RUNTIME_VERSION}') return;
|
||||
@@ -70,6 +70,20 @@ const ENGINE_SOURCE = `(function(){
|
||||
if(a.strokeWidth!=null&&b.strokeWidth!=null) out.strokeWidth=lerp(a.strokeWidth,b.strokeWidth,t);
|
||||
else if(b.strokeWidth!=null) out.strokeWidth=b.strokeWidth;
|
||||
else if(a.strokeWidth!=null) out.strokeWidth=a.strokeWidth;
|
||||
if(a.blur!=null && b.blur!=null) out.blur=lerp(a.blur,b.blur,t);
|
||||
else if(b.blur!=null) out.blur=b.blur;
|
||||
else if(a.blur!=null) out.blur=a.blur;
|
||||
var shadowNumKeys=['shadowBlur','shadowOpacity','shadowAngle','shadowDistance'];
|
||||
for(var sk=0;sk<shadowNumKeys.length;sk++){
|
||||
var skName=shadowNumKeys[sk];
|
||||
var sav=a[skName], sbv=b[skName];
|
||||
if(sav!=null && sbv!=null) out[skName]=lerp(sav,sbv,t);
|
||||
else if(sbv!=null) out[skName]=sbv;
|
||||
else if(sav!=null) out[skName]=sav;
|
||||
}
|
||||
if(a.shadowColor && b.shadowColor) out.shadowColor=lerpColor(a.shadowColor,b.shadowColor,t);
|
||||
else if(b.shadowColor) out.shadowColor=b.shadowColor;
|
||||
else if(a.shadowColor) out.shadowColor=a.shadowColor;
|
||||
return out;
|
||||
}
|
||||
function sample(layer,time){
|
||||
@@ -489,9 +503,17 @@ const ENGINE_SOURCE = `(function(){
|
||||
var rot=layer.rotationKeyframes||[];
|
||||
var total=main.length+pos.length+rot.length;
|
||||
if(total<=1) return true;
|
||||
var mn=Infinity, mx=-Infinity;
|
||||
function scan(arr){ for(var i=0;i<arr.length;i++){ var t=arr[i].time; if(t<mn) mn=t; if(t>mx) mx=t; } }
|
||||
var mn=Infinity, mx=-Infinity, lastHold=false;
|
||||
function scan(arr){
|
||||
for(var i=0;i<arr.length;i++){
|
||||
var k=arr[i], t=k.time;
|
||||
if(t<mn) mn=t;
|
||||
if(t>mx){ mx=t; lastHold=!!k.hold; }
|
||||
else if(t===mx && k.hold){ lastHold=true; }
|
||||
}
|
||||
}
|
||||
scan(main); scan(pos); scan(rot);
|
||||
if(lastHold) mx=Infinity;
|
||||
var EPS=1e-6;
|
||||
return time>=mn-EPS && time<=mx+EPS;
|
||||
}
|
||||
|
||||
+14
-1
@@ -134,6 +134,12 @@ function sampleKeyframeSet(
|
||||
*
|
||||
* Returns null (→ always visible) when the layer has zero or one total
|
||||
* keyframes — a single keyframe is a static layer with no animation range.
|
||||
*
|
||||
* Special case: when the keyframe with the latest time has `hold: true`,
|
||||
* the max is extended to Infinity — the layer stays alive past its last
|
||||
* keyframe, pinned to that keyframe's sampled value. That's how users
|
||||
* turn the final keyframe of a short anim into a still frame so the
|
||||
* layer stays on-screen for the remainder of the timeline.
|
||||
*/
|
||||
export function layerActiveRange(
|
||||
layer: Layer,
|
||||
@@ -145,12 +151,19 @@ export function layerActiveRange(
|
||||
if (total <= 1) return null
|
||||
let min = Infinity
|
||||
let max = -Infinity
|
||||
let lastHold = false
|
||||
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
|
||||
if (k.time > max) {
|
||||
max = k.time
|
||||
lastHold = !!k.hold
|
||||
} else if (k.time === max && k.hold) {
|
||||
lastHold = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lastHold) max = Infinity
|
||||
return { min, max }
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user