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>
This commit is contained in:
thandClaude Opus 4.7 committed 2026-04-21 22:00:35 +02:00
1 parent 1e89ea86e0
commit 82ee5c98d0
10 files changed
+133 -48

No files matched your search

+7 -26
View File
@@ -1,6 +1,6 @@
import type { Scene } from '@/types/scene'
const RUNTIME_VERSION = '13'
const RUNTIME_VERSION = '14'
const ENGINE_SOURCE = `(function(){
if (window.AnimatrPlayer && window.AnimatrPlayer.__v === '${RUNTIME_VERSION}') return;
@@ -128,36 +128,17 @@ const ENGINE_SOURCE = `(function(){
})).then(function(){return imgs;});
}
function effectiveEnd(scene){
var max=0;
for(var i=0;i<scene.layers.length;i++){
var kfs=scene.layers[i].keyframes;
for(var j=0;j<kfs.length;j++){ if(kfs[j].time>max) max=kfs[j].time; }
}
var instances=scene.clipInstances||[];
var clips=scene.clips||[];
var clipById={};
for(var ci=0;ci<clips.length;ci++) clipById[clips[ci].id]=clips[ci];
for(var ii=0;ii<instances.length;ii++){
var inst=instances[ii];
var cl=clipById[inst.clipId];
if(!cl) continue;
var trimS=inst.trimStart||0;
var trimE=inst.trimEnd||0;
var end=inst.startTime+Math.max(0,cl.duration-trimS-trimE);
if(end>max) max=end;
}
if(max<=0) return scene.duration;
return Math.min(max,scene.duration);
}
function mount(container,scene,opts){
if(!container)return;
opts=opts||{};
var loop=opts.loop!==false;
// Honour explicit in/out points; fall back to 0 and the animation's own end.
// Honour explicit in/out points. When outTime isn't set, play the
// full timeline (scene.duration) — matches the editor's playbackOut
// getter. Using the last keyframe as a fallback made still-frame
// endings and trailing silence (intentionally empty space before
// the OUT mark) unreachable, so the preview looped early.
var rangeIn=Math.max(0, Math.min(scene.duration, scene.inTime==null?0:scene.inTime));
var autoEnd=effectiveEnd(scene);
var rangeOut=Math.max(rangeIn, Math.min(scene.duration, scene.outTime==null?autoEnd:scene.outTime));
var rangeOut=Math.max(rangeIn, Math.min(scene.duration, scene.outTime==null?scene.duration:scene.outTime));
var sceneEnd=rangeOut;
var cs=getComputedStyle(container);
if(cs.position==='static')container.style.position='relative';