@remotion/web-renderer 4.0.489 → 4.0.491

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @remotion/web-renderer
2
2
 
3
- Render videos in the browser (not yet released)
3
+ Render videos in the browser
4
4
 
5
5
  [![NPM Downloads](https://img.shields.io/npm/dm/@remotion/web-renderer.svg?style=flat&color=black&label=Downloads)](https://npmcharts.com/compare/@remotion/web-renderer?minimal=true)
6
6
 
@@ -7,7 +7,7 @@ export type ErrorHolder = {
7
7
  error: Error | null;
8
8
  };
9
9
  export declare function checkForError(errorHolder: ErrorHolder): void;
10
- export declare function createScaffold<Props extends Record<string, unknown>>({ width, height, delayRenderTimeoutInMilliseconds, logLevel, resolvedProps, id, mediaCacheSizeInBytes, durationInFrames, fps, initialFrame, schema, Component, audioEnabled, videoEnabled, defaultCodec, defaultOutName, allowHtmlInCanvas, pixelDensity }: {
10
+ export declare function createScaffold<Props extends Record<string, unknown>>({ width, height, delayRenderTimeoutInMilliseconds, logLevel, resolvedProps, id, mediaCacheSizeInBytes, durationInFrames, fps, initialFrame, schema, Component, audioEnabled, videoEnabled, defaultCodec, defaultOutName, useHtmlInCanvas, pixelDensity }: {
11
11
  width: number;
12
12
  height: number;
13
13
  delayRenderTimeoutInMilliseconds: number;
@@ -24,7 +24,7 @@ export declare function createScaffold<Props extends Record<string, unknown>>({
24
24
  videoEnabled: boolean;
25
25
  defaultCodec: Codec | null;
26
26
  defaultOutName: string | null;
27
- allowHtmlInCanvas: boolean;
27
+ useHtmlInCanvas: boolean;
28
28
  pixelDensity: number;
29
29
  }): {
30
30
  delayRenderScope: DelayRenderScope;
@@ -1,7 +1,10 @@
1
1
  import type { LinearGradientInfo } from './parse-linear-gradient';
2
- export declare const calculateTransforms: ({ element, rootElement, }: {
2
+ type TransformStyle = Pick<CSSStyleDeclaration, 'display' | 'rotate' | 'scale' | 'transform' | 'transformOrigin'>;
3
+ export type TransformStyleCache = WeakMap<Element, TransformStyle>;
4
+ export declare const calculateTransforms: ({ element, rootElement, transformStyleCache, }: {
3
5
  element: HTMLElement | SVGElement;
4
6
  rootElement: HTMLElement | SVGElement;
7
+ transformStyleCache: TransformStyleCache;
5
8
  }) => {
6
9
  dimensions: DOMRect;
7
10
  totalMatrix: DOMMatrix;
@@ -20,3 +23,4 @@ export declare const calculateTransforms: ({ element, rootElement, }: {
20
23
  needsPrecompositing: boolean;
21
24
  };
22
25
  };
26
+ export {};
@@ -1,4 +1,4 @@
1
- export declare const hasTransformCssValue: (style: CSSStyleDeclaration) => boolean;
2
- export declare const hasRotateCssValue: (style: CSSStyleDeclaration) => boolean;
3
- export declare const hasScaleCssValue: (style: CSSStyleDeclaration) => boolean;
4
- export declare const hasAnyTransformCssValue: (style: CSSStyleDeclaration) => boolean;
1
+ export declare const hasTransformCssValue: (style: Pick<CSSStyleDeclaration, "transform">) => boolean;
2
+ export declare const hasRotateCssValue: (style: Pick<CSSStyleDeclaration, "rotate">) => boolean;
3
+ export declare const hasScaleCssValue: (style: Pick<CSSStyleDeclaration, "scale">) => boolean;
4
+ export declare const hasAnyTransformCssValue: (style: Pick<CSSStyleDeclaration, "rotate" | "scale" | "transform">) => boolean;
@@ -1,3 +1,4 @@
1
+ import { type TransformStyleCache } from './calculate-transforms';
1
2
  import type { DrawFn } from './drawn-fn';
2
3
  export type ProcessNodeReturnValue = {
3
4
  type: 'continue';
@@ -5,7 +6,7 @@ export type ProcessNodeReturnValue = {
5
6
  } | {
6
7
  type: 'skip-children';
7
8
  };
8
- export declare const processNode: ({ element, context, draw, logLevel, parentRect, internalState, rootElement, scale, waitForPageResponsiveness, }: {
9
+ export declare const processNode: ({ element, context, draw, logLevel, parentRect, internalState, rootElement, scale, waitForPageResponsiveness, transformStyleCache, }: {
9
10
  element: HTMLElement | SVGElement;
10
11
  context: OffscreenCanvasRenderingContext2D;
11
12
  draw: DrawFn;
@@ -32,4 +33,5 @@ export declare const processNode: ({ element, context, draw, logLevel, parentRec
32
33
  rootElement: HTMLElement | SVGElement;
33
34
  scale: number;
34
35
  waitForPageResponsiveness: (() => Promise<void>) | null;
36
+ transformStyleCache: TransformStyleCache;
35
37
  }) => Promise<ProcessNodeReturnValue>;
@@ -1,5 +1,6 @@
1
+ import type { TransformStyleCache } from '../calculate-transforms';
1
2
  import type { ProcessNodeReturnValue } from '../process-node';
2
- export declare const handleTextNode: ({ node, context, logLevel, parentRect, internalState, rootElement, onlyBackgroundClipText, scale, waitForPageResponsiveness, }: {
3
+ export declare const handleTextNode: ({ node, context, logLevel, parentRect, internalState, rootElement, onlyBackgroundClipText, scale, waitForPageResponsiveness, transformStyleCache, }: {
3
4
  node: Text;
4
5
  context: OffscreenCanvasRenderingContext2D;
5
6
  logLevel: "error" | "info" | "trace" | "verbose" | "warn";
@@ -26,4 +27,5 @@ export declare const handleTextNode: ({ node, context, logLevel, parentRect, int
26
27
  onlyBackgroundClipText: boolean;
27
28
  scale: number;
28
29
  waitForPageResponsiveness: (() => Promise<void>) | null;
30
+ transformStyleCache: TransformStyleCache;
29
31
  }) => Promise<ProcessNodeReturnValue>;
@@ -824,6 +824,8 @@ import ReactDOM from "react-dom/client";
824
824
  import { Internals as Internals3 } from "remotion";
825
825
 
826
826
  // src/html-in-canvas.ts
827
+ var nestedHtmlInCanvasSupport = null;
828
+ var forceDisableHtmlInCanvasForTesting = false;
827
829
  var supportsNativeHtmlInCanvas = () => {
828
830
  if (typeof document === "undefined") {
829
831
  return false;
@@ -831,6 +833,121 @@ var supportsNativeHtmlInCanvas = () => {
831
833
  const ctx = document.createElement("canvas").getContext("2d");
832
834
  return typeof ctx?.drawElementImage === "function";
833
835
  };
836
+ var runNestedHtmlInCanvasProbe = () => {
837
+ if (!supportsNativeHtmlInCanvas() || !document.body) {
838
+ return Promise.resolve(false);
839
+ }
840
+ const outer = document.createElement("canvas");
841
+ const inner = document.createElement("canvas");
842
+ const outerTarget = document.createElement("div");
843
+ const innerTarget = document.createElement("div");
844
+ const outerCtx = outer.getContext("2d");
845
+ const innerCtx = inner.getContext("2d");
846
+ if (!outerCtx || !innerCtx || typeof outer.requestPaint !== "function" || typeof inner.requestPaint !== "function") {
847
+ return Promise.resolve(false);
848
+ }
849
+ outer.layoutSubtree = true;
850
+ inner.layoutSubtree = true;
851
+ outer.width = 4;
852
+ outer.height = 4;
853
+ inner.width = 4;
854
+ inner.height = 4;
855
+ Object.assign(outer.style, {
856
+ display: "block",
857
+ height: "4px",
858
+ left: "0",
859
+ pointerEvents: "none",
860
+ position: "fixed",
861
+ top: "0",
862
+ visibility: "visible",
863
+ width: "4px",
864
+ zIndex: "2147483647"
865
+ });
866
+ Object.assign(inner.style, {
867
+ display: "block",
868
+ height: "4px",
869
+ width: "4px"
870
+ });
871
+ Object.assign(outerTarget.style, {
872
+ display: "block",
873
+ height: "4px",
874
+ width: "4px"
875
+ });
876
+ Object.assign(innerTarget.style, {
877
+ backgroundColor: "rgb(255, 0, 0)",
878
+ display: "block",
879
+ height: "4px",
880
+ width: "4px"
881
+ });
882
+ inner.appendChild(innerTarget);
883
+ outerTarget.appendChild(inner);
884
+ outer.appendChild(outerTarget);
885
+ document.body.appendChild(outer);
886
+ return new Promise((resolve) => {
887
+ let innerPainted = false;
888
+ let settled = false;
889
+ let timeout = null;
890
+ const settle = (supported) => {
891
+ if (settled) {
892
+ return;
893
+ }
894
+ settled = true;
895
+ if (timeout !== null) {
896
+ window.clearTimeout(timeout);
897
+ }
898
+ outer.remove();
899
+ resolve(supported);
900
+ };
901
+ timeout = window.setTimeout(() => settle(false), 1000);
902
+ inner.addEventListener("paint", () => {
903
+ try {
904
+ innerCtx.reset();
905
+ const transform = innerCtx.drawElementImage(innerTarget, 0, 0, 4, 4);
906
+ innerTarget.style.transform = transform.toString();
907
+ innerPainted = true;
908
+ requestAnimationFrame(() => outer.requestPaint());
909
+ } catch {
910
+ settle(false);
911
+ }
912
+ });
913
+ outer.addEventListener("paint", () => {
914
+ if (!innerPainted) {
915
+ return;
916
+ }
917
+ try {
918
+ outerCtx.reset();
919
+ if (typeof outer.captureElementImage !== "function") {
920
+ settle(false);
921
+ return;
922
+ }
923
+ const elementImage = outer.captureElementImage(outerTarget);
924
+ const transform = outerCtx.drawElementImage(elementImage, 0, 0, 4, 4);
925
+ elementImage.close();
926
+ outerTarget.style.transform = transform.toString();
927
+ const pixel = outerCtx.getImageData(2, 2, 1, 1).data;
928
+ settle(pixel[0] > 200 && pixel[1] < 20 && pixel[2] < 20 && pixel[3] > 200);
929
+ } catch {
930
+ settle(false);
931
+ }
932
+ });
933
+ inner.requestPaint();
934
+ });
935
+ };
936
+ var supportsNestedHtmlInCanvas = () => {
937
+ if (forceDisableHtmlInCanvasForTesting) {
938
+ return Promise.resolve(false);
939
+ }
940
+ if (!nestedHtmlInCanvasSupport) {
941
+ nestedHtmlInCanvasSupport = runNestedHtmlInCanvasProbe();
942
+ }
943
+ return nestedHtmlInCanvasSupport;
944
+ };
945
+ var countLayoutSubtreeCanvases = (element) => {
946
+ return Array.from(element.querySelectorAll("canvas")).filter((canvas) => canvas.layoutSubtree === true).length;
947
+ };
948
+ var containsLayoutSubtreeCanvas = (element) => {
949
+ return countLayoutSubtreeCanvases(element) > 0;
950
+ };
834
951
  var setupHtmlInCanvas = ({
835
952
  wrapper,
836
953
  div,
@@ -872,14 +989,36 @@ var drawWithHtmlInCanvas = async ({
872
989
  htmlInCanvasContext,
873
990
  element,
874
991
  scaledWidth,
875
- scaledHeight
992
+ scaledHeight,
993
+ waitForRenderReady,
994
+ useElementImage
876
995
  }) => {
877
996
  const { ctx, layoutCanvas } = htmlInCanvasContext;
997
+ if (layoutCanvas.width !== scaledWidth || layoutCanvas.height !== scaledHeight) {
998
+ layoutCanvas.width = scaledWidth;
999
+ layoutCanvas.height = scaledHeight;
1000
+ }
878
1001
  await waitForPaint(layoutCanvas);
879
- layoutCanvas.width = scaledWidth;
880
- layoutCanvas.height = scaledHeight;
1002
+ const nestedPaintCycles = useElementImage ? countLayoutSubtreeCanvases(element) * 2 + 1 : 0;
1003
+ for (let i = 0;i < nestedPaintCycles; i++) {
1004
+ await new Promise((resolve) => requestAnimationFrame(() => resolve()));
1005
+ await waitForRenderReady();
1006
+ await waitForPaint(layoutCanvas);
1007
+ }
881
1008
  ctx.reset();
882
- ctx.drawElementImage(element, 0, 0, scaledWidth, scaledHeight);
1009
+ if (useElementImage) {
1010
+ if (typeof layoutCanvas.captureElementImage !== "function") {
1011
+ throw new Error("canvas.captureElementImage() is unavailable");
1012
+ }
1013
+ const elementImage = layoutCanvas.captureElementImage(element);
1014
+ try {
1015
+ ctx.drawElementImage(elementImage, 0, 0, scaledWidth, scaledHeight);
1016
+ } finally {
1017
+ elementImage.close();
1018
+ }
1019
+ } else {
1020
+ ctx.drawElementImage(element, 0, 0, scaledWidth, scaledHeight);
1021
+ }
883
1022
  const offscreen = new OffscreenCanvas(scaledWidth, scaledHeight);
884
1023
  const offCtx = offscreen.getContext("2d");
885
1024
  if (!offCtx) {
@@ -1020,7 +1159,7 @@ function createScaffold({
1020
1159
  videoEnabled,
1021
1160
  defaultCodec,
1022
1161
  defaultOutName,
1023
- allowHtmlInCanvas,
1162
+ useHtmlInCanvas,
1024
1163
  pixelDensity
1025
1164
  }) {
1026
1165
  if (!ReactDOM.createRoot) {
@@ -1047,7 +1186,7 @@ function createScaffold({
1047
1186
  const cleanupCSS = Internals3.CSSUtils.injectCSS(Internals3.CSSUtils.makeDefaultPreviewCSS(`.${scaffoldClassName}`, "white"));
1048
1187
  wrapper.appendChild(div);
1049
1188
  document.body.appendChild(wrapper);
1050
- const htmlInCanvasContext = allowHtmlInCanvas ? setupHtmlInCanvas({ wrapper, div, width, height }) : null;
1189
+ const htmlInCanvasContext = useHtmlInCanvas ? setupHtmlInCanvas({ wrapper, div, width, height }) : null;
1051
1190
  const errorHolder = { error: null };
1052
1191
  const root = ReactDOM.createRoot(div, {
1053
1192
  onUncaughtError: (err, errorInfo) => {
@@ -2092,6 +2231,15 @@ var filterRequiresPrecompositing = (filter) => {
2092
2231
  }
2093
2232
  return null;
2094
2233
  };
2234
+ var snapshotTransformStyle = (computedStyle) => {
2235
+ return {
2236
+ display: computedStyle.display,
2237
+ rotate: computedStyle.rotate,
2238
+ scale: computedStyle.scale,
2239
+ transform: computedStyle.transform,
2240
+ transformOrigin: computedStyle.transformOrigin
2241
+ };
2242
+ };
2095
2243
  var isReplacedElement = (element) => {
2096
2244
  return element instanceof HTMLImageElement || element instanceof HTMLVideoElement || element instanceof HTMLCanvasElement || element instanceof HTMLIFrameElement || element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement || element instanceof HTMLObjectElement || element instanceof HTMLEmbedElement;
2097
2245
  };
@@ -2142,7 +2290,8 @@ var getGlobalTransformOrigin = ({ transform }) => {
2142
2290
  };
2143
2291
  var calculateTransforms = ({
2144
2292
  element,
2145
- rootElement
2293
+ rootElement,
2294
+ transformStyleCache
2146
2295
  }) => {
2147
2296
  let parent = element;
2148
2297
  const transforms = [];
@@ -2152,10 +2301,19 @@ var calculateTransforms = ({
2152
2301
  let maskImageInfo = null;
2153
2302
  let filterForPrecompositing = null;
2154
2303
  while (parent) {
2304
+ const cachedTransformStyle = transformStyleCache.get(parent);
2305
+ const shouldReadComputedStyle = parent === element || cachedTransformStyle === undefined;
2155
2306
  const originalTransition = parent.style.transition;
2156
2307
  parent.style.transition = "none";
2157
- const computedStyle = getComputedStyle(parent);
2308
+ const computedStyle = shouldReadComputedStyle ? getComputedStyle(parent) : null;
2309
+ const transformStyle = computedStyle === null ? cachedTransformStyle : snapshotTransformStyle(computedStyle);
2310
+ if (computedStyle !== null) {
2311
+ transformStyleCache.set(parent, transformStyle);
2312
+ }
2158
2313
  if (parent === element) {
2314
+ if (computedStyle === null) {
2315
+ throw new Error("Element computed style not found");
2316
+ }
2159
2317
  elementComputedStyle = computedStyle;
2160
2318
  opacity = parseFloat(computedStyle.opacity);
2161
2319
  const maskImageValue = getMaskImageValue(computedStyle);
@@ -2178,9 +2336,9 @@ var calculateTransforms = ({
2178
2336
  }
2179
2337
  });
2180
2338
  }
2181
- const hasApplicableTransformCssValue = canApplyCssTransforms({ computedStyle, element: parent }) && hasAnyTransformCssValue(computedStyle);
2339
+ const hasApplicableTransformCssValue = canApplyCssTransforms({ computedStyle: transformStyle, element: parent }) && hasAnyTransformCssValue(transformStyle);
2182
2340
  if (hasApplicableTransformCssValue || parent === element) {
2183
- const toParse = hasApplicableTransformCssValue && hasTransformCssValue(computedStyle) ? computedStyle.transform : undefined;
2341
+ const toParse = hasApplicableTransformCssValue && hasTransformCssValue(transformStyle) ? transformStyle.transform : undefined;
2184
2342
  const matrix = new DOMMatrix(toParse);
2185
2343
  const resetTransforms = makeTransformResetter(parent);
2186
2344
  const { scale, rotate } = parent.style;
@@ -2195,7 +2353,7 @@ var calculateTransforms = ({
2195
2353
  const cleanup = resetTransforms(hasApplicableTransformCssValue);
2196
2354
  transforms.push({
2197
2355
  element: parent,
2198
- transformOrigin: computedStyle.transformOrigin,
2356
+ transformOrigin: transformStyle.transformOrigin,
2199
2357
  boundingClientRect: null,
2200
2358
  matrices: additionalMatrices
2201
2359
  });
@@ -2805,7 +2963,8 @@ var drawBackground = async ({
2805
2963
  internalState,
2806
2964
  scale,
2807
2965
  onlyBackgroundClipText: true,
2808
- waitForPageResponsiveness: null
2966
+ waitForPageResponsiveness: null,
2967
+ waitForRenderReady: () => Promise.resolve()
2809
2968
  });
2810
2969
  onlyBackgroundClipText.setTransform(new DOMMatrix().scale(scale, scale));
2811
2970
  element.style.backgroundClip = originalBackgroundClip;
@@ -4000,13 +4159,15 @@ var processNode = async ({
4000
4159
  internalState,
4001
4160
  rootElement,
4002
4161
  scale,
4003
- waitForPageResponsiveness
4162
+ waitForPageResponsiveness,
4163
+ transformStyleCache
4004
4164
  }) => {
4005
4165
  let __stack = [];
4006
4166
  try {
4007
4167
  const transforms = __using(__stack, calculateTransforms({
4008
4168
  element,
4009
- rootElement
4169
+ rootElement,
4170
+ transformStyleCache
4010
4171
  }), 0);
4011
4172
  const { opacity, computedStyle, totalMatrix, dimensions, precompositing } = transforms;
4012
4173
  if (opacity === 0) {
@@ -4065,7 +4226,8 @@ var processNode = async ({
4065
4226
  internalState,
4066
4227
  scale,
4067
4228
  onlyBackgroundClipText: false,
4068
- waitForPageResponsiveness
4229
+ waitForPageResponsiveness,
4230
+ waitForRenderReady: () => Promise.resolve()
4069
4231
  });
4070
4232
  if (waitForPageResponsiveness !== null) {
4071
4233
  await waitForPageResponsiveness();
@@ -4173,29 +4335,25 @@ var applyTextTransform = (text, transform) => {
4173
4335
  };
4174
4336
 
4175
4337
  // src/drawing/text/find-line-breaks.text.ts
4338
+ var wordSegmenter = new Intl.Segmenter("en", { granularity: "word" });
4176
4339
  var findWords = (span) => {
4177
4340
  const originalText = span.textContent;
4178
- const segmenter = new Intl.Segmenter("en", { granularity: "word" });
4179
- const segments = segmenter.segment(span.textContent);
4180
- const words = Array.from(segments).map((s) => s.segment);
4341
+ const segments = Array.from(wordSegmenter.segment(originalText));
4181
4342
  const tokens = [];
4182
- for (let i = 0;i < words.length; i++) {
4183
- const wordsBefore = words.slice(0, i);
4184
- const wordsAfter = words.slice(i + 1);
4185
- const word = words[i];
4186
- const wordsBeforeText = wordsBefore.join("");
4187
- const wordsAfterText = wordsAfter.join("");
4343
+ for (const { index, segment } of segments) {
4344
+ const wordsBeforeText = originalText.slice(0, index);
4345
+ const wordsAfterText = originalText.slice(index + segment.length);
4188
4346
  const beforeNode = document.createTextNode(wordsBeforeText);
4189
4347
  const afterNode = document.createTextNode(wordsAfterText);
4190
4348
  const interstitialNode = document.createElement("span");
4191
- interstitialNode.textContent = word;
4349
+ interstitialNode.textContent = segment;
4192
4350
  span.textContent = "";
4193
4351
  span.appendChild(beforeNode);
4194
4352
  span.appendChild(interstitialNode);
4195
4353
  span.appendChild(afterNode);
4196
4354
  const rect = interstitialNode.getBoundingClientRect();
4197
4355
  span.textContent = originalText;
4198
- tokens.push({ text: word, rect });
4356
+ tokens.push({ text: segment, rect });
4199
4357
  }
4200
4358
  return tokens;
4201
4359
  };
@@ -4492,10 +4650,10 @@ var drawText = ({
4492
4650
  const ctm = contextToDraw.getTransform();
4493
4651
  const blurScale = Math.hypot(ctm.a, ctm.b);
4494
4652
  const { strokeFirst } = parsePaintOrder(paintOrder);
4653
+ const measurements = contextToDraw.measureText(originalText);
4654
+ const { fontBoundingBoxDescent, fontBoundingBoxAscent } = measurements;
4655
+ const fontHeight = fontBoundingBoxAscent + fontBoundingBoxDescent;
4495
4656
  for (const token of tokens) {
4496
- const measurements = contextToDraw.measureText(originalText);
4497
- const { fontBoundingBoxDescent, fontBoundingBoxAscent } = measurements;
4498
- const fontHeight = fontBoundingBoxAscent + fontBoundingBoxDescent;
4499
4657
  const leading = token.rect.height - fontHeight;
4500
4658
  const halfLeading = leading / 2;
4501
4659
  const x = (isRTL ? token.rect.right : token.rect.left) - parentRect.x;
@@ -4552,7 +4710,8 @@ var handleTextNode = async ({
4552
4710
  rootElement,
4553
4711
  onlyBackgroundClipText,
4554
4712
  scale,
4555
- waitForPageResponsiveness
4713
+ waitForPageResponsiveness,
4714
+ transformStyleCache
4556
4715
  }) => {
4557
4716
  const span = document.createElement("span");
4558
4717
  const parent = node.parentNode;
@@ -4570,7 +4729,8 @@ var handleTextNode = async ({
4570
4729
  internalState,
4571
4730
  rootElement,
4572
4731
  scale,
4573
- waitForPageResponsiveness
4732
+ waitForPageResponsiveness,
4733
+ transformStyleCache
4574
4734
  });
4575
4735
  parent.insertBefore(node, span);
4576
4736
  parent.removeChild(span);
@@ -4587,7 +4747,8 @@ var walkOverNode = ({
4587
4747
  rootElement,
4588
4748
  onlyBackgroundClipText,
4589
4749
  scale,
4590
- waitForPageResponsiveness
4750
+ waitForPageResponsiveness,
4751
+ transformStyleCache
4591
4752
  }) => {
4592
4753
  if (node instanceof HTMLElement || node instanceof SVGElement) {
4593
4754
  return processNode({
@@ -4599,7 +4760,8 @@ var walkOverNode = ({
4599
4760
  internalState,
4600
4761
  rootElement,
4601
4762
  scale,
4602
- waitForPageResponsiveness
4763
+ waitForPageResponsiveness,
4764
+ transformStyleCache
4603
4765
  });
4604
4766
  }
4605
4767
  if (node instanceof Text) {
@@ -4612,7 +4774,8 @@ var walkOverNode = ({
4612
4774
  rootElement,
4613
4775
  onlyBackgroundClipText,
4614
4776
  scale,
4615
- waitForPageResponsiveness
4777
+ waitForPageResponsiveness,
4778
+ transformStyleCache
4616
4779
  });
4617
4780
  }
4618
4781
  throw new Error("Unknown node type");
@@ -4645,6 +4808,7 @@ var compose = async ({
4645
4808
  let __stack = [];
4646
4809
  try {
4647
4810
  const treeWalker = document.createTreeWalker(element, onlyBackgroundClipText ? NodeFilter.SHOW_TEXT : NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, getFilterFunction);
4811
+ const transformStyleCache = new WeakMap;
4648
4812
  if (onlyBackgroundClipText) {
4649
4813
  treeWalker.nextNode();
4650
4814
  if (!treeWalker.currentNode) {
@@ -4664,7 +4828,8 @@ var compose = async ({
4664
4828
  rootElement: element,
4665
4829
  onlyBackgroundClipText,
4666
4830
  scale,
4667
- waitForPageResponsiveness
4831
+ waitForPageResponsiveness,
4832
+ transformStyleCache
4668
4833
  });
4669
4834
  if (val.type === "skip-children") {
4670
4835
  if (!skipToNextNonDescendant(treeWalker)) {
@@ -4699,17 +4864,21 @@ var createLayer = async ({
4699
4864
  cutout,
4700
4865
  htmlInCanvasContext,
4701
4866
  onHtmlInCanvasLayerOutcome,
4702
- waitForPageResponsiveness
4867
+ waitForPageResponsiveness,
4868
+ waitForRenderReady
4703
4869
  }) => {
4704
4870
  const scaledWidth = Math.ceil(cutout.width * scale);
4705
4871
  const scaledHeight = Math.ceil(cutout.height * scale);
4706
4872
  if (!onlyBackgroundClipText && element instanceof HTMLElement && htmlInCanvasContext && onHtmlInCanvasLayerOutcome) {
4873
+ const hasNestedHtmlInCanvas = containsLayoutSubtreeCanvas(element);
4707
4874
  try {
4708
4875
  const offCtx = await drawWithHtmlInCanvas({
4709
4876
  htmlInCanvasContext,
4710
4877
  element,
4711
4878
  scaledWidth,
4712
- scaledHeight
4879
+ scaledHeight,
4880
+ waitForRenderReady,
4881
+ useElementImage: hasNestedHtmlInCanvas
4713
4882
  });
4714
4883
  onHtmlInCanvasLayerOutcome({ native: true });
4715
4884
  return offCtx;
@@ -4970,7 +5139,6 @@ var internalRenderMediaOnWeb = async ({
4970
5139
  muted,
4971
5140
  scale,
4972
5141
  isProduction,
4973
- allowHtmlInCanvas,
4974
5142
  sampleRate
4975
5143
  }) => {
4976
5144
  let __stack2 = [];
@@ -4984,7 +5152,7 @@ var internalRenderMediaOnWeb = async ({
4984
5152
  }
4985
5153
  htmlInCanvasLayerOutcomeReported = true;
4986
5154
  if (outcome.native) {
4987
- Internals9.Log.warn({ logLevel, tag: "@remotion/web-renderer" }, "Using Chromium experimental HTML-in-canvas (drawElementImage) for video frames. See https://github.com/WICG/html-in-canvas");
5155
+ Internals9.Log.warn({ logLevel, tag: "@remotion/web-renderer" }, "Using Chromium experimental HTML-in-canvas (drawElementImage) for video frames. See https://remotion.dev/docs/client-side-rendering/html-in-canvas");
4988
5156
  } else if (outcome.shouldWarn) {
4989
5157
  Internals9.Log.warn({ logLevel, tag: "@remotion/web-renderer" }, `Not using HTML-in-canvas: ${outcome.reason}`);
4990
5158
  }
@@ -5038,6 +5206,7 @@ var internalRenderMediaOnWeb = async ({
5038
5206
  if (signal?.aborted) {
5039
5207
  return Promise.reject(new Error("renderMediaOnWeb() was cancelled"));
5040
5208
  }
5209
+ const useHtmlInCanvas = await supportsNestedHtmlInCanvas();
5041
5210
  const scaffold = __using(__stack2, createScaffold({
5042
5211
  width: resolved.width,
5043
5212
  height: resolved.height,
@@ -5055,7 +5224,7 @@ var internalRenderMediaOnWeb = async ({
5055
5224
  initialFrame: 0,
5056
5225
  defaultCodec: resolved.defaultCodec,
5057
5226
  defaultOutName: resolved.defaultOutName,
5058
- allowHtmlInCanvas,
5227
+ useHtmlInCanvas,
5059
5228
  pixelDensity: scale
5060
5229
  }), 0);
5061
5230
  const {
@@ -5066,27 +5235,6 @@ var internalRenderMediaOnWeb = async ({
5066
5235
  errorHolder,
5067
5236
  htmlInCanvasContext
5068
5237
  } = scaffold;
5069
- if (allowHtmlInCanvas && !htmlInCanvasContext) {
5070
- if (!supportsNativeHtmlInCanvas()) {
5071
- onHtmlInCanvasLayerOutcome({
5072
- native: false,
5073
- reason: "This browser does not expose CanvasRenderingContext2D.prototype.drawElementImage. In Chromium, enable chrome://flags/#canvas-draw-element and use a version that ships the API.",
5074
- shouldWarn: false
5075
- });
5076
- } else {
5077
- onHtmlInCanvasLayerOutcome({
5078
- native: false,
5079
- reason: "drawElementImage is available but canvas.requestPaint() is missing. Use a Chromium version that ships requestPaint.",
5080
- shouldWarn: true
5081
- });
5082
- }
5083
- } else if (!allowHtmlInCanvas) {
5084
- onHtmlInCanvasLayerOutcome({
5085
- native: false,
5086
- reason: "allowHtmlInCanvas is false; using the built-in DOM composer.",
5087
- shouldWarn: false
5088
- });
5089
- }
5090
5238
  const internalState = __using(__stack2, makeInternalState(), 0);
5091
5239
  const pageResponsivenessController = createPageResponsivenessController({
5092
5240
  intervalInMilliseconds: pageResponsivenessIntervalInMilliseconds,
@@ -5105,6 +5253,17 @@ var internalRenderMediaOnWeb = async ({
5105
5253
  fps: resolved.fps,
5106
5254
  logLevel
5107
5255
  }), 0);
5256
+ const waitForRenderReady = async () => {
5257
+ await waitForReady({
5258
+ timeoutInMilliseconds: delayRenderTimeoutInMilliseconds,
5259
+ scope: delayRenderScope,
5260
+ signal,
5261
+ apiName: "renderMediaOnWeb",
5262
+ internalState,
5263
+ keepalive
5264
+ });
5265
+ checkForError(errorHolder);
5266
+ };
5108
5267
  const artifactsHandler = handleArtifacts();
5109
5268
  const webFsTarget = outputTarget === "web-fs" ? await createWebFsTarget() : null;
5110
5269
  const target = webFsTarget ? new StreamTarget(webFsTarget.stream) : new BufferTarget;
@@ -5123,15 +5282,7 @@ var internalRenderMediaOnWeb = async ({
5123
5282
  if (signal?.aborted) {
5124
5283
  throw new Error("renderMediaOnWeb() was cancelled");
5125
5284
  }
5126
- await waitForReady({
5127
- timeoutInMilliseconds: delayRenderTimeoutInMilliseconds,
5128
- scope: delayRenderScope,
5129
- signal,
5130
- apiName: "renderMediaOnWeb",
5131
- internalState,
5132
- keepalive
5133
- });
5134
- checkForError(errorHolder);
5285
+ await waitForRenderReady();
5135
5286
  if (signal?.aborted) {
5136
5287
  throw new Error("renderMediaOnWeb() was cancelled");
5137
5288
  }
@@ -5189,15 +5340,7 @@ var internalRenderMediaOnWeb = async ({
5189
5340
  throw new Error("renderMediaOnWeb() was cancelled");
5190
5341
  }
5191
5342
  timeUpdater.current?.update(frame);
5192
- await waitForReady({
5193
- timeoutInMilliseconds: delayRenderTimeoutInMilliseconds,
5194
- scope: delayRenderScope,
5195
- signal,
5196
- apiName: "renderMediaOnWeb",
5197
- keepalive,
5198
- internalState
5199
- });
5200
- checkForError(errorHolder);
5343
+ await waitForRenderReady();
5201
5344
  if (signal?.aborted) {
5202
5345
  throw new Error("renderMediaOnWeb() was cancelled");
5203
5346
  }
@@ -5215,7 +5358,8 @@ var internalRenderMediaOnWeb = async ({
5215
5358
  cutout: new DOMRect(0, 0, resolved.width, resolved.height),
5216
5359
  htmlInCanvasContext,
5217
5360
  onHtmlInCanvasLayerOutcome: htmlInCanvasContext ? onHtmlInCanvasLayerOutcome : undefined,
5218
- waitForPageResponsiveness
5361
+ waitForPageResponsiveness,
5362
+ waitForRenderReady
5219
5363
  });
5220
5364
  internalState.addCreateFrameTime(performance.now() - createFrameStart);
5221
5365
  layerCanvas = layer.canvas;
@@ -5384,7 +5528,6 @@ var renderMediaOnWeb = (options) => {
5384
5528
  muted: options.muted ?? false,
5385
5529
  scale: options.scale ?? 1,
5386
5530
  isProduction: options.isProduction ?? true,
5387
- allowHtmlInCanvas: options.allowHtmlInCanvas ?? false,
5388
5531
  sampleRate: options.sampleRate ?? 48000
5389
5532
  }));
5390
5533
  return onlyOneRenderAtATimeQueue.ref;
@@ -5443,15 +5586,14 @@ async function internalRenderStillOnWeb({
5443
5586
  onArtifact,
5444
5587
  licenseKey,
5445
5588
  scale,
5446
- isProduction,
5447
- allowHtmlInCanvas
5589
+ isProduction
5448
5590
  }) {
5449
5591
  let __stack = [];
5450
5592
  try {
5451
5593
  validateScale(scale);
5452
5594
  const onHtmlInCanvasLayerOutcome = (outcome) => {
5453
5595
  if (outcome.native) {
5454
- Internals10.Log.warn({ logLevel, tag: "@remotion/web-renderer" }, "Using Chromium experimental HTML-in-canvas (drawElementImage) for this frame. Pixels may differ from the built-in DOM composer. Set allowHtmlInCanvas: false to force software rasterization. See https://github.com/WICG/html-in-canvas");
5596
+ Internals10.Log.warn({ logLevel, tag: "@remotion/web-renderer" }, "Using Chromium experimental HTML-in-canvas (drawElementImage) for this frame. See https://remotion.dev/docs/client-side-rendering/html-in-canvas");
5455
5597
  } else if (outcome.shouldWarn) {
5456
5598
  Internals10.Log.warn({ logLevel, tag: "@remotion/web-renderer" }, `Not using HTML-in-canvas: ${outcome.reason}`);
5457
5599
  }
@@ -5470,6 +5612,7 @@ async function internalRenderStillOnWeb({
5470
5612
  if (signal?.aborted) {
5471
5613
  return Promise.reject(new Error("renderStillOnWeb() was cancelled"));
5472
5614
  }
5615
+ const useHtmlInCanvas = await supportsNestedHtmlInCanvas();
5473
5616
  const internalState = __using(__stack, makeInternalState(), 0);
5474
5617
  const scaffold = __using(__stack, createScaffold({
5475
5618
  width: resolved.width,
@@ -5488,7 +5631,7 @@ async function internalRenderStillOnWeb({
5488
5631
  initialFrame: frame,
5489
5632
  defaultCodec: resolved.defaultCodec,
5490
5633
  defaultOutName: resolved.defaultOutName,
5491
- allowHtmlInCanvas,
5634
+ useHtmlInCanvas,
5492
5635
  pixelDensity: scale
5493
5636
  }), 0);
5494
5637
  const {
@@ -5498,32 +5641,8 @@ async function internalRenderStillOnWeb({
5498
5641
  errorHolder,
5499
5642
  htmlInCanvasContext
5500
5643
  } = scaffold;
5501
- if (allowHtmlInCanvas && !htmlInCanvasContext) {
5502
- if (!supportsNativeHtmlInCanvas()) {
5503
- onHtmlInCanvasLayerOutcome({
5504
- native: false,
5505
- reason: "This browser does not expose CanvasRenderingContext2D.prototype.drawElementImage. In Chromium, enable chrome://flags/#canvas-draw-element and use a version that ships the API.",
5506
- shouldWarn: false
5507
- });
5508
- } else {
5509
- onHtmlInCanvasLayerOutcome({
5510
- native: false,
5511
- reason: "drawElementImage is available but canvas.requestPaint() is missing. Use a Chromium version that ships requestPaint.",
5512
- shouldWarn: true
5513
- });
5514
- }
5515
- } else if (!allowHtmlInCanvas) {
5516
- onHtmlInCanvasLayerOutcome({
5517
- native: false,
5518
- reason: "allowHtmlInCanvas is false; using the built-in DOM composer.",
5519
- shouldWarn: false
5520
- });
5521
- }
5522
5644
  const artifactsHandler = handleArtifacts();
5523
- try {
5524
- if (signal?.aborted) {
5525
- throw new Error("renderStillOnWeb() was cancelled");
5526
- }
5645
+ const waitForRenderReady = async () => {
5527
5646
  await waitForReady({
5528
5647
  timeoutInMilliseconds: delayRenderTimeoutInMilliseconds,
5529
5648
  scope: delayRenderScope,
@@ -5533,6 +5652,12 @@ async function internalRenderStillOnWeb({
5533
5652
  keepalive: null
5534
5653
  });
5535
5654
  checkForError(errorHolder);
5655
+ };
5656
+ try {
5657
+ if (signal?.aborted) {
5658
+ throw new Error("renderStillOnWeb() was cancelled");
5659
+ }
5660
+ await waitForRenderReady();
5536
5661
  if (signal?.aborted) {
5537
5662
  throw new Error("renderStillOnWeb() was cancelled");
5538
5663
  }
@@ -5545,7 +5670,8 @@ async function internalRenderStillOnWeb({
5545
5670
  cutout: new DOMRect(0, 0, resolved.width, resolved.height),
5546
5671
  htmlInCanvasContext,
5547
5672
  onHtmlInCanvasLayerOutcome: htmlInCanvasContext ? onHtmlInCanvasLayerOutcome : undefined,
5548
- waitForPageResponsiveness: null
5673
+ waitForPageResponsiveness: null,
5674
+ waitForRenderReady
5549
5675
  });
5550
5676
  const { canvas } = capturedFrame;
5551
5677
  const assets = collectAssets.current.collectAssets();
@@ -5596,8 +5722,7 @@ var renderStillOnWeb = (options) => {
5596
5722
  onArtifact: options.onArtifact ?? null,
5597
5723
  licenseKey: options.licenseKey ?? null,
5598
5724
  scale: options.scale ?? 1,
5599
- isProduction: options.isProduction ?? true,
5600
- allowHtmlInCanvas: options.allowHtmlInCanvas ?? false
5725
+ isProduction: options.isProduction ?? true
5601
5726
  }));
5602
5727
  return onlyOneRenderAtATimeQueue.ref;
5603
5728
  };
@@ -1,11 +1,15 @@
1
1
  type Canvas2DWithDrawElement = CanvasRenderingContext2D & {
2
- drawElementImage: (element: Element, dx: number, dy: number, dwidth: number, dheight: number) => DOMMatrix;
2
+ drawElementImage: (element: Element | ElementImage, dx: number, dy: number, dwidth: number, dheight: number) => DOMMatrix;
3
3
  };
4
4
  type HTMLCanvasWithLayoutSubtree = HTMLCanvasElement & {
5
+ captureElementImage?: (element: Element) => ElementImage;
5
6
  layoutSubtree?: boolean;
6
7
  requestPaint?: () => void;
7
8
  };
9
+ export declare const setForceDisableHtmlInCanvasForTesting: (forceDisable: boolean) => void;
8
10
  export declare const supportsNativeHtmlInCanvas: () => boolean;
11
+ export declare const supportsNestedHtmlInCanvas: () => Promise<boolean>;
12
+ export declare const containsLayoutSubtreeCanvas: (element: HTMLElement) => boolean;
9
13
  export type HtmlInCanvasContext = {
10
14
  layoutCanvas: HTMLCanvasWithLayoutSubtree;
11
15
  ctx: Canvas2DWithDrawElement;
@@ -29,11 +33,13 @@ export declare const setupHtmlInCanvas: ({ wrapper, div, width, height, }: {
29
33
  * The caller is responsible for ensuring the frame content is ready (via
30
34
  * waitForReady) before calling this function.
31
35
  */
32
- export declare const drawWithHtmlInCanvas: ({ htmlInCanvasContext, element, scaledWidth, scaledHeight, }: {
36
+ export declare const drawWithHtmlInCanvas: ({ htmlInCanvasContext, element, scaledWidth, scaledHeight, waitForRenderReady, useElementImage, }: {
33
37
  htmlInCanvasContext: HtmlInCanvasContext;
34
38
  element: HTMLElement;
35
39
  scaledWidth: number;
36
40
  scaledHeight: number;
41
+ waitForRenderReady: () => Promise<void>;
42
+ useElementImage: boolean;
37
43
  }) => Promise<OffscreenCanvasRenderingContext2D>;
38
44
  export declare const teardownHtmlInCanvas: ({ htmlInCanvasContext, wrapper, div, }: {
39
45
  htmlInCanvasContext: HtmlInCanvasContext;
@@ -67,7 +67,6 @@ type OptionalRenderMediaOnWebOptions<Schema extends $ZodObject> = {
67
67
  isProduction: boolean;
68
68
  muted: boolean;
69
69
  scale: number;
70
- allowHtmlInCanvas: boolean;
71
70
  sampleRate: number;
72
71
  };
73
72
  export type RenderMediaOnWebOptions<Schema extends $ZodObject, Props extends Record<string, unknown>> = MandatoryRenderMediaOnWebOptions<Schema, Props> & Partial<OptionalRenderMediaOnWebOptions<Schema>> & InputPropsIfHasProps<Schema, Props>;
@@ -20,7 +20,6 @@ type OptionalRenderStillOnWebOptions<Schema extends $ZodObject> = {
20
20
  licenseKey: string | null;
21
21
  scale: number;
22
22
  isProduction: boolean;
23
- allowHtmlInCanvas: boolean;
24
23
  };
25
24
  export type RenderStillOnWebOptions<Schema extends $ZodObject, Props extends Record<string, unknown>> = MandatoryRenderStillOnWebOptions<Schema, Props> & Partial<OptionalRenderStillOnWebOptions<Schema>> & InputPropsIfHasProps<Schema, Props>;
26
25
  export declare const renderStillOnWeb: <Schema extends $ZodObject<Readonly<Readonly<{
@@ -6,7 +6,7 @@ export type HtmlInCanvasLayerOutcome = {
6
6
  reason: string;
7
7
  shouldWarn: boolean;
8
8
  };
9
- export declare const createLayer: ({ element, scale, logLevel, internalState, onlyBackgroundClipText, cutout, htmlInCanvasContext, onHtmlInCanvasLayerOutcome, waitForPageResponsiveness, }: {
9
+ export declare const createLayer: ({ element, scale, logLevel, internalState, onlyBackgroundClipText, cutout, htmlInCanvasContext, onHtmlInCanvasLayerOutcome, waitForPageResponsiveness, waitForRenderReady, }: {
10
10
  element: HTMLElement | SVGElement;
11
11
  scale: number;
12
12
  logLevel: "error" | "info" | "trace" | "verbose" | "warn";
@@ -33,4 +33,5 @@ export declare const createLayer: ({ element, scale, logLevel, internalState, on
33
33
  htmlInCanvasContext?: HtmlInCanvasContext | null | undefined;
34
34
  onHtmlInCanvasLayerOutcome?: ((outcome: HtmlInCanvasLayerOutcome) => void) | undefined;
35
35
  waitForPageResponsiveness: (() => Promise<void>) | null;
36
+ waitForRenderReady: () => Promise<void>;
36
37
  }) => Promise<OffscreenCanvasRenderingContext2D>;
@@ -1,5 +1,6 @@
1
+ import type { TransformStyleCache } from './drawing/calculate-transforms';
1
2
  import type { ProcessNodeReturnValue } from './drawing/process-node';
2
- export declare const walkOverNode: ({ node, context, logLevel, parentRect, internalState, rootElement, onlyBackgroundClipText, scale, waitForPageResponsiveness, }: {
3
+ export declare const walkOverNode: ({ node, context, logLevel, parentRect, internalState, rootElement, onlyBackgroundClipText, scale, waitForPageResponsiveness, transformStyleCache, }: {
3
4
  node: Node;
4
5
  context: OffscreenCanvasRenderingContext2D;
5
6
  logLevel: "error" | "info" | "trace" | "verbose" | "warn";
@@ -26,4 +27,5 @@ export declare const walkOverNode: ({ node, context, logLevel, parentRect, inter
26
27
  onlyBackgroundClipText: boolean;
27
28
  scale: number;
28
29
  waitForPageResponsiveness: (() => Promise<void>) | null;
30
+ transformStyleCache: TransformStyleCache;
29
31
  }) => Promise<ProcessNodeReturnValue>;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "url": "https://github.com/remotion-dev/remotion/tree/main/packages/web-renderer"
4
4
  },
5
5
  "name": "@remotion/web-renderer",
6
- "version": "4.0.489",
6
+ "version": "4.0.491",
7
7
  "main": "dist/index.js",
8
8
  "type": "module",
9
9
  "scripts": {
@@ -22,19 +22,20 @@
22
22
  "@mediabunny/mp3-encoder": "1.50.8",
23
23
  "@mediabunny/aac-encoder": "1.50.8",
24
24
  "@mediabunny/flac-encoder": "1.50.8",
25
- "@remotion/licensing": "4.0.489",
26
- "remotion": "4.0.489",
25
+ "@remotion/licensing": "4.0.491",
26
+ "remotion": "4.0.491",
27
27
  "mediabunny": "1.50.8"
28
28
  },
29
29
  "devDependencies": {
30
+ "@remotion/effects": "4.0.491",
30
31
  "@react-three/fiber": "9.2.0",
31
- "@remotion/eslint-config-internal": "4.0.489",
32
- "@remotion/paths": "4.0.489",
33
- "@remotion/player": "4.0.489",
34
- "@remotion/media": "4.0.489",
35
- "@remotion/shapes": "4.0.489",
36
- "@remotion/three": "4.0.489",
37
- "@remotion/transitions": "4.0.489",
32
+ "@remotion/eslint-config-internal": "4.0.491",
33
+ "@remotion/paths": "4.0.491",
34
+ "@remotion/player": "4.0.491",
35
+ "@remotion/media": "4.0.491",
36
+ "@remotion/shapes": "4.0.491",
37
+ "@remotion/three": "4.0.491",
38
+ "@remotion/transitions": "4.0.491",
38
39
  "@types/three": "0.170.0",
39
40
  "@typescript/native-preview": "7.0.0-dev.20260217.1",
40
41
  "@vitejs/plugin-react": "4.3.4",
@@ -68,6 +69,6 @@
68
69
  "renderer",
69
70
  "web"
70
71
  ],
71
- "description": "Render videos in the browser (not yet released)",
72
+ "description": "Render videos in the browser",
72
73
  "homepage": "https://www.remotion.dev/docs/web-renderer/"
73
74
  }