// Pure operations on the timeline (an ordered list of segments of takes). import type { Segment } from './types.ts'; // Pieces shorter than this (seconds) are dropped after an edit. const MIN_PIECE = 0.04; export function randomId(n = 8): string { const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; let s = ''; for (let i = 0; i < n; i++) s += chars[Math.floor(Math.random() * chars.length)]; return s; } export const segDuration = (s: Segment) => Math.max(0, s.end - s.start); export function timelineDuration(segs: Segment[]): number { return segs.reduce((acc, s) => acc + segDuration(s), 0); } // Timeline start time of each segment. export function segmentStarts(segs: Segment[]): number[] { const out: number[] = []; let t = 0; for (const s of segs) { out.push(t); t += segDuration(s); } return out; } // Which segment contains timeline time t, and the corresponding take time. export function locate(segs: Segment[], t: number): { index: number; takeTime: number } | null { if (segs.length === 0) return null; let acc = 0; for (let i = 0; i < segs.length; i++) { const d = segDuration(segs[i]); if (t < acc + d || i === segs.length - 1) { const off = Math.min(Math.max(t - acc, 0), d); return { index: i, takeTime: segs[i].start + off }; } acc += d; } return null; } export function splitAt(segs: Segment[], t: number): Segment[] { const loc = locate(segs, t); if (!loc) return segs; const s = segs[loc.index]; if (loc.takeTime - s.start < MIN_PIECE || s.end - loc.takeTime < MIN_PIECE) return segs; const a: Segment = { ...s, end: loc.takeTime }; const b: Segment = { ...s, id: randomId(), start: loc.takeTime }; return [...segs.slice(0, loc.index), a, b, ...segs.slice(loc.index + 1)]; } // Remove timeline range [t0, t1]. export function cutRange(segs: Segment[], t0: number, t1: number): Segment[] { if (t1 < t0) [t0, t1] = [t1, t0]; const out: Segment[] = []; let acc = 0; for (const s of segs) { const d = segDuration(s); const a = acc; const b = acc + d; acc = b; if (t1 <= a || t0 >= b) { out.push(s); continue; } let usedId = false; if (t0 > a) { const piece = { ...s, end: s.start + (t0 - a) }; if (segDuration(piece) >= MIN_PIECE) { out.push(piece); usedId = true; } } if (t1 < b) { const piece = { ...s, id: usedId ? randomId() : s.id, start: s.start + (t1 - a) }; if (segDuration(piece) >= MIN_PIECE) out.push(piece); } } return out; } export function moveSegment(segs: Segment[], index: number, delta: number): Segment[] { const j = index + delta; if (index < 0 || index >= segs.length || j < 0 || j >= segs.length) return segs; const out = segs.slice(); const [s] = out.splice(index, 1); out.splice(j, 0, s); return out; } // Timeline time where take time `takeTime` of take `takeId` appears (first occurrence), or null if cut. export function takeTimeToTimeline(segs: Segment[], takeId: string, takeTime: number): number | null { let acc = 0; for (const s of segs) { if (s.takeId === takeId && takeTime >= s.start && takeTime <= s.end) return acc + (takeTime - s.start); acc += segDuration(s); } return null; }