export interface Keyframe { id: string; time: number; position: [number, number, number]; rotation: [number, number, number]; scale: [number, number, number]; easing: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out'; } export interface Track { objectId: string; keyframes: Keyframe[]; } export function interpolateKF( a: Keyframe, b: Keyframe, t: number ): { position: [number,number,number]; rotation: [number,number,number]; scale: [number,number,number] } { const ease = (x: number, type: Keyframe['easing']): number => { switch (type) { case 'ease-in': return x * x; case 'ease-out': return 1 - (1 - x) * (1 - x); case 'ease-in-out': return x < 0.5 ? 2*x*x : 1 - Math.pow(-2*x+2, 2)/2; default: return x; } }; const et = ease(t, b.easing); const lerp = (a: number, b: number) => a + (b - a) * et; const lerpV3 = ( a: [number,number,number], b: [number,number,number] ): [number,number,number] => [ lerp(a[0], b[0]), lerp(a[1], b[1]), lerp(a[2], b[2]), ]; return { position: lerpV3(a.position, b.position), rotation: lerpV3(a.rotation, b.rotation), scale: lerpV3(a.scale, b.scale), }; } export function getFramesAtTime(keyframes: Keyframe[], time: number) { const sorted = [...keyframes].sort((a, b) => a.time - b.time); if (!sorted.length) return null; if (time <= sorted[0].time) return { a: sorted[0], b: sorted[0], t: 0 }; const last = sorted[sorted.length - 1]; if (time >= last.time) return { a: last, b: last, t: 0 }; for (let i = 0; i < sorted.length - 1; i++) { if (time >= sorted[i].time && time <= sorted[i+1].time) { const span = sorted[i+1].time - sorted[i].time; const t = span === 0 ? 0 : (time - sorted[i].time) / span; return { a: sorted[i], b: sorted[i+1], t }; } } return null; }