framewebworker 0.1.0 → 0.1.1

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.
@@ -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.0",
3
+ "version": "0.1.1",
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",