@remotion/web-renderer 4.0.428 → 4.0.430

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 (71) hide show
  1. package/README.md +7 -7
  2. package/dist/add-sample.js +20 -0
  3. package/dist/artifact.js +56 -0
  4. package/dist/audio.js +42 -0
  5. package/dist/can-use-webfs-target.js +19 -0
  6. package/dist/compose.js +85 -0
  7. package/dist/create-scaffold.js +104 -0
  8. package/dist/drawing/border-radius.js +151 -0
  9. package/dist/drawing/calculate-object-fit.js +208 -0
  10. package/dist/drawing/calculate-transforms.js +127 -0
  11. package/dist/drawing/clamp-rect-to-parent-bounds.js +18 -0
  12. package/dist/drawing/do-rects-intersect.js +6 -0
  13. package/dist/drawing/draw-background.js +62 -0
  14. package/dist/drawing/draw-border.js +353 -0
  15. package/dist/drawing/draw-box-shadow.js +103 -0
  16. package/dist/drawing/draw-dom-element.js +85 -0
  17. package/dist/drawing/draw-element.js +84 -0
  18. package/dist/drawing/draw-outline.js +93 -0
  19. package/dist/drawing/draw-rounded.js +34 -0
  20. package/dist/drawing/drawn-fn.js +1 -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.js +30 -0
  25. package/dist/drawing/get-pretransform-rect.js +49 -0
  26. package/dist/drawing/handle-3d-transform.js +26 -0
  27. package/dist/drawing/handle-mask.js +21 -0
  28. package/dist/drawing/has-transform.js +14 -0
  29. package/dist/drawing/mask-image.js +14 -0
  30. package/dist/drawing/opacity.js +7 -0
  31. package/dist/drawing/overflow.js +14 -0
  32. package/dist/drawing/parse-linear-gradient.js +260 -0
  33. package/dist/drawing/parse-transform-origin.js +7 -0
  34. package/dist/drawing/precompose.d.ts +11 -0
  35. package/dist/drawing/precompose.js +14 -0
  36. package/dist/drawing/process-node.js +122 -0
  37. package/dist/drawing/round-to-expand-rect.js +7 -0
  38. package/dist/drawing/text/apply-text-transform.js +12 -0
  39. package/dist/drawing/text/draw-text.js +53 -0
  40. package/dist/drawing/text/find-line-breaks.text.js +118 -0
  41. package/dist/drawing/text/get-collapsed-text.d.ts +1 -0
  42. package/dist/drawing/text/get-collapsed-text.js +46 -0
  43. package/dist/drawing/text/handle-text-node.js +24 -0
  44. package/dist/drawing/text/parse-paint-order.d.ts +8 -0
  45. package/dist/drawing/transform-in-3d.js +177 -0
  46. package/dist/drawing/transform-rect-with-matrix.js +19 -0
  47. package/dist/drawing/transform.js +10 -0
  48. package/dist/drawing/turn-svg-into-drawable.js +41 -0
  49. package/dist/esm/index.mjs +37 -4
  50. package/dist/frame-range.js +15 -0
  51. package/dist/get-audio-encoding-config.js +18 -0
  52. package/dist/get-biggest-bounding-client-rect.js +43 -0
  53. package/dist/index.js +2 -0
  54. package/dist/internal-state.js +36 -0
  55. package/dist/mediabunny-mappings.js +63 -0
  56. package/dist/output-target.js +1 -0
  57. package/dist/props-if-has-props.js +1 -0
  58. package/dist/render-media-on-web.js +304 -0
  59. package/dist/render-operations-queue.js +3 -0
  60. package/dist/render-still-on-web.js +110 -0
  61. package/dist/send-telemetry-event.js +22 -0
  62. package/dist/take-screenshot.js +30 -0
  63. package/dist/throttle-progress.js +43 -0
  64. package/dist/tree-walker-cleanup-after-children.js +33 -0
  65. package/dist/update-time.js +17 -0
  66. package/dist/validate-video-frame.js +34 -0
  67. package/dist/wait-for-ready.js +39 -0
  68. package/dist/walk-tree.js +14 -0
  69. package/dist/web-fs-target.js +41 -0
  70. package/dist/with-resolvers.js +9 -0
  71. package/package.json +10 -9
