@remotion/web-renderer 4.0.396 → 4.0.397

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 (48) hide show
  1. package/dist/compose.js +11 -5
  2. package/dist/create-scaffold.js +10 -3
  3. package/dist/drawing/border-radius.js +9 -32
  4. package/dist/drawing/calculate-transforms.d.ts +9 -3
  5. package/dist/drawing/calculate-transforms.js +31 -9
  6. package/dist/drawing/clamp-rect-to-parent-bounds.d.ts +4 -0
  7. package/dist/drawing/clamp-rect-to-parent-bounds.js +11 -0
  8. package/dist/drawing/do-rects-intersect.d.ts +1 -0
  9. package/dist/drawing/do-rects-intersect.js +6 -0
  10. package/dist/drawing/draw-box-shadow.d.ts +18 -0
  11. package/dist/drawing/draw-box-shadow.js +103 -0
  12. package/dist/drawing/draw-element.d.ts +4 -1
  13. package/dist/drawing/draw-element.js +37 -6
  14. package/dist/drawing/draw-outline.js +9 -32
  15. package/dist/drawing/draw-rounded.d.ts +9 -0
  16. package/dist/drawing/draw-rounded.js +34 -0
  17. package/dist/drawing/get-pretransform-rect.js +5 -0
  18. package/dist/drawing/handle-3d-transform.d.ts +9 -8
  19. package/dist/drawing/handle-3d-transform.js +11 -25
  20. package/dist/drawing/handle-mask.d.ts +8 -0
  21. package/dist/drawing/handle-mask.js +19 -0
  22. package/dist/drawing/mask-image.d.ts +3 -0
  23. package/dist/drawing/mask-image.js +14 -0
  24. package/dist/drawing/parse-linear-gradient.d.ts +14 -0
  25. package/dist/drawing/parse-linear-gradient.js +260 -0
  26. package/dist/drawing/precompose.d.ts +11 -0
  27. package/dist/drawing/precompose.js +13 -0
  28. package/dist/drawing/process-node.d.ts +4 -3
  29. package/dist/drawing/process-node.js +89 -14
  30. package/dist/drawing/round-to-expand-rect.d.ts +1 -0
  31. package/dist/drawing/round-to-expand-rect.js +7 -0
  32. package/dist/drawing/text/draw-text.d.ts +5 -1
  33. package/dist/drawing/text/draw-text.js +10 -5
  34. package/dist/drawing/text/find-line-breaks.text.d.ts +1 -1
  35. package/dist/drawing/text/find-line-breaks.text.js +2 -2
  36. package/dist/drawing/text/handle-text-node.d.ts +2 -1
  37. package/dist/drawing/text/handle-text-node.js +3 -2
  38. package/dist/drawing/transform-in-3d.d.ts +3 -1
  39. package/dist/drawing/transform-in-3d.js +30 -28
  40. package/dist/drawing/transform.d.ts +2 -1
  41. package/dist/drawing/transform.js +6 -2
  42. package/dist/esm/index.mjs +788 -211
  43. package/dist/get-biggest-bounding-client-rect.js +19 -4
  44. package/dist/internal-state.d.ts +2 -2
  45. package/dist/internal-state.js +7 -7
  46. package/dist/render-media-on-web.d.ts +1 -0
  47. package/dist/render-media-on-web.js +22 -14
  48. package/package.json +6 -6
package/dist/compose.js CHANGED
@@ -2,7 +2,7 @@ import { drawDomElement } from './drawing/draw-dom-element';
2
2
  import { processNode } from './drawing/process-node';
3
3
  import { handleTextNode } from './drawing/text/handle-text-node';
4
4
  import { skipToNextNonDescendant } from './walk-tree';
5
- const walkOverNode = ({ node, context, logLevel, parentRect, internalState, }) => {
5
+ const walkOverNode = ({ node, context, logLevel, parentRect, internalState, rootElement, }) => {
6
6
  if (node instanceof HTMLElement || node instanceof SVGElement) {
7
7
  return processNode({
8
8
  element: node,
@@ -11,6 +11,7 @@ const walkOverNode = ({ node, context, logLevel, parentRect, internalState, }) =
11
11
  logLevel,
12
12
  parentRect,
13
13
  internalState,
14
+ rootElement,
14
15
  });
15
16
  }
16
17
  if (node instanceof Text) {
@@ -20,6 +21,7 @@ const walkOverNode = ({ node, context, logLevel, parentRect, internalState, }) =
20
21
  logLevel,
21
22
  parentRect,
22
23
  internalState,
24
+ rootElement,
23
25
  });
24
26
  }
