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
@@ -16,7 +16,7 @@ import { useSceneStore } from '@/stores/scene'
|
||||
import { isLayerActiveAt, sampleLayer } from '@/lib/interpolate'
|
||||
import { importImageFile, loadImage } from '@/lib/fileLoader'
|
||||
import { useClipartStore } from '@/stores/clipart'
|
||||
import { drawSceneFrame, loadSceneImages } from '@/lib/render'
|
||||
import { drawButtonFrame, drawSceneFrame, loadSceneImages } from '@/lib/render'
|
||||
import type {
|
||||
Action,
|
||||
AnimatableProps,
|
||||
@@ -2697,9 +2697,24 @@ function drawPreview() {
|
||||
ctx.save()
|
||||
ctx.translate(viewX.value, viewY.value)
|
||||
ctx.scale(zoom.value, zoom.value)
|
||||
drawSceneFrame(ctx, store.scene, store.playhead, previewImages.value, {
|
||||
crop: cropObjects.value,
|
||||
})
|
||||
// In button-edit mode the preview should reflect the *button*'s stage,
|
||||
// not the main scene behind it. `store.playhead` is already pinned to
|
||||
// BUTTON_STATE_TIMES[activeButtonState] (or to normal in hit mode), so
|
||||
// we can pass it straight through.
|
||||
if (store.activeButtonAsset) {
|
||||
drawButtonFrame(
|
||||
ctx,
|
||||
store.activeButtonAsset,
|
||||
store.scene,
|
||||
store.playhead,
|
||||
previewImages.value,
|
||||
{ background: '#1a1a1f' },
|
||||
)
|
||||
} else {
|
||||
drawSceneFrame(ctx, store.scene, store.playhead, previewImages.value, {
|
||||
crop: cropObjects.value,
|
||||
})
|
||||
}
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +102,30 @@ const duration = computed(() =>
|
||||
store.activeButtonAssetId ? 5 : store.activeDuration,
|
||||
)
|
||||
const inClipEdit = computed(() => store.activeClipId !== null)
|
||||
|
||||
// Only surface the "drop a clip to add a new track" dropzone when the user
|
||||
// has at least one clip in the library — otherwise the hint is confusing
|
||||
// (you can't drop what you don't have).
|
||||
const showClipDropzone = computed(
|
||||
() =>
|
||||
!inClipEdit.value &&
|
||||
!store.activeButtonAssetId &&
|
||||
(store.scene.clips?.length ?? 0) > 0,
|
||||
)
|
||||
|
||||
// Keep the dropzone hint horizontally centred in the *visible* ruler area
|
||||
// (right of the sticky name-spacer) regardless of horizontal scroll. The
|
||||
// dropzone itself is as wide as .tl-content, so its natural flex-center
|
||||
// is not at the visual center — we position the hint absolutely at
|
||||
// `scrollLeft + trackNameWidth + (visibleRulerWidth / 2)`.
|
||||
const dropzoneHintStyle = computed(() => {
|
||||
const tn = trackNameWidth.value
|
||||
const vp = viewportWidth.value
|
||||
if (vp <= 0) return {}
|
||||
const visibleRulerW = Math.max(0, vp - tn)
|
||||
const leftPx = scrollLeft.value + tn + visibleRulerW / 2
|
||||
return { left: `${leftPx}px` }
|
||||
})
|
||||
const clipInstancesByTrack = computed<Record<string, ClipInstance[]>>(() => {
|
||||
const out: Record<string, ClipInstance[]> = {}
|
||||
for (const ci of store.scene.clipInstances ?? []) {
|
||||
@@ -2710,7 +2734,7 @@ onBeforeUnmount(() => {
|
||||
{{ store.activeButtonAssetId ? t('timeline.emptyButtonState') : t('timeline.noTracks') }}
|
||||
</p>
|
||||
<div
|
||||
v-if="!inClipEdit && !store.activeButtonAssetId"
|
||||
v-if="showClipDropzone"
|
||||
class="track clip-track-dropzone"
|
||||
:class="{ hot: clipDragOverDropzone }"
|
||||
@dragenter="onNewTrackDragEnter"
|
||||
@@ -2718,7 +2742,9 @@ onBeforeUnmount(() => {
|
||||
@dragleave="onNewTrackDragLeave"
|
||||
@drop="onNewTrackDrop"
|
||||
>
|
||||
<span class="dropzone-hint">{{ t('timeline.dropNewTrackHint') }}</span>
|
||||
<span class="dropzone-hint" :style="dropzoneHintStyle">
|
||||
{{ t('timeline.dropNewTrackHint') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4118,6 +4144,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.clip-track-dropzone {
|
||||
position: relative;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -4138,9 +4165,12 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.dropzone-hint {
|
||||
position: sticky;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
/* `left` is driven from a computed so the hint stays in the centre of
|
||||
the *visible* ruler area (i.e. centred in the user's viewport, right
|
||||
of the sticky name-spacer) regardless of horizontal scroll. */
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user