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>
This commit is contained in:
1 parent
616973b0fa
commit
31604052ca
16 files changed
+84
-2809
No files matched your search
+2
-254
@@ -1,6 +1,6 @@
|
||||
import type { Scene } from '@/types/scene'
|
||||
|
||||
const RUNTIME_VERSION = '6'
|
||||
const RUNTIME_VERSION = '7'
|
||||
|
||||
const ENGINE_SOURCE = `(function(){
|
||||
if (window.AnimatrPlayer && window.AnimatrPlayer.__v === '${RUNTIME_VERSION}') return;
|
||||
@@ -197,174 +197,9 @@ const ENGINE_SOURCE = `(function(){
|
||||
jumpTo: null,
|
||||
forcePause: false,
|
||||
forcePlay: false,
|
||||
buttonStates: {},
|
||||
buttonDisabled: {},
|
||||
buttonLabels: {},
|
||||
warn: function(m){ if(window.console)window.console.warn('[animatr]',m); }
|
||||
};
|
||||
(scene.markers||[]).forEach(function(mk){ runtime.markers[mk.name]=mk.time; });
|
||||
(scene.buttonInstances||[]).forEach(function(bi){
|
||||
if(bi.label) runtime.buttonLabels[bi.label]=bi.id;
|
||||
});
|
||||
function resolveButtonRef(ref){
|
||||
// Try as real instance id first, then as label.
|
||||
var bis=scene.buttonInstances||[];
|
||||
for(var bri=0;bri<bis.length;bri++) if(bis[bri].id===ref) return ref;
|
||||
return runtime.buttonLabels[ref]||null;
|
||||
}
|
||||
var buttonHits=[]; // refreshed per draw()
|
||||
function getEffectiveButtonState(bi){
|
||||
var ovD=runtime.buttonDisabled[bi.id];
|
||||
var isDis=ovD===undefined?!!bi.disabled:ovD;
|
||||
if(isDis) return 'disabled';
|
||||
return runtime.buttonStates[bi.id]||'normal';
|
||||
}
|
||||
function setButtonState(id, st){
|
||||
if(st==='normal') delete runtime.buttonStates[id];
|
||||
else runtime.buttonStates[id]=st;
|
||||
}
|
||||
var buttonTransitions={}; // id -> { from, to, startedAt, duration, easing }
|
||||
function startButtonTransition(id, from, to, duration, easing){
|
||||
buttonTransitions[id]={from:from, to:to, startedAt:performance.now(), duration:duration, easing:easing};
|
||||
}
|
||||
function easingProgressRt(u, e){
|
||||
if(u<=0) return 0; if(u>=1) return 1;
|
||||
if(e==='easeIn') return u*u;
|
||||
if(e==='easeOut') return 1-(1-u)*(1-u);
|
||||
if(e==='easeInOut') return u<0.5?2*u*u:1-Math.pow(-2*u+2,2)/2;
|
||||
return u;
|
||||
}
|
||||
function sampleButtonTransition(id){
|
||||
var tr=buttonTransitions[id]; if(!tr) return null;
|
||||
var dur=Math.max(0.0001, tr.duration);
|
||||
var raw=(performance.now()-tr.startedAt)/1000/dur;
|
||||
if(raw>=1){ delete buttonTransitions[id]; return { u:1, from:tr.from, to:tr.to, done:true }; }
|
||||
return { u:easingProgressRt(raw, tr.easing), from:tr.from, to:tr.to, done:false };
|
||||
}
|
||||
function applyButtonChange(bi, clip, to){
|
||||
var from=getEffectiveButtonState(bi);
|
||||
if(from===to) return;
|
||||
var trs=clip.transitions||[];
|
||||
for(var ti=0; ti<trs.length; ti++){
|
||||
if(trs[ti].from===from && trs[ti].to===to && trs[ti].duration>0){
|
||||
startButtonTransition(bi.id, from, to, trs[ti].duration, trs[ti].easing);
|
||||
break;
|
||||
}
|
||||
}
|
||||
setButtonState(bi.id, to);
|
||||
}
|
||||
function lerpScalar(a,b,t){return a+(b-a)*t;}
|
||||
function lerpRuntimeProps(a,b,t){
|
||||
var out={
|
||||
x:lerpScalar(a.x,b.x,t),
|
||||
y:lerpScalar(a.y,b.y,t),
|
||||
scaleX:lerpScalar(a.scaleX,b.scaleX,t),
|
||||
scaleY:lerpScalar(a.scaleY,b.scaleY,t),
|
||||
rotation:lerpScalar(a.rotation,b.rotation,t),
|
||||
opacity:lerpScalar(a.opacity,b.opacity,t)
|
||||
};
|
||||
if(a.fill&&b.fill) out.fill=lerpColor(a.fill,b.fill,t);
|
||||
else if(b.fill) out.fill=b.fill;
|
||||
else if(a.fill) out.fill=a.fill;
|
||||
return out;
|
||||
}
|
||||
function drawButtonStatesBlended(clip, from, to, u){
|
||||
var fromLs=(clip.states&&clip.states[from])||[];
|
||||
var toLs=(clip.states&&clip.states[to])||[];
|
||||
var toByName={};
|
||||
for(var ii=0; ii<toLs.length; ii++) toByName[toLs[ii].name]=toLs[ii];
|
||||
var fromByName={};
|
||||
for(var ij=0; ij<fromLs.length; ij++) fromByName[fromLs[ij].name]=fromLs[ij];
|
||||
// Unmatched from-layers fade out.
|
||||
for(var fi=0; fi<fromLs.length; fi++){
|
||||
var fl=fromLs[fi];
|
||||
if(toByName[fl.name]) continue;
|
||||
var flp=sample(fl,0);
|
||||
drawLayer(fl, 0, { opacity: flp.opacity*(1-u) });
|
||||
}
|
||||
for(var ti2=0; ti2<toLs.length; ti2++){
|
||||
var tl=toLs[ti2];
|
||||
var fm=fromByName[tl.name];
|
||||
if(fm){
|
||||
var ap=sample(fm,0);
|
||||
var bp=sample(tl,0);
|
||||
drawLayer(tl, 0, lerpRuntimeProps(ap,bp,u));
|
||||
} else {
|
||||
var tlp=sample(tl,0);
|
||||
drawLayer(tl, 0, { opacity: tlp.opacity*u });
|
||||
}
|
||||
}
|
||||
}
|
||||
function pointerToSceneXY(e){
|
||||
var rc=canvas.getBoundingClientRect();
|
||||
var lw=scene.canvas.width, lh=scene.canvas.height;
|
||||
var cw=size.w, ch=size.h;
|
||||
if(!cw||!ch||!rc.width||!rc.height) return null;
|
||||
var sc=Math.min(cw/lw, ch/lh);
|
||||
var ox=(cw-lw*sc)/2, oy=(ch-lh*sc)/2;
|
||||
var cx=(e.clientX-rc.left)/rc.width*cw;
|
||||
var cy=(e.clientY-rc.top)/rc.height*ch;
|
||||
return { x:(cx-ox)/sc, y:(cy-oy)/sc };
|
||||
}
|
||||
function hitButtonAt(p){
|
||||
for(var i=buttonHits.length-1; i>=0; i--){
|
||||
var h=buttonHits[i];
|
||||
var lx=p.x-(h.bi.x||0), ly=p.y-(h.bi.y||0);
|
||||
if(lx>=h.hit.left && lx<=h.hit.right && ly>=h.hit.top && ly<=h.hit.bottom) return h;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function findClipForInstance(bi){
|
||||
var cs=scene.buttonClips||[];
|
||||
for(var i=0;i<cs.length;i++) if(cs[i].id===bi.buttonClipId) return cs[i];
|
||||
return null;
|
||||
}
|
||||
function onStageMove(e){
|
||||
var p=pointerToSceneXY(e);
|
||||
if(!p) return;
|
||||
var hit=hitButtonAt(p);
|
||||
for(var i=0;i<buttonHits.length;i++){
|
||||
var bh=buttonHits[i];
|
||||
if(getEffectiveButtonState(bh.bi)==='disabled') continue;
|
||||
var clip=findClipForInstance(bh.bi);
|
||||
if(!clip) continue;
|
||||
if(hit && hit.bi.id===bh.bi.id){
|
||||
if(getEffectiveButtonState(bh.bi)==='normal') applyButtonChange(bh.bi, clip, 'hover');
|
||||
} else {
|
||||
var cur=getEffectiveButtonState(bh.bi);
|
||||
if(cur==='hover'||cur==='pressed') applyButtonChange(bh.bi, clip, 'normal');
|
||||
}
|
||||
}
|
||||
}
|
||||
function onStageDown(e){
|
||||
var p=pointerToSceneXY(e);
|
||||
if(!p) return;
|
||||
var hit=hitButtonAt(p);
|
||||
if(!hit) return;
|
||||
if(getEffectiveButtonState(hit.bi)==='disabled') return;
|
||||
var clip=findClipForInstance(hit.bi);
|
||||
if(clip) applyButtonChange(hit.bi, clip, 'pressed');
|
||||
}
|
||||
function onStageUp(e){
|
||||
var p=pointerToSceneXY(e);
|
||||
if(!p) return;
|
||||
for(var i=0;i<buttonHits.length;i++){
|
||||
var bh=buttonHits[i];
|
||||
if(getEffectiveButtonState(bh.bi)!=='pressed') continue;
|
||||
var lx=p.x-(bh.bi.x||0), ly=p.y-(bh.bi.y||0);
|
||||
var inside=lx>=bh.hit.left&&lx<=bh.hit.right&&ly>=bh.hit.top&&ly<=bh.hit.bottom;
|
||||
var clip=findClipForInstance(bh.bi);
|
||||
if(clip) applyButtonChange(bh.bi, clip, inside?'hover':'normal');
|
||||
if(inside){
|
||||
var acts=bh.bi.onClick||[];
|
||||
for(var ai=0; ai<acts.length; ai++) evalAction(acts[ai]);
|
||||
}
|
||||
}
|
||||
}
|
||||
canvas.addEventListener('mousemove', onStageMove);
|
||||
canvas.addEventListener('mousedown', onStageDown);
|
||||
canvas.addEventListener('mouseup', onStageUp);
|
||||
var BUTTON_STATES=['normal','hover','pressed','disabled'];
|
||||
function parseScriptSource(source){
|
||||
var outActs=[];
|
||||
var outErrs=[];
|
||||
@@ -399,18 +234,6 @@ const ENGINE_SOURCE = `(function(){
|
||||
else parsed={type:'jumpTo',time:tm};
|
||||
}
|
||||
}
|
||||
} else if(cmd==='state'){
|
||||
if(toks.length<3) err='state: expected btn:<label> <state>';
|
||||
else if(toks[1].indexOf('btn:')!==0) err='state: expected btn:<label>';
|
||||
else {
|
||||
var st=toks[2];
|
||||
if(BUTTON_STATES.indexOf(st)<0) err='state: unknown state '+st;
|
||||
else parsed={type:'setButtonState',instanceId:toks[1].slice(4),state:st};
|
||||
}
|
||||
} else if(cmd==='enable'||cmd==='disable'){
|
||||
if(toks.length<2) err=cmd+': missing btn:<label>';
|
||||
else if(toks[1].indexOf('btn:')!==0) err=cmd+': expected btn:<label>';
|
||||
else parsed={type:'setButtonDisabled',instanceId:toks[1].slice(4),disabled:cmd==='disable'};
|
||||
} else {
|
||||
err='unknown command "'+cmd+'"';
|
||||
}
|
||||
@@ -432,26 +255,6 @@ const ENGINE_SOURCE = `(function(){
|
||||
runtime.jumpTo=Math.max(0,Math.min(runtime.duration,mt));
|
||||
return;
|
||||
}
|
||||
case 'setButtonState': {
|
||||
var sid=resolveButtonRef(act.instanceId);
|
||||
if(!sid){ runtime.warn('Unknown button ref: '+act.instanceId); return; }
|
||||
var sBi=null, sClip=null;
|
||||
var sBis=scene.buttonInstances||[];
|
||||
for(var sbI=0; sbI<sBis.length; sbI++) if(sBis[sbI].id===sid){ sBi=sBis[sbI]; break; }
|
||||
if(sBi){
|
||||
var sClips=scene.buttonClips||[];
|
||||
for(var scI=0; scI<sClips.length; scI++) if(sClips[scI].id===sBi.buttonClipId){ sClip=sClips[scI]; break; }
|
||||
}
|
||||
if(sBi && sClip) applyButtonChange(sBi, sClip, act.state);
|
||||
else setButtonState(sid, act.state);
|
||||
return;
|
||||
}
|
||||
case 'setButtonDisabled': {
|
||||
var did=resolveButtonRef(act.instanceId);
|
||||
if(!did){ runtime.warn('Unknown button ref: '+act.instanceId); return; }
|
||||
runtime.buttonDisabled[did]=act.disabled;
|
||||
return;
|
||||
}
|
||||
case 'script': {
|
||||
var parsed=parseScriptSource(act.source);
|
||||
if(parsed.errors.length){
|
||||
@@ -573,11 +376,10 @@ const ENGINE_SOURCE = `(function(){
|
||||
}
|
||||
ctxm.restore();
|
||||
}
|
||||
function drawLayer(layer,time,propsOverride){
|
||||
function drawLayer(layer,time){
|
||||
var asset=assetMap[layer.assetId];
|
||||
if(!asset||!layer.visible)return;
|
||||
var p=sample(layer,time);
|
||||
if(propsOverride){ for(var poKey in propsOverride) p[poKey]=propsOverride[poKey]; }
|
||||
ctx.save();
|
||||
ctx.globalAlpha=ctx.globalAlpha*p.opacity;
|
||||
ctx.translate(p.x,p.y);
|
||||
@@ -807,60 +609,6 @@ const ENGINE_SOURCE = `(function(){
|
||||
ctx.drawImage(topOff,0,0);
|
||||
ctx.restore();
|
||||
}
|
||||
// Draw button instances on top of everything else, and record their
|
||||
// scene-space hit rectangles for pointer handling.
|
||||
buttonHits.length=0;
|
||||
var bInsts=scene.buttonInstances||[];
|
||||
var bClips=scene.buttonClips||[];
|
||||
var bClipById={};
|
||||
for(var bcI=0;bcI<bClips.length;bcI++) bClipById[bClips[bcI].id]=bClips[bcI];
|
||||
for(var biI=0;biI<bInsts.length;biI++){
|
||||
var bi=bInsts[biI];
|
||||
if(time<bi.startTime) continue;
|
||||
var bclp=bClipById[bi.buttonClipId];
|
||||
if(!bclp) continue;
|
||||
var bst=getEffectiveButtonState(bi);
|
||||
var btr=sampleButtonTransition(bi.id);
|
||||
var bx=bi.x||0, by=bi.y||0;
|
||||
ctx.save();
|
||||
ctx.translate(bx, by);
|
||||
if(btr && !btr.done && bst!=='disabled'){
|
||||
drawButtonStatesBlended(bclp, btr.from, btr.to, btr.u);
|
||||
} else {
|
||||
var bStack=(bclp.states&&bclp.states[bst])||[];
|
||||
for(var blI=0; blI<bStack.length; blI++) drawLayer(bStack[blI], 0);
|
||||
}
|
||||
ctx.restore();
|
||||
// Hit rect: explicit hitArea on clip, else bbox of first rect/circle
|
||||
// layer in the normal state.
|
||||
var hit=null;
|
||||
if(bclp.hitArea){
|
||||
var ox2=bclp.hitArea.offsetX==null?(bclp.hitArea.width/2):bclp.hitArea.offsetX;
|
||||
var oy2=bclp.hitArea.offsetY==null?(bclp.hitArea.height/2):bclp.hitArea.offsetY;
|
||||
hit={left:-ox2, top:-oy2, right:bclp.hitArea.width-ox2, bottom:bclp.hitArea.height-oy2};
|
||||
}
|
||||
if(!hit){
|
||||
var normalLs=(bclp.states&&bclp.states.normal)||[];
|
||||
for(var nli=0; nli<normalLs.length; nli++){
|
||||
var nl=normalLs[nli];
|
||||
var na=assetMap[nl.assetId];
|
||||
if(!na) continue;
|
||||
if(na.kind==='rect'||na.kind==='image'){
|
||||
var rox=nl.offsetX==null?na.width/2:nl.offsetX;
|
||||
var roy=nl.offsetY==null?na.height/2:nl.offsetY;
|
||||
hit={left:-rox, top:-roy, right:na.width-rox, bottom:na.height-roy};
|
||||
break;
|
||||
}
|
||||
if(na.kind==='circle'){
|
||||
var cox=nl.offsetX||0, coy=nl.offsetY||0;
|
||||
hit={left:cox-na.radius, top:coy-na.radius, right:cox+na.radius, bottom:coy+na.radius};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!hit) hit={left:-60, top:-20, right:60, bottom:20};
|
||||
buttonHits.push({bi:bi, hit:hit});
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
loadImages(scene.assets).then(function(loaded){
|
||||
|
||||
Reference in new issue
Block a user