0.8.6: clip instances get stage transforms (x/y/scale/rotation)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
53c0410824
commit
d60684edd1
7 files changed
+512
-41
No files matched your search
+127
-34
@@ -23,6 +23,7 @@ import type {
|
||||
Asset,
|
||||
ButtonAsset,
|
||||
ButtonState,
|
||||
ClipInstance,
|
||||
HitShape,
|
||||
Keyframe,
|
||||
Layer,
|
||||
@@ -728,9 +729,14 @@ interface RenderedClipLayer extends RenderedLayer {
|
||||
instanceId: string
|
||||
}
|
||||
|
||||
interface RenderedClipInstance {
|
||||
instance: ClipInstance
|
||||
items: RenderedClipLayer[]
|
||||
}
|
||||
|
||||
type StackItem =
|
||||
| { kind: 'layer'; item: RenderedLayer }
|
||||
| { kind: 'clip'; item: RenderedClipLayer }
|
||||
| { kind: 'clipInstance'; bundle: RenderedClipInstance }
|
||||
|
||||
function applyLivePose(props: AnimatableProps, layerId: string): AnimatableProps {
|
||||
const live = liveLayerPose.value
|
||||
@@ -833,23 +839,26 @@ const stackItems = computed<StackItem[]>(() => {
|
||||
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
|
||||
// Bundle the clip's layers under a single clip-instance entry
|
||||
// so the template can wrap them in a draggable Konva group that
|
||||
// carries the instance's x / y / scale transform.
|
||||
const items: RenderedClipLayer[] = []
|
||||
for (let k = clip.layers.length - 1; k >= 0; k--) {
|
||||
const cl = clip.layers[k]
|
||||
if (!cl.visible) continue
|
||||
if (!isLayerActiveAt(cl, localTime)) 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),
|
||||
},
|
||||
items.push({
|
||||
instanceId: inst.id,
|
||||
layer: cl,
|
||||
asset: ca,
|
||||
props: sampleLayer(cl, localTime),
|
||||
})
|
||||
}
|
||||
if (items.length) {
|
||||
out.push({ kind: 'clipInstance', bundle: { instance: inst, items } })
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
@@ -1224,6 +1233,50 @@ function imageConfig(
|
||||
}
|
||||
}
|
||||
|
||||
/** Per-clip-instance wrapper group. Carries the instance's static
|
||||
* x / y / scale so the user can reposition the packed clip on the
|
||||
* stage without touching its inner keyframes. `rotation` is
|
||||
* deliberately absent — the user asked for move + scale only.
|
||||
* The inner child configs keep `listening: false`, so pointer
|
||||
* events funnel to the outer group and drag/transform apply as a
|
||||
* unit. */
|
||||
function clipInstanceGroupConfig(inst: ClipInstance) {
|
||||
return {
|
||||
id: `ci-group:${inst.id}`,
|
||||
x: inst.x ?? 0,
|
||||
y: inst.y ?? 0,
|
||||
scaleX: inst.scaleX ?? 1,
|
||||
scaleY: inst.scaleY ?? 1,
|
||||
rotation: inst.rotation ?? 0,
|
||||
draggable: true,
|
||||
}
|
||||
}
|
||||
|
||||
function onClipInstanceDragEnd(
|
||||
e: Konva.KonvaEventObject<DragEvent>,
|
||||
instanceId: string,
|
||||
) {
|
||||
const node = e.target
|
||||
store.setClipInstanceTransform(instanceId, {
|
||||
x: node.x(),
|
||||
y: node.y(),
|
||||
})
|
||||
}
|
||||
|
||||
function onClipInstanceTransformEnd(
|
||||
e: Konva.KonvaEventObject<Event>,
|
||||
instanceId: string,
|
||||
) {
|
||||
const node = e.target
|
||||
store.setClipInstanceTransform(instanceId, {
|
||||
scaleX: node.scaleX(),
|
||||
scaleY: node.scaleY(),
|
||||
rotation: node.rotation(),
|
||||
x: node.x(),
|
||||
y: node.y(),
|
||||
})
|
||||
}
|
||||
|
||||
function clipBaseConfig(instanceId: string, layer: Layer, props: AnimatableProps) {
|
||||
return {
|
||||
id: `ci:${instanceId}:${layer.id}`,
|
||||
@@ -1235,7 +1288,11 @@ function clipBaseConfig(instanceId: string, layer: Layer, props: AnimatableProps
|
||||
opacity: props.opacity,
|
||||
visible: layer.visible,
|
||||
draggable: false,
|
||||
listening: false,
|
||||
// Keep clip children hit-testable so a click on the clip's
|
||||
// visible shape bubbles up to the parent group's @mousedown
|
||||
// selection handler. They don't carry their own handlers, so
|
||||
// the event bubbles straight up.
|
||||
listening: true,
|
||||
...shadowCfg(props),
|
||||
}
|
||||
}
|
||||
@@ -1808,7 +1865,9 @@ function onPivotDragEnd(e: Konva.KonvaEventObject<DragEvent>) {
|
||||
}
|
||||
|
||||
const transformerConfig = {
|
||||
rotateEnabled: false,
|
||||
rotateEnabled: true,
|
||||
rotationSnaps: [0, 45, 90, 135, 180, -135, -90, -45],
|
||||
rotationSnapTolerance: 4,
|
||||
enabledAnchors: [
|
||||
'top-left',
|
||||
'top-center',
|
||||
@@ -1837,6 +1896,22 @@ function attachTransformer(retriesLeft = 3) {
|
||||
const stage = stageRef.value?.getStage()
|
||||
const tr = transformerRef.value?.getNode()
|
||||
if (!stage || !tr) return
|
||||
|
||||
// Clip-instance selection takes precedence — the transformer wraps
|
||||
// the group so its move + scale handles update the instance's
|
||||
// static x / y / scale.
|
||||
const clipInstId = store.selectedClipInstanceId
|
||||
if (clipInstId) {
|
||||
const groupNode = stage.findOne<Konva.Node>(`#ci-group:${clipInstId}`)
|
||||
if (!groupNode && retriesLeft > 0) {
|
||||
attachRetryTimer = setTimeout(() => attachTransformer(retriesLeft - 1), 16)
|
||||
return
|
||||
}
|
||||
tr.nodes(groupNode ? [groupNode] : [])
|
||||
tr.getLayer()?.batchDraw()
|
||||
return
|
||||
}
|
||||
|
||||
const id = store.selectedLayerId
|
||||
if (!id) {
|
||||
tr.nodes([])
|
||||
@@ -1865,7 +1940,13 @@ function attachTransformer(retriesLeft = 3) {
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [store.selectedLayerId, stackItems.value, store.playhead] as const,
|
||||
() =>
|
||||
[
|
||||
store.selectedLayerId,
|
||||
store.selectedClipInstanceId,
|
||||
stackItems.value,
|
||||
store.playhead,
|
||||
] as const,
|
||||
async () => {
|
||||
await nextTick()
|
||||
attachTransformer()
|
||||
@@ -2902,7 +2983,7 @@ onBeforeUnmount(() => {
|
||||
:key="
|
||||
entry.kind === 'layer'
|
||||
? `l:${entry.item.layer.id}`
|
||||
: `ci:${entry.item.instanceId}:${entry.item.layer.id}:${i}`
|
||||
: `ci:${entry.bundle.instance.id}:${i}`
|
||||
"
|
||||
>
|
||||
<template v-if="entry.kind === 'layer'">
|
||||
@@ -2989,26 +3070,38 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
</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-line
|
||||
v-else-if="entry.item.asset.kind === 'polygon'"
|
||||
:config="clipPolygonConfig(entry.item)"
|
||||
/>
|
||||
<v-image
|
||||
v-else-if="entry.item.asset.kind === 'image' && clipImageConfig(entry.item)"
|
||||
:config="clipImageConfig(entry.item)!"
|
||||
/>
|
||||
<v-group
|
||||
:config="clipInstanceGroupConfig(entry.bundle.instance)"
|
||||
@mousedown="store.selectClipInstance(entry.bundle.instance.id)"
|
||||
@dragend="onClipInstanceDragEnd($event, entry.bundle.instance.id)"
|
||||
@transformend="onClipInstanceTransformEnd($event, entry.bundle.instance.id)"
|
||||
>
|
||||
<template
|
||||
v-for="sub in entry.bundle.items"
|
||||
:key="`ci:${entry.bundle.instance.id}:${sub.layer.id}`"
|
||||
>
|
||||
<v-rect
|
||||
v-if="sub.asset.kind === 'rect'"
|
||||
:config="clipRectConfig(sub)"
|
||||
/>
|
||||
<v-circle
|
||||
v-else-if="sub.asset.kind === 'circle'"
|
||||
:config="clipCircleConfig(sub)"
|
||||
/>
|
||||
<v-text
|
||||
v-else-if="sub.asset.kind === 'text'"
|
||||
:config="clipTextConfig(sub)"
|
||||
/>
|
||||
<v-line
|
||||
v-else-if="sub.asset.kind === 'polygon'"
|
||||
:config="clipPolygonConfig(sub)"
|
||||
/>
|
||||
<v-image
|
||||
v-else-if="sub.asset.kind === 'image' && clipImageConfig(sub)"
|
||||
:config="clipImageConfig(sub)!"
|
||||
/>
|
||||
</template>
|
||||
</v-group>
|
||||
</template>
|
||||
</template>
|
||||
<template
|
||||
|
||||
Reference in new issue
Block a user