1// Pure operations on the timeline (an ordered list of segments of takes).
3import type { Segment } from './types.ts';
5// Pieces shorter than this (seconds) are dropped after an edit.
6const MIN_PIECE = 0.04;
8export function randomId(n = 8): string {
9 const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
10 let s = '';
11 for (let i = 0; i < n; i++) s += chars[Math.floor(Math.random() * chars.length)];
12 return s;
13}
15export const segDuration = (s: Segment) => Math.max(0, s.end - s.start);
17export function timelineDuration(segs: Segment[]): number {
18 return segs.reduce((acc, s) => acc + segDuration(s), 0);
19}
21// Timeline start time of each segment.
22export function segmentStarts(segs: Segment[]): number[] {
23 const out: number[] = [];
24 let t = 0;
25 for (const s of segs) {
26 out.push(t);
27 t += segDuration(s);
28 }
29 return out;
30}
32// Which segment contains timeline time t, and the corresponding take time.
33export function locate(segs: Segment[], t: number): { index: number; takeTime: number } | null {
34 if (segs.length === 0) return null;
35 let acc = 0;
36 for (let i = 0; i < segs.length; i++) {
37 const d = segDuration(segs[i]);
38 if (t < acc + d || i === segs.length - 1) {
39 const off = Math.min(Math.max(t - acc, 0), d);
40 return { index: i, takeTime: segs[i].start + off };
41 }
42 acc += d;
43 }
44 return null;
45}
47export function splitAt(segs: Segment[], t: number): Segment[] {
48 const loc = locate(segs, t);
49 if (!loc) return segs;
50 const s = segs[loc.index];
51 if (loc.takeTime - s.start < MIN_PIECE || s.end - loc.takeTime < MIN_PIECE) return segs;
52 const a: Segment = { ...s, end: loc.takeTime };
53 const b: Segment = { ...s, id: randomId(), start: loc.takeTime };
54 return [...segs.slice(0, loc.index), a, b, ...segs.slice(loc.index + 1)];
55}
57// Remove timeline range [t0, t1].
58export function cutRange(segs: Segment[], t0: number, t1: number): Segment[] {
59 if (t1 < t0) [t0, t1] = [t1, t0];
60 const out: Segment[] = [];
61 let acc = 0;
62 for (const s of segs) {
63 const d = segDuration(s);
64 const a = acc;
65 const b = acc + d;
66 acc = b;
67 if (t1 <= a || t0 >= b) {
68 out.push(s);
69 continue;
70 }
71 let usedId = false;
72 if (t0 > a) {
73 const piece = { ...s, end: s.start + (t0 - a) };
74 if (segDuration(piece) >= MIN_PIECE) {
75 out.push(piece);
76 usedId = true;
77 }
78 }
79 if (t1 < b) {
80 const piece = { ...s, id: usedId ? randomId() : s.id, start: s.start + (t1 - a) };
81 if (segDuration(piece) >= MIN_PIECE) out.push(piece);
82 }
83 }
84 return out;
85}
87export function moveSegment(segs: Segment[], index: number, delta: number): Segment[] {
88 const j = index + delta;
89 if (index < 0 || index >= segs.length || j < 0 || j >= segs.length) return segs;
90 const out = segs.slice();
91 const [s] = out.splice(index, 1);
92 out.splice(j, 0, s);
93 return out;
94}
96// Timeline time where take time `takeTime` of take `takeId` appears (first occurrence), or null if cut.
97export function takeTimeToTimeline(segs: Segment[], takeId: string, takeTime: number): number | null {
98 let acc = 0;
99 for (const s of segs) {
100 if (s.takeId === takeId && takeTime >= s.start && takeTime <= s.end) return acc + (takeTime - s.start);
101 acc += segDuration(s);
102 }
103 return null;
104}