@bendyline/squisq-video-react 2.2.10 → 2.3.0
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/README.md +15 -5
- package/dist/{chunk-2XACUF6E.js → chunk-32QCPFXE.js} +189 -85
- package/dist/{chunk-MEPETH5V.js → chunk-5MFQMJ5Z.js} +157 -3
- package/dist/{chunk-ERG6OLLO.js → chunk-CRXJOZSH.js} +3 -3
- package/dist/chunk-NTRCMTCF.js +401 -0
- package/dist/chunk-U3RSDWQL.js +978 -0
- package/dist/chunk-YTEDBL6F.js +1075 -0
- package/dist/components/index.d.ts +4 -2
- package/dist/components/index.js +14 -5
- package/dist/cover-image/index.d.ts +23 -0
- package/dist/cover-image/index.js +11 -0
- package/dist/encoder/index.d.ts +1 -1
- package/dist/encoder/index.js +2 -2
- package/dist/hooks/index.d.ts +8 -15
- package/dist/hooks/index.js +8 -4
- package/dist/index.d.ts +4 -2
- package/dist/index.js +18 -6
- package/dist/{mainThreadEncoder-BgcFyYvO.d.ts → mainThreadEncoder-BVCLjlfj.d.ts} +9 -0
- package/dist/{useVideoExport-raCbwbwb.d.ts → useVideoExport-B2eVCvTX.d.ts} +29 -5
- package/dist/workers/encode.worker.js +1 -1
- package/package.json +8 -4
- package/dist/chunk-GLOLS2CQ.js +0 -1567
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useFrameCapture
|
|
3
|
+
} from "./chunk-YTEDBL6F.js";
|
|
4
|
+
|
|
5
|
+
// src/CoverImageExportModal.tsx
|
|
6
|
+
import { useCallback, useId, useRef, useState } from "react";
|
|
7
|
+
import { useModalDialog } from "@bendyline/squisq-react";
|
|
8
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
var MIN_DIMENSION = 64;
|
|
10
|
+
var MAX_DIMENSION = 7680;
|
|
11
|
+
var MAX_PIXELS = 33177600;
|
|
12
|
+
var COVER_IMAGE_SIZE_PRESETS = [{ label: "YouTube cover", width: 1280, height: 720 }];
|
|
13
|
+
var FORMAT_DETAILS = {
|
|
14
|
+
png: { extension: "png", mime: "image/png", label: "PNG \u2014 lossless" },
|
|
15
|
+
jpeg: { extension: "jpg", mime: "image/jpeg", label: "JPEG \u2014 smaller file" },
|
|
16
|
+
webp: { extension: "webp", mime: "image/webp", label: "WebP \u2014 compact" }
|
|
17
|
+
};
|
|
18
|
+
function validateCoverImageDimensions(width, height) {
|
|
19
|
+
if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height)) {
|
|
20
|
+
return "Width and height must be whole numbers.";
|
|
21
|
+
}
|
|
22
|
+
if (width < MIN_DIMENSION || height < MIN_DIMENSION || width > MAX_DIMENSION || height > MAX_DIMENSION) {
|
|
23
|
+
return `Width and height must be between ${MIN_DIMENSION} and ${MAX_DIMENSION} pixels.`;
|
|
24
|
+
}
|
|
25
|
+
if (width * height > MAX_PIXELS) {
|
|
26
|
+
return "Resolution is too large. Choose a size at or below 33 megapixels.";
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
function coverImageFilename(requestedName, format) {
|
|
31
|
+
const extension = FORMAT_DETAILS[format].extension;
|
|
32
|
+
const base = requestedName?.replace(/\.[^.]+$/, "").replace(/[<>:"/\\|?*]/g, "-").split("").map((character) => character.charCodeAt(0) < 32 ? "-" : character).join("").trim().replace(/[. ]+$/g, "") || "document";
|
|
33
|
+
return `${base}-cover.${extension}`;
|
|
34
|
+
}
|
|
35
|
+
function canvasToBlob(canvas, format, quality) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
canvas.toBlob(
|
|
38
|
+
(blob) => {
|
|
39
|
+
if (blob) resolve(blob);
|
|
40
|
+
else reject(new Error("The browser could not encode the cover image."));
|
|
41
|
+
},
|
|
42
|
+
FORMAT_DETAILS[format].mime,
|
|
43
|
+
format === "png" ? void 0 : quality
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function chooseSaveTarget(filename, format) {
|
|
48
|
+
const picker = window.showSaveFilePicker;
|
|
49
|
+
if (!picker) return void 0;
|
|
50
|
+
const details = FORMAT_DETAILS[format];
|
|
51
|
+
try {
|
|
52
|
+
return await picker.call(window, {
|
|
53
|
+
suggestedName: filename,
|
|
54
|
+
types: [
|
|
55
|
+
{
|
|
56
|
+
description: `${details.label.split(" \u2014")[0]} image`,
|
|
57
|
+
accept: { [details.mime]: [`.${details.extension}`] }
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
});
|
|
61
|
+
} catch (caught) {
|
|
62
|
+
if (caught instanceof DOMException && caught.name === "AbortError") return null;
|
|
63
|
+
throw caught;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function downloadBlob(blob, filename) {
|
|
67
|
+
const url = URL.createObjectURL(blob);
|
|
68
|
+
const anchor = document.createElement("a");
|
|
69
|
+
anchor.href = url;
|
|
70
|
+
anchor.download = filename;
|
|
71
|
+
document.body.appendChild(anchor);
|
|
72
|
+
anchor.click();
|
|
73
|
+
anchor.remove();
|
|
74
|
+
window.setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
75
|
+
}
|
|
76
|
+
var overlayStyle = {
|
|
77
|
+
position: "fixed",
|
|
78
|
+
inset: 0,
|
|
79
|
+
zIndex: 1e4,
|
|
80
|
+
display: "flex",
|
|
81
|
+
alignItems: "center",
|
|
82
|
+
justifyContent: "center",
|
|
83
|
+
background: "rgba(0, 0, 0, 0.55)"
|
|
84
|
+
};
|
|
85
|
+
var rowStyle = {
|
|
86
|
+
display: "grid",
|
|
87
|
+
gridTemplateColumns: "1fr 1fr",
|
|
88
|
+
gap: 12
|
|
89
|
+
};
|
|
90
|
+
var labelStyle = {
|
|
91
|
+
display: "grid",
|
|
92
|
+
gap: 5,
|
|
93
|
+
marginBottom: 12,
|
|
94
|
+
fontSize: 13,
|
|
95
|
+
fontWeight: 600
|
|
96
|
+
};
|
|
97
|
+
function CoverImageExportModal({
|
|
98
|
+
doc,
|
|
99
|
+
mediaProvider,
|
|
100
|
+
theme,
|
|
101
|
+
coverSlideTemplate,
|
|
102
|
+
defaultWidth = 1920,
|
|
103
|
+
defaultHeight = 1080,
|
|
104
|
+
defaultFileName,
|
|
105
|
+
colorScheme = "light",
|
|
106
|
+
saveOutput,
|
|
107
|
+
onClose
|
|
108
|
+
}) {
|
|
109
|
+
const overlayRef = useRef(null);
|
|
110
|
+
const dialogRef = useRef(null);
|
|
111
|
+
const titleId = useId();
|
|
112
|
+
const capture = useFrameCapture();
|
|
113
|
+
const [format, setFormat] = useState("png");
|
|
114
|
+
const [width, setWidth] = useState(defaultWidth);
|
|
115
|
+
const [height, setHeight] = useState(defaultHeight);
|
|
116
|
+
const [quality, setQuality] = useState(0.92);
|
|
117
|
+
const [busy, setBusy] = useState(false);
|
|
118
|
+
const [error, setError] = useState(null);
|
|
119
|
+
const dark = colorScheme === "dark";
|
|
120
|
+
const surface = dark ? "#111827" : "#ffffff";
|
|
121
|
+
const control = dark ? "#0f172a" : "#ffffff";
|
|
122
|
+
const text = dark ? "#f8fafc" : "#1f2937";
|
|
123
|
+
const muted = dark ? "#94a3b8" : "#6b7280";
|
|
124
|
+
const border = dark ? "#475569" : "#cbd5e1";
|
|
125
|
+
const filename = coverImageFilename(defaultFileName, format);
|
|
126
|
+
const dimensionError = validateCoverImageDimensions(width, height);
|
|
127
|
+
const handleClose = useCallback(() => {
|
|
128
|
+
if (busy) return;
|
|
129
|
+
capture.destroy();
|
|
130
|
+
onClose();
|
|
131
|
+
}, [busy, capture, onClose]);
|
|
132
|
+
useModalDialog({
|
|
133
|
+
rootRef: overlayRef,
|
|
134
|
+
dialogRef,
|
|
135
|
+
closeOnEscape: !busy,
|
|
136
|
+
onClose: handleClose
|
|
137
|
+
});
|
|
138
|
+
const handleExport = useCallback(async () => {
|
|
139
|
+
if (dimensionError || !doc.startBlock) return;
|
|
140
|
+
setError(null);
|
|
141
|
+
try {
|
|
142
|
+
const saveTarget = saveOutput ? void 0 : await chooseSaveTarget(filename, format);
|
|
143
|
+
if (saveTarget === null) return;
|
|
144
|
+
setBusy(true);
|
|
145
|
+
await capture.init(
|
|
146
|
+
doc,
|
|
147
|
+
{
|
|
148
|
+
width,
|
|
149
|
+
height,
|
|
150
|
+
animationsEnabled: false,
|
|
151
|
+
theme,
|
|
152
|
+
mediaProvider: mediaProvider ?? void 0,
|
|
153
|
+
showCoverSlide: true,
|
|
154
|
+
coverSlideTemplate
|
|
155
|
+
},
|
|
156
|
+
"off"
|
|
157
|
+
);
|
|
158
|
+
await capture.setCoverVisible(true);
|
|
159
|
+
const canvas = await capture.captureCanvasFrame(0);
|
|
160
|
+
const blob = await canvasToBlob(canvas, format, quality);
|
|
161
|
+
if (saveOutput) {
|
|
162
|
+
const saved = await saveOutput(blob, filename);
|
|
163
|
+
if (saved === false) {
|
|
164
|
+
capture.destroy();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
} else if (saveTarget) {
|
|
168
|
+
const writable = await saveTarget.createWritable();
|
|
169
|
+
await writable.write(blob);
|
|
170
|
+
await writable.close();
|
|
171
|
+
} else {
|
|
172
|
+
downloadBlob(blob, filename);
|
|
173
|
+
}
|
|
174
|
+
capture.destroy();
|
|
175
|
+
onClose();
|
|
176
|
+
} catch (caught) {
|
|
177
|
+
capture.destroy();
|
|
178
|
+
setError(caught instanceof Error ? caught.message : "The cover image could not be exported.");
|
|
179
|
+
} finally {
|
|
180
|
+
setBusy(false);
|
|
181
|
+
}
|
|
182
|
+
}, [
|
|
183
|
+
capture,
|
|
184
|
+
coverSlideTemplate,
|
|
185
|
+
dimensionError,
|
|
186
|
+
doc,
|
|
187
|
+
filename,
|
|
188
|
+
format,
|
|
189
|
+
height,
|
|
190
|
+
mediaProvider,
|
|
191
|
+
onClose,
|
|
192
|
+
quality,
|
|
193
|
+
saveOutput,
|
|
194
|
+
theme,
|
|
195
|
+
width
|
|
196
|
+
]);
|
|
197
|
+
const fieldStyle = {
|
|
198
|
+
boxSizing: "border-box",
|
|
199
|
+
width: "100%",
|
|
200
|
+
minHeight: 34,
|
|
201
|
+
border: `1px solid ${border}`,
|
|
202
|
+
borderRadius: 4,
|
|
203
|
+
background: control,
|
|
204
|
+
color: text,
|
|
205
|
+
padding: "6px 8px",
|
|
206
|
+
colorScheme
|
|
207
|
+
};
|
|
208
|
+
return /* @__PURE__ */ jsx(
|
|
209
|
+
"div",
|
|
210
|
+
{
|
|
211
|
+
ref: overlayRef,
|
|
212
|
+
style: overlayStyle,
|
|
213
|
+
"data-color-scheme": colorScheme,
|
|
214
|
+
onClick: (event) => event.stopPropagation(),
|
|
215
|
+
children: /* @__PURE__ */ jsxs(
|
|
216
|
+
"div",
|
|
217
|
+
{
|
|
218
|
+
ref: dialogRef,
|
|
219
|
+
role: "dialog",
|
|
220
|
+
"aria-modal": "true",
|
|
221
|
+
"aria-labelledby": titleId,
|
|
222
|
+
tabIndex: -1,
|
|
223
|
+
style: {
|
|
224
|
+
position: "relative",
|
|
225
|
+
boxSizing: "border-box",
|
|
226
|
+
width: "min(440px, calc(100vw - 32px))",
|
|
227
|
+
padding: 24,
|
|
228
|
+
border: `1px solid ${border}`,
|
|
229
|
+
borderRadius: 8,
|
|
230
|
+
background: surface,
|
|
231
|
+
color: text,
|
|
232
|
+
boxShadow: "0 18px 48px rgba(0, 0, 0, 0.28)",
|
|
233
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
234
|
+
colorScheme
|
|
235
|
+
},
|
|
236
|
+
onClick: (event) => event.stopPropagation(),
|
|
237
|
+
children: [
|
|
238
|
+
/* @__PURE__ */ jsx("h2", { id: titleId, style: { margin: "0 36px 18px 0", fontSize: 19 }, children: "Export cover slide" }),
|
|
239
|
+
/* @__PURE__ */ jsx(
|
|
240
|
+
"button",
|
|
241
|
+
{
|
|
242
|
+
type: "button",
|
|
243
|
+
"aria-label": "Close cover image export",
|
|
244
|
+
disabled: busy,
|
|
245
|
+
onClick: handleClose,
|
|
246
|
+
style: {
|
|
247
|
+
position: "absolute",
|
|
248
|
+
top: 12,
|
|
249
|
+
right: 12,
|
|
250
|
+
width: 32,
|
|
251
|
+
height: 32,
|
|
252
|
+
border: 0,
|
|
253
|
+
background: "transparent",
|
|
254
|
+
color: text,
|
|
255
|
+
fontSize: 24,
|
|
256
|
+
cursor: busy ? "default" : "pointer"
|
|
257
|
+
},
|
|
258
|
+
children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xD7" })
|
|
259
|
+
}
|
|
260
|
+
),
|
|
261
|
+
/* @__PURE__ */ jsxs("label", { style: labelStyle, children: [
|
|
262
|
+
"Format",
|
|
263
|
+
/* @__PURE__ */ jsx(
|
|
264
|
+
"select",
|
|
265
|
+
{
|
|
266
|
+
"aria-label": "Image format",
|
|
267
|
+
value: format,
|
|
268
|
+
disabled: busy,
|
|
269
|
+
onChange: (event) => setFormat(event.target.value),
|
|
270
|
+
style: fieldStyle,
|
|
271
|
+
children: Object.entries(FORMAT_DETAILS).map(([value, details]) => /* @__PURE__ */ jsx("option", { value, children: details.label }, value))
|
|
272
|
+
}
|
|
273
|
+
)
|
|
274
|
+
] }),
|
|
275
|
+
/* @__PURE__ */ jsxs("div", { style: rowStyle, children: [
|
|
276
|
+
/* @__PURE__ */ jsxs("label", { style: labelStyle, children: [
|
|
277
|
+
"Width",
|
|
278
|
+
/* @__PURE__ */ jsx(
|
|
279
|
+
"input",
|
|
280
|
+
{
|
|
281
|
+
"aria-label": "Image width",
|
|
282
|
+
type: "number",
|
|
283
|
+
min: MIN_DIMENSION,
|
|
284
|
+
max: MAX_DIMENSION,
|
|
285
|
+
step: 1,
|
|
286
|
+
value: width,
|
|
287
|
+
disabled: busy,
|
|
288
|
+
onChange: (event) => setWidth(Number(event.target.value)),
|
|
289
|
+
style: fieldStyle
|
|
290
|
+
}
|
|
291
|
+
)
|
|
292
|
+
] }),
|
|
293
|
+
/* @__PURE__ */ jsxs("label", { style: labelStyle, children: [
|
|
294
|
+
"Height",
|
|
295
|
+
/* @__PURE__ */ jsx(
|
|
296
|
+
"input",
|
|
297
|
+
{
|
|
298
|
+
"aria-label": "Image height",
|
|
299
|
+
type: "number",
|
|
300
|
+
min: MIN_DIMENSION,
|
|
301
|
+
max: MAX_DIMENSION,
|
|
302
|
+
step: 1,
|
|
303
|
+
value: height,
|
|
304
|
+
disabled: busy,
|
|
305
|
+
onChange: (event) => setHeight(Number(event.target.value)),
|
|
306
|
+
style: fieldStyle
|
|
307
|
+
}
|
|
308
|
+
)
|
|
309
|
+
] })
|
|
310
|
+
] }),
|
|
311
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: 8, margin: "-2px 0 14px" }, children: [
|
|
312
|
+
/* @__PURE__ */ jsx(
|
|
313
|
+
"button",
|
|
314
|
+
{
|
|
315
|
+
type: "button",
|
|
316
|
+
disabled: busy,
|
|
317
|
+
onClick: () => {
|
|
318
|
+
setWidth(defaultWidth);
|
|
319
|
+
setHeight(defaultHeight);
|
|
320
|
+
},
|
|
321
|
+
children: "1\xD7"
|
|
322
|
+
}
|
|
323
|
+
),
|
|
324
|
+
/* @__PURE__ */ jsx(
|
|
325
|
+
"button",
|
|
326
|
+
{
|
|
327
|
+
type: "button",
|
|
328
|
+
disabled: busy,
|
|
329
|
+
onClick: () => {
|
|
330
|
+
setWidth(defaultWidth * 2);
|
|
331
|
+
setHeight(defaultHeight * 2);
|
|
332
|
+
},
|
|
333
|
+
children: "2\xD7"
|
|
334
|
+
}
|
|
335
|
+
),
|
|
336
|
+
COVER_IMAGE_SIZE_PRESETS.map((preset) => /* @__PURE__ */ jsx(
|
|
337
|
+
"button",
|
|
338
|
+
{
|
|
339
|
+
type: "button",
|
|
340
|
+
disabled: busy,
|
|
341
|
+
title: `${preset.width} \xD7 ${preset.height} pixels`,
|
|
342
|
+
onClick: () => {
|
|
343
|
+
setWidth(preset.width);
|
|
344
|
+
setHeight(preset.height);
|
|
345
|
+
},
|
|
346
|
+
children: preset.label
|
|
347
|
+
},
|
|
348
|
+
preset.label
|
|
349
|
+
)),
|
|
350
|
+
/* @__PURE__ */ jsxs("span", { style: { alignSelf: "center", color: muted, fontSize: 12 }, children: [
|
|
351
|
+
width.toLocaleString(),
|
|
352
|
+
" \xD7 ",
|
|
353
|
+
height.toLocaleString(),
|
|
354
|
+
" pixels"
|
|
355
|
+
] })
|
|
356
|
+
] }),
|
|
357
|
+
format !== "png" && /* @__PURE__ */ jsxs("label", { style: labelStyle, children: [
|
|
358
|
+
"Quality: ",
|
|
359
|
+
Math.round(quality * 100),
|
|
360
|
+
"%",
|
|
361
|
+
/* @__PURE__ */ jsx(
|
|
362
|
+
"input",
|
|
363
|
+
{
|
|
364
|
+
"aria-label": "Image quality",
|
|
365
|
+
type: "range",
|
|
366
|
+
min: 0.5,
|
|
367
|
+
max: 1,
|
|
368
|
+
step: 0.05,
|
|
369
|
+
value: quality,
|
|
370
|
+
disabled: busy,
|
|
371
|
+
onChange: (event) => setQuality(Number(event.target.value))
|
|
372
|
+
}
|
|
373
|
+
)
|
|
374
|
+
] }),
|
|
375
|
+
(dimensionError || error) && /* @__PURE__ */ jsx("p", { role: "alert", style: { margin: "0 0 12px", color: dark ? "#fca5a5" : "#b91c1c" }, children: dimensionError ?? error }),
|
|
376
|
+
!doc.startBlock && /* @__PURE__ */ jsx("p", { role: "alert", style: { margin: "0 0 12px", color: dark ? "#fca5a5" : "#b91c1c" }, children: "This document does not have a cover slide to export." }),
|
|
377
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8 }, children: [
|
|
378
|
+
/* @__PURE__ */ jsx("button", { type: "button", disabled: busy, onClick: handleClose, children: "Cancel" }),
|
|
379
|
+
/* @__PURE__ */ jsx(
|
|
380
|
+
"button",
|
|
381
|
+
{
|
|
382
|
+
type: "button",
|
|
383
|
+
disabled: busy || !!dimensionError || !doc.startBlock,
|
|
384
|
+
onClick: () => void handleExport(),
|
|
385
|
+
style: { minWidth: 132 },
|
|
386
|
+
children: busy ? "Rendering\u2026" : "Choose location\u2026"
|
|
387
|
+
}
|
|
388
|
+
)
|
|
389
|
+
] })
|
|
390
|
+
]
|
|
391
|
+
}
|
|
392
|
+
)
|
|
393
|
+
}
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export {
|
|
398
|
+
validateCoverImageDimensions,
|
|
399
|
+
coverImageFilename,
|
|
400
|
+
CoverImageExportModal
|
|
401
|
+
};
|