@@ -0,0 +1,93 @@
1
+ import { drawRoundedRectPath } from './draw-rounded';
2
+ export const parseOutlineWidth = (value) => {
3
+ return parseFloat(value) || 0;
4
+ };
5
+ export const parseOutlineOffset = (value) => {
6
+ return parseFloat(value) || 0;
7
+ };
8
+ const getLineDashPattern = (style, width) => {
9
+ if (style === 'dashed') {
10
+ return [width * 2, width];
11
+ }
12
+ if (style === 'dotted') {
13
+ return [width, width];
14
+ }
15
+ return [];
16
+ };
17
+ export const drawOutline = ({ ctx, rect, borderRadius, computedStyle, }) => {
18
+ const outlineWidth = parseOutlineWidth(computedStyle.outlineWidth);
19
+ const { outlineStyle } = computedStyle;
20
+ const outlineColor = computedStyle.outlineColor || 'black';
21
+ const outlineOffset = parseOutlineOffset(computedStyle.outlineOffset);
22
+ // Check if we have a visible outline
23
+ if (outlineWidth <= 0 ||
24
+ outlineStyle === 'none' ||
25
+ outlineStyle === 'hidden') {
26
+ return;
27
+ }
28
+ // Save original canvas state
29
+ const originalStrokeStyle = ctx.strokeStyle;
30
+ const originalLineWidth = ctx.lineWidth;
31
+ const originalLineDash = ctx.getLineDash();
32
+ ctx.strokeStyle = outlineColor;
33
+ ctx.lineWidth = outlineWidth;
34
+ ctx.setLineDash(getLineDashPattern(outlineStyle, outlineWidth));
35
+ // Calculate outline position
36
+ // Outline is drawn outside the border edge, offset by outlineOffset
37
+ const halfWidth = outlineWidth / 2;
38
+ const offset = outlineOffset + halfWidth;
39
+ const outlineX = rect.left - offset;
40
+ const outlineY = rect.top - offset;
41
+ const outlineW = rect.width + offset * 2;
42
+ const outlineH = rect.height + offset * 2;
43
+ // Adjust border radius for the outline offset
44
+ // When outline-offset is positive, we need to expand the radius
45
+ // When outline-offset is negative, we need to shrink the radius
46
+ const adjustedBorderRadius = {
47
+ topLeft: {
48
+ horizontal: borderRadius.topLeft.horizontal === 0
49
+ ? 0
50
+ : Math.max(0, borderRadius.topLeft.horizontal + offset),
51
+ vertical: borderRadius.topLeft.vertical === 0
52
+ ? 0
53
+ : Math.max(0, borderRadius.topLeft.vertical + offset),
54
+ },
55
+ topRight: {
56
+ horizontal: borderRadius.topRight.horizontal === 0
57
+ ? 0
58
+ : Math.max(0, borderRadius.topRight.horizontal + offset),
59
+ vertical: borderRadius.topRight.vertical === 0
60
+ ? 0
61
+ : Math.max(0, borderRadius.topRight.vertical + offset),
62
+ },
63
+ bottomRight: {
64
+ horizontal: borderRadius.bottomRight.horizontal === 0
65
+ ? 0
66
+ : Math.max(0, borderRadius.bottomRight.horizontal + offset),
67
+ vertical: borderRadius.bottomRight.vertical === 0
68
+ ? 0
69
+ : Math.max(0, borderRadius.bottomRight.vertical + offset),
70
+ },
71
+ bottomLeft: {
72
+ horizontal: borderRadius.bottomLeft.horizontal === 0
73
+ ? 0
74
+ : Math.max(0, borderRadius.bottomLeft.horizontal + offset),
75
+ vertical: borderRadius.bottomLeft.vertical === 0
76
+ ? 0
77
+ : Math.max(0, borderRadius.bottomLeft.vertical + offset),
78
+ },
79
+ };
80
+ drawRoundedRectPath({
81
+ ctx,
82
+ x: outlineX,
83
+ y: outlineY,
84
+ width: outlineW,
85
+ height: outlineH,
86
+ borderRadius: adjustedBorderRadius,
87
+ });
88
+ ctx.stroke();
89
+ // Restore original canvas state
90
+ ctx.strokeStyle = originalStrokeStyle;
91
+ ctx.lineWidth = originalLineWidth;
92
+ ctx.setLineDash(originalLineDash);
93
+ };
@@ -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
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -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,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
+ };
@@ -0,0 +1,49 @@
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;
5
+ export function getPreTransformRect(targetRect, matrix) {
6
+ // 1. Determine the effective 2D transformation by transforming basis vectors
7
+ const origin = new DOMPoint(0, 0).matrixTransform(matrix);
8
+ const unitX = new DOMPoint(1, 0).matrixTransform(matrix);
9
+ const unitY = new DOMPoint(0, 1).matrixTransform(matrix);
10
+ // 2. Compute the 2D basis vectors after transformation
11
+ const basisX = { x: unitX.x - origin.x, y: unitX.y - origin.y };
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
+ }
22
+ // 3. Build the effective 2D matrix and invert it
23
+ const effective2D = new DOMMatrix([
24
+ basisX.x,
25
+ basisX.y, // a, b (first column)
26
+ basisY.x,
27
+ basisY.y, // c, d (second column)
28
+ origin.x,
29
+ origin.y, // e, f (translation)
30
+ ]);
31
+ const inverse2D = effective2D.inverse();
32
+ const wasNotInvertible = isNaN(inverse2D.m11);
33
+ // For example, a 90 degree rotation, is not being rendered
34
+ if (wasNotInvertible) {
35
+ return new DOMRect(0, 0, 0, 0);
36
+ }
37
+ // 4. Transform target rect corners using the 2D inverse
38
+ const corners = [
39
+ new DOMPoint(targetRect.x, targetRect.y),
40
+ new DOMPoint(targetRect.x + targetRect.width, targetRect.y),
41
+ new DOMPoint(targetRect.x + targetRect.width, targetRect.y + targetRect.height),
42
+ new DOMPoint(targetRect.x, targetRect.y + targetRect.height),
43
+ ];
44
+ const transformedCorners = corners.map((c) => c.matrixTransform(inverse2D));
45
+ // 5. Compute bounding box
46
+ const xs = transformedCorners.map((p) => p.x);
47
+ const ys = transformedCorners.map((p) => p.y);
48
+ return new DOMRect(Math.min(...xs), Math.min(...ys), Math.max(...xs) - Math.min(...xs), Math.max(...ys) - Math.min(...ys));
49
+ }
@@ -0,0 +1,26 @@
1
+ import { getBiggestBoundingClientRect } from '../get-biggest-bounding-client-rect';
2
+ import { getNarrowerRect } from './clamp-rect-to-parent-bounds';
3
+ import { getPreTransformRect } from './get-pretransform-rect';
4
+ import { transformIn3d } from './transform-in-3d';
5
+ export const getPrecomposeRectFor3DTransform = ({ element, parentRect, matrix, }) => {
6
+ const unclampedBiggestBoundingClientRect = getBiggestBoundingClientRect(element);
7
+ const biggestPossiblePretransformRect = getPreTransformRect(parentRect, matrix);
8
+ const preTransformRect = getNarrowerRect({
9
+ firstRect: unclampedBiggestBoundingClientRect,
10
+ secondRect: biggestPossiblePretransformRect,
11
+ });
12
+ return preTransformRect;
13
+ };
14
+ export const handle3dTransform = ({ matrix, precomposeRect, tempCanvas, rectAfterTransforms, internalState, }) => {
15
+ const { canvas: transformed, rect: transformedRect } = transformIn3d({
16
+ untransformedRect: precomposeRect,
17
+ matrix,
18
+ sourceCanvas: tempCanvas,
19
+ rectAfterTransforms,
20
+ internalState,
21
+ });
22
+ if (transformedRect.width <= 0 || transformedRect.height <= 0) {
23
+ return null;
24
+ }
25
+ return transformed;
26
+ };
@@ -0,0 +1,21 @@
1
+ import { getBiggestBoundingClientRect } from '../get-biggest-bounding-client-rect';
2
+ import { createCanvasGradient } from './parse-linear-gradient';
3
+ export const getPrecomposeRectForMask = (element) => {
4
+ const boundingRect = getBiggestBoundingClientRect(element);
5
+ return boundingRect;
6
+ };
7
+ export const handleMask = ({ gradientInfo, rect, precomposeRect, tempContext, }) => {
8
+ const rectOffsetX = rect.left - precomposeRect.left;
9
+ const rectOffsetY = rect.top - precomposeRect.top;
10
+ const rectToFill = new DOMRect(rectOffsetX, rectOffsetY, rect.width, rect.height);
11
+ const gradient = createCanvasGradient({
12
+ ctx: tempContext,
13
+ rect: rectToFill,
14
+ gradientInfo,
15
+ offsetLeft: 0,
16
+ offsetTop: 0,
17
+ });
18
+ tempContext.globalCompositeOperation = 'destination-in';
19
+ tempContext.fillStyle = gradient;
20
+ tempContext.fillRect(rectToFill.left, rectToFill.top, rectToFill.width, rectToFill.height);
21
+ };
@@ -0,0 +1,14 @@
1
+ export const hasTransformCssValue = (style) => {
2
+ return style.transform !== 'none' && style.transform !== '';
3
+ };
4
+ export const hasRotateCssValue = (style) => {
5
+ return style.rotate !== 'none' && style.rotate !== '';
6
+ };
7
+ export const hasScaleCssValue = (style) => {
8
+ return style.scale !== 'none' && style.scale !== '';
9
+ };
10
+ export const hasAnyTransformCssValue = (style) => {
11
+ return (hasTransformCssValue(style) ||
12
+ hasRotateCssValue(style) ||
13
+ hasScaleCssValue(style));
14
+ };
@@ -0,0 +1,14 @@
1
+ import { parseLinearGradient } from './parse-linear-gradient';
2
+ export const getMaskImageValue = (computedStyle) => {
3
+ // Check both standard and webkit-prefixed properties
4
+ const { maskImage, webkitMaskImage } = computedStyle;
5
+ const value = maskImage || webkitMaskImage;
6
+ if (!value || value === 'none') {
7
+ return null;
8
+ }
9
+ return value;
10
+ };
11
+ export const parseMaskImage = (maskImageValue) => {
12
+ // Only linear gradients are supported for now
13
+ return parseLinearGradient(maskImageValue);
14
+ };
@@ -0,0 +1,7 @@
1
+ export const setOpacity = ({ ctx, opacity, }) => {
2
+ const previousAlpha = ctx.globalAlpha;
3
+ ctx.globalAlpha = previousAlpha * opacity;
4
+ return () => {
5
+ ctx.globalAlpha = previousAlpha;
6
+ };
7
+ };
@@ -0,0 +1,14 @@
1
+ import { setBorderRadius } from './border-radius';
2
+ export const setOverflowHidden = ({ ctx, rect, borderRadius, overflowHidden, computedStyle, backgroundClip, }) => {
3
+ if (!overflowHidden) {
4
+ return () => { };
5
+ }
6
+ return setBorderRadius({
7
+ ctx,
8
+ rect,
9
+ borderRadius,
10
+ forceClipEvenWhenZero: true,
11
+ computedStyle,
12
+ backgroundClip,
13
+ });
14
+ };
@@ -0,0 +1,260 @@
1
+ import { NoReactInternals } from 'remotion/no-react';
2
+ const isValidColor = (color) => {
3
+ try {
4
+ const result = NoReactInternals.processColor(color);
5
+ return result !== null && result !== undefined;
6
+ }
7
+ catch (_a) {
8
+ return false;
9
+ }
10
+ };
11
+ const parseDirection = (directionStr) => {
12
+ const trimmed = directionStr.trim().toLowerCase();
13
+ // Handle keywords like "to right", "to bottom", etc.
14
+ if (trimmed.startsWith('to ')) {
15
+ const direction = trimmed.substring(3).trim();
16
+ switch (direction) {
17
+ case 'top':
18
+ return 0;
19
+ case 'right':
20
+ return 90;
21
+ case 'bottom':
22
+ return 180;
23
+ case 'left':
24
+ return 270;
25
+ case 'top right':
26
+ case 'right top':
27
+ return 45;
28
+ case 'bottom right':
29
+ case 'right bottom':
30
+ return 135;
31
+ case 'bottom left':
32
+ case 'left bottom':
33
+ return 225;
34
+ case 'top left':
35
+ case 'left top':
36
+ return 315;
37
+ default:
38
+ return 180; // Default to bottom
39
+ }
40
+ }
41
+ // Handle angle values: deg, rad, grad, turn
42
+ const angleMatch = trimmed.match(/^(-?\d+\.?\d*)(deg|rad|grad|turn)$/);
43
+ if (angleMatch) {
44
+ const value = parseFloat(angleMatch[1]);
45
+ const unit = angleMatch[2];
46
+ switch (unit) {
47
+ case 'deg':
48
+ return value;
49
+ case 'rad':
50
+ return (value * 180) / Math.PI;
51
+ case 'grad':
52
+ return (value * 360) / 400;
53
+ case 'turn':
54
+ return value * 360;
55
+ default:
56
+ return value;
57
+ }
58
+ }
59
+ // Default: to bottom
60
+ return 180;
61
+ };
62
+ const parseColorStops = (colorStopsStr) => {
63
+ // Split by comma, but respect parentheses in rgba(), rgb(), hsl(), hsla()
64
+ const parts = colorStopsStr.split(/,(?![^(]*\))/);
65
+ const stops = [];
66
+ for (const part of parts) {
67
+ const trimmed = part.trim();
68
+ if (!trimmed)
69
+ continue;
70
+ // Extract color: can be rgb(), rgba(), hsl(), hsla(), hex, or named color
71
+ const colorMatch = trimmed.match(/(rgba?\([^)]+\)|hsla?\([^)]+\)|#[0-9a-f]{3,8}|[a-z]+)/i);
72
+ if (!colorMatch) {
73
+ continue;
74
+ }
75
+ const colorStr = colorMatch[0];
76
+ // Validate that this is actually a valid CSS color
77
+ if (!isValidColor(colorStr)) {
78
+ continue;
79
+ }
80
+ const remaining = trimmed
81
+ .substring(colorMatch.index + colorStr.length)
82
+ .trim();
83
+ // Canvas API supports CSS colors directly, so we can use the color string as-is
84
+ const normalizedColor = colorStr;
85
+ // Parse position if provided
86
+ let position = null;
87
+ if (remaining) {
88
+ const posMatch = remaining.match(/(-?\d+\.?\d*)(%|px)?/);
89
+ if (posMatch) {
90
+ const value = parseFloat(posMatch[1]);
91
+ const unit = posMatch[2];
92
+ if (unit === '%') {
93
+ position = value / 100;
94
+ }
95
+ else if (unit === 'px') {
96
+ // px values need element dimensions, which we don't have here
97
+ // We'll handle this as a percentage for now (not fully CSS-compliant but good enough)
98
+ position = null;
99
+ }
100
+ else {
101
+ position = value / 100; // Assume percentage if no unit
102
+ }
103
+ }
104
+ }
105
+ stops.push({
106
+ color: normalizedColor,
107
+ position: position !== null ? position : -1, // -1 means needs to be calculated
108
+ });
109
+ }
110
+ if (stops.length === 0) {
111
+ return null;
112
+ }
113
+ // Distribute positions evenly for stops that don't have explicit positions
114
+ let lastExplicitIndex = -1;
115
+ let lastExplicitPosition = 0;
116
+ for (let i = 0; i < stops.length; i++) {
117
+ if (stops[i].position !== -1) {
118
+ // Found an explicit position
119
+ if (lastExplicitIndex >= 0) {
120
+ // Interpolate between last explicit and current explicit
121
+ const numImplicit = i - lastExplicitIndex - 1;
122
+ if (numImplicit > 0) {
123
+ const step = (stops[i].position - lastExplicitPosition) / (numImplicit + 1);
124
+ for (let j = lastExplicitIndex + 1; j < i; j++) {
125
+ stops[j].position =
126
+ lastExplicitPosition + step * (j - lastExplicitIndex);
127
+ }
128
+ }
129
+ }
130
+ else {
131
+ // Backfill from start to first explicit
132
+ const numImplicit = i;
133
+ if (numImplicit > 0) {
134
+ const step = stops[i].position / (numImplicit + 1);
135
+ for (let j = 0; j < i; j++) {
136
+ stops[j].position = step * (j + 1);
137
+ }
138
+ }
139
+ }
140
+ lastExplicitIndex = i;
141
+ lastExplicitPosition = stops[i].position;
142
+ }
143
+ }
144
+ // If no explicit positions were provided at all, distribute evenly
145
+ // Check this BEFORE handling trailing stops
146
+ if (stops.every((s) => s.position === -1)) {
147
+ if (stops.length === 1) {
148
+ stops[0].position = 0.5;
149
+ }
150
+ else {
151
+ for (let i = 0; i < stops.length; i++) {
152
+ stops[i].position = i / (stops.length - 1);
153
+ }
154
+ }
155
+ }
156
+ else if (lastExplicitIndex < stops.length - 1) {
157
+ const numImplicit = stops.length - 1 - lastExplicitIndex;
158
+ const step = (1 - lastExplicitPosition) / (numImplicit + 1);
159
+ for (let i = lastExplicitIndex + 1; i < stops.length; i++) {
160
+ stops[i].position = lastExplicitPosition + step * (i - lastExplicitIndex);
161
+ }
162
+ }
163
+ // Clamp positions to 0-1
164
+ for (const stop of stops) {
165
+ stop.position = Math.max(0, Math.min(1, stop.position));
166
+ }
167
+ return stops;
168
+ };
169
+ const extractGradientContent = (backgroundImage) => {
170
+ const prefix = 'linear-gradient(';
171
+ const startIndex = backgroundImage.toLowerCase().indexOf(prefix);
172
+ if (startIndex === -1) {
173
+ return null;
174
+ }
175
+ // Find matching closing parenthesis, handling nested parens from rgb(), rgba(), etc.
176
+ let depth = 0;
177
+ const contentStart = startIndex + prefix.length;
178
+ for (let i = contentStart; i < backgroundImage.length; i++) {
179
+ const char = backgroundImage[i];
180
+ if (char === '(') {
181
+ depth++;
182
+ }
183
+ else if (char === ')') {
184
+ if (depth === 0) {
185
+ return backgroundImage.substring(contentStart, i).trim();
186
+ }
187
+ depth--;
188
+ }
189
+ }
190
+ return null;
191
+ };
192
+ export const parseLinearGradient = (backgroundImage) => {
193
+ if (!backgroundImage || backgroundImage === 'none') {
194
+ return null;
195
+ }
196
+ const content = extractGradientContent(backgroundImage);
197
+ if (!content) {
198
+ return null;
199
+ }
200
+ // Try to identify the direction/angle part vs color stops
201
+ // Direction/angle is optional and comes first
202
+ // It can be: "to right", "45deg", etc.
203
+ // Split into parts, respecting parentheses
204
+ const parts = content.split(/,(?![^(]*\))/);
205
+ let angle = 180; // Default: to bottom
206
+ let colorStopsStart = 0;
207
+ // Check if first part is a direction/angle
208
+ if (parts.length > 0) {
209
+ const firstPart = parts[0].trim();
210
+ // Check if it looks like a direction or angle (not a color)
211
+ const isDirection = firstPart.startsWith('to ') ||
212
+ /^-?\d+\.?\d*(deg|rad|grad|turn)$/.test(firstPart);
213
+ if (isDirection) {
214
+ angle = parseDirection(firstPart);
215
+ colorStopsStart = 1;
216
+ }
217
+ }
218
+ // Parse color stops
219
+ const colorStopsStr = parts.slice(colorStopsStart).join(',');
220
+ const colorStops = parseColorStops(colorStopsStr);
221
+ if (!colorStops || colorStops.length === 0) {
222
+ return null;
223
+ }
224
+ return {
225
+ angle,
226
+ colorStops,
227
+ };
228
+ };
229
+ export const createCanvasGradient = ({ ctx, rect, gradientInfo, offsetLeft, offsetTop, }) => {
230
+ // Convert angle to radians
231
+ // CSS angles: 0deg = to top, 90deg = to right, 180deg = to bottom, 270deg = to left
232
+ // We need to calculate the gradient line that spans the rectangle at the given angle
233
+ const angleRad = ((gradientInfo.angle - 90) * Math.PI) / 180;
234
+ const centerX = rect.left - offsetLeft + rect.width / 2;
235
+ const centerY = rect.top - offsetTop + rect.height / 2;
236
+ // Calculate gradient line endpoints
237
+ // The gradient line passes through the center and has the specified angle
238
+ const cos = Math.cos(angleRad);
239
+ const sin = Math.sin(angleRad);
240
+ // Find the intersection of the gradient line with the rectangle edges
241
+ const halfWidth = rect.width / 2;
242
+ const halfHeight = rect.height / 2;
243
+ // Calculate the length from center to edge along the gradient line.
244
+ // Primary formula should always be > 0 for valid angles and non-zero rects;
245
+ // fall back to diagonal length only for degenerate or invalid cases.
246
+ let length = Math.abs(cos) * halfWidth + Math.abs(sin) * halfHeight;
247
+ if (!Number.isFinite(length) || length === 0) {
248
+ length = Math.sqrt(halfWidth ** 2 + halfHeight ** 2);
249
+ }
250
+ const x0 = centerX - cos * length;
251
+ const y0 = centerY - sin * length;
252
+ const x1 = centerX + cos * length;
253
+ const y1 = centerY + sin * length;
254
+ const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
255
+ // Add color stops
256
+ for (const stop of gradientInfo.colorStops) {
257
+ gradient.addColorStop(stop.position, stop.color);
258
+ }
259
+ return gradient;
260
+ };
@@ -0,0 +1,7 @@
1
+ export const parseTransformOrigin = (transformOrigin) => {
2
+ if (transformOrigin.trim() === '') {
3
+ return null;
4
+ }
5
+ const [x, y] = transformOrigin.split(' ');
6
+ return { x: parseFloat(x), y: parseFloat(y) };
7
+ };
@@ -0,0 +1,11 @@
1
+ import type { LogLevel } from 'remotion';
2
+ import type { InternalState } from '../internal-state';
3
+ export declare const precomposeDOMElement: ({ boundingRect, element, logLevel, internalState, }: {
4
+ boundingRect: DOMRect;
5
+ element: HTMLElement | SVGElement;
6
+ logLevel: LogLevel;
7
+ internalState: InternalState;
8
+ }) => Promise<{
9
+ tempCanvas: OffscreenCanvas;
10
+ tempContext: OffscreenCanvasRenderingContext2D;
11
+ }>;
@@ -0,0 +1,14 @@
1
+ import { compose } from '../compose';
2
+ export const precomposeDOMElement = async ({ boundingRect, element, 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: false,
12
+ });
13
+ return { tempCanvas, tempContext };
14
+ };