@hyperframes/producer 0.4.7 → 0.4.9

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.
@@ -5117,12 +5117,12 @@ var require_common = __commonJS({
5117
5117
  createDebug.skips = [];
5118
5118
  createDebug.formatters = {};
5119
5119
  function selectColor(namespace) {
5120
- let hash = 0;
5120
+ let hash2 = 0;
5121
5121
  for (let i = 0; i < namespace.length; i++) {
5122
- hash = (hash << 5) - hash + namespace.charCodeAt(i);
5123
- hash |= 0;
5122
+ hash2 = (hash2 << 5) - hash2 + namespace.charCodeAt(i);
5123
+ hash2 |= 0;
5124
5124
  }
5125
- return createDebug.colors[Math.abs(hash) % createDebug.colors.length];
5125
+ return createDebug.colors[Math.abs(hash2) % createDebug.colors.length];
5126
5126
  }
5127
5127
  createDebug.selectColor = selectColor;
5128
5128
  function createDebug(namespace) {
@@ -54350,25 +54350,25 @@ var require_data = __commonJS({
54350
54350
  var notmodified_1 = __importDefault2(require_notmodified());
54351
54351
  var debug6 = (0, debug_1.default)("get-uri:data");
54352
54352
  var DataReadable = class extends stream_1.Readable {
54353
- constructor(hash, buf) {
54353
+ constructor(hash2, buf) {
54354
54354
  super();
54355
54355
  this.push(buf);
54356
54356
  this.push(null);
54357
- this.hash = hash;
54357
+ this.hash = hash2;
54358
54358
  }
54359
54359
  };
54360
54360
  var data = async ({ href: uri }, { cache } = {}) => {
54361
54361
  const shasum = (0, crypto_1.createHash)("sha1");
54362
54362
  shasum.update(uri);
54363
- const hash = shasum.digest("hex");
54364
- debug6('generated SHA1 hash for "data:" URI: %o', hash);
54365
- if (cache?.hash === hash) {
54366
- debug6("got matching cache SHA1 hash: %o", hash);
54363
+ const hash2 = shasum.digest("hex");
54364
+ debug6('generated SHA1 hash for "data:" URI: %o', hash2);
54365
+ if (cache?.hash === hash2) {
54366
+ debug6("got matching cache SHA1 hash: %o", hash2);
54367
54367
  throw new notmodified_1.default();
54368
54368
  } else {
54369
54369
  debug6('creating Readable stream from "data:" URI buffer');
54370
54370
  const { buffer } = (0, data_uri_to_buffer_1.dataUriToBuffer)(uri);
54371
- return new DataReadable(hash, Buffer.from(buffer));
54371
+ return new DataReadable(hash2, Buffer.from(buffer));
54372
54372
  }
54373
54373
  };
54374
54374
  exports.data = data;
@@ -75363,14 +75363,14 @@ var require_dist10 = __commonJS({
75363
75363
  (0, quickjs_emscripten_1.getQuickJS)(),
75364
75364
  this.loadPacFile()
75365
75365
  ]);
75366
- const hash = crypto3.createHash("sha1").update(code).digest("hex");
75367
- if (this.resolver && this.resolverHash === hash) {
75366
+ const hash2 = crypto3.createHash("sha1").update(code).digest("hex");
75367
+ if (this.resolver && this.resolverHash === hash2) {
75368
75368
  debug6("Same sha1 hash for code - contents have not changed, reusing previous proxy resolver");
75369
75369
  return this.resolver;
75370
75370
  }
75371
75371
  debug6("Creating new proxy resolver instance");
75372
75372
  this.resolver = (0, pac_resolver_1.createPacResolver)(qjs, code, this.opts);
75373
- this.resolverHash = hash;
75373
+ this.resolverHash = hash2;
75374
75374
  return this.resolver;
75375
75375
  } catch (err) {
75376
75376
  if (this.resolver && err.code === "ENOTMODIFIED") {
@@ -102433,6 +102433,7 @@ var mediaRules = [
102433
102433
  const timedTagPositions = [];
102434
102434
  for (const tag of tags) {
102435
102435
  if (tag.name === "video" || tag.name === "audio") continue;
102436
+ if (readAttr(tag.raw, "data-composition-id")) continue;
102436
102437
  if (readAttr(tag.raw, "data-start")) {
102437
102438
  timedTagPositions.push({
102438
102439
  name: tag.name,
@@ -103539,7 +103540,8 @@ function lintHyperframeHtml(html, options = {}) {
103539
103540
  }
103540
103541
 
103541
103542
  // ../core/src/compiler/rewriteSubCompPaths.ts
103542
- import { join as join4, resolve as resolve6, dirname as dirname4 } from "path";
103543
+ import { posix } from "path";
103544
+ var { join: join4, resolve: resolve6, dirname: dirname4 } = posix;
103543
103545
  var PATH_ATTRS = ["src", "href"];
103544
103546
  var CSS_URL_RE = /\burl\(\s*(["']?)([^)"']+)\1\s*\)/g;
103545
103547
  function isAbsoluteOrSpecial(val) {
@@ -103706,11 +103708,19 @@ async function pageScreenshotCapture(page, options) {
103706
103708
  });
103707
103709
  return Buffer.from(result.data, "base64");
103708
103710
  }
103711
+ var TRANSPARENT_BG_STYLE_ID = "__hf_transparent_bg__";
103709
103712
  async function initTransparentBackground(page) {
103710
103713
  const client = await getCdpSession(page);
103711
103714
  await client.send("Emulation.setDefaultBackgroundColorOverride", {
103712
103715
  color: { r: 0, g: 0, b: 0, a: 0 }
103713
103716
  });
103717
+ await page.evaluate((styleId) => {
103718
+ if (document.getElementById(styleId)) return;
103719
+ const style = document.createElement("style");
103720
+ style.id = styleId;
103721
+ style.textContent = "html,body,[data-composition-id]{background:transparent !important;background-color:transparent !important;background-image:none !important;}";
103722
+ document.head.appendChild(style);
103723
+ }, TRANSPARENT_BG_STYLE_ID);
103714
103724
  }
103715
103725
  async function captureAlphaPng(page, width, height) {
103716
103726
  const client = await getCdpSession(page);
@@ -103724,6 +103734,57 @@ async function captureAlphaPng(page, width, height) {
103724
103734
  });
103725
103735
  return Buffer.from(result.data, "base64");
103726
103736
  }
103737
+ var DOM_LAYER_MASK_STYLE_ID = "__hf_dom_layer_mask__";
103738
+ async function applyDomLayerMask(page, showIds, extraHideIds) {
103739
+ await page.evaluate(
103740
+ (args) => {
103741
+ const existing = document.getElementById(args.styleId);
103742
+ if (existing) existing.remove();
103743
+ const showSelectors = [];
103744
+ for (const id of args.show) {
103745
+ const escaped = CSS.escape(id);
103746
+ showSelectors.push(`#${escaped}`, `#${escaped} *`);
103747
+ const renderEscaped = CSS.escape(`__render_frame_${id}__`);
103748
+ showSelectors.push(`#${renderEscaped}`, `#${renderEscaped} *`);
103749
+ }
103750
+ const massHideRule = "body *{visibility:hidden !important;}";
103751
+ const showRule = showSelectors.length === 0 ? "" : `${showSelectors.join(",")}{visibility:visible !important;}`;
103752
+ const style = document.createElement("style");
103753
+ style.id = args.styleId;
103754
+ style.textContent = `${massHideRule}
103755
+ ${showRule}`;
103756
+ document.head.appendChild(style);
103757
+ for (const id of args.hide) {
103758
+ const el = document.getElementById(id);
103759
+ if (el) {
103760
+ el.style.setProperty("visibility", "hidden", "important");
103761
+ }
103762
+ const img = document.getElementById(`__render_frame_${id}__`);
103763
+ if (img) {
103764
+ img.style.setProperty("visibility", "hidden", "important");
103765
+ }
103766
+ }
103767
+ },
103768
+ { show: showIds, hide: extraHideIds, styleId: DOM_LAYER_MASK_STYLE_ID }
103769
+ );
103770
+ }
103771
+ async function removeDomLayerMask(page, extraHideIds) {
103772
+ await page.evaluate(
103773
+ (args) => {
103774
+ const style = document.getElementById(args.styleId);
103775
+ if (style) style.remove();
103776
+ for (const id of args.hide) {
103777
+ const el = document.getElementById(id);
103778
+ if (el) {
103779
+ el.style.removeProperty("visibility");
103780
+ }
103781
+ const img = document.getElementById(`__render_frame_${id}__`);
103782
+ if (img) img.style.removeProperty("visibility");
103783
+ }
103784
+ },
103785
+ { hide: extraHideIds, styleId: DOM_LAYER_MASK_STYLE_ID }
103786
+ );
103787
+ }
103727
103788
  async function injectVideoFramesBatch(page, updates) {
103728
103789
  if (updates.length === 0) return;
103729
103790
  await page.evaluate(
@@ -103764,6 +103825,7 @@ async function injectVideoFramesBatch(page, updates) {
103764
103825
  img.style.objectPosition = computedStyle.objectPosition;
103765
103826
  img.style.zIndex = computedStyle.zIndex;
103766
103827
  for (const property of visualProperties) {
103828
+ if (property === "opacity") continue;
103767
103829
  if (sourceIsStatic && (property === "top" || property === "left" || property === "right" || property === "bottom" || property === "inset")) {
103768
103830
  continue;
103769
103831
  }
@@ -104667,6 +104729,10 @@ function isHdrColorSpace(cs) {
104667
104729
  if (!cs) return false;
104668
104730
  return cs.colorPrimaries.includes("bt2020") || cs.colorSpace.includes("bt2020") || cs.colorTransfer === "smpte2084" || cs.colorTransfer === "arib-std-b67";
104669
104731
  }
104732
+ function detectTransfer(cs) {
104733
+ if (cs?.colorTransfer === "smpte2084") return "pq";
104734
+ return "hlg";
104735
+ }
104670
104736
  var DEFAULT_HDR10_MASTERING = {
104671
104737
  masterDisplay: "G(13250,34500)B(7500,3000)R(34000,16000)WP(15635,16450)L(10000000,1)",
104672
104738
  maxCll: "1000,400"
@@ -105161,10 +105227,10 @@ import { finished } from "stream/promises";
105161
105227
  var downloadPathCache = /* @__PURE__ */ new Map();
105162
105228
  var inFlightDownloads = /* @__PURE__ */ new Map();
105163
105229
  function getFilenameFromUrl(url) {
105164
- const hash = createHash("md5").update(url).digest("hex").slice(0, 12);
105230
+ const hash2 = createHash("md5").update(url).digest("hex").slice(0, 12);
105165
105231
  const urlObj = new URL(url);
105166
105232
  const ext = extname2(urlObj.pathname) || ".mp4";
105167
- return `download_${hash}${ext}`;
105233
+ return `download_${hash2}${ext}`;
105168
105234
  }
105169
105235
  async function downloadToTemp(url, destDir, timeoutMs = 3e5) {
105170
105236
  const cachedPath = downloadPathCache.get(url);
@@ -105665,34 +105731,6 @@ function createVideoFrameInjector(frameLookup, config2) {
105665
105731
  }
105666
105732
  };
105667
105733
  }
105668
- async function hideVideoElements(page, videoIds) {
105669
- if (videoIds.length === 0) return;
105670
- await page.evaluate((ids) => {
105671
- for (const id of ids) {
105672
- const el = document.getElementById(id);
105673
- if (el) {
105674
- el.style.setProperty("visibility", "hidden", "important");
105675
- el.style.setProperty("opacity", "0", "important");
105676
- const img = document.getElementById(`__render_frame_${id}__`);
105677
- if (img) img.style.setProperty("visibility", "hidden", "important");
105678
- }
105679
- }
105680
- }, videoIds);
105681
- }
105682
- async function showVideoElements(page, videoIds) {
105683
- if (videoIds.length === 0) return;
105684
- await page.evaluate((ids) => {
105685
- for (const id of ids) {
105686
- const el = document.getElementById(id);
105687
- if (el) {
105688
- el.style.removeProperty("visibility");
105689
- el.style.removeProperty("opacity");
105690
- const img = document.getElementById(`__render_frame_${id}__`);
105691
- if (img) img.style.removeProperty("visibility");
105692
- }
105693
- }
105694
- }, videoIds);
105695
- }
105696
105734
  async function queryElementStacking(page, nativeHdrVideoIds) {
105697
105735
  const hdrIds = Array.from(nativeHdrVideoIds);
105698
105736
  return page.evaluate((hdrIdList) => {
@@ -105710,14 +105748,103 @@ async function queryElementStacking(page, nativeHdrVideoIds) {
105710
105748
  }
105711
105749
  return 0;
105712
105750
  }
105751
+ function getEffectiveBorderRadius(node) {
105752
+ function resolveRadius(value, el) {
105753
+ if (value.includes("%")) {
105754
+ const pct = parseFloat(value) / 100;
105755
+ const htmlEl = el;
105756
+ const w = htmlEl.offsetWidth || 0;
105757
+ const h = htmlEl.offsetHeight || 0;
105758
+ return pct * Math.min(w, h);
105759
+ }
105760
+ return parseFloat(value) || 0;
105761
+ }
105762
+ const selfCs = window.getComputedStyle(node);
105763
+ const selfRadii = [
105764
+ resolveRadius(selfCs.borderTopLeftRadius, node),
105765
+ resolveRadius(selfCs.borderTopRightRadius, node),
105766
+ resolveRadius(selfCs.borderBottomRightRadius, node),
105767
+ resolveRadius(selfCs.borderBottomLeftRadius, node)
105768
+ ];
105769
+ if (selfRadii[0] > 0 || selfRadii[1] > 0 || selfRadii[2] > 0 || selfRadii[3] > 0) {
105770
+ return selfRadii;
105771
+ }
105772
+ let current = node.parentElement;
105773
+ while (current) {
105774
+ const cs = window.getComputedStyle(current);
105775
+ if (cs.overflow !== "visible") {
105776
+ const tl = resolveRadius(cs.borderTopLeftRadius, current);
105777
+ const tr = resolveRadius(cs.borderTopRightRadius, current);
105778
+ const brr = resolveRadius(cs.borderBottomRightRadius, current);
105779
+ const bl = resolveRadius(cs.borderBottomLeftRadius, current);
105780
+ if (tl > 0 || tr > 0 || brr > 0 || bl > 0) {
105781
+ return [tl, tr, brr, bl];
105782
+ }
105783
+ }
105784
+ current = current.parentElement;
105785
+ }
105786
+ return [0, 0, 0, 0];
105787
+ }
105788
+ function getEffectiveOpacity(node) {
105789
+ let opacity = 1;
105790
+ let current = node;
105791
+ while (current) {
105792
+ const cs = window.getComputedStyle(current);
105793
+ const val = parseFloat(cs.opacity);
105794
+ opacity *= Number.isNaN(val) ? 1 : val;
105795
+ current = current.parentElement;
105796
+ }
105797
+ return opacity;
105798
+ }
105799
+ function getViewportMatrix(node) {
105800
+ const chain = [];
105801
+ let current = node;
105802
+ while (current instanceof HTMLElement) {
105803
+ chain.push(current);
105804
+ const next = current.offsetParent ?? current.parentElement;
105805
+ if (next === current) break;
105806
+ current = next;
105807
+ }
105808
+ let mat = new DOMMatrix();
105809
+ for (let i = chain.length - 1; i >= 0; i--) {
105810
+ const htmlEl = chain[i];
105811
+ if (!htmlEl) continue;
105812
+ mat = mat.translate(htmlEl.offsetLeft, htmlEl.offsetTop);
105813
+ const cs = window.getComputedStyle(htmlEl);
105814
+ if (cs.transform && cs.transform !== "none") {
105815
+ const origin = cs.transformOrigin.split(" ");
105816
+ const ox = resolveLength(origin[0] ?? "0", htmlEl.offsetWidth);
105817
+ const oy = resolveLength(origin[1] ?? "0", htmlEl.offsetHeight);
105818
+ try {
105819
+ const t = new DOMMatrix(cs.transform);
105820
+ 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)) {
105821
+ mat = mat.translate(ox, oy).multiply(t).translate(-ox, -oy);
105822
+ }
105823
+ } catch {
105824
+ }
105825
+ }
105826
+ }
105827
+ return mat.toString();
105828
+ }
105829
+ function resolveLength(value, basis) {
105830
+ if (value.endsWith("%")) {
105831
+ const pct = parseFloat(value) / 100;
105832
+ return Number.isFinite(pct) ? pct * basis : 0;
105833
+ }
105834
+ const n = parseFloat(value);
105835
+ return Number.isFinite(n) ? n : 0;
105836
+ }
105713
105837
  for (const el of elements) {
105714
105838
  const id = el.id;
105715
105839
  if (!id) continue;
105716
105840
  const rect = el.getBoundingClientRect();
105717
105841
  const style = window.getComputedStyle(el);
105718
105842
  const zIndex = getEffectiveZIndex(el);
105719
- const opacity = parseFloat(style.opacity) || 1;
105843
+ const isHdrEl = hdrSet.has(id);
105844
+ const opacityStartNode = isHdrEl ? el.parentElement : el;
105845
+ const opacity = opacityStartNode ? getEffectiveOpacity(opacityStartNode) : 1;
105720
105846
  const visible = style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0;
105847
+ const htmlEl = el instanceof HTMLElement ? el : null;
105721
105848
  results.push({
105722
105849
  id,
105723
105850
  zIndex,
@@ -105725,9 +105852,16 @@ async function queryElementStacking(page, nativeHdrVideoIds) {
105725
105852
  y: Math.round(rect.y),
105726
105853
  width: Math.round(rect.width),
105727
105854
  height: Math.round(rect.height),
105855
+ layoutWidth: htmlEl?.offsetWidth || Math.round(rect.width),
105856
+ layoutHeight: htmlEl?.offsetHeight || Math.round(rect.height),
105728
105857
  opacity,
105729
105858
  visible,
105730
- isHdr: hdrSet.has(id)
105859
+ isHdr: hdrSet.has(id),
105860
+ // For HDR elements, use the full accumulated viewport matrix so the
105861
+ // affine blit can apply rotation/scale/translate properly. For DOM
105862
+ // elements, the element-level transform is sufficient for reference.
105863
+ transform: isHdrEl ? getViewportMatrix(el) : style.transform || "none",
105864
+ borderRadius: isHdrEl ? getEffectiveBorderRadius(el) : [0, 0, 0, 0]
105731
105865
  });
105732
105866
  }
105733
105867
  return results;
@@ -106485,6 +106619,99 @@ function blitRgb48leRegion(canvas, source2, dx, dy, sw, sh, canvasWidth, canvasH
106485
106619
  }
106486
106620
  }
106487
106621
  }
106622
+ function blitRgb48leAffine(canvas, source2, matrix, srcW, srcH, canvasW, canvasH, opacity, borderRadius) {
106623
+ const a = matrix[0];
106624
+ const b = matrix[1];
106625
+ const c = matrix[2];
106626
+ const d = matrix[3];
106627
+ const tx = matrix[4];
106628
+ const ty = matrix[5];
106629
+ if (a === void 0 || b === void 0 || c === void 0 || d === void 0 || tx === void 0 || ty === void 0)
106630
+ return;
106631
+ const det = a * d - b * c;
106632
+ if (Math.abs(det) < 1e-10) return;
106633
+ const invA = d / det;
106634
+ const invB = -b / det;
106635
+ const invC = -c / det;
106636
+ const invD = a / det;
106637
+ const invTx = -(invA * tx + invC * ty);
106638
+ const invTy = -(invB * tx + invD * ty);
106639
+ const op = opacity ?? 1;
106640
+ const hasMask = borderRadius !== void 0;
106641
+ const corners = [
106642
+ [tx, ty],
106643
+ [a * srcW + tx, b * srcW + ty],
106644
+ [c * srcH + tx, d * srcH + ty],
106645
+ [a * srcW + c * srcH + tx, b * srcW + d * srcH + ty]
106646
+ ];
106647
+ let minX = canvasW, maxX = 0, minY = canvasH, maxY = 0;
106648
+ for (const corner of corners) {
106649
+ const cx = corner[0] ?? 0;
106650
+ const cy = corner[1] ?? 0;
106651
+ if (cx < minX) minX = cx;
106652
+ if (cx > maxX) maxX = cx;
106653
+ if (cy < minY) minY = cy;
106654
+ if (cy > maxY) maxY = cy;
106655
+ }
106656
+ const startX = Math.max(0, Math.floor(minX));
106657
+ const endX = Math.min(canvasW, Math.ceil(maxX));
106658
+ const startY = Math.max(0, Math.floor(minY));
106659
+ const endY = Math.min(canvasH, Math.ceil(maxY));
106660
+ for (let dy = startY; dy < endY; dy++) {
106661
+ for (let dx = startX; dx < endX; dx++) {
106662
+ const sx = invA * dx + invC * dy + invTx;
106663
+ const sy = invB * dx + invD * dy + invTy;
106664
+ if (sx < 0 || sy < 0 || sx >= srcW || sy >= srcH) continue;
106665
+ let effectiveOp = op;
106666
+ if (hasMask) {
106667
+ const ma = roundedRectAlpha(sx, sy, srcW, srcH, borderRadius);
106668
+ if (ma <= 0) continue;
106669
+ effectiveOp *= ma;
106670
+ }
106671
+ const x0 = Math.floor(sx);
106672
+ const y0 = Math.floor(sy);
106673
+ const fx = sx - x0;
106674
+ const fy = sy - y0;
106675
+ const x1 = Math.min(x0 + 1, srcW - 1);
106676
+ const y1 = Math.min(y0 + 1, srcH - 1);
106677
+ const off00 = (y0 * srcW + x0) * 6;
106678
+ const off10 = (y0 * srcW + x1) * 6;
106679
+ const off01 = (y1 * srcW + x0) * 6;
106680
+ const off11 = (y1 * srcW + x1) * 6;
106681
+ const w00 = (1 - fx) * (1 - fy);
106682
+ const w10 = fx * (1 - fy);
106683
+ const w01 = (1 - fx) * fy;
106684
+ const w11 = fx * fy;
106685
+ const sr = source2.readUInt16LE(off00) * w00 + source2.readUInt16LE(off10) * w10 + source2.readUInt16LE(off01) * w01 + source2.readUInt16LE(off11) * w11;
106686
+ const sg = source2.readUInt16LE(off00 + 2) * w00 + source2.readUInt16LE(off10 + 2) * w10 + source2.readUInt16LE(off01 + 2) * w01 + source2.readUInt16LE(off11 + 2) * w11;
106687
+ const sb = source2.readUInt16LE(off00 + 4) * w00 + source2.readUInt16LE(off10 + 4) * w10 + source2.readUInt16LE(off01 + 4) * w01 + source2.readUInt16LE(off11 + 4) * w11;
106688
+ const dstOff = (dy * canvasW + dx) * 6;
106689
+ if (effectiveOp >= 0.999) {
106690
+ canvas.writeUInt16LE(Math.round(sr), dstOff);
106691
+ canvas.writeUInt16LE(Math.round(sg), dstOff + 2);
106692
+ canvas.writeUInt16LE(Math.round(sb), dstOff + 4);
106693
+ } else {
106694
+ const invEff = 1 - effectiveOp;
106695
+ const dr = canvas.readUInt16LE(dstOff);
106696
+ const dg = canvas.readUInt16LE(dstOff + 2);
106697
+ const db = canvas.readUInt16LE(dstOff + 4);
106698
+ canvas.writeUInt16LE(Math.round(sr * effectiveOp + dr * invEff), dstOff);
106699
+ canvas.writeUInt16LE(Math.round(sg * effectiveOp + dg * invEff), dstOff + 2);
106700
+ canvas.writeUInt16LE(Math.round(sb * effectiveOp + db * invEff), dstOff + 4);
106701
+ }
106702
+ }
106703
+ }
106704
+ }
106705
+ function parseTransformMatrix(css) {
106706
+ if (!css || css === "none") return null;
106707
+ const match2 = css.match(
106708
+ /^matrix\(\s*([^,]+),\s*([^,]+),\s*([^,]+),\s*([^,]+),\s*([^,]+),\s*([^,)]+)\s*\)$/
106709
+ );
106710
+ if (!match2) return null;
106711
+ const values = match2.slice(1, 7).map(Number);
106712
+ if (!values.every(Number.isFinite)) return null;
106713
+ return values;
106714
+ }
106488
106715
 
106489
106716
  // ../engine/src/utils/layerCompositor.ts
106490
106717
  function groupIntoLayers(elements) {
@@ -106505,6 +106732,637 @@ function groupIntoLayers(elements) {
106505
106732
  return layers;
106506
106733
  }
106507
106734
 
106735
+ // ../engine/src/utils/shaderTransitions.ts
106736
+ var PQ_M1 = 0.1593017578125;
106737
+ var PQ_M2 = 78.84375;
106738
+ var PQ_C1 = 0.8359375;
106739
+ var PQ_C2 = 18.8515625;
106740
+ var PQ_C3 = 18.6875;
106741
+ function pqEotf(signal) {
106742
+ const sp = Math.pow(Math.max(0, signal), 1 / PQ_M2);
106743
+ const num = Math.max(sp - PQ_C1, 0);
106744
+ const den = PQ_C2 - PQ_C3 * sp;
106745
+ return den > 0 ? Math.pow(num / den, 1 / PQ_M1) : 0;
106746
+ }
106747
+ function pqOetf(linear) {
106748
+ const lp = Math.pow(Math.max(0, linear), PQ_M1);
106749
+ return Math.pow((PQ_C1 + PQ_C2 * lp) / (1 + PQ_C3 * lp), PQ_M2);
106750
+ }
106751
+ function hlgEotf(signal) {
106752
+ const a = 0.17883277;
106753
+ const b = 1 - 4 * a;
106754
+ const c = 0.5 - a * Math.log(4 * a);
106755
+ if (signal <= 0.5) {
106756
+ return signal * signal / 3;
106757
+ }
106758
+ return (Math.exp((signal - c) / a) + b) / 12;
106759
+ }
106760
+ function hlgOetf(linear) {
106761
+ const a = 0.17883277;
106762
+ const b = 1 - 4 * a;
106763
+ const c = 0.5 - a * Math.log(4 * a);
106764
+ if (linear <= 1 / 12) {
106765
+ return Math.sqrt(3 * linear);
106766
+ }
106767
+ return a * Math.log(12 * linear - b) + c;
106768
+ }
106769
+ function buildLut(fn) {
106770
+ const lut = new Uint16Array(65536);
106771
+ for (let i = 0; i < 65536; i++) {
106772
+ lut[i] = Math.round(fn(i / 65535) * 65535);
106773
+ }
106774
+ return lut;
106775
+ }
106776
+ var HLG_OOTF_LW = 1e3;
106777
+ var HLG_OOTF_GAMMA = 1.2 * Math.pow(1.111, Math.log2(HLG_OOTF_LW / 1e3));
106778
+ function hlgSceneToPqDisplay(sceneLinear) {
106779
+ const displayNits = HLG_OOTF_LW * Math.pow(Math.max(0, sceneLinear), HLG_OOTF_GAMMA);
106780
+ return displayNits / 1e4;
106781
+ }
106782
+ function pqDisplayToHlgScene(displayNormalized) {
106783
+ const displayNits = displayNormalized * 1e4;
106784
+ return Math.pow(Math.max(0, displayNits / HLG_OOTF_LW), 1 / HLG_OOTF_GAMMA);
106785
+ }
106786
+ var hlgToPqLut = null;
106787
+ var pqToHlgLut = null;
106788
+ function getHlgToPqLut() {
106789
+ if (!hlgToPqLut) hlgToPqLut = buildLut((v) => pqOetf(hlgSceneToPqDisplay(hlgEotf(v))));
106790
+ return hlgToPqLut;
106791
+ }
106792
+ function getPqToHlgLut() {
106793
+ if (!pqToHlgLut) pqToHlgLut = buildLut((v) => hlgOetf(pqDisplayToHlgScene(pqEotf(v))));
106794
+ return pqToHlgLut;
106795
+ }
106796
+ function convertTransfer(buf, from2, to) {
106797
+ if (from2 === to) return;
106798
+ const lut = from2 === "hlg" ? getHlgToPqLut() : getPqToHlgLut();
106799
+ const len = buf.length / 2;
106800
+ for (let i = 0; i < len; i++) {
106801
+ const off = i * 2;
106802
+ buf.writeUInt16LE(lut[buf.readUInt16LE(off)] ?? 0, off);
106803
+ }
106804
+ }
106805
+ function sampleRgb48le(buf, u, v, w, h) {
106806
+ const uc = Math.max(0, Math.min(1, u));
106807
+ const vc = Math.max(0, Math.min(1, v));
106808
+ const sx = uc * (w - 1);
106809
+ const sy = vc * (h - 1);
106810
+ const x0 = Math.floor(sx);
106811
+ const y0 = Math.floor(sy);
106812
+ const x1 = Math.min(x0 + 1, w - 1);
106813
+ const y1 = Math.min(y0 + 1, h - 1);
106814
+ const fx = sx - x0;
106815
+ const fy = sy - y0;
106816
+ const w00 = (1 - fx) * (1 - fy);
106817
+ const w10 = fx * (1 - fy);
106818
+ const w01 = (1 - fx) * fy;
106819
+ const w11 = fx * fy;
106820
+ const off00 = (y0 * w + x0) * 6;
106821
+ const off10 = (y0 * w + x1) * 6;
106822
+ const off01 = (y1 * w + x0) * 6;
106823
+ const off11 = (y1 * w + x1) * 6;
106824
+ const r = Math.round(
106825
+ buf.readUInt16LE(off00) * w00 + buf.readUInt16LE(off10) * w10 + buf.readUInt16LE(off01) * w01 + buf.readUInt16LE(off11) * w11
106826
+ );
106827
+ const g = Math.round(
106828
+ buf.readUInt16LE(off00 + 2) * w00 + buf.readUInt16LE(off10 + 2) * w10 + buf.readUInt16LE(off01 + 2) * w01 + buf.readUInt16LE(off11 + 2) * w11
106829
+ );
106830
+ const b = Math.round(
106831
+ buf.readUInt16LE(off00 + 4) * w00 + buf.readUInt16LE(off10 + 4) * w10 + buf.readUInt16LE(off01 + 4) * w01 + buf.readUInt16LE(off11 + 4) * w11
106832
+ );
106833
+ return [r, g, b];
106834
+ }
106835
+ function mix16(a, b, t) {
106836
+ return Math.round(a * (1 - t) + b * t);
106837
+ }
106838
+ function clamp16(v) {
106839
+ return Math.max(0, Math.min(65535, v));
106840
+ }
106841
+ function smoothstep(edge0, edge1, x) {
106842
+ const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
106843
+ return t * t * (3 - 2 * t);
106844
+ }
106845
+ function hash(x, y) {
106846
+ return (Math.sin(x * 127.1 + y * 311.7) * 43758.5453 % 1 + 1) % 1;
106847
+ }
106848
+ function vnoise(px, py) {
106849
+ const ix = Math.floor(px);
106850
+ const iy = Math.floor(py);
106851
+ let fx = px - ix;
106852
+ let fy = py - iy;
106853
+ fx = fx * fx * fx * (fx * (fx * 6 - 15) + 10);
106854
+ fy = fy * fy * fy * (fy * (fy * 6 - 15) + 10);
106855
+ const h00 = hash(ix, iy);
106856
+ const h10 = hash(ix + 1, iy);
106857
+ const h01 = hash(ix, iy + 1);
106858
+ const h11 = hash(ix + 1, iy + 1);
106859
+ return h00 * (1 - fx) * (1 - fy) + h10 * fx * (1 - fy) + h01 * (1 - fx) * fy + h11 * fx * fy;
106860
+ }
106861
+ var ROT_A = 0.8;
106862
+ var ROT_B = 0.6;
106863
+ function fbm(px, py) {
106864
+ let value = 0;
106865
+ let amplitude = 0.5;
106866
+ let x = px;
106867
+ let y = py;
106868
+ for (let i = 0; i < 5; i++) {
106869
+ value += amplitude * vnoise(x, y);
106870
+ const nx = ROT_A * x - ROT_B * y;
106871
+ const ny = ROT_B * x + ROT_A * y;
106872
+ x = nx * 2.02;
106873
+ y = ny * 2.02;
106874
+ amplitude *= 0.5;
106875
+ }
106876
+ return value;
106877
+ }
106878
+ var TRANSITIONS = {};
106879
+ var crossfade = (from2, to, out, w, h, p) => {
106880
+ const inv = 1 - p;
106881
+ for (let i = 0; i < w * h; i++) {
106882
+ const o = i * 6;
106883
+ out.writeUInt16LE(Math.round(from2.readUInt16LE(o) * inv + to.readUInt16LE(o) * p), o);
106884
+ out.writeUInt16LE(
106885
+ Math.round(from2.readUInt16LE(o + 2) * inv + to.readUInt16LE(o + 2) * p),
106886
+ o + 2
106887
+ );
106888
+ out.writeUInt16LE(
106889
+ Math.round(from2.readUInt16LE(o + 4) * inv + to.readUInt16LE(o + 4) * p),
106890
+ o + 4
106891
+ );
106892
+ }
106893
+ };
106894
+ TRANSITIONS["crossfade"] = crossfade;
106895
+ var flashThroughWhite = (from2, to, out, w, h, p) => {
106896
+ const toWhite = smoothstep(0, 0.45, p);
106897
+ const fromWhite = 1 - smoothstep(0.5, 1, p);
106898
+ const blend = smoothstep(0.35, 0.65, p);
106899
+ for (let i = 0; i < w * h; i++) {
106900
+ const o = i * 6;
106901
+ const fromR = mix16(from2.readUInt16LE(o), 65535, toWhite);
106902
+ const fromG = mix16(from2.readUInt16LE(o + 2), 65535, toWhite);
106903
+ const fromB = mix16(from2.readUInt16LE(o + 4), 65535, toWhite);
106904
+ const toR = mix16(to.readUInt16LE(o), 65535, fromWhite);
106905
+ const toG = mix16(to.readUInt16LE(o + 2), 65535, fromWhite);
106906
+ const toB = mix16(to.readUInt16LE(o + 4), 65535, fromWhite);
106907
+ out.writeUInt16LE(mix16(fromR, toR, blend), o);
106908
+ out.writeUInt16LE(mix16(fromG, toG, blend), o + 2);
106909
+ out.writeUInt16LE(mix16(fromB, toB, blend), o + 4);
106910
+ }
106911
+ };
106912
+ TRANSITIONS["flash-through-white"] = flashThroughWhite;
106913
+ var chromaticSplit = (from2, to, out, w, h, p) => {
106914
+ for (let i = 0; i < w * h; i++) {
106915
+ const ux = i % w / w;
106916
+ const uy = Math.floor(i / w) / h;
106917
+ const o = i * 6;
106918
+ const cx = ux - 0.5;
106919
+ const cy = uy - 0.5;
106920
+ const fromShift = p * 0.06;
106921
+ const fr = sampleRgb48le(from2, ux + cx * fromShift, uy + cy * fromShift, w, h)[0];
106922
+ const fg = sampleRgb48le(from2, ux, uy, w, h)[1];
106923
+ const fb = sampleRgb48le(from2, ux - cx * fromShift, uy - cy * fromShift, w, h)[2];
106924
+ const toShift = (1 - p) * 0.06;
106925
+ const tr = sampleRgb48le(to, ux - cx * toShift, uy - cy * toShift, w, h)[0];
106926
+ const tg = sampleRgb48le(to, ux, uy, w, h)[1];
106927
+ const tb = sampleRgb48le(to, ux + cx * toShift, uy + cy * toShift, w, h)[2];
106928
+ out.writeUInt16LE(clamp16(mix16(fr, tr, p)), o);
106929
+ out.writeUInt16LE(clamp16(mix16(fg, tg, p)), o + 2);
106930
+ out.writeUInt16LE(clamp16(mix16(fb, tb, p)), o + 4);
106931
+ }
106932
+ };
106933
+ TRANSITIONS["chromatic-split"] = chromaticSplit;
106934
+ var sdfIris = (from2, to, out, w, h, p) => {
106935
+ const accentBright = [65535, 55e3, 35e3];
106936
+ for (let i = 0; i < w * h; i++) {
106937
+ const ux = i % w / w;
106938
+ const uy = Math.floor(i / w) / h;
106939
+ const o = i * 6;
106940
+ const ax = (ux - 0.5) * (w / h);
106941
+ const ay = uy - 0.5;
106942
+ const d = Math.sqrt(ax * ax + ay * ay);
106943
+ const radius = p * 1.2;
106944
+ const fw = 3e-3;
106945
+ const edge = smoothstep(radius + fw, radius - fw, d);
106946
+ const ring1 = Math.exp(-Math.abs(d - radius) * 25);
106947
+ const ring2 = Math.exp(-Math.abs(d - radius + 0.04) * 20) * 0.5;
106948
+ const ring3 = Math.exp(-Math.abs(d - radius + 0.08) * 15) * 0.25;
106949
+ const glow = (ring1 + ring2 + ring3) * p * (1 - p) * 4;
106950
+ const [fromR, fromG, fromB] = sampleRgb48le(from2, ux, uy, w, h);
106951
+ const [toR, toG, toB] = sampleRgb48le(to, ux, uy, w, h);
106952
+ out.writeUInt16LE(clamp16(mix16(fromR, toR, edge) + accentBright[0] * glow * 0.6), o);
106953
+ out.writeUInt16LE(clamp16(mix16(fromG, toG, edge) + accentBright[1] * glow * 0.6), o + 2);
106954
+ out.writeUInt16LE(clamp16(mix16(fromB, toB, edge) + accentBright[2] * glow * 0.6), o + 4);
106955
+ }
106956
+ };
106957
+ TRANSITIONS["sdf-iris"] = sdfIris;
106958
+ function glitchRand(x, y) {
106959
+ return (Math.sin(x * 12.9898 + y * 78.233) * 43758.5453 % 1 + 1) % 1;
106960
+ }
106961
+ var glitch = (from2, to, out, w, h, p) => {
106962
+ const intensity = p * (1 - p) * 4;
106963
+ for (let i = 0; i < w * h; i++) {
106964
+ const ux = i % w / w;
106965
+ const uy = Math.floor(i / w) / h;
106966
+ const o = i * 6;
106967
+ const lineY = Math.floor(uy * 60) / 60;
106968
+ const lineDisp = (glitchRand(lineY, Math.floor(p * 17)) - 0.5) * 0.18 * intensity;
106969
+ const blockX = Math.floor(ux * 12);
106970
+ const blockY = Math.floor(uy * 8);
106971
+ const progressStep = Math.floor(p * 11);
106972
+ const br = glitchRand(blockX + progressStep, blockY + progressStep);
106973
+ const ba = (br >= 0.83 ? 1 : 0) * intensity;
106974
+ const bdx = (glitchRand(blockX * 2.1, blockY * 2.1) - 0.5) * 0.35 * ba;
106975
+ const bdy = (glitchRand(blockX * 3.7, blockY * 3.7) - 0.5) * 0.35 * ba;
106976
+ const uvx = Math.max(0, Math.min(1, ux + lineDisp + bdx));
106977
+ const uvy = Math.max(0, Math.min(1, uy + bdy));
106978
+ const shift = intensity * 0.035;
106979
+ const r = sampleRgb48le(from2, uvx + shift, uvy, w, h)[0];
106980
+ const g = sampleRgb48le(from2, uvx, uvy, w, h)[1];
106981
+ const b = sampleRgb48le(from2, uvx - shift, uvy, w, h)[2];
106982
+ let cr = r / 65535;
106983
+ let cg = g / 65535;
106984
+ let cb = b / 65535;
106985
+ const scanline = (uy * h * 0.5 % 1 + 1) % 1 >= 0.5 ? 0.05 * intensity : 0;
106986
+ cr -= scanline;
106987
+ cg -= scanline;
106988
+ cb -= scanline;
106989
+ const flicker = 1 + (glitchRand(Math.floor(p * 23), 0) - 0.5) * 0.3 * intensity;
106990
+ cr *= flicker;
106991
+ cg *= flicker;
106992
+ cb *= flicker;
106993
+ const levels = 256 - (256 - 8) * (intensity * 0.5);
106994
+ cr = Math.floor(cr * levels) / levels;
106995
+ cg = Math.floor(cg * levels) / levels;
106996
+ cb = Math.floor(cb * levels) / levels;
106997
+ const [toR, toG, toB] = sampleRgb48le(to, ux, uy, w, h);
106998
+ out.writeUInt16LE(clamp16(mix16(Math.round(cr * 65535), toR, p)), o);
106999
+ out.writeUInt16LE(clamp16(mix16(Math.round(cg * 65535), toG, p)), o + 2);
107000
+ out.writeUInt16LE(clamp16(mix16(Math.round(cb * 65535), toB, p)), o + 4);
107001
+ }
107002
+ };
107003
+ TRANSITIONS["glitch"] = glitch;
107004
+ function aces(x) {
107005
+ return Math.max(0, Math.min(1, x * (2.51 * x + 0.03) / (x * (2.43 * x + 0.59) + 0.14)));
107006
+ }
107007
+ var lightLeak = (from2, to, out, w, h, p) => {
107008
+ const accent = [5e4 / 65535, 25e3 / 65535, 5e3 / 65535];
107009
+ const accentBright = [65535 / 65535, 55e3 / 65535, 35e3 / 65535];
107010
+ const lpx = 1.3;
107011
+ const lpy = -0.2;
107012
+ for (let i = 0; i < w * h; i++) {
107013
+ const ux = i % w / w;
107014
+ const uy = Math.floor(i / w) / h;
107015
+ const o = i * 6;
107016
+ const dx = ux - lpx;
107017
+ const dy = uy - lpy;
107018
+ const dist = Math.sqrt(dx * dx + dy * dy);
107019
+ const leak = Math.max(0, Math.min(1, Math.exp(-dist * 1.8) * p * 4));
107020
+ const warmR = accent[0] + (accentBright[0] - accent[0]) * dist * 0.7;
107021
+ const warmG = accent[1] + (accentBright[1] - accent[1]) * dist * 0.7;
107022
+ const warmB = accent[2] + (accentBright[2] - accent[2]) * dist * 0.7;
107023
+ const flare = Math.exp(-Math.abs(uy - (-0.2 + ux * 0.3)) * 15) * leak * 0.3;
107024
+ const [fr, fg, fb] = sampleRgb48le(from2, ux, uy, w, h);
107025
+ const fromR = fr / 65535;
107026
+ const fromG = fg / 65535;
107027
+ const fromB = fb / 65535;
107028
+ const overR = aces(fromR + warmR * leak * 3 + accentBright[0] * flare);
107029
+ const overG = aces(fromG + warmG * leak * 3 + accentBright[1] * flare);
107030
+ const overB = aces(fromB + warmB * leak * 3 + accentBright[2] * flare);
107031
+ const [toR, toG, toB] = sampleRgb48le(to, ux, uy, w, h);
107032
+ const blend = smoothstep(0.15, 0.85, p);
107033
+ out.writeUInt16LE(clamp16(mix16(Math.round(overR * 65535), toR, blend)), o);
107034
+ out.writeUInt16LE(clamp16(mix16(Math.round(overG * 65535), toG, blend)), o + 2);
107035
+ out.writeUInt16LE(clamp16(mix16(Math.round(overB * 65535), toB, blend)), o + 4);
107036
+ }
107037
+ };
107038
+ TRANSITIONS["light-leak"] = lightLeak;
107039
+ var crossWarpMorph = (from2, to, out, w, h, p) => {
107040
+ for (let i = 0; i < w * h; i++) {
107041
+ const ux = i % w / w;
107042
+ const uy = Math.floor(i / w) / h;
107043
+ const o = i * 6;
107044
+ const dispX = fbm(ux * 3, uy * 3) - 0.5;
107045
+ const dispY = fbm(ux * 3 + 7.3, uy * 3 + 3.7) - 0.5;
107046
+ const fromUx = Math.max(0, Math.min(1, ux + dispX * p * 0.5));
107047
+ const fromUy = Math.max(0, Math.min(1, uy + dispY * p * 0.5));
107048
+ const toUx = Math.max(0, Math.min(1, ux - dispX * (1 - p) * 0.5));
107049
+ const toUy = Math.max(0, Math.min(1, uy - dispY * (1 - p) * 0.5));
107050
+ const [fromR, fromG, fromB] = sampleRgb48le(from2, fromUx, fromUy, w, h);
107051
+ const [toR, toG, toB] = sampleRgb48le(to, toUx, toUy, w, h);
107052
+ const n = fbm(ux * 4 + 3.1, uy * 4 + 1.7);
107053
+ const blend = smoothstep(0.4, 0.6, n + p * 1.2 - 0.6);
107054
+ out.writeUInt16LE(clamp16(mix16(fromR, toR, blend)), o);
107055
+ out.writeUInt16LE(clamp16(mix16(fromG, toG, blend)), o + 2);
107056
+ out.writeUInt16LE(clamp16(mix16(fromB, toB, blend)), o + 4);
107057
+ }
107058
+ };
107059
+ TRANSITIONS["cross-warp-morph"] = crossWarpMorph;
107060
+ var whipPan = (from2, to, out, w, h, p) => {
107061
+ const fromOff = p * 1.5;
107062
+ const toOff = (1 - p) * 1.5;
107063
+ for (let i = 0; i < w * h; i++) {
107064
+ const ux = i % w / w;
107065
+ const uy = Math.floor(i / w) / h;
107066
+ const o = i * 6;
107067
+ let fromR = 0, fromG = 0, fromB = 0;
107068
+ for (let s = 0; s < 10; s++) {
107069
+ const f = s / 10;
107070
+ const fuv = Math.max(0, Math.min(1, ux + fromOff + p * 0.08 * f));
107071
+ const [r, g, b] = sampleRgb48le(from2, fuv, uy, w, h);
107072
+ fromR += r;
107073
+ fromG += g;
107074
+ fromB += b;
107075
+ }
107076
+ fromR /= 10;
107077
+ fromG /= 10;
107078
+ fromB /= 10;
107079
+ let toR = 0, toG = 0, toB = 0;
107080
+ for (let s = 0; s < 10; s++) {
107081
+ const f = s / 10;
107082
+ const tuv = Math.max(0, Math.min(1, ux - toOff - (1 - p) * 0.08 * f));
107083
+ const [r, g, b] = sampleRgb48le(to, tuv, uy, w, h);
107084
+ toR += r;
107085
+ toG += g;
107086
+ toB += b;
107087
+ }
107088
+ toR /= 10;
107089
+ toG /= 10;
107090
+ toB /= 10;
107091
+ out.writeUInt16LE(clamp16(mix16(Math.round(fromR), Math.round(toR), p)), o);
107092
+ out.writeUInt16LE(clamp16(mix16(Math.round(fromG), Math.round(toG), p)), o + 2);
107093
+ out.writeUInt16LE(clamp16(mix16(Math.round(fromB), Math.round(toB), p)), o + 4);
107094
+ }
107095
+ };
107096
+ TRANSITIONS["whip-pan"] = whipPan;
107097
+ var cinematicZoom = (from2, to, out, w, h, p) => {
107098
+ const fromS = p * 0.08;
107099
+ const toS = (1 - p) * 0.06;
107100
+ for (let i = 0; i < w * h; i++) {
107101
+ const ux = i % w / w;
107102
+ const uy = Math.floor(i / w) / h;
107103
+ const o = i * 6;
107104
+ const dx = ux - 0.5;
107105
+ const dy = uy - 0.5;
107106
+ let fr = 0, fg = 0, fb = 0;
107107
+ for (let s = 0; s < 12; s++) {
107108
+ const f = s / 12;
107109
+ const rr = sampleRgb48le(
107110
+ from2,
107111
+ ux - dx * fromS * 1.06 * f,
107112
+ uy - dy * fromS * 1.06 * f,
107113
+ w,
107114
+ h
107115
+ )[0];
107116
+ const gg = sampleRgb48le(from2, ux - dx * fromS * f, uy - dy * fromS * f, w, h)[1];
107117
+ const bb = sampleRgb48le(
107118
+ from2,
107119
+ ux - dx * fromS * 0.94 * f,
107120
+ uy - dy * fromS * 0.94 * f,
107121
+ w,
107122
+ h
107123
+ )[2];
107124
+ fr += rr;
107125
+ fg += gg;
107126
+ fb += bb;
107127
+ }
107128
+ fr /= 12;
107129
+ fg /= 12;
107130
+ fb /= 12;
107131
+ let tr = 0, tg = 0, tb = 0;
107132
+ for (let s = 0; s < 12; s++) {
107133
+ const f = s / 12;
107134
+ const rr = sampleRgb48le(to, ux + dx * toS * 1.06 * f, uy + dy * toS * 1.06 * f, w, h)[0];
107135
+ const gg = sampleRgb48le(to, ux + dx * toS * f, uy + dy * toS * f, w, h)[1];
107136
+ const bb = sampleRgb48le(to, ux + dx * toS * 0.94 * f, uy + dy * toS * 0.94 * f, w, h)[2];
107137
+ tr += rr;
107138
+ tg += gg;
107139
+ tb += bb;
107140
+ }
107141
+ tr /= 12;
107142
+ tg /= 12;
107143
+ tb /= 12;
107144
+ out.writeUInt16LE(clamp16(mix16(Math.round(fr), Math.round(tr), p)), o);
107145
+ out.writeUInt16LE(clamp16(mix16(Math.round(fg), Math.round(tg), p)), o + 2);
107146
+ out.writeUInt16LE(clamp16(mix16(Math.round(fb), Math.round(tb), p)), o + 4);
107147
+ }
107148
+ };
107149
+ TRANSITIONS["cinematic-zoom"] = cinematicZoom;
107150
+ var gravitationalLens = (from2, to, out, w, h, p) => {
107151
+ for (let i = 0; i < w * h; i++) {
107152
+ const ux = i % w / w;
107153
+ const uy = Math.floor(i / w) / h;
107154
+ const o = i * 6;
107155
+ const uvx = ux - 0.5;
107156
+ const uvy = uy - 0.5;
107157
+ const dist = Math.sqrt(uvx * uvx + uvy * uvy);
107158
+ const pull = p * 2;
107159
+ const warpStr = pull * 0.3 / (dist + 0.1);
107160
+ const warpedX = Math.max(0, Math.min(1, ux - uvx * warpStr));
107161
+ const warpedY = Math.max(0, Math.min(1, uy - uvy * warpStr));
107162
+ const [, ag] = sampleRgb48le(from2, warpedX, warpedY, w, h);
107163
+ const horizon = smoothstep(0, 0.3, dist / (1 - p * 0.85 + 1e-3));
107164
+ const shift = pull * 0.02 / (dist + 0.2);
107165
+ const rSampX = Math.max(0, Math.min(1, ux - uvx * (warpStr + shift)));
107166
+ const rSampY = Math.max(0, Math.min(1, uy - uvy * (warpStr + shift)));
107167
+ const bSampX = Math.max(0, Math.min(1, ux - uvx * (warpStr - shift)));
107168
+ const bSampY = Math.max(0, Math.min(1, uy - uvy * (warpStr - shift)));
107169
+ const ar = sampleRgb48le(from2, rSampX, rSampY, w, h)[0];
107170
+ const ab = sampleRgb48le(from2, bSampX, bSampY, w, h)[2];
107171
+ const lensedR = Math.round(ar * horizon);
107172
+ const lensedG = Math.round(ag * horizon);
107173
+ const lensedB = Math.round(ab * horizon);
107174
+ const [toR, toG, toB] = sampleRgb48le(to, ux, uy, w, h);
107175
+ const blend = smoothstep(0.3, 0.9, p);
107176
+ out.writeUInt16LE(clamp16(mix16(lensedR, toR, blend)), o);
107177
+ out.writeUInt16LE(clamp16(mix16(lensedG, toG, blend)), o + 2);
107178
+ out.writeUInt16LE(clamp16(mix16(lensedB, toB, blend)), o + 4);
107179
+ }
107180
+ };
107181
+ TRANSITIONS["gravitational-lens"] = gravitationalLens;
107182
+ var rippleWaves = (from2, to, out, w, h, p) => {
107183
+ const accentBright = [65535, 55e3, 35e3];
107184
+ for (let i = 0; i < w * h; i++) {
107185
+ const ux = i % w / w;
107186
+ const uy = Math.floor(i / w) / h;
107187
+ const o = i * 6;
107188
+ const uvx = ux - 0.5;
107189
+ const uvy = uy - 0.5;
107190
+ const dist = Math.sqrt(uvx * uvx + uvy * uvy);
107191
+ const nux = uvx + 1e-3;
107192
+ const nuy = uvy + 1e-3;
107193
+ const nlen = Math.sqrt(nux * nux + nuy * nuy);
107194
+ const dirx = nux / nlen;
107195
+ const diry = nuy / nlen;
107196
+ const fromAmp = p * 0.04;
107197
+ const fw1 = Math.exp(Math.sin(dist * 25 - p * 12) - 1);
107198
+ const fw2 = Math.exp(Math.sin(dist * 50 - p * 18) - 1) * 0.5;
107199
+ const fromUx = Math.max(0, Math.min(1, ux + dirx * (fw1 + fw2) * fromAmp));
107200
+ const fromUy = Math.max(0, Math.min(1, uy + diry * (fw1 + fw2) * fromAmp));
107201
+ const toAmp = (1 - p) * 0.04;
107202
+ const tw1 = Math.exp(Math.sin(dist * 25 + p * 12) - 1);
107203
+ const tw2 = Math.exp(Math.sin(dist * 50 + p * 18) - 1) * 0.5;
107204
+ const toUx = Math.max(0, Math.min(1, ux - dirx * (tw1 + tw2) * toAmp));
107205
+ const toUy = Math.max(0, Math.min(1, uy - diry * (tw1 + tw2) * toAmp));
107206
+ const [fromR, fromG, fromB] = sampleRgb48le(from2, fromUx, fromUy, w, h);
107207
+ const [toR, toG, toB] = sampleRgb48le(to, toUx, toUy, w, h);
107208
+ const peak = fw1 * p;
107209
+ const tintR = accentBright[0] * peak * 0.1;
107210
+ const tintG = accentBright[1] * peak * 0.1;
107211
+ const tintB = accentBright[2] * peak * 0.1;
107212
+ out.writeUInt16LE(clamp16(mix16(Math.round(fromR + tintR), toR, p)), o);
107213
+ out.writeUInt16LE(clamp16(mix16(Math.round(fromG + tintG), toG, p)), o + 2);
107214
+ out.writeUInt16LE(clamp16(mix16(Math.round(fromB + tintB), toB, p)), o + 4);
107215
+ }
107216
+ };
107217
+ TRANSITIONS["ripple-waves"] = rippleWaves;
107218
+ var swirlVortex = (from2, to, out, w, h, p) => {
107219
+ for (let i = 0; i < w * h; i++) {
107220
+ const ux = i % w / w;
107221
+ const uy = Math.floor(i / w) / h;
107222
+ const o = i * 6;
107223
+ const uvx = ux - 0.5;
107224
+ const uvy = uy - 0.5;
107225
+ const dist = Math.sqrt(uvx * uvx + uvy * uvy);
107226
+ const warp = fbm(ux * 4, uy * 4) * 0.5;
107227
+ const fromAng = p * (1 - dist) * 10 + warp * p * 3;
107228
+ const fs8 = Math.sin(fromAng);
107229
+ const fc = Math.cos(fromAng);
107230
+ const fromUx = Math.max(0, Math.min(1, uvx * fc - uvy * fs8 + 0.5));
107231
+ const fromUy = Math.max(0, Math.min(1, uvx * fs8 + uvy * fc + 0.5));
107232
+ const toAng = -(1 - p) * (1 - dist) * 10 - warp * (1 - p) * 3;
107233
+ const ts = Math.sin(toAng);
107234
+ const tc = Math.cos(toAng);
107235
+ const toUx = Math.max(0, Math.min(1, uvx * tc - uvy * ts + 0.5));
107236
+ const toUy = Math.max(0, Math.min(1, uvx * ts + uvy * tc + 0.5));
107237
+ const [fromR, fromG, fromB] = sampleRgb48le(from2, fromUx, fromUy, w, h);
107238
+ const [toR, toG, toB] = sampleRgb48le(to, toUx, toUy, w, h);
107239
+ out.writeUInt16LE(clamp16(mix16(fromR, toR, p)), o);
107240
+ out.writeUInt16LE(clamp16(mix16(fromG, toG, p)), o + 2);
107241
+ out.writeUInt16LE(clamp16(mix16(fromB, toB, p)), o + 4);
107242
+ }
107243
+ };
107244
+ TRANSITIONS["swirl-vortex"] = swirlVortex;
107245
+ var thermalDistortion = (from2, to, out, w, h, p) => {
107246
+ const accentBright = [65535, 55e3, 35e3];
107247
+ for (let i = 0; i < w * h; i++) {
107248
+ const ux = i % w / w;
107249
+ const uy = Math.floor(i / w) / h;
107250
+ const o = i * 6;
107251
+ const heat = p * 1.5;
107252
+ const yFade = smoothstep(1, 0, uy);
107253
+ const shimmer = Math.sin(uy * 40 + fbm(ux * 6, uy * 6) * 8) * fbm(ux * 3 + 0, uy * 3 + p * 2);
107254
+ const dispX = shimmer * heat * 0.03 * yFade;
107255
+ const fromUx = Math.max(0, Math.min(1, ux + dispX));
107256
+ const [fromR, fromG, fromB] = sampleRgb48le(from2, fromUx, uy, w, h);
107257
+ const invShimmer = Math.sin(uy * 40 + fbm(ux * 6 + 3, uy * 6 + 3) * 8) * fbm(ux * 3 + 3, uy * 3 + p * 2);
107258
+ const dispX2 = invShimmer * (1 - p) * 0.03 * yFade;
107259
+ const toUx = Math.max(0, Math.min(1, ux + dispX2));
107260
+ const [toR, toG, toB] = sampleRgb48le(to, toUx, uy, w, h);
107261
+ const haze = heat * yFade * 0.15 * (1 - p);
107262
+ out.writeUInt16LE(clamp16(mix16(fromR, toR, p) + Math.round(accentBright[0] * haze)), o);
107263
+ out.writeUInt16LE(clamp16(mix16(fromG, toG, p) + Math.round(accentBright[1] * haze)), o + 2);
107264
+ out.writeUInt16LE(clamp16(mix16(fromB, toB, p) + Math.round(accentBright[2] * haze)), o + 4);
107265
+ }
107266
+ };
107267
+ TRANSITIONS["thermal-distortion"] = thermalDistortion;
107268
+ var domainWarp = (from2, to, out, w, h, p) => {
107269
+ const accentDark = [25e3, 8e3, 2e3];
107270
+ const accentBright = [65535, 55e3, 35e3];
107271
+ for (let i = 0; i < w * h; i++) {
107272
+ const ux = i % w / w;
107273
+ const uy = Math.floor(i / w) / h;
107274
+ const o = i * 6;
107275
+ const qx = fbm(ux * 3, uy * 3);
107276
+ const qy = fbm(ux * 3 + 5.2, uy * 3 + 1.3);
107277
+ const rx = fbm(ux * 3 + qx * 4 + 1.7, uy * 3 + qy * 4 + 9.2);
107278
+ const ry = fbm(ux * 3 + qx * 4 + 8.3, uy * 3 + qy * 4 + 2.8);
107279
+ const n = fbm(ux * 3 + rx * 2, uy * 3 + ry * 2);
107280
+ const warpDirX = (qx - 0.5) * 0.4;
107281
+ const warpDirY = (qy - 0.5) * 0.4;
107282
+ const aUx = Math.max(0, Math.min(1, ux + warpDirX * p));
107283
+ const aUy = Math.max(0, Math.min(1, uy + warpDirY * p));
107284
+ const bUx = Math.max(0, Math.min(1, ux - warpDirX * (1 - p)));
107285
+ const bUy = Math.max(0, Math.min(1, uy - warpDirY * (1 - p)));
107286
+ const [aR, aG, aB] = sampleRgb48le(from2, aUx, aUy, w, h);
107287
+ const [bR, bG, bB] = sampleRgb48le(to, bUx, bUy, w, h);
107288
+ const e = smoothstep(p - 0.08, p + 0.08, n);
107289
+ const ed = Math.abs(n - p);
107290
+ const pStep = p >= 1 ? 1 : 0;
107291
+ const em = smoothstep(0.1, 0, ed) * (1 - pStep);
107292
+ const ecBlend = smoothstep(0, 0.1, ed);
107293
+ const ecR = accentDark[0] + (accentBright[0] - accentDark[0]) * (1 - ecBlend);
107294
+ const ecG = accentDark[1] + (accentBright[1] - accentDark[1]) * (1 - ecBlend);
107295
+ const ecB = accentDark[2] + (accentBright[2] - accentDark[2]) * (1 - ecBlend);
107296
+ out.writeUInt16LE(clamp16(mix16(bR, aR, e) + Math.round(ecR * em * 2)), o);
107297
+ out.writeUInt16LE(clamp16(mix16(bG, aG, e) + Math.round(ecG * em * 2)), o + 2);
107298
+ out.writeUInt16LE(clamp16(mix16(bB, aB, e) + Math.round(ecB * em * 2)), o + 4);
107299
+ }
107300
+ };
107301
+ TRANSITIONS["domain-warp"] = domainWarp;
107302
+ function ridged(px, py) {
107303
+ let value = 0;
107304
+ let amplitude = 0.5;
107305
+ let x = px;
107306
+ let y = py;
107307
+ for (let i = 0; i < 5; i++) {
107308
+ value += amplitude * Math.abs(vnoise(x, y) * 2 - 1);
107309
+ const nx = ROT_A * x - ROT_B * y;
107310
+ const ny = ROT_B * x + ROT_A * y;
107311
+ x = nx * 2.02;
107312
+ y = ny * 2.02;
107313
+ amplitude *= 0.5;
107314
+ }
107315
+ return value;
107316
+ }
107317
+ var ridgedBurn = (from2, to, out, w, h, p) => {
107318
+ const accent = [5e4, 25e3, 5e3];
107319
+ const accentDark = [25e3, 8e3, 2e3];
107320
+ const accentBright = [65535, 55e3, 35e3];
107321
+ for (let i = 0; i < w * h; i++) {
107322
+ const ux = i % w / w;
107323
+ const uy = Math.floor(i / w) / h;
107324
+ const o = i * 6;
107325
+ const [aR, aG, aB] = sampleRgb48le(from2, ux, uy, w, h);
107326
+ const [bR, bG, bB] = sampleRgb48le(to, ux, uy, w, h);
107327
+ const n = ridged(ux * 4, uy * 4);
107328
+ const e = smoothstep(p - 0.04, p + 0.04, n);
107329
+ const heat = smoothstep(0.12, 0, Math.abs(n - p));
107330
+ const pStep = p >= 1 ? 1 : 0;
107331
+ const heatMasked = heat * (1 - pStep);
107332
+ let burnR = accentDark[0] + (accent[0] - accentDark[0]) * smoothstep(0, 0.25, heatMasked);
107333
+ let burnG = accentDark[1] + (accent[1] - accentDark[1]) * smoothstep(0, 0.25, heatMasked);
107334
+ let burnB = accentDark[2] + (accent[2] - accentDark[2]) * smoothstep(0, 0.25, heatMasked);
107335
+ const blend2 = smoothstep(0.25, 0.5, heatMasked);
107336
+ burnR = burnR + (accentBright[0] - burnR) * blend2;
107337
+ burnG = burnG + (accentBright[1] - burnG) * blend2;
107338
+ burnB = burnB + (accentBright[2] - burnB) * blend2;
107339
+ const blend3 = smoothstep(0.5, 1, heatMasked);
107340
+ burnR = burnR + (65535 - burnR) * blend3;
107341
+ burnG = burnG + (65535 - burnG) * blend3;
107342
+ burnB = burnB + (65535 - burnB) * blend3;
107343
+ const sparks = (vnoise(ux * 80, uy * 80) >= 0.92 ? 1 : 0) * heatMasked * 3;
107344
+ out.writeUInt16LE(
107345
+ clamp16(
107346
+ mix16(bR, aR, e) + Math.round(burnR * heatMasked * 3.5) + Math.round(accentBright[0] * sparks)
107347
+ ),
107348
+ o
107349
+ );
107350
+ out.writeUInt16LE(
107351
+ clamp16(
107352
+ mix16(bG, aG, e) + Math.round(burnG * heatMasked * 3.5) + Math.round(accentBright[1] * sparks)
107353
+ ),
107354
+ o + 2
107355
+ );
107356
+ out.writeUInt16LE(
107357
+ clamp16(
107358
+ mix16(bB, aB, e) + Math.round(burnB * heatMasked * 3.5) + Math.round(accentBright[2] * sparks)
107359
+ ),
107360
+ o + 4
107361
+ );
107362
+ }
107363
+ };
107364
+ TRANSITIONS["ridged-burn"] = ridgedBurn;
107365
+
106508
107366
  // src/services/renderOrchestrator.ts
106509
107367
  import { join as join15, dirname as dirname10, resolve as resolve10 } from "path";
106510
107368
  import { randomUUID } from "crypto";
@@ -108352,6 +109210,63 @@ function applyRenderModeHints(cfg, compiled, log = defaultLogger) {
108352
109210
  reasons: compiled.renderModeHints.reasons.map((reason) => reason.message)
108353
109211
  });
108354
109212
  }
109213
+ function blitHdrVideoLayer(canvas, el, time, fps, hdrFrameDirs, hdrStartTimes, width, height, log, sourceTransfer, targetTransfer) {
109214
+ const frameDir = hdrFrameDirs.get(el.id);
109215
+ const startTime = hdrStartTimes.get(el.id);
109216
+ if (!frameDir || startTime === void 0) {
109217
+ return;
109218
+ }
109219
+ const videoFrameIndex = Math.round((time - startTime) * fps) + 1;
109220
+ if (videoFrameIndex < 1) return;
109221
+ const maxIndex = getMaxFrameIndex(frameDir);
109222
+ const effectiveIndex = maxIndex > 0 ? Math.min(videoFrameIndex, maxIndex) : videoFrameIndex;
109223
+ const framePath = join15(frameDir, `frame_${String(effectiveIndex).padStart(4, "0")}.png`);
109224
+ if (!existsSync15(framePath)) {
109225
+ return;
109226
+ }
109227
+ try {
109228
+ const { data: hdrRgb, width: srcW, height: srcH } = decodePngToRgb48le(readFileSync9(framePath));
109229
+ if (sourceTransfer && targetTransfer && sourceTransfer !== targetTransfer) {
109230
+ convertTransfer(hdrRgb, sourceTransfer, targetTransfer);
109231
+ }
109232
+ const viewportMatrix = parseTransformMatrix(el.transform);
109233
+ const br = el.borderRadius;
109234
+ const hasBorderRadius = br[0] > 0 || br[1] > 0 || br[2] > 0 || br[3] > 0;
109235
+ const borderRadiusParam = hasBorderRadius ? br : void 0;
109236
+ if (viewportMatrix) {
109237
+ blitRgb48leAffine(
109238
+ canvas,
109239
+ hdrRgb,
109240
+ viewportMatrix,
109241
+ srcW,
109242
+ srcH,
109243
+ width,
109244
+ height,
109245
+ el.opacity < 0.999 ? el.opacity : void 0,
109246
+ borderRadiusParam
109247
+ );
109248
+ } else {
109249
+ blitRgb48leRegion(
109250
+ canvas,
109251
+ hdrRgb,
109252
+ el.x,
109253
+ el.y,
109254
+ srcW,
109255
+ srcH,
109256
+ width,
109257
+ height,
109258
+ el.opacity < 0.999 ? el.opacity : void 0,
109259
+ borderRadiusParam
109260
+ );
109261
+ }
109262
+ } catch (err) {
109263
+ if (log) {
109264
+ log.debug(`HDR blit failed for ${el.id}`, {
109265
+ error: err instanceof Error ? err.message : String(err)
109266
+ });
109267
+ }
109268
+ }
109269
+ }
108355
109270
  function createRenderJob(config2) {
108356
109271
  return {
108357
109272
  id: randomUUID(),
@@ -108622,6 +109537,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108622
109537
  perfStages.browserProbeMs = Date.now() - probeStart;
108623
109538
  job.duration = composition.duration;
108624
109539
  job.totalFrames = Math.ceil(composition.duration * job.config.fps);
109540
+ const totalFrames = job.totalFrames;
108625
109541
  if (job.duration <= 0) {
108626
109542
  const diagnostics = [];
108627
109543
  try {
@@ -108679,7 +109595,8 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108679
109595
  const compiledDir = join15(workDir, "compiled");
108680
109596
  let extractionResult = null;
108681
109597
  const nativeHdrVideoIds = /* @__PURE__ */ new Set();
108682
- if (composition.videos.length > 0) {
109598
+ const videoTransfers = /* @__PURE__ */ new Map();
109599
+ if (job.config.hdr && composition.videos.length > 0) {
108683
109600
  await Promise.all(
108684
109601
  composition.videos.map(async (v) => {
108685
109602
  let videoPath = v.src;
@@ -108691,6 +109608,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108691
109608
  const meta = await extractVideoMetadata(videoPath);
108692
109609
  if (isHdrColorSpace(meta.colorSpace)) {
108693
109610
  nativeHdrVideoIds.add(v.id);
109611
+ videoTransfers.set(v.id, detectTransfer(meta.colorSpace));
108694
109612
  }
108695
109613
  })
108696
109614
  );
@@ -108732,13 +109650,19 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108732
109650
  perfStages.videoExtractMs = Date.now() - stage2Start;
108733
109651
  }
108734
109652
  let effectiveHdr;
108735
- if (frameLookup) {
109653
+ if (job.config.hdr && frameLookup) {
108736
109654
  const colorSpaces = (extractionResult?.extracted ?? []).map((ext) => ext.metadata.colorSpace);
108737
109655
  const info = analyzeCompositionHdr(colorSpaces);
108738
109656
  if (info.hasHdr && info.dominantTransfer) {
108739
109657
  effectiveHdr = { transfer: info.dominantTransfer };
108740
109658
  }
108741
109659
  }
109660
+ if (job.config.hdr && !effectiveHdr && nativeHdrVideoIds.size > 0) {
109661
+ const firstTransfer = videoTransfers.values().next().value;
109662
+ if (firstTransfer) {
109663
+ effectiveHdr = { transfer: firstTransfer };
109664
+ }
109665
+ }
108742
109666
  if (effectiveHdr && outputFormat !== "mp4") {
108743
109667
  log.info(`[Render] HDR source detected but format is ${outputFormat} \u2014 using SDR`);
108744
109668
  effectiveHdr = void 0;
@@ -108789,15 +109713,15 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108789
109713
  format: needsAlpha ? "png" : "jpeg",
108790
109714
  quality: needsAlpha ? void 0 : job.config.quality === "draft" ? 80 : 95
108791
109715
  };
108792
- const workerCount = calculateOptimalWorkers(job.totalFrames, job.config.workers, cfg);
109716
+ const workerCount = calculateOptimalWorkers(totalFrames, job.config.workers, cfg);
108793
109717
  const FORMAT_EXT = { mp4: ".mp4", webm: ".webm", mov: ".mov" };
108794
109718
  const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
108795
109719
  const videoOnlyPath = join15(workDir, `video-only${videoExt}`);
108796
- const hasHdrVideo = effectiveHdr && composition.videos.length > 0 && frameLookup;
108797
- const encoderHdr = hasHdrVideo ? effectiveHdr : void 0;
109720
+ const hasHdrContent = effectiveHdr && nativeHdrVideoIds.size > 0;
109721
+ const encoderHdr = hasHdrContent ? effectiveHdr : void 0;
108798
109722
  const preset = getEncoderPreset(job.config.quality, outputFormat, encoderHdr);
108799
109723
  job.framesRendered = 0;
108800
- if (hasHdrVideo) {
109724
+ if (hasHdrContent) {
108801
109725
  log.info("[Render] HDR layered composite: z-ordered DOM + native HLG video layers");
108802
109726
  const hdrVideoIds = composition.videos.filter((v) => nativeHdrVideoIds.has(v.id)).map((v) => v.id);
108803
109727
  const hdrVideoSrcPaths = /* @__PURE__ */ new Map();
@@ -108810,6 +109734,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108810
109734
  }
108811
109735
  hdrVideoSrcPaths.set(v.id, srcPath);
108812
109736
  }
109737
+ if (!fileServer) throw new Error("fileServer must be initialized before HDR compositing");
108813
109738
  const domSession = await createCaptureSession(
108814
109739
  fileServer.url,
108815
109740
  framesDir,
@@ -108821,6 +109746,34 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108821
109746
  assertNotAborted();
108822
109747
  lastBrowserConsole = domSession.browserConsoleBuffer;
108823
109748
  await initTransparentBackground(domSession.page);
109749
+ const transitionMeta = await domSession.page.evaluate(() => {
109750
+ return window.__hf?.transitions ?? [];
109751
+ });
109752
+ const sceneElements = await domSession.page.evaluate(() => {
109753
+ const scenes = document.querySelectorAll(".scene");
109754
+ const map2 = {};
109755
+ for (const scene of scenes) {
109756
+ const els = scene.querySelectorAll("[data-start]");
109757
+ map2[scene.id] = Array.from(els).map((e) => e.id);
109758
+ }
109759
+ return map2;
109760
+ });
109761
+ const transitionRanges = transitionMeta.map((t) => ({
109762
+ ...t,
109763
+ startFrame: Math.floor(t.time * job.config.fps),
109764
+ endFrame: Math.ceil((t.time + t.duration) * job.config.fps)
109765
+ }));
109766
+ if (transitionRanges.length > 0) {
109767
+ log.info("[Render] Detected shader transitions for HDR compositing", {
109768
+ count: transitionRanges.length,
109769
+ transitions: transitionRanges.map((t) => ({
109770
+ shader: t.shader,
109771
+ from: t.fromScene,
109772
+ to: t.toScene,
109773
+ frames: `${t.startFrame}-${t.endFrame}`
109774
+ }))
109775
+ });
109776
+ }
108824
109777
  const hdrEncoder = await spawnStreamingEncoder(
108825
109778
  videoOnlyPath,
108826
109779
  {
@@ -108838,7 +109791,28 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108838
109791
  { ffmpegStreamingTimeout: 36e5 }
108839
109792
  );
108840
109793
  assertNotAborted();
108841
- const { execSync: execSync2 } = await import("child_process");
109794
+ const hdrExtractionDims = /* @__PURE__ */ new Map();
109795
+ const hdrVideoStartTimes = /* @__PURE__ */ new Map();
109796
+ for (const v of composition.videos) {
109797
+ if (hdrVideoIds.includes(v.id)) {
109798
+ hdrVideoStartTimes.set(v.id, v.start);
109799
+ }
109800
+ }
109801
+ const uniqueStartTimes = [...new Set(hdrVideoStartTimes.values())].sort((a, b) => a - b);
109802
+ for (const seekTime of uniqueStartTimes) {
109803
+ await domSession.page.evaluate((t) => {
109804
+ if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
109805
+ }, seekTime);
109806
+ if (domSession.onBeforeCapture) {
109807
+ await domSession.onBeforeCapture(domSession.page, seekTime);
109808
+ }
109809
+ const stacking = await queryElementStacking(domSession.page, nativeHdrVideoIds);
109810
+ for (const el of stacking) {
109811
+ if (el.isHdr && el.layoutWidth > 0 && el.layoutHeight > 0 && !hdrExtractionDims.has(el.id)) {
109812
+ hdrExtractionDims.set(el.id, { width: el.layoutWidth, height: el.layoutHeight });
109813
+ }
109814
+ }
109815
+ }
108842
109816
  const hdrFrameDirs = /* @__PURE__ */ new Map();
108843
109817
  for (const [videoId, srcPath] of hdrVideoSrcPaths) {
108844
109818
  const video = composition.videos.find((v) => v.id === videoId);
@@ -108846,24 +109820,190 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108846
109820
  const frameDir = join15(framesDir, `hdr_${videoId}`);
108847
109821
  mkdirSync10(frameDir, { recursive: true });
108848
109822
  const duration = video.end - video.start;
108849
- try {
108850
- execSync2(
108851
- `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")}"`,
108852
- { maxBuffer: 1024 * 1024, stdio: ["pipe", "pipe", "pipe"] }
108853
- );
108854
- } catch (err) {
109823
+ const dims = hdrExtractionDims.get(videoId) ?? { width, height };
109824
+ const ffmpegArgs = [
109825
+ "-ss",
109826
+ String(video.mediaStart),
109827
+ "-i",
109828
+ srcPath,
109829
+ "-t",
109830
+ String(duration),
109831
+ "-r",
109832
+ String(job.config.fps),
109833
+ "-vf",
109834
+ `scale=${dims.width}:${dims.height}:force_original_aspect_ratio=increase,crop=${dims.width}:${dims.height}`,
109835
+ "-pix_fmt",
109836
+ "rgb48le",
109837
+ "-c:v",
109838
+ "png",
109839
+ "-y",
109840
+ join15(frameDir, "frame_%04d.png")
109841
+ ];
109842
+ const result = await runFfmpeg(ffmpegArgs, { signal: abortSignal });
109843
+ if (!result.success) {
108855
109844
  log.warn("HDR frame pre-extraction failed; loop will fill with black", {
108856
109845
  videoId,
108857
109846
  srcPath,
108858
- error: err instanceof Error ? err.message : String(err)
109847
+ stderr: result.stderr.slice(-400)
108859
109848
  });
108860
109849
  }
108861
109850
  hdrFrameDirs.set(videoId, frameDir);
108862
109851
  }
108863
109852
  assertNotAborted();
108864
109853
  try {
109854
+ let countNonZeroAlpha2 = function(rgba) {
109855
+ let n = 0;
109856
+ for (let p = 3; p < rgba.length; p += 4) {
109857
+ if (rgba[p] !== 0) n++;
109858
+ }
109859
+ return n;
109860
+ }, countNonZeroRgb482 = function(buf) {
109861
+ let n = 0;
109862
+ for (let p = 0; p < buf.length; p += 6) {
109863
+ if (buf[p] !== 0 || buf[p + 1] !== 0 || buf[p + 2] !== 0) n++;
109864
+ }
109865
+ return n;
109866
+ };
109867
+ var countNonZeroAlpha = countNonZeroAlpha2, countNonZeroRgb48 = countNonZeroRgb482;
108865
109868
  const beforeCaptureHook = domSession.onBeforeCapture;
108866
- for (let i = 0; i < job.totalFrames; i++) {
109869
+ const cleanedUpVideos = /* @__PURE__ */ new Set();
109870
+ const hdrVideoEndTimes = /* @__PURE__ */ new Map();
109871
+ for (const v of composition.videos) {
109872
+ if (hdrFrameDirs.has(v.id)) {
109873
+ hdrVideoEndTimes.set(v.id, v.end);
109874
+ }
109875
+ }
109876
+ const debugDumpEnabled = process.env.KEEP_TEMP === "1";
109877
+ const debugDumpDir = debugDumpEnabled ? join15(framesDir, "debug-composite") : null;
109878
+ if (debugDumpDir && !existsSync15(debugDumpDir)) {
109879
+ mkdirSync10(debugDumpDir, { recursive: true });
109880
+ }
109881
+ async function compositeToBuffer(canvas, time, fullStacking, elementFilter, debugFrameIndex = -1) {
109882
+ const filteredStacking = elementFilter ? fullStacking.filter((e) => elementFilter.has(e.id)) : fullStacking;
109883
+ const layers = groupIntoLayers(filteredStacking);
109884
+ const shouldLog = debugDumpEnabled && debugFrameIndex >= 0;
109885
+ if (shouldLog) {
109886
+ log.info("[diag] compositeToBuffer plan", {
109887
+ frame: debugFrameIndex,
109888
+ time: time.toFixed(3),
109889
+ filterSize: elementFilter?.size,
109890
+ fullStackingCount: fullStacking.length,
109891
+ filteredCount: filteredStacking.length,
109892
+ layerCount: layers.length,
109893
+ layers: layers.map(
109894
+ (l) => l.type === "hdr" ? {
109895
+ type: "hdr",
109896
+ id: l.element.id,
109897
+ z: l.element.zIndex,
109898
+ visible: l.element.visible,
109899
+ opacity: l.element.opacity,
109900
+ bounds: `${Math.round(l.element.x)},${Math.round(l.element.y)} ${Math.round(l.element.width)}x${Math.round(l.element.height)}`
109901
+ } : { type: "dom", ids: l.elementIds }
109902
+ )
109903
+ });
109904
+ }
109905
+ for (let layerIdx = 0; layerIdx < layers.length; layerIdx++) {
109906
+ const layer = layers[layerIdx];
109907
+ if (layer.type === "hdr") {
109908
+ const before2 = shouldLog ? countNonZeroRgb482(canvas) : 0;
109909
+ blitHdrVideoLayer(
109910
+ canvas,
109911
+ layer.element,
109912
+ time,
109913
+ job.config.fps,
109914
+ hdrFrameDirs,
109915
+ hdrVideoStartTimes,
109916
+ width,
109917
+ height,
109918
+ log,
109919
+ videoTransfers.get(layer.element.id),
109920
+ effectiveHdr?.transfer
109921
+ );
109922
+ if (shouldLog) {
109923
+ const after2 = countNonZeroRgb482(canvas);
109924
+ const frameDir = hdrFrameDirs.get(layer.element.id);
109925
+ const startTime = hdrVideoStartTimes.get(layer.element.id) ?? 0;
109926
+ const localTime = time - startTime;
109927
+ const frameNum = Math.floor(localTime * job.config.fps) + 1;
109928
+ const expectedFrame = frameDir ? join15(frameDir, `frame_${String(frameNum).padStart(4, "0")}.png`) : null;
109929
+ log.info("[diag] hdr layer blit", {
109930
+ frame: debugFrameIndex,
109931
+ layerIdx,
109932
+ id: layer.element.id,
109933
+ pixelsAdded: after2 - before2,
109934
+ totalNonZero: after2,
109935
+ startTime,
109936
+ localTime: localTime.toFixed(3),
109937
+ hdrFrameNum: frameNum,
109938
+ expectedFrame,
109939
+ expectedFrameExists: expectedFrame ? existsSync15(expectedFrame) : false
109940
+ });
109941
+ }
109942
+ } else {
109943
+ const allElementIds = fullStacking.map((e) => e.id);
109944
+ const layerIds = new Set(layer.elementIds);
109945
+ const hideIds = allElementIds.filter((id) => !layerIds.has(id));
109946
+ await domSession.page.evaluate((t) => {
109947
+ if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
109948
+ }, time);
109949
+ if (beforeCaptureHook) {
109950
+ await beforeCaptureHook(domSession.page, time);
109951
+ }
109952
+ await applyDomLayerMask(domSession.page, layer.elementIds, hideIds);
109953
+ const domPng = await captureAlphaPng(domSession.page, width, height);
109954
+ await removeDomLayerMask(domSession.page, hideIds);
109955
+ try {
109956
+ const { data: domRgba } = decodePng(domPng);
109957
+ if (!effectiveHdr) {
109958
+ throw new Error(
109959
+ "Invariant violation: effectiveHdr is undefined inside HDR layer branch"
109960
+ );
109961
+ }
109962
+ const before2 = shouldLog ? countNonZeroRgb482(canvas) : 0;
109963
+ const alphaPixels = shouldLog ? countNonZeroAlpha2(domRgba) : 0;
109964
+ blitRgba8OverRgb48le(domRgba, canvas, width, height, effectiveHdr.transfer);
109965
+ if (shouldLog && debugDumpDir) {
109966
+ const after2 = countNonZeroRgb482(canvas);
109967
+ const dumpName = `frame_${String(debugFrameIndex).padStart(4, "0")}_layer_${String(layerIdx).padStart(2, "0")}_dom.png`;
109968
+ const dumpPath = join15(debugDumpDir, dumpName);
109969
+ writeFileSync4(dumpPath, domPng);
109970
+ log.info("[diag] dom layer blit", {
109971
+ frame: debugFrameIndex,
109972
+ layerIdx,
109973
+ layerIds: layer.elementIds,
109974
+ hideCount: hideIds.length,
109975
+ pngBytes: domPng.length,
109976
+ alphaPixels,
109977
+ pixelsAdded: after2 - before2,
109978
+ totalNonZero: after2,
109979
+ dumpPath
109980
+ });
109981
+ }
109982
+ } catch (err) {
109983
+ log.warn("DOM layer decode/blit failed; skipping overlay", {
109984
+ layerIds: layer.elementIds,
109985
+ error: err instanceof Error ? err.message : String(err)
109986
+ });
109987
+ }
109988
+ }
109989
+ }
109990
+ if (shouldLog && debugDumpDir) {
109991
+ const finalNonZero = countNonZeroRgb482(canvas);
109992
+ log.info("[diag] compositeToBuffer end", {
109993
+ frame: debugFrameIndex,
109994
+ finalNonZeroPixels: finalNonZero,
109995
+ totalPixels: width * height,
109996
+ coverage: (finalNonZero / (width * height) * 100).toFixed(1) + "%"
109997
+ });
109998
+ }
109999
+ }
110000
+ const bufSize = width * height * 6;
110001
+ const hasTransitions = transitionRanges.length > 0;
110002
+ const transBufferA = hasTransitions ? Buffer.alloc(bufSize) : null;
110003
+ const transBufferB = hasTransitions ? Buffer.alloc(bufSize) : null;
110004
+ const transOutput = hasTransitions ? Buffer.alloc(bufSize) : null;
110005
+ const normalCanvas = Buffer.alloc(bufSize);
110006
+ for (let i = 0; i < totalFrames; i++) {
108867
110007
  assertNotAborted();
108868
110008
  const time = i / job.config.fps;
108869
110009
  await domSession.page.evaluate((t) => {
@@ -108873,81 +110013,118 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108873
110013
  await beforeCaptureHook(domSession.page, time);
108874
110014
  }
108875
110015
  const stackingInfo = await queryElementStacking(domSession.page, nativeHdrVideoIds);
108876
- const layers = groupIntoLayers(stackingInfo);
110016
+ const activeTransition = transitionRanges.find(
110017
+ (t) => i >= t.startFrame && i <= t.endFrame
110018
+ );
108877
110019
  if (i % 30 === 0) {
108878
110020
  const hdrEl = stackingInfo.find((e) => e.isHdr);
108879
- const hdrInLayers = layers.some((l) => l.type === "hdr");
108880
110021
  log.debug("[Render] HDR layer composite frame", {
108881
110022
  frame: i,
108882
110023
  time: time.toFixed(2),
108883
110024
  hdrElement: hdrEl ? { z: hdrEl.zIndex, visible: hdrEl.visible, width: hdrEl.width } : null,
108884
- hdrLayerPresent: hdrInLayers,
108885
- layerCount: layers.length
110025
+ stackingCount: stackingInfo.length,
110026
+ activeTransition: activeTransition?.shader
108886
110027
  });
108887
110028
  }
108888
- const canvas = Buffer.alloc(width * height * 6);
108889
- for (const layer of layers) {
108890
- if (layer.type === "hdr") {
108891
- const el = layer.element;
108892
- const frameDir = hdrFrameDirs.get(el.id);
108893
- const video = composition.videos.find((v) => v.id === el.id);
108894
- if (!frameDir || !video) continue;
108895
- const videoFrameIndex = Math.round((time - video.start) * job.config.fps) + 1;
108896
- const maxIndex = getMaxFrameIndex(frameDir);
108897
- const inBounds = videoFrameIndex >= 1 && (maxIndex === 0 || videoFrameIndex <= maxIndex);
108898
- const framePath = inBounds ? join15(frameDir, `frame_${String(videoFrameIndex).padStart(4, "0")}.png`) : null;
108899
- if (framePath !== null && existsSync15(framePath)) {
108900
- try {
108901
- const hdrRgb = decodePngToRgb48le(readFileSync9(framePath)).data;
108902
- blitRgb48leRegion(
108903
- canvas,
108904
- hdrRgb,
108905
- el.x,
108906
- el.y,
108907
- el.width,
108908
- el.height,
108909
- width,
108910
- height,
108911
- el.opacity < 0.999 ? el.opacity : void 0
108912
- );
108913
- } catch (err) {
108914
- log.warn("HDR layer decode/blit failed; skipping layer for frame", {
108915
- frameIndex: i,
108916
- videoId: el.id,
108917
- framePath,
108918
- error: err instanceof Error ? err.message : String(err)
108919
- });
108920
- }
108921
- }
108922
- } else {
108923
- const allElementIds = stackingInfo.map((e) => e.id);
108924
- const layerIds = new Set(layer.elementIds);
108925
- const hideIds = allElementIds.filter(
108926
- (id) => !layerIds.has(id) || nativeHdrVideoIds.has(id)
108927
- );
108928
- await hideVideoElements(domSession.page, hideIds);
108929
- const domPng = await captureAlphaPng(domSession.page, width, height);
108930
- await showVideoElements(domSession.page, hideIds);
110029
+ if (activeTransition && transBufferA && transBufferB && transOutput) {
110030
+ const progress = activeTransition.endFrame === activeTransition.startFrame ? 1 : (i - activeTransition.startFrame) / (activeTransition.endFrame - activeTransition.startFrame);
110031
+ const sceneAIds = new Set(sceneElements[activeTransition.fromScene] ?? []);
110032
+ const sceneBIds = new Set(sceneElements[activeTransition.toScene] ?? []);
110033
+ transBufferA.fill(0);
110034
+ transBufferB.fill(0);
110035
+ for (const [sceneBuf, sceneIds] of [
110036
+ [transBufferA, sceneAIds],
110037
+ [transBufferB, sceneBIds]
110038
+ ]) {
108931
110039
  await domSession.page.evaluate((t) => {
108932
110040
  if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
108933
110041
  }, time);
110042
+ if (beforeCaptureHook) {
110043
+ await beforeCaptureHook(domSession.page, time);
110044
+ }
110045
+ for (const el of stackingInfo) {
110046
+ if (!el.isHdr || !sceneIds.has(el.id)) continue;
110047
+ blitHdrVideoLayer(
110048
+ sceneBuf,
110049
+ el,
110050
+ time,
110051
+ job.config.fps,
110052
+ hdrFrameDirs,
110053
+ hdrVideoStartTimes,
110054
+ width,
110055
+ height,
110056
+ log,
110057
+ videoTransfers.get(el.id),
110058
+ effectiveHdr?.transfer
110059
+ );
110060
+ }
110061
+ const showIds = Array.from(sceneIds);
110062
+ const hideIds = stackingInfo.map((e) => e.id).filter((id) => !sceneIds.has(id) || nativeHdrVideoIds.has(id));
110063
+ await applyDomLayerMask(domSession.page, showIds, hideIds);
110064
+ const domPng = await captureAlphaPng(domSession.page, width, height);
110065
+ await removeDomLayerMask(domSession.page, hideIds);
108934
110066
  try {
108935
110067
  const { data: domRgba } = decodePng(domPng);
108936
- const hdrTransfer = effectiveHdr ? effectiveHdr.transfer : "hlg";
108937
- blitRgba8OverRgb48le(domRgba, canvas, width, height, hdrTransfer);
110068
+ if (!effectiveHdr) {
110069
+ throw new Error(
110070
+ "Invariant violation: effectiveHdr is undefined inside hasHdrVideo branch"
110071
+ );
110072
+ }
110073
+ blitRgba8OverRgb48le(
110074
+ domRgba,
110075
+ sceneBuf,
110076
+ width,
110077
+ height,
110078
+ effectiveHdr.transfer
110079
+ );
108938
110080
  } catch (err) {
108939
- log.warn("DOM layer decode/blit failed; skipping overlay for frame", {
110081
+ log.warn("DOM layer decode/blit failed; skipping overlay for transition scene", {
108940
110082
  frameIndex: i,
108941
- layerIds: layer.elementIds,
110083
+ sceneIds: Array.from(sceneIds),
108942
110084
  error: err instanceof Error ? err.message : String(err)
108943
110085
  });
108944
110086
  }
108945
110087
  }
110088
+ const transitionFn = TRANSITIONS[activeTransition.shader] ?? crossfade;
110089
+ transitionFn(transBufferA, transBufferB, transOutput, width, height, progress);
110090
+ hdrEncoder.writeFrame(transOutput);
110091
+ } else {
110092
+ normalCanvas.fill(0);
110093
+ await compositeToBuffer(normalCanvas, time, stackingInfo, void 0, i);
110094
+ if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
110095
+ const previewPath = join15(
110096
+ debugDumpDir,
110097
+ `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`
110098
+ );
110099
+ writeFileSync4(previewPath, normalCanvas);
110100
+ }
110101
+ hdrEncoder.writeFrame(normalCanvas);
110102
+ }
110103
+ if (process.env.KEEP_TEMP !== "1") {
110104
+ for (const [videoId, endTime] of hdrVideoEndTimes) {
110105
+ if (time > endTime && !cleanedUpVideos.has(videoId)) {
110106
+ const stillNeeded = activeTransition && (sceneElements[activeTransition.fromScene]?.includes(videoId) || sceneElements[activeTransition.toScene]?.includes(videoId));
110107
+ if (!stillNeeded) {
110108
+ const frameDir = hdrFrameDirs.get(videoId);
110109
+ if (frameDir) {
110110
+ try {
110111
+ rmSync3(frameDir, { recursive: true, force: true });
110112
+ } catch (err) {
110113
+ log.warn("Failed to clean up HDR frame directory", {
110114
+ videoId,
110115
+ frameDir,
110116
+ error: err instanceof Error ? err.message : String(err)
110117
+ });
110118
+ }
110119
+ }
110120
+ cleanedUpVideos.add(videoId);
110121
+ }
110122
+ }
110123
+ }
108946
110124
  }
108947
- hdrEncoder.writeFrame(canvas);
108948
110125
  job.framesRendered = i + 1;
108949
- if ((i + 1) % 10 === 0 || i + 1 === job.totalFrames) {
108950
- const frameProgress = (i + 1) / job.totalFrames;
110126
+ if ((i + 1) % 10 === 0 || i + 1 === totalFrames) {
110127
+ const frameProgress = (i + 1) / totalFrames;
108951
110128
  updateJobStatus(
108952
110129
  job,
108953
110130
  "rendering",
@@ -108990,7 +110167,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108990
110167
  assertNotAborted();
108991
110168
  }
108992
110169
  if (enableStreamingEncode && streamingEncoder) {
108993
- const reorderBuffer = createFrameReorderBuffer(0, job.totalFrames);
110170
+ const reorderBuffer = createFrameReorderBuffer(0, totalFrames);
108994
110171
  const currentEncoder = streamingEncoder;
108995
110172
  if (workerCount > 1) {
108996
110173
  const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
@@ -109047,7 +110224,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109047
110224
  }
109048
110225
  assertNotAborted();
109049
110226
  lastBrowserConsole = session.browserConsoleBuffer;
109050
- for (let i = 0; i < job.totalFrames; i++) {
110227
+ for (let i = 0; i < totalFrames; i++) {
109051
110228
  assertNotAborted();
109052
110229
  const time = i / job.config.fps;
109053
110230
  const { buffer } = await captureFrameToBuffer(session, i, time);
@@ -109055,7 +110232,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109055
110232
  currentEncoder.writeFrame(buffer);
109056
110233
  reorderBuffer.advanceTo(i + 1);
109057
110234
  job.framesRendered = i + 1;
109058
- const frameProgress = (i + 1) / job.totalFrames;
110235
+ const frameProgress = (i + 1) / totalFrames;
109059
110236
  const progress = 25 + frameProgress * 55;
109060
110237
  updateJobStatus(
109061
110238
  job,
@@ -109228,12 +110405,12 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109228
110405
  chunkedEncode: enableChunkedEncode,
109229
110406
  chunkSizeFrames: enableChunkedEncode ? chunkedEncodeSize : null,
109230
110407
  compositionDurationSeconds: composition.duration,
109231
- totalFrames: job.totalFrames,
110408
+ totalFrames,
109232
110409
  resolution: { width, height },
109233
110410
  videoCount: composition.videos.length,
109234
110411
  audioCount: composition.audios.length,
109235
110412
  stages: perfStages,
109236
- captureAvgMs: job.totalFrames > 0 ? Math.round((perfStages.captureMs ?? 0) / job.totalFrames) : void 0
110413
+ captureAvgMs: totalFrames > 0 ? Math.round((perfStages.captureMs ?? 0) / totalFrames) : void 0
109237
110414
  };
109238
110415
  job.perfSummary = perfSummary;
109239
110416
  if (job.config.debug) {
@@ -109251,6 +110428,8 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109251
110428
  const debugOutput = join15(workDir, `output${videoExt}`);
109252
110429
  copyFileSync2(outputPath, debugOutput);
109253
110430
  }
110431
+ } else if (process.env.KEEP_TEMP === "1") {
110432
+ log.info("KEEP_TEMP=1 \u2014 leaving workDir on disk for inspection", { workDir });
109254
110433
  } else {
109255
110434
  await safeCleanup(
109256
110435
  "remove workDir",