@hyperframes/producer 0.4.7 → 0.4.8

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/dist/index.js CHANGED
@@ -5116,12 +5116,12 @@ var require_common = __commonJS({
5116
5116
  createDebug.skips = [];
5117
5117
  createDebug.formatters = {};
5118
5118
  function selectColor(namespace) {
5119
- let hash = 0;
5119
+ let hash2 = 0;
5120
5120
  for (let i = 0; i < namespace.length; i++) {
5121
- hash = (hash << 5) - hash + namespace.charCodeAt(i);
5122
- hash |= 0;
5121
+ hash2 = (hash2 << 5) - hash2 + namespace.charCodeAt(i);
5122
+ hash2 |= 0;
5123
5123
  }
5124
- return createDebug.colors[Math.abs(hash) % createDebug.colors.length];
5124
+ return createDebug.colors[Math.abs(hash2) % createDebug.colors.length];
5125
5125
  }
5126
5126
  createDebug.selectColor = selectColor;
5127
5127
  function createDebug(namespace) {
@@ -54349,25 +54349,25 @@ var require_data = __commonJS({
54349
54349
  var notmodified_1 = __importDefault2(require_notmodified());
54350
54350
  var debug6 = (0, debug_1.default)("get-uri:data");
54351
54351
  var DataReadable = class extends stream_1.Readable {
54352
- constructor(hash, buf) {
54352
+ constructor(hash2, buf) {
54353
54353
  super();
54354
54354
  this.push(buf);
54355
54355
  this.push(null);
54356
- this.hash = hash;
54356
+ this.hash = hash2;
54357
54357
  }
54358
54358
  };
54359
54359
  var data = async ({ href: uri }, { cache } = {}) => {
54360
54360
  const shasum = (0, crypto_1.createHash)("sha1");
54361
54361
  shasum.update(uri);
54362
- const hash = shasum.digest("hex");
54363
- debug6('generated SHA1 hash for "data:" URI: %o', hash);
54364
- if (cache?.hash === hash) {
54365
- debug6("got matching cache SHA1 hash: %o", hash);
54362
+ const hash2 = shasum.digest("hex");
54363
+ debug6('generated SHA1 hash for "data:" URI: %o', hash2);
54364
+ if (cache?.hash === hash2) {
54365
+ debug6("got matching cache SHA1 hash: %o", hash2);
54366
54366
  throw new notmodified_1.default();
54367
54367
  } else {
54368
54368
  debug6('creating Readable stream from "data:" URI buffer');
54369
54369
  const { buffer } = (0, data_uri_to_buffer_1.dataUriToBuffer)(uri);
54370
- return new DataReadable(hash, Buffer.from(buffer));
54370
+ return new DataReadable(hash2, Buffer.from(buffer));
54371
54371
  }
54372
54372
  };
54373
54373
  exports.data = data;
@@ -75362,14 +75362,14 @@ var require_dist10 = __commonJS({
75362
75362
  (0, quickjs_emscripten_1.getQuickJS)(),
75363
75363
  this.loadPacFile()
75364
75364
  ]);
75365
- const hash = crypto3.createHash("sha1").update(code).digest("hex");
75366
- if (this.resolver && this.resolverHash === hash) {
75365
+ const hash2 = crypto3.createHash("sha1").update(code).digest("hex");
75366
+ if (this.resolver && this.resolverHash === hash2) {
75367
75367
  debug6("Same sha1 hash for code - contents have not changed, reusing previous proxy resolver");
75368
75368
  return this.resolver;
75369
75369
  }
75370
75370
  debug6("Creating new proxy resolver instance");
75371
75371
  this.resolver = (0, pac_resolver_1.createPacResolver)(qjs, code, this.opts);
75372
- this.resolverHash = hash;
75372
+ this.resolverHash = hash2;
75373
75373
  return this.resolver;
75374
75374
  } catch (err) {
75375
75375
  if (this.resolver && err.code === "ENOTMODIFIED") {
@@ -99644,6 +99644,7 @@ var mediaRules = [
99644
99644
  const timedTagPositions = [];
99645
99645
  for (const tag of tags) {
99646
99646
  if (tag.name === "video" || tag.name === "audio") continue;
99647
+ if (readAttr(tag.raw, "data-composition-id")) continue;
99647
99648
  if (readAttr(tag.raw, "data-start")) {
99648
99649
  timedTagPositions.push({
99649
99650
  name: tag.name,
@@ -100750,7 +100751,8 @@ function lintHyperframeHtml(html, options = {}) {
100750
100751
  }
100751
100752
 
100752
100753
  // ../core/src/compiler/rewriteSubCompPaths.ts
100753
- import { join as join4, resolve as resolve6, dirname as dirname4 } from "path";
100754
+ import { posix } from "path";
100755
+ var { join: join4, resolve: resolve6, dirname: dirname4 } = posix;
100754
100756
  var PATH_ATTRS = ["src", "href"];
100755
100757
  var CSS_URL_RE = /\burl\(\s*(["']?)([^)"']+)\1\s*\)/g;
100756
100758
  function isAbsoluteOrSpecial(val) {
@@ -100917,11 +100919,19 @@ async function pageScreenshotCapture(page, options) {
100917
100919
  });
100918
100920
  return Buffer.from(result.data, "base64");
100919
100921
  }
100922
+ var TRANSPARENT_BG_STYLE_ID = "__hf_transparent_bg__";
100920
100923
  async function initTransparentBackground(page) {
100921
100924
  const client = await getCdpSession(page);
100922
100925
  await client.send("Emulation.setDefaultBackgroundColorOverride", {
100923
100926
  color: { r: 0, g: 0, b: 0, a: 0 }
100924
100927
  });
100928
+ await page.evaluate((styleId) => {
100929
+ if (document.getElementById(styleId)) return;
100930
+ const style = document.createElement("style");
100931
+ style.id = styleId;
100932
+ style.textContent = "html,body,[data-composition-id]{background:transparent !important;background-color:transparent !important;background-image:none !important;}";
100933
+ document.head.appendChild(style);
100934
+ }, TRANSPARENT_BG_STYLE_ID);
100925
100935
  }
100926
100936
  async function captureAlphaPng(page, width, height) {
100927
100937
  const client = await getCdpSession(page);
@@ -100935,6 +100945,57 @@ async function captureAlphaPng(page, width, height) {
100935
100945
  });
100936
100946
  return Buffer.from(result.data, "base64");
100937
100947
  }
100948
+ var DOM_LAYER_MASK_STYLE_ID = "__hf_dom_layer_mask__";
100949
+ async function applyDomLayerMask(page, showIds, extraHideIds) {
100950
+ await page.evaluate(
100951
+ (args) => {
100952
+ const existing = document.getElementById(args.styleId);
100953
+ if (existing) existing.remove();
100954
+ const showSelectors = [];
100955
+ for (const id of args.show) {
100956
+ const escaped = CSS.escape(id);
100957
+ showSelectors.push(`#${escaped}`, `#${escaped} *`);
100958
+ const renderEscaped = CSS.escape(`__render_frame_${id}__`);
100959
+ showSelectors.push(`#${renderEscaped}`, `#${renderEscaped} *`);
100960
+ }
100961
+ const massHideRule = "body *{visibility:hidden !important;}";
100962
+ const showRule = showSelectors.length === 0 ? "" : `${showSelectors.join(",")}{visibility:visible !important;}`;
100963
+ const style = document.createElement("style");
100964
+ style.id = args.styleId;
100965
+ style.textContent = `${massHideRule}
100966
+ ${showRule}`;
100967
+ document.head.appendChild(style);
100968
+ for (const id of args.hide) {
100969
+ const el = document.getElementById(id);
100970
+ if (el) {
100971
+ el.style.setProperty("visibility", "hidden", "important");
100972
+ }
100973
+ const img = document.getElementById(`__render_frame_${id}__`);
100974
+ if (img) {
100975
+ img.style.setProperty("visibility", "hidden", "important");
100976
+ }
100977
+ }
100978
+ },
100979
+ { show: showIds, hide: extraHideIds, styleId: DOM_LAYER_MASK_STYLE_ID }
100980
+ );
100981
+ }
100982
+ async function removeDomLayerMask(page, extraHideIds) {
100983
+ await page.evaluate(
100984
+ (args) => {
100985
+ const style = document.getElementById(args.styleId);
100986
+ if (style) style.remove();
100987
+ for (const id of args.hide) {
100988
+ const el = document.getElementById(id);
100989
+ if (el) {
100990
+ el.style.removeProperty("visibility");
100991
+ }
100992
+ const img = document.getElementById(`__render_frame_${id}__`);
100993
+ if (img) img.style.removeProperty("visibility");
100994
+ }
100995
+ },
100996
+ { hide: extraHideIds, styleId: DOM_LAYER_MASK_STYLE_ID }
100997
+ );
100998
+ }
100938
100999
  async function injectVideoFramesBatch(page, updates) {
100939
101000
  if (updates.length === 0) return;
100940
101001
  await page.evaluate(
@@ -100975,6 +101036,7 @@ async function injectVideoFramesBatch(page, updates) {
100975
101036
  img.style.objectPosition = computedStyle.objectPosition;
100976
101037
  img.style.zIndex = computedStyle.zIndex;
100977
101038
  for (const property of visualProperties) {
101039
+ if (property === "opacity") continue;
100978
101040
  if (sourceIsStatic && (property === "top" || property === "left" || property === "right" || property === "bottom" || property === "inset")) {
100979
101041
  continue;
100980
101042
  }
@@ -101878,6 +101940,10 @@ function isHdrColorSpace(cs) {
101878
101940
  if (!cs) return false;
101879
101941
  return cs.colorPrimaries.includes("bt2020") || cs.colorSpace.includes("bt2020") || cs.colorTransfer === "smpte2084" || cs.colorTransfer === "arib-std-b67";
101880
101942
  }
101943
+ function detectTransfer(cs) {
101944
+ if (cs?.colorTransfer === "smpte2084") return "pq";
101945
+ return "hlg";
101946
+ }
101881
101947
  var DEFAULT_HDR10_MASTERING = {
101882
101948
  masterDisplay: "G(13250,34500)B(7500,3000)R(34000,16000)WP(15635,16450)L(10000000,1)",
101883
101949
  maxCll: "1000,400"
@@ -102372,10 +102438,10 @@ import { finished } from "stream/promises";
102372
102438
  var downloadPathCache = /* @__PURE__ */ new Map();
102373
102439
  var inFlightDownloads = /* @__PURE__ */ new Map();
102374
102440
  function getFilenameFromUrl(url) {
102375
- const hash = createHash("md5").update(url).digest("hex").slice(0, 12);
102441
+ const hash2 = createHash("md5").update(url).digest("hex").slice(0, 12);
102376
102442
  const urlObj = new URL(url);
102377
102443
  const ext = extname2(urlObj.pathname) || ".mp4";
102378
- return `download_${hash}${ext}`;
102444
+ return `download_${hash2}${ext}`;
102379
102445
  }
102380
102446
  async function downloadToTemp(url, destDir, timeoutMs = 3e5) {
102381
102447
  const cachedPath = downloadPathCache.get(url);
@@ -102876,34 +102942,6 @@ function createVideoFrameInjector(frameLookup, config2) {
102876
102942
  }
102877
102943
  };
102878
102944
  }
102879
- async function hideVideoElements(page, videoIds) {
102880
- if (videoIds.length === 0) return;
102881
- await page.evaluate((ids) => {
102882
- for (const id of ids) {
102883
- const el = document.getElementById(id);
102884
- if (el) {
102885
- el.style.setProperty("visibility", "hidden", "important");
102886
- el.style.setProperty("opacity", "0", "important");
102887
- const img = document.getElementById(`__render_frame_${id}__`);
102888
- if (img) img.style.setProperty("visibility", "hidden", "important");
102889
- }
102890
- }
102891
- }, videoIds);
102892
- }
102893
- async function showVideoElements(page, videoIds) {
102894
- if (videoIds.length === 0) return;
102895
- await page.evaluate((ids) => {
102896
- for (const id of ids) {
102897
- const el = document.getElementById(id);
102898
- if (el) {
102899
- el.style.removeProperty("visibility");
102900
- el.style.removeProperty("opacity");
102901
- const img = document.getElementById(`__render_frame_${id}__`);
102902
- if (img) img.style.removeProperty("visibility");
102903
- }
102904
- }
102905
- }, videoIds);
102906
- }
102907
102945
  async function queryElementStacking(page, nativeHdrVideoIds) {
102908
102946
  const hdrIds = Array.from(nativeHdrVideoIds);
102909
102947
  return page.evaluate((hdrIdList) => {
@@ -102921,14 +102959,103 @@ async function queryElementStacking(page, nativeHdrVideoIds) {
102921
102959
  }
102922
102960
  return 0;
102923
102961
  }
102962
+ function getEffectiveBorderRadius(node) {
102963
+ function resolveRadius(value, el) {
102964
+ if (value.includes("%")) {
102965
+ const pct = parseFloat(value) / 100;
102966
+ const htmlEl = el;
102967
+ const w = htmlEl.offsetWidth || 0;
102968
+ const h = htmlEl.offsetHeight || 0;
102969
+ return pct * Math.min(w, h);
102970
+ }
102971
+ return parseFloat(value) || 0;
102972
+ }
102973
+ const selfCs = window.getComputedStyle(node);
102974
+ const selfRadii = [
102975
+ resolveRadius(selfCs.borderTopLeftRadius, node),
102976
+ resolveRadius(selfCs.borderTopRightRadius, node),
102977
+ resolveRadius(selfCs.borderBottomRightRadius, node),
102978
+ resolveRadius(selfCs.borderBottomLeftRadius, node)
102979
+ ];
102980
+ if (selfRadii[0] > 0 || selfRadii[1] > 0 || selfRadii[2] > 0 || selfRadii[3] > 0) {
102981
+ return selfRadii;
102982
+ }
102983
+ let current = node.parentElement;
102984
+ while (current) {
102985
+ const cs = window.getComputedStyle(current);
102986
+ if (cs.overflow !== "visible") {
102987
+ const tl = resolveRadius(cs.borderTopLeftRadius, current);
102988
+ const tr = resolveRadius(cs.borderTopRightRadius, current);
102989
+ const brr = resolveRadius(cs.borderBottomRightRadius, current);
102990
+ const bl = resolveRadius(cs.borderBottomLeftRadius, current);
102991
+ if (tl > 0 || tr > 0 || brr > 0 || bl > 0) {
102992
+ return [tl, tr, brr, bl];
102993
+ }
102994
+ }
102995
+ current = current.parentElement;
102996
+ }
102997
+ return [0, 0, 0, 0];
102998
+ }
102999
+ function getEffectiveOpacity(node) {
103000
+ let opacity = 1;
103001
+ let current = node;
103002
+ while (current) {
103003
+ const cs = window.getComputedStyle(current);
103004
+ const val = parseFloat(cs.opacity);
103005
+ opacity *= Number.isNaN(val) ? 1 : val;
103006
+ current = current.parentElement;
103007
+ }
103008
+ return opacity;
103009
+ }
103010
+ function getViewportMatrix(node) {
103011
+ const chain = [];
103012
+ let current = node;
103013
+ while (current instanceof HTMLElement) {
103014
+ chain.push(current);
103015
+ const next = current.offsetParent ?? current.parentElement;
103016
+ if (next === current) break;
103017
+ current = next;
103018
+ }
103019
+ let mat = new DOMMatrix();
103020
+ for (let i = chain.length - 1; i >= 0; i--) {
103021
+ const htmlEl = chain[i];
103022
+ if (!htmlEl) continue;
103023
+ mat = mat.translate(htmlEl.offsetLeft, htmlEl.offsetTop);
103024
+ const cs = window.getComputedStyle(htmlEl);
103025
+ if (cs.transform && cs.transform !== "none") {
103026
+ const origin = cs.transformOrigin.split(" ");
103027
+ const ox = resolveLength(origin[0] ?? "0", htmlEl.offsetWidth);
103028
+ const oy = resolveLength(origin[1] ?? "0", htmlEl.offsetHeight);
103029
+ try {
103030
+ const t = new DOMMatrix(cs.transform);
103031
+ if (Number.isFinite(t.a) && Number.isFinite(t.b) && Number.isFinite(t.c) && Number.isFinite(t.d) && Number.isFinite(t.e) && Number.isFinite(t.f)) {
103032
+ mat = mat.translate(ox, oy).multiply(t).translate(-ox, -oy);
103033
+ }
103034
+ } catch {
103035
+ }
103036
+ }
103037
+ }
103038
+ return mat.toString();
103039
+ }
103040
+ function resolveLength(value, basis) {
103041
+ if (value.endsWith("%")) {
103042
+ const pct = parseFloat(value) / 100;
103043
+ return Number.isFinite(pct) ? pct * basis : 0;
103044
+ }
103045
+ const n = parseFloat(value);
103046
+ return Number.isFinite(n) ? n : 0;
103047
+ }
102924
103048
  for (const el of elements) {
102925
103049
  const id = el.id;
102926
103050
  if (!id) continue;
102927
103051
  const rect = el.getBoundingClientRect();
102928
103052
  const style = window.getComputedStyle(el);
102929
103053
  const zIndex = getEffectiveZIndex(el);
102930
- const opacity = parseFloat(style.opacity) || 1;
103054
+ const isHdrEl = hdrSet.has(id);
103055
+ const opacityStartNode = isHdrEl ? el.parentElement : el;
103056
+ const opacity = opacityStartNode ? getEffectiveOpacity(opacityStartNode) : 1;
102931
103057
  const visible = style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0;
103058
+ const htmlEl = el instanceof HTMLElement ? el : null;
102932
103059
  results.push({
102933
103060
  id,
102934
103061
  zIndex,
@@ -102936,9 +103063,16 @@ async function queryElementStacking(page, nativeHdrVideoIds) {
102936
103063
  y: Math.round(rect.y),
102937
103064
  width: Math.round(rect.width),
102938
103065
  height: Math.round(rect.height),
103066
+ layoutWidth: htmlEl?.offsetWidth || Math.round(rect.width),
103067
+ layoutHeight: htmlEl?.offsetHeight || Math.round(rect.height),
102939
103068
  opacity,
102940
103069
  visible,
102941
- isHdr: hdrSet.has(id)
103070
+ isHdr: hdrSet.has(id),
103071
+ // For HDR elements, use the full accumulated viewport matrix so the
103072
+ // affine blit can apply rotation/scale/translate properly. For DOM
103073
+ // elements, the element-level transform is sufficient for reference.
103074
+ transform: isHdrEl ? getViewportMatrix(el) : style.transform || "none",
103075
+ borderRadius: isHdrEl ? getEffectiveBorderRadius(el) : [0, 0, 0, 0]
102942
103076
  });
102943
103077
  }
102944
103078
  return results;
@@ -106320,6 +106454,99 @@ function blitRgb48leRegion(canvas, source2, dx, dy, sw, sh, canvasWidth, canvasH
106320
106454
  }
106321
106455
  }
106322
106456
  }
106457
+ function blitRgb48leAffine(canvas, source2, matrix, srcW, srcH, canvasW, canvasH, opacity, borderRadius) {
106458
+ const a = matrix[0];
106459
+ const b = matrix[1];
106460
+ const c = matrix[2];
106461
+ const d = matrix[3];
106462
+ const tx = matrix[4];
106463
+ const ty = matrix[5];
106464
+ if (a === void 0 || b === void 0 || c === void 0 || d === void 0 || tx === void 0 || ty === void 0)
106465
+ return;
106466
+ const det = a * d - b * c;
106467
+ if (Math.abs(det) < 1e-10) return;
106468
+ const invA = d / det;
106469
+ const invB = -b / det;
106470
+ const invC = -c / det;
106471
+ const invD = a / det;
106472
+ const invTx = -(invA * tx + invC * ty);
106473
+ const invTy = -(invB * tx + invD * ty);
106474
+ const op = opacity ?? 1;
106475
+ const hasMask = borderRadius !== void 0;
106476
+ const corners = [
106477
+ [tx, ty],
106478
+ [a * srcW + tx, b * srcW + ty],
106479
+ [c * srcH + tx, d * srcH + ty],
106480
+ [a * srcW + c * srcH + tx, b * srcW + d * srcH + ty]
106481
+ ];
106482
+ let minX = canvasW, maxX = 0, minY = canvasH, maxY = 0;
106483
+ for (const corner of corners) {
106484
+ const cx = corner[0] ?? 0;
106485
+ const cy = corner[1] ?? 0;
106486
+ if (cx < minX) minX = cx;
106487
+ if (cx > maxX) maxX = cx;
106488
+ if (cy < minY) minY = cy;
106489
+ if (cy > maxY) maxY = cy;
106490
+ }
106491
+ const startX = Math.max(0, Math.floor(minX));
106492
+ const endX = Math.min(canvasW, Math.ceil(maxX));
106493
+ const startY = Math.max(0, Math.floor(minY));
106494
+ const endY = Math.min(canvasH, Math.ceil(maxY));
106495
+ for (let dy = startY; dy < endY; dy++) {
106496
+ for (let dx = startX; dx < endX; dx++) {
106497
+ const sx = invA * dx + invC * dy + invTx;
106498
+ const sy = invB * dx + invD * dy + invTy;
106499
+ if (sx < 0 || sy < 0 || sx >= srcW || sy >= srcH) continue;
106500
+ let effectiveOp = op;
106501
+ if (hasMask) {
106502
+ const ma = roundedRectAlpha(sx, sy, srcW, srcH, borderRadius);
106503
+ if (ma <= 0) continue;
106504
+ effectiveOp *= ma;
106505
+ }
106506
+ const x0 = Math.floor(sx);
106507
+ const y0 = Math.floor(sy);
106508
+ const fx = sx - x0;
106509
+ const fy = sy - y0;
106510
+ const x1 = Math.min(x0 + 1, srcW - 1);
106511
+ const y1 = Math.min(y0 + 1, srcH - 1);
106512
+ const off00 = (y0 * srcW + x0) * 6;
106513
+ const off10 = (y0 * srcW + x1) * 6;
106514
+ const off01 = (y1 * srcW + x0) * 6;
106515
+ const off11 = (y1 * srcW + x1) * 6;
106516
+ const w00 = (1 - fx) * (1 - fy);
106517
+ const w10 = fx * (1 - fy);
106518
+ const w01 = (1 - fx) * fy;
106519
+ const w11 = fx * fy;
106520
+ const sr = source2.readUInt16LE(off00) * w00 + source2.readUInt16LE(off10) * w10 + source2.readUInt16LE(off01) * w01 + source2.readUInt16LE(off11) * w11;
106521
+ const sg = source2.readUInt16LE(off00 + 2) * w00 + source2.readUInt16LE(off10 + 2) * w10 + source2.readUInt16LE(off01 + 2) * w01 + source2.readUInt16LE(off11 + 2) * w11;
106522
+ const sb = source2.readUInt16LE(off00 + 4) * w00 + source2.readUInt16LE(off10 + 4) * w10 + source2.readUInt16LE(off01 + 4) * w01 + source2.readUInt16LE(off11 + 4) * w11;
106523
+ const dstOff = (dy * canvasW + dx) * 6;
106524
+ if (effectiveOp >= 0.999) {
106525
+ canvas.writeUInt16LE(Math.round(sr), dstOff);
106526
+ canvas.writeUInt16LE(Math.round(sg), dstOff + 2);
106527
+ canvas.writeUInt16LE(Math.round(sb), dstOff + 4);
106528
+ } else {
106529
+ const invEff = 1 - effectiveOp;
106530
+ const dr = canvas.readUInt16LE(dstOff);
106531
+ const dg = canvas.readUInt16LE(dstOff + 2);
106532
+ const db = canvas.readUInt16LE(dstOff + 4);
106533
+ canvas.writeUInt16LE(Math.round(sr * effectiveOp + dr * invEff), dstOff);
106534
+ canvas.writeUInt16LE(Math.round(sg * effectiveOp + dg * invEff), dstOff + 2);
106535
+ canvas.writeUInt16LE(Math.round(sb * effectiveOp + db * invEff), dstOff + 4);
106536
+ }
106537
+ }
106538
+ }
106539
+ }
106540
+ function parseTransformMatrix(css) {
106541
+ if (!css || css === "none") return null;
106542
+ const match2 = css.match(
106543
+ /^matrix\(\s*([^,]+),\s*([^,]+),\s*([^,]+),\s*([^,]+),\s*([^,]+),\s*([^,)]+)\s*\)$/
106544
+ );
106545
+ if (!match2) return null;
106546
+ const values = match2.slice(1, 7).map(Number);
106547
+ if (!values.every(Number.isFinite)) return null;
106548
+ return values;
106549
+ }
106323
106550
 
106324
106551
  // ../engine/src/utils/layerCompositor.ts
106325
106552
  function groupIntoLayers(elements) {
@@ -106340,6 +106567,637 @@ function groupIntoLayers(elements) {
106340
106567
  return layers;
106341
106568
  }
106342
106569
 
106570
+ // ../engine/src/utils/shaderTransitions.ts
106571
+ var PQ_M1 = 0.1593017578125;
106572
+ var PQ_M2 = 78.84375;
106573
+ var PQ_C1 = 0.8359375;
106574
+ var PQ_C2 = 18.8515625;
106575
+ var PQ_C3 = 18.6875;
106576
+ function pqEotf(signal) {
106577
+ const sp = Math.pow(Math.max(0, signal), 1 / PQ_M2);
106578
+ const num = Math.max(sp - PQ_C1, 0);
106579
+ const den = PQ_C2 - PQ_C3 * sp;
106580
+ return den > 0 ? Math.pow(num / den, 1 / PQ_M1) : 0;
106581
+ }
106582
+ function pqOetf(linear) {
106583
+ const lp = Math.pow(Math.max(0, linear), PQ_M1);
106584
+ return Math.pow((PQ_C1 + PQ_C2 * lp) / (1 + PQ_C3 * lp), PQ_M2);
106585
+ }
106586
+ function hlgEotf(signal) {
106587
+ const a = 0.17883277;
106588
+ const b = 1 - 4 * a;
106589
+ const c = 0.5 - a * Math.log(4 * a);
106590
+ if (signal <= 0.5) {
106591
+ return signal * signal / 3;
106592
+ }
106593
+ return (Math.exp((signal - c) / a) + b) / 12;
106594
+ }
106595
+ function hlgOetf(linear) {
106596
+ const a = 0.17883277;
106597
+ const b = 1 - 4 * a;
106598
+ const c = 0.5 - a * Math.log(4 * a);
106599
+ if (linear <= 1 / 12) {
106600
+ return Math.sqrt(3 * linear);
106601
+ }
106602
+ return a * Math.log(12 * linear - b) + c;
106603
+ }
106604
+ function buildLut(fn) {
106605
+ const lut = new Uint16Array(65536);
106606
+ for (let i = 0; i < 65536; i++) {
106607
+ lut[i] = Math.round(fn(i / 65535) * 65535);
106608
+ }
106609
+ return lut;
106610
+ }
106611
+ var HLG_OOTF_LW = 1e3;
106612
+ var HLG_OOTF_GAMMA = 1.2 * Math.pow(1.111, Math.log2(HLG_OOTF_LW / 1e3));
106613
+ function hlgSceneToPqDisplay(sceneLinear) {
106614
+ const displayNits = HLG_OOTF_LW * Math.pow(Math.max(0, sceneLinear), HLG_OOTF_GAMMA);
106615
+ return displayNits / 1e4;
106616
+ }
106617
+ function pqDisplayToHlgScene(displayNormalized) {
106618
+ const displayNits = displayNormalized * 1e4;
106619
+ return Math.pow(Math.max(0, displayNits / HLG_OOTF_LW), 1 / HLG_OOTF_GAMMA);
106620
+ }
106621
+ var hlgToPqLut = null;
106622
+ var pqToHlgLut = null;
106623
+ function getHlgToPqLut() {
106624
+ if (!hlgToPqLut) hlgToPqLut = buildLut((v) => pqOetf(hlgSceneToPqDisplay(hlgEotf(v))));
106625
+ return hlgToPqLut;
106626
+ }
106627
+ function getPqToHlgLut() {
106628
+ if (!pqToHlgLut) pqToHlgLut = buildLut((v) => hlgOetf(pqDisplayToHlgScene(pqEotf(v))));
106629
+ return pqToHlgLut;
106630
+ }
106631
+ function convertTransfer(buf, from2, to) {
106632
+ if (from2 === to) return;
106633
+ const lut = from2 === "hlg" ? getHlgToPqLut() : getPqToHlgLut();
106634
+ const len = buf.length / 2;
106635
+ for (let i = 0; i < len; i++) {
106636
+ const off = i * 2;
106637
+ buf.writeUInt16LE(lut[buf.readUInt16LE(off)] ?? 0, off);
106638
+ }
106639
+ }
106640
+ function sampleRgb48le(buf, u, v, w, h) {
106641
+ const uc = Math.max(0, Math.min(1, u));
106642
+ const vc = Math.max(0, Math.min(1, v));
106643
+ const sx = uc * (w - 1);
106644
+ const sy = vc * (h - 1);
106645
+ const x0 = Math.floor(sx);
106646
+ const y0 = Math.floor(sy);
106647
+ const x1 = Math.min(x0 + 1, w - 1);
106648
+ const y1 = Math.min(y0 + 1, h - 1);
106649
+ const fx = sx - x0;
106650
+ const fy = sy - y0;
106651
+ const w00 = (1 - fx) * (1 - fy);
106652
+ const w10 = fx * (1 - fy);
106653
+ const w01 = (1 - fx) * fy;
106654
+ const w11 = fx * fy;
106655
+ const off00 = (y0 * w + x0) * 6;
106656
+ const off10 = (y0 * w + x1) * 6;
106657
+ const off01 = (y1 * w + x0) * 6;
106658
+ const off11 = (y1 * w + x1) * 6;
106659
+ const r = Math.round(
106660
+ buf.readUInt16LE(off00) * w00 + buf.readUInt16LE(off10) * w10 + buf.readUInt16LE(off01) * w01 + buf.readUInt16LE(off11) * w11
106661
+ );
106662
+ const g = Math.round(
106663
+ buf.readUInt16LE(off00 + 2) * w00 + buf.readUInt16LE(off10 + 2) * w10 + buf.readUInt16LE(off01 + 2) * w01 + buf.readUInt16LE(off11 + 2) * w11
106664
+ );
106665
+ const b = Math.round(
106666
+ buf.readUInt16LE(off00 + 4) * w00 + buf.readUInt16LE(off10 + 4) * w10 + buf.readUInt16LE(off01 + 4) * w01 + buf.readUInt16LE(off11 + 4) * w11
106667
+ );
106668
+ return [r, g, b];
106669
+ }
106670
+ function mix16(a, b, t) {
106671
+ return Math.round(a * (1 - t) + b * t);
106672
+ }
106673
+ function clamp16(v) {
106674
+ return Math.max(0, Math.min(65535, v));
106675
+ }
106676
+ function smoothstep(edge0, edge1, x) {
106677
+ const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
106678
+ return t * t * (3 - 2 * t);
106679
+ }
106680
+ function hash(x, y) {
106681
+ return (Math.sin(x * 127.1 + y * 311.7) * 43758.5453 % 1 + 1) % 1;
106682
+ }
106683
+ function vnoise(px, py) {
106684
+ const ix = Math.floor(px);
106685
+ const iy = Math.floor(py);
106686
+ let fx = px - ix;
106687
+ let fy = py - iy;
106688
+ fx = fx * fx * fx * (fx * (fx * 6 - 15) + 10);
106689
+ fy = fy * fy * fy * (fy * (fy * 6 - 15) + 10);
106690
+ const h00 = hash(ix, iy);
106691
+ const h10 = hash(ix + 1, iy);
106692
+ const h01 = hash(ix, iy + 1);
106693
+ const h11 = hash(ix + 1, iy + 1);
106694
+ return h00 * (1 - fx) * (1 - fy) + h10 * fx * (1 - fy) + h01 * (1 - fx) * fy + h11 * fx * fy;
106695
+ }
106696
+ var ROT_A = 0.8;
106697
+ var ROT_B = 0.6;
106698
+ function fbm(px, py) {
106699
+ let value = 0;
106700
+ let amplitude = 0.5;
106701
+ let x = px;
106702
+ let y = py;
106703
+ for (let i = 0; i < 5; i++) {
106704
+ value += amplitude * vnoise(x, y);
106705
+ const nx = ROT_A * x - ROT_B * y;
106706
+ const ny = ROT_B * x + ROT_A * y;
106707
+ x = nx * 2.02;
106708
+ y = ny * 2.02;
106709
+ amplitude *= 0.5;
106710
+ }
106711
+ return value;
106712
+ }
106713
+ var TRANSITIONS = {};
106714
+ var crossfade = (from2, to, out, w, h, p) => {
106715
+ const inv = 1 - p;
106716
+ for (let i = 0; i < w * h; i++) {
106717
+ const o = i * 6;
106718
+ out.writeUInt16LE(Math.round(from2.readUInt16LE(o) * inv + to.readUInt16LE(o) * p), o);
106719
+ out.writeUInt16LE(
106720
+ Math.round(from2.readUInt16LE(o + 2) * inv + to.readUInt16LE(o + 2) * p),
106721
+ o + 2
106722
+ );
106723
+ out.writeUInt16LE(
106724
+ Math.round(from2.readUInt16LE(o + 4) * inv + to.readUInt16LE(o + 4) * p),
106725
+ o + 4
106726
+ );
106727
+ }
106728
+ };
106729
+ TRANSITIONS["crossfade"] = crossfade;
106730
+ var flashThroughWhite = (from2, to, out, w, h, p) => {
106731
+ const toWhite = smoothstep(0, 0.45, p);
106732
+ const fromWhite = 1 - smoothstep(0.5, 1, p);
106733
+ const blend = smoothstep(0.35, 0.65, p);
106734
+ for (let i = 0; i < w * h; i++) {
106735
+ const o = i * 6;
106736
+ const fromR = mix16(from2.readUInt16LE(o), 65535, toWhite);
106737
+ const fromG = mix16(from2.readUInt16LE(o + 2), 65535, toWhite);
106738
+ const fromB = mix16(from2.readUInt16LE(o + 4), 65535, toWhite);
106739
+ const toR = mix16(to.readUInt16LE(o), 65535, fromWhite);
106740
+ const toG = mix16(to.readUInt16LE(o + 2), 65535, fromWhite);
106741
+ const toB = mix16(to.readUInt16LE(o + 4), 65535, fromWhite);
106742
+ out.writeUInt16LE(mix16(fromR, toR, blend), o);
106743
+ out.writeUInt16LE(mix16(fromG, toG, blend), o + 2);
106744
+ out.writeUInt16LE(mix16(fromB, toB, blend), o + 4);
106745
+ }
106746
+ };
106747
+ TRANSITIONS["flash-through-white"] = flashThroughWhite;
106748
+ var chromaticSplit = (from2, to, out, w, h, p) => {
106749
+ for (let i = 0; i < w * h; i++) {
106750
+ const ux = i % w / w;
106751
+ const uy = Math.floor(i / w) / h;
106752
+ const o = i * 6;
106753
+ const cx = ux - 0.5;
106754
+ const cy = uy - 0.5;
106755
+ const fromShift = p * 0.06;
106756
+ const fr = sampleRgb48le(from2, ux + cx * fromShift, uy + cy * fromShift, w, h)[0];
106757
+ const fg = sampleRgb48le(from2, ux, uy, w, h)[1];
106758
+ const fb = sampleRgb48le(from2, ux - cx * fromShift, uy - cy * fromShift, w, h)[2];
106759
+ const toShift = (1 - p) * 0.06;
106760
+ const tr = sampleRgb48le(to, ux - cx * toShift, uy - cy * toShift, w, h)[0];
106761
+ const tg = sampleRgb48le(to, ux, uy, w, h)[1];
106762
+ const tb = sampleRgb48le(to, ux + cx * toShift, uy + cy * toShift, w, h)[2];
106763
+ out.writeUInt16LE(clamp16(mix16(fr, tr, p)), o);
106764
+ out.writeUInt16LE(clamp16(mix16(fg, tg, p)), o + 2);
106765
+ out.writeUInt16LE(clamp16(mix16(fb, tb, p)), o + 4);
106766
+ }
106767
+ };
106768
+ TRANSITIONS["chromatic-split"] = chromaticSplit;
106769
+ var sdfIris = (from2, to, out, w, h, p) => {
106770
+ const accentBright = [65535, 55e3, 35e3];
106771
+ for (let i = 0; i < w * h; i++) {
106772
+ const ux = i % w / w;
106773
+ const uy = Math.floor(i / w) / h;
106774
+ const o = i * 6;
106775
+ const ax = (ux - 0.5) * (w / h);
106776
+ const ay = uy - 0.5;
106777
+ const d = Math.sqrt(ax * ax + ay * ay);
106778
+ const radius = p * 1.2;
106779
+ const fw = 3e-3;
106780
+ const edge = smoothstep(radius + fw, radius - fw, d);
106781
+ const ring1 = Math.exp(-Math.abs(d - radius) * 25);
106782
+ const ring2 = Math.exp(-Math.abs(d - radius + 0.04) * 20) * 0.5;
106783
+ const ring3 = Math.exp(-Math.abs(d - radius + 0.08) * 15) * 0.25;
106784
+ const glow = (ring1 + ring2 + ring3) * p * (1 - p) * 4;
106785
+ const [fromR, fromG, fromB] = sampleRgb48le(from2, ux, uy, w, h);
106786
+ const [toR, toG, toB] = sampleRgb48le(to, ux, uy, w, h);
106787
+ out.writeUInt16LE(clamp16(mix16(fromR, toR, edge) + accentBright[0] * glow * 0.6), o);
106788
+ out.writeUInt16LE(clamp16(mix16(fromG, toG, edge) + accentBright[1] * glow * 0.6), o + 2);
106789
+ out.writeUInt16LE(clamp16(mix16(fromB, toB, edge) + accentBright[2] * glow * 0.6), o + 4);
106790
+ }
106791
+ };
106792
+ TRANSITIONS["sdf-iris"] = sdfIris;
106793
+ function glitchRand(x, y) {
106794
+ return (Math.sin(x * 12.9898 + y * 78.233) * 43758.5453 % 1 + 1) % 1;
106795
+ }
106796
+ var glitch = (from2, to, out, w, h, p) => {
106797
+ const intensity = p * (1 - p) * 4;
106798
+ for (let i = 0; i < w * h; i++) {
106799
+ const ux = i % w / w;
106800
+ const uy = Math.floor(i / w) / h;
106801
+ const o = i * 6;
106802
+ const lineY = Math.floor(uy * 60) / 60;
106803
+ const lineDisp = (glitchRand(lineY, Math.floor(p * 17)) - 0.5) * 0.18 * intensity;
106804
+ const blockX = Math.floor(ux * 12);
106805
+ const blockY = Math.floor(uy * 8);
106806
+ const progressStep = Math.floor(p * 11);
106807
+ const br = glitchRand(blockX + progressStep, blockY + progressStep);
106808
+ const ba = (br >= 0.83 ? 1 : 0) * intensity;
106809
+ const bdx = (glitchRand(blockX * 2.1, blockY * 2.1) - 0.5) * 0.35 * ba;
106810
+ const bdy = (glitchRand(blockX * 3.7, blockY * 3.7) - 0.5) * 0.35 * ba;
106811
+ const uvx = Math.max(0, Math.min(1, ux + lineDisp + bdx));
106812
+ const uvy = Math.max(0, Math.min(1, uy + bdy));
106813
+ const shift = intensity * 0.035;
106814
+ const r = sampleRgb48le(from2, uvx + shift, uvy, w, h)[0];
106815
+ const g = sampleRgb48le(from2, uvx, uvy, w, h)[1];
106816
+ const b = sampleRgb48le(from2, uvx - shift, uvy, w, h)[2];
106817
+ let cr = r / 65535;
106818
+ let cg = g / 65535;
106819
+ let cb = b / 65535;
106820
+ const scanline = (uy * h * 0.5 % 1 + 1) % 1 >= 0.5 ? 0.05 * intensity : 0;
106821
+ cr -= scanline;
106822
+ cg -= scanline;
106823
+ cb -= scanline;
106824
+ const flicker = 1 + (glitchRand(Math.floor(p * 23), 0) - 0.5) * 0.3 * intensity;
106825
+ cr *= flicker;
106826
+ cg *= flicker;
106827
+ cb *= flicker;
106828
+ const levels = 256 - (256 - 8) * (intensity * 0.5);
106829
+ cr = Math.floor(cr * levels) / levels;
106830
+ cg = Math.floor(cg * levels) / levels;
106831
+ cb = Math.floor(cb * levels) / levels;
106832
+ const [toR, toG, toB] = sampleRgb48le(to, ux, uy, w, h);
106833
+ out.writeUInt16LE(clamp16(mix16(Math.round(cr * 65535), toR, p)), o);
106834
+ out.writeUInt16LE(clamp16(mix16(Math.round(cg * 65535), toG, p)), o + 2);
106835
+ out.writeUInt16LE(clamp16(mix16(Math.round(cb * 65535), toB, p)), o + 4);
106836
+ }
106837
+ };
106838
+ TRANSITIONS["glitch"] = glitch;
106839
+ function aces(x) {
106840
+ return Math.max(0, Math.min(1, x * (2.51 * x + 0.03) / (x * (2.43 * x + 0.59) + 0.14)));
106841
+ }
106842
+ var lightLeak = (from2, to, out, w, h, p) => {
106843
+ const accent = [5e4 / 65535, 25e3 / 65535, 5e3 / 65535];
106844
+ const accentBright = [65535 / 65535, 55e3 / 65535, 35e3 / 65535];
106845
+ const lpx = 1.3;
106846
+ const lpy = -0.2;
106847
+ for (let i = 0; i < w * h; i++) {
106848
+ const ux = i % w / w;
106849
+ const uy = Math.floor(i / w) / h;
106850
+ const o = i * 6;
106851
+ const dx = ux - lpx;
106852
+ const dy = uy - lpy;
106853
+ const dist = Math.sqrt(dx * dx + dy * dy);
106854
+ const leak = Math.max(0, Math.min(1, Math.exp(-dist * 1.8) * p * 4));
106855
+ const warmR = accent[0] + (accentBright[0] - accent[0]) * dist * 0.7;
106856
+ const warmG = accent[1] + (accentBright[1] - accent[1]) * dist * 0.7;
106857
+ const warmB = accent[2] + (accentBright[2] - accent[2]) * dist * 0.7;
106858
+ const flare = Math.exp(-Math.abs(uy - (-0.2 + ux * 0.3)) * 15) * leak * 0.3;
106859
+ const [fr, fg, fb] = sampleRgb48le(from2, ux, uy, w, h);
106860
+ const fromR = fr / 65535;
106861
+ const fromG = fg / 65535;
106862
+ const fromB = fb / 65535;
106863
+ const overR = aces(fromR + warmR * leak * 3 + accentBright[0] * flare);
106864
+ const overG = aces(fromG + warmG * leak * 3 + accentBright[1] * flare);
106865
+ const overB = aces(fromB + warmB * leak * 3 + accentBright[2] * flare);
106866
+ const [toR, toG, toB] = sampleRgb48le(to, ux, uy, w, h);
106867
+ const blend = smoothstep(0.15, 0.85, p);
106868
+ out.writeUInt16LE(clamp16(mix16(Math.round(overR * 65535), toR, blend)), o);
106869
+ out.writeUInt16LE(clamp16(mix16(Math.round(overG * 65535), toG, blend)), o + 2);
106870
+ out.writeUInt16LE(clamp16(mix16(Math.round(overB * 65535), toB, blend)), o + 4);
106871
+ }
106872
+ };
106873
+ TRANSITIONS["light-leak"] = lightLeak;
106874
+ var crossWarpMorph = (from2, to, out, w, h, p) => {
106875
+ for (let i = 0; i < w * h; i++) {
106876
+ const ux = i % w / w;
106877
+ const uy = Math.floor(i / w) / h;
106878
+ const o = i * 6;
106879
+ const dispX = fbm(ux * 3, uy * 3) - 0.5;
106880
+ const dispY = fbm(ux * 3 + 7.3, uy * 3 + 3.7) - 0.5;
106881
+ const fromUx = Math.max(0, Math.min(1, ux + dispX * p * 0.5));
106882
+ const fromUy = Math.max(0, Math.min(1, uy + dispY * p * 0.5));
106883
+ const toUx = Math.max(0, Math.min(1, ux - dispX * (1 - p) * 0.5));
106884
+ const toUy = Math.max(0, Math.min(1, uy - dispY * (1 - p) * 0.5));
106885
+ const [fromR, fromG, fromB] = sampleRgb48le(from2, fromUx, fromUy, w, h);
106886
+ const [toR, toG, toB] = sampleRgb48le(to, toUx, toUy, w, h);
106887
+ const n = fbm(ux * 4 + 3.1, uy * 4 + 1.7);
106888
+ const blend = smoothstep(0.4, 0.6, n + p * 1.2 - 0.6);
106889
+ out.writeUInt16LE(clamp16(mix16(fromR, toR, blend)), o);
106890
+ out.writeUInt16LE(clamp16(mix16(fromG, toG, blend)), o + 2);
106891
+ out.writeUInt16LE(clamp16(mix16(fromB, toB, blend)), o + 4);
106892
+ }
106893
+ };
106894
+ TRANSITIONS["cross-warp-morph"] = crossWarpMorph;
106895
+ var whipPan = (from2, to, out, w, h, p) => {
106896
+ const fromOff = p * 1.5;
106897
+ const toOff = (1 - p) * 1.5;
106898
+ for (let i = 0; i < w * h; i++) {
106899
+ const ux = i % w / w;
106900
+ const uy = Math.floor(i / w) / h;
106901
+ const o = i * 6;
106902
+ let fromR = 0, fromG = 0, fromB = 0;
106903
+ for (let s = 0; s < 10; s++) {
106904
+ const f = s / 10;
106905
+ const fuv = Math.max(0, Math.min(1, ux + fromOff + p * 0.08 * f));
106906
+ const [r, g, b] = sampleRgb48le(from2, fuv, uy, w, h);
106907
+ fromR += r;
106908
+ fromG += g;
106909
+ fromB += b;
106910
+ }
106911
+ fromR /= 10;
106912
+ fromG /= 10;
106913
+ fromB /= 10;
106914
+ let toR = 0, toG = 0, toB = 0;
106915
+ for (let s = 0; s < 10; s++) {
106916
+ const f = s / 10;
106917
+ const tuv = Math.max(0, Math.min(1, ux - toOff - (1 - p) * 0.08 * f));
106918
+ const [r, g, b] = sampleRgb48le(to, tuv, uy, w, h);
106919
+ toR += r;
106920
+ toG += g;
106921
+ toB += b;
106922
+ }
106923
+ toR /= 10;
106924
+ toG /= 10;
106925
+ toB /= 10;
106926
+ out.writeUInt16LE(clamp16(mix16(Math.round(fromR), Math.round(toR), p)), o);
106927
+ out.writeUInt16LE(clamp16(mix16(Math.round(fromG), Math.round(toG), p)), o + 2);
106928
+ out.writeUInt16LE(clamp16(mix16(Math.round(fromB), Math.round(toB), p)), o + 4);
106929
+ }
106930
+ };
106931
+ TRANSITIONS["whip-pan"] = whipPan;
106932
+ var cinematicZoom = (from2, to, out, w, h, p) => {
106933
+ const fromS = p * 0.08;
106934
+ const toS = (1 - p) * 0.06;
106935
+ for (let i = 0; i < w * h; i++) {
106936
+ const ux = i % w / w;
106937
+ const uy = Math.floor(i / w) / h;
106938
+ const o = i * 6;
106939
+ const dx = ux - 0.5;
106940
+ const dy = uy - 0.5;
106941
+ let fr = 0, fg = 0, fb = 0;
106942
+ for (let s = 0; s < 12; s++) {
106943
+ const f = s / 12;
106944
+ const rr = sampleRgb48le(
106945
+ from2,
106946
+ ux - dx * fromS * 1.06 * f,
106947
+ uy - dy * fromS * 1.06 * f,
106948
+ w,
106949
+ h
106950
+ )[0];
106951
+ const gg = sampleRgb48le(from2, ux - dx * fromS * f, uy - dy * fromS * f, w, h)[1];
106952
+ const bb = sampleRgb48le(
106953
+ from2,
106954
+ ux - dx * fromS * 0.94 * f,
106955
+ uy - dy * fromS * 0.94 * f,
106956
+ w,
106957
+ h
106958
+ )[2];
106959
+ fr += rr;
106960
+ fg += gg;
106961
+ fb += bb;
106962
+ }
106963
+ fr /= 12;
106964
+ fg /= 12;
106965
+ fb /= 12;
106966
+ let tr = 0, tg = 0, tb = 0;
106967
+ for (let s = 0; s < 12; s++) {
106968
+ const f = s / 12;
106969
+ const rr = sampleRgb48le(to, ux + dx * toS * 1.06 * f, uy + dy * toS * 1.06 * f, w, h)[0];
106970
+ const gg = sampleRgb48le(to, ux + dx * toS * f, uy + dy * toS * f, w, h)[1];
106971
+ const bb = sampleRgb48le(to, ux + dx * toS * 0.94 * f, uy + dy * toS * 0.94 * f, w, h)[2];
106972
+ tr += rr;
106973
+ tg += gg;
106974
+ tb += bb;
106975
+ }
106976
+ tr /= 12;
106977
+ tg /= 12;
106978
+ tb /= 12;
106979
+ out.writeUInt16LE(clamp16(mix16(Math.round(fr), Math.round(tr), p)), o);
106980
+ out.writeUInt16LE(clamp16(mix16(Math.round(fg), Math.round(tg), p)), o + 2);
106981
+ out.writeUInt16LE(clamp16(mix16(Math.round(fb), Math.round(tb), p)), o + 4);
106982
+ }
106983
+ };
106984
+ TRANSITIONS["cinematic-zoom"] = cinematicZoom;
106985
+ var gravitationalLens = (from2, to, out, w, h, p) => {
106986
+ for (let i = 0; i < w * h; i++) {
106987
+ const ux = i % w / w;
106988
+ const uy = Math.floor(i / w) / h;
106989
+ const o = i * 6;
106990
+ const uvx = ux - 0.5;
106991
+ const uvy = uy - 0.5;
106992
+ const dist = Math.sqrt(uvx * uvx + uvy * uvy);
106993
+ const pull = p * 2;
106994
+ const warpStr = pull * 0.3 / (dist + 0.1);
106995
+ const warpedX = Math.max(0, Math.min(1, ux - uvx * warpStr));
106996
+ const warpedY = Math.max(0, Math.min(1, uy - uvy * warpStr));
106997
+ const [, ag] = sampleRgb48le(from2, warpedX, warpedY, w, h);
106998
+ const horizon = smoothstep(0, 0.3, dist / (1 - p * 0.85 + 1e-3));
106999
+ const shift = pull * 0.02 / (dist + 0.2);
107000
+ const rSampX = Math.max(0, Math.min(1, ux - uvx * (warpStr + shift)));
107001
+ const rSampY = Math.max(0, Math.min(1, uy - uvy * (warpStr + shift)));
107002
+ const bSampX = Math.max(0, Math.min(1, ux - uvx * (warpStr - shift)));
107003
+ const bSampY = Math.max(0, Math.min(1, uy - uvy * (warpStr - shift)));
107004
+ const ar = sampleRgb48le(from2, rSampX, rSampY, w, h)[0];
107005
+ const ab = sampleRgb48le(from2, bSampX, bSampY, w, h)[2];
107006
+ const lensedR = Math.round(ar * horizon);
107007
+ const lensedG = Math.round(ag * horizon);
107008
+ const lensedB = Math.round(ab * horizon);
107009
+ const [toR, toG, toB] = sampleRgb48le(to, ux, uy, w, h);
107010
+ const blend = smoothstep(0.3, 0.9, p);
107011
+ out.writeUInt16LE(clamp16(mix16(lensedR, toR, blend)), o);
107012
+ out.writeUInt16LE(clamp16(mix16(lensedG, toG, blend)), o + 2);
107013
+ out.writeUInt16LE(clamp16(mix16(lensedB, toB, blend)), o + 4);
107014
+ }
107015
+ };
107016
+ TRANSITIONS["gravitational-lens"] = gravitationalLens;
107017
+ var rippleWaves = (from2, to, out, w, h, p) => {
107018
+ const accentBright = [65535, 55e3, 35e3];
107019
+ for (let i = 0; i < w * h; i++) {
107020
+ const ux = i % w / w;
107021
+ const uy = Math.floor(i / w) / h;
107022
+ const o = i * 6;
107023
+ const uvx = ux - 0.5;
107024
+ const uvy = uy - 0.5;
107025
+ const dist = Math.sqrt(uvx * uvx + uvy * uvy);
107026
+ const nux = uvx + 1e-3;
107027
+ const nuy = uvy + 1e-3;
107028
+ const nlen = Math.sqrt(nux * nux + nuy * nuy);
107029
+ const dirx = nux / nlen;
107030
+ const diry = nuy / nlen;
107031
+ const fromAmp = p * 0.04;
107032
+ const fw1 = Math.exp(Math.sin(dist * 25 - p * 12) - 1);
107033
+ const fw2 = Math.exp(Math.sin(dist * 50 - p * 18) - 1) * 0.5;
107034
+ const fromUx = Math.max(0, Math.min(1, ux + dirx * (fw1 + fw2) * fromAmp));
107035
+ const fromUy = Math.max(0, Math.min(1, uy + diry * (fw1 + fw2) * fromAmp));
107036
+ const toAmp = (1 - p) * 0.04;
107037
+ const tw1 = Math.exp(Math.sin(dist * 25 + p * 12) - 1);
107038
+ const tw2 = Math.exp(Math.sin(dist * 50 + p * 18) - 1) * 0.5;
107039
+ const toUx = Math.max(0, Math.min(1, ux - dirx * (tw1 + tw2) * toAmp));
107040
+ const toUy = Math.max(0, Math.min(1, uy - diry * (tw1 + tw2) * toAmp));
107041
+ const [fromR, fromG, fromB] = sampleRgb48le(from2, fromUx, fromUy, w, h);
107042
+ const [toR, toG, toB] = sampleRgb48le(to, toUx, toUy, w, h);
107043
+ const peak = fw1 * p;
107044
+ const tintR = accentBright[0] * peak * 0.1;
107045
+ const tintG = accentBright[1] * peak * 0.1;
107046
+ const tintB = accentBright[2] * peak * 0.1;
107047
+ out.writeUInt16LE(clamp16(mix16(Math.round(fromR + tintR), toR, p)), o);
107048
+ out.writeUInt16LE(clamp16(mix16(Math.round(fromG + tintG), toG, p)), o + 2);
107049
+ out.writeUInt16LE(clamp16(mix16(Math.round(fromB + tintB), toB, p)), o + 4);
107050
+ }
107051
+ };
107052
+ TRANSITIONS["ripple-waves"] = rippleWaves;
107053
+ var swirlVortex = (from2, to, out, w, h, p) => {
107054
+ for (let i = 0; i < w * h; i++) {
107055
+ const ux = i % w / w;
107056
+ const uy = Math.floor(i / w) / h;
107057
+ const o = i * 6;
107058
+ const uvx = ux - 0.5;
107059
+ const uvy = uy - 0.5;
107060
+ const dist = Math.sqrt(uvx * uvx + uvy * uvy);
107061
+ const warp = fbm(ux * 4, uy * 4) * 0.5;
107062
+ const fromAng = p * (1 - dist) * 10 + warp * p * 3;
107063
+ const fs8 = Math.sin(fromAng);
107064
+ const fc = Math.cos(fromAng);
107065
+ const fromUx = Math.max(0, Math.min(1, uvx * fc - uvy * fs8 + 0.5));
107066
+ const fromUy = Math.max(0, Math.min(1, uvx * fs8 + uvy * fc + 0.5));
107067
+ const toAng = -(1 - p) * (1 - dist) * 10 - warp * (1 - p) * 3;
107068
+ const ts = Math.sin(toAng);
107069
+ const tc = Math.cos(toAng);
107070
+ const toUx = Math.max(0, Math.min(1, uvx * tc - uvy * ts + 0.5));
107071
+ const toUy = Math.max(0, Math.min(1, uvx * ts + uvy * tc + 0.5));
107072
+ const [fromR, fromG, fromB] = sampleRgb48le(from2, fromUx, fromUy, w, h);
107073
+ const [toR, toG, toB] = sampleRgb48le(to, toUx, toUy, w, h);
107074
+ out.writeUInt16LE(clamp16(mix16(fromR, toR, p)), o);
107075
+ out.writeUInt16LE(clamp16(mix16(fromG, toG, p)), o + 2);
107076
+ out.writeUInt16LE(clamp16(mix16(fromB, toB, p)), o + 4);
107077
+ }
107078
+ };
107079
+ TRANSITIONS["swirl-vortex"] = swirlVortex;
107080
+ var thermalDistortion = (from2, to, out, w, h, p) => {
107081
+ const accentBright = [65535, 55e3, 35e3];
107082
+ for (let i = 0; i < w * h; i++) {
107083
+ const ux = i % w / w;
107084
+ const uy = Math.floor(i / w) / h;
107085
+ const o = i * 6;
107086
+ const heat = p * 1.5;
107087
+ const yFade = smoothstep(1, 0, uy);
107088
+ const shimmer = Math.sin(uy * 40 + fbm(ux * 6, uy * 6) * 8) * fbm(ux * 3 + 0, uy * 3 + p * 2);
107089
+ const dispX = shimmer * heat * 0.03 * yFade;
107090
+ const fromUx = Math.max(0, Math.min(1, ux + dispX));
107091
+ const [fromR, fromG, fromB] = sampleRgb48le(from2, fromUx, uy, w, h);
107092
+ const invShimmer = Math.sin(uy * 40 + fbm(ux * 6 + 3, uy * 6 + 3) * 8) * fbm(ux * 3 + 3, uy * 3 + p * 2);
107093
+ const dispX2 = invShimmer * (1 - p) * 0.03 * yFade;
107094
+ const toUx = Math.max(0, Math.min(1, ux + dispX2));
107095
+ const [toR, toG, toB] = sampleRgb48le(to, toUx, uy, w, h);
107096
+ const haze = heat * yFade * 0.15 * (1 - p);
107097
+ out.writeUInt16LE(clamp16(mix16(fromR, toR, p) + Math.round(accentBright[0] * haze)), o);
107098
+ out.writeUInt16LE(clamp16(mix16(fromG, toG, p) + Math.round(accentBright[1] * haze)), o + 2);
107099
+ out.writeUInt16LE(clamp16(mix16(fromB, toB, p) + Math.round(accentBright[2] * haze)), o + 4);
107100
+ }
107101
+ };
107102
+ TRANSITIONS["thermal-distortion"] = thermalDistortion;
107103
+ var domainWarp = (from2, to, out, w, h, p) => {
107104
+ const accentDark = [25e3, 8e3, 2e3];
107105
+ const accentBright = [65535, 55e3, 35e3];
107106
+ for (let i = 0; i < w * h; i++) {
107107
+ const ux = i % w / w;
107108
+ const uy = Math.floor(i / w) / h;
107109
+ const o = i * 6;
107110
+ const qx = fbm(ux * 3, uy * 3);
107111
+ const qy = fbm(ux * 3 + 5.2, uy * 3 + 1.3);
107112
+ const rx = fbm(ux * 3 + qx * 4 + 1.7, uy * 3 + qy * 4 + 9.2);
107113
+ const ry = fbm(ux * 3 + qx * 4 + 8.3, uy * 3 + qy * 4 + 2.8);
107114
+ const n = fbm(ux * 3 + rx * 2, uy * 3 + ry * 2);
107115
+ const warpDirX = (qx - 0.5) * 0.4;
107116
+ const warpDirY = (qy - 0.5) * 0.4;
107117
+ const aUx = Math.max(0, Math.min(1, ux + warpDirX * p));
107118
+ const aUy = Math.max(0, Math.min(1, uy + warpDirY * p));
107119
+ const bUx = Math.max(0, Math.min(1, ux - warpDirX * (1 - p)));
107120
+ const bUy = Math.max(0, Math.min(1, uy - warpDirY * (1 - p)));
107121
+ const [aR, aG, aB] = sampleRgb48le(from2, aUx, aUy, w, h);
107122
+ const [bR, bG, bB] = sampleRgb48le(to, bUx, bUy, w, h);
107123
+ const e = smoothstep(p - 0.08, p + 0.08, n);
107124
+ const ed = Math.abs(n - p);
107125
+ const pStep = p >= 1 ? 1 : 0;
107126
+ const em = smoothstep(0.1, 0, ed) * (1 - pStep);
107127
+ const ecBlend = smoothstep(0, 0.1, ed);
107128
+ const ecR = accentDark[0] + (accentBright[0] - accentDark[0]) * (1 - ecBlend);
107129
+ const ecG = accentDark[1] + (accentBright[1] - accentDark[1]) * (1 - ecBlend);
107130
+ const ecB = accentDark[2] + (accentBright[2] - accentDark[2]) * (1 - ecBlend);
107131
+ out.writeUInt16LE(clamp16(mix16(bR, aR, e) + Math.round(ecR * em * 2)), o);
107132
+ out.writeUInt16LE(clamp16(mix16(bG, aG, e) + Math.round(ecG * em * 2)), o + 2);
107133
+ out.writeUInt16LE(clamp16(mix16(bB, aB, e) + Math.round(ecB * em * 2)), o + 4);
107134
+ }
107135
+ };
107136
+ TRANSITIONS["domain-warp"] = domainWarp;
107137
+ function ridged(px, py) {
107138
+ let value = 0;
107139
+ let amplitude = 0.5;
107140
+ let x = px;
107141
+ let y = py;
107142
+ for (let i = 0; i < 5; i++) {
107143
+ value += amplitude * Math.abs(vnoise(x, y) * 2 - 1);
107144
+ const nx = ROT_A * x - ROT_B * y;
107145
+ const ny = ROT_B * x + ROT_A * y;
107146
+ x = nx * 2.02;
107147
+ y = ny * 2.02;
107148
+ amplitude *= 0.5;
107149
+ }
107150
+ return value;
107151
+ }
107152
+ var ridgedBurn = (from2, to, out, w, h, p) => {
107153
+ const accent = [5e4, 25e3, 5e3];
107154
+ const accentDark = [25e3, 8e3, 2e3];
107155
+ const accentBright = [65535, 55e3, 35e3];
107156
+ for (let i = 0; i < w * h; i++) {
107157
+ const ux = i % w / w;
107158
+ const uy = Math.floor(i / w) / h;
107159
+ const o = i * 6;
107160
+ const [aR, aG, aB] = sampleRgb48le(from2, ux, uy, w, h);
107161
+ const [bR, bG, bB] = sampleRgb48le(to, ux, uy, w, h);
107162
+ const n = ridged(ux * 4, uy * 4);
107163
+ const e = smoothstep(p - 0.04, p + 0.04, n);
107164
+ const heat = smoothstep(0.12, 0, Math.abs(n - p));
107165
+ const pStep = p >= 1 ? 1 : 0;
107166
+ const heatMasked = heat * (1 - pStep);
107167
+ let burnR = accentDark[0] + (accent[0] - accentDark[0]) * smoothstep(0, 0.25, heatMasked);
107168
+ let burnG = accentDark[1] + (accent[1] - accentDark[1]) * smoothstep(0, 0.25, heatMasked);
107169
+ let burnB = accentDark[2] + (accent[2] - accentDark[2]) * smoothstep(0, 0.25, heatMasked);
107170
+ const blend2 = smoothstep(0.25, 0.5, heatMasked);
107171
+ burnR = burnR + (accentBright[0] - burnR) * blend2;
107172
+ burnG = burnG + (accentBright[1] - burnG) * blend2;
107173
+ burnB = burnB + (accentBright[2] - burnB) * blend2;
107174
+ const blend3 = smoothstep(0.5, 1, heatMasked);
107175
+ burnR = burnR + (65535 - burnR) * blend3;
107176
+ burnG = burnG + (65535 - burnG) * blend3;
107177
+ burnB = burnB + (65535 - burnB) * blend3;
107178
+ const sparks = (vnoise(ux * 80, uy * 80) >= 0.92 ? 1 : 0) * heatMasked * 3;
107179
+ out.writeUInt16LE(
107180
+ clamp16(
107181
+ mix16(bR, aR, e) + Math.round(burnR * heatMasked * 3.5) + Math.round(accentBright[0] * sparks)
107182
+ ),
107183
+ o
107184
+ );
107185
+ out.writeUInt16LE(
107186
+ clamp16(
107187
+ mix16(bG, aG, e) + Math.round(burnG * heatMasked * 3.5) + Math.round(accentBright[1] * sparks)
107188
+ ),
107189
+ o + 2
107190
+ );
107191
+ out.writeUInt16LE(
107192
+ clamp16(
107193
+ mix16(bB, aB, e) + Math.round(burnB * heatMasked * 3.5) + Math.round(accentBright[2] * sparks)
107194
+ ),
107195
+ o + 4
107196
+ );
107197
+ }
107198
+ };
107199
+ TRANSITIONS["ridged-burn"] = ridgedBurn;
107200
+
106343
107201
  // src/services/renderOrchestrator.ts
106344
107202
  import { join as join15, dirname as dirname10, resolve as resolve10 } from "path";
106345
107203
  import { randomUUID } from "crypto";
@@ -108187,6 +109045,63 @@ function applyRenderModeHints(cfg, compiled, log = defaultLogger) {
108187
109045
  reasons: compiled.renderModeHints.reasons.map((reason) => reason.message)
108188
109046
  });
108189
109047
  }
109048
+ function blitHdrVideoLayer(canvas, el, time, fps, hdrFrameDirs, hdrStartTimes, width, height, log, sourceTransfer, targetTransfer) {
109049
+ const frameDir = hdrFrameDirs.get(el.id);
109050
+ const startTime = hdrStartTimes.get(el.id);
109051
+ if (!frameDir || startTime === void 0) {
109052
+ return;
109053
+ }
109054
+ const videoFrameIndex = Math.round((time - startTime) * fps) + 1;
109055
+ if (videoFrameIndex < 1) return;
109056
+ const maxIndex = getMaxFrameIndex(frameDir);
109057
+ const effectiveIndex = maxIndex > 0 ? Math.min(videoFrameIndex, maxIndex) : videoFrameIndex;
109058
+ const framePath = join15(frameDir, `frame_${String(effectiveIndex).padStart(4, "0")}.png`);
109059
+ if (!existsSync15(framePath)) {
109060
+ return;
109061
+ }
109062
+ try {
109063
+ const { data: hdrRgb, width: srcW, height: srcH } = decodePngToRgb48le(readFileSync9(framePath));
109064
+ if (sourceTransfer && targetTransfer && sourceTransfer !== targetTransfer) {
109065
+ convertTransfer(hdrRgb, sourceTransfer, targetTransfer);
109066
+ }
109067
+ const viewportMatrix = parseTransformMatrix(el.transform);
109068
+ const br = el.borderRadius;
109069
+ const hasBorderRadius = br[0] > 0 || br[1] > 0 || br[2] > 0 || br[3] > 0;
109070
+ const borderRadiusParam = hasBorderRadius ? br : void 0;
109071
+ if (viewportMatrix) {
109072
+ blitRgb48leAffine(
109073
+ canvas,
109074
+ hdrRgb,
109075
+ viewportMatrix,
109076
+ srcW,
109077
+ srcH,
109078
+ width,
109079
+ height,
109080
+ el.opacity < 0.999 ? el.opacity : void 0,
109081
+ borderRadiusParam
109082
+ );
109083
+ } else {
109084
+ blitRgb48leRegion(
109085
+ canvas,
109086
+ hdrRgb,
109087
+ el.x,
109088
+ el.y,
109089
+ srcW,
109090
+ srcH,
109091
+ width,
109092
+ height,
109093
+ el.opacity < 0.999 ? el.opacity : void 0,
109094
+ borderRadiusParam
109095
+ );
109096
+ }
109097
+ } catch (err) {
109098
+ if (log) {
109099
+ log.debug(`HDR blit failed for ${el.id}`, {
109100
+ error: err instanceof Error ? err.message : String(err)
109101
+ });
109102
+ }
109103
+ }
109104
+ }
108190
109105
  function createRenderJob(config2) {
108191
109106
  return {
108192
109107
  id: randomUUID(),
@@ -108457,6 +109372,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108457
109372
  perfStages.browserProbeMs = Date.now() - probeStart;
108458
109373
  job.duration = composition.duration;
108459
109374
  job.totalFrames = Math.ceil(composition.duration * job.config.fps);
109375
+ const totalFrames = job.totalFrames;
108460
109376
  if (job.duration <= 0) {
108461
109377
  const diagnostics = [];
108462
109378
  try {
@@ -108514,7 +109430,8 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108514
109430
  const compiledDir = join15(workDir, "compiled");
108515
109431
  let extractionResult = null;
108516
109432
  const nativeHdrVideoIds = /* @__PURE__ */ new Set();
108517
- if (composition.videos.length > 0) {
109433
+ const videoTransfers = /* @__PURE__ */ new Map();
109434
+ if (job.config.hdr && composition.videos.length > 0) {
108518
109435
  await Promise.all(
108519
109436
  composition.videos.map(async (v) => {
108520
109437
  let videoPath = v.src;
@@ -108526,6 +109443,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108526
109443
  const meta = await extractVideoMetadata(videoPath);
108527
109444
  if (isHdrColorSpace(meta.colorSpace)) {
108528
109445
  nativeHdrVideoIds.add(v.id);
109446
+ videoTransfers.set(v.id, detectTransfer(meta.colorSpace));
108529
109447
  }
108530
109448
  })
108531
109449
  );
@@ -108567,13 +109485,19 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108567
109485
  perfStages.videoExtractMs = Date.now() - stage2Start;
108568
109486
  }
108569
109487
  let effectiveHdr;
108570
- if (frameLookup) {
109488
+ if (job.config.hdr && frameLookup) {
108571
109489
  const colorSpaces = (extractionResult?.extracted ?? []).map((ext) => ext.metadata.colorSpace);
108572
109490
  const info = analyzeCompositionHdr(colorSpaces);
108573
109491
  if (info.hasHdr && info.dominantTransfer) {
108574
109492
  effectiveHdr = { transfer: info.dominantTransfer };
108575
109493
  }
108576
109494
  }
109495
+ if (job.config.hdr && !effectiveHdr && nativeHdrVideoIds.size > 0) {
109496
+ const firstTransfer = videoTransfers.values().next().value;
109497
+ if (firstTransfer) {
109498
+ effectiveHdr = { transfer: firstTransfer };
109499
+ }
109500
+ }
108577
109501
  if (effectiveHdr && outputFormat !== "mp4") {
108578
109502
  log.info(`[Render] HDR source detected but format is ${outputFormat} \u2014 using SDR`);
108579
109503
  effectiveHdr = void 0;
@@ -108624,15 +109548,15 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108624
109548
  format: needsAlpha ? "png" : "jpeg",
108625
109549
  quality: needsAlpha ? void 0 : job.config.quality === "draft" ? 80 : 95
108626
109550
  };
108627
- const workerCount = calculateOptimalWorkers(job.totalFrames, job.config.workers, cfg);
109551
+ const workerCount = calculateOptimalWorkers(totalFrames, job.config.workers, cfg);
108628
109552
  const FORMAT_EXT = { mp4: ".mp4", webm: ".webm", mov: ".mov" };
108629
109553
  const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
108630
109554
  const videoOnlyPath = join15(workDir, `video-only${videoExt}`);
108631
- const hasHdrVideo = effectiveHdr && composition.videos.length > 0 && frameLookup;
108632
- const encoderHdr = hasHdrVideo ? effectiveHdr : void 0;
109555
+ const hasHdrContent = effectiveHdr && nativeHdrVideoIds.size > 0;
109556
+ const encoderHdr = hasHdrContent ? effectiveHdr : void 0;
108633
109557
  const preset = getEncoderPreset(job.config.quality, outputFormat, encoderHdr);
108634
109558
  job.framesRendered = 0;
108635
- if (hasHdrVideo) {
109559
+ if (hasHdrContent) {
108636
109560
  log.info("[Render] HDR layered composite: z-ordered DOM + native HLG video layers");
108637
109561
  const hdrVideoIds = composition.videos.filter((v) => nativeHdrVideoIds.has(v.id)).map((v) => v.id);
108638
109562
  const hdrVideoSrcPaths = /* @__PURE__ */ new Map();
@@ -108645,6 +109569,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108645
109569
  }
108646
109570
  hdrVideoSrcPaths.set(v.id, srcPath);
108647
109571
  }
109572
+ if (!fileServer) throw new Error("fileServer must be initialized before HDR compositing");
108648
109573
  const domSession = await createCaptureSession(
108649
109574
  fileServer.url,
108650
109575
  framesDir,
@@ -108656,6 +109581,34 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108656
109581
  assertNotAborted();
108657
109582
  lastBrowserConsole = domSession.browserConsoleBuffer;
108658
109583
  await initTransparentBackground(domSession.page);
109584
+ const transitionMeta = await domSession.page.evaluate(() => {
109585
+ return window.__hf?.transitions ?? [];
109586
+ });
109587
+ const sceneElements = await domSession.page.evaluate(() => {
109588
+ const scenes = document.querySelectorAll(".scene");
109589
+ const map2 = {};
109590
+ for (const scene of scenes) {
109591
+ const els = scene.querySelectorAll("[data-start]");
109592
+ map2[scene.id] = Array.from(els).map((e) => e.id);
109593
+ }
109594
+ return map2;
109595
+ });
109596
+ const transitionRanges = transitionMeta.map((t) => ({
109597
+ ...t,
109598
+ startFrame: Math.floor(t.time * job.config.fps),
109599
+ endFrame: Math.ceil((t.time + t.duration) * job.config.fps)
109600
+ }));
109601
+ if (transitionRanges.length > 0) {
109602
+ log.info("[Render] Detected shader transitions for HDR compositing", {
109603
+ count: transitionRanges.length,
109604
+ transitions: transitionRanges.map((t) => ({
109605
+ shader: t.shader,
109606
+ from: t.fromScene,
109607
+ to: t.toScene,
109608
+ frames: `${t.startFrame}-${t.endFrame}`
109609
+ }))
109610
+ });
109611
+ }
108659
109612
  const hdrEncoder = await spawnStreamingEncoder(
108660
109613
  videoOnlyPath,
108661
109614
  {
@@ -108673,7 +109626,28 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108673
109626
  { ffmpegStreamingTimeout: 36e5 }
108674
109627
  );
108675
109628
  assertNotAborted();
108676
- const { execSync: execSync2 } = await import("child_process");
109629
+ const hdrExtractionDims = /* @__PURE__ */ new Map();
109630
+ const hdrVideoStartTimes = /* @__PURE__ */ new Map();
109631
+ for (const v of composition.videos) {
109632
+ if (hdrVideoIds.includes(v.id)) {
109633
+ hdrVideoStartTimes.set(v.id, v.start);
109634
+ }
109635
+ }
109636
+ const uniqueStartTimes = [...new Set(hdrVideoStartTimes.values())].sort((a, b) => a - b);
109637
+ for (const seekTime of uniqueStartTimes) {
109638
+ await domSession.page.evaluate((t) => {
109639
+ if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
109640
+ }, seekTime);
109641
+ if (domSession.onBeforeCapture) {
109642
+ await domSession.onBeforeCapture(domSession.page, seekTime);
109643
+ }
109644
+ const stacking = await queryElementStacking(domSession.page, nativeHdrVideoIds);
109645
+ for (const el of stacking) {
109646
+ if (el.isHdr && el.layoutWidth > 0 && el.layoutHeight > 0 && !hdrExtractionDims.has(el.id)) {
109647
+ hdrExtractionDims.set(el.id, { width: el.layoutWidth, height: el.layoutHeight });
109648
+ }
109649
+ }
109650
+ }
108677
109651
  const hdrFrameDirs = /* @__PURE__ */ new Map();
108678
109652
  for (const [videoId, srcPath] of hdrVideoSrcPaths) {
108679
109653
  const video = composition.videos.find((v) => v.id === videoId);
@@ -108681,24 +109655,190 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108681
109655
  const frameDir = join15(framesDir, `hdr_${videoId}`);
108682
109656
  mkdirSync10(frameDir, { recursive: true });
108683
109657
  const duration = video.end - video.start;
108684
- try {
108685
- execSync2(
108686
- `ffmpeg -ss ${video.mediaStart} -i "${srcPath}" -t ${duration} -r ${job.config.fps} -vf "scale=${width}:${height}:force_original_aspect_ratio=increase,crop=${width}:${height}" -pix_fmt rgb48le -c:v png "${join15(frameDir, "frame_%04d.png")}"`,
108687
- { maxBuffer: 1024 * 1024, stdio: ["pipe", "pipe", "pipe"] }
108688
- );
108689
- } catch (err) {
109658
+ const dims = hdrExtractionDims.get(videoId) ?? { width, height };
109659
+ const ffmpegArgs = [
109660
+ "-ss",
109661
+ String(video.mediaStart),
109662
+ "-i",
109663
+ srcPath,
109664
+ "-t",
109665
+ String(duration),
109666
+ "-r",
109667
+ String(job.config.fps),
109668
+ "-vf",
109669
+ `scale=${dims.width}:${dims.height}:force_original_aspect_ratio=increase,crop=${dims.width}:${dims.height}`,
109670
+ "-pix_fmt",
109671
+ "rgb48le",
109672
+ "-c:v",
109673
+ "png",
109674
+ "-y",
109675
+ join15(frameDir, "frame_%04d.png")
109676
+ ];
109677
+ const result = await runFfmpeg(ffmpegArgs, { signal: abortSignal });
109678
+ if (!result.success) {
108690
109679
  log.warn("HDR frame pre-extraction failed; loop will fill with black", {
108691
109680
  videoId,
108692
109681
  srcPath,
108693
- error: err instanceof Error ? err.message : String(err)
109682
+ stderr: result.stderr.slice(-400)
108694
109683
  });
108695
109684
  }
108696
109685
  hdrFrameDirs.set(videoId, frameDir);
108697
109686
  }
108698
109687
  assertNotAborted();
108699
109688
  try {
109689
+ let countNonZeroAlpha2 = function(rgba) {
109690
+ let n = 0;
109691
+ for (let p = 3; p < rgba.length; p += 4) {
109692
+ if (rgba[p] !== 0) n++;
109693
+ }
109694
+ return n;
109695
+ }, countNonZeroRgb482 = function(buf) {
109696
+ let n = 0;
109697
+ for (let p = 0; p < buf.length; p += 6) {
109698
+ if (buf[p] !== 0 || buf[p + 1] !== 0 || buf[p + 2] !== 0) n++;
109699
+ }
109700
+ return n;
109701
+ };
109702
+ var countNonZeroAlpha = countNonZeroAlpha2, countNonZeroRgb48 = countNonZeroRgb482;
108700
109703
  const beforeCaptureHook = domSession.onBeforeCapture;
108701
- for (let i = 0; i < job.totalFrames; i++) {
109704
+ const cleanedUpVideos = /* @__PURE__ */ new Set();
109705
+ const hdrVideoEndTimes = /* @__PURE__ */ new Map();
109706
+ for (const v of composition.videos) {
109707
+ if (hdrFrameDirs.has(v.id)) {
109708
+ hdrVideoEndTimes.set(v.id, v.end);
109709
+ }
109710
+ }
109711
+ const debugDumpEnabled = process.env.KEEP_TEMP === "1";
109712
+ const debugDumpDir = debugDumpEnabled ? join15(framesDir, "debug-composite") : null;
109713
+ if (debugDumpDir && !existsSync15(debugDumpDir)) {
109714
+ mkdirSync10(debugDumpDir, { recursive: true });
109715
+ }
109716
+ async function compositeToBuffer(canvas, time, fullStacking, elementFilter, debugFrameIndex = -1) {
109717
+ const filteredStacking = elementFilter ? fullStacking.filter((e) => elementFilter.has(e.id)) : fullStacking;
109718
+ const layers = groupIntoLayers(filteredStacking);
109719
+ const shouldLog = debugDumpEnabled && debugFrameIndex >= 0;
109720
+ if (shouldLog) {
109721
+ log.info("[diag] compositeToBuffer plan", {
109722
+ frame: debugFrameIndex,
109723
+ time: time.toFixed(3),
109724
+ filterSize: elementFilter?.size,
109725
+ fullStackingCount: fullStacking.length,
109726
+ filteredCount: filteredStacking.length,
109727
+ layerCount: layers.length,
109728
+ layers: layers.map(
109729
+ (l) => l.type === "hdr" ? {
109730
+ type: "hdr",
109731
+ id: l.element.id,
109732
+ z: l.element.zIndex,
109733
+ visible: l.element.visible,
109734
+ opacity: l.element.opacity,
109735
+ bounds: `${Math.round(l.element.x)},${Math.round(l.element.y)} ${Math.round(l.element.width)}x${Math.round(l.element.height)}`
109736
+ } : { type: "dom", ids: l.elementIds }
109737
+ )
109738
+ });
109739
+ }
109740
+ for (let layerIdx = 0; layerIdx < layers.length; layerIdx++) {
109741
+ const layer = layers[layerIdx];
109742
+ if (layer.type === "hdr") {
109743
+ const before2 = shouldLog ? countNonZeroRgb482(canvas) : 0;
109744
+ blitHdrVideoLayer(
109745
+ canvas,
109746
+ layer.element,
109747
+ time,
109748
+ job.config.fps,
109749
+ hdrFrameDirs,
109750
+ hdrVideoStartTimes,
109751
+ width,
109752
+ height,
109753
+ log,
109754
+ videoTransfers.get(layer.element.id),
109755
+ effectiveHdr?.transfer
109756
+ );
109757
+ if (shouldLog) {
109758
+ const after2 = countNonZeroRgb482(canvas);
109759
+ const frameDir = hdrFrameDirs.get(layer.element.id);
109760
+ const startTime = hdrVideoStartTimes.get(layer.element.id) ?? 0;
109761
+ const localTime = time - startTime;
109762
+ const frameNum = Math.floor(localTime * job.config.fps) + 1;
109763
+ const expectedFrame = frameDir ? join15(frameDir, `frame_${String(frameNum).padStart(4, "0")}.png`) : null;
109764
+ log.info("[diag] hdr layer blit", {
109765
+ frame: debugFrameIndex,
109766
+ layerIdx,
109767
+ id: layer.element.id,
109768
+ pixelsAdded: after2 - before2,
109769
+ totalNonZero: after2,
109770
+ startTime,
109771
+ localTime: localTime.toFixed(3),
109772
+ hdrFrameNum: frameNum,
109773
+ expectedFrame,
109774
+ expectedFrameExists: expectedFrame ? existsSync15(expectedFrame) : false
109775
+ });
109776
+ }
109777
+ } else {
109778
+ const allElementIds = fullStacking.map((e) => e.id);
109779
+ const layerIds = new Set(layer.elementIds);
109780
+ const hideIds = allElementIds.filter((id) => !layerIds.has(id));
109781
+ await domSession.page.evaluate((t) => {
109782
+ if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
109783
+ }, time);
109784
+ if (beforeCaptureHook) {
109785
+ await beforeCaptureHook(domSession.page, time);
109786
+ }
109787
+ await applyDomLayerMask(domSession.page, layer.elementIds, hideIds);
109788
+ const domPng = await captureAlphaPng(domSession.page, width, height);
109789
+ await removeDomLayerMask(domSession.page, hideIds);
109790
+ try {
109791
+ const { data: domRgba } = decodePng(domPng);
109792
+ if (!effectiveHdr) {
109793
+ throw new Error(
109794
+ "Invariant violation: effectiveHdr is undefined inside HDR layer branch"
109795
+ );
109796
+ }
109797
+ const before2 = shouldLog ? countNonZeroRgb482(canvas) : 0;
109798
+ const alphaPixels = shouldLog ? countNonZeroAlpha2(domRgba) : 0;
109799
+ blitRgba8OverRgb48le(domRgba, canvas, width, height, effectiveHdr.transfer);
109800
+ if (shouldLog && debugDumpDir) {
109801
+ const after2 = countNonZeroRgb482(canvas);
109802
+ const dumpName = `frame_${String(debugFrameIndex).padStart(4, "0")}_layer_${String(layerIdx).padStart(2, "0")}_dom.png`;
109803
+ const dumpPath = join15(debugDumpDir, dumpName);
109804
+ writeFileSync4(dumpPath, domPng);
109805
+ log.info("[diag] dom layer blit", {
109806
+ frame: debugFrameIndex,
109807
+ layerIdx,
109808
+ layerIds: layer.elementIds,
109809
+ hideCount: hideIds.length,
109810
+ pngBytes: domPng.length,
109811
+ alphaPixels,
109812
+ pixelsAdded: after2 - before2,
109813
+ totalNonZero: after2,
109814
+ dumpPath
109815
+ });
109816
+ }
109817
+ } catch (err) {
109818
+ log.warn("DOM layer decode/blit failed; skipping overlay", {
109819
+ layerIds: layer.elementIds,
109820
+ error: err instanceof Error ? err.message : String(err)
109821
+ });
109822
+ }
109823
+ }
109824
+ }
109825
+ if (shouldLog && debugDumpDir) {
109826
+ const finalNonZero = countNonZeroRgb482(canvas);
109827
+ log.info("[diag] compositeToBuffer end", {
109828
+ frame: debugFrameIndex,
109829
+ finalNonZeroPixels: finalNonZero,
109830
+ totalPixels: width * height,
109831
+ coverage: (finalNonZero / (width * height) * 100).toFixed(1) + "%"
109832
+ });
109833
+ }
109834
+ }
109835
+ const bufSize = width * height * 6;
109836
+ const hasTransitions = transitionRanges.length > 0;
109837
+ const transBufferA = hasTransitions ? Buffer.alloc(bufSize) : null;
109838
+ const transBufferB = hasTransitions ? Buffer.alloc(bufSize) : null;
109839
+ const transOutput = hasTransitions ? Buffer.alloc(bufSize) : null;
109840
+ const normalCanvas = Buffer.alloc(bufSize);
109841
+ for (let i = 0; i < totalFrames; i++) {
108702
109842
  assertNotAborted();
108703
109843
  const time = i / job.config.fps;
108704
109844
  await domSession.page.evaluate((t) => {
@@ -108708,81 +109848,118 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108708
109848
  await beforeCaptureHook(domSession.page, time);
108709
109849
  }
108710
109850
  const stackingInfo = await queryElementStacking(domSession.page, nativeHdrVideoIds);
108711
- const layers = groupIntoLayers(stackingInfo);
109851
+ const activeTransition = transitionRanges.find(
109852
+ (t) => i >= t.startFrame && i <= t.endFrame
109853
+ );
108712
109854
  if (i % 30 === 0) {
108713
109855
  const hdrEl = stackingInfo.find((e) => e.isHdr);
108714
- const hdrInLayers = layers.some((l) => l.type === "hdr");
108715
109856
  log.debug("[Render] HDR layer composite frame", {
108716
109857
  frame: i,
108717
109858
  time: time.toFixed(2),
108718
109859
  hdrElement: hdrEl ? { z: hdrEl.zIndex, visible: hdrEl.visible, width: hdrEl.width } : null,
108719
- hdrLayerPresent: hdrInLayers,
108720
- layerCount: layers.length
109860
+ stackingCount: stackingInfo.length,
109861
+ activeTransition: activeTransition?.shader
108721
109862
  });
108722
109863
  }
108723
- const canvas = Buffer.alloc(width * height * 6);
108724
- for (const layer of layers) {
108725
- if (layer.type === "hdr") {
108726
- const el = layer.element;
108727
- const frameDir = hdrFrameDirs.get(el.id);
108728
- const video = composition.videos.find((v) => v.id === el.id);
108729
- if (!frameDir || !video) continue;
108730
- const videoFrameIndex = Math.round((time - video.start) * job.config.fps) + 1;
108731
- const maxIndex = getMaxFrameIndex(frameDir);
108732
- const inBounds = videoFrameIndex >= 1 && (maxIndex === 0 || videoFrameIndex <= maxIndex);
108733
- const framePath = inBounds ? join15(frameDir, `frame_${String(videoFrameIndex).padStart(4, "0")}.png`) : null;
108734
- if (framePath !== null && existsSync15(framePath)) {
108735
- try {
108736
- const hdrRgb = decodePngToRgb48le(readFileSync9(framePath)).data;
108737
- blitRgb48leRegion(
108738
- canvas,
108739
- hdrRgb,
108740
- el.x,
108741
- el.y,
108742
- el.width,
108743
- el.height,
108744
- width,
108745
- height,
108746
- el.opacity < 0.999 ? el.opacity : void 0
108747
- );
108748
- } catch (err) {
108749
- log.warn("HDR layer decode/blit failed; skipping layer for frame", {
108750
- frameIndex: i,
108751
- videoId: el.id,
108752
- framePath,
108753
- error: err instanceof Error ? err.message : String(err)
108754
- });
108755
- }
108756
- }
108757
- } else {
108758
- const allElementIds = stackingInfo.map((e) => e.id);
108759
- const layerIds = new Set(layer.elementIds);
108760
- const hideIds = allElementIds.filter(
108761
- (id) => !layerIds.has(id) || nativeHdrVideoIds.has(id)
108762
- );
108763
- await hideVideoElements(domSession.page, hideIds);
108764
- const domPng = await captureAlphaPng(domSession.page, width, height);
108765
- await showVideoElements(domSession.page, hideIds);
109864
+ if (activeTransition && transBufferA && transBufferB && transOutput) {
109865
+ const progress = activeTransition.endFrame === activeTransition.startFrame ? 1 : (i - activeTransition.startFrame) / (activeTransition.endFrame - activeTransition.startFrame);
109866
+ const sceneAIds = new Set(sceneElements[activeTransition.fromScene] ?? []);
109867
+ const sceneBIds = new Set(sceneElements[activeTransition.toScene] ?? []);
109868
+ transBufferA.fill(0);
109869
+ transBufferB.fill(0);
109870
+ for (const [sceneBuf, sceneIds] of [
109871
+ [transBufferA, sceneAIds],
109872
+ [transBufferB, sceneBIds]
109873
+ ]) {
108766
109874
  await domSession.page.evaluate((t) => {
108767
109875
  if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
108768
109876
  }, time);
109877
+ if (beforeCaptureHook) {
109878
+ await beforeCaptureHook(domSession.page, time);
109879
+ }
109880
+ for (const el of stackingInfo) {
109881
+ if (!el.isHdr || !sceneIds.has(el.id)) continue;
109882
+ blitHdrVideoLayer(
109883
+ sceneBuf,
109884
+ el,
109885
+ time,
109886
+ job.config.fps,
109887
+ hdrFrameDirs,
109888
+ hdrVideoStartTimes,
109889
+ width,
109890
+ height,
109891
+ log,
109892
+ videoTransfers.get(el.id),
109893
+ effectiveHdr?.transfer
109894
+ );
109895
+ }
109896
+ const showIds = Array.from(sceneIds);
109897
+ const hideIds = stackingInfo.map((e) => e.id).filter((id) => !sceneIds.has(id) || nativeHdrVideoIds.has(id));
109898
+ await applyDomLayerMask(domSession.page, showIds, hideIds);
109899
+ const domPng = await captureAlphaPng(domSession.page, width, height);
109900
+ await removeDomLayerMask(domSession.page, hideIds);
108769
109901
  try {
108770
109902
  const { data: domRgba } = decodePng(domPng);
108771
- const hdrTransfer = effectiveHdr ? effectiveHdr.transfer : "hlg";
108772
- blitRgba8OverRgb48le(domRgba, canvas, width, height, hdrTransfer);
109903
+ if (!effectiveHdr) {
109904
+ throw new Error(
109905
+ "Invariant violation: effectiveHdr is undefined inside hasHdrVideo branch"
109906
+ );
109907
+ }
109908
+ blitRgba8OverRgb48le(
109909
+ domRgba,
109910
+ sceneBuf,
109911
+ width,
109912
+ height,
109913
+ effectiveHdr.transfer
109914
+ );
108773
109915
  } catch (err) {
108774
- log.warn("DOM layer decode/blit failed; skipping overlay for frame", {
109916
+ log.warn("DOM layer decode/blit failed; skipping overlay for transition scene", {
108775
109917
  frameIndex: i,
108776
- layerIds: layer.elementIds,
109918
+ sceneIds: Array.from(sceneIds),
108777
109919
  error: err instanceof Error ? err.message : String(err)
108778
109920
  });
108779
109921
  }
108780
109922
  }
109923
+ const transitionFn = TRANSITIONS[activeTransition.shader] ?? crossfade;
109924
+ transitionFn(transBufferA, transBufferB, transOutput, width, height, progress);
109925
+ hdrEncoder.writeFrame(transOutput);
109926
+ } else {
109927
+ normalCanvas.fill(0);
109928
+ await compositeToBuffer(normalCanvas, time, stackingInfo, void 0, i);
109929
+ if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
109930
+ const previewPath = join15(
109931
+ debugDumpDir,
109932
+ `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`
109933
+ );
109934
+ writeFileSync4(previewPath, normalCanvas);
109935
+ }
109936
+ hdrEncoder.writeFrame(normalCanvas);
109937
+ }
109938
+ if (process.env.KEEP_TEMP !== "1") {
109939
+ for (const [videoId, endTime] of hdrVideoEndTimes) {
109940
+ if (time > endTime && !cleanedUpVideos.has(videoId)) {
109941
+ const stillNeeded = activeTransition && (sceneElements[activeTransition.fromScene]?.includes(videoId) || sceneElements[activeTransition.toScene]?.includes(videoId));
109942
+ if (!stillNeeded) {
109943
+ const frameDir = hdrFrameDirs.get(videoId);
109944
+ if (frameDir) {
109945
+ try {
109946
+ rmSync3(frameDir, { recursive: true, force: true });
109947
+ } catch (err) {
109948
+ log.warn("Failed to clean up HDR frame directory", {
109949
+ videoId,
109950
+ frameDir,
109951
+ error: err instanceof Error ? err.message : String(err)
109952
+ });
109953
+ }
109954
+ }
109955
+ cleanedUpVideos.add(videoId);
109956
+ }
109957
+ }
109958
+ }
108781
109959
  }
108782
- hdrEncoder.writeFrame(canvas);
108783
109960
  job.framesRendered = i + 1;
108784
- if ((i + 1) % 10 === 0 || i + 1 === job.totalFrames) {
108785
- const frameProgress = (i + 1) / job.totalFrames;
109961
+ if ((i + 1) % 10 === 0 || i + 1 === totalFrames) {
109962
+ const frameProgress = (i + 1) / totalFrames;
108786
109963
  updateJobStatus(
108787
109964
  job,
108788
109965
  "rendering",
@@ -108825,7 +110002,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108825
110002
  assertNotAborted();
108826
110003
  }
108827
110004
  if (enableStreamingEncode && streamingEncoder) {
108828
- const reorderBuffer = createFrameReorderBuffer(0, job.totalFrames);
110005
+ const reorderBuffer = createFrameReorderBuffer(0, totalFrames);
108829
110006
  const currentEncoder = streamingEncoder;
108830
110007
  if (workerCount > 1) {
108831
110008
  const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
@@ -108882,7 +110059,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108882
110059
  }
108883
110060
  assertNotAborted();
108884
110061
  lastBrowserConsole = session.browserConsoleBuffer;
108885
- for (let i = 0; i < job.totalFrames; i++) {
110062
+ for (let i = 0; i < totalFrames; i++) {
108886
110063
  assertNotAborted();
108887
110064
  const time = i / job.config.fps;
108888
110065
  const { buffer } = await captureFrameToBuffer(session, i, time);
@@ -108890,7 +110067,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108890
110067
  currentEncoder.writeFrame(buffer);
108891
110068
  reorderBuffer.advanceTo(i + 1);
108892
110069
  job.framesRendered = i + 1;
108893
- const frameProgress = (i + 1) / job.totalFrames;
110070
+ const frameProgress = (i + 1) / totalFrames;
108894
110071
  const progress = 25 + frameProgress * 55;
108895
110072
  updateJobStatus(
108896
110073
  job,
@@ -109063,12 +110240,12 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109063
110240
  chunkedEncode: enableChunkedEncode,
109064
110241
  chunkSizeFrames: enableChunkedEncode ? chunkedEncodeSize : null,
109065
110242
  compositionDurationSeconds: composition.duration,
109066
- totalFrames: job.totalFrames,
110243
+ totalFrames,
109067
110244
  resolution: { width, height },
109068
110245
  videoCount: composition.videos.length,
109069
110246
  audioCount: composition.audios.length,
109070
110247
  stages: perfStages,
109071
- captureAvgMs: job.totalFrames > 0 ? Math.round((perfStages.captureMs ?? 0) / job.totalFrames) : void 0
110248
+ captureAvgMs: totalFrames > 0 ? Math.round((perfStages.captureMs ?? 0) / totalFrames) : void 0
109072
110249
  };
109073
110250
  job.perfSummary = perfSummary;
109074
110251
  if (job.config.debug) {
@@ -109086,6 +110263,8 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109086
110263
  const debugOutput = join15(workDir, `output${videoExt}`);
109087
110264
  copyFileSync2(outputPath, debugOutput);
109088
110265
  }
110266
+ } else if (process.env.KEEP_TEMP === "1") {
110267
+ log.info("KEEP_TEMP=1 \u2014 leaving workDir on disk for inspection", { workDir });
109089
110268
  } else {
109090
110269
  await safeCleanup(
109091
110270
  "remove workDir",