framewebworker 0.1.2 → 0.1.4

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,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"]}