0.6.0: background video, in/out range, top-level masks, track icons
- background video layer (document-level), syncs to playhead in editor and fullscreen - playback in/out brackets on the scrub ruler; i/o set them, r rewinds to in-point - togglePlay, tick, fullscreen loop, WebM + server FFmpeg export all honor [in, out] - top-level mask layers cascade over everything below until a group boundary - fullscreen preview slaves localTime to the bg video so it stops stuttering - welcome "new project" now actually loads a blank scene (was a no-op) - default canvas background hardened to white via normalizeScene - new keyframe button (+) in the name-spacer, backspace/delete removes selected kfs - drag-end auto-selects the created/shifted keyframe so the path is visible - animation path updates live during drag and spans every selected keyframe - track rows show an icon per asset kind (rect/circle/image/text/polygon/freeform) - default mask mode is negative, buttons read − then + - sticky track-name columns raised above playhead/brackets; orange bracket styling - tests for playback range, blank scene, mask defaults, cascading top-level masks Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
69de48ea8d
commit
d4c4830fe2
17 files changed
+1332
-153
No files matched your search
+52
-5
@@ -345,8 +345,12 @@ export function drawSceneFrame(
|
||||
ctx.rect(0, 0, lw, lh)
|
||||
ctx.clip()
|
||||
}
|
||||
ctx.fillStyle = scene.canvas.background
|
||||
ctx.fillRect(0, 0, lw, lh)
|
||||
// A background video takes over the backdrop — keep the stage area
|
||||
// transparent so the DOM video underneath is visible.
|
||||
if (!scene.backgroundVideo) {
|
||||
ctx.fillStyle = scene.canvas.background
|
||||
ctx.fillRect(0, 0, lw, lh)
|
||||
}
|
||||
|
||||
const assetMap = new Map<string, Asset>()
|
||||
for (const a of scene.assets) assetMap.set(a.id, a)
|
||||
@@ -367,11 +371,47 @@ export function drawSceneFrame(
|
||||
const rawOrder = scene.trackOrder?.length
|
||||
? scene.trackOrder
|
||||
: scene.layers.map((l) => l.id)
|
||||
|
||||
// Top-level mask layers (i.e. not inside a group) mask everything below
|
||||
// them in the stack until the very bottom. Render everything into an
|
||||
// offscreen buffer so we can apply each mask to whatever has been drawn
|
||||
// so far; we only pay for this when at least one top-level mask exists.
|
||||
const hasTopLevelMask = rawOrder.some((id) => {
|
||||
const l = layerById.get(id)
|
||||
return !!l && !!l.isMask
|
||||
})
|
||||
|
||||
let drawCtx: CanvasRenderingContext2D = ctx
|
||||
let off: HTMLCanvasElement | null = null
|
||||
if (hasTopLevelMask) {
|
||||
off = document.createElement('canvas')
|
||||
off.width = ctx.canvas.width
|
||||
off.height = ctx.canvas.height
|
||||
const oc = off.getContext('2d')
|
||||
if (oc) {
|
||||
const m = ctx.getTransform()
|
||||
oc.setTransform(m.a, m.b, m.c, m.d, m.e, m.f)
|
||||
drawCtx = oc
|
||||
} else {
|
||||
off = null
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = rawOrder.length - 1; i >= 0; i--) {
|
||||
const id = rawOrder[i]
|
||||
const layer = layerById.get(id)
|
||||
if (layer) {
|
||||
drawLayers(ctx, [layer], assetMap, images, time)
|
||||
if (layer.isMask) {
|
||||
// Apply this mask to everything already drawn into the offscreen
|
||||
// buffer (the entire stack below it).
|
||||
drawCtx.save()
|
||||
drawCtx.globalCompositeOperation =
|
||||
layer.maskMode === 'negative' ? 'destination-out' : 'destination-in'
|
||||
drawMaskShape(drawCtx, layer, assetMap, time)
|
||||
drawCtx.restore()
|
||||
} else {
|
||||
drawLayers(drawCtx, [layer], assetMap, images, time)
|
||||
}
|
||||
continue
|
||||
}
|
||||
const instances = instancesByTrack.get(id) ?? []
|
||||
@@ -384,7 +424,7 @@ export function drawSceneFrame(
|
||||
const visible = Math.max(0, clip.duration - trimStart - trimEnd)
|
||||
const offsetIntoBlock = time - inst.startTime
|
||||
if (offsetIntoBlock < 0 || offsetIntoBlock > visible) continue
|
||||
drawLayers(ctx, clip.layers, assetMap, images, trimStart + offsetIntoBlock)
|
||||
drawLayers(drawCtx, clip.layers, assetMap, images, trimStart + offsetIntoBlock)
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -395,9 +435,16 @@ export function drawSceneFrame(
|
||||
const cl = layerById.get(group.childIds[k])
|
||||
if (cl) childLayers.push(cl)
|
||||
}
|
||||
drawMaskedGroup(ctx, childLayers, assetMap, images, time)
|
||||
drawMaskedGroup(drawCtx, childLayers, assetMap, images, time)
|
||||
}
|
||||
}
|
||||
|
||||
if (off) {
|
||||
ctx.save()
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
||||
ctx.drawImage(off, 0, 0)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
Reference in new issue
Block a user