init
This commit is contained in:
commit
c6863f2bfc
34 files changed
+6909
No files matched your search
@@ -0,0 +1,227 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { AnimatableProps, Asset, Easing, Keyframe, Layer, Scene } from '@/types/scene'
|
||||
import { DEFAULT_BASE_PROPS } from '@/types/scene'
|
||||
import { sampleLayer } from '@/lib/interpolate'
|
||||
|
||||
const uid = () => Math.random().toString(36).slice(2, 10)
|
||||
|
||||
type AssetInit = Asset extends infer A ? (A extends Asset ? Omit<A, 'id'> : never) : never
|
||||
|
||||
const KEYFRAME_EPSILON = 1e-3
|
||||
|
||||
interface State {
|
||||
scene: Scene
|
||||
selectedLayerId: string | null
|
||||
playhead: number
|
||||
playing: boolean
|
||||
}
|
||||
|
||||
export const useSceneStore = defineStore('scene', {
|
||||
state: (): State => ({
|
||||
scene: {
|
||||
version: 1,
|
||||
canvas: { width: 1280, height: 720, background: '#ffffff' },
|
||||
duration: 10,
|
||||
fps: 60,
|
||||
assets: [],
|
||||
layers: [],
|
||||
},
|
||||
selectedLayerId: null,
|
||||
playhead: 0,
|
||||
playing: false,
|
||||
}),
|
||||
getters: {
|
||||
lastKeyframeTime(state): number {
|
||||
let max = 0
|
||||
for (const layer of state.scene.layers) {
|
||||
for (const kf of layer.keyframes) {
|
||||
if (kf.time > max) max = kf.time
|
||||
}
|
||||
}
|
||||
return Math.min(state.scene.duration, max)
|
||||
},
|
||||
effectiveEnd(state): number {
|
||||
let max = 0
|
||||
for (const layer of state.scene.layers) {
|
||||
for (const kf of layer.keyframes) {
|
||||
if (kf.time > max) max = kf.time
|
||||
}
|
||||
}
|
||||
if (max <= 0) return state.scene.duration
|
||||
return Math.min(max, state.scene.duration)
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
addAsset(asset: AssetInit): Asset {
|
||||
const created = { ...asset, id: uid() } as Asset
|
||||
this.scene.assets.push(created)
|
||||
return created
|
||||
},
|
||||
addLayer(assetId: string, name?: string): Layer {
|
||||
const asset = this.scene.assets.find((a) => a.id === assetId)
|
||||
const base: AnimatableProps = {
|
||||
...DEFAULT_BASE_PROPS,
|
||||
x: this.scene.canvas.width / 2,
|
||||
y: this.scene.canvas.height / 2,
|
||||
}
|
||||
if (asset && 'fill' in asset) base.fill = asset.fill
|
||||
const layer: Layer = {
|
||||
id: uid(),
|
||||
name: name ?? asset?.name ?? 'Layer',
|
||||
assetId,
|
||||
visible: true,
|
||||
base,
|
||||
keyframes: [
|
||||
{
|
||||
id: uid(),
|
||||
time: 0,
|
||||
props: { ...base },
|
||||
easing: 'linear',
|
||||
},
|
||||
],
|
||||
}
|
||||
if (asset && (asset.kind === 'rect' || asset.kind === 'image')) {
|
||||
layer.offsetX = asset.width / 2
|
||||
layer.offsetY = asset.height / 2
|
||||
}
|
||||
this.scene.layers.push(layer)
|
||||
this.selectedLayerId = layer.id
|
||||
return layer
|
||||
},
|
||||
removeLayer(id: string) {
|
||||
this.scene.layers = this.scene.layers.filter((l) => l.id !== id)
|
||||
if (this.selectedLayerId === id) this.selectedLayerId = null
|
||||
},
|
||||
selectLayer(id: string | null) {
|
||||
this.selectedLayerId = id
|
||||
},
|
||||
setPlayhead(t: number) {
|
||||
this.playhead = Math.max(0, Math.min(this.scene.duration, t))
|
||||
},
|
||||
setDuration(seconds: number) {
|
||||
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))
|
||||
},
|
||||
setCanvasSize(width: number, height: number) {
|
||||
this.scene.canvas.width = Math.max(1, Math.round(width))
|
||||
this.scene.canvas.height = Math.max(1, Math.round(height))
|
||||
},
|
||||
setCanvasBackground(color: string) {
|
||||
this.scene.canvas.background = color
|
||||
},
|
||||
upsertKeyframe(layerId: string, time: number, props: Partial<AnimatableProps>) {
|
||||
const layer = this.scene.layers.find((l) => l.id === layerId)
|
||||
if (!layer) return
|
||||
const existing = layer.keyframes.find((k) => Math.abs(k.time - time) < KEYFRAME_EPSILON)
|
||||
if (existing) {
|
||||
existing.props = { ...existing.props, ...props }
|
||||
return
|
||||
}
|
||||
const snapshot = sampleLayer(layer, time)
|
||||
const kf: Keyframe = {
|
||||
id: uid(),
|
||||
time,
|
||||
props: { ...snapshot, ...props },
|
||||
easing: 'linear',
|
||||
}
|
||||
layer.keyframes.push(kf)
|
||||
layer.keyframes.sort((a, b) => a.time - b.time)
|
||||
},
|
||||
removeKeyframeAt(layerId: string, time: number) {
|
||||
const layer = this.scene.layers.find((l) => l.id === layerId)
|
||||
if (!layer) return
|
||||
layer.keyframes = layer.keyframes.filter((k) => Math.abs(k.time - time) >= KEYFRAME_EPSILON)
|
||||
},
|
||||
setKeyframeEasing(layerId: string, time: number, easing: Easing) {
|
||||
const layer = this.scene.layers.find((l) => l.id === layerId)
|
||||
if (!layer) return
|
||||
const kf = layer.keyframes.find((k) => Math.abs(k.time - time) < KEYFRAME_EPSILON)
|
||||
if (kf) kf.easing = easing
|
||||
},
|
||||
setKeyframeTime(layerId: string, keyframeId: string, newTime: number) {
|
||||
const layer = this.scene.layers.find((l) => l.id === layerId)
|
||||
if (!layer) return
|
||||
const kf = layer.keyframes.find((k) => k.id === keyframeId)
|
||||
if (!kf) return
|
||||
kf.time = Math.max(0, Math.min(this.scene.duration, newTime))
|
||||
layer.keyframes.sort((a, b) => a.time - b.time)
|
||||
},
|
||||
shiftLayerPivot(
|
||||
layerId: string,
|
||||
dWorldX: number,
|
||||
dWorldY: number,
|
||||
dLocalX: number,
|
||||
dLocalY: number,
|
||||
) {
|
||||
const layer = this.scene.layers.find((l) => l.id === layerId)
|
||||
if (!layer) return
|
||||
layer.offsetX = (layer.offsetX ?? 0) + dLocalX
|
||||
layer.offsetY = (layer.offsetY ?? 0) + dLocalY
|
||||
layer.base.x += dWorldX
|
||||
layer.base.y += dWorldY
|
||||
for (const kf of layer.keyframes) {
|
||||
if (kf.props.x !== undefined) kf.props.x += dWorldX
|
||||
if (kf.props.y !== undefined) kf.props.y += dWorldY
|
||||
}
|
||||
},
|
||||
duplicateKeyframe(layerId: string, keyframeId: string): string | null {
|
||||
const layer = this.scene.layers.find((l) => l.id === layerId)
|
||||
if (!layer) return null
|
||||
const source = layer.keyframes.find((k) => k.id === keyframeId)
|
||||
if (!source) return null
|
||||
const fps = this.scene.fps > 0 ? this.scene.fps : 60
|
||||
const offset = Math.max(3 / fps, 0.1)
|
||||
const duration = this.scene.duration
|
||||
const occupied = (t: number) =>
|
||||
layer.keyframes.some((k) => Math.abs(k.time - t) < KEYFRAME_EPSILON)
|
||||
let target = source.time + offset
|
||||
while (occupied(target) && target < duration) target += offset
|
||||
if (target > duration) target = duration
|
||||
if (occupied(target)) {
|
||||
target = source.time - offset
|
||||
while (occupied(target) && target > 0) target -= offset
|
||||
if (target < 0) target = 0
|
||||
if (occupied(target)) return null
|
||||
}
|
||||
const newKf: Keyframe = {
|
||||
id: uid(),
|
||||
time: target,
|
||||
props: { ...source.props },
|
||||
easing: source.easing,
|
||||
}
|
||||
layer.keyframes.push(newKf)
|
||||
layer.keyframes.sort((a, b) => a.time - b.time)
|
||||
return newKf.id
|
||||
},
|
||||
play() {
|
||||
if (this.playing) return
|
||||
this.playing = true
|
||||
},
|
||||
pause() {
|
||||
this.playing = false
|
||||
},
|
||||
togglePlay() {
|
||||
if (this.playing) this.pause()
|
||||
else {
|
||||
const end = this.effectiveEnd
|
||||
if (end <= 0) return
|
||||
if (this.playhead >= end) this.playhead = 0
|
||||
this.play()
|
||||
}
|
||||
},
|
||||
tick(deltaSeconds: number) {
|
||||
if (!this.playing) return
|
||||
const end = this.effectiveEnd
|
||||
const next = this.playhead + deltaSeconds
|
||||
if (next >= end) {
|
||||
this.playhead = end
|
||||
this.playing = false
|
||||
} else {
|
||||
this.playhead = next
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in new issue
Block a user