Repository navigation
Expand file tree
/
Copy pathdrawFrame.ts
More file actions
133 lines (121 loc) · 4.53 KB
/
Copy pathdrawFrame.ts
File metadata and controls
133 lines (121 loc) · 4.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import { ClipOp, FilterMode, MipmapMode, Skia, TileMode } from '@shopify/react-native-skia';
import type { SkCanvas } from '@shopify/react-native-skia';
import type { VideoFrame } from '@azzapp/react-native-skia-video';
import type { SharedValue } from 'react-native-reanimated';
import { drawCaptions } from '../captions/drawCaptions';
import type { CaptionBounds } from '../captions/drawCaptions';
import type { Transcript } from '../captions/types';
import { cameraAt } from './camera';
import type { CameraPath } from './camera';
import type { Settings } from './types';
/**
* Draws one frame: the source video seen through one (or two) moving crop
* windows, then the captions on top. One worklet for the live preview AND
* the export.
*
* The two halves of the app never meet: the camera works in SOURCE space
* (which slice of the landscape frame to show), the captions in OUTPUT
* space (fractions of the vertical canvas). Captions are drawn after the
* crop transform is restored, so they never move with the camera.
*
* NOTE: worklet closures are captured where a function is DEFINED, so every
* helper must appear before the function that calls it.
*/
type Rect = { x: number; y: number; width: number; height: number };
/**
* Draws the video frame into `dest`, showing the slice centered on `center`
* (fractions of the source frame). Cover-fitted: the slice always fills the
* destination, whatever the two aspect ratios are.
*/
const drawWindow = (
canvas: SkCanvas,
frame: VideoFrame,
dest: Rect,
center: { x: number; y: number },
zoom: number
) => {
'worklet';
const image = Skia.Image.MakeImageFromNativeTextureUnstable(
frame.texture,
frame.width,
frame.height
);
const rotated = frame.rotation % 180 !== 0;
const visibleWidth = rotated ? image.height() : image.width();
const visibleHeight = rotated ? image.width() : image.height();
const scale = Math.max(dest.width / visibleWidth, dest.height / visibleHeight) * zoom;
// How far the requested center sits from the frame's own center, in
// destination pixels — the camera pan, in other words.
const panX = (center.x - 0.5) * visibleWidth * scale;
const panY = (center.y - 0.5) * visibleHeight * scale;
const matrix = Skia.Matrix();
matrix.translate(dest.x + dest.width / 2 - panX, dest.y + dest.height / 2 - panY);
matrix.scale(scale, scale);
matrix.rotate((frame.rotation * Math.PI) / 180);
matrix.translate(-image.width() / 2, -image.height() / 2);
const shader = image.makeShaderOptions(
TileMode.Clamp,
TileMode.Clamp,
FilterMode.Linear,
MipmapMode.None,
matrix
);
const paint = Skia.Paint();
paint.setShader(shader);
canvas.save();
canvas.clipRect(
Skia.XYWHRect(dest.x, dest.y, dest.width, dest.height),
ClipOp.Intersect,
false
);
canvas.drawPaint(paint);
canvas.restore();
paint.dispose();
shader.dispose();
image.dispose();
};
export type DrawFrameOptions = {
canvas: SkCanvas;
width: number;
height: number;
currentTime: number;
frames: Record<string, VideoFrame>;
/** One path per window: 1 for follow/group, 2 for the split view. */
paths: CameraPath[];
transcript: Transcript;
settings: Settings;
/** Preview only: caption placement being dragged, merged in live. */
captionOverride?: SharedValue<{ y: number; size: number } | null> | null;
/** Preview only: where the captions landed, for hit-testing. */
captionBoundsOut?: SharedValue<CaptionBounds> | null;
/** Preview only: drives the progress bar without re-rendering React. */
playbackTimeOut?: SharedValue<number> | null;
};
export const drawFrame = (options: DrawFrameOptions) => {
'worklet';
const { canvas, width, height, currentTime, settings } = options;
canvas.drawColor(Skia.Color('#000000'));
const frame = options.frames['video'];
if (!frame) return;
const windows = options.paths.length;
const windowHeight = height / Math.max(windows, 1);
for (let i = 0; i < windows; i++) {
const dest = { x: 0, y: i * windowHeight, width, height: windowHeight };
const path = options.paths[i]!;
drawWindow(canvas, frame, dest, cameraAt(path, currentTime), path.zoom);
}
if (settings.showCaptions) {
const override = options.captionOverride?.value;
const captions = override ? { ...settings.captions, ...override } : settings.captions;
drawCaptions(
canvas,
width,
height,
currentTime,
options.transcript,
captions,
options.captionBoundsOut?.value ?? null
);
}
if (options.playbackTimeOut) options.playbackTimeOut.value = currentTime;
};