framewebworker 0.1.1 → 0.1.2

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,177 @@
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
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/captions.ts","../src/worker/render-worker.ts"],"names":["ctx"],"mappings":";AAyGO,SAAS,UAAA,CACd,MACA,SAAA,EACc;AACd,EAAA,OAAO,YAAY,EAAE,GAAG,IAAA,EAAM,GAAG,WAAU,GAAI,IAAA;AACjD;AAEO,SAAS,iBAAA,CACd,UACA,WAAA,EACkB;AAClB,EAAA,OAAO,QAAA,CAAS,MAAA;AAAA,IACd,CAAC,GAAA,KAAQ,WAAA,IAAe,GAAA,CAAI,SAAA,IAAa,cAAc,GAAA,CAAI;AAAA,GAC7D;AACF;AAEA,SAAS,QAAA,CACPA,IAAAA,EACA,IAAA,EACA,QAAA,EACU;AACV,EAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AAC5B,EAAA,MAAM,QAAkB,EAAC;AACzB,EAAA,IAAI,OAAA,GAAU,EAAA;AAEd,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,MAAM,OAAO,OAAA,GAAU,CAAA,EAAG,OAAO,CAAA,CAAA,EAAI,IAAI,CAAA,CAAA,GAAK,IAAA;AAC9C,IAAA,IAAIA,KAAI,WAAA,CAAY,IAAI,CAAA,CAAE,KAAA,GAAQ,YAAY,OAAA,EAAS;AACrD,MAAA,KAAA,CAAM,KAAK,OAAO,CAAA;AAClB,MAAA,OAAA,GAAU,IAAA;AAAA,IACZ,CAAA,MAAO;AACL,MAAA,OAAA,GAAU,IAAA;AAAA,IACZ;AAAA,EACF;AACA,EAAA,IAAI,OAAA,EAAS,KAAA,CAAM,IAAA,CAAK,OAAO,CAAA;AAC/B,EAAA,OAAO,KAAA;AACT;AAEO,SAAS,aAAA,CACdA,IAAAA,EACA,OAAA,EACA,aAAA,EACA,aACA,YAAA,EACM;AACN,EAAA,MAAM,KAAA,GAAQ,aAAA;AACd,EAAA,MAAM,OAAO,KAAA,CAAM,SAAA,GAAY,QAAQ,IAAA,CAAK,WAAA,KAAgB,OAAA,CAAQ,IAAA;AAEpE,EAAAA,KAAI,IAAA,EAAK;AAET,EAAA,MAAM,cAAA,GAAkB,KAAA,CAAM,QAAA,GAAW,IAAA,GAAQ,YAAA;AACjD,EAAAA,IAAAA,CAAI,OAAO,CAAA,EAAG,KAAA,CAAM,UAAU,CAAA,CAAA,EAAI,cAAc,CAAA,GAAA,EAAM,KAAA,CAAM,UAAU,CAAA,CAAA;AACtE,EAAAA,IAAAA,CAAI,YAAY,KAAA,CAAM,SAAA;AACtB,EAAAA,KAAI,YAAA,GAAe,QAAA;AAEnB,EAAA,MAAM,KAAA,GAAQ,MAAM,QAAA,GAAW,WAAA;AAC/B,EAAA,MAAM,KAAA,GAAQ,QAAA,CAASA,IAAAA,EAAK,IAAA,EAAM,KAAK,CAAA;AACvC,EAAA,MAAM,KAAA,GAAQ,iBAAiB,KAAA,CAAM,UAAA;AACrC,EAAA,MAAM,MAAA,GAAS,MAAM,MAAA,GAAS,KAAA;AAE9B,EAAA,IAAI,KAAA;AACJ,EAAA,IAAI,KAAA,CAAM,aAAa,KAAA,EAAO;AAC5B,IAAA,KAAA,GAAQ,cAAA,GAAiB,GAAA;AAAA,EAC3B,CAAA,MAAA,IAAW,KAAA,CAAM,QAAA,KAAa,QAAA,EAAU;AACtC,IAAA,KAAA,GAAQ,YAAA,GAAe,CAAA,GAAI,MAAA,GAAS,CAAA,GAAI,KAAA;AAAA,EAC1C,CAAA,MAAO;AACL,IAAA,KAAA,GAAQ,eAAe,cAAA,GAAiB,GAAA;AAAA,EAC1C;AAEA,EAAA,MAAM,KAAK,WAAA,GAAc,CAAA;AAEzB,EAAA,KAAA,CAAM,OAAA,CAAQ,CAAC,IAAA,EAAM,CAAA,KAAM;AACzB,IAAA,MAAM,CAAA,GAAI,QAAQ,CAAA,GAAI,KAAA;AAGtB,IAAA,IAAI,KAAA,CAAM,eAAA,IAAmB,KAAA,CAAM,eAAA,KAAoB,aAAA,EAAe;AACpE,MAAA,MAAM,OAAA,GAAUA,IAAAA,CAAI,WAAA,CAAY,IAAI,CAAA;AACpC,MAAA,MAAM,EAAA,GAAK,OAAA,CAAQ,KAAA,GAAQ,KAAA,CAAM,iBAAA,GAAoB,CAAA;AACrD,MAAA,MAAM,EAAA,GAAK,QAAQ,KAAA,CAAM,iBAAA;AACzB,MAAA,MAAM,EAAA,GAAK,KAAK,EAAA,GAAK,CAAA;AACrB,MAAA,MAAM,KAAK,CAAA,GAAI,KAAA;AAEf,MAAAA,IAAAA,CAAI,YAAY,KAAA,CAAM,eAAA;AACtB,MAAA,IAAI,KAAA,CAAM,mBAAmB,CAAA,EAAG;AAC9B,QAAA,SAAA,CAAUA,MAAK,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,MAAM,gBAAgB,CAAA;AACrD,QAAAA,KAAI,IAAA,EAAK;AAAA,MACX,CAAA,MAAO;AACL,QAAAA,IAAAA,CAAI,QAAA,CAAS,EAAA,EAAI,EAAA,EAAI,IAAI,EAAE,CAAA;AAAA,MAC7B;AAAA,IACF;AAGA,IAAA,IAAI,MAAM,MAAA,EAAQ;AAChB,MAAAA,IAAAA,CAAI,cAAc,KAAA,CAAM,WAAA;AACxB,MAAAA,IAAAA,CAAI,aAAa,KAAA,CAAM,UAAA;AACvB,MAAAA,IAAAA,CAAI,gBAAgB,KAAA,CAAM,aAAA;AAC1B,MAAAA,IAAAA,CAAI,gBAAgB,KAAA,CAAM,aAAA;AAAA,IAC5B;AAGA,IAAA,IAAI,KAAA,CAAM,WAAA,GAAc,CAAA,IAAK,KAAA,CAAM,gBAAgB,aAAA,EAAe;AAChE,MAAAA,IAAAA,CAAI,YAAY,KAAA,CAAM,WAAA;AACtB,MAAAA,IAAAA,CAAI,cAAc,KAAA,CAAM,WAAA;AACxB,MAAAA,IAAAA,CAAI,UAAA,CAAW,IAAA,EAAM,EAAA,EAAI,CAAC,CAAA;AAAA,IAC5B;AAGA,IAAAA,KAAI,WAAA,GAAc,aAAA;AAClB,IAAAA,KAAI,UAAA,GAAa,CAAA;AACjB,IAAAA,IAAAA,CAAI,YAAY,KAAA,CAAM,KAAA;AACtB,IAAAA,IAAAA,CAAI,QAAA,CAAS,IAAA,EAAM,EAAA,EAAI,CAAC,CAAA;AAAA,EAC1B,CAAC,CAAA;AAED,EAAAA,KAAI,OAAA,EAAQ;AACd;AAEA,SAAS,UACPA,IAAAA,EACA,CAAA,EACA,CAAA,EACA,CAAA,EACA,GACA,CAAA,EACM;AACN,EAAAA,KAAI,SAAA,EAAU;AACd,EAAAA,IAAAA,CAAI,MAAA,CAAO,CAAA,GAAI,CAAA,EAAG,CAAC,CAAA;AACnB,EAAAA,IAAAA,CAAI,MAAA,CAAO,CAAA,GAAI,CAAA,GAAI,GAAG,CAAC,CAAA;AACvB,EAAAA,IAAAA,CAAI,iBAAiB,CAAA,GAAI,CAAA,EAAG,GAAG,CAAA,GAAI,CAAA,EAAG,IAAI,CAAC,CAAA;AAC3C,EAAAA,KAAI,MAAA,CAAO,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,IAAI,CAAC,CAAA;AAC3B,EAAAA,IAAAA,CAAI,gBAAA,CAAiB,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,GAAG,CAAA,GAAI,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,CAAC,CAAA;AACnD,EAAAA,IAAAA,CAAI,MAAA,CAAO,CAAA,GAAI,CAAA,EAAG,IAAI,CAAC,CAAA;AACvB,EAAAA,IAAAA,CAAI,iBAAiB,CAAA,EAAG,CAAA,GAAI,GAAG,CAAA,EAAG,CAAA,GAAI,IAAI,CAAC,CAAA;AAC3C,EAAAA,IAAAA,CAAI,MAAA,CAAO,CAAA,EAAG,CAAA,GAAI,CAAC,CAAA;AACnB,EAAAA,KAAI,gBAAA,CAAiB,CAAA,EAAG,CAAA,EAAG,CAAA,GAAI,GAAG,CAAC,CAAA;AACnC,EAAAA,KAAI,SAAA,EAAU;AAChB;;;AC5OA,IAAM,UAAA,GAAa,IAAA;AAEnB,IAAI,GAAA,GAAgD,IAAA;AACpD,IAAI,IAAA,GAA8B,IAAA;AAClC,IAAI,SAA8B,EAAC;AACnC,IAAI,YAAA,GAA8B,IAAA;AAElC,UAAA,CAAW,SAAA,GAAY,CAAC,KAAA,KAAuC;AAC7D,EAAA,MAAM,MAAM,KAAA,CAAM,IAAA;AAElB,EAAA,IAAI;AACF,IAAA,QAAQ,IAAI,IAAA;AAAM,MAChB,KAAK,MAAA,EAAQ;AACX,QAAA,YAAA,GAAe,GAAA,CAAI,KAAA;AACnB,QAAA,GAAA,GAAM,GAAA,CAAI,MAAA,CAAO,UAAA,CAAW,IAAI,CAAA;AAChC,QAAA,IAAA,GAAO,GAAA,CAAI,IAAA;AACX,QAAA,MAAA,GAAS,EAAC;AACV,QAAA;AAAA,MACF;AAAA,MAEA,KAAK,OAAA,EAAS;AACZ,QAAA,IAAI,CAAC,GAAA,IAAO,CAAC,IAAA,IAAQ,GAAA,CAAI,UAAU,YAAA,EAAc;AAC/C,UAAA,GAAA,CAAI,OAAO,KAAA,EAAM;AACjB,UAAA;AAAA,QACF;AAEA,QAAA,MAAM,EAAE,KAAA,EAAO,MAAA,EAAQ,QAAA,EAAU,cAAa,GAAI,IAAA;AAElD,QAAA,GAAA,CAAI,UAAU,GAAA,CAAI,MAAA,EAAQ,CAAA,EAAG,CAAA,EAAG,OAAO,MAAM,CAAA;AAC7C,QAAA,GAAA,CAAI,OAAO,KAAA,EAAM;AAEjB,QAAA,IAAI,QAAA,CAAS,SAAS,CAAA,EAAG;AACvB,UAAA,MAAM,MAAA,GAAS,iBAAA,CAAkB,QAAA,EAAU,GAAA,CAAI,SAAS,CAAA;AACxD,UAAA,KAAA,MAAW,OAAO,MAAA,EAAQ;AACxB,YAAA,MAAM,QAAA,GAAW,UAAA,CAAW,YAAA,EAAc,GAAA,CAAI,KAAK,CAAA;AAEnD,YAAA,aAAA,CAAc,GAAA,EAA4C,GAAA,EAAK,QAAA,EAAU,KAAA,EAAO,MAAM,CAAA;AAAA,UACxF;AAAA,QACF;AAEA,QAAA,MAAM,YAAY,GAAA,CAAI,YAAA,CAAa,CAAA,EAAG,CAAA,EAAG,OAAO,MAAM,CAAA;AACtD,QAAA,MAAA,CAAO,IAAA,CAAK;AAAA,UACV,MAAA,EAAQ,UAAU,IAAA,CAAK,MAAA;AAAA,UACvB,WAAW,GAAA,CAAI,SAAA;AAAA,UACf,KAAA;AAAA,UACA;AAAA,SACD,CAAA;AAED,QAAA,MAAM,QAAA,GAA2B;AAAA,UAC/B,IAAA,EAAM,UAAA;AAAA,UACN,OAAO,GAAA,CAAI,KAAA;AAAA,UACX,YAAA,EAAc,IAAI,UAAA,GAAa,CAAA;AAAA,UAC/B,aAAa,IAAA,CAAK;AAAA,SACpB;AACA,QAAA,UAAA,CAAW,YAAY,QAAQ,CAAA;AAC/B,QAAA;AAAA,MACF;AAAA,MAEA,KAAK,KAAA,EAAO;AACV,QAAA,IAAI,GAAA,CAAI,UAAU,YAAA,EAAc;AAEhC,QAAA,MAAM,kBAAkB,MAAA,CAAO,GAAA,CAAI,CAAC,CAAA,KAAM,EAAE,MAAM,CAAA;AAClD,QAAA,MAAM,IAAA,GAAuB;AAAA,UAC3B,IAAA,EAAM,MAAA;AAAA,UACN,OAAO,GAAA,CAAI,KAAA;AAAA,UACX,MAAA,EAAQ,CAAC,GAAG,MAAM;AAAA,SACpB;AACA,QAAA,UAAA,CAAW,WAAA,CAAY,MAAM,eAAe,CAAA;AAE5C,QAAA,GAAA,GAAM,IAAA;AACN,QAAA,IAAA,GAAO,IAAA;AACP,QAAA,MAAA,GAAS,EAAC;AACV,QAAA,YAAA,GAAe,IAAA;AACf,QAAA;AAAA,MACF;AAAA,MAEA,KAAK,OAAA,EAAS;AACZ,QAAA,IAAI,GAAA,CAAI,UAAU,YAAA,EAAc;AAChC,QAAA,GAAA,GAAM,IAAA;AACN,QAAA,IAAA,GAAO,IAAA;AACP,QAAA,MAAA,GAAS,EAAC;AACV,QAAA,YAAA,GAAe,IAAA;AACf,QAAA;AAAA,MACF;AAAA;AACF,EACF,SAAS,GAAA,EAAK;AACZ,IAAA,MAAM,KAAA,GAAwB;AAAA,MAC5B,IAAA,EAAM,OAAA;AAAA,MACN,OAAO,GAAA,CAAI,KAAA;AAAA,MACX,SAAS,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG;AAAA,KAC1D;AACA,IAAA,UAAA,CAAW,YAAY,KAAK,CAAA;AAAA,EAC9B;AACF,CAAA","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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "framewebworker",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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,256 +0,0 @@
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
- };