@remotion/web-renderer 4.0.398 → 4.0.399

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.
Files changed (76) hide show
  1. package/dist/calculate-transforms.d.ts +0 -2
  2. package/dist/calculate-transforms.js +0 -17
  3. package/dist/composable.d.ts +8 -2
  4. package/dist/compose-canvas.js +4 -28
  5. package/dist/compose.d.ts +2 -1
  6. package/dist/compose.js +34 -35
  7. package/dist/drawing/border-radius.d.ts +3 -1
  8. package/dist/drawing/border-radius.js +27 -6
  9. package/dist/drawing/calculate-object-fit.d.ts +40 -0
  10. package/dist/drawing/calculate-object-fit.js +208 -0
  11. package/dist/drawing/draw-background.d.ts +13 -0
  12. package/dist/drawing/draw-background.js +62 -0
  13. package/dist/drawing/draw-box-shadow.d.ts +1 -1
  14. package/dist/drawing/draw-box-shadow.js +1 -1
  15. package/dist/drawing/draw-dom-element.js +78 -10
  16. package/dist/drawing/draw-element-to-canvas.d.ts +1 -2
  17. package/dist/drawing/draw-element-to-canvas.js +36 -8
  18. package/dist/drawing/draw-element.d.ts +4 -1
  19. package/dist/drawing/draw-element.js +21 -35
  20. package/dist/drawing/fit-svg-into-its-dimensions.d.ts +12 -0
  21. package/dist/drawing/fit-svg-into-its-dimensions.js +35 -0
  22. package/dist/drawing/get-clipped-background.d.ts +8 -0
  23. package/dist/drawing/get-clipped-background.js +14 -0
  24. package/dist/drawing/get-padding-box.d.ts +1 -0
  25. package/dist/drawing/get-padding-box.js +30 -0
  26. package/dist/drawing/get-pretransform-rect.js +13 -0
  27. package/dist/drawing/go-rects-intersect.d.ts +1 -0
  28. package/dist/drawing/go-rects-intersect.js +6 -0
  29. package/dist/drawing/handle-3d-transform.d.ts +4 -2
  30. package/dist/drawing/handle-3d-transform.js +4 -3
  31. package/dist/drawing/handle-mask.js +2 -0
  32. package/dist/drawing/overflow.d.ts +3 -1
  33. package/dist/drawing/overflow.js +3 -1
  34. package/dist/drawing/parse-linear-gradient.d.ts +3 -1
  35. package/dist/drawing/parse-linear-gradient.js +3 -3
  36. package/dist/drawing/precompose.js +1 -0
  37. package/dist/drawing/process-node.js +5 -6
  38. package/dist/drawing/text/draw-text.d.ts +2 -1
  39. package/dist/drawing/text/draw-text.js +9 -3
  40. package/dist/drawing/text/get-base-height.d.ts +1 -0
  41. package/dist/drawing/text/get-base-height.js +13 -0
  42. package/dist/drawing/text/handle-text-node.d.ts +2 -1
  43. package/dist/drawing/text/handle-text-node.js +2 -2
  44. package/dist/drawing/transform-in-3d.d.ts +3 -2
  45. package/dist/drawing/transform-in-3d.js +106 -87
  46. package/dist/esm/index.mjs +678 -168
  47. package/dist/find-capturable-elements.d.ts +1 -1
  48. package/dist/find-capturable-elements.js +22 -20
  49. package/dist/internal-state.d.ts +19 -0
  50. package/dist/internal-state.js +9 -0
  51. package/dist/render-still-on-web.js +1 -0
  52. package/dist/send-telemetry-event.js +1 -1
  53. package/dist/take-screenshot.js +1 -0
  54. package/dist/tree-walker-cleanup-after-children.d.ts +5 -0
  55. package/dist/tree-walker-cleanup-after-children.js +33 -0
  56. package/package.json +9 -8
  57. package/dist/border-radius.d.ts +0 -31
  58. package/dist/border-radius.js +0 -152
  59. package/dist/drawing/canvas-offset-from-rect.d.ts +0 -8
  60. package/dist/drawing/canvas-offset-from-rect.js +0 -12
  61. package/dist/drawing/compose-canvas.d.ts +0 -1
  62. package/dist/drawing/compose-canvas.js +0 -36
  63. package/dist/drawing/compose-svg.d.ts +0 -1
  64. package/dist/drawing/compose-svg.js +0 -34
  65. package/dist/drawing/compose.d.ts +0 -5
  66. package/dist/drawing/compose.js +0 -6
  67. package/dist/drawing/get-bounding-box-including-shadow.d.ts +0 -1
  68. package/dist/drawing/get-bounding-box-including-shadow.js +0 -6
  69. package/dist/drawing/get-computed-style-cache.d.ts +0 -0
  70. package/dist/drawing/get-computed-style-cache.js +0 -1
  71. package/dist/find-canvas-elements.d.ts +0 -1
  72. package/dist/find-canvas-elements.js +0 -13
  73. package/dist/opacity.d.ts +0 -4
  74. package/dist/opacity.js +0 -7
  75. package/dist/transform.d.ts +0 -4
  76. package/dist/transform.js +0 -6
