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:
1 parent
c3764201b8
commit
f29dc448fb
6 files changed
+232
-30
No files matched your search
+34
-5
@@ -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';
|
||||
|
||||
+37
-7
@@ -241,7 +241,7 @@ function drawLayers(
|
||||
const offY = layer.offsetY ?? asset.canvas.height / 2
|
||||
ctx.translate(-offX, -offY)
|
||||
const bs = buttonStates?.get(layer.id) ?? 'normal'
|
||||
drawLayers(ctx, asset.layers, assetMap, images, BUTTON_STATE_TIMES[bs], undefined, true)
|
||||
drawMaskedGroup(ctx, asset.layers, assetMap, images, BUTTON_STATE_TIMES[bs], undefined, true)
|
||||
}
|
||||
|
||||
clearShadow(ctx)
|
||||
@@ -254,6 +254,7 @@ function drawMaskShape(
|
||||
layer: Layer,
|
||||
assetMap: Map<string, Asset>,
|
||||
time: number,
|
||||
alwaysActive?: boolean,
|
||||
) {
|
||||
const asset = assetMap.get(layer.assetId)
|
||||
if (!asset) return
|
||||
@@ -264,7 +265,7 @@ function drawMaskShape(
|
||||
asset.kind !== 'polygon'
|
||||
)
|
||||
return
|
||||
if (!isLayerActiveAt(layer, time)) return
|
||||
if (!alwaysActive && !isLayerActiveAt(layer, time)) return
|
||||
const p = sampleLayer(layer, time)
|
||||
ctx.save()
|
||||
ctx.translate(p.x, p.y)
|
||||
@@ -307,11 +308,12 @@ function drawMaskedGroup(
|
||||
images: Record<string, HTMLImageElement>,
|
||||
time: number,
|
||||
buttonStates?: Map<string, ButtonState>,
|
||||
alwaysActive?: boolean,
|
||||
) {
|
||||
const masks = children.filter((l) => l.isMask)
|
||||
const content = children.filter((l) => !l.isMask)
|
||||
if (masks.length === 0) {
|
||||
drawLayers(ctx, children, assetMap, images, time, buttonStates)
|
||||
drawLayers(ctx, children, assetMap, images, time, buttonStates, alwaysActive)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -322,16 +324,17 @@ function drawMaskedGroup(
|
||||
off.height = h
|
||||
const offCtx = off.getContext('2d')
|
||||
if (!offCtx) {
|
||||
drawLayers(ctx, content, assetMap, images, time, buttonStates)
|
||||
drawLayers(ctx, content, assetMap, images, time, buttonStates, alwaysActive)
|
||||
return
|
||||
}
|
||||
const m = ctx.getTransform()
|
||||
offCtx.setTransform(m.a, m.b, m.c, m.d, m.e, m.f)
|
||||
drawLayers(offCtx, content, assetMap, images, time, buttonStates)
|
||||
drawLayers(offCtx, content, assetMap, images, time, buttonStates, alwaysActive)
|
||||
for (const mask of masks) {
|
||||
if (!alwaysActive && !isLayerActiveAt(mask, time)) continue
|
||||
offCtx.save()
|
||||
offCtx.globalCompositeOperation = mask.maskMode === 'negative' ? 'destination-out' : 'destination-in'
|
||||
drawMaskShape(offCtx, mask, assetMap, time)
|
||||
drawMaskShape(offCtx, mask, assetMap, time, alwaysActive)
|
||||
offCtx.restore()
|
||||
}
|
||||
ctx.save()
|
||||
@@ -481,6 +484,33 @@ export function renderButtonThumbnail(
|
||||
ctx.scale(scale, scale)
|
||||
const assetMap = new Map<string, Asset>()
|
||||
for (const a of scene.assets) assetMap.set(a.id, a)
|
||||
drawLayers(ctx, button.layers, assetMap, images, BUTTON_STATE_TIMES.normal, undefined, true)
|
||||
drawMaskedGroup(ctx, button.layers, assetMap, images, BUTTON_STATE_TIMES.normal, undefined, true)
|
||||
return canvas.toDataURL('image/png')
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a single button-asset frame into the supplied context. Used by the
|
||||
* mask-preview overlay in button-edit mode so the preview reflects the
|
||||
* button's own stage instead of the main scene. The caller is responsible
|
||||
* for applying viewport translate/zoom before calling.
|
||||
*/
|
||||
export function drawButtonFrame(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
button: ButtonAsset,
|
||||
scene: Scene,
|
||||
time: number,
|
||||
images: Record<string, HTMLImageElement>,
|
||||
options?: { background?: string },
|
||||
): void {
|
||||
const { width: w, height: h } = button.canvas
|
||||
if (w <= 0 || h <= 0) return
|
||||
ctx.save()
|
||||
if (options?.background !== undefined) {
|
||||
ctx.fillStyle = options.background
|
||||
ctx.fillRect(0, 0, w, h)
|
||||
}
|
||||
const assetMap = new Map<string, Asset>()
|
||||
for (const a of scene.assets) assetMap.set(a.id, a)
|
||||
drawMaskedGroup(ctx, button.layers, assetMap, images, time, undefined, true)
|
||||
ctx.restore()
|
||||
}
|
||||
Reference in new issue
Block a user