framewebworker 0.1.2 → 0.1.3
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.
- package/dist/index.cjs +318 -260
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -4
- package/dist/index.d.ts +27 -4
- package/dist/index.js +318 -260
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +16 -7
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +29 -5
- package/dist/react/index.d.ts +29 -5
- package/dist/react/index.js +16 -7
- package/dist/react/index.js.map +1 -1
- package/dist/worker/render-worker.js +256 -0
- package/package.json +1 -1
- package/dist/render-worker.js +0 -177
- package/dist/render-worker.js.map +0 -1
package/dist/react/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/useRender.ts","../../src/react/useStitch.ts"],"names":["useState","useRef","useCallback"],"mappings":"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","file":"index.js","sourcesContent":["'use client';\n\nimport { useState, useCallback, useRef } from 'react';\nimport type { ClipInput, RenderOptions, FrameWorker } from '../types.js';\n\nexport interface UseRenderState {\n progress: number;\n isRendering: boolean;\n error: Error | null;\n blob: Blob | null;\n url: string | null;\n}\n\nexport interface UseRenderActions {\n render: (clip: ClipInput, options?: Omit<RenderOptions, 'onProgress' | 'signal'>) => Promise<Blob | null>;\n cancel: () => void;\n reset: () => void;\n}\n\nexport type UseRenderResult = UseRenderState & UseRenderActions;\n\nexport function useRender(frameWorker: FrameWorker): UseRenderResult {\n const [state, setState] = useState<UseRenderState>({\n progress: 0,\n isRendering: false,\n error: null,\n blob: null,\n url: null,\n });\n\n const abortRef = useRef<AbortController | null>(null);\n const urlRef = useRef<string | null>(null);\n\n const cancel = useCallback(() => {\n abortRef.current?.abort();\n }, []);\n\n const reset = useCallback(() => {\n if (urlRef.current) {\n URL.revokeObjectURL(urlRef.current);\n urlRef.current = null;\n }\n setState({ progress: 0, isRendering: false, error: null, blob: null, url: null });\n }, []);\n\n const render = useCallback(\n async (\n clip: ClipInput,\n options?: Omit<RenderOptions, 'onProgress' | 'signal'>\n ): Promise<Blob | null> => {\n if (urlRef.current) {\n URL.revokeObjectURL(urlRef.current);\n urlRef.current = null;\n }\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n setState({ progress: 0, isRendering: true, error: null, blob: null, url: null });\n\n try {\n const blob = await frameWorker.render(clip, {\n ...options,\n signal: controller.signal,\n onProgress: (p) => {\n setState((prev) => ({ ...prev, progress: p }));\n },\n });\n\n const url = URL.createObjectURL(blob);\n urlRef.current = url;\n setState({ progress: 1, isRendering: false, error: null, blob, url });\n return blob;\n } catch (err) {\n if (err instanceof DOMException && err.name === 'AbortError') {\n setState((prev) => ({ ...prev, isRendering: false, error: null }));\n return null;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n setState((prev) => ({ ...prev, isRendering: false, error }));\n return null;\n }\n },\n [frameWorker]\n );\n\n return { ...state, render, cancel, reset };\n}\n","'use client';\n\nimport { useState, useCallback, useRef } from 'react';\nimport type { ClipInput, StitchOptions, FrameWorker } from '../types.js';\n\nexport interface UseStitchState {\n progress: number;\n isRendering: boolean;\n error: Error | null;\n blob: Blob | null;\n url: string | null;\n}\n\nexport interface UseStitchActions {\n stitch: (clips: ClipInput[], options?: Omit<StitchOptions, 'onProgress' | 'signal'>) => Promise<Blob | null>;\n cancel: () => void;\n reset: () => void;\n}\n\nexport type UseStitchResult = UseStitchState & UseStitchActions;\n\nexport function useStitch(frameWorker: FrameWorker): UseStitchResult {\n const [state, setState] = useState<UseStitchState>({\n progress: 0,\n isRendering: false,\n error: null,\n blob: null,\n url: null,\n });\n\n const abortRef = useRef<AbortController | null>(null);\n const urlRef = useRef<string | null>(null);\n\n const cancel = useCallback(() => {\n abortRef.current?.abort();\n }, []);\n\n const reset = useCallback(() => {\n if (urlRef.current) {\n URL.revokeObjectURL(urlRef.current);\n urlRef.current = null;\n }\n setState({ progress: 0, isRendering: false, error: null, blob: null, url: null });\n }, []);\n\n const stitch = useCallback(\n async (\n clips: ClipInput[],\n options?: Omit<StitchOptions, 'onProgress' | 'signal'>\n ): Promise<Blob | null> => {\n if (urlRef.current) {\n URL.revokeObjectURL(urlRef.current);\n urlRef.current = null;\n }\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n setState({ progress: 0, isRendering: true, error: null, blob: null, url: null });\n\n try {\n const blob = await frameWorker.stitch(clips, {\n ...options,\n signal: controller.signal,\n onProgress: (p) => {\n setState((prev) => ({ ...prev, progress: p.overall }));\n },\n });\n\n const url = URL.createObjectURL(blob);\n urlRef.current = url;\n setState({ progress: 1, isRendering: false, error: null, blob, url });\n return blob;\n } catch (err) {\n if (err instanceof DOMException && err.name === 'AbortError') {\n setState((prev) => ({ ...prev, isRendering: false, error: null }));\n return null;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n setState((prev) => ({ ...prev, isRendering: false, error }));\n return null;\n }\n },\n [frameWorker]\n );\n\n return { ...state, stitch, cancel, reset };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/react/useRender.ts","../../src/react/useStitch.ts"],"names":["useState","useRef","useCallback"],"mappings":"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","file":"index.js","sourcesContent":["'use client';\n\nimport { useState, useCallback, useRef } from 'react';\nimport type { ClipInput, RenderOptions, FrameWorker } from '../types.js';\n\nexport interface UseRenderState {\n progress: number;\n isRendering: boolean;\n error: Error | null;\n blob: Blob | null;\n url: string | null;\n}\n\nexport interface UseRenderActions {\n render: (clip: ClipInput, options?: Omit<RenderOptions, 'onProgress' | 'signal'>) => Promise<Blob | null>;\n cancel: () => void;\n reset: () => void;\n}\n\nexport type UseRenderResult = UseRenderState & UseRenderActions;\n\nexport function useRender(frameWorker: FrameWorker): UseRenderResult {\n const [state, setState] = useState<UseRenderState>({\n progress: 0,\n isRendering: false,\n error: null,\n blob: null,\n url: null,\n });\n\n const abortRef = useRef<AbortController | null>(null);\n const urlRef = useRef<string | null>(null);\n\n const cancel = useCallback(() => {\n abortRef.current?.abort();\n }, []);\n\n const reset = useCallback(() => {\n if (urlRef.current) {\n URL.revokeObjectURL(urlRef.current);\n urlRef.current = null;\n }\n setState({ progress: 0, isRendering: false, error: null, blob: null, url: null });\n }, []);\n\n const render = useCallback(\n async (\n clip: ClipInput,\n options?: Omit<RenderOptions, 'onProgress' | 'signal'>\n ): Promise<Blob | null> => {\n if (urlRef.current) {\n URL.revokeObjectURL(urlRef.current);\n urlRef.current = null;\n }\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n setState({ progress: 0, isRendering: true, error: null, blob: null, url: null });\n\n try {\n const blob = await frameWorker.render(clip, {\n ...options,\n signal: controller.signal,\n onProgress: (p) => {\n setState((prev) => ({ ...prev, progress: p }));\n },\n });\n\n const url = URL.createObjectURL(blob);\n urlRef.current = url;\n setState({ progress: 1, isRendering: false, error: null, blob, url });\n return blob;\n } catch (err) {\n if (err instanceof DOMException && err.name === 'AbortError') {\n setState((prev) => ({ ...prev, isRendering: false, error: null }));\n return null;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n setState((prev) => ({ ...prev, isRendering: false, error }));\n return null;\n }\n },\n [frameWorker]\n );\n\n return { ...state, render, cancel, reset };\n}\n","'use client';\n\nimport { useState, useCallback, useRef } from 'react';\nimport type { ClipInput, StitchOptions, RichProgress, RenderMetrics, FrameWorker } from '../types.js';\n\nexport interface UseStitchState {\n progress: RichProgress;\n isRendering: boolean;\n error: Error | null;\n blob: Blob | null;\n url: string | null;\n metrics: RenderMetrics | null;\n}\n\nexport interface UseStitchActions {\n stitch: (clips: ClipInput[], options?: Omit<StitchOptions, 'onProgress' | 'onComplete' | 'signal'>) => Promise<Blob | null>;\n cancel: () => void;\n reset: () => void;\n}\n\nexport type UseStitchResult = UseStitchState & UseStitchActions;\n\nconst INITIAL_PROGRESS: RichProgress = { overall: 0, clips: [] };\n\nexport function useStitch(frameWorker: FrameWorker): UseStitchResult {\n const [state, setState] = useState<UseStitchState>({\n progress: INITIAL_PROGRESS,\n isRendering: false,\n error: null,\n blob: null,\n url: null,\n metrics: null,\n });\n\n const abortRef = useRef<AbortController | null>(null);\n const urlRef = useRef<string | null>(null);\n\n const cancel = useCallback(() => {\n abortRef.current?.abort();\n }, []);\n\n const reset = useCallback(() => {\n if (urlRef.current) {\n URL.revokeObjectURL(urlRef.current);\n urlRef.current = null;\n }\n setState({ progress: INITIAL_PROGRESS, isRendering: false, error: null, blob: null, url: null, metrics: null });\n }, []);\n\n const stitch = useCallback(\n async (\n clips: ClipInput[],\n options?: Omit<StitchOptions, 'onProgress' | 'onComplete' | 'signal'>\n ): Promise<Blob | null> => {\n if (urlRef.current) {\n URL.revokeObjectURL(urlRef.current);\n urlRef.current = null;\n }\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n setState({ progress: INITIAL_PROGRESS, isRendering: true, error: null, blob: null, url: null, metrics: null });\n\n try {\n const { blob, metrics } = await frameWorker.stitch(clips, {\n ...options,\n signal: controller.signal,\n onProgress: (p) => {\n setState((prev) => ({ ...prev, progress: p }));\n },\n onComplete: (m) => {\n setState((prev) => ({ ...prev, metrics: m }));\n },\n });\n\n const url = URL.createObjectURL(blob);\n urlRef.current = url;\n const doneProgress: RichProgress = {\n overall: 1,\n clips: clips.map((_, i) => ({ index: i, status: 'done', progress: 1 })),\n };\n setState((prev) => ({ ...prev, progress: doneProgress, isRendering: false, blob, url }));\n return blob;\n } catch (err) {\n if (err instanceof DOMException && err.name === 'AbortError') {\n setState((prev) => ({ ...prev, isRendering: false, error: null }));\n return null;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n setState((prev) => ({ ...prev, isRendering: false, error }));\n return null;\n }\n },\n [frameWorker]\n );\n\n return { ...state, stitch, cancel, reset };\n}\n"]}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// src/captions.ts
|
|
2
|
+
var STYLE_PRESETS = {
|
|
3
|
+
hormozi: {
|
|
4
|
+
preset: "hormozi",
|
|
5
|
+
fontFamily: 'Impact, "Arial Black", sans-serif',
|
|
6
|
+
fontSize: 64,
|
|
7
|
+
fontWeight: "900",
|
|
8
|
+
color: "#FFFFFF",
|
|
9
|
+
strokeColor: "#000000",
|
|
10
|
+
strokeWidth: 4,
|
|
11
|
+
backgroundColor: "transparent",
|
|
12
|
+
backgroundPadding: 0,
|
|
13
|
+
backgroundRadius: 0,
|
|
14
|
+
position: "bottom",
|
|
15
|
+
textAlign: "center",
|
|
16
|
+
lineHeight: 1.1,
|
|
17
|
+
maxWidth: 0.9,
|
|
18
|
+
shadow: true,
|
|
19
|
+
shadowColor: "rgba(0,0,0,0.9)",
|
|
20
|
+
shadowBlur: 6,
|
|
21
|
+
shadowOffsetX: 2,
|
|
22
|
+
shadowOffsetY: 2,
|
|
23
|
+
uppercase: true,
|
|
24
|
+
wordHighlight: true,
|
|
25
|
+
wordHighlightColor: "#FFD700",
|
|
26
|
+
wordHighlightTextColor: "#000000"
|
|
27
|
+
},
|
|
28
|
+
modern: {
|
|
29
|
+
preset: "modern",
|
|
30
|
+
fontFamily: '"Inter", "Helvetica Neue", Arial, sans-serif',
|
|
31
|
+
fontSize: 42,
|
|
32
|
+
fontWeight: "700",
|
|
33
|
+
color: "#FFFFFF",
|
|
34
|
+
strokeColor: "transparent",
|
|
35
|
+
strokeWidth: 0,
|
|
36
|
+
backgroundColor: "rgba(0,0,0,0.65)",
|
|
37
|
+
backgroundPadding: 12,
|
|
38
|
+
backgroundRadius: 8,
|
|
39
|
+
position: "bottom",
|
|
40
|
+
textAlign: "center",
|
|
41
|
+
lineHeight: 1.3,
|
|
42
|
+
maxWidth: 0.85,
|
|
43
|
+
shadow: false,
|
|
44
|
+
shadowColor: "transparent",
|
|
45
|
+
shadowBlur: 0,
|
|
46
|
+
shadowOffsetX: 0,
|
|
47
|
+
shadowOffsetY: 0,
|
|
48
|
+
uppercase: false,
|
|
49
|
+
wordHighlight: false,
|
|
50
|
+
wordHighlightColor: "#3B82F6",
|
|
51
|
+
wordHighlightTextColor: "#FFFFFF"
|
|
52
|
+
},
|
|
53
|
+
minimal: {
|
|
54
|
+
preset: "minimal",
|
|
55
|
+
fontFamily: '"Helvetica Neue", Arial, sans-serif',
|
|
56
|
+
fontSize: 36,
|
|
57
|
+
fontWeight: "400",
|
|
58
|
+
color: "#FFFFFF",
|
|
59
|
+
strokeColor: "transparent",
|
|
60
|
+
strokeWidth: 0,
|
|
61
|
+
backgroundColor: "transparent",
|
|
62
|
+
backgroundPadding: 0,
|
|
63
|
+
backgroundRadius: 0,
|
|
64
|
+
position: "bottom",
|
|
65
|
+
textAlign: "center",
|
|
66
|
+
lineHeight: 1.4,
|
|
67
|
+
maxWidth: 0.8,
|
|
68
|
+
shadow: true,
|
|
69
|
+
shadowColor: "rgba(0,0,0,0.8)",
|
|
70
|
+
shadowBlur: 8,
|
|
71
|
+
shadowOffsetX: 0,
|
|
72
|
+
shadowOffsetY: 2,
|
|
73
|
+
uppercase: false,
|
|
74
|
+
wordHighlight: false,
|
|
75
|
+
wordHighlightColor: "#FFFFFF",
|
|
76
|
+
wordHighlightTextColor: "#000000"
|
|
77
|
+
},
|
|
78
|
+
bold: {
|
|
79
|
+
preset: "bold",
|
|
80
|
+
fontFamily: '"Arial Black", "Helvetica Neue", Arial, sans-serif',
|
|
81
|
+
fontSize: 56,
|
|
82
|
+
fontWeight: "900",
|
|
83
|
+
color: "#FFFF00",
|
|
84
|
+
strokeColor: "#000000",
|
|
85
|
+
strokeWidth: 5,
|
|
86
|
+
backgroundColor: "transparent",
|
|
87
|
+
backgroundPadding: 0,
|
|
88
|
+
backgroundRadius: 0,
|
|
89
|
+
position: "center",
|
|
90
|
+
textAlign: "center",
|
|
91
|
+
lineHeight: 1.2,
|
|
92
|
+
maxWidth: 0.88,
|
|
93
|
+
shadow: true,
|
|
94
|
+
shadowColor: "rgba(0,0,0,1)",
|
|
95
|
+
shadowBlur: 4,
|
|
96
|
+
shadowOffsetX: 3,
|
|
97
|
+
shadowOffsetY: 3,
|
|
98
|
+
uppercase: true,
|
|
99
|
+
wordHighlight: false,
|
|
100
|
+
wordHighlightColor: "#FF0000",
|
|
101
|
+
wordHighlightTextColor: "#FFFFFF"
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
function mergeStyle(base, overrides) {
|
|
105
|
+
return overrides ? { ...base, ...overrides } : base;
|
|
106
|
+
}
|
|
107
|
+
function getActiveCaptions(segments, currentTime) {
|
|
108
|
+
return segments.filter(
|
|
109
|
+
(seg) => currentTime >= seg.startTime && currentTime < seg.endTime
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
function wrapText(ctx2, text, maxWidth) {
|
|
113
|
+
const words = text.split(" ");
|
|
114
|
+
const lines = [];
|
|
115
|
+
let current = "";
|
|
116
|
+
for (const word of words) {
|
|
117
|
+
const test = current ? `${current} ${word}` : word;
|
|
118
|
+
if (ctx2.measureText(test).width > maxWidth && current) {
|
|
119
|
+
lines.push(current);
|
|
120
|
+
current = word;
|
|
121
|
+
} else {
|
|
122
|
+
current = test;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (current) lines.push(current);
|
|
126
|
+
return lines;
|
|
127
|
+
}
|
|
128
|
+
function renderCaption(ctx2, segment, resolvedStyle, canvasWidth, canvasHeight) {
|
|
129
|
+
const style = resolvedStyle;
|
|
130
|
+
const text = style.uppercase ? segment.text.toUpperCase() : segment.text;
|
|
131
|
+
ctx2.save();
|
|
132
|
+
const scaledFontSize = style.fontSize / 1080 * canvasHeight;
|
|
133
|
+
ctx2.font = `${style.fontWeight} ${scaledFontSize}px ${style.fontFamily}`;
|
|
134
|
+
ctx2.textAlign = style.textAlign;
|
|
135
|
+
ctx2.textBaseline = "bottom";
|
|
136
|
+
const maxPx = style.maxWidth * canvasWidth;
|
|
137
|
+
const lines = wrapText(ctx2, text, maxPx);
|
|
138
|
+
const lineH = scaledFontSize * style.lineHeight;
|
|
139
|
+
const totalH = lines.length * lineH;
|
|
140
|
+
let baseY;
|
|
141
|
+
if (style.position === "top") {
|
|
142
|
+
baseY = scaledFontSize * 1.5;
|
|
143
|
+
} else if (style.position === "center") {
|
|
144
|
+
baseY = canvasHeight / 2 - totalH / 2 + lineH;
|
|
145
|
+
} else {
|
|
146
|
+
baseY = canvasHeight - scaledFontSize * 1.2;
|
|
147
|
+
}
|
|
148
|
+
const cx = canvasWidth / 2;
|
|
149
|
+
lines.forEach((line, i) => {
|
|
150
|
+
const y = baseY + i * lineH;
|
|
151
|
+
if (style.backgroundColor && style.backgroundColor !== "transparent") {
|
|
152
|
+
const metrics = ctx2.measureText(line);
|
|
153
|
+
const bw = metrics.width + style.backgroundPadding * 2;
|
|
154
|
+
const bh = lineH + style.backgroundPadding;
|
|
155
|
+
const bx = cx - bw / 2;
|
|
156
|
+
const by = y - lineH;
|
|
157
|
+
ctx2.fillStyle = style.backgroundColor;
|
|
158
|
+
if (style.backgroundRadius > 0) {
|
|
159
|
+
roundRect(ctx2, bx, by, bw, bh, style.backgroundRadius);
|
|
160
|
+
ctx2.fill();
|
|
161
|
+
} else {
|
|
162
|
+
ctx2.fillRect(bx, by, bw, bh);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (style.shadow) {
|
|
166
|
+
ctx2.shadowColor = style.shadowColor;
|
|
167
|
+
ctx2.shadowBlur = style.shadowBlur;
|
|
168
|
+
ctx2.shadowOffsetX = style.shadowOffsetX;
|
|
169
|
+
ctx2.shadowOffsetY = style.shadowOffsetY;
|
|
170
|
+
}
|
|
171
|
+
if (style.strokeWidth > 0 && style.strokeColor !== "transparent") {
|
|
172
|
+
ctx2.lineWidth = style.strokeWidth;
|
|
173
|
+
ctx2.strokeStyle = style.strokeColor;
|
|
174
|
+
ctx2.strokeText(line, cx, y);
|
|
175
|
+
}
|
|
176
|
+
ctx2.shadowColor = "transparent";
|
|
177
|
+
ctx2.shadowBlur = 0;
|
|
178
|
+
ctx2.fillStyle = style.color;
|
|
179
|
+
ctx2.fillText(line, cx, y);
|
|
180
|
+
});
|
|
181
|
+
ctx2.restore();
|
|
182
|
+
}
|
|
183
|
+
function roundRect(ctx2, x, y, w, h, r) {
|
|
184
|
+
ctx2.beginPath();
|
|
185
|
+
ctx2.moveTo(x + r, y);
|
|
186
|
+
ctx2.lineTo(x + w - r, y);
|
|
187
|
+
ctx2.quadraticCurveTo(x + w, y, x + w, y + r);
|
|
188
|
+
ctx2.lineTo(x + w, y + h - r);
|
|
189
|
+
ctx2.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
|
|
190
|
+
ctx2.lineTo(x + r, y + h);
|
|
191
|
+
ctx2.quadraticCurveTo(x, y + h, x, y + h - r);
|
|
192
|
+
ctx2.lineTo(x, y + r);
|
|
193
|
+
ctx2.quadraticCurveTo(x, y, x + r, y);
|
|
194
|
+
ctx2.closePath();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/worker/render-worker.ts
|
|
198
|
+
var meta = null;
|
|
199
|
+
var canvas = null;
|
|
200
|
+
var ctx = null;
|
|
201
|
+
var collected = [];
|
|
202
|
+
self.onmessage = (event) => {
|
|
203
|
+
const msg = event.data;
|
|
204
|
+
switch (msg.type) {
|
|
205
|
+
case "init": {
|
|
206
|
+
meta = msg.meta;
|
|
207
|
+
canvas = new OffscreenCanvas(meta.width, meta.height);
|
|
208
|
+
ctx = canvas.getContext("2d");
|
|
209
|
+
collected.length = 0;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
case "frame": {
|
|
213
|
+
if (!meta || !ctx) return;
|
|
214
|
+
const { bitmap, timestamp, index } = msg;
|
|
215
|
+
ctx.clearRect(0, 0, meta.width, meta.height);
|
|
216
|
+
ctx.drawImage(bitmap, 0, 0, meta.width, meta.height);
|
|
217
|
+
bitmap.close();
|
|
218
|
+
if (meta.captions?.segments?.length) {
|
|
219
|
+
const baseStylePreset = meta.captions.style?.preset ?? "modern";
|
|
220
|
+
const baseStyle = mergeStyle(STYLE_PRESETS[baseStylePreset], meta.captions.style);
|
|
221
|
+
const active = getActiveCaptions(meta.captions.segments, timestamp);
|
|
222
|
+
for (const seg of active) {
|
|
223
|
+
const segStyle = mergeStyle(baseStyle, seg.style);
|
|
224
|
+
renderCaption(
|
|
225
|
+
ctx,
|
|
226
|
+
seg,
|
|
227
|
+
segStyle,
|
|
228
|
+
meta.width,
|
|
229
|
+
meta.height
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const imageData = ctx.getImageData(0, 0, meta.width, meta.height);
|
|
234
|
+
const buffer = imageData.data.buffer.slice(
|
|
235
|
+
imageData.data.byteOffset,
|
|
236
|
+
imageData.data.byteOffset + imageData.data.byteLength
|
|
237
|
+
);
|
|
238
|
+
collected.push({ buffer, timestamp, width: meta.width, height: meta.height });
|
|
239
|
+
const progress = { type: "progress", value: (index + 1) / meta.totalFrames };
|
|
240
|
+
self.postMessage(progress);
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
case "end": {
|
|
244
|
+
const frames = collected.slice();
|
|
245
|
+
const transferables = frames.map((f) => f.buffer);
|
|
246
|
+
const done = { type: "done", frames };
|
|
247
|
+
self.postMessage(done, transferables);
|
|
248
|
+
collected.length = 0;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
case "abort": {
|
|
252
|
+
collected.length = 0;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
package/package.json
CHANGED
package/dist/render-worker.js
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
// src/captions.ts
|
|
2
|
-
function mergeStyle(base, overrides) {
|
|
3
|
-
return overrides ? { ...base, ...overrides } : base;
|
|
4
|
-
}
|
|
5
|
-
function getActiveCaptions(segments, currentTime) {
|
|
6
|
-
return segments.filter(
|
|
7
|
-
(seg) => currentTime >= seg.startTime && currentTime < seg.endTime
|
|
8
|
-
);
|
|
9
|
-
}
|
|
10
|
-
function wrapText(ctx2, text, maxWidth) {
|
|
11
|
-
const words = text.split(" ");
|
|
12
|
-
const lines = [];
|
|
13
|
-
let current = "";
|
|
14
|
-
for (const word of words) {
|
|
15
|
-
const test = current ? `${current} ${word}` : word;
|
|
16
|
-
if (ctx2.measureText(test).width > maxWidth && current) {
|
|
17
|
-
lines.push(current);
|
|
18
|
-
current = word;
|
|
19
|
-
} else {
|
|
20
|
-
current = test;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
if (current) lines.push(current);
|
|
24
|
-
return lines;
|
|
25
|
-
}
|
|
26
|
-
function renderCaption(ctx2, segment, resolvedStyle, canvasWidth, canvasHeight) {
|
|
27
|
-
const style = resolvedStyle;
|
|
28
|
-
const text = style.uppercase ? segment.text.toUpperCase() : segment.text;
|
|
29
|
-
ctx2.save();
|
|
30
|
-
const scaledFontSize = style.fontSize / 1080 * canvasHeight;
|
|
31
|
-
ctx2.font = `${style.fontWeight} ${scaledFontSize}px ${style.fontFamily}`;
|
|
32
|
-
ctx2.textAlign = style.textAlign;
|
|
33
|
-
ctx2.textBaseline = "bottom";
|
|
34
|
-
const maxPx = style.maxWidth * canvasWidth;
|
|
35
|
-
const lines = wrapText(ctx2, text, maxPx);
|
|
36
|
-
const lineH = scaledFontSize * style.lineHeight;
|
|
37
|
-
const totalH = lines.length * lineH;
|
|
38
|
-
let baseY;
|
|
39
|
-
if (style.position === "top") {
|
|
40
|
-
baseY = scaledFontSize * 1.5;
|
|
41
|
-
} else if (style.position === "center") {
|
|
42
|
-
baseY = canvasHeight / 2 - totalH / 2 + lineH;
|
|
43
|
-
} else {
|
|
44
|
-
baseY = canvasHeight - scaledFontSize * 1.2;
|
|
45
|
-
}
|
|
46
|
-
const cx = canvasWidth / 2;
|
|
47
|
-
lines.forEach((line, i) => {
|
|
48
|
-
const y = baseY + i * lineH;
|
|
49
|
-
if (style.backgroundColor && style.backgroundColor !== "transparent") {
|
|
50
|
-
const metrics = ctx2.measureText(line);
|
|
51
|
-
const bw = metrics.width + style.backgroundPadding * 2;
|
|
52
|
-
const bh = lineH + style.backgroundPadding;
|
|
53
|
-
const bx = cx - bw / 2;
|
|
54
|
-
const by = y - lineH;
|
|
55
|
-
ctx2.fillStyle = style.backgroundColor;
|
|
56
|
-
if (style.backgroundRadius > 0) {
|
|
57
|
-
roundRect(ctx2, bx, by, bw, bh, style.backgroundRadius);
|
|
58
|
-
ctx2.fill();
|
|
59
|
-
} else {
|
|
60
|
-
ctx2.fillRect(bx, by, bw, bh);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
if (style.shadow) {
|
|
64
|
-
ctx2.shadowColor = style.shadowColor;
|
|
65
|
-
ctx2.shadowBlur = style.shadowBlur;
|
|
66
|
-
ctx2.shadowOffsetX = style.shadowOffsetX;
|
|
67
|
-
ctx2.shadowOffsetY = style.shadowOffsetY;
|
|
68
|
-
}
|
|
69
|
-
if (style.strokeWidth > 0 && style.strokeColor !== "transparent") {
|
|
70
|
-
ctx2.lineWidth = style.strokeWidth;
|
|
71
|
-
ctx2.strokeStyle = style.strokeColor;
|
|
72
|
-
ctx2.strokeText(line, cx, y);
|
|
73
|
-
}
|
|
74
|
-
ctx2.shadowColor = "transparent";
|
|
75
|
-
ctx2.shadowBlur = 0;
|
|
76
|
-
ctx2.fillStyle = style.color;
|
|
77
|
-
ctx2.fillText(line, cx, y);
|
|
78
|
-
});
|
|
79
|
-
ctx2.restore();
|
|
80
|
-
}
|
|
81
|
-
function roundRect(ctx2, x, y, w, h, r) {
|
|
82
|
-
ctx2.beginPath();
|
|
83
|
-
ctx2.moveTo(x + r, y);
|
|
84
|
-
ctx2.lineTo(x + w - r, y);
|
|
85
|
-
ctx2.quadraticCurveTo(x + w, y, x + w, y + r);
|
|
86
|
-
ctx2.lineTo(x + w, y + h - r);
|
|
87
|
-
ctx2.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
|
|
88
|
-
ctx2.lineTo(x + r, y + h);
|
|
89
|
-
ctx2.quadraticCurveTo(x, y + h, x, y + h - r);
|
|
90
|
-
ctx2.lineTo(x, y + r);
|
|
91
|
-
ctx2.quadraticCurveTo(x, y, x + r, y);
|
|
92
|
-
ctx2.closePath();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// src/worker/render-worker.ts
|
|
96
|
-
var workerSelf = self;
|
|
97
|
-
var ctx = null;
|
|
98
|
-
var meta = null;
|
|
99
|
-
var frames = [];
|
|
100
|
-
var currentJobId = null;
|
|
101
|
-
workerSelf.onmessage = (event) => {
|
|
102
|
-
const msg = event.data;
|
|
103
|
-
try {
|
|
104
|
-
switch (msg.type) {
|
|
105
|
-
case "init": {
|
|
106
|
-
currentJobId = msg.jobId;
|
|
107
|
-
ctx = msg.canvas.getContext("2d");
|
|
108
|
-
meta = msg.meta;
|
|
109
|
-
frames = [];
|
|
110
|
-
break;
|
|
111
|
-
}
|
|
112
|
-
case "frame": {
|
|
113
|
-
if (!ctx || !meta || msg.jobId !== currentJobId) {
|
|
114
|
-
msg.bitmap.close();
|
|
115
|
-
break;
|
|
116
|
-
}
|
|
117
|
-
const { width, height, captions, captionStyle } = meta;
|
|
118
|
-
ctx.drawImage(msg.bitmap, 0, 0, width, height);
|
|
119
|
-
msg.bitmap.close();
|
|
120
|
-
if (captions.length > 0) {
|
|
121
|
-
const active = getActiveCaptions(captions, msg.timestamp);
|
|
122
|
-
for (const seg of active) {
|
|
123
|
-
const segStyle = mergeStyle(captionStyle, seg.style);
|
|
124
|
-
renderCaption(ctx, seg, segStyle, width, height);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
const imageData = ctx.getImageData(0, 0, width, height);
|
|
128
|
-
frames.push({
|
|
129
|
-
buffer: imageData.data.buffer,
|
|
130
|
-
timestamp: msg.timestamp,
|
|
131
|
-
width,
|
|
132
|
-
height
|
|
133
|
-
});
|
|
134
|
-
const progress = {
|
|
135
|
-
type: "progress",
|
|
136
|
-
jobId: msg.jobId,
|
|
137
|
-
currentFrame: msg.frameIndex + 1,
|
|
138
|
-
totalFrames: meta.totalFrames
|
|
139
|
-
};
|
|
140
|
-
workerSelf.postMessage(progress);
|
|
141
|
-
break;
|
|
142
|
-
}
|
|
143
|
-
case "end": {
|
|
144
|
-
if (msg.jobId !== currentJobId) break;
|
|
145
|
-
const transferBuffers = frames.map((f) => f.buffer);
|
|
146
|
-
const done = {
|
|
147
|
-
type: "done",
|
|
148
|
-
jobId: msg.jobId,
|
|
149
|
-
frames: [...frames]
|
|
150
|
-
};
|
|
151
|
-
workerSelf.postMessage(done, transferBuffers);
|
|
152
|
-
ctx = null;
|
|
153
|
-
meta = null;
|
|
154
|
-
frames = [];
|
|
155
|
-
currentJobId = null;
|
|
156
|
-
break;
|
|
157
|
-
}
|
|
158
|
-
case "abort": {
|
|
159
|
-
if (msg.jobId !== currentJobId) break;
|
|
160
|
-
ctx = null;
|
|
161
|
-
meta = null;
|
|
162
|
-
frames = [];
|
|
163
|
-
currentJobId = null;
|
|
164
|
-
break;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
} catch (err) {
|
|
168
|
-
const error = {
|
|
169
|
-
type: "error",
|
|
170
|
-
jobId: msg.jobId,
|
|
171
|
-
message: err instanceof Error ? err.message : String(err)
|
|
172
|
-
};
|
|
173
|
-
workerSelf.postMessage(error);
|
|
174
|
-
}
|
|
175
|
-
};
|
|
176
|
-
//# sourceMappingURL=render-worker.js.map
|
|
177
|
-
//# sourceMappingURL=render-worker.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/captions.ts","../src/worker/render-worker.ts"],"names":["ctx"],"mappings":"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","file":"render-worker.js","sourcesContent":["import type { CaptionSegment, CaptionStyle, CaptionStylePreset } from './types.js';\n\nexport const STYLE_PRESETS: Record<CaptionStylePreset, CaptionStyle> = {\n hormozi: {\n preset: 'hormozi',\n fontFamily: 'Impact, \"Arial Black\", sans-serif',\n fontSize: 64,\n fontWeight: '900',\n color: '#FFFFFF',\n strokeColor: '#000000',\n strokeWidth: 4,\n backgroundColor: 'transparent',\n backgroundPadding: 0,\n backgroundRadius: 0,\n position: 'bottom',\n textAlign: 'center',\n lineHeight: 1.1,\n maxWidth: 0.9,\n shadow: true,\n shadowColor: 'rgba(0,0,0,0.9)',\n shadowBlur: 6,\n shadowOffsetX: 2,\n shadowOffsetY: 2,\n uppercase: true,\n wordHighlight: true,\n wordHighlightColor: '#FFD700',\n wordHighlightTextColor: '#000000',\n },\n modern: {\n preset: 'modern',\n fontFamily: '\"Inter\", \"Helvetica Neue\", Arial, sans-serif',\n fontSize: 42,\n fontWeight: '700',\n color: '#FFFFFF',\n strokeColor: 'transparent',\n strokeWidth: 0,\n backgroundColor: 'rgba(0,0,0,0.65)',\n backgroundPadding: 12,\n backgroundRadius: 8,\n position: 'bottom',\n textAlign: 'center',\n lineHeight: 1.3,\n maxWidth: 0.85,\n shadow: false,\n shadowColor: 'transparent',\n shadowBlur: 0,\n shadowOffsetX: 0,\n shadowOffsetY: 0,\n uppercase: false,\n wordHighlight: false,\n wordHighlightColor: '#3B82F6',\n wordHighlightTextColor: '#FFFFFF',\n },\n minimal: {\n preset: 'minimal',\n fontFamily: '\"Helvetica Neue\", Arial, sans-serif',\n fontSize: 36,\n fontWeight: '400',\n color: '#FFFFFF',\n strokeColor: 'transparent',\n strokeWidth: 0,\n backgroundColor: 'transparent',\n backgroundPadding: 0,\n backgroundRadius: 0,\n position: 'bottom',\n textAlign: 'center',\n lineHeight: 1.4,\n maxWidth: 0.8,\n shadow: true,\n shadowColor: 'rgba(0,0,0,0.8)',\n shadowBlur: 8,\n shadowOffsetX: 0,\n shadowOffsetY: 2,\n uppercase: false,\n wordHighlight: false,\n wordHighlightColor: '#FFFFFF',\n wordHighlightTextColor: '#000000',\n },\n bold: {\n preset: 'bold',\n fontFamily: '\"Arial Black\", \"Helvetica Neue\", Arial, sans-serif',\n fontSize: 56,\n fontWeight: '900',\n color: '#FFFF00',\n strokeColor: '#000000',\n strokeWidth: 5,\n backgroundColor: 'transparent',\n backgroundPadding: 0,\n backgroundRadius: 0,\n position: 'center',\n textAlign: 'center',\n lineHeight: 1.2,\n maxWidth: 0.88,\n shadow: true,\n shadowColor: 'rgba(0,0,0,1)',\n shadowBlur: 4,\n shadowOffsetX: 3,\n shadowOffsetY: 3,\n uppercase: true,\n wordHighlight: false,\n wordHighlightColor: '#FF0000',\n wordHighlightTextColor: '#FFFFFF',\n },\n};\n\nexport function mergeStyle(\n base: CaptionStyle,\n overrides?: Partial<CaptionStyle>\n): CaptionStyle {\n return overrides ? { ...base, ...overrides } : base;\n}\n\nexport function getActiveCaptions(\n segments: CaptionSegment[],\n currentTime: number\n): CaptionSegment[] {\n return segments.filter(\n (seg) => currentTime >= seg.startTime && currentTime < seg.endTime\n );\n}\n\nfunction wrapText(\n ctx: CanvasRenderingContext2D,\n text: string,\n maxWidth: number\n): string[] {\n const words = text.split(' ');\n const lines: string[] = [];\n let current = '';\n\n for (const word of words) {\n const test = current ? `${current} ${word}` : word;\n if (ctx.measureText(test).width > maxWidth && current) {\n lines.push(current);\n current = word;\n } else {\n current = test;\n }\n }\n if (current) lines.push(current);\n return lines;\n}\n\nexport function renderCaption(\n ctx: CanvasRenderingContext2D,\n segment: CaptionSegment,\n resolvedStyle: CaptionStyle,\n canvasWidth: number,\n canvasHeight: number\n): void {\n const style = resolvedStyle;\n const text = style.uppercase ? segment.text.toUpperCase() : segment.text;\n\n ctx.save();\n\n const scaledFontSize = (style.fontSize / 1080) * canvasHeight;\n ctx.font = `${style.fontWeight} ${scaledFontSize}px ${style.fontFamily}`;\n ctx.textAlign = style.textAlign;\n ctx.textBaseline = 'bottom';\n\n const maxPx = style.maxWidth * canvasWidth;\n const lines = wrapText(ctx, text, maxPx);\n const lineH = scaledFontSize * style.lineHeight;\n const totalH = lines.length * lineH;\n\n let baseY: number;\n if (style.position === 'top') {\n baseY = scaledFontSize * 1.5;\n } else if (style.position === 'center') {\n baseY = canvasHeight / 2 - totalH / 2 + lineH;\n } else {\n baseY = canvasHeight - scaledFontSize * 1.2;\n }\n\n const cx = canvasWidth / 2;\n\n lines.forEach((line, i) => {\n const y = baseY + i * lineH;\n\n // Background box\n if (style.backgroundColor && style.backgroundColor !== 'transparent') {\n const metrics = ctx.measureText(line);\n const bw = metrics.width + style.backgroundPadding * 2;\n const bh = lineH + style.backgroundPadding;\n const bx = cx - bw / 2;\n const by = y - lineH;\n\n ctx.fillStyle = style.backgroundColor;\n if (style.backgroundRadius > 0) {\n roundRect(ctx, bx, by, bw, bh, style.backgroundRadius);\n ctx.fill();\n } else {\n ctx.fillRect(bx, by, bw, bh);\n }\n }\n\n // Shadow\n if (style.shadow) {\n ctx.shadowColor = style.shadowColor;\n ctx.shadowBlur = style.shadowBlur;\n ctx.shadowOffsetX = style.shadowOffsetX;\n ctx.shadowOffsetY = style.shadowOffsetY;\n }\n\n // Stroke\n if (style.strokeWidth > 0 && style.strokeColor !== 'transparent') {\n ctx.lineWidth = style.strokeWidth;\n ctx.strokeStyle = style.strokeColor;\n ctx.strokeText(line, cx, y);\n }\n\n // Fill\n ctx.shadowColor = 'transparent';\n ctx.shadowBlur = 0;\n ctx.fillStyle = style.color;\n ctx.fillText(line, cx, y);\n });\n\n ctx.restore();\n}\n\nfunction roundRect(\n ctx: CanvasRenderingContext2D,\n x: number,\n y: number,\n w: number,\n h: number,\n r: number\n): void {\n ctx.beginPath();\n ctx.moveTo(x + r, y);\n ctx.lineTo(x + w - r, y);\n ctx.quadraticCurveTo(x + w, y, x + w, y + r);\n ctx.lineTo(x + w, y + h - r);\n ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);\n ctx.lineTo(x + r, y + h);\n ctx.quadraticCurveTo(x, y + h, x, y + h - r);\n ctx.lineTo(x, y + r);\n ctx.quadraticCurveTo(x, y, x + r, y);\n ctx.closePath();\n}\n","import type { WorkerInbound, WorkerOutbound, WorkerClipMeta, TransferableFrame } from './protocol.js';\nimport { getActiveCaptions, renderCaption, STYLE_PRESETS, mergeStyle } from '../captions.js';\n\n// Cast self to the worker global type to get the correct postMessage signature\nconst workerSelf = self as unknown as DedicatedWorkerGlobalScope;\n\nlet ctx: OffscreenCanvasRenderingContext2D | null = null;\nlet meta: WorkerClipMeta | null = null;\nlet frames: TransferableFrame[] = [];\nlet currentJobId: string | null = null;\n\nworkerSelf.onmessage = (event: MessageEvent<WorkerInbound>) => {\n const msg = event.data;\n\n try {\n switch (msg.type) {\n case 'init': {\n currentJobId = msg.jobId;\n ctx = msg.canvas.getContext('2d') as OffscreenCanvasRenderingContext2D;\n meta = msg.meta;\n frames = [];\n break;\n }\n\n case 'frame': {\n if (!ctx || !meta || msg.jobId !== currentJobId) {\n msg.bitmap.close();\n break;\n }\n\n const { width, height, captions, captionStyle } = meta;\n\n ctx.drawImage(msg.bitmap, 0, 0, width, height);\n msg.bitmap.close(); // release GPU memory\n\n if (captions.length > 0) {\n const active = getActiveCaptions(captions, msg.timestamp);\n for (const seg of active) {\n const segStyle = mergeStyle(captionStyle, seg.style);\n // OffscreenCanvasRenderingContext2D shares the same canvas 2D API\n renderCaption(ctx as unknown as CanvasRenderingContext2D, seg, segStyle, width, height);\n }\n }\n\n const imageData = ctx.getImageData(0, 0, width, height);\n frames.push({\n buffer: imageData.data.buffer,\n timestamp: msg.timestamp,\n width,\n height,\n });\n\n const progress: WorkerOutbound = {\n type: 'progress',\n jobId: msg.jobId,\n currentFrame: msg.frameIndex + 1,\n totalFrames: meta.totalFrames,\n };\n workerSelf.postMessage(progress);\n break;\n }\n\n case 'end': {\n if (msg.jobId !== currentJobId) break;\n\n const transferBuffers = frames.map((f) => f.buffer);\n const done: WorkerOutbound = {\n type: 'done',\n jobId: msg.jobId,\n frames: [...frames],\n };\n workerSelf.postMessage(done, transferBuffers);\n\n ctx = null;\n meta = null;\n frames = [];\n currentJobId = null;\n break;\n }\n\n case 'abort': {\n if (msg.jobId !== currentJobId) break;\n ctx = null;\n meta = null;\n frames = [];\n currentJobId = null;\n break;\n }\n }\n } catch (err) {\n const error: WorkerOutbound = {\n type: 'error',\n jobId: msg.jobId,\n message: err instanceof Error ? err.message : String(err),\n };\n workerSelf.postMessage(error);\n }\n};\n"]}
|