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
+57
-13
@@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
|
||||
import type {
|
||||
AnimatableProps,
|
||||
Asset,
|
||||
BackgroundVideo,
|
||||
Clip,
|
||||
ClipInstance,
|
||||
ClipTrack,
|
||||
@@ -111,6 +112,7 @@ function normalizeScene(scene: Scene): Scene {
|
||||
scene.clipTracks ||= []
|
||||
scene.clipInstances ||= []
|
||||
scene.groups ||= []
|
||||
if (!scene.canvas.background) scene.canvas.background = '#ffffff'
|
||||
|
||||
const known = new Set<string>()
|
||||
for (const l of scene.layers) known.add(l.id)
|
||||
@@ -154,16 +156,20 @@ function normalizeScene(scene: Scene): Scene {
|
||||
}
|
||||
|
||||
|
||||
export function createBlankScene(): Scene {
|
||||
return normalizeScene({
|
||||
version: 1,
|
||||
canvas: { width: 1280, height: 720, background: '#ffffff' },
|
||||
duration: 10,
|
||||
fps: 60,
|
||||
assets: [],
|
||||
layers: [],
|
||||
})
|
||||
}
|
||||
|
||||
export const useSceneStore = defineStore('scene', {
|
||||
state: (): State => ({
|
||||
scene: normalizeScene({
|
||||
version: 1,
|
||||
canvas: { width: 1280, height: 720, background: '#ffffff' },
|
||||
duration: 10,
|
||||
fps: 60,
|
||||
assets: [],
|
||||
layers: [],
|
||||
}),
|
||||
scene: createBlankScene(),
|
||||
selectedLayerId: null,
|
||||
playhead: 0,
|
||||
playing: false,
|
||||
@@ -219,6 +225,18 @@ export const useSceneStore = defineStore('scene', {
|
||||
if (max <= 0) return this.activeDuration
|
||||
return Math.min(max, this.activeDuration)
|
||||
},
|
||||
/** Effective in-point for playback/export. Defaults to 0, clamped to duration. */
|
||||
playbackIn(): number {
|
||||
if (this.activeClipId) return 0
|
||||
const v = this.scene.inTime ?? 0
|
||||
return Math.max(0, Math.min(this.scene.duration, v))
|
||||
},
|
||||
/** Effective out-point for playback/export. Defaults to duration, clamped >=in. */
|
||||
playbackOut(): number {
|
||||
if (this.activeClipId) return this.activeDuration
|
||||
const v = this.scene.outTime ?? this.scene.duration
|
||||
return Math.max(this.playbackIn, Math.min(this.scene.duration, v))
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
addAsset(asset: AssetInit): Asset {
|
||||
@@ -325,6 +343,30 @@ export const useSceneStore = defineStore('scene', {
|
||||
setCanvasBackground(color: string) {
|
||||
this.scene.canvas.background = color
|
||||
},
|
||||
setBackgroundVideo(video: BackgroundVideo | null) {
|
||||
if (video && video.src) this.scene.backgroundVideo = { ...video }
|
||||
else delete this.scene.backgroundVideo
|
||||
},
|
||||
setPlaybackRange(inTime: number | null, outTime: number | null) {
|
||||
const dur = this.scene.duration
|
||||
const hasIn = inTime !== null && inTime !== undefined
|
||||
const hasOut = outTime !== null && outTime !== undefined
|
||||
if (hasIn) {
|
||||
const v = Math.max(0, Math.min(dur, inTime!))
|
||||
if (v === 0 && this.scene.outTime === undefined) delete this.scene.inTime
|
||||
else this.scene.inTime = v
|
||||
}
|
||||
if (hasOut) {
|
||||
const v = Math.max(this.scene.inTime ?? 0, Math.min(dur, outTime!))
|
||||
if (v === dur && this.scene.inTime === undefined) delete this.scene.outTime
|
||||
else this.scene.outTime = v
|
||||
}
|
||||
// Re-clamp playhead into the new range
|
||||
const lo = this.scene.inTime ?? 0
|
||||
const hi = this.scene.outTime ?? dur
|
||||
if (this.playhead < lo) this.playhead = lo
|
||||
else if (this.playhead > hi) this.playhead = hi
|
||||
},
|
||||
upsertKeyframe(layerId: string, time: number, props: Partial<AnimatableProps>) {
|
||||
const layer = this.activeLayers.find((l) => l.id === layerId)
|
||||
if (!layer) return
|
||||
@@ -935,7 +977,7 @@ export const useSceneStore = defineStore('scene', {
|
||||
if (isMask) {
|
||||
layer.isMask = true
|
||||
if (mode) layer.maskMode = mode
|
||||
else if (!layer.maskMode) layer.maskMode = 'positive'
|
||||
else if (!layer.maskMode) layer.maskMode = 'negative'
|
||||
} else {
|
||||
delete layer.isMask
|
||||
delete layer.maskMode
|
||||
@@ -1219,15 +1261,17 @@ export const useSceneStore = defineStore('scene', {
|
||||
togglePlay() {
|
||||
if (this.playing) this.pause()
|
||||
else {
|
||||
const end = this.effectiveEnd
|
||||
if (end <= 0) return
|
||||
if (this.playhead >= end) this.playhead = 0
|
||||
const end = this.playbackOut
|
||||
const start = this.playbackIn
|
||||
if (end <= start) return
|
||||
// If the playhead sits outside the in/out range, snap to the in-point.
|
||||
if (this.playhead < start || this.playhead >= end) this.playhead = start
|
||||
this.play()
|
||||
}
|
||||
},
|
||||
tick(deltaSeconds: number) {
|
||||
if (!this.playing) return
|
||||
const end = this.effectiveEnd
|
||||
const end = this.playbackOut
|
||||
const next = this.playhead + deltaSeconds
|
||||
if (next >= end) {
|
||||
this.playhead = end
|
||||
|
||||
Reference in new issue
Block a user