ai-chatbot/lib/video-canvas/director-object-tracks.ts

148 lines
4.9 KiB
TypeScript

import type { DirectorScene, DirectorObject, DirectorVector } from "./director-scene";
export type DirectorObjectFrame = {
id: string;
time: number;
position: DirectorVector;
rotation: DirectorVector;
scale: DirectorVector;
visible: boolean;
};
export type DirectorObjectTrack = {
muted: boolean;
easing: "linear" | "smooth";
keyframes: DirectorObjectFrame[];
};
export function readObjectTracks(
value: unknown,
objects: DirectorObject[],
duration: number,
): Record<string, DirectorObjectTrack> {
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
const vector = (v: any, positive = false) =>
Array.isArray(v) &&
v.length === 3 &&
v.every(
(n: any) =>
typeof n === "number" && Number.isFinite(n) && Math.abs(n) <= 10000 && (!positive || n > 0),
);
return Object.fromEntries(
Object.entries(value)
.filter(([id]) => objects.some((o) => o.id === id))
.map(([id, raw]) => {
const t = raw as any,
seen = new Set<number>(),
ids = new Set<string>();
const keyframes = (Array.isArray(t?.keyframes) ? t.keyframes : [])
.filter((k: any) => {
if (
!k ||
typeof k.id !== "string" ||
ids.has(k.id) ||
typeof k.time !== "number" ||
!Number.isFinite(k.time) ||
k.time < 0 ||
k.time > duration ||
seen.has(k.time) ||
!vector(k.position) ||
!vector(k.rotation) ||
!vector(k.scale, true)
)
return false;
seen.add(k.time);
ids.add(k.id);
return true;
})
.slice(0, 100)
.map((k: any) => ({
id: k.id,
time: k.time,
position: [...k.position],
rotation: [...k.rotation],
scale: [...k.scale],
visible: k.visible !== false,
}))
.sort((a: any, b: any) => a.time - b.time);
return [
id,
{
muted: t?.muted === true,
easing: t?.easing === "smooth" ? "smooth" : "linear",
keyframes,
},
];
}),
);
}
export function sampleDirectorObjects(scene: DirectorScene, time: number | null): DirectorObject[] {
if (time === null) return scene.objects;
return scene.objects.map((object) => {
const track = scene.objectTracks?.[object.id];
if (!track || track.muted || !track.keyframes.length) return object;
const keys = track.keyframes,
t = Math.max(0, Math.min(scene.motion.duration, time)),
right = keys.findIndex((k) => k.time > t);
const a = right < 0 ? keys[keys.length - 1] : keys[Math.max(0, right - 1)],
b = right < 0 ? a : keys[right];
let u = a === b ? 0 : Math.max(0, Math.min(1, (t - a.time) / (b.time - a.time)));
if (track.easing === "smooth") u = u * u * (3 - 2 * u);
const vector = (x: DirectorVector, y: DirectorVector, rotation = false): DirectorVector =>
x.map((n, i) => {
let delta = y[i] - n;
if (rotation) delta = Math.atan2(Math.sin(delta), Math.cos(delta));
return n + delta * u;
}) as DirectorVector;
return {
...object,
position: vector(a.position, b.position),
rotation: vector(a.rotation, b.rotation, true),
scale: vector(a.scale, b.scale),
visible: a.visible,
};
});
}
export function recordObjectFrames(
scene: DirectorScene,
ids: string[],
time: number,
id: () => string,
): DirectorScene {
if (!Number.isFinite(time) || time < 0 || time > scene.motion.duration)
throw Error("关键帧时间超出时间轴");
const tracks = { ...scene.objectTracks };
for (const object of scene.objects.filter((o) => ids.includes(o.id))) {
const track = tracks[object.id] || { muted: false, easing: "linear" as const, keyframes: [] };
const existing = track.keyframes.find((k) => Math.abs(k.time - time) < 0.01);
if (!existing && track.keyframes.length >= 100) throw Error("每个对象最多 100 个关键帧");
const frame = {
id: existing?.id || id(),
time: existing?.time ?? Math.round(time * 100) / 100,
position: [...object.position] as DirectorVector,
rotation: [...object.rotation] as DirectorVector,
scale: [...object.scale] as DirectorVector,
visible: object.visible !== false,
};
tracks[object.id] = {
...track,
keyframes: [...track.keyframes.filter((k) => k.id !== frame.id), frame].sort(
(a, b) => a.time - b.time,
),
};
}
return { ...scene, objectTracks: tracks };
}
export function resizeObjectTracks(
tracks: Record<string, DirectorObjectTrack> | undefined,
oldDuration: number,
newDuration: number,
) {
return Object.fromEntries(
Object.entries(tracks || {}).map(([id, t]) => [
id,
{
...t,
keyframes: t.keyframes.map((k) => ({ ...k, time: (k.time * newDuration) / oldDuration })),
},
]),
);
}