@@ -1,17 +1,85 @@
1
+ import { calculateObjectFit, parseObjectFit } from './calculate-object-fit';
2
+ import { fitSvgIntoItsContainer } from './fit-svg-into-its-dimensions';
1
3
  import { turnSvgIntoDrawable } from './turn-svg-into-drawable';
4
+ const getReadableImageError = (err, node) => {
5
+ if (!(err instanceof DOMException)) {
6
+ return null;
7
+ }
8
+ if (err.name === 'SecurityError') {
9
+ return new Error(`Could not draw image with src="${node.src}" to canvas: ` +
10
+ `The image is tainted due to CORS restrictions. ` +
11
+ `The server hosting this image must respond with the "Access-Control-Allow-Origin" header. ` +
12
+ `See: https://remotion.dev/docs/client-side-rendering/migration`);
13
+ }
14
+ if (err.name === 'InvalidStateError') {
15
+ return new Error(`Could not draw image with src="${node.src}" to canvas: ` +
16
+ `The image is in a broken state. ` +
17
+ `This usually means the image failed to load - check that the URL is valid and accessible.`);
18
+ }
19
+ return null;
20
+ };
21
+ /**
22
+ * Draw an SVG element using "contain" behavior (the default for SVGs).
23
+ */
24
+ const drawSvg = ({ drawable, dimensions, contextToDraw, }) => {
25
+ const fitted = fitSvgIntoItsContainer({
26
+ containerSize: dimensions,
27
+ elementSize: {
28
+ width: drawable.width,
29
+ height: drawable.height,
30
+ },
31
+ });
32
+ contextToDraw.drawImage(drawable, fitted.left, fitted.top, fitted.width, fitted.height);
33
+ };
34
+ /**
35
+ * Draw an image or canvas element using the object-fit CSS property.
36
+ */
37
+ const drawReplacedElement = ({ drawable, dimensions, computedStyle, contextToDraw, }) => {
38
+ const objectFit = parseObjectFit(computedStyle.objectFit);
39
+ const intrinsicSize = drawable instanceof HTMLImageElement
40
+ ? { width: drawable.naturalWidth, height: drawable.naturalHeight }
41
+ : { width: drawable.width, height: drawable.height };
42
+ const result = calculateObjectFit({
43
+ objectFit,
44
+ containerSize: {
45
+ width: dimensions.width,
46
+ height: dimensions.height,
47
+ left: dimensions.left,
48
+ top: dimensions.top,
49
+ },
50
+ intrinsicSize,
51
+ });
52
+ // Use the 9-argument drawImage to support source cropping (for cover mode)
53
+ contextToDraw.drawImage(drawable, result.sourceX, result.sourceY, result.sourceWidth, result.sourceHeight, result.destX, result.destY, result.destWidth, result.destHeight);
54
+ };
2
55
  export const drawDomElement = (node) => {
3
- const domDrawFn = async ({ dimensions, contextToDraw }) => {
4
- const drawable = await (node instanceof SVGSVGElement
5
- ? turnSvgIntoDrawable(node)
6
- : node instanceof HTMLImageElement
7
- ? node
8
- : node instanceof HTMLCanvasElement
9
- ? node
10
- : null);
11
- if (!drawable) {
56
+ const domDrawFn = async ({ dimensions, contextToDraw, computedStyle, }) => {
57
+ // Handle SVG elements separately - they use "contain" behavior by default
58
+ if (node instanceof SVGSVGElement) {
59
+ const drawable = await turnSvgIntoDrawable(node);
60
+ drawSvg({ drawable, dimensions, contextToDraw });
12
61
  return;
13
62
  }
14
- contextToDraw.drawImage(drawable, dimensions.left, dimensions.top, dimensions.width, dimensions.height);
63
+ // Handle replaced elements (img, canvas) with object-fit support
64
+ if (node instanceof HTMLImageElement || node instanceof HTMLCanvasElement) {
65
+ try {
66
+ drawReplacedElement({
67
+ drawable: node,
68
+ dimensions,
69
+ computedStyle,
70
+ contextToDraw,
71
+ });
72
+ }
73
+ catch (err) {
74
+ if (node instanceof HTMLImageElement) {
75
+ const readableError = getReadableImageError(err, node);
76
+ if (readableError) {
77
+ throw readableError;
78
+ }
79
+ }
80
+ throw err;
81
+ }
82
+ }
15
83
  };
16
84
  return domDrawFn;
17
85
  };
@@ -1,12 +1,11 @@
1
1
  import type { LogLevel } from 'remotion';
2
2
  import type { DrawFn } from './drawn-fn';
3
3
  export type DrawElementToCanvasReturnValue = 'continue' | 'skip-children';
4
- export declare const drawElementToCanvas: ({ element, context, draw, offsetLeft, offsetTop, logLevel, parentRect, }: {
4
+ export declare const drawElementToCanvas: ({ element, context, draw, offsetLeft, offsetTop, logLevel, }: {
5
5
  element: HTMLElement | SVGElement;
6
6
  context: OffscreenCanvasRenderingContext2D;
7
7
  draw: DrawFn;
8
8
  offsetLeft: number;
9
9
  offsetTop: number;
10
10
  logLevel: LogLevel;
11
- parentRect: DOMRect;
12
11
  }) => Promise<DrawElementToCanvasReturnValue>;
@@ -1,9 +1,11 @@
1
+ import { Internals } from 'remotion';
2
+ import { compose } from '../compose';
3
+ import { getBiggestBoundingClientRect } from '../get-biggest-bounding-client-rect';
1
4
  import { calculateTransforms } from './calculate-transforms';
2
5
  import { drawElement } from './draw-element';
3
- import { handle3dTransform } from './handle-3d-transform';
4
- export const drawElementToCanvas = async ({ element, context, draw, offsetLeft, offsetTop, logLevel, parentRect, }) => {
5
- const transforms = calculateTransforms({ element, offsetLeft, offsetTop });
6
- const { totalMatrix, reset, dimensions, opacity, computedStyle } = transforms;
6
+ import { transformIn3d } from './transform-in-3d';
7
+ export const drawElementToCanvas = async ({ element, context, draw, offsetLeft, offsetTop, logLevel, }) => {
8
+ const { totalMatrix, reset, dimensions, opacity, computedStyle } = calculateTransforms(element);
7
9
  if (opacity === 0) {
8
10
  reset();
9
11
  return 'continue';
@@ -13,14 +15,40 @@ export const drawElementToCanvas = async ({ element, context, draw, offsetLeft,
13
15
  return 'continue';
14
16
  }
15
17
  if (!totalMatrix.is2D) {
16
- await handle3dTransform({
18
+ const biggestBoundingClientRect = getBiggestBoundingClientRect(element);
19
+ const canvasOffsetLeft = Math.min(biggestBoundingClientRect.left, 0);
20
+ const canvasOffsetTop = Math.min(biggestBoundingClientRect.top, 0);
21
+ const tempCanvasWidth = Math.max(biggestBoundingClientRect.width, biggestBoundingClientRect.right);
22
+ const tempCanvasHeight = Math.max(biggestBoundingClientRect.height, biggestBoundingClientRect.bottom);
23
+ const start = Date.now();
24
+ const tempCanvas = new OffscreenCanvas(tempCanvasWidth, tempCanvasHeight);
25
+ const context2 = tempCanvas.getContext('2d');
26
+ if (!context2) {
27
+ throw new Error('Could not get context');
28
+ }
29
+ await compose({
17
30
  element,
18
- totalMatrix,
19
- parentRect,
20
- context,
31
+ context: context2,
32
+ offsetLeft: canvasOffsetLeft,
33
+ offsetTop: canvasOffsetTop,
21
34
  logLevel,
22
35
  });
36
+ const afterCompose = Date.now();
37
+ const transformed = transformIn3d({
38
+ canvasWidth: tempCanvasWidth,
39
+ canvasHeight: tempCanvasHeight,
40
+ matrix: totalMatrix,
41
+ sourceCanvas: tempCanvas,
42
+ offsetLeft: canvasOffsetLeft,
43
+ offsetTop: canvasOffsetTop,
44
+ });
45
+ context.drawImage(transformed, 0, 0);
46
+ const afterDraw = Date.now();
23
47
  reset();
48
+ Internals.Log.trace({
49
+ logLevel,
50
+ tag: '@remotion/web-renderer',
51
+ }, `Transforming element in 3D - canvas size: ${tempCanvasWidth}x${tempCanvasHeight} - compose: ${afterCompose - start}ms - draw: ${afterDraw - afterCompose}ms`);
24
52
  return 'skip-children';
25
53
  }
26
54
  await drawElement({
@@ -1,6 +1,7 @@
1
1
  import type { LogLevel } from 'remotion';
2
+ import type { InternalState } from '../internal-state';
2
3
  import type { DrawFn } from './drawn-fn';
3
- export declare const drawElement: ({ rect, computedStyle, context, draw, opacity, totalMatrix, parentRect, logLevel, }: {
4
+ export declare const drawElement: ({ rect, computedStyle, context, draw, opacity, totalMatrix, parentRect, logLevel, element, internalState, }: {
4
5
  rect: DOMRect;
5
6
  computedStyle: CSSStyleDeclaration;
6
7
  context: OffscreenCanvasRenderingContext2D;
@@ -9,6 +10,8 @@ export declare const drawElement: ({ rect, computedStyle, context, draw, opacity
9
10
  draw: DrawFn;
10
11
  parentRect: DOMRect;
11
12
  logLevel: LogLevel;
13
+ element: HTMLElement | SVGElement;
14
+ internalState: InternalState;
12
15
  }) => Promise<{
13
16
  cleanupAfterChildren: () => void;
14
17
  }>;
@@ -1,14 +1,13 @@
1
1
  import { parseBorderRadius, setBorderRadius } from './border-radius';
2
+ import { drawBackground } from './draw-background';
2
3
  import { drawBorder } from './draw-border';
3
- import { setBoxShadow } from './draw-box-shadow';
4
+ import { drawBorderRadius } from './draw-box-shadow';
4
5
  import { drawOutline } from './draw-outline';
5
6
  import { setOpacity } from './opacity';
6
7
  import { setOverflowHidden } from './overflow';
7
- import { createCanvasGradient, parseLinearGradient, } from './parse-linear-gradient';
8
8
  import { setTransform } from './transform';
9
- export const drawElement = async ({ rect, computedStyle, context, draw, opacity, totalMatrix, parentRect, logLevel, }) => {
10
- const background = computedStyle.backgroundColor;
11
- const { backgroundImage } = computedStyle;
9
+ export const drawElement = async ({ rect, computedStyle, context, draw, opacity, totalMatrix, parentRect, logLevel, element, internalState, }) => {
10
+ const { backgroundImage, backgroundColor, backgroundClip } = computedStyle;
12
11
  const borderRadius = parseBorderRadius({
13
12
  borderRadius: computedStyle.borderRadius,
14
13
  width: rect.width,
@@ -24,7 +23,7 @@ export const drawElement = async ({ rect, computedStyle, context, draw, opacity,
24
23
  opacity,
25
24
  });
26
25
  // Draw box shadow before border radius clip and background
27
- setBoxShadow({
26
+ drawBorderRadius({
28
27
  ctx: context,
29
28
  computedStyle,
30
29
  rect,
@@ -36,43 +35,28 @@ export const drawElement = async ({ rect, computedStyle, context, draw, opacity,
36
35
  rect,
37
36
  borderRadius,
38
37
  forceClipEvenWhenZero: false,
38
+ computedStyle,
39
+ backgroundClip,
40
+ });
41
+ await drawBackground({
42
+ backgroundImage,
43
+ context,
44
+ rect,
45
+ backgroundColor,
46
+ backgroundClip,
47
+ element,
48
+ logLevel,
49
+ internalState,
50
+ computedStyle,
39
51
  });
40
- // Try to draw linear gradient first
41
- let gradientDrawn = false;
42
- if (backgroundImage && backgroundImage !== 'none') {
43
- const gradientInfo = parseLinearGradient(backgroundImage);
44
- if (gradientInfo) {
45
- const gradient = createCanvasGradient({
46
- ctx: context,
47
- rect,
48
- gradientInfo,
49
- });
50
- const originalFillStyle = context.fillStyle;
51
- context.fillStyle = gradient;
52
- context.fillRect(rect.left, rect.top, rect.width, rect.height);
53
- context.fillStyle = originalFillStyle;
54
- gradientDrawn = true;
55
- }
56
- }
57
- // Fallback to solid background color if no gradient was drawn
58
- if (!gradientDrawn &&
59
- background &&
60
- background !== 'transparent' &&
61
- !(background.startsWith('rgba') &&
62
- (background.endsWith(', 0)') || background.endsWith(',0')))) {
63
- const originalFillStyle = context.fillStyle;
64
- context.fillStyle = background;
65
- context.fillRect(rect.left, rect.top, rect.width, rect.height);
66
- context.fillStyle = originalFillStyle;
67
- }
68
52
  await draw({ dimensions: rect, computedStyle, contextToDraw: context });
53
+ finishBorderRadius();
69
54
  drawBorder({
70
55
  ctx: context,
71
56
  rect,
72
57
  borderRadius,
73
58
  computedStyle,
74
59
  });
75
- finishBorderRadius();
76
60
  // Drawing outline ignores overflow: hidden, finishing it and starting a new one for the outline
77
61
  drawOutline({
78
62
  ctx: context,
@@ -85,6 +69,8 @@ export const drawElement = async ({ rect, computedStyle, context, draw, opacity,
85
69
  rect,
86
70
  borderRadius,
87
71
  overflowHidden: computedStyle.overflow === 'hidden',
72
+ computedStyle,
73
+ backgroundClip,
88
74
  });
89
75
  finishTransform();
90
76
  return {
@@ -0,0 +1,12 @@
1
+ export declare const fitSvgIntoItsContainer: ({ containerSize, elementSize, }: {
2
+ containerSize: DOMRect;
3
+ elementSize: {
4
+ width: number;
5
+ height: number;
6
+ };
7
+ }) => {
8
+ width: number;
9
+ height: number;
10
+ top: number;
11
+ left: number;
12
+ };
@@ -0,0 +1,35 @@
1
+ // The bitmap created from the SVG height and width might not be what we expect.
2
+ // Adjust the dimensions
3
+ export const fitSvgIntoItsContainer = ({ containerSize, elementSize, }) => {
4
+ // If was already fitting, no need to calculate and lose precision
5
+ if (Math.round(containerSize.width) === Math.round(elementSize.width) &&
6
+ Math.round(containerSize.height) === Math.round(elementSize.height)) {
7
+ return {
8
+ width: containerSize.width,
9
+ height: containerSize.height,
10
+ top: containerSize.top,
11
+ left: containerSize.left,
12
+ };
13
+ }
14
+ if (containerSize.width <= 0 || containerSize.height <= 0) {
15
+ throw new Error(`Container must have positive dimensions, but got ${containerSize.width}x${containerSize.height}`);
16
+ }
17
+ if (elementSize.width <= 0 || elementSize.height <= 0) {
18
+ throw new Error(`Element must have positive dimensions, but got ${elementSize.width}x${elementSize.height}`);
19
+ }
20
+ const heightRatio = containerSize.height / elementSize.height;
21
+ const widthRatio = containerSize.width / elementSize.width;
22
+ const ratio = Math.min(heightRatio, widthRatio);
23
+ const newWidth = elementSize.width * ratio;
24
+ const newHeight = elementSize.height * ratio;
25
+ if (newWidth > containerSize.width + 0.000001 ||
26
+ newHeight > containerSize.height + 0.000001) {
27
+ throw new Error(`Element is too big to fit into the container. Max size: ${containerSize.width}x${containerSize.height}, element size: ${newWidth}x${newHeight}`);
28
+ }
29
+ return {
30
+ width: newWidth,
31
+ height: newHeight,
32
+ top: (containerSize.height - newHeight) / 2 + containerSize.top,
33
+ left: (containerSize.width - newWidth) / 2 + containerSize.left,
34
+ };
35
+ };
@@ -0,0 +1,8 @@
1
+ import type { LogLevel } from 'remotion';
2
+ import type { InternalState } from '../internal-state';
3
+ export declare const getClippedBackground: ({ element, boundingRect, logLevel, internalState, }: {
4
+ element: HTMLElement | SVGElement;
5
+ boundingRect: DOMRect;
6
+ logLevel: LogLevel;
7
+ internalState: InternalState;
8
+ }) => Promise<OffscreenCanvasRenderingContext2D>;
@@ -0,0 +1,14 @@
1
+ import { compose } from '../compose';
2
+ export const getClippedBackground = async ({ element, boundingRect, logLevel, internalState, }) => {
3
+ const tempCanvas = new OffscreenCanvas(boundingRect.width, boundingRect.height);
4
+ const tempContext = tempCanvas.getContext('2d');
5
+ await compose({
6
+ element,
7
+ context: tempContext,
8
+ logLevel,
9
+ parentRect: boundingRect,
10
+ internalState,
11
+ onlyBackgroundClip: true,
12
+ });
13
+ return tempContext;
14
+ };
@@ -0,0 +1 @@
1
+ export declare const getBoxBasedOnBackgroundClip: (rect: DOMRect, computedStyle: CSSStyleDeclaration, backgroundClip: string | undefined) => DOMRect;
@@ -0,0 +1,30 @@
1
+ const getPaddingBox = (rect, computedStyle) => {
2
+ const borderLeft = parseFloat(computedStyle.borderLeftWidth);
3
+ const borderRight = parseFloat(computedStyle.borderRightWidth);
4
+ const borderTop = parseFloat(computedStyle.borderTopWidth);
5
+ const borderBottom = parseFloat(computedStyle.borderBottomWidth);
6
+ return new DOMRect(rect.left + borderLeft, rect.top + borderTop, rect.width - borderLeft - borderRight, rect.height - borderTop - borderBottom);
7
+ };
8
+ const getContentBox = (rect, computedStyle) => {
9
+ const paddingBox = getPaddingBox(rect, computedStyle);
10
+ const paddingLeft = parseFloat(computedStyle.paddingLeft);
11
+ const paddingRight = parseFloat(computedStyle.paddingRight);
12
+ const paddingTop = parseFloat(computedStyle.paddingTop);
13
+ const paddingBottom = parseFloat(computedStyle.paddingBottom);
14
+ return new DOMRect(paddingBox.left + paddingLeft, paddingBox.top + paddingTop, paddingBox.width - paddingLeft - paddingRight, paddingBox.height - paddingTop - paddingBottom);
15
+ };
16
+ export const getBoxBasedOnBackgroundClip = (rect, computedStyle, backgroundClip) => {
17
+ if (!backgroundClip) {
18
+ return rect;
19
+ }
20
+ if (backgroundClip.includes('text')) {
21
+ return rect;
22
+ }
23
+ if (backgroundClip.includes('padding-box')) {
24
+ return getPaddingBox(rect, computedStyle);
25
+ }
26
+ if (backgroundClip.includes('content-box')) {
27
+ return getContentBox(rect, computedStyle);
28
+ }
29
+ return rect;
30
+ };
@@ -1,3 +1,7 @@
1
+ // In some cases, we get a matrix that is too compressed:
2
+ // e.g. https://github.com/remotion-dev/remotion/issues/6185
3
+ // > You're rotating around the X-axis by ~89.96°, which means the Y-axis gets compressed to ⁠cos(89.96°) ≈ 0.000691 of its original size in the viewport.
4
+ const MAX_SCALE_FACTOR = 100;
1
5
  export function getPreTransformRect(targetRect, matrix) {
2
6
  // 1. Determine the effective 2D transformation by transforming basis vectors
3
7
  const origin = new DOMPoint(0, 0).matrixTransform(matrix);
@@ -6,6 +10,15 @@ export function getPreTransformRect(targetRect, matrix) {
6
10
  // 2. Compute the 2D basis vectors after transformation
7
11
  const basisX = { x: unitX.x - origin.x, y: unitX.y - origin.y };
8
12
  const basisY = { x: unitY.x - origin.x, y: unitY.y - origin.y };
13
+ // Check effective scale in each axis
14
+ const scaleX = Math.hypot(basisX.x, basisX.y);
15
+ const scaleY = Math.hypot(basisY.x, basisY.y);
16
+ // If either axis is too compressed, the inverse will explode
17
+ const minScale = Math.min(scaleX, scaleY);
18
+ if (minScale < 1 / MAX_SCALE_FACTOR) {
19
+ // Content is nearly invisible, e.g. 89.96deg X rotation (edge-on view)
20
+ return new DOMRect(0, 0, 0, 0);
21
+ }
9
22
  // 3. Build the effective 2D matrix and invert it
10
23
  const effective2D = new DOMMatrix([
11
24
  basisX.x,
@@ -0,0 +1 @@
1
+ export declare function doRectsIntersect(rect1: DOMRect, rect2: DOMRect): boolean;
@@ -0,0 +1,6 @@
1
+ export function doRectsIntersect(rect1, rect2) {
2
+ return !(rect1.right <= rect2.left ||
3
+ rect1.left >= rect2.right ||
4
+ rect1.bottom <= rect2.top ||
5
+ rect1.top >= rect2.bottom);
6
+ }
@@ -1,11 +1,13 @@
1
+ import type { InternalState } from '../internal-state';
1
2
  export declare const getPrecomposeRectFor3DTransform: ({ element, parentRect, matrix, }: {
2
3
  element: HTMLElement | SVGElement;
3
4
  parentRect: DOMRect;
4
5
  matrix: DOMMatrix;
5
6
  }) => DOMRect;
6
- export declare const handle3dTransform: ({ matrix, precomposeRect, tempCanvas, rectAfterTransforms, }: {
7
+ export declare const handle3dTransform: ({ matrix, precomposeRect, tempCanvas, rectAfterTransforms, internalState, }: {
7
8
  matrix: DOMMatrix;
8
9
  precomposeRect: DOMRect;
9
10
  tempCanvas: OffscreenCanvas;
10
11
  rectAfterTransforms: DOMRect;
11
- }) => readonly [OffscreenCanvas, () => void] | null;
12
+ internalState: InternalState;
13
+ }) => OffscreenCanvas | null;
@@ -11,15 +11,16 @@ export const getPrecomposeRectFor3DTransform = ({ element, parentRect, matrix, }
11
11
  });
12
12
  return preTransformRect;
13
13
  };
14
- export const handle3dTransform = ({ matrix, precomposeRect, tempCanvas, rectAfterTransforms, }) => {
15
- const { canvas: transformed, rect: transformedRect, cleanup, } = transformIn3d({
14
+ export const handle3dTransform = ({ matrix, precomposeRect, tempCanvas, rectAfterTransforms, internalState, }) => {
15
+ const { canvas: transformed, rect: transformedRect } = transformIn3d({
16
16
  untransformedRect: precomposeRect,
17
17
  matrix,
18
18
  sourceCanvas: tempCanvas,
19
19
  rectAfterTransforms,
20
+ internalState,
20
21
  });
21
22
  if (transformedRect.width <= 0 || transformedRect.height <= 0) {
22
23
  return null;
23
24
  }
24
- return [transformed, cleanup];
25
+ return transformed;
25
26
  };
@@ -12,6 +12,8 @@ export const handleMask = ({ gradientInfo, rect, precomposeRect, tempContext, })
12
12
  ctx: tempContext,
13
13
  rect: rectToFill,
14
14
  gradientInfo,
15
+ offsetLeft: 0,
16
+ offsetTop: 0,
15
17
  });
16
18
  tempContext.globalCompositeOperation = 'destination-in';
17
19
  tempContext.fillStyle = gradient;
@@ -1,7 +1,9 @@
1
1
  import type { BorderRadiusCorners } from './border-radius';
2
- export declare const setOverflowHidden: ({ ctx, rect, borderRadius, overflowHidden, }: {
2
+ export declare const setOverflowHidden: ({ ctx, rect, borderRadius, overflowHidden, computedStyle, backgroundClip, }: {
3
3
  ctx: OffscreenCanvasRenderingContext2D;
4
4
  rect: DOMRect;
5
5
  borderRadius: BorderRadiusCorners;
6
6
  overflowHidden: boolean;
7
+ computedStyle: CSSStyleDeclaration;
8
+ backgroundClip: string;
7
9
  }) => () => void;
@@ -1,5 +1,5 @@
1
1
  import { setBorderRadius } from './border-radius';
2
- export const setOverflowHidden = ({ ctx, rect, borderRadius, overflowHidden, }) => {
2
+ export const setOverflowHidden = ({ ctx, rect, borderRadius, overflowHidden, computedStyle, backgroundClip, }) => {
3
3
  if (!overflowHidden) {
4
4
  return () => { };
5
5
  }
@@ -8,5 +8,7 @@ export const setOverflowHidden = ({ ctx, rect, borderRadius, overflowHidden, })
8
8
  rect,
9
9
  borderRadius,
10
10
  forceClipEvenWhenZero: true,
11
+ computedStyle,
12
+ backgroundClip,
11
13
  });
12
14
  };
@@ -7,8 +7,10 @@ export interface LinearGradientInfo {
7
7
  colorStops: ColorStop[];
8
8
  }
9
9
  export declare const parseLinearGradient: (backgroundImage: string) => LinearGradientInfo | null;
10
- export declare const createCanvasGradient: ({ ctx, rect, gradientInfo, }: {
10
+ export declare const createCanvasGradient: ({ ctx, rect, gradientInfo, offsetLeft, offsetTop, }: {
11
11
  ctx: OffscreenCanvasRenderingContext2D;
12
12
  rect: DOMRect;
13
13
  gradientInfo: LinearGradientInfo;
14
+ offsetLeft: number;
15
+ offsetTop: number;
14
16
  }) => CanvasGradient;
@@ -226,13 +226,13 @@ export const parseLinearGradient = (backgroundImage) => {
226
226
  colorStops,
227
227
  };
228
228
  };
229
- export const createCanvasGradient = ({ ctx, rect, gradientInfo, }) => {
229
+ export const createCanvasGradient = ({ ctx, rect, gradientInfo, offsetLeft, offsetTop, }) => {
230
230
  // Convert angle to radians
231
231
  // CSS angles: 0deg = to top, 90deg = to right, 180deg = to bottom, 270deg = to left
232
232
  // We need to calculate the gradient line that spans the rectangle at the given angle
233
233
  const angleRad = ((gradientInfo.angle - 90) * Math.PI) / 180;
234
- const centerX = rect.left + rect.width / 2;
235
- const centerY = rect.top + rect.height / 2;
234
+ const centerX = rect.left - offsetLeft + rect.width / 2;
235
+ const centerY = rect.top - offsetTop + rect.height / 2;
236
236
  // Calculate gradient line endpoints
237
237
  // The gradient line passes through the center and has the specified angle
238
238
  const cos = Math.cos(angleRad);
@@ -8,6 +8,7 @@ export const precomposeDOMElement = async ({ boundingRect, element, logLevel, in
8
8
  logLevel,
9
9
  parentRect: boundingRect,
10
10
  internalState,
11
+ onlyBackgroundClip: false,
11
12
  });
12
13
  return { tempCanvas, tempContext };
13
14
  };
@@ -57,7 +57,6 @@ export const processNode = async ({ element, context, draw, logLevel, parentRect
57
57
  internalState,
58
58
  });
59
59
  let drawable = tempCanvas;
60
- let cleanupWebGL = () => { };
61
60
  const rectAfterTransforms = roundToExpandRect(transformDOMRect({
62
61
  rect: precomposeRect,
63
62
  matrix: totalMatrix,
@@ -76,17 +75,16 @@ export const processNode = async ({ element, context, draw, logLevel, parentRect
76
75
  precomposeRect,
77
76
  tempCanvas: drawable,
78
77
  rectAfterTransforms,
78
+ internalState,
79
79
  });
80
80
  if (t) {
81
- const [transformed, cleanup] = t;
82
- drawable = transformed;
83
- cleanupWebGL = cleanup;
81
+ drawable = t;
84
82
  }
85
83
  }
86
84
  const previousTransform = context.getTransform();
87
85
  if (drawable) {
88
86
  context.setTransform(new DOMMatrix());
89
- context.drawImage(drawable, rectAfterTransforms.left - parentRect.x, rectAfterTransforms.top - parentRect.y, rectAfterTransforms.width, rectAfterTransforms.height);
87
+ context.drawImage(drawable, 0, drawable.height - rectAfterTransforms.height, rectAfterTransforms.width, rectAfterTransforms.height, rectAfterTransforms.left - parentRect.x, rectAfterTransforms.top - parentRect.y, rectAfterTransforms.width, rectAfterTransforms.height);
90
88
  context.setTransform(previousTransform);
91
89
  Internals.Log.trace({
92
90
  logLevel,
@@ -98,7 +96,6 @@ export const processNode = async ({ element, context, draw, logLevel, parentRect
98
96
  });
99
97
  }
100
98
  reset();
101
- cleanupWebGL();
102
99
  return { type: 'skip-children' };
103
100
  }
104
101
  const { cleanupAfterChildren } = await drawElement({
@@ -110,6 +107,8 @@ export const processNode = async ({ element, context, draw, logLevel, parentRect
110
107
  totalMatrix,
111
108
  parentRect,
112
109
  logLevel,
110
+ element,
111
+ internalState,
113
112
  });
114
113
  reset();
115
114
  return { type: 'continue', cleanupAfterChildren };
@@ -1,6 +1,7 @@
1
1
  import type { LogLevel } from 'remotion';
2
2
  import type { DrawFn } from '../drawn-fn';
3
- export declare const drawText: ({ span, logLevel, }: {
3
+ export declare const drawText: ({ span, logLevel, onlyBackgroundClip, }: {
4
4
  span: HTMLSpanElement;
5
5
  logLevel: LogLevel;
6
+ onlyBackgroundClip: boolean;
6
7
  }) => DrawFn;
@@ -2,9 +2,9 @@ import { Internals } from 'remotion';
2
2
  import { applyTextTransform } from './apply-text-transform';
3
3
  import { findLineBreaks } from './find-line-breaks.text';
4
4
  import { getCollapsedText } from './get-collapsed-text';
5
- export const drawText = ({ span, logLevel, }) => {
5
+ export const drawText = ({ span, logLevel, onlyBackgroundClip, }) => {
6
6
  const drawFn = ({ dimensions: rect, computedStyle, contextToDraw }) => {
7
- const { fontFamily, fontSize, fontWeight, color, direction, writingMode, letterSpacing, textTransform, } = computedStyle;
7
+ const { fontFamily, fontSize, fontWeight, direction, writingMode, letterSpacing, textTransform, webkitTextFillColor, } = computedStyle;
8
8
  const isVertical = writingMode !== 'horizontal-tb';
9
9
  if (isVertical) {
10
10
  // TODO: Only warn once per render.
@@ -17,7 +17,13 @@ export const drawText = ({ span, logLevel, }) => {
17
17
  contextToDraw.save();
18
18
  const fontSizePx = parseFloat(fontSize);
19
19
  contextToDraw.font = `${fontWeight} ${fontSizePx}px ${fontFamily}`;
20
- contextToDraw.fillStyle = color;
20
+ contextToDraw.fillStyle =
21
+ // If text is being applied with backgroundClipText, we need to use a solid color otherwise it won't get
22
+ // applied in canvas
23
+ onlyBackgroundClip
24
+ ? 'black'
25
+ : // -webkit-text-fill-color overrides color, and defaults to the value of `color`
26
+ webkitTextFillColor;
21
27
  contextToDraw.letterSpacing = letterSpacing;
22
28
  const isRTL = direction === 'rtl';
23
29
  contextToDraw.textAlign = isRTL ? 'right' : 'left';
@@ -0,0 +1 @@
1
+ export declare const getOpticalHeight: (span: HTMLSpanElement) => number;
@@ -0,0 +1,13 @@
1
+ // A text with e.g. font-size: 12px does not always get rendered with 12px
2
+ // With "Arial", it gets rendered with 13.5px height, with SF Pro, it gets
3
+ // rendered with 14px height. I therefore conclude there is no logic
4
+ // and we need to measure it. I rendered the string "1", "a" and "qË" and
5
+ // they all have the same height, so it doesn't matter if certain characters
6
+ // go low or high.
7
+ export const getOpticalHeight = (span) => {
8
+ const originalText = span.textContent;
9
+ span.textContent = '1';
10
+ const { height } = span.getBoundingClientRect();
11
+ span.textContent = originalText;
12
+ return height;
13
+ };