fix: masks inside buttons render across all paths

Buttons used to ignore `isMask` on every render path — the mask
shape drew as normal content instead of clipping the button's
other layers. Every entry point now routes button children through
`drawMaskedGroup`:

- `drawLayers` nested-button branch
- new `drawButtonFrame` (used by the mask-preview overlay in
  button-edit mode so the preview reflects the button's own
  stage instead of the main scene)
- `renderButtonThumbnail` so sidebar previews match the stage
- exported-runtime button branch mirrors the split; bumped
  `RUNTIME_VERSION` to '11' so cached players reload

Also:

- Timeline clip-track dropzone hides until the library has at
  least one clip, and the hint stays centred in the visible
  ruler area instead of drifting with horizontal scroll.
- Regression tests pin positive/negative button masks in
  `drawButtonFrame` and verify `drawSceneFrame` applies
  button-internal masks when the button sits on the main stage.

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 19:43:23 +02:00
1 parent c3764201b8
commit f29dc448fb
6 files changed
+232 -30

No files matched your search

+34 -5
View File
@@ -1,6 +1,6 @@
import type { Scene } from '@/types/scene'
const RUNTIME_VERSION = '10'
const RUNTIME_VERSION = '11'
const ENGINE_SOURCE = `(function(){
if (window.AnimatrPlayer && window.AnimatrPlayer.__v === '${RUNTIME_VERSION}') return;
@@ -444,11 +444,11 @@ const ENGINE_SOURCE = `(function(){
}
if(closed) tctx.closePath();
}
function drawMaskShape(ctxm,layer,time){
function drawMaskShape(ctxm,layer,time,alwaysActive){
var asset=assetMap[layer.assetId];
if(!asset) return;
if(asset.kind!=='rect' && asset.kind!=='circle' && asset.kind!=='text' && asset.kind!=='polygon') return;
if(!layerActiveAt(layer,time)) return;
if(!alwaysActive && !layerActiveAt(layer,time)) return;
var p=sample(layer,time);
ctxm.save();
ctxm.translate(p.x,p.y);
@@ -617,8 +617,37 @@ const ENGINE_SOURCE = `(function(){
var bState=resolveButtonState(layer);
var stateTime=bState==='hover'?2:(bState==='pressed'?3:(bState==='disabled'?4:1));
var bLayers=asset.layers||[];
for(var bli=bLayers.length-1;bli>=0;bli--){
drawLayer(bLayers[bli], stateTime, true);
var bContent=[], bMasks=[];
for(var bli=0;bli<bLayers.length;bli++){
if(bLayers[bli].isMask) bMasks.push(bLayers[bli]); else bContent.push(bLayers[bli]);
}
if(!bMasks.length){
for(var bli2=bContent.length-1;bli2>=0;bli2--){
drawLayer(bContent[bli2], stateTime, true);
}
} else {
var bcanvas=document.createElement('canvas');
bcanvas.width=canvas.width; bcanvas.height=canvas.height;
var bctx=bcanvas.getContext('2d');
var btm=ctx.getTransform();
bctx.setTransform(btm.a,btm.b,btm.c,btm.d,btm.e,btm.f);
var savedBCtx=ctx;
ctx=bctx;
for(var bli3=bContent.length-1;bli3>=0;bli3--){
drawLayer(bContent[bli3], stateTime, true);
}
for(var bmk=0;bmk<bMasks.length;bmk++){
var bML=bMasks[bmk];
bctx.save();
bctx.globalCompositeOperation=(bML.maskMode==='negative'?'destination-out':'destination-in');
drawMaskShape(bctx,bML,stateTime,true);
bctx.restore();
}
ctx=savedBCtx;
ctx.save();
ctx.setTransform(1,0,0,1,0,0);
ctx.drawImage(bcanvas,0,0);
ctx.restore();
}
}
ctx.shadowColor='transparent';