import type { DirectorCamera, DirectorVector } from "./director-scene"; export type DirectorPose = Pick; export function mergeDirectorCameraPose( camera: DirectorCamera, pose: Partial, ): DirectorCamera { return { ...camera, position: pose.position ?? camera.position, target: pose.target ?? camera.target, fov: pose.fov ?? camera.fov, }; } export type DirectorKeyframe = DirectorPose & { id: string; time: number }; export type DirectorMotion = { duration: number; easing: "linear" | "smooth"; loop: boolean; keyframes: DirectorKeyframe[]; }; export const defaultDirectorMotion = (): DirectorMotion => ({ duration: 5, easing: "smooth", loop: false, keyframes: [], }); const vector = (v: unknown): v is DirectorVector => Array.isArray(v) && v.length === 3 && v.every( (n) => typeof n === "number" && Number.isFinite(n) && Math.abs(n) <= 10000, ); export function validDirectorPose(p: DirectorPose) { return ( !!p && vector(p.position) && vector(p.target) && Number.isFinite(p.fov) && p.fov >= 15 && p.fov <= 90 && Math.hypot(...p.position.map((n, i) => n - p.target[i])) > 0.001 ); } export function readDirectorMotion(input: unknown): DirectorMotion { const v = (input || {}) as Partial; const duration = Number.isFinite(v.duration) ? Math.max(2, Math.min(15, Number(v.duration))) : 5; const seen = new Set(); const times = new Set(); const keyframes = (Array.isArray(v.keyframes) ? v.keyframes : []) .filter((k) => { if ( !k || typeof k.id !== "string" || seen.has(k.id) || !Number.isFinite(k.time) || k.time < 0 || k.time > duration || times.has(k.time) || !validDirectorPose(k) ) return false; seen.add(k.id); times.add(k.time); return true; }) .slice(0, 100) .map((k) => ({ id: k.id, time: k.time, position: [...k.position] as DirectorVector, target: [...k.target] as DirectorVector, fov: k.fov, })) .sort((a, b) => a.time - b.time); return { duration, easing: v.easing === "linear" ? "linear" : "smooth", loop: v.loop === true, keyframes, }; } const lerpVector = ( a: DirectorVector, b: DirectorVector, t: number, ): DirectorVector => a.map((n, i) => n + (b[i] - n) * t) as DirectorVector; export function sampleDirectorMotion( m: DirectorMotion, time: number, ): DirectorPose | null { const keys = m.keyframes; if (!keys.length) return null; const t = Number.isFinite(time) ? Math.max(0, Math.min(m.duration, time)) : 0; const right = keys.findIndex((k) => k.time > t); const a = right < 0 ? keys[keys.length - 1] : keys[Math.max(0, right - 1)]; const 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 (m.easing === "smooth") u = u * u * (3 - 2 * u); const pose = { position: lerpVector(a.position, b.position, u), target: lerpVector(a.target, b.target, u), fov: a.fov + (b.fov - a.fov) * u, }; // A path crossing its look-at target has no defined viewing direction. if (!validDirectorPose(pose)) { const direction = a.position.map((n, i) => n - a.target[i]); const length = Math.hypot(...direction); pose.target = pose.position.map( (n, i) => n - (direction[i] / length) * 0.01, ) as DirectorVector; } return pose; } export function resizeDirectorMotion( m: DirectorMotion, duration: number, ): DirectorMotion { const next = Math.max(2, Math.min(15, duration)); return { ...m, duration: next, keyframes: m.keyframes.map((k) => ({ ...k, time: (k.time / m.duration) * next, })), }; } export function presetDirectorMotion( pose: DirectorPose, kind: "push" | "pull" | "orbit" | "pan", duration = 5, ): DirectorMotion { const m = { ...defaultDirectorMotion(), duration }; const offset = pose.position.map( (n, i) => n - pose.target[i], ) as DirectorVector; const count = kind === "orbit" ? 9 : 2; m.keyframes = Array.from({ length: count }, (_, i) => { const u = i / (count - 1); let position: DirectorVector = [...pose.position], target: DirectorVector = [...pose.target]; if (kind === "push" || kind === "pull") position = pose.target.map( (n, j) => n + offset[j] * (1 + u * (kind === "push" ? -0.5 : 0.5)), ) as DirectorVector; if (kind === "orbit") { const angle = (u * Math.PI) / 2; position = [ pose.target[0] + offset[0] * Math.cos(angle) + offset[2] * Math.sin(angle), pose.position[1], pose.target[2] - offset[0] * Math.sin(angle) + offset[2] * Math.cos(angle), ]; } if (kind === "pan") { const length = Math.hypot(offset[0], offset[2]) || 1; const shift: DirectorVector = [ (offset[2] / length) * 2 * u, 0, (-offset[0] / length) * 2 * u, ]; position = position.map((n, j) => n + shift[j]) as DirectorVector; target = target.map((n, j) => n + shift[j]) as DirectorVector; } return { id: `${kind}-${i}`, time: u * duration, position, target, fov: pose.fov, }; }); if (kind === "orbit") m.easing = "linear"; return m; } export function directorMotionPrompt(m: DirectorMotion) { if (m.keyframes.length < 2) return ""; // Keep the model prompt compact; the full trajectory remains in the canvas data. const keys = m.keyframes.length <= 9 ? m.keyframes : Array.from({ length: 9 }, (_, i) => ({ time: (m.duration * i) / 8, ...sampleDirectorMotion(m, (m.duration * i) / 8)!, })); const lines = keys.slice(1).map((b, i) => { const a = keys[i], av = a.position.map((n, j) => n - a.target[j]), bv = b.position.map((n, j) => n - b.target[j]); const ar = Math.hypot(...av), br = Math.hypot(...bv), angle = (Math.acos( Math.max( -1, Math.min( 1, av.reduce((sum, n, j) => sum + n * bv[j], 0) / (ar * br), ), ), ) * 180) / Math.PI; const descriptions: string[] = []; if (angle > 2) descriptions.push(`围绕注视主体转动约 ${Math.round(angle)} 度`); if (br < ar * 0.98) descriptions.push("向主体推进"); else if (br > ar * 1.02) descriptions.push("远离主体拉远"); if (Math.hypot(...b.target.map((n, j) => n - a.target[j])) > 0.05) descriptions.push("注视点随镜头平移"); if (Math.abs(b.position[1] - a.position[1]) > 0.05) descriptions.push( b.position[1] > a.position[1] ? "相机升高" : "相机降低", ); if (Math.abs(b.fov - a.fov) > 0.5) descriptions.push( `视角从 ${a.fov.toFixed(0)}° 变至 ${b.fov.toFixed(0)}°`, ); return `${a.time.toFixed(2)}–${b.time.toFixed(2)}秒:${descriptions.join(",") || "保持机位和构图"}。`; }); return `单一连续镜头,不切镜,总时长 ${m.duration} 秒;镜头${m.easing === "smooth" ? "平滑缓入缓出" : "匀速"}运动。\n${lines.join("\n")}\n保持参考图中的主体和场景一致,镜头连续运动。`; }