25
27
  throw new Error('Unknown node type');
@@ -58,6 +60,7 @@ export const compose = async ({ element, context, logLevel, parentRect, internal
58
60
  logLevel,
59
61
  parentRect,
60
62
  internalState,
63
+ rootElement: element,
61
64
  });
62
65
  if (val.type === 'skip-children') {
63
66
  if (!skipToNextNonDescendant(treeWalker)) {
@@ -65,10 +68,13 @@ export const compose = async ({ element, context, logLevel, parentRect, internal
65
68
  }
66
69
  }
67
70
  else {
68
- cleanupAfterChildren.push({
69
- element: treeWalker.currentNode,
70
- cleanupFn: val.cleanupAfterChildren,
71
- });
71
+ if (val.cleanupAfterChildren) {
72
+ // Last registered must be cleaned up first
73
+ cleanupAfterChildren.unshift({
74
+ element: treeWalker.currentNode,
75
+ cleanupFn: val.cleanupAfterChildren,
76
+ });
77
+ }
72
78
  if (!treeWalker.nextNode()) {
73
79
  break;
74
80
  }
@@ -10,17 +10,23 @@ export async function createScaffold({ width, height, delayRenderTimeoutInMillis
10
10
  throw new Error('@remotion/web-renderer requires React 18 or higher');
11
11
  }
12
12
  const div = document.createElement('div');
13
- // Match same behavior as renderEntry.tsx
13
+ // Match same behavior as in portal-node.ts
14
+ div.style.position = 'fixed';
14
15
  div.style.display = 'flex';
16
+ div.style.flexDirection = 'column';
15
17
  div.style.backgroundColor = 'transparent';
16
- div.style.position = 'fixed';
17
18
  div.style.width = `${width}px`;
18
19
  div.style.height = `${height}px`;
19
20
  div.style.zIndex = '-9999';
20
21
  div.style.top = '0';
21
- div.style.visibility = 'hidden';
22
22
  div.style.left = '0';
23
+ div.style.right = '0';
24
+ div.style.bottom = '0';
25
+ div.style.visibility = 'hidden';
23
26
  div.style.pointerEvents = 'none';
27
+ const scaffoldClassName = `remotion-scaffold-${Math.random().toString(36).substring(2, 15)}`;
28
+ div.className = scaffoldClassName;
29
+ const cleanupCSS = Internals.CSSUtils.injectCSS(Internals.CSSUtils.makeDefaultPreviewCSS(`.${scaffoldClassName}`, 'white'));
24
30
  document.body.appendChild(div);
25
31
  const { promise, resolve, reject } = withResolvers();
26
32
  // TODO: This might not work in React 18
@@ -90,6 +96,7 @@ export async function createScaffold({ width, height, delayRenderTimeoutInMillis
90
96
  cleanupScaffold: () => {
91
97
  root.unmount();
92
98
  div.remove();
99
+ cleanupCSS();
93
100
  },
94
101
  timeUpdater,
95
102
  collectAssets,
@@ -1,3 +1,4 @@
1
+ import { drawRoundedRectPath } from './draw-rounded';
1
2
  function parseValue({ value, reference, }) {
2
3
  value = value.trim();
3
4
  if (value.endsWith('%')) {
@@ -114,38 +115,14 @@ export function setBorderRadius({ ctx, rect, borderRadius, forceClipEvenWhenZero
114
115
  return () => { };
115
116
  }
116
117
  ctx.save();
117
- ctx.beginPath();
118
- // Start at top-left corner, after the horizontal radius
119
- ctx.moveTo(rect.left + borderRadius.topLeft.horizontal, rect.top);
120
- // Top edge to top-right corner
121
- ctx.lineTo(rect.left + rect.width - borderRadius.topRight.horizontal, rect.top);
122
- // Top-right corner (elliptical arc)
123
- if (borderRadius.topRight.horizontal > 0 ||
124
- borderRadius.topRight.vertical > 0) {
125
- ctx.ellipse(rect.left + rect.width - borderRadius.topRight.horizontal, rect.top + borderRadius.topRight.vertical, borderRadius.topRight.horizontal, borderRadius.topRight.vertical, 0, -Math.PI / 2, 0);
126
- }
127
- // Right edge to bottom-right corner
128
- ctx.lineTo(rect.left + rect.width, rect.top + rect.height - borderRadius.bottomRight.vertical);
129
- // Bottom-right corner (elliptical arc)
130
- if (borderRadius.bottomRight.horizontal > 0 ||
131
- borderRadius.bottomRight.vertical > 0) {
132
- ctx.ellipse(rect.left + rect.width - borderRadius.bottomRight.horizontal, rect.top + rect.height - borderRadius.bottomRight.vertical, borderRadius.bottomRight.horizontal, borderRadius.bottomRight.vertical, 0, 0, Math.PI / 2);
133
- }
134
- // Bottom edge to bottom-left corner
135
- ctx.lineTo(rect.left + borderRadius.bottomLeft.horizontal, rect.top + rect.height);
136
- // Bottom-left corner (elliptical arc)
137
- if (borderRadius.bottomLeft.horizontal > 0 ||
138
- borderRadius.bottomLeft.vertical > 0) {
139
- ctx.ellipse(rect.left + borderRadius.bottomLeft.horizontal, rect.top + rect.height - borderRadius.bottomLeft.vertical, borderRadius.bottomLeft.horizontal, borderRadius.bottomLeft.vertical, 0, Math.PI / 2, Math.PI);
140
- }
141
- // Left edge to top-left corner
142
- ctx.lineTo(rect.left, rect.top + borderRadius.topLeft.vertical);
143
- // Top-left corner (elliptical arc)
144
- if (borderRadius.topLeft.horizontal > 0 ||
145
- borderRadius.topLeft.vertical > 0) {
146
- ctx.ellipse(rect.left + borderRadius.topLeft.horizontal, rect.top + borderRadius.topLeft.vertical, borderRadius.topLeft.horizontal, borderRadius.topLeft.vertical, 0, Math.PI, (Math.PI * 3) / 2);
147
- }
148
- ctx.closePath();
118
+ drawRoundedRectPath({
119
+ ctx,
120
+ x: rect.left,
121
+ y: rect.top,
122
+ width: rect.width,
123
+ height: rect.height,
124
+ borderRadius,
125
+ });
149
126
  ctx.clip();
150
127
  return () => {
151
128
  ctx.restore();
@@ -1,7 +1,7 @@
1
- export declare const calculateTransforms: ({ element, offsetLeft, offsetTop, }: {
1
+ import type { LinearGradientInfo } from './parse-linear-gradient';
2
+ export declare const calculateTransforms: ({ element, rootElement, }: {
2
3
  element: HTMLElement | SVGElement;
3
- offsetLeft: number;
4
- offsetTop: number;
4
+ rootElement: HTMLElement | SVGElement;
5
5
  }) => {
6
6
  dimensions: DOMRect;
7
7
  totalMatrix: DOMMatrix;
@@ -12,4 +12,10 @@ export declare const calculateTransforms: ({ element, offsetLeft, offsetTop, }:
12
12
  };
13
13
  computedStyle: CSSStyleDeclaration;
14
14
  opacity: number;
15
+ maskImageInfo: LinearGradientInfo | null;
16
+ precompositing: {
17
+ needs3DTransformViaWebGL: boolean;
18
+ needsMaskImage: LinearGradientInfo | null;
19
+ needsPrecompositing: boolean;
20
+ };
15
21
  };
@@ -1,4 +1,5 @@
1
1
  import { hasAnyTransformCssValue, hasTransformCssValue } from './has-transform';
2
+ import { getMaskImageValue, parseMaskImage } from './mask-image';
2
3
  import { parseTransformOrigin } from './parse-transform-origin';
3
4
  const getInternalTransformOrigin = (transform) => {
4
5
  var _a;
@@ -10,23 +11,37 @@ const getInternalTransformOrigin = (transform) => {
10
11
  };
11
12
  return origin;
12
13
  };
13
- const getGlobalTransformOrigin = ({ transform, offsetLeft, offsetTop, }) => {
14
+ const getGlobalTransformOrigin = ({ transform }) => {
14
15
  const { x: originX, y: originY } = getInternalTransformOrigin(transform);
15
16
  return {
16
- x: originX + transform.boundingClientRect.left - offsetLeft,
17
- y: originY + transform.boundingClientRect.top - offsetTop,
17
+ x: originX + transform.boundingClientRect.left,
18
+ y: originY + transform.boundingClientRect.top,
18
19
  };
19
20
  };
20
- export const calculateTransforms = ({ element, offsetLeft, offsetTop, }) => {
21
+ export const calculateTransforms = ({ element, rootElement, }) => {
21
22
  // Compute the cumulative transform by traversing parent nodes
22
23
  let parent = element;
23
24
  const transforms = [];
24
25
  const toReset = [];
26
+ let opacity = 1;
25
27
  let elementComputedStyle = null;
28
+ let maskImageInfo = null;
26
29
  while (parent) {
27
30
  const computedStyle = getComputedStyle(parent);
28
31
  if (parent === element) {
29
32
  elementComputedStyle = computedStyle;
33
+ opacity = parseFloat(computedStyle.opacity);
34
+ const maskImageValue = getMaskImageValue(computedStyle);
35
+ maskImageInfo = maskImageValue ? parseMaskImage(maskImageValue) : null;
36
+ const originalMaskImage = parent.style.maskImage;
37
+ const originalWebkitMaskImage = parent.style.webkitMaskImage;
38
+ parent.style.maskImage = 'none';
39
+ parent.style.webkitMaskImage = 'none';
40
+ const parentRef = parent;
41
+ toReset.push(() => {
42
+ parentRef.style.maskImage = originalMaskImage;
43
+ parentRef.style.webkitMaskImage = originalWebkitMaskImage;
44
+ });
30
45
  }
31
46
  if (hasAnyTransformCssValue(computedStyle) || parent === element) {
32
47
  const toParse = hasTransformCssValue(computedStyle)
@@ -63,6 +78,9 @@ export const calculateTransforms = ({ element, offsetLeft, offsetTop, }) => {
63
78
  parentRef.style.rotate = rotate;
64
79
  });
65
80
  }
81
+ if (parent === rootElement) {
82
+ break;
83
+ }
66
84
  parent = parent.parentElement;
67
85
  }
68
86
  for (const transform of transforms) {
@@ -75,8 +93,6 @@ export const calculateTransforms = ({ element, offsetLeft, offsetTop, }) => {
75
93
  for (const matrix of transform.matrices) {
76
94
  const globalTransformOrigin = getGlobalTransformOrigin({
77
95
  transform,
78
- offsetLeft,
79
- offsetTop,
80
96
  });
81
97
  const transformMatrix = new DOMMatrix()
82
98
  .translate(globalTransformOrigin.x, globalTransformOrigin.y)
@@ -88,6 +104,8 @@ export const calculateTransforms = ({ element, offsetLeft, offsetTop, }) => {
88
104
  if (!elementComputedStyle) {
89
105
  throw new Error('Element computed style not found');
90
106
  }
107
+ const needs3DTransformViaWebGL = !totalMatrix.is2D;
108
+ const needsMaskImage = maskImageInfo !== null;
91
109
  return {
92
110
  dimensions,
93
111
  totalMatrix,
@@ -98,8 +116,12 @@ export const calculateTransforms = ({ element, offsetLeft, offsetTop, }) => {
98
116
  },
99
117
  nativeTransformOrigin,
100
118
  computedStyle: elementComputedStyle,
101
- opacity: elementComputedStyle.opacity && elementComputedStyle.opacity !== ''
102
- ? parseFloat(elementComputedStyle.opacity)
103
- : 1,
119
+ opacity,
120
+ maskImageInfo,
121
+ precompositing: {
122
+ needs3DTransformViaWebGL,
123
+ needsMaskImage: maskImageInfo,
124
+ needsPrecompositing: Boolean(needs3DTransformViaWebGL || needsMaskImage),
125
+ },
104
126
  };
105
127
  };
@@ -2,3 +2,7 @@ export declare const getNarrowerRect: ({ firstRect, secondRect, }: {
2
2
  firstRect: DOMRect;
3
3
  secondRect: DOMRect;
4
4
  }) => DOMRect;
5
+ export declare const getWiderRectAndExpand: ({ firstRect, secondRect, }: {
6
+ firstRect: DOMRect | null;
7
+ secondRect: DOMRect;
8
+ }) => DOMRect;
@@ -1,3 +1,4 @@
1
+ import { roundToExpandRect } from './round-to-expand-rect';
1
2
  export const getNarrowerRect = ({ firstRect, secondRect, }) => {
2
3
  const left = Math.max(firstRect.left, secondRect.left);
3
4
  const top = Math.max(firstRect.top, secondRect.top);
@@ -5,3 +6,13 @@ export const getNarrowerRect = ({ firstRect, secondRect, }) => {
5
6
  const right = Math.min(firstRect.right, secondRect.right);
6
7
  return new DOMRect(left, top, right - left, bottom - top);
7
8
  };
9
+ export const getWiderRectAndExpand = ({ firstRect, secondRect, }) => {
10
+ if (firstRect === null) {
11
+ return roundToExpandRect(secondRect);
12
+ }
13
+ const left = Math.min(firstRect.left, secondRect.left);
14
+ const top = Math.min(firstRect.top, secondRect.top);
15
+ const bottom = Math.max(firstRect.bottom, secondRect.bottom);
16
+ const right = Math.max(firstRect.right, secondRect.right);
17
+ return roundToExpandRect(new DOMRect(left, top, right - left, bottom - top));
18
+ };
@@ -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
+ }
@@ -0,0 +1,18 @@
1
+ import type { LogLevel } from 'remotion';
2
+ import type { BorderRadiusCorners } from './border-radius';
3
+ interface BoxShadow {
4
+ offsetX: number;
5
+ offsetY: number;
6
+ blurRadius: number;
7
+ color: string;
8
+ inset: boolean;
9
+ }
10
+ export declare const parseBoxShadow: (boxShadowValue: string) => BoxShadow[];
11
+ export declare const setBoxShadow: ({ ctx, rect, borderRadius, computedStyle, logLevel, }: {
12
+ ctx: OffscreenCanvasRenderingContext2D;
13
+ rect: DOMRect;
14
+ borderRadius: BorderRadiusCorners;
15
+ computedStyle: CSSStyleDeclaration;
16
+ logLevel: LogLevel;
17
+ }) => void;
18
+ export {};
@@ -0,0 +1,103 @@
1
+ import { Internals } from 'remotion';
2
+ import { drawRoundedRectPath } from './draw-rounded';
3
+ export const parseBoxShadow = (boxShadowValue) => {
4
+ if (!boxShadowValue || boxShadowValue === 'none') {
5
+ return [];
6
+ }
7
+ const shadows = [];
8
+ // Split by comma, but respect rgba() colors
9
+ const shadowStrings = boxShadowValue.split(/,(?![^(]*\))/);
10
+ for (const shadowStr of shadowStrings) {
11
+ const trimmed = shadowStr.trim();
12
+ if (!trimmed || trimmed === 'none') {
13
+ continue;
14
+ }
15
+ const shadow = {
16
+ offsetX: 0,
17
+ offsetY: 0,
18
+ blurRadius: 0,
19
+ color: 'rgba(0, 0, 0, 0.5)',
20
+ inset: false,
21
+ };
22
+ // Check for inset
23
+ shadow.inset = /\binset\b/i.test(trimmed);
24
+ // Remove 'inset' keyword
25
+ let remaining = trimmed.replace(/\binset\b/gi, '').trim();
26
+ // Extract color (can be rgb(), rgba(), hsl(), hsla(), hex, or named color)
27
+ const colorMatch = remaining.match(/(rgba?\([^)]+\)|hsla?\([^)]+\)|#[0-9a-f]{3,8}|[a-z]+)/i);
28
+ if (colorMatch) {
29
+ shadow.color = colorMatch[0];
30
+ remaining = remaining.replace(colorMatch[0], '').trim();
31
+ }
32
+ // Parse remaining numeric values (offset-x offset-y blur spread)
33
+ const numbers = remaining.match(/[+-]?\d*\.?\d+(?:px|em|rem|%)?/gi) || [];
34
+ const values = numbers.map((n) => parseFloat(n) || 0);
35
+ if (values.length >= 2) {
36
+ shadow.offsetX = values[0];
37
+ shadow.offsetY = values[1];
38
+ if (values.length >= 3) {
39
+ shadow.blurRadius = Math.max(0, values[2]); // Blur cannot be negative
40
+ }
41
+ }
42
+ shadows.push(shadow);
43
+ }
44
+ return shadows;
45
+ };
46
+ export const setBoxShadow = ({ ctx, rect, borderRadius, computedStyle, logLevel, }) => {
47
+ const shadows = parseBoxShadow(computedStyle.boxShadow);
48
+ if (shadows.length === 0) {
49
+ return;
50
+ }
51
+ // Draw shadows from last to first (so first shadow appears on top)
52
+ for (let i = shadows.length - 1; i >= 0; i--) {
53
+ const shadow = shadows[i];
54
+ const newLeft = rect.left + Math.min(shadow.offsetX, 0) - shadow.blurRadius;
55
+ const newRight = rect.right + Math.max(shadow.offsetX, 0) + shadow.blurRadius;
56
+ const newTop = rect.top + Math.min(shadow.offsetY, 0) - shadow.blurRadius;
57
+ const newBottom = rect.bottom + Math.max(shadow.offsetY, 0) + shadow.blurRadius;
58
+ const newRect = new DOMRect(newLeft, newTop, newRight - newLeft, newBottom - newTop);
59
+ const leftOffset = rect.left - newLeft;
60
+ const topOffset = rect.top - newTop;
61
+ const newCanvas = new OffscreenCanvas(newRect.width, newRect.height);
62
+ const newCtx = newCanvas.getContext('2d');
63
+ if (!newCtx) {
64
+ throw new Error('Failed to get context');
65
+ }
66
+ if (shadow.inset) {
67
+ // TODO: Only warn once per render.
68
+ Internals.Log.warn({
69
+ logLevel,
70
+ tag: '@remotion/web-renderer',
71
+ }, 'Detected "box-shadow" with "inset". This is not yet supported in @remotion/web-renderer');
72
+ continue;
73
+ }
74
+ // Apply shadow properties to canvas
75
+ newCtx.shadowBlur = shadow.blurRadius;
76
+ newCtx.shadowColor = shadow.color;
77
+ newCtx.shadowOffsetX = shadow.offsetX;
78
+ newCtx.shadowOffsetY = shadow.offsetY;
79
+ newCtx.fillStyle = 'black';
80
+ drawRoundedRectPath({
81
+ ctx: newCtx,
82
+ x: leftOffset,
83
+ y: topOffset,
84
+ width: rect.width,
85
+ height: rect.height,
86
+ borderRadius,
87
+ });
88
+ newCtx.fill();
89
+ // Cut out the shape, leaving only shadow
90
+ newCtx.shadowColor = 'transparent';
91
+ newCtx.globalCompositeOperation = 'destination-out';
92
+ drawRoundedRectPath({
93
+ ctx: newCtx,
94
+ x: leftOffset,
95
+ y: topOffset,
96
+ width: rect.width,
97
+ height: rect.height,
98
+ borderRadius,
99
+ });
100
+ newCtx.fill();
101
+ ctx.drawImage(newCanvas, rect.left - leftOffset, rect.top - topOffset);
102
+ }
103
+ };
@@ -1,11 +1,14 @@
1
+ import type { LogLevel } from 'remotion';
1
2
  import type { DrawFn } from './drawn-fn';
2
- export declare const drawElement: ({ rect, computedStyle, context, draw, opacity, totalMatrix, }: {
3
+ export declare const drawElement: ({ rect, computedStyle, context, draw, opacity, totalMatrix, parentRect, logLevel, }: {
3
4
  rect: DOMRect;
4
5
  computedStyle: CSSStyleDeclaration;
5
6
  context: OffscreenCanvasRenderingContext2D;
6
7
  opacity: number;
7
8
  totalMatrix: DOMMatrix;
8
9
  draw: DrawFn;
10
+ parentRect: DOMRect;
11
+ logLevel: LogLevel;
9
12
  }) => Promise<{
10
13
  cleanupAfterChildren: () => void;
11
14
  }>;
@@ -1,11 +1,14 @@
1
1
  import { parseBorderRadius, setBorderRadius } from './border-radius';
2
2
  import { drawBorder } from './draw-border';
3
+ import { setBoxShadow } from './draw-box-shadow';
3
4
  import { drawOutline } from './draw-outline';
4
5
  import { setOpacity } from './opacity';
5
6
  import { setOverflowHidden } from './overflow';
7
+ import { createCanvasGradient, parseLinearGradient, } from './parse-linear-gradient';
6
8
  import { setTransform } from './transform';
7
- export const drawElement = async ({ rect, computedStyle, context, draw, opacity, totalMatrix, }) => {
9
+ export const drawElement = async ({ rect, computedStyle, context, draw, opacity, totalMatrix, parentRect, logLevel, }) => {
8
10
  const background = computedStyle.backgroundColor;
11
+ const { backgroundImage } = computedStyle;
9
12
  const borderRadius = parseBorderRadius({
10
13
  borderRadius: computedStyle.borderRadius,
11
14
  width: rect.width,
@@ -14,18 +17,46 @@ export const drawElement = async ({ rect, computedStyle, context, draw, opacity,
14
17
  const finishTransform = setTransform({
15
18
  ctx: context,
16
19
  transform: totalMatrix,
20
+ parentRect,
17
21
  });
18
- const finishBorderRadius = setBorderRadius({
22
+ const finishOpacity = setOpacity({
19
23
  ctx: context,
24
+ opacity,
25
+ });
26
+ // Draw box shadow before border radius clip and background
27
+ setBoxShadow({
28
+ ctx: context,
29
+ computedStyle,
20
30
  rect,
21
31
  borderRadius,
22
- forceClipEvenWhenZero: false,
32
+ logLevel,
23
33
  });
24
- const finishOpacity = setOpacity({
34
+ const finishBorderRadius = setBorderRadius({
25
35
  ctx: context,
26
- opacity,
36
+ rect,
37
+ borderRadius,
38
+ forceClipEvenWhenZero: false,
27
39
  });
28
- if (background &&
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 &&
29
60
  background !== 'transparent' &&
30
61
  !(background.startsWith('rgba') &&
31
62
  (background.endsWith(', 0)') || background.endsWith(',0')))) {
@@ -1,3 +1,4 @@
1
+ import { drawRoundedRectPath } from './draw-rounded';
1
2
  export const parseOutlineWidth = (value) => {
2
3
  return parseFloat(value) || 0;
3
4
  };
@@ -28,7 +29,6 @@ export const drawOutline = ({ ctx, rect, borderRadius, computedStyle, }) => {
28
29
  const originalStrokeStyle = ctx.strokeStyle;
29
30
  const originalLineWidth = ctx.lineWidth;
30
31
  const originalLineDash = ctx.getLineDash();
31
- ctx.beginPath();
32
32
  ctx.strokeStyle = outlineColor;
33
33
  ctx.lineWidth = outlineWidth;
34
34
  ctx.setLineDash(getLineDashPattern(outlineStyle, outlineWidth));
@@ -77,37 +77,14 @@ export const drawOutline = ({ ctx, rect, borderRadius, computedStyle, }) => {
77
77
  : Math.max(0, borderRadius.bottomLeft.vertical + offset),
78
78
  },
79
79
  };
80
- // Draw continuous path with border radius
81
- ctx.moveTo(outlineX + adjustedBorderRadius.topLeft.horizontal, outlineY);
82
- // Top edge
83
- ctx.lineTo(outlineX + outlineW - adjustedBorderRadius.topRight.horizontal, outlineY);
84
- // Top-right corner
85
- if (adjustedBorderRadius.topRight.horizontal > 0 ||
86
- adjustedBorderRadius.topRight.vertical > 0) {
87
- ctx.ellipse(outlineX + outlineW - adjustedBorderRadius.topRight.horizontal, outlineY + adjustedBorderRadius.topRight.vertical, adjustedBorderRadius.topRight.horizontal, adjustedBorderRadius.topRight.vertical, 0, -Math.PI / 2, 0);
88
- }
89
- // Right edge
90
- ctx.lineTo(outlineX + outlineW, outlineY + outlineH - adjustedBorderRadius.bottomRight.vertical);
91
- // Bottom-right corner
92
- if (adjustedBorderRadius.bottomRight.horizontal > 0 ||
93
- adjustedBorderRadius.bottomRight.vertical > 0) {
94
- ctx.ellipse(outlineX + outlineW - adjustedBorderRadius.bottomRight.horizontal, outlineY + outlineH - adjustedBorderRadius.bottomRight.vertical, adjustedBorderRadius.bottomRight.horizontal, adjustedBorderRadius.bottomRight.vertical, 0, 0, Math.PI / 2);
95
- }
96
- // Bottom edge
97
- ctx.lineTo(outlineX + adjustedBorderRadius.bottomLeft.horizontal, outlineY + outlineH);
98
- // Bottom-left corner
99
- if (adjustedBorderRadius.bottomLeft.horizontal > 0 ||
100
- adjustedBorderRadius.bottomLeft.vertical > 0) {
101
- ctx.ellipse(outlineX + adjustedBorderRadius.bottomLeft.horizontal, outlineY + outlineH - adjustedBorderRadius.bottomLeft.vertical, adjustedBorderRadius.bottomLeft.horizontal, adjustedBorderRadius.bottomLeft.vertical, 0, Math.PI / 2, Math.PI);
102
- }
103
- // Left edge
104
- ctx.lineTo(outlineX, outlineY + adjustedBorderRadius.topLeft.vertical);
105
- // Top-left corner
106
- if (adjustedBorderRadius.topLeft.horizontal > 0 ||
107
- adjustedBorderRadius.topLeft.vertical > 0) {
108
- ctx.ellipse(outlineX + adjustedBorderRadius.topLeft.horizontal, outlineY + adjustedBorderRadius.topLeft.vertical, adjustedBorderRadius.topLeft.horizontal, adjustedBorderRadius.topLeft.vertical, 0, Math.PI, (Math.PI * 3) / 2);
109
- }
110
- ctx.closePath();
80
+ drawRoundedRectPath({
81
+ ctx,
82
+ x: outlineX,
83
+ y: outlineY,
84
+ width: outlineW,
85
+ height: outlineH,
86
+ borderRadius: adjustedBorderRadius,
87
+ });
111
88
  ctx.stroke();
112
89
  // Restore original canvas state
113
90
  ctx.strokeStyle = originalStrokeStyle;
@@ -0,0 +1,9 @@
1
+ import type { BorderRadiusCorners } from './border-radius';
2
+ export declare const drawRoundedRectPath: ({ ctx, x, y, width, height, borderRadius, }: {
3
+ ctx: OffscreenCanvasRenderingContext2D;
4
+ x: number;
5
+ y: number;
6
+ width: number;
7
+ height: number;
8
+ borderRadius: BorderRadiusCorners;
9
+ }) => void;
@@ -0,0 +1,34 @@
1
+ export const drawRoundedRectPath = ({ ctx, x, y, width, height, borderRadius, }) => {
2
+ ctx.beginPath();
3
+ // Start from top-left corner, after the radius
4
+ ctx.moveTo(x + borderRadius.topLeft.horizontal, y);
5
+ // Top edge
6
+ ctx.lineTo(x + width - borderRadius.topRight.horizontal, y);
7
+ // Top-right corner
8
+ if (borderRadius.topRight.horizontal > 0 ||
9
+ borderRadius.topRight.vertical > 0) {
10
+ ctx.ellipse(x + width - borderRadius.topRight.horizontal, y + borderRadius.topRight.vertical, borderRadius.topRight.horizontal, borderRadius.topRight.vertical, 0, -Math.PI / 2, 0);
11
+ }
12
+ // Right edge
13
+ ctx.lineTo(x + width, y + height - borderRadius.bottomRight.vertical);
14
+ // Bottom-right corner
15
+ if (borderRadius.bottomRight.horizontal > 0 ||
16
+ borderRadius.bottomRight.vertical > 0) {
17
+ ctx.ellipse(x + width - borderRadius.bottomRight.horizontal, y + height - borderRadius.bottomRight.vertical, borderRadius.bottomRight.horizontal, borderRadius.bottomRight.vertical, 0, 0, Math.PI / 2);
18
+ }
19
+ // Bottom edge
20
+ ctx.lineTo(x + borderRadius.bottomLeft.horizontal, y + height);
21
+ // Bottom-left corner
22
+ if (borderRadius.bottomLeft.horizontal > 0 ||
23
+ borderRadius.bottomLeft.vertical > 0) {
24
+ ctx.ellipse(x + borderRadius.bottomLeft.horizontal, y + height - borderRadius.bottomLeft.vertical, borderRadius.bottomLeft.horizontal, borderRadius.bottomLeft.vertical, 0, Math.PI / 2, Math.PI);
25
+ }
26
+ // Left edge
27
+ ctx.lineTo(x, y + borderRadius.topLeft.vertical);
28
+ // Top-left corner
29
+ if (borderRadius.topLeft.horizontal > 0 ||
30
+ borderRadius.topLeft.vertical > 0) {
31
+ ctx.ellipse(x + borderRadius.topLeft.horizontal, y + borderRadius.topLeft.vertical, borderRadius.topLeft.horizontal, borderRadius.topLeft.vertical, 0, Math.PI, (Math.PI * 3) / 2);
32
+ }
33
+ ctx.closePath();
34
+ };
@@ -16,6 +16,11 @@ export function getPreTransformRect(targetRect, matrix) {
16
16
  origin.y, // e, f (translation)
17
17
  ]);
18
18
  const inverse2D = effective2D.inverse();
19
+ const wasNotInvertible = isNaN(inverse2D.m11);
20
+ // For example, a 90 degree rotation, is not being rendered
21
+ if (wasNotInvertible) {
22
+ return new DOMRect(0, 0, 0, 0);
23
+ }
19
24
  // 4. Transform target rect corners using the 2D inverse
20
25
  const corners = [
21
26
  new DOMPoint(targetRect.x, targetRect.y),
@@ -1,10 +1,11 @@
1
- import type { LogLevel } from 'remotion';
2
- import type { InternalState } from '../internal-state';
3
- export declare const handle3dTransform: ({ element, matrix, parentRect, context, logLevel, internalState, }: {
1
+ export declare const getPrecomposeRectFor3DTransform: ({ element, parentRect, matrix, }: {
4
2
  element: HTMLElement | SVGElement;
5
- matrix: DOMMatrix;
6
3
  parentRect: DOMRect;
7
- context: OffscreenCanvasRenderingContext2D;
8
- logLevel: LogLevel;
9
- internalState: InternalState;
10
- }) => Promise<void>;
4
+ matrix: DOMMatrix;
5
+ }) => DOMRect;
6
+ export declare const handle3dTransform: ({ matrix, precomposeRect, tempCanvas, rectAfterTransforms, }: {
7
+ matrix: DOMMatrix;
8
+ precomposeRect: DOMRect;
9
+ tempCanvas: OffscreenCanvas;
10
+ rectAfterTransforms: DOMRect;
11
+ }) => readonly [OffscreenCanvas, () => void] | null;