@vosjs/cli 0.15.0 → 0.16.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.
@@ -2359,8 +2359,13 @@ function parseTranscript(raw) {
2359
2359
 
2360
2360
  // src/plugin/framesTake.ts
2361
2361
  var VIDEO_TOKEN = "__VOILA_CLI_VIDEO__";
2362
+ function stillSupersample(w, h) {
2363
+ return Math.min(3, Math.max(1, Math.ceil(1800 / Math.max(1, w, h))));
2364
+ }
2362
2365
  async function stillsInPage(opts) {
2363
2366
  const w = window;
2367
+ const RW = opts.W * opts.ss;
2368
+ const RH = opts.H * opts.ss;
2364
2369
  try {
2365
2370
  const vblob = await (await fetch(opts.videoUrl)).blob();
2366
2371
  const vurl = URL.createObjectURL(vblob);
@@ -2377,11 +2382,11 @@ async function stillsInPage(opts) {
2377
2382
  THREE: w.__THREE__,
2378
2383
  gsap: w.__gsap__,
2379
2384
  resolution: {
2380
- width: opts.W,
2381
- height: opts.H,
2385
+ width: RW,
2386
+ height: RH,
2382
2387
  pixelRatio: 1,
2383
- drawingBufferWidth: opts.W,
2384
- drawingBufferHeight: opts.H
2388
+ drawingBufferWidth: RW,
2389
+ drawingBufferHeight: RH
2385
2390
  },
2386
2391
  preserveDrawingBuffer: true
2387
2392
  };
@@ -2391,32 +2396,44 @@ async function stillsInPage(opts) {
2391
2396
  timeline.pause();
2392
2397
  timeline.seek(0, false);
2393
2398
  const canvas = document.querySelector("canvas");
2394
- canvas.width = opts.W;
2395
- canvas.height = opts.H;
2396
- canvas.style.width = opts.W + "px";
2397
- canvas.style.height = opts.H + "px";
2399
+ canvas.width = RW;
2400
+ canvas.height = RH;
2401
+ canvas.style.width = RW + "px";
2402
+ canvas.style.height = RH + "px";
2403
+ const out = document.createElement("canvas");
2404
+ out.width = opts.W;
2405
+ out.height = opts.H;
2398
2406
  const raf = () => new Promise((r) => requestAnimationFrame(r));
2399
2407
  const wvr = () => w.__vos__?.waitForVideosReady ? w.__vos__.waitForVideosReady() : null;
2400
2408
  const pending = () => w.__vos__?.pendingDecodes?.size ?? 0;
2401
- const toPng = () => new Promise(
2402
- (res, rej) => canvas.toBlob(
2409
+ const toPng = () => new Promise((res, rej) => {
2410
+ let src = canvas;
2411
+ if (opts.ss > 1) {
2412
+ const g = out.getContext("2d");
2413
+ g.imageSmoothingEnabled = true;
2414
+ g.imageSmoothingQuality = "high";
2415
+ g.clearRect(0, 0, opts.W, opts.H);
2416
+ g.drawImage(canvas, 0, 0, opts.W, opts.H);
2417
+ src = out;
2418
+ }
2419
+ src.toBlob(
2403
2420
  (b) => b ? res(b) : rej(new Error("toBlob failed")),
2404
2421
  "image/png"
2405
- )
2406
- );
2422
+ );
2423
+ });
2407
2424
  const collectVideos = () => {
2408
- const out = /* @__PURE__ */ new Set();
2425
+ const out2 = /* @__PURE__ */ new Set();
2409
2426
  const cache = w.__vos__?.videoCache;
2410
2427
  if (cache) {
2411
2428
  const vals = cache instanceof Map ? [...cache.values()] : Object.values(cache);
2412
2429
  for (const v of vals) {
2413
- if (v?.tagName === "VIDEO") out.add(v);
2414
- else if (v?.el?.tagName === "VIDEO") out.add(v.el);
2415
- else if (v?.video?.tagName === "VIDEO") out.add(v.video);
2430
+ if (v?.tagName === "VIDEO") out2.add(v);
2431
+ else if (v?.el?.tagName === "VIDEO") out2.add(v.el);
2432
+ else if (v?.video?.tagName === "VIDEO") out2.add(v.video);
2416
2433
  }
2417
2434
  }
2418
- document.querySelectorAll("video").forEach((v) => out.add(v));
2419
- return [...out];
2435
+ document.querySelectorAll("video").forEach((v) => out2.add(v));
2436
+ return [...out2];
2420
2437
  };
2421
2438
  const videosSettled = () => collectVideos().every((v) => !v.seeking && v.readyState >= 2);
2422
2439
  const waitVideosSettled = async (maxMs) => {
@@ -2502,7 +2519,10 @@ async function framesTake(browser, dir, opts) {
2502
2519
  const server = await startTakeServer(dir, {
2503
2520
  "/render.html": renderPageHtml()
2504
2521
  });
2505
- const context = await browser.newContext({ viewport: { width, height } });
2522
+ const ss = stillSupersample(width, height);
2523
+ const context = await browser.newContext({
2524
+ viewport: { width: width * ss, height: height * ss }
2525
+ });
2506
2526
  try {
2507
2527
  const page = await context.newPage();
2508
2528
  page.on("console", (m) => {
@@ -2522,6 +2542,7 @@ async function framesTake(browser, dir, opts) {
2522
2542
  videoUrl: `/${RECORDING_NAME}`,
2523
2543
  W: width,
2524
2544
  H: height,
2545
+ ss,
2525
2546
  shots: named.map(({ time, name }) => ({ time, name }))
2526
2547
  }).catch(() => {
2527
2548
  });
@@ -2592,14 +2613,7 @@ async function writeIndexJson(result) {
2592
2613
  }
2593
2614
 
2594
2615
  // src/plugin/deliver.ts
2595
- import {
2596
- mkdir as mkdir4,
2597
- mkdtemp,
2598
- rename as rename4,
2599
- rm as rm3,
2600
- stat,
2601
- writeFile as writeFile6
2602
- } from "fs/promises";
2616
+ import { mkdir as mkdir4, mkdtemp, rename as rename4, rm as rm3, stat, writeFile as writeFile6 } from "fs/promises";
2603
2617
  import { tmpdir } from "os";
2604
2618
  import { join as join5, relative, resolve as resolve3 } from "path";
2605
2619
  import { totalDuration } from "@vosjs/timeline";
@@ -2720,6 +2734,8 @@ async function renderTake(browser, dir, outFile, opts) {
2720
2734
  import { compileVosConfig as compileVosConfig3 } from "@vosjs/core";
2721
2735
  async function posterStillInPage(opts) {
2722
2736
  const w = window;
2737
+ const RW = opts.W * opts.ss;
2738
+ const RH = opts.H * opts.ss;
2723
2739
  try {
2724
2740
  const mod = await import(
2725
2741
  /* @vite-ignore */
@@ -2733,11 +2749,11 @@ async function posterStillInPage(opts) {
2733
2749
  THREE: w.__THREE__,
2734
2750
  gsap: w.__gsap__,
2735
2751
  resolution: {
2736
- width: opts.W,
2737
- height: opts.H,
2752
+ width: RW,
2753
+ height: RH,
2738
2754
  pixelRatio: 1,
2739
- drawingBufferWidth: opts.W,
2740
- drawingBufferHeight: opts.H
2755
+ drawingBufferWidth: RW,
2756
+ drawingBufferHeight: RH
2741
2757
  },
2742
2758
  preserveDrawingBuffer: true
2743
2759
  };
@@ -2747,16 +2763,27 @@ async function posterStillInPage(opts) {
2747
2763
  timeline.pause();
2748
2764
  timeline.seek(opts.time, false);
2749
2765
  const canvas = document.querySelector("canvas");
2750
- canvas.width = opts.W;
2751
- canvas.height = opts.H;
2752
- canvas.style.width = opts.W + "px";
2753
- canvas.style.height = opts.H + "px";
2766
+ canvas.width = RW;
2767
+ canvas.height = RH;
2768
+ canvas.style.width = RW + "px";
2769
+ canvas.style.height = RH + "px";
2754
2770
  const raf = () => new Promise((r) => requestAnimationFrame(r));
2755
2771
  await raf();
2756
2772
  await raf();
2757
2773
  await raf();
2774
+ let src = canvas;
2775
+ if (opts.ss > 1) {
2776
+ const out = document.createElement("canvas");
2777
+ out.width = opts.W;
2778
+ out.height = opts.H;
2779
+ const g = out.getContext("2d");
2780
+ g.imageSmoothingEnabled = true;
2781
+ g.imageSmoothingQuality = "high";
2782
+ g.drawImage(canvas, 0, 0, opts.W, opts.H);
2783
+ src = out;
2784
+ }
2758
2785
  const png = await new Promise(
2759
- (res) => canvas.toBlob((b) => res(b), "image/png")
2786
+ (res) => src.toBlob((b) => res(b), "image/png")
2760
2787
  );
2761
2788
  if (!png) throw new Error("toBlob returned null");
2762
2789
  await fetch("/save?name=" + opts.outName, { method: "POST", body: png });
@@ -2774,8 +2801,9 @@ async function renderPosterStills(browser, config, serveDir, shots, time) {
2774
2801
  });
2775
2802
  try {
2776
2803
  for (const shot of shots) {
2804
+ const ss = stillSupersample(shot.width, shot.height);
2777
2805
  const context = await browser.newContext({
2778
- viewport: { width: shot.width, height: shot.height }
2806
+ viewport: { width: shot.width * ss, height: shot.height * ss }
2779
2807
  });
2780
2808
  try {
2781
2809
  const page = await context.newPage();
@@ -2794,6 +2822,7 @@ async function renderPosterStills(browser, config, serveDir, shots, time) {
2794
2822
  animationCode,
2795
2823
  W: shot.width,
2796
2824
  H: shot.height,
2825
+ ss,
2797
2826
  time,
2798
2827
  outName: shot.name
2799
2828
  }).catch(() => {
@@ -3325,7 +3354,8 @@ function blurPlane(src, w, h, r, passes = 3) {
3325
3354
  for (let y = 0; y < h; y++) {
3326
3355
  let acc = 0;
3327
3356
  const row = y * w;
3328
- for (let x = -r; x <= r; x++) acc += src[row + Math.min(w - 1, Math.max(0, x))];
3357
+ for (let x = -r; x <= r; x++)
3358
+ acc += src[row + Math.min(w - 1, Math.max(0, x))];
3329
3359
  for (let x = 0; x < w; x++) {
3330
3360
  tmp[row + x] = acc / (2 * r + 1);
3331
3361
  const add = src[row + Math.min(w - 1, x + r + 1)];
@@ -3335,7 +3365,8 @@ function blurPlane(src, w, h, r, passes = 3) {
3335
3365
  }
3336
3366
  for (let x = 0; x < w; x++) {
3337
3367
  let acc = 0;
3338
- for (let y = -r; y <= r; y++) acc += tmp[Math.min(h - 1, Math.max(0, y)) * w + x];
3368
+ for (let y = -r; y <= r; y++)
3369
+ acc += tmp[Math.min(h - 1, Math.max(0, y)) * w + x];
3339
3370
  for (let y = 0; y < h; y++) {
3340
3371
  src[y * w + x] = acc / (2 * r + 1);
3341
3372
  const add = tmp[Math.min(h - 1, y + r + 1) * w + x];
@@ -3351,7 +3382,8 @@ function roundedCoverage(x, y, rect, r) {
3351
3382
  for (const dx of [0.25, 0.75]) {
3352
3383
  const px = x + dx;
3353
3384
  const py = y + dy;
3354
- if (px < rect.x || py < rect.y || px > rect.x + rect.w || py > rect.y + rect.h) continue;
3385
+ if (px < rect.x || py < rect.y || px > rect.x + rect.w || py > rect.y + rect.h)
3386
+ continue;
3355
3387
  const cx = Math.min(Math.max(px, rect.x + r), rect.x + rect.w - r);
3356
3388
  const cy = Math.min(Math.max(py, rect.y + r), rect.y + rect.h - r);
3357
3389
  if ((px - cx) ** 2 + (py - cy) ** 2 <= r * r) inside++;
@@ -3362,24 +3394,34 @@ function roundedCoverage(x, y, rect, r) {
3362
3394
  function bakeShot(shot, opts = {}) {
3363
3395
  const margin = Math.round(shot.w * (opts.margin ?? 0.06));
3364
3396
  const radius = shot.w * (opts.radius ?? 0.014);
3365
- const shadowA = opts.shadow ?? 0.38;
3366
- const blur = Math.round(shot.w * (opts.blur ?? 0.03));
3367
- const offsetY = Math.round(shot.w * (opts.offsetY ?? 0.012));
3397
+ const shadowA = opts.shadow ?? 0.3;
3398
+ const blur = Math.round(shot.w * (opts.blur ?? 0.05));
3399
+ const offsetY = Math.round(shot.w * (opts.offsetY ?? 0.02));
3368
3400
  const hair = opts.hairline ?? 0;
3369
3401
  const w = shot.w + margin * 2;
3370
3402
  const h = shot.h + margin * 2;
3371
3403
  const out = new Uint8Array(w * h * 4);
3372
3404
  const rect = { x: margin, y: margin, w: shot.w, h: shot.h };
3373
3405
  if (shadowA > 0) {
3374
- const mask = new Float32Array(w * h);
3375
- for (let y = 0; y < h; y++)
3376
- for (let x = 0; x < w; x++) {
3377
- const c = roundedCoverage(x, y - offsetY, rect, radius);
3378
- if (c > 0) mask[y * w + x] = c;
3379
- }
3380
- blurPlane(mask, w, h, Math.max(1, Math.round(blur / 2)));
3406
+ const layers = [
3407
+ [Math.max(1, Math.round(blur / 4)), Math.round(offsetY / 5), 0.4],
3408
+ [blur, offsetY, 0.6]
3409
+ ];
3410
+ const acc = new Float32Array(w * h);
3411
+ for (const [lb, lo, share] of layers) {
3412
+ const mask = new Float32Array(w * h);
3413
+ for (let y = 0; y < h; y++)
3414
+ for (let x = 0; x < w; x++) {
3415
+ const c = roundedCoverage(x, y - lo, rect, radius);
3416
+ if (c > 0) mask[y * w + x] = c;
3417
+ }
3418
+ blurPlane(mask, w, h, Math.max(1, Math.round(lb / 2)));
3419
+ const a = shadowA * share;
3420
+ for (let i = 0; i < w * h; i++)
3421
+ acc[i] = 1 - (1 - acc[i]) * (1 - mask[i] * a);
3422
+ }
3381
3423
  for (let i = 0; i < w * h; i++) {
3382
- const a = mask[i] * shadowA;
3424
+ const a = acc[i];
3383
3425
  if (a <= 2e-3) continue;
3384
3426
  out[i * 4] = 0;
3385
3427
  out[i * 4 + 1] = 0;
@@ -3399,7 +3441,12 @@ function bakeShot(shot, opts = {}) {
3399
3441
  let g = shot.data[si + 1];
3400
3442
  let b = shot.data[si + 2];
3401
3443
  if (hair > 0) {
3402
- const edge = Math.min(x - rect.x, rect.x + rect.w - x, y - rect.y, rect.y + rect.h - y);
3444
+ const edge = Math.min(
3445
+ x - rect.x,
3446
+ rect.x + rect.w - x,
3447
+ y - rect.y,
3448
+ rect.y + rect.h - y
3449
+ );
3403
3450
  if (edge < 1.5) {
3404
3451
  r = Math.round(r * (1 - hair));
3405
3452
  g = Math.round(g * (1 - hair));
@@ -3924,12 +3971,18 @@ var rgb = (hex2) => {
3924
3971
  const n = parseInt(m[1], 16);
3925
3972
  return [n >> 16 & 255, n >> 8 & 255, n & 255];
3926
3973
  };
3927
- var toHex = (c) => "#" + c.map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0")).join("");
3974
+ var toHex = (c) => "#" + c.map(
3975
+ (v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0")
3976
+ ).join("");
3928
3977
  function mixHex(a, b, t) {
3929
3978
  const pa = rgb(a);
3930
3979
  const pb = rgb(b);
3931
3980
  if (!pa || !pb) return a;
3932
- return toHex([pa[0] + (pb[0] - pa[0]) * t, pa[1] + (pb[1] - pa[1]) * t, pa[2] + (pb[2] - pa[2]) * t]);
3981
+ return toHex([
3982
+ pa[0] + (pb[0] - pa[0]) * t,
3983
+ pa[1] + (pb[1] - pa[1]) * t,
3984
+ pa[2] + (pb[2] - pa[2]) * t
3985
+ ]);
3933
3986
  }
3934
3987
  function rgba(hex2, alpha) {
3935
3988
  const c = rgb(hex2) ?? [255, 255, 255];
@@ -3945,14 +3998,15 @@ function resolveFace(family, weights) {
3945
3998
  const first = family.split(",")[0].replace(/['"]/g, "").replace(/\s+variable$/i, "").trim();
3946
3999
  const entry = findFontFamily2(first);
3947
4000
  if (!entry) return null;
3948
- const fonts = [...new Set(weights.map((w) => nearestFontWeight(entry, w)))].map((w) => ({
4001
+ const fonts = [
4002
+ ...new Set(weights.map((w) => nearestFontWeight(entry, w)))
4003
+ ].map((w) => ({
3949
4004
  family: entry.family,
3950
4005
  url: fontFaceUrl(entry.slug, w),
3951
4006
  weight: w
3952
4007
  }));
3953
4008
  return { stack: fontStack(entry), category: entry.category, fonts };
3954
4009
  }
3955
- var HOUSE_SERIF = "Fraunces";
3956
4010
  function posterValues(brand, words) {
3957
4011
  const b = brand ?? {};
3958
4012
  const bgA = b.bgA && HEX.test(b.bgA) ? b.bgA : null;
@@ -3974,7 +4028,11 @@ function posterValues(brand, words) {
3974
4028
  if (bgA) values.grain = lightGround ? 10 : 22;
3975
4029
  if (accent) values.blobA = rgba(accent, lightGround ? 0.28 : 0.4);
3976
4030
  if (bgC) values.blobB = rgba(bgC, 0.75);
3977
- if (accent) values.blobC = rgba(mixHex(accent, "#ffffff", 0.55), lightGround ? 0.35 : 0.25);
4031
+ if (accent)
4032
+ values.blobC = rgba(
4033
+ mixHex(accent, "#ffffff", 0.55),
4034
+ lightGround ? 0.35 : 0.25
4035
+ );
3978
4036
  if (wordmark) values.brand = wordmark;
3979
4037
  const headline = (words.headline ?? "").trim();
3980
4038
  if (headline) values.headline = headline;
@@ -3982,10 +4040,9 @@ function posterValues(brand, words) {
3982
4040
  values.kicker = kicker ? kicker : [wordmark, release].filter(Boolean).join(" ").toUpperCase();
3983
4041
  const fonts = [];
3984
4042
  const display = resolveFace(b.fontDisplay, [600, 700]);
3985
- const serif = display && display.category === "serif" ? display : resolveFace(HOUSE_SERIF, [600, 700]);
3986
- if (serif) {
3987
- values.fontDisplay = serif.stack;
3988
- fonts.push(...serif.fonts);
4043
+ if (display) {
4044
+ values.fontDisplay = display.stack;
4045
+ fonts.push(...display.fonts);
3989
4046
  }
3990
4047
  const body = resolveFace(b.fontBody, [700]);
3991
4048
  if (body) {
@@ -3996,7 +4053,6 @@ function posterValues(brand, words) {
3996
4053
  }
3997
4054
 
3998
4055
  // src/plugin/stages.ts
3999
- var HOUSE_SERIF2 = "Fraunces";
4000
4056
  var str = (v, fallback) => typeof v === "string" && v.trim() ? v.trim() : fallback;
4001
4057
  var firstFamily = (stack) => stack.split(",")[0].replace(/['"]/g, "").trim();
4002
4058
  function stageSplitCover(input) {
@@ -4007,7 +4063,7 @@ function stageSplitCover(input) {
4007
4063
  const ground = `linear-gradient(135deg, ${str(v.bgA, "#8a3d2a")}, ${str(v.bgC, str(v.bgB, "#5c5a2e"))})`;
4008
4064
  const ink = str(v.ink, "#fff6ec");
4009
4065
  const inkSoft = str(v.inkSoft, ink);
4010
- const serif = firstFamily(str(v.fontDisplay, HOUSE_SERIF2));
4066
+ const display = firstFamily(str(v.fontDisplay, "Lexend"));
4011
4067
  const body = firstFamily(str(v.fontBody, "Lexend"));
4012
4068
  const headline = str(v.headline, "");
4013
4069
  const kicker = str(v.kicker, "");
@@ -4026,8 +4082,8 @@ function stageSplitCover(input) {
4026
4082
  `frame.inset=${JSON.stringify(inset)}`,
4027
4083
  'frame.focus={"cx":0,"cy":0}',
4028
4084
  "frame.radius=16",
4029
- "frame.shadow=0.45",
4030
- "frame.shadowContact=0.25",
4085
+ "frame.shadow=0.5",
4086
+ "frame.shadowContact=0",
4031
4087
  "frame.border=0",
4032
4088
  "zoom=[]",
4033
4089
  `tilt=[{"id":"stage","in":0,"out":${input.sourceSeconds.toFixed(3)},"rx":3,"ry":${portrait || square ? 0 : 10}}]`,
@@ -4044,7 +4100,10 @@ function stageSplitCover(input) {
4044
4100
  if (!text) return;
4045
4101
  const lines = text.split("\n");
4046
4102
  const longest = Math.max(...lines.map((l) => l.length));
4047
- const widest = Math.min(column, longest * px * (preset === "title" ? 0.5 : 0.62) / designW);
4103
+ const widest = Math.min(
4104
+ column,
4105
+ longest * px * (preset === "title" ? 0.5 : 0.62) / designW
4106
+ );
4048
4107
  const lh = preset === "title" ? 1.05 : 1.2;
4049
4108
  const h = lines.length * px * lh / 1080;
4050
4109
  clips.push({
@@ -4059,24 +4118,157 @@ function stageSplitCover(input) {
4059
4118
  maxWidth: column,
4060
4119
  lineHeight: lh,
4061
4120
  transform: { x: left + widest / 2, y, scale: 1, rotation: 0 },
4062
- shadow: "none",
4121
+ // Words on the ground carry no footage shadow: a drop shadow under a
4122
+ // headline on a plate is the old-web tell.
4123
+ shadow: 0,
4063
4124
  start: 0,
4064
4125
  duration: +input.outputSeconds.toFixed(3),
4065
4126
  enter: "none",
4066
4127
  exit: "none",
4067
4128
  ...extra
4068
4129
  });
4069
- boxes.push({ x: left, y: y - h / 2, w: widest, h, color, role, label: text.length > 24 ? `${text.slice(0, 24)}\u2026` : text });
4130
+ boxes.push({
4131
+ x: left,
4132
+ y: y - h / 2,
4133
+ w: widest,
4134
+ h,
4135
+ color,
4136
+ role,
4137
+ label: text.length > 24 ? `${text.slice(0, 24)}\u2026` : text
4138
+ });
4070
4139
  };
4071
4140
  const kickerY = portrait ? 0.1 : square ? 0.12 : 0.24;
4072
4141
  const titleY = portrait ? 0.24 : square ? 0.28 : 0.46;
4073
4142
  const brandY = portrait ? 0.94 : square ? 0.94 : 0.86;
4074
- word("stage-kicker", kicker, "label", "JetBrains Mono", 19, kickerY, inkSoft, { letterSpacing: 6, weight: 400 }, "body");
4075
- word("stage-title", headline, "title", serif, size, titleY, ink, { weight: 600 }, "headline");
4076
- word("stage-brand", brand, "label", body, 25, brandY, ink, { weight: 700, letterSpacing: 1 }, "body");
4143
+ word(
4144
+ "stage-kicker",
4145
+ kicker,
4146
+ "label",
4147
+ "JetBrains Mono",
4148
+ 19,
4149
+ kickerY,
4150
+ inkSoft,
4151
+ { letterSpacing: 6, weight: 400 },
4152
+ "body"
4153
+ );
4154
+ word(
4155
+ "stage-title",
4156
+ headline,
4157
+ "title",
4158
+ display,
4159
+ size,
4160
+ titleY,
4161
+ ink,
4162
+ { weight: 600, letterSpacing: -Math.round(size * 0.02) },
4163
+ "headline"
4164
+ );
4165
+ word(
4166
+ "stage-brand",
4167
+ brand,
4168
+ "label",
4169
+ body,
4170
+ 25,
4171
+ brandY,
4172
+ ink,
4173
+ { weight: 700, letterSpacing: 1 },
4174
+ "body"
4175
+ );
4077
4176
  set.push(`overlays=${JSON.stringify(clips)}`);
4078
4177
  return { set, text: boxes, shot };
4079
4178
  }
4179
+ var TILE_MAX_PX = 700;
4180
+ function isTileSize(size) {
4181
+ return Math.max(size.w, size.h) < TILE_MAX_PX;
4182
+ }
4183
+ function stageTile(input) {
4184
+ const R = input.size.w / Math.max(1, input.size.h);
4185
+ const square = R <= 1.15;
4186
+ const v = input.values;
4187
+ const ground = `linear-gradient(135deg, ${str(v.bgA, "#8a3d2a")}, ${str(v.bgC, str(v.bgB, "#5c5a2e"))})`;
4188
+ const ink = str(v.ink, "#fff6ec");
4189
+ const display = firstFamily(str(v.fontDisplay, "Lexend"));
4190
+ const headline = str(v.headline, "") || str(v.brand, "Release");
4191
+ const wordless = input.text === "none";
4192
+ const aspect = input.footageAspect && input.footageAspect > 0 ? input.footageAspect : 16 / 9;
4193
+ const left = square ? 0.08 : 0.07;
4194
+ const top = wordless ? square ? 0.1 : 0.12 : square ? 0.42 : 0.47;
4195
+ const visible = wordless ? square ? 0.66 : 0.8 : square ? 0.56 : 0.62;
4196
+ const cardW = (1 - left) / visible;
4197
+ const cardH = cardW * R / aspect;
4198
+ const inset = {
4199
+ left,
4200
+ right: +(1 - left - cardW).toFixed(4),
4201
+ top,
4202
+ bottom: +(1 - top - cardH).toFixed(4)
4203
+ };
4204
+ const shot = {
4205
+ x: inset.left,
4206
+ y: inset.top,
4207
+ w: 1 - inset.left - inset.right,
4208
+ h: 1 - inset.top - inset.bottom
4209
+ };
4210
+ const set = [
4211
+ `frame.background=${JSON.stringify(ground)}`,
4212
+ "frame.backgroundMedia=null",
4213
+ "frame.fit=cover",
4214
+ `frame.inset=${JSON.stringify(inset)}`,
4215
+ 'frame.focus={"cx":0,"cy":0}',
4216
+ "frame.radius=24",
4217
+ "frame.shadow=0.5",
4218
+ "frame.shadowContact=0",
4219
+ "frame.border=0",
4220
+ "zoom=[]",
4221
+ "tilt=[]",
4222
+ "cursor.visible=false",
4223
+ "cursor.clickFx.style=none"
4224
+ ];
4225
+ const designW = 1080 * input.size.w / input.size.h;
4226
+ const column = square ? 0.84 : 0.86;
4227
+ const px = square ? 96 : 108;
4228
+ const lines = headline.split("\n");
4229
+ const longest = Math.max(...lines.map((l) => l.length));
4230
+ const widest = Math.min(column, longest * px * 0.5 / designW);
4231
+ const lh = 1.05;
4232
+ const h = lines.length * px * lh / 1080;
4233
+ const y = square ? 0.2 : 0.24;
4234
+ const clip = {
4235
+ id: "stage-title",
4236
+ kind: "text",
4237
+ text: headline,
4238
+ preset: "title",
4239
+ family: display,
4240
+ size: px,
4241
+ color: ink,
4242
+ align: "left",
4243
+ maxWidth: column,
4244
+ lineHeight: lh,
4245
+ weight: 600,
4246
+ letterSpacing: -Math.round(px * 0.02),
4247
+ transform: { x: left + widest / 2, y, scale: 1, rotation: 0 },
4248
+ shadow: 0,
4249
+ start: 0,
4250
+ duration: +input.outputSeconds.toFixed(3),
4251
+ enter: "none",
4252
+ exit: "none"
4253
+ };
4254
+ if (wordless) {
4255
+ set.push("overlays=[]");
4256
+ return { set, text: [], shot };
4257
+ }
4258
+ set.push(`overlays=${JSON.stringify([clip])}`);
4259
+ const text = [
4260
+ {
4261
+ x: left,
4262
+ y: y - h / 2,
4263
+ w: widest,
4264
+ h,
4265
+ color: ink,
4266
+ role: "headline",
4267
+ label: headline.length > 24 ? `${headline.slice(0, 24)}\u2026` : headline
4268
+ }
4269
+ ];
4270
+ return { set, text, shot };
4271
+ }
4080
4272
 
4081
4273
  // src/plugin/motionPlan.ts
4082
4274
  import { ratedSegments as ratedSegments4, spanOutputExtent as spanOutputExtent4 } from "@vosjs/studio-core";
@@ -4293,6 +4485,10 @@ function templateForCard(d, opts) {
4293
4485
  if (opts.poster === null) return null;
4294
4486
  if (opts.poster) return { config: opts.poster.config, from: opts.poster.from };
4295
4487
  if (!d.template) return null;
4488
+ if (isTileSize(d.px)) {
4489
+ const config2 = templateByName(d.template) ?? templateByName("card-on-gradient");
4490
+ if (config2) return { config: config2, from: "stage tile", stage: "tile" };
4491
+ }
4296
4492
  let name = d.template;
4297
4493
  let note;
4298
4494
  const wants = templateOf(templateByName(name) ?? {});
@@ -4303,7 +4499,8 @@ function templateForCard(d, opts) {
4303
4499
  }
4304
4500
  const config = templateByName(name);
4305
4501
  if (!config) return null;
4306
- if (name === "split-cover") return { config, from: "stage split-cover", note, stage: true };
4502
+ if (name === "split-cover")
4503
+ return { config, from: "stage split-cover", note, stage: "split-cover" };
4307
4504
  return { config, from: `template ${name}`, note };
4308
4505
  }
4309
4506
  function lookOverrides(look, placement, size, video, opts) {
@@ -4324,7 +4521,12 @@ function lookOverrides(look, placement, size, video, opts) {
4324
4521
  }
4325
4522
  if (!opts.keepMedia) set.push("frame.backgroundMedia=null");
4326
4523
  if (opts.still) {
4327
- set.push("zoom=[]", "tilt=[]", "cursor.visible=false", "cursor.clickFx.style=none");
4524
+ set.push(
4525
+ "zoom=[]",
4526
+ "tilt=[]",
4527
+ "cursor.visible=false",
4528
+ "cursor.clickFx.style=none"
4529
+ );
4328
4530
  }
4329
4531
  return set;
4330
4532
  }
@@ -4496,9 +4698,14 @@ async function deliverTake(browser, dir, opts) {
4496
4698
  if (!set.length) return opts.overrides;
4497
4699
  return { ...opts.overrides, set };
4498
4700
  };
4499
- const cardPlans = destinations.filter((d) => d.kind !== "video" && d.genre === "card" && !NOT_FROM_FOOTAGE[d.id]).map((d) => ({ d, plan: templateForCard(d, opts) })).filter((p) => p.plan !== null);
4701
+ const cardPlans = destinations.filter(
4702
+ (d) => d.kind !== "video" && d.genre === "card" && !NOT_FROM_FOOTAGE[d.id]
4703
+ ).map((d) => ({ d, plan: templateForCard(d, opts) })).filter(
4704
+ (p) => p.plan !== null
4705
+ );
4500
4706
  const posterCardIds = new Set(cardPlans.map((p) => p.d.id));
4501
- for (const p of cardPlans) if (p.plan.note) skipped.push(`note: ${p.plan.note}`);
4707
+ for (const p of cardPlans)
4708
+ if (p.plan.note) skipped.push(`note: ${p.plan.note}`);
4502
4709
  if (cardPlans.length) {
4503
4710
  const meta = doc.source.meta;
4504
4711
  const heroTime = opts.shotTime ?? (stillTimes.length ? stillTimes[0] : duration / 2);
@@ -4534,19 +4741,24 @@ async function deliverTake(browser, dir, opts) {
4534
4741
  const baked = bakeShot(raw, {
4535
4742
  margin: PAD,
4536
4743
  hairline: fill.lightGround ? 0.14 : 0,
4537
- shadow: fill.lightGround ? 0.32 : 0.45
4744
+ shadow: fill.lightGround ? 0.28 : 0.4
4538
4745
  });
4539
4746
  await writeFile6(join5(serveDir, "shot.png"), encodePng(baked));
4540
4747
  const shotAspect = raw.w / raw.h;
4541
4748
  for (const { d, plan } of cardPlans) {
4542
4749
  if (plan.stage) {
4543
- const staged = stageSplitCover({
4750
+ const stageInput = {
4544
4751
  size: d.px,
4545
4752
  values: fill.values,
4546
4753
  sourceSeconds: meta.durationMs / 1e3,
4547
- outputSeconds: duration
4548
- });
4549
- opts.onPhase?.(`${d.channel} ${d.asset} (${specWords(d)}) from ${plan.from}`);
4754
+ outputSeconds: duration,
4755
+ text: d.text,
4756
+ footageAspect: (meta.captureWidth ?? meta.width) / (meta.captureHeight ?? meta.height)
4757
+ };
4758
+ const staged = plan.stage === "tile" ? stageTile(stageInput) : stageSplitCover(stageInput);
4759
+ opts.onPhase?.(
4760
+ `${d.channel} ${d.asset} (${specWords(d)}) from ${plan.from}`
4761
+ );
4550
4762
  const shotDir = await mkdtemp(join5(tmpdir(), "vos-stage-"));
4551
4763
  try {
4552
4764
  const captured = await framesTake(browser, dir, {
@@ -4563,7 +4775,9 @@ async function deliverTake(browser, dir, opts) {
4563
4775
  await rename4(captured.frames[0].file, to2);
4564
4776
  const bytes2 = (await stat(to2)).size;
4565
4777
  if (d.maxBytes !== void 0 && bytes2 > d.maxBytes) {
4566
- skipped.push(`${d.channel} ${d.asset}: ${overCeiling(bytes2, d.maxBytes)} (kept at ${to2})`);
4778
+ skipped.push(
4779
+ `${d.channel} ${d.asset}: ${overCeiling(bytes2, d.maxBytes)} (kept at ${to2})`
4780
+ );
4567
4781
  continue;
4568
4782
  }
4569
4783
  assets.push({
@@ -4577,9 +4791,10 @@ async function deliverTake(browser, dir, opts) {
4577
4791
  seconds: null,
4578
4792
  frameTime: heroTime,
4579
4793
  source: "stage",
4580
- template: "split-cover-stage",
4794
+ template: `${plan.stage}-stage`,
4581
4795
  text: staged.text,
4582
- shot: staged.shot
4796
+ shot: staged.shot,
4797
+ ...plan.stage === "tile" ? { crop: true } : {}
4583
4798
  });
4584
4799
  } finally {
4585
4800
  await rm3(shotDir, { recursive: true, force: true });
@@ -4618,7 +4833,9 @@ async function deliverTake(browser, dir, opts) {
4618
4833
  opts.posterTime ?? posterDuration * 0.9,
4619
4834
  Math.max(0, posterDuration - 0.05)
4620
4835
  );
4621
- opts.onPhase?.(`${d.channel} ${d.asset} (${specWords(d)}) from ${plan.from}, ${filled.aspect}`);
4836
+ opts.onPhase?.(
4837
+ `${d.channel} ${d.asset} (${specWords(d)}) from ${plan.from}, ${filled.aspect}`
4838
+ );
4622
4839
  await renderPosterStills(
4623
4840
  browser,
4624
4841
  config,
@@ -5033,7 +5250,7 @@ function stillFindings(a, img, m) {
5033
5250
  const sh = Math.min(1, a.shot.y + a.shot.h) * img.h - sy;
5034
5251
  const rect = { x: sx, y: sy, w: Math.max(1, sw), h: Math.max(1, sh) };
5035
5252
  const ink = inkCoverage(img, rect);
5036
- if (ink < BLANK_INK) {
5253
+ if (ink < (a.crop ? BLANK_INK / 2 : BLANK_INK)) {
5037
5254
  out.push({
5038
5255
  code: "blank",
5039
5256
  severity: "error",
@@ -5043,7 +5260,7 @@ function stillFindings(a, img, m) {
5043
5260
  bbox: rect
5044
5261
  });
5045
5262
  }
5046
- if (a.shot.w < 0.5 || a.shot.w > 1.2) {
5263
+ if (!a.crop && (a.shot.w < 0.5 || a.shot.w > 1.2)) {
5047
5264
  out.push({
5048
5265
  code: "subject",
5049
5266
  severity: "error",
@@ -5131,7 +5348,12 @@ function stillFindings(a, img, m) {
5131
5348
  function textFindings(a, img) {
5132
5349
  const out = [];
5133
5350
  for (const t of a.text ?? []) {
5134
- const box = { x: t.x * img.w, y: t.y * img.h, w: t.w * img.w, h: t.h * img.h };
5351
+ const box = {
5352
+ x: t.x * img.w,
5353
+ y: t.y * img.h,
5354
+ w: t.w * img.w,
5355
+ h: t.h * img.h
5356
+ };
5135
5357
  const name = t.label ? `"${t.label}"` : "a text box";
5136
5358
  if (box.x < 0 || box.y < 0 || box.x + box.w > img.w + 0.5 || box.y + box.h > img.h + 0.5) {
5137
5359
  out.push({
@@ -5206,7 +5428,9 @@ function duplicateFindings(stills) {
5206
5428
  if (g.length > 1) groups.push(g);
5207
5429
  }
5208
5430
  return groups.map((g) => {
5209
- const composed = g.every((s) => stills.find((x) => x.destination === s.destination)?.composed);
5431
+ const composed = g.every(
5432
+ (s) => stills.find((x) => x.destination === s.destination)?.composed
5433
+ );
5210
5434
  return {
5211
5435
  code: "duplicate",
5212
5436
  severity: composed ? "info" : g.length >= 3 ? "error" : "warning",
@@ -5220,7 +5444,21 @@ async function videoFrame(file, at2, ffmpeg) {
5220
5444
  try {
5221
5445
  const { stdout } = await execFileP(
5222
5446
  ffmpeg,
5223
- ["-v", "error", "-ss", at2.toFixed(3), "-i", file, "-frames:v", "1", "-f", "image2pipe", "-vcodec", "png", "-"],
5447
+ [
5448
+ "-v",
5449
+ "error",
5450
+ "-ss",
5451
+ at2.toFixed(3),
5452
+ "-i",
5453
+ file,
5454
+ "-frames:v",
5455
+ "1",
5456
+ "-f",
5457
+ "image2pipe",
5458
+ "-vcodec",
5459
+ "png",
5460
+ "-"
5461
+ ],
5224
5462
  { encoding: "buffer", maxBuffer: 64 * 1024 * 1024 }
5225
5463
  );
5226
5464
  return decodePng(new Uint8Array(stdout));
@@ -5283,14 +5521,19 @@ async function pictureChecks(assets) {
5283
5521
  }
5284
5522
  const seconds = a.seconds ?? 0;
5285
5523
  const first = await videoFrame(a.file, 0, "ffmpeg");
5286
- const last = await videoFrame(a.file, Math.max(0, seconds - 0.1), "ffmpeg");
5524
+ const last = await videoFrame(
5525
+ a.file,
5526
+ Math.max(0, seconds - 0.1),
5527
+ "ffmpeg"
5528
+ );
5287
5529
  for (const [name, img] of [
5288
5530
  ["first", first],
5289
5531
  ["last", last]
5290
5532
  ]) {
5291
5533
  if (!img) continue;
5292
5534
  const m = measureStill(img);
5293
- if (name === "first") measured.push({ destination: a.destination, measure: m });
5535
+ if (name === "first")
5536
+ measured.push({ destination: a.destination, measure: m });
5294
5537
  const subject = m.card ?? { x: 0, y: 0, w: img.w, h: img.h };
5295
5538
  const ink = inkCoverage(img, subject);
5296
5539
  if (ink < BLANK_INK) {
@@ -5316,7 +5559,11 @@ async function pictureChecks(assets) {
5316
5559
  }
5317
5560
  }
5318
5561
  findings.push(...duplicateFindings(stills));
5319
- const order = { error: 0, warning: 1, info: 2 };
5562
+ const order = {
5563
+ error: 0,
5564
+ warning: 1,
5565
+ info: 2
5566
+ };
5320
5567
  findings.sort((a, b) => order[a.severity] - order[b.severity]);
5321
5568
  return { findings, measured };
5322
5569
  }
@@ -5468,7 +5715,8 @@ async function validateKit(kitPath, opts = {}) {
5468
5715
  text: a.text,
5469
5716
  seconds,
5470
5717
  composed: a.composed,
5471
- shot: a.source === "poster" || a.source === "stage" ? a.shot : void 0
5718
+ shot: a.source === "poster" || a.source === "stage" ? a.shot : void 0,
5719
+ crop: a.crop
5472
5720
  });
5473
5721
  if (!spec) {
5474
5722
  if (a.channel !== "demo")
@@ -10149,4 +10397,4 @@ export {
10149
10397
  convertAgentBrowser,
10150
10398
  run
10151
10399
  };
10152
- //# sourceMappingURL=chunk-HGLKXIXH.js.map
10400
+ //# sourceMappingURL=chunk-PKHORUJ2.js.map