This commit is contained in:
th committed 2026-04-19 21:13:53 +02:00
1 parent cc6b11e61d
commit b977ee35a8
11 files changed
+1641 -310

No files matched your search

+199 -43
View File
@@ -126,21 +126,86 @@ interface RenderedLayer {
props: AnimatableProps
}
const rendered = computed<RenderedLayer[]>(() =>
store.scene.layers
.slice()
.reverse()
.map((layer) => {
interface RenderedClipLayer extends RenderedLayer {
instanceId: string
}
type StackItem =
| { kind: 'layer'; item: RenderedLayer }
| { kind: 'clip'; item: RenderedClipLayer }
const stackItems = computed<StackItem[]>(() => {
// Clip-edit scope: just render active clip's layers (top of timeline = top of canvas)
if (store.activeClipId) {
const out: StackItem[] = []
const layers = store.activeLayers.slice().reverse()
for (const layer of layers) {
const asset = assetMap.value.get(layer.assetId)
if (!asset) return null
return {
layer,
asset,
props: sampleLayer(layer, store.playhead),
if (!asset) continue
out.push({
kind: 'layer',
item: { layer, asset, props: sampleLayer(layer, store.playhead) },
})
}
return out
}
const out: StackItem[] = []
const layerById = new Map<string, Layer>()
for (const l of store.scene.layers) layerById.set(l.id, l)
const clipById = new Map<string, NonNullable<typeof store.scene.clips>[number]>()
for (const c of store.scene.clips ?? []) clipById.set(c.id, c)
const instancesByTrack = new Map<string, NonNullable<typeof store.scene.clipInstances>>()
for (const ci of store.scene.clipInstances ?? []) {
const arr = instancesByTrack.get(ci.clipTrackId) ?? []
arr.push(ci)
instancesByTrack.set(ci.clipTrackId, arr)
}
const order = store.scene.trackOrder ?? store.scene.layers.map((l) => l.id)
// iterate bottom → top: reverse the array
for (let i = order.length - 1; i >= 0; i--) {
const id = order[i]
const layer = layerById.get(id)
if (layer) {
const asset = assetMap.value.get(layer.assetId)
if (!asset) continue
out.push({
kind: 'layer',
item: { layer, asset, props: sampleLayer(layer, store.playhead) },
})
continue
}
const instances = instancesByTrack.get(id) ?? []
for (const inst of instances) {
const clip = clipById.get(inst.clipId)
if (!clip) continue
const trimStart = inst.trimStart ?? 0
const trimEnd = inst.trimEnd ?? 0
const visible = Math.max(0, clip.duration - trimStart - trimEnd)
const offsetIntoBlock = store.playhead - inst.startTime
if (offsetIntoBlock < 0 || offsetIntoBlock > visible) continue
const localTime = trimStart + offsetIntoBlock
// Clip's own layers: reverse too so layer[0] ends up on top
for (let k = clip.layers.length - 1; k >= 0; k--) {
const cl = clip.layers[k]
if (!cl.visible) continue
const ca = assetMap.value.get(cl.assetId)
if (!ca) continue
out.push({
kind: 'clip',
item: {
instanceId: inst.id,
layer: cl,
asset: ca,
props: sampleLayer(cl, localTime),
},
})
}
})
.filter((x): x is RenderedLayer => x !== null),
)
}
}
return out
})
function baseNodeConfig(layer: Layer, props: AnimatableProps) {
return {
@@ -209,6 +274,70 @@ function imageConfig(
}
}
function clipBaseConfig(instanceId: string, layer: Layer, props: AnimatableProps) {
return {
id: `ci:${instanceId}:${layer.id}`,
x: props.x,
y: props.y,
scaleX: props.scaleX,
scaleY: props.scaleY,
rotation: props.rotation,
opacity: props.opacity,
visible: layer.visible,
draggable: false,
listening: false,
}
}
function clipRectConfig(item: RenderedClipLayer) {
const asset = item.asset as Extract<Asset, { kind: 'rect' }>
return {
...clipBaseConfig(item.instanceId, item.layer, item.props),
width: asset.width,
height: asset.height,
offsetX: item.layer.offsetX ?? asset.width / 2,
offsetY: item.layer.offsetY ?? asset.height / 2,
fill: item.props.fill ?? asset.fill,
}
}
function clipCircleConfig(item: RenderedClipLayer) {
const asset = item.asset as Extract<Asset, { kind: 'circle' }>
return {
...clipBaseConfig(item.instanceId, item.layer, item.props),
radius: asset.radius,
offsetX: item.layer.offsetX ?? 0,
offsetY: item.layer.offsetY ?? 0,
fill: item.props.fill ?? asset.fill,
}
}
function clipTextConfig(item: RenderedClipLayer) {
const asset = item.asset as Extract<Asset, { kind: 'text' }>
return {
...clipBaseConfig(item.instanceId, item.layer, item.props),
text: asset.text,
fontSize: asset.fontSize,
offsetX: item.layer.offsetX ?? 0,
offsetY: item.layer.offsetY ?? 0,
fill: item.props.fill ?? asset.fill,
}
}
function clipImageConfig(item: RenderedClipLayer) {
const asset = item.asset as Extract<Asset, { kind: 'image' }>
const img = imageCache.value[asset.id]
if (!img) return null
return {
...clipBaseConfig(item.instanceId, item.layer, item.props),
image: img,
width: asset.width,
height: asset.height,
offsetX: item.layer.offsetX ?? asset.width / 2,
offsetY: item.layer.offsetY ?? asset.height / 2,
}
}
interface PivotInfo {
layerId: string
sampled: AnimatableProps
@@ -217,7 +346,7 @@ interface PivotInfo {
const pivotInfo = computed<PivotInfo | null>(() => {
const id = store.selectedLayerId
if (!id) return null
const layer = store.scene.layers.find((l) => l.id === id)
const layer = store.activeLayers.find((l) => l.id === id)
if (!layer) return null
const asset = assetMap.value.get(layer.assetId)
if (!asset) return null
@@ -303,7 +432,7 @@ function attachTransformer() {
}
watch(
() => [store.selectedLayerId, rendered.value.length, store.playhead] as const,
() => [store.selectedLayerId, stackItems.value.length, store.playhead] as const,
async () => {
await nextTick()
attachTransformer()
@@ -332,14 +461,14 @@ function handleStageMouseDown(e: Konva.KonvaEventObject<MouseEvent>) {
store.selectLayer(null)
return
}
const match = store.scene.layers.find((l) => l.id === id)
const match = store.activeLayers.find((l) => l.id === id)
store.selectLayer(match ? match.id : null)
}
function handleDragEnd(e: Konva.KonvaEventObject<DragEvent>) {
const node = e.target
const id = node.id()
if (!store.scene.layers.some((l) => l.id === id)) return
if (!store.activeLayers.some((l) => l.id === id)) return
store.upsertKeyframe(id, store.playhead, {
x: node.x(),
y: node.y(),
@@ -349,7 +478,7 @@ function handleDragEnd(e: Konva.KonvaEventObject<DragEvent>) {
function handleTransformEnd(e: Konva.KonvaEventObject<Event>) {
const node = e.target
const id = node.id()
if (!store.scene.layers.some((l) => l.id === id)) return
if (!store.activeLayers.some((l) => l.id === id)) return
store.upsertKeyframe(id, store.playhead, {
x: node.x(),
y: node.y(),
@@ -530,31 +659,58 @@ onBeforeUnmount(() => {
<v-group :config="viewConfig">
<v-group :config="clipGroupConfig">
<v-rect :config="backgroundConfig" />
<template v-for="item in rendered" :key="item.layer.id">
<v-rect
v-if="item.asset.kind === 'rect'"
:config="rectConfig(item.layer, item.asset, item.props)"
@dragend="handleDragEnd"
@transformend="handleTransformEnd"
/>
<v-circle
v-else-if="item.asset.kind === 'circle'"
:config="circleConfig(item.layer, item.asset, item.props)"
@dragend="handleDragEnd"
@transformend="handleTransformEnd"
/>
<v-text
v-else-if="item.asset.kind === 'text'"
:config="textConfig(item.layer, item.asset, item.props)"
@dragend="handleDragEnd"
@transformend="handleTransformEnd"
/>
<v-image
v-else-if="item.asset.kind === 'image' && imageConfig(item.layer, item.asset, item.props)"
:config="imageConfig(item.layer, item.asset, item.props)!"
@dragend="handleDragEnd"
@transformend="handleTransformEnd"
/>
<template
v-for="(entry, i) in stackItems"
:key="
entry.kind === 'layer'
? `l:${entry.item.layer.id}`
: `ci:${entry.item.instanceId}:${entry.item.layer.id}:${i}`
"
>
<template v-if="entry.kind === 'layer'">
<v-rect
v-if="entry.item.asset.kind === 'rect'"
:config="rectConfig(entry.item.layer, entry.item.asset, entry.item.props)"
@dragend="handleDragEnd"
@transformend="handleTransformEnd"
/>
<v-circle
v-else-if="entry.item.asset.kind === 'circle'"
:config="circleConfig(entry.item.layer, entry.item.asset, entry.item.props)"
@dragend="handleDragEnd"
@transformend="handleTransformEnd"
/>
<v-text
v-else-if="entry.item.asset.kind === 'text'"
:config="textConfig(entry.item.layer, entry.item.asset, entry.item.props)"
@dragend="handleDragEnd"
@transformend="handleTransformEnd"
/>
<v-image
v-else-if="entry.item.asset.kind === 'image' && imageConfig(entry.item.layer, entry.item.asset, entry.item.props)"
:config="imageConfig(entry.item.layer, entry.item.asset, entry.item.props)!"
@dragend="handleDragEnd"
@transformend="handleTransformEnd"
/>
</template>
<template v-else>
<v-rect
v-if="entry.item.asset.kind === 'rect'"
:config="clipRectConfig(entry.item)"
/>
<v-circle
v-else-if="entry.item.asset.kind === 'circle'"
:config="clipCircleConfig(entry.item)"
/>
<v-text
v-else-if="entry.item.asset.kind === 'text'"
:config="clipTextConfig(entry.item)"
/>
<v-image
v-else-if="entry.item.asset.kind === 'image' && clipImageConfig(entry.item)"
:config="clipImageConfig(entry.item)!"
/>
</template>
</template>
</v-group>
<v-rect :config="canvasBorderConfig" />