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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "framewebworker",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Browser-native video rendering and clip export library. Trim, caption, and export MP4 Blobs in the browser — no server needed.",
5
5
  "keywords": [
6
6
  "video",
@@ -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"]}