@bendyline/squisq-cli 2.8.10 → 2.8.12

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 CHANGED
@@ -84,6 +84,7 @@ squisq video doc.json -o out.mp4
84
84
  | `--dither` | GIF dithering: `bayer`, `sierra2_4a`, or `none` | `sierra2_4a` |
85
85
  | `--bayer-scale` | Ordered Bayer strength, 0–5 | 3 |
86
86
  | `-t, --theme` | Squisq theme id to apply | none |
87
+ | `--motion` | Motion profile: `calm`, `documentary` or `vibrant` | doc's, then theme's |
87
88
  | `--transform` | Transform style to apply before rendering | none |
88
89
  | `--cover-preroll` | Seconds of cover-slide pre-roll before the story starts | 2 |
89
90
  | `--width` / `--height` | Dimension overrides in pixels — **must be even** | MP4 1080p; GIF 960×540 |
@@ -18,7 +18,7 @@ import {
18
18
  renderDocToMp4,
19
19
  resolveDashboardDimensions,
20
20
  validateDashboardImageDimensions
21
- } from "./chunk-4FJZYJC3.js";
21
+ } from "./chunk-6MZC4YQV.js";
22
22
  import {
23
23
  GIF_EXPORT_DEFAULTS,
24
24
  framesToGifNative,
package/dist/api.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Doc } from '@bendyline/squisq/schemas';
1
+ import { Doc, MotionSpec } from '@bendyline/squisq/schemas';
2
2
  import { DashboardStyleId } from '@bendyline/squisq/doc';
3
3
  import { ContentContainer } from '@bendyline/squisq/storage';
4
4
  export { MemoryContentContainer } from '@bendyline/squisq/storage';
@@ -63,6 +63,8 @@ interface FrameManifest {
63
63
  width: number;
64
64
  height: number;
65
65
  captionStyle: string | null;
66
+ /** JSON of the motion spec the frames were captured with (`null` = doc/theme default). */
67
+ motion?: string | null;
66
68
  animationsEnabled: boolean;
67
69
  coverPreRoll: number;
68
70
  captureFormat: CaptureFormat;
@@ -344,6 +346,12 @@ interface RenderDocToMp4Options {
344
346
  height?: number;
345
347
  /** Caption mode to bake into the video (default: off). */
346
348
  captionStyle?: 'off' | 'standard' | 'social';
349
+ /**
350
+ * Motion profile override: `calm` (pre-profile output), `documentary` or
351
+ * `vibrant`, or a spec with overrides. Omitted → the doc's `motion` /
352
+ * frontmatter `squisq-motion`, then the theme's `renderStyle.motionProfile`.
353
+ */
354
+ motion?: MotionSpec | null;
347
355
  /** Render layer animations and block transitions (default: true). */
348
356
  animationsEnabled?: boolean;
349
357
  /**
@@ -405,6 +413,8 @@ interface RenderDocToGifOptions {
405
413
  height?: number;
406
414
  /** Caption mode to bake into the GIF (default: standard). */
407
415
  captionStyle?: 'off' | 'standard' | 'social';
416
+ /** Motion profile override (see {@link RenderDocToMp4Options.motion}). */
417
+ motion?: MotionSpec | null;
408
418
  /** Seconds of cover-slide pre-roll (default: 0). */
409
419
  coverPreRoll?: number;
410
420
  /** Render layer animations and block transitions (default: false). */
package/dist/api.js CHANGED
@@ -1128,6 +1128,7 @@ async function withRenderPage(doc, container, options, fn) {
1128
1128
  height,
1129
1129
  includeAudio,
1130
1130
  captionStyle,
1131
+ motion,
1131
1132
  animationsEnabled,
1132
1133
  displayMode,
1133
1134
  dashboard,
@@ -1188,6 +1189,7 @@ async function withRenderPage(doc, container, options, fn) {
1188
1189
  width,
1189
1190
  height,
1190
1191
  captionStyle,
1192
+ motion,
1191
1193
  animationsEnabled,
1192
1194
  displayMode,
1193
1195
  dashboard
@@ -1198,7 +1200,14 @@ async function withRenderPage(doc, container, options, fn) {
1198
1200
  signal?.throwIfAborted();
1199
1201
  let browser;
1200
1202
  try {
1201
- browser = await chromium.launch({ headless: true });
1203
+ browser = await chromium.launch({
1204
+ headless: true,
1205
+ // Keyframe animations must be sampled by the main thread: the
1206
+ // compositor rasterises a transforming layer differently depending on
1207
+ // whether it caught the animation running, which made mid-motion stills
1208
+ // differ between otherwise identical renders.
1209
+ args: ["--disable-threaded-animation"]
1210
+ });
1202
1211
  } catch (err) {
1203
1212
  signal?.throwIfAborted();
1204
1213
  const detail = err instanceof Error ? err.message.split("\n")[0] : String(err);
@@ -1248,6 +1257,13 @@ Page errors:
1248
1257
  `The standalone player failed to boot in headless Chromium. Render API did not initialize within ${RENDER_BOOT_TIMEOUT_MS / 1e3} seconds.${errorDetail}`
1249
1258
  );
1250
1259
  }
1260
+ signal?.throwIfAborted();
1261
+ await page.evaluate(
1262
+ () => Promise.race([
1263
+ document.fonts?.ready ?? Promise.resolve(void 0),
1264
+ new Promise((resolve2) => setTimeout(resolve2, 1e4))
1265
+ ]).then(() => void 0)
1266
+ );
1251
1267
  renderAPI = await page.evaluateHandle(() => {
1252
1268
  const root = document.getElementById("squisq-root");
1253
1269
  const player = window.SquisqPlayer;
@@ -1334,7 +1350,17 @@ async function iterateDocFrames(session, options, visit) {
1334
1350
  };
1335
1351
  }
1336
1352
  async function captureDocFrames(doc, container, options) {
1337
- const { fps, width, height, captionStyle, coverPreRoll, animationsEnabled, onProgress, signal } = options;
1353
+ const {
1354
+ fps,
1355
+ width,
1356
+ height,
1357
+ captionStyle,
1358
+ motion,
1359
+ coverPreRoll,
1360
+ animationsEnabled,
1361
+ onProgress,
1362
+ signal
1363
+ } = options;
1338
1364
  signal?.throwIfAborted();
1339
1365
  resolveAppliedCoverPreRoll(coverPreRoll, true);
1340
1366
  const ffmpegPath = await requireFfmpeg(signal);
@@ -1343,7 +1369,16 @@ async function captureDocFrames(doc, container, options) {
1343
1369
  return await withRenderPage(
1344
1370
  doc,
1345
1371
  container,
1346
- { signal, width, height, includeAudio: true, captionStyle, animationsEnabled, onProgress },
1372
+ {
1373
+ signal,
1374
+ width,
1375
+ height,
1376
+ includeAudio: true,
1377
+ captionStyle,
1378
+ motion,
1379
+ animationsEnabled,
1380
+ onProgress
1381
+ },
1347
1382
  async (session) => {
1348
1383
  const loop = await iterateDocFrames(
1349
1384
  session,
@@ -1378,6 +1413,7 @@ async function renderDocToMp4Piped(doc, container, options, render) {
1378
1413
  throw new Error("resume requires framesDir so there is a spool to resume from");
1379
1414
  }
1380
1415
  const captionStyle = options.captionStyle === "off" ? void 0 : options.captionStyle;
1416
+ const motion = options.motion ?? null;
1381
1417
  const animationsEnabled = options.animationsEnabled ?? true;
1382
1418
  const ffmpegPath = await requireFfmpeg(signal);
1383
1419
  const outputPath = resolvePath(options.outputPath);
@@ -1389,6 +1425,7 @@ async function renderDocToMp4Piped(doc, container, options, render) {
1389
1425
  width,
1390
1426
  height,
1391
1427
  captionStyle: captionStyle ?? null,
1428
+ motion: motion === null ? null : JSON.stringify(motion),
1392
1429
  animationsEnabled,
1393
1430
  coverPreRoll,
1394
1431
  captureFormat
@@ -1406,7 +1443,16 @@ async function renderDocToMp4Piped(doc, container, options, render) {
1406
1443
  const { loop, activeSink } = await withRenderPage(
1407
1444
  doc,
1408
1445
  container,
1409
- { signal, width, height, includeAudio: true, captionStyle, animationsEnabled, onProgress },
1446
+ {
1447
+ signal,
1448
+ width,
1449
+ height,
1450
+ includeAudio: true,
1451
+ captionStyle,
1452
+ motion,
1453
+ animationsEnabled,
1454
+ onProgress
1455
+ },
1410
1456
  async (session) => {
1411
1457
  const hasCover = coverPreRoll > 0 ? await session.renderAPI.evaluate((api) => api.hasCoverBlock()) : false;
1412
1458
  const appliedCoverPreRoll = resolveAppliedCoverPreRoll(coverPreRoll, hasCover);
@@ -1606,6 +1652,7 @@ async function renderDocToMp4(doc, container, options) {
1606
1652
  width: dimensions.width,
1607
1653
  height: dimensions.height,
1608
1654
  captionStyle: options.captionStyle === "off" ? void 0 : options.captionStyle,
1655
+ motion: options.motion ?? null,
1609
1656
  coverPreRoll: options.coverPreRoll ?? 0,
1610
1657
  animationsEnabled: options.animationsEnabled ?? true,
1611
1658
  onProgress: options.onProgress,
@@ -1669,6 +1716,7 @@ async function renderDocToGif(doc, container, options) {
1669
1716
  width,
1670
1717
  height,
1671
1718
  captionStyle: options.captionStyle === "off" ? void 0 : options.captionStyle ?? "standard",
1719
+ motion: options.motion ?? null,
1672
1720
  coverPreRoll: options.coverPreRoll ?? 0,
1673
1721
  animationsEnabled: options.animationsEnabled ?? false,
1674
1722
  onProgress: options.onProgress,
@@ -542,7 +542,7 @@ function mp4Format() {
542
542
  const animationsEnabled = typeof mp4Opts.animationsEnabled === "boolean" ? mp4Opts.animationsEnabled : MP4_DEFAULTS.animationsEnabled;
543
543
  const outputPath = join2(tmpdir(), `squisq-mp4-${randomBytes(8).toString("hex")}.mp4`);
544
544
  try {
545
- const { renderDocToMp4: renderDocToMp42 } = await import("./api-O34CQ6ZP.js");
545
+ const { renderDocToMp4: renderDocToMp42 } = await import("./api-P7LOLIZT.js");
546
546
  await renderDocToMp42(input.doc, input.container, {
547
547
  outputPath,
548
548
  fps,
@@ -583,7 +583,7 @@ function gifFormat() {
583
583
  const defaultHeight = portrait ? GIF_DEFAULTS.width : GIF_DEFAULTS.height;
584
584
  const outputPath = join2(tmpdir(), `squisq-gif-${randomBytes(8).toString("hex")}.gif`);
585
585
  try {
586
- const { renderDocToGif: renderDocToGif2 } = await import("./api-O34CQ6ZP.js");
586
+ const { renderDocToGif: renderDocToGif2 } = await import("./api-P7LOLIZT.js");
587
587
  const result = await renderDocToGif2(input.doc, input.container, {
588
588
  outputPath,
589
589
  fps: typeof gifOpts.fps === "number" ? gifOpts.fps : GIF_DEFAULTS.fps,
@@ -626,7 +626,7 @@ function pngFormat() {
626
626
  async exportDoc(input, options) {
627
627
  options.signal?.throwIfAborted();
628
628
  const pngOpts = options.formatOptions?.png ?? {};
629
- const { renderDocToDashboardPng: renderDocToDashboardPng2 } = await import("./api-O34CQ6ZP.js");
629
+ const { renderDocToDashboardPng: renderDocToDashboardPng2 } = await import("./api-P7LOLIZT.js");
630
630
  const result = await renderDocToDashboardPng2(input.doc, input.container, {
631
631
  resolution: pngOpts.resolution,
632
632
  width: pngOpts.width,
@@ -1125,6 +1125,7 @@ async function withRenderPage(doc, container, options, fn) {
1125
1125
  height,
1126
1126
  includeAudio,
1127
1127
  captionStyle,
1128
+ motion,
1128
1129
  animationsEnabled,
1129
1130
  displayMode,
1130
1131
  dashboard,
@@ -1185,6 +1186,7 @@ async function withRenderPage(doc, container, options, fn) {
1185
1186
  width,
1186
1187
  height,
1187
1188
  captionStyle,
1189
+ motion,
1188
1190
  animationsEnabled,
1189
1191
  displayMode,
1190
1192
  dashboard
@@ -1195,7 +1197,14 @@ async function withRenderPage(doc, container, options, fn) {
1195
1197
  signal?.throwIfAborted();
1196
1198
  let browser;
1197
1199
  try {
1198
- browser = await chromium.launch({ headless: true });
1200
+ browser = await chromium.launch({
1201
+ headless: true,
1202
+ // Keyframe animations must be sampled by the main thread: the
1203
+ // compositor rasterises a transforming layer differently depending on
1204
+ // whether it caught the animation running, which made mid-motion stills
1205
+ // differ between otherwise identical renders.
1206
+ args: ["--disable-threaded-animation"]
1207
+ });
1199
1208
  } catch (err) {
1200
1209
  signal?.throwIfAborted();
1201
1210
  const detail = err instanceof Error ? err.message.split("\n")[0] : String(err);
@@ -1245,6 +1254,13 @@ Page errors:
1245
1254
  `The standalone player failed to boot in headless Chromium. Render API did not initialize within ${RENDER_BOOT_TIMEOUT_MS / 1e3} seconds.${errorDetail}`
1246
1255
  );
1247
1256
  }
1257
+ signal?.throwIfAborted();
1258
+ await page.evaluate(
1259
+ () => Promise.race([
1260
+ document.fonts?.ready ?? Promise.resolve(void 0),
1261
+ new Promise((resolve2) => setTimeout(resolve2, 1e4))
1262
+ ]).then(() => void 0)
1263
+ );
1248
1264
  renderAPI = await page.evaluateHandle(() => {
1249
1265
  const root = document.getElementById("squisq-root");
1250
1266
  const player = window.SquisqPlayer;
@@ -1331,7 +1347,17 @@ async function iterateDocFrames(session, options, visit) {
1331
1347
  };
1332
1348
  }
1333
1349
  async function captureDocFrames(doc, container, options) {
1334
- const { fps, width, height, captionStyle, coverPreRoll, animationsEnabled, onProgress, signal } = options;
1350
+ const {
1351
+ fps,
1352
+ width,
1353
+ height,
1354
+ captionStyle,
1355
+ motion,
1356
+ coverPreRoll,
1357
+ animationsEnabled,
1358
+ onProgress,
1359
+ signal
1360
+ } = options;
1335
1361
  signal?.throwIfAborted();
1336
1362
  resolveAppliedCoverPreRoll(coverPreRoll, true);
1337
1363
  const ffmpegPath = await requireFfmpeg(signal);
@@ -1340,7 +1366,16 @@ async function captureDocFrames(doc, container, options) {
1340
1366
  return await withRenderPage(
1341
1367
  doc,
1342
1368
  container,
1343
- { signal, width, height, includeAudio: true, captionStyle, animationsEnabled, onProgress },
1369
+ {
1370
+ signal,
1371
+ width,
1372
+ height,
1373
+ includeAudio: true,
1374
+ captionStyle,
1375
+ motion,
1376
+ animationsEnabled,
1377
+ onProgress
1378
+ },
1344
1379
  async (session) => {
1345
1380
  const loop = await iterateDocFrames(
1346
1381
  session,
@@ -1375,6 +1410,7 @@ async function renderDocToMp4Piped(doc, container, options, render) {
1375
1410
  throw new Error("resume requires framesDir so there is a spool to resume from");
1376
1411
  }
1377
1412
  const captionStyle = options.captionStyle === "off" ? void 0 : options.captionStyle;
1413
+ const motion = options.motion ?? null;
1378
1414
  const animationsEnabled = options.animationsEnabled ?? true;
1379
1415
  const ffmpegPath = await requireFfmpeg(signal);
1380
1416
  const outputPath = resolvePath(options.outputPath);
@@ -1386,6 +1422,7 @@ async function renderDocToMp4Piped(doc, container, options, render) {
1386
1422
  width,
1387
1423
  height,
1388
1424
  captionStyle: captionStyle ?? null,
1425
+ motion: motion === null ? null : JSON.stringify(motion),
1389
1426
  animationsEnabled,
1390
1427
  coverPreRoll,
1391
1428
  captureFormat
@@ -1403,7 +1440,16 @@ async function renderDocToMp4Piped(doc, container, options, render) {
1403
1440
  const { loop, activeSink } = await withRenderPage(
1404
1441
  doc,
1405
1442
  container,
1406
- { signal, width, height, includeAudio: true, captionStyle, animationsEnabled, onProgress },
1443
+ {
1444
+ signal,
1445
+ width,
1446
+ height,
1447
+ includeAudio: true,
1448
+ captionStyle,
1449
+ motion,
1450
+ animationsEnabled,
1451
+ onProgress
1452
+ },
1407
1453
  async (session) => {
1408
1454
  const hasCover = coverPreRoll > 0 ? await session.renderAPI.evaluate((api) => api.hasCoverBlock()) : false;
1409
1455
  const appliedCoverPreRoll = resolveAppliedCoverPreRoll(coverPreRoll, hasCover);
@@ -1603,6 +1649,7 @@ async function renderDocToMp4(doc, container, options) {
1603
1649
  width: dimensions.width,
1604
1650
  height: dimensions.height,
1605
1651
  captionStyle: options.captionStyle === "off" ? void 0 : options.captionStyle,
1652
+ motion: options.motion ?? null,
1606
1653
  coverPreRoll: options.coverPreRoll ?? 0,
1607
1654
  animationsEnabled: options.animationsEnabled ?? true,
1608
1655
  onProgress: options.onProgress,
@@ -1666,6 +1713,7 @@ async function renderDocToGif(doc, container, options) {
1666
1713
  width,
1667
1714
  height,
1668
1715
  captionStyle: options.captionStyle === "off" ? void 0 : options.captionStyle ?? "standard",
1716
+ motion: options.motion ?? null,
1669
1717
  coverPreRoll: options.coverPreRoll ?? 0,
1670
1718
  animationsEnabled: options.animationsEnabled ?? false,
1671
1719
  onProgress: options.onProgress,
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  renderDocToDashboardPng,
10
10
  renderDocToGif,
11
11
  renderDocToMp4
12
- } from "./chunk-4FJZYJC3.js";
12
+ } from "./chunk-6MZC4YQV.js";
13
13
  import "./chunk-Y5XKUWJ5.js";
14
14
 
15
15
  // src/index.ts
@@ -319,6 +319,7 @@ import { validateVideoExportOptions } from "@bendyline/squisq-video";
319
319
  var VALID_QUALITIES = ["draft", "normal", "high"];
320
320
  var VALID_ORIENTATIONS = ["landscape", "portrait"];
321
321
  var VALID_CAPTIONS = ["off", "standard", "social"];
322
+ var VALID_MOTION = ["calm", "documentary", "vibrant"];
322
323
  var VALID_FORMATS2 = ["mp4", "gif"];
323
324
  var VALID_GIF_DITHERS = ["bayer", "sierra2_4a", "none"];
324
325
  var VALID_FRAME_TRANSPORTS = ["pipe", "memory"];
@@ -338,6 +339,9 @@ function registerVideoCommand(program2) {
338
339
  "--no-auto-templates",
339
340
  "Disable content-aware template auto-picking for unannotated headings"
340
341
  ).option("-t, --theme <id>", "Squisq theme ID to apply (e.g., documentary, cinematic, bold)").option(
342
+ "--motion <profile>",
343
+ `Motion profile: ${VALID_MOTION.join(", ")} (default: the doc's, then the theme's)`
344
+ ).option(
341
345
  "--transform <style>",
342
346
  "Transform style to apply before rendering (e.g., documentary, magazine, minimal)"
343
347
  ).option(
@@ -415,6 +419,10 @@ async function runVideo(inputPath, opts) {
415
419
  if (!VALID_CAPTIONS.includes(captions)) {
416
420
  throw new Error(`Invalid captions "${captions}". Valid: ${VALID_CAPTIONS.join(", ")}`);
417
421
  }
422
+ const motion = opts.motion;
423
+ if (motion !== void 0 && !VALID_MOTION.includes(motion)) {
424
+ throw new Error(`Invalid motion "${motion}". Valid: ${VALID_MOTION.join(", ")}`);
425
+ }
418
426
  const coverPreRoll = Number(opts.coverPreroll ?? "2");
419
427
  if (!Number.isFinite(coverPreRoll) || coverPreRoll < 0) {
420
428
  throw new Error("Cover pre-roll must be a number of seconds >= 0");
@@ -498,7 +506,7 @@ async function runVideo(inputPath, opts) {
498
506
  if (opts.theme) {
499
507
  doc = { ...doc, themeId: opts.theme };
500
508
  }
501
- const motionLabel = animationsEnabled ? "animations on" : "animations off";
509
+ const motionLabel = animationsEnabled ? `animations on${motion ? ` (${motion})` : ""}` : "animations off";
502
510
  const qualityLabel = outputFormat === "mp4" ? `, quality: ${quality}, transport: ${frameTransport}` : "";
503
511
  console.error(
504
512
  `Rendering ${outputFormat.toUpperCase()}: ${fps} fps${qualityLabel}, orientation: ${orientation}, captions: ${captions}, ${motionLabel}`
@@ -510,6 +518,7 @@ async function runVideo(inputPath, opts) {
510
518
  width,
511
519
  height,
512
520
  captionStyle: captions,
521
+ motion,
513
522
  coverPreRoll,
514
523
  animationsEnabled,
515
524
  onProgress: (phase, percent) => writeProgress(phase, percent, 100)