@mineo71/showtime 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,8 +20,17 @@ export function introFrames(t: Timeline): number {
20
20
  return t.intro ? toFrames(t.intro.durationMs, t.fps) : 0;
21
21
  }
22
22
 
23
+ export function outroFrames(t: Timeline): number {
24
+ return t.outro ? toFrames(t.outro.durationMs, t.fps) : 0;
25
+ }
26
+
27
+ /** First frame after the video (where the outro starts). */
28
+ export function videoEndFrame(t: Timeline): number {
29
+ return introFrames(t) + toFrames(outputDurationMs(t.segments), t.fps);
30
+ }
31
+
23
32
  export function totalFrames(t: Timeline): number {
24
- return Math.max(1, introFrames(t) + toFrames(outputDurationMs(t.segments), t.fps));
33
+ return Math.max(1, videoEndFrame(t) + outroFrames(t));
25
34
  }
26
35
 
27
36
  export function segmentPlan(t: Timeline): SegmentPlan[] {
@@ -48,7 +57,7 @@ export function sourceMsAtFrame(t: Timeline, frame: number): number | null {
48
57
 
49
58
  export function qaShots(t: Timeline, steps: StepEvent[]): Shot[] {
50
59
  const intro = introFrames(t);
51
- const last = totalFrames(t) - 1;
60
+ const last = Math.max(0, videoEndFrame(t) - 1);
52
61
  const shots: Shot[] = [];
53
62
  if (t.intro) shots.push({ frame: Math.floor(intro / 2), name: '00-intro' });
54
63
  steps.forEach((s, i) => {
@@ -56,5 +65,6 @@ export function qaShots(t: Timeline, steps: StepEvent[]): Shot[] {
56
65
  const frame = Math.min(last, intro + toFrames(sourceToOutput(t.segments, mid), t.fps));
57
66
  shots.push({ frame, name: `${String(i + 1).padStart(2, '0')}-${s.sceneId}-${s.index + 1}-${s.do}` });
58
67
  });
68
+ if (t.outro) shots.push({ frame: videoEndFrame(t) + Math.round(outroFrames(t) * 0.6), name: '99-outro' });
59
69
  return shots;
60
70
  }
@@ -1,3 +1,5 @@
1
+ import type { CameraKey, Rect } from './types';
2
+
1
3
  export interface Box {
2
4
  x: number;
3
5
  y: number;
@@ -27,3 +29,22 @@ export function windowLayout(
27
29
  const top = Math.round((output.height - height - barPx) / 2);
28
30
  return { x: Math.round((output.width - width) / 2), y: top + barPx, width, height, bar: barPx };
29
31
  }
32
+
33
+ const r2 = (v: number) => Math.round(v * 100) / 100;
34
+
35
+ /** Where a stage rect (CSS px) lands in the output: through the camera, inside the window viewport. */
36
+ export function projectRect(
37
+ r: Rect,
38
+ cam: Pick<CameraKey, 'scale' | 'cx' | 'cy'>,
39
+ stage: { width: number; height: number },
40
+ win: Box,
41
+ ): Rect {
42
+ const w = win.width * cam.scale;
43
+ const h = win.height * cam.scale;
44
+ return {
45
+ x: r2(win.x + win.width / 2 + (r.x / stage.width - cam.cx) * w),
46
+ y: r2(win.y + win.height / 2 + (r.y / stage.height - cam.cy) * h),
47
+ width: r2((r.width / stage.width) * w),
48
+ height: r2((r.height / stage.height) * h),
49
+ };
50
+ }
@@ -81,6 +81,8 @@ export interface Caption {
81
81
  startMs: number;
82
82
  endMs: number;
83
83
  text: string;
84
+ /** Bottom unless the step's target would sit under a bottom caption. */
85
+ position: 'bottom' | 'top';
84
86
  }
85
87
 
86
88
  export interface Spotlight {
@@ -101,6 +103,10 @@ export interface Timeline {
101
103
  intro: { title: string; subtitle?: string; durationMs: number } | null;
102
104
  /** Browser window around the take, or null for the bare viewport. */
103
105
  chrome: { address: string; dark: boolean } | null;
106
+ /** Closing card after the video. */
107
+ outro: { title: string; subtitle?: string; url?: string; durationMs: number } | null;
108
+ /** Logo for the intro and outro cards, as a data URI. */
109
+ logo: string | null;
104
110
  segments: Segment[];
105
111
  camera: CameraKey[];
106
112
  moves: Move[];