bak
This commit is contained in:
1 parent
cc6b11e61d
commit
b977ee35a8
11 files changed
+1641
-310
No files matched your search
+231
-33
@@ -1,5 +1,15 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { AnimatableProps, Asset, Easing, Keyframe, Layer, Scene } from '@/types/scene'
|
||||
import type {
|
||||
AnimatableProps,
|
||||
Asset,
|
||||
Clip,
|
||||
ClipInstance,
|
||||
ClipTrack,
|
||||
Easing,
|
||||
Keyframe,
|
||||
Layer,
|
||||
Scene,
|
||||
} from '@/types/scene'
|
||||
import { DEFAULT_BASE_PROPS } from '@/types/scene'
|
||||
import { sampleLayer } from '@/lib/interpolate'
|
||||
|
||||
@@ -14,6 +24,7 @@ interface State {
|
||||
selectedLayerId: string | null
|
||||
playhead: number
|
||||
playing: boolean
|
||||
activeClipId: string | null
|
||||
}
|
||||
|
||||
function allKeyframes(layer: Layer): Keyframe[] {
|
||||
@@ -90,39 +101,86 @@ function hasKeys(obj: Partial<AnimatableProps>): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
function normalizeScene(scene: Scene): Scene {
|
||||
scene.clips ||= []
|
||||
scene.clipTracks ||= []
|
||||
scene.clipInstances ||= []
|
||||
if (!scene.trackOrder) {
|
||||
scene.trackOrder = []
|
||||
for (const l of scene.layers) scene.trackOrder.push(l.id)
|
||||
for (const t of scene.clipTracks) scene.trackOrder.push(t.id)
|
||||
} else {
|
||||
const known = new Set<string>()
|
||||
for (const l of scene.layers) known.add(l.id)
|
||||
for (const t of scene.clipTracks) known.add(t.id)
|
||||
scene.trackOrder = scene.trackOrder.filter((id) => known.has(id))
|
||||
for (const l of scene.layers) if (!scene.trackOrder.includes(l.id)) scene.trackOrder.unshift(l.id)
|
||||
for (const t of scene.clipTracks) if (!scene.trackOrder.includes(t.id)) scene.trackOrder.push(t.id)
|
||||
}
|
||||
return scene
|
||||
}
|
||||
|
||||
export const useSceneStore = defineStore('scene', {
|
||||
state: (): State => ({
|
||||
scene: {
|
||||
scene: normalizeScene({
|
||||
version: 1,
|
||||
canvas: { width: 1280, height: 720, background: '#ffffff' },
|
||||
duration: 10,
|
||||
fps: 60,
|
||||
assets: [],
|
||||
layers: [],
|
||||
},
|
||||
}),
|
||||
selectedLayerId: null,
|
||||
playhead: 0,
|
||||
playing: false,
|
||||
activeClipId: null,
|
||||
}),
|
||||
getters: {
|
||||
lastKeyframeTime(state): number {
|
||||
let max = 0
|
||||
for (const layer of state.scene.layers) {
|
||||
for (const kf of allKeyframes(layer)) {
|
||||
if (kf.time > max) max = kf.time
|
||||
}
|
||||
}
|
||||
return Math.min(state.scene.duration, max)
|
||||
activeClip(state): Clip | null {
|
||||
if (!state.activeClipId) return null
|
||||
const clips = state.scene.clips ?? []
|
||||
return clips.find((c) => c.id === state.activeClipId) ?? null
|
||||
},
|
||||
effectiveEnd(state): number {
|
||||
activeLayers(): Layer[] {
|
||||
const clip = this.activeClip
|
||||
return clip ? clip.layers : this.scene.layers
|
||||
},
|
||||
activeDuration(): number {
|
||||
const clip = this.activeClip
|
||||
return clip ? clip.duration : this.scene.duration
|
||||
},
|
||||
lastKeyframeTime(): number {
|
||||
let max = 0
|
||||
for (const layer of state.scene.layers) {
|
||||
for (const layer of this.activeLayers) {
|
||||
for (const kf of allKeyframes(layer)) {
|
||||
if (kf.time > max) max = kf.time
|
||||
}
|
||||
}
|
||||
if (max <= 0) return state.scene.duration
|
||||
return Math.min(max, state.scene.duration)
|
||||
return Math.min(this.activeDuration, max)
|
||||
},
|
||||
effectiveEnd(): number {
|
||||
let max = 0
|
||||
for (const layer of this.activeLayers) {
|
||||
for (const kf of allKeyframes(layer)) {
|
||||
if (kf.time > max) max = kf.time
|
||||
}
|
||||
}
|
||||
if (!this.activeClipId) {
|
||||
const clipById = new Map<string, Clip>()
|
||||
for (const c of this.scene.clips ?? []) clipById.set(c.id, c)
|
||||
for (const inst of this.scene.clipInstances ?? []) {
|
||||
const clip = clipById.get(inst.clipId)
|
||||
if (!clip) continue
|
||||
const visible = Math.max(
|
||||
0,
|
||||
clip.duration - (inst.trimStart ?? 0) - (inst.trimEnd ?? 0),
|
||||
)
|
||||
const end = inst.startTime + visible
|
||||
if (end > max) max = end
|
||||
}
|
||||
}
|
||||
if (max <= 0) return this.activeDuration
|
||||
return Math.min(max, this.activeDuration)
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
@@ -163,16 +221,25 @@ export const useSceneStore = defineStore('scene', {
|
||||
layer.offsetY = 0
|
||||
}
|
||||
}
|
||||
this.scene.layers.unshift(layer)
|
||||
this.activeLayers.unshift(layer)
|
||||
if (!this.activeClipId) {
|
||||
const order = (this.scene.trackOrder ||= [])
|
||||
if (!order.includes(layer.id)) order.unshift(layer.id)
|
||||
}
|
||||
this.selectedLayerId = layer.id
|
||||
return layer
|
||||
},
|
||||
removeLayer(id: string) {
|
||||
this.scene.layers = this.scene.layers.filter((l) => l.id !== id)
|
||||
const layers = this.activeLayers
|
||||
const idx = layers.findIndex((l) => l.id === id)
|
||||
if (idx >= 0) layers.splice(idx, 1)
|
||||
if (!this.activeClipId && this.scene.trackOrder) {
|
||||
this.scene.trackOrder = this.scene.trackOrder.filter((x) => x !== id)
|
||||
}
|
||||
if (this.selectedLayerId === id) this.selectedLayerId = null
|
||||
},
|
||||
moveLayer(fromIndex: number, toIndex: number) {
|
||||
const arr = this.scene.layers
|
||||
const arr = this.activeLayers
|
||||
if (fromIndex < 0 || fromIndex >= arr.length) return
|
||||
if (toIndex < 0 || toIndex > arr.length) return
|
||||
const clamped = Math.min(toIndex, arr.length - 1)
|
||||
@@ -180,15 +247,30 @@ export const useSceneStore = defineStore('scene', {
|
||||
const [moved] = arr.splice(fromIndex, 1)
|
||||
arr.splice(clamped, 0, moved)
|
||||
},
|
||||
moveTrack(fromIndex: number, toIndex: number) {
|
||||
const order = this.scene.trackOrder ?? []
|
||||
if (fromIndex < 0 || fromIndex >= order.length) return
|
||||
if (toIndex < 0 || toIndex > order.length) return
|
||||
const clamped = Math.min(toIndex, order.length - 1)
|
||||
if (fromIndex === clamped) return
|
||||
const [moved] = order.splice(fromIndex, 1)
|
||||
order.splice(clamped, 0, moved)
|
||||
},
|
||||
selectLayer(id: string | null) {
|
||||
this.selectedLayerId = id
|
||||
},
|
||||
setPlayhead(t: number) {
|
||||
this.playhead = Math.max(0, Math.min(this.scene.duration, t))
|
||||
this.playhead = Math.max(0, Math.min(this.activeDuration, t))
|
||||
},
|
||||
setDuration(seconds: number) {
|
||||
this.scene.duration = Math.max(0.1, seconds)
|
||||
if (this.playhead > this.scene.duration) this.playhead = this.scene.duration
|
||||
const clip = this.activeClip
|
||||
if (clip) {
|
||||
clip.duration = Math.max(0.1, seconds)
|
||||
if (this.playhead > clip.duration) this.playhead = clip.duration
|
||||
} else {
|
||||
this.scene.duration = Math.max(0.1, seconds)
|
||||
if (this.playhead > this.scene.duration) this.playhead = this.scene.duration
|
||||
}
|
||||
},
|
||||
setFps(fps: number) {
|
||||
this.scene.fps = Math.max(1, Math.round(fps))
|
||||
@@ -201,7 +283,7 @@ export const useSceneStore = defineStore('scene', {
|
||||
this.scene.canvas.background = color
|
||||
},
|
||||
upsertKeyframe(layerId: string, time: number, props: Partial<AnimatableProps>) {
|
||||
const layer = this.scene.layers.find((l) => l.id === layerId)
|
||||
const layer = this.activeLayers.find((l) => l.id === layerId)
|
||||
if (!layer) return
|
||||
if (!layer.separated) {
|
||||
const snapshot = sampleLayer(layer, time)
|
||||
@@ -224,7 +306,7 @@ export const useSceneStore = defineStore('scene', {
|
||||
}
|
||||
},
|
||||
removeKeyframeAt(layerId: string, time: number) {
|
||||
const layer = this.scene.layers.find((l) => l.id === layerId)
|
||||
const layer = this.activeLayers.find((l) => l.id === layerId)
|
||||
if (!layer) return
|
||||
const filter = (kfs?: Keyframe[]) =>
|
||||
kfs ? kfs.filter((k) => Math.abs(k.time - time) >= KEYFRAME_EPSILON) : kfs
|
||||
@@ -233,7 +315,7 @@ export const useSceneStore = defineStore('scene', {
|
||||
if (layer.rotationKeyframes) layer.rotationKeyframes = filter(layer.rotationKeyframes)
|
||||
},
|
||||
removeKeyframe(layerId: string, keyframeId: string) {
|
||||
const layer = this.scene.layers.find((l) => l.id === layerId)
|
||||
const layer = this.activeLayers.find((l) => l.id === layerId)
|
||||
if (!layer) return
|
||||
const found = findKf(layer, keyframeId)
|
||||
if (!found) return
|
||||
@@ -241,13 +323,13 @@ export const useSceneStore = defineStore('scene', {
|
||||
if (idx >= 0) found.arr.splice(idx, 1)
|
||||
},
|
||||
setKeyframeEasing(layerId: string, keyframeId: string, easing: Easing) {
|
||||
const layer = this.scene.layers.find((l) => l.id === layerId)
|
||||
const layer = this.activeLayers.find((l) => l.id === layerId)
|
||||
if (!layer) return
|
||||
const found = findKf(layer, keyframeId)
|
||||
if (found) found.kf.easing = easing
|
||||
},
|
||||
setKeyframeHold(layerId: string, keyframeId: string, hold: boolean) {
|
||||
const layer = this.scene.layers.find((l) => l.id === layerId)
|
||||
const layer = this.activeLayers.find((l) => l.id === layerId)
|
||||
if (!layer) return
|
||||
const found = findKf(layer, keyframeId)
|
||||
if (!found) return
|
||||
@@ -255,11 +337,11 @@ export const useSceneStore = defineStore('scene', {
|
||||
else delete found.kf.hold
|
||||
},
|
||||
setKeyframeTime(layerId: string, keyframeId: string, newTime: number) {
|
||||
const layer = this.scene.layers.find((l) => l.id === layerId)
|
||||
const layer = this.activeLayers.find((l) => l.id === layerId)
|
||||
if (!layer) return
|
||||
const found = findKf(layer, keyframeId)
|
||||
if (!found) return
|
||||
found.kf.time = Math.max(0, Math.min(this.scene.duration, newTime))
|
||||
found.kf.time = Math.max(0, Math.min(this.activeDuration, newTime))
|
||||
sortKfs(found.arr)
|
||||
},
|
||||
shiftLayerPivot(
|
||||
@@ -269,7 +351,7 @@ export const useSceneStore = defineStore('scene', {
|
||||
dLocalX: number,
|
||||
dLocalY: number,
|
||||
) {
|
||||
const layer = this.scene.layers.find((l) => l.id === layerId)
|
||||
const layer = this.activeLayers.find((l) => l.id === layerId)
|
||||
if (!layer) return
|
||||
layer.offsetX = (layer.offsetX ?? 0) + dLocalX
|
||||
layer.offsetY = (layer.offsetY ?? 0) + dLocalY
|
||||
@@ -286,14 +368,14 @@ export const useSceneStore = defineStore('scene', {
|
||||
shift(layer.positionKeyframes)
|
||||
},
|
||||
duplicateKeyframe(layerId: string, keyframeId: string): string | null {
|
||||
const layer = this.scene.layers.find((l) => l.id === layerId)
|
||||
const layer = this.activeLayers.find((l) => l.id === layerId)
|
||||
if (!layer) return null
|
||||
const found = findKf(layer, keyframeId)
|
||||
if (!found) return null
|
||||
const { kf: source, arr } = found
|
||||
const fps = this.scene.fps > 0 ? this.scene.fps : 60
|
||||
const offset = Math.max(3 / fps, 0.1)
|
||||
const duration = this.scene.duration
|
||||
const duration = this.activeDuration
|
||||
const occupied = (t: number) => arr.some((k) => Math.abs(k.time - t) < KEYFRAME_EPSILON)
|
||||
let target = source.time + offset
|
||||
while (occupied(target) && target < duration) target += offset
|
||||
@@ -315,7 +397,7 @@ export const useSceneStore = defineStore('scene', {
|
||||
return newKf.id
|
||||
},
|
||||
setLayerSeparated(layerId: string, separated: boolean) {
|
||||
const layer = this.scene.layers.find((l) => l.id === layerId)
|
||||
const layer = this.activeLayers.find((l) => l.id === layerId)
|
||||
if (!layer) return
|
||||
if (!!layer.separated === separated) return
|
||||
if (separated) {
|
||||
@@ -381,7 +463,123 @@ export const useSceneStore = defineStore('scene', {
|
||||
}
|
||||
},
|
||||
loadScene(scene: Scene) {
|
||||
this.scene = scene
|
||||
this.scene = normalizeScene(scene)
|
||||
this.selectedLayerId = null
|
||||
this.playhead = 0
|
||||
this.playing = false
|
||||
this.activeClipId = null
|
||||
},
|
||||
addClip(name?: string): Clip {
|
||||
const clip: Clip = {
|
||||
id: uid(),
|
||||
name: name?.trim() || 'Clip',
|
||||
duration: Math.max(1, Math.min(5, this.scene.duration)),
|
||||
layers: [],
|
||||
}
|
||||
;(this.scene.clips ||= []).push(clip)
|
||||
if (!this.scene.clipTracks?.length) this.addClipTrack()
|
||||
return clip
|
||||
},
|
||||
removeClip(id: string) {
|
||||
if (this.scene.clips) {
|
||||
this.scene.clips = this.scene.clips.filter((c) => c.id !== id)
|
||||
}
|
||||
if (this.scene.clipInstances) {
|
||||
this.scene.clipInstances = this.scene.clipInstances.filter((ci) => ci.clipId !== id)
|
||||
}
|
||||
if (this.activeClipId === id) {
|
||||
this.activeClipId = null
|
||||
this.selectedLayerId = null
|
||||
this.playhead = 0
|
||||
}
|
||||
},
|
||||
renameClip(id: string, name: string) {
|
||||
const clip = this.scene.clips?.find((c) => c.id === id)
|
||||
if (clip) clip.name = name.trim() || clip.name
|
||||
},
|
||||
addClipTrack(name?: string): ClipTrack {
|
||||
const track: ClipTrack = { id: uid(), name }
|
||||
;(this.scene.clipTracks ||= []).push(track)
|
||||
;(this.scene.trackOrder ||= []).push(track.id)
|
||||
return track
|
||||
},
|
||||
removeClipTrack(id: string) {
|
||||
if (this.scene.clipTracks) {
|
||||
this.scene.clipTracks = this.scene.clipTracks.filter((t) => t.id !== id)
|
||||
}
|
||||
if (this.scene.clipInstances) {
|
||||
this.scene.clipInstances = this.scene.clipInstances.filter((ci) => ci.clipTrackId !== id)
|
||||
}
|
||||
if (this.scene.trackOrder) {
|
||||
this.scene.trackOrder = this.scene.trackOrder.filter((x) => x !== id)
|
||||
}
|
||||
},
|
||||
addClipInstance(clipId: string, startTime: number, clipTrackId: string): ClipInstance | null {
|
||||
const clip = this.scene.clips?.find((c) => c.id === clipId)
|
||||
if (!clip) return null
|
||||
const track = this.scene.clipTracks?.find((t) => t.id === clipTrackId)
|
||||
if (!track) return null
|
||||
const instance: ClipInstance = {
|
||||
id: uid(),
|
||||
clipId,
|
||||
startTime: Math.max(0, startTime),
|
||||
clipTrackId,
|
||||
}
|
||||
;(this.scene.clipInstances ||= []).push(instance)
|
||||
return instance
|
||||
},
|
||||
moveClipInstance(id: string, startTime: number, clipTrackId?: string) {
|
||||
const inst = this.scene.clipInstances?.find((ci) => ci.id === id)
|
||||
if (!inst) return
|
||||
inst.startTime = Math.max(0, startTime)
|
||||
if (clipTrackId) {
|
||||
const track = this.scene.clipTracks?.find((t) => t.id === clipTrackId)
|
||||
if (track) inst.clipTrackId = clipTrackId
|
||||
}
|
||||
},
|
||||
trimClipInstance(
|
||||
id: string,
|
||||
patch: { startTime?: number; trimStart?: number; trimEnd?: number },
|
||||
) {
|
||||
const inst = this.scene.clipInstances?.find((ci) => ci.id === id)
|
||||
if (!inst) return
|
||||
const clip = this.scene.clips?.find((c) => c.id === inst.clipId)
|
||||
if (!clip) return
|
||||
const MIN_VISIBLE = 0.05
|
||||
let trimStart = patch.trimStart ?? inst.trimStart ?? 0
|
||||
let trimEnd = patch.trimEnd ?? inst.trimEnd ?? 0
|
||||
let startTime = patch.startTime ?? inst.startTime
|
||||
trimStart = Math.max(0, trimStart)
|
||||
trimEnd = Math.max(0, trimEnd)
|
||||
const maxBudget = clip.duration - MIN_VISIBLE
|
||||
if (trimStart + trimEnd > maxBudget) {
|
||||
if (patch.trimStart !== undefined && patch.trimEnd === undefined) {
|
||||
trimStart = Math.max(0, maxBudget - trimEnd)
|
||||
} else if (patch.trimEnd !== undefined && patch.trimStart === undefined) {
|
||||
trimEnd = Math.max(0, maxBudget - trimStart)
|
||||
} else {
|
||||
trimStart = Math.min(trimStart, maxBudget)
|
||||
trimEnd = Math.max(0, maxBudget - trimStart)
|
||||
}
|
||||
}
|
||||
inst.trimStart = trimStart
|
||||
inst.trimEnd = trimEnd
|
||||
inst.startTime = Math.max(0, startTime)
|
||||
},
|
||||
removeClipInstance(id: string) {
|
||||
if (!this.scene.clipInstances) return
|
||||
this.scene.clipInstances = this.scene.clipInstances.filter((ci) => ci.id !== id)
|
||||
},
|
||||
enterClip(id: string) {
|
||||
const clip = this.scene.clips?.find((c) => c.id === id)
|
||||
if (!clip) return
|
||||
this.activeClipId = id
|
||||
this.selectedLayerId = null
|
||||
this.playhead = 0
|
||||
this.playing = false
|
||||
},
|
||||
exitClip() {
|
||||
this.activeClipId = null
|
||||
this.selectedLayerId = null
|
||||
this.playhead = 0
|
||||
this.playing = false
|
||||
|
||||
Reference in new issue
Block a user