@remotion/web-renderer 4.0.388 → 4.0.390

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/compose.js CHANGED
@@ -1,7 +1,14 @@
1
1
  import { drawElementToCanvas } from './drawing/draw-element-to-canvas';
2
+ import { handleTextNode } from './drawing/text/handle-text-node';
3
+ import { turnSvgIntoDrawable } from './drawing/turn-svg-into-drawable';
2
4
  export const compose = async (element, context) => {
3
5
  const treeWalker = document.createTreeWalker(element, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, (node) => {
4
6
  if (node instanceof Element) {
7
+ // SVG does have children, but we process SVG elements in its
8
+ // entirety
9
+ if (node.parentElement instanceof SVGSVGElement) {
10
+ return NodeFilter.FILTER_REJECT;
11
+ }
5
12
  const computedStyle = getComputedStyle(node);
6
13
  return computedStyle.display === 'none'
7
14
  ? NodeFilter.FILTER_REJECT
@@ -12,7 +19,25 @@ export const compose = async (element, context) => {
12
19
  while (treeWalker.nextNode()) {
13
20
  const node = treeWalker.currentNode;
14
21
  if (node instanceof HTMLElement || node instanceof SVGElement) {
15
- await drawElementToCanvas({ element: node, context });
22
+ await drawElementToCanvas({
23
+ element: node,
24
+ context,
25
+ draw: async (dimensions) => {
26
+ const drawable = await (node instanceof SVGSVGElement
27
+ ? turnSvgIntoDrawable(node)
28
+ : node instanceof HTMLImageElement
29
+ ? node
30
+ : node instanceof HTMLCanvasElement
31
+ ? node
32
+ : null);
33
+ if (drawable) {
34
+ context.drawImage(drawable, dimensions.left, dimensions.top, dimensions.width, dimensions.height);
35
+ }
36
+ },
37
+ });
38
+ }
39
+ else if (node instanceof Text) {
40
+ await handleTextNode(node, context);
16
41
  }
17
42
  }
18
43
  };
@@ -7,4 +7,5 @@ export declare const calculateTransforms: (element: HTMLElement | SVGElement) =>
7
7
  y: number;
8
8
  };
9
9
  opacity: number;
10
+ computedStyle: CSSStyleDeclaration;
10
11
  };
@@ -22,6 +22,7 @@ export const calculateTransforms = (element) => {
22
22
  const transforms = [];
23
23
  const toReset = [];
24
24
  let opacity = 1;
25
+ let elementComputedStyle = null;
25
26
  while (parent) {
26
27
  const computedStyle = getComputedStyle(parent);
27
28
  // Multiply opacity values from element and all parents
@@ -29,6 +30,9 @@ export const calculateTransforms = (element) => {
29
30
  if (parentOpacity && parentOpacity !== '') {
30
31
  opacity *= parseFloat(parentOpacity);
31
32
  }
33
+ if (parent === element) {
34
+ elementComputedStyle = computedStyle;
35
+ }
32
36
  if ((computedStyle.transform && computedStyle.transform !== 'none') ||
33
37
  parent === element) {
34
38
  const toParse = computedStyle.transform === 'none' || computedStyle.transform === ''
@@ -67,6 +71,9 @@ export const calculateTransforms = (element) => {
67
71
  .translate(-globalTransformOrigin.x, -globalTransformOrigin.y);
68
72
  totalMatrix.multiplySelf(transformMatrix);
69
73
  }
74
+ if (!elementComputedStyle) {
75
+ throw new Error('Element computed style not found');
76
+ }
70
77
  return {
71
78
  dimensions,
72
79
  totalMatrix,
@@ -77,5 +84,6 @@ export const calculateTransforms = (element) => {
77
84
  },
78
85
  nativeTransformOrigin,
79
86
  opacity,
87
+ computedStyle: elementComputedStyle,
80
88
  };
81
89
  };
@@ -1,4 +1,5 @@
1
- export declare const drawElementToCanvas: ({ element, context, }: {
1
+ export declare const drawElementToCanvas: ({ element, context, draw, }: {
2
2
  element: HTMLElement | SVGElement;
3
3
  context: OffscreenCanvasRenderingContext2D;
4
+ draw: (dimensions: DOMRect, computedStyle: CSSStyleDeclaration) => Promise<void> | void;
4
5
  }) => Promise<void>;
@@ -3,9 +3,8 @@ import { calculateTransforms } from './calculate-transforms';
3
3
  import { drawBorder } from './draw-border';
4
4
  import { setOpacity } from './opacity';
5
5
  import { setTransform } from './transform';
6
- import { turnSvgIntoDrawable } from './turn-svg-into-drawable';
7
- export const drawElementToCanvas = async ({ element, context, }) => {
8
- const { totalMatrix, reset, dimensions, opacity } = calculateTransforms(element);
6
+ export const drawElementToCanvas = async ({ element, context, draw, }) => {
7
+ const { totalMatrix, reset, dimensions, opacity, computedStyle } = calculateTransforms(element);
9
8
  if (opacity === 0) {
10
9
  reset();
11
10
  return;
@@ -14,7 +13,6 @@ export const drawElementToCanvas = async ({ element, context, }) => {
14
13
  reset();
15
14
  return;
16
15
  }
17
- const computedStyle = getComputedStyle(element);
18
16
  const background = computedStyle.backgroundColor;
19
17
  const borderRadius = parseBorderRadius({
20
18
  borderRadius: computedStyle.borderRadius,
@@ -37,13 +35,6 @@ export const drawElementToCanvas = async ({ element, context, }) => {
37
35
  ctx: context,
38
36
  opacity,
39
37
  });
40
- const drawable = element instanceof SVGSVGElement
41
- ? await turnSvgIntoDrawable(element)
42
- : element instanceof HTMLImageElement
43
- ? element
44
- : element instanceof HTMLCanvasElement
45
- ? element
46
- : null;
47
38
  if (background &&
48
39
  background !== 'transparent' &&
49
40
  !(background.startsWith('rgba') &&
@@ -53,9 +44,7 @@ export const drawElementToCanvas = async ({ element, context, }) => {
53
44
  context.fillRect(dimensions.left, dimensions.top, dimensions.width, dimensions.height);
54
45
  context.fillStyle = originalFillStyle;
55
46
  }
56
- if (drawable) {
57
- context.drawImage(drawable, dimensions.left, dimensions.top, dimensions.width, dimensions.height);
58
- }
47
+ await draw(dimensions, computedStyle);
59
48
  drawBorder({
60
49
  ctx: context,
61
50
  x: dimensions.left,
@@ -0,0 +1,5 @@
1
+ export declare function findLineBreaks(span: HTMLSpanElement, rtl: boolean): Array<{
2
+ text: string;
3
+ offsetTop: number;
4
+ offsetHorizontal: number;
5
+ }>;
@@ -0,0 +1,67 @@
1
+ import { getCollapsedText } from './get-collapsed-text';
2
+ export function findLineBreaks(span, rtl) {
3
+ const textNode = span.childNodes[0];
4
+ const originalText = textNode.textContent;
5
+ const originalRect = span.getBoundingClientRect();
6
+ const computedStyle = getComputedStyle(span);
7
+ const segmenter = new Intl.Segmenter('en', { granularity: 'word' });
8
+ const segments = segmenter.segment(originalText);
9
+ const words = Array.from(segments).map((s) => s.segment);
10
+ const lines = [];
11
+ let currentLine = '';
12
+ let testText = '';
13
+ let previousRect = originalRect;
14
+ textNode.textContent = '';
15
+ for (let i = 0; i < words.length; i += 1) {
16
+ const word = words[i];
17
+ testText += word;
18
+ let wordsToAdd = word;
19
+ while (typeof words[i + 1] !== 'undefined' && words[i + 1].trim() === '') {
20
+ testText += words[i + 1];
21
+ wordsToAdd += words[i + 1];
22
+ i++;
23
+ }
24
+ previousRect = span.getBoundingClientRect();
25
+ textNode.textContent = testText;
26
+ const collapsedText = getCollapsedText(span);
27
+ textNode.textContent = collapsedText;
28
+ const rect = span.getBoundingClientRect();
29
+ const currentHeight = rect.height;
30
+ // If height changed significantly, we had a line break
31
+ if (previousRect &&
32
+ previousRect.height !== 0 &&
33
+ Math.abs(currentHeight - previousRect.height) > 2) {
34
+ const offsetHorizontal = rtl
35
+ ? previousRect.right - originalRect.right
36
+ : previousRect.left - originalRect.left;
37
+ const shouldCollapse = !computedStyle.whiteSpaceCollapse.includes('preserve');
38
+ lines.push({
39
+ text: shouldCollapse ? currentLine.trim() : currentLine,
40
+ offsetTop: currentHeight - previousRect.height,
41
+ offsetHorizontal,
42
+ });
43
+ currentLine = wordsToAdd;
44
+ }
45
+ else {
46
+ currentLine += wordsToAdd;
47
+ }
48
+ }
49
+ // Add the last line
50
+ if (currentLine) {
51
+ textNode.textContent = testText;
52
+ const rects = span.getClientRects();
53
+ const rect = span.getBoundingClientRect();
54
+ const lastRect = rects[rects.length - 1];
55
+ const offsetHorizontal = rtl
56
+ ? lastRect.right - originalRect.right
57
+ : lastRect.left - originalRect.left;
58
+ lines.push({
59
+ text: currentLine,
60
+ offsetTop: rect.height - previousRect.height,
61
+ offsetHorizontal,
62
+ });
63
+ }
64
+ // Reset to original text
65
+ textNode.textContent = originalText;
66
+ return lines;
67
+ }
@@ -0,0 +1 @@
1
+ export declare const getCollapsedText: (span: HTMLSpanElement) => string;
@@ -0,0 +1,46 @@
1
+ export const getCollapsedText = (span) => {
2
+ const textNode = span.firstChild;
3
+ if (!textNode || textNode.nodeType !== Node.TEXT_NODE) {
4
+ throw new Error('Span must contain a single text node');
5
+ }
6
+ const originalText = textNode.textContent || '';
7
+ let collapsedText = originalText;
8
+ // Helper to measure width
9
+ const measureWidth = (text) => {
10
+ textNode.textContent = text;
11
+ return span.getBoundingClientRect().width;
12
+ };
13
+ const originalWidth = measureWidth(originalText);
14
+ // Test leading whitespace
15
+ if (/^\s/.test(collapsedText)) {
16
+ const trimmedLeading = collapsedText.replace(/^\s+/, '');
17
+ const newWidth = measureWidth(trimmedLeading);
18
+ if (newWidth === originalWidth) {
19
+ // Whitespace was collapsed by the browser
20
+ collapsedText = trimmedLeading;
21
+ }
22
+ }
23
+ // Test trailing whitespace (on current collapsed text)
24
+ if (/\s$/.test(collapsedText)) {
25
+ const currentWidth = measureWidth(collapsedText);
26
+ const trimmedTrailing = collapsedText.replace(/\s+$/, '');
27
+ const newWidth = measureWidth(trimmedTrailing);
28
+ if (newWidth === currentWidth) {
29
+ // Whitespace was collapsed by the browser
30
+ collapsedText = trimmedTrailing;
31
+ }
32
+ }
33
+ // Test internal duplicate whitespace (on current collapsed text)
34
+ if (/\s\s/.test(collapsedText)) {
35
+ const currentWidth = measureWidth(collapsedText);
36
+ const collapsedInternal = collapsedText.replace(/\s\s+/g, ' ');
37
+ const newWidth = measureWidth(collapsedInternal);
38
+ if (newWidth === currentWidth) {
39
+ // Whitespace was collapsed by the browser
40
+ collapsedText = collapsedInternal;
41
+ }
42
+ }
43
+ // Restore original text
44
+ textNode.textContent = originalText;
45
+ return collapsedText;
46
+ };
@@ -0,0 +1 @@
1
+ export declare const handleTextNode: (node: Text, context: OffscreenCanvasRenderingContext2D) => Promise<void>;
@@ -0,0 +1,81 @@
1
+ // Supported:
2
+ // - fontFamily
3
+ // - fontSize
4
+ // - fontWeight
5
+ // - color
6
+ // - lineHeight
7
+ // - direction
8
+ // - letterSpacing
9
+ // - textTransform
10
+ // Not supported:
11
+ // - writingMode
12
+ // - textDecoration
13
+ import { Internals } from 'remotion';
14
+ import { drawElementToCanvas } from '../draw-element-to-canvas';
15
+ import { findLineBreaks } from './find-line-breaks.text';
16
+ import { getCollapsedText } from './get-collapsed-text';
17
+ const applyTextTransform = (text, transform) => {
18
+ if (transform === 'uppercase') {
19
+ return text.toUpperCase();
20
+ }
21
+ if (transform === 'lowercase') {
22
+ return text.toLowerCase();
23
+ }
24
+ if (transform === 'capitalize') {
25
+ return text.replace(/\b\w/g, (char) => char.toUpperCase());
26
+ }
27
+ return text;
28
+ };
29
+ export const handleTextNode = async (node, context) => {
30
+ const span = document.createElement('span');
31
+ const parent = node.parentNode;
32
+ if (!parent) {
33
+ throw new Error('Text node has no parent');
34
+ }
35
+ parent.insertBefore(span, node);
36
+ span.appendChild(node);
37
+ await drawElementToCanvas({
38
+ context,
39
+ element: span,
40
+ draw(rect, style) {
41
+ const { fontFamily, fontSize, fontWeight, color, lineHeight, direction, writingMode, letterSpacing, textTransform, } = style;
42
+ const isVertical = writingMode !== 'horizontal-tb';
43
+ if (isVertical) {
44
+ // TODO: Only warn once per render.
45
+ Internals.Log.warn({
46
+ logLevel: 'warn',
47
+ tag: '@remotion/web-renderer',
48
+ }, 'Detected "writing-mode" CSS property. Vertical text is not yet supported in @remotion/web-renderer');
49
+ return;
50
+ }
51
+ context.save();
52
+ context.font = `${fontWeight} ${fontSize} ${fontFamily}`;
53
+ context.fillStyle = color;
54
+ context.letterSpacing = letterSpacing;
55
+ const fontSizePx = parseFloat(fontSize);
56
+ // TODO: This is not necessarily correct, need to create text and measure to know for sure
57
+ const lineHeightPx = lineHeight === 'normal' ? 1.2 * fontSizePx : parseFloat(lineHeight);
58
+ const baselineOffset = (lineHeightPx - fontSizePx) / 2;
59
+ const isRTL = direction === 'rtl';
60
+ context.textAlign = isRTL ? 'right' : 'left';
61
+ context.textBaseline = 'top';
62
+ const originalText = span.textContent;
63
+ const collapsedText = getCollapsedText(span);
64
+ const transformedText = applyTextTransform(collapsedText, textTransform);
65
+ span.textContent = transformedText;
66
+ // For RTL text, fill from the right edge instead of left
67
+ const xPosition = isRTL ? rect.right : rect.left;
68
+ const lines = findLineBreaks(span, isRTL);
69
+ let offsetTop = 0;
70
+ for (const line of lines) {
71
+ context.fillText(line.text, xPosition + line.offsetHorizontal, rect.top + baselineOffset + offsetTop);
72
+ offsetTop += line.offsetTop;
73
+ }
74
+ span.textContent = originalText;
75
+ context.restore();
76
+ },
77
+ });
78
+ // Undo the layout manipulation
79
+ parent.insertBefore(node, span);
80
+ parent.removeChild(span);
81
+ };
@@ -8259,7 +8259,7 @@ class Output {
8259
8259
  */
8260
8260
 
8261
8261
  // src/render-media-on-web.tsx
8262
- import { Internals as Internals3 } from "remotion";
8262
+ import { Internals as Internals4 } from "remotion";
8263
8263
 
8264
8264
  // src/add-sample.ts
8265
8265
  var addVideoSampleAndCloseFrame = async (frameToEncode, videoSampleSource) => {
@@ -8826,12 +8826,16 @@ var calculateTransforms = (element) => {
8826
8826
  const transforms = [];
8827
8827
  const toReset = [];
8828
8828
  let opacity = 1;
8829
+ let elementComputedStyle = null;
8829
8830
  while (parent) {
8830
8831
  const computedStyle = getComputedStyle(parent);
8831
8832
  const parentOpacity = computedStyle.opacity;
8832
8833
  if (parentOpacity && parentOpacity !== "") {
8833
8834
  opacity *= parseFloat(parentOpacity);
8834
8835
  }
8836
+ if (parent === element) {
8837
+ elementComputedStyle = computedStyle;
8838
+ }
8835
8839
  if (computedStyle.transform && computedStyle.transform !== "none" || parent === element) {
8836
8840
  const toParse = computedStyle.transform === "none" || computedStyle.transform === "" ? undefined : computedStyle.transform;
8837
8841
  const matrix = new DOMMatrix(toParse);
@@ -8864,6 +8868,9 @@ var calculateTransforms = (element) => {
8864
8868
  const transformMatrix = new DOMMatrix().translate(globalTransformOrigin.x, globalTransformOrigin.y).multiply(transform.matrix).translate(-globalTransformOrigin.x, -globalTransformOrigin.y);
8865
8869
  totalMatrix.multiplySelf(transformMatrix);
8866
8870
  }
8871
+ if (!elementComputedStyle) {
8872
+ throw new Error("Element computed style not found");
8873
+ }
8867
8874
  return {
8868
8875
  dimensions,
8869
8876
  totalMatrix,
@@ -8873,7 +8880,8 @@ var calculateTransforms = (element) => {
8873
8880
  }
8874
8881
  },
8875
8882
  nativeTransformOrigin,
8876
- opacity
8883
+ opacity,
8884
+ computedStyle: elementComputedStyle
8877
8885
  };
8878
8886
  };
8879
8887
 
@@ -8987,46 +8995,13 @@ var setTransform = ({
8987
8995
  };
8988
8996
  };
8989
8997
 
8990
- // src/drawing/turn-svg-into-drawable.ts
8991
- var turnSvgIntoDrawable = (svg) => {
8992
- const originalTransform = svg.style.transform;
8993
- const originalTransformOrigin = svg.style.transformOrigin;
8994
- const originalMarginLeft = svg.style.marginLeft;
8995
- const originalMarginRight = svg.style.marginRight;
8996
- const originalMarginTop = svg.style.marginTop;
8997
- const originalMarginBottom = svg.style.marginBottom;
8998
- svg.style.transform = "none";
8999
- svg.style.transformOrigin = "";
9000
- svg.style.marginLeft = "0";
9001
- svg.style.marginRight = "0";
9002
- svg.style.marginTop = "0";
9003
- svg.style.marginBottom = "0";
9004
- const svgData = new XMLSerializer().serializeToString(svg);
9005
- svg.style.marginLeft = originalMarginLeft;
9006
- svg.style.marginRight = originalMarginRight;
9007
- svg.style.marginTop = originalMarginTop;
9008
- svg.style.marginBottom = originalMarginBottom;
9009
- svg.style.transform = originalTransform;
9010
- svg.style.transformOrigin = originalTransformOrigin;
9011
- return new Promise((resolve, reject) => {
9012
- const image = new Image;
9013
- const url2 = `data:image/svg+xml;base64,${btoa(svgData)}`;
9014
- image.onload = function() {
9015
- resolve(image);
9016
- };
9017
- image.onerror = () => {
9018
- reject(new Error("Failed to convert SVG to image"));
9019
- };
9020
- image.src = url2;
9021
- });
9022
- };
9023
-
9024
8998
  // src/drawing/draw-element-to-canvas.ts
9025
8999
  var drawElementToCanvas = async ({
9026
9000
  element,
9027
- context
9001
+ context,
9002
+ draw
9028
9003
  }) => {
9029
- const { totalMatrix, reset, dimensions, opacity } = calculateTransforms(element);
9004
+ const { totalMatrix, reset, dimensions, opacity, computedStyle } = calculateTransforms(element);
9030
9005
  if (opacity === 0) {
9031
9006
  reset();
9032
9007
  return;
@@ -9035,7 +9010,6 @@ var drawElementToCanvas = async ({
9035
9010
  reset();
9036
9011
  return;
9037
9012
  }
9038
- const computedStyle = getComputedStyle(element);
9039
9013
  const background = computedStyle.backgroundColor;
9040
9014
  const borderRadius = parseBorderRadius({
9041
9015
  borderRadius: computedStyle.borderRadius,
@@ -9058,16 +9032,13 @@ var drawElementToCanvas = async ({
9058
9032
  ctx: context,
9059
9033
  opacity
9060
9034
  });
9061
- const drawable = element instanceof SVGSVGElement ? await turnSvgIntoDrawable(element) : element instanceof HTMLImageElement ? element : element instanceof HTMLCanvasElement ? element : null;
9062
9035
  if (background && background !== "transparent" && !(background.startsWith("rgba") && (background.endsWith(", 0)") || background.endsWith(",0")))) {
9063
9036
  const originalFillStyle = context.fillStyle;
9064
9037
  context.fillStyle = background;
9065
9038
  context.fillRect(dimensions.left, dimensions.top, dimensions.width, dimensions.height);
9066
9039
  context.fillStyle = originalFillStyle;
9067
9040
  }
9068
- if (drawable) {
9069
- context.drawImage(drawable, dimensions.left, dimensions.top, dimensions.width, dimensions.height);
9070
- }
9041
+ await draw(dimensions, computedStyle);
9071
9042
  drawBorder({
9072
9043
  ctx: context,
9073
9044
  x: dimensions.left,
@@ -9083,10 +9054,221 @@ var drawElementToCanvas = async ({
9083
9054
  reset();
9084
9055
  };
9085
9056
 
9057
+ // src/drawing/text/handle-text-node.ts
9058
+ import { Internals as Internals3 } from "remotion";
9059
+
9060
+ // src/drawing/text/get-collapsed-text.ts
9061
+ var getCollapsedText = (span) => {
9062
+ const textNode = span.firstChild;
9063
+ if (!textNode || textNode.nodeType !== Node.TEXT_NODE) {
9064
+ throw new Error("Span must contain a single text node");
9065
+ }
9066
+ const originalText = textNode.textContent || "";
9067
+ let collapsedText = originalText;
9068
+ const measureWidth = (text) => {
9069
+ textNode.textContent = text;
9070
+ return span.getBoundingClientRect().width;
9071
+ };
9072
+ const originalWidth = measureWidth(originalText);
9073
+ if (/^\s/.test(collapsedText)) {
9074
+ const trimmedLeading = collapsedText.replace(/^\s+/, "");
9075
+ const newWidth = measureWidth(trimmedLeading);
9076
+ if (newWidth === originalWidth) {
9077
+ collapsedText = trimmedLeading;
9078
+ }
9079
+ }
9080
+ if (/\s$/.test(collapsedText)) {
9081
+ const currentWidth = measureWidth(collapsedText);
9082
+ const trimmedTrailing = collapsedText.replace(/\s+$/, "");
9083
+ const newWidth = measureWidth(trimmedTrailing);
9084
+ if (newWidth === currentWidth) {
9085
+ collapsedText = trimmedTrailing;
9086
+ }
9087
+ }
9088
+ if (/\s\s/.test(collapsedText)) {
9089
+ const currentWidth = measureWidth(collapsedText);
9090
+ const collapsedInternal = collapsedText.replace(/\s\s+/g, " ");
9091
+ const newWidth = measureWidth(collapsedInternal);
9092
+ if (newWidth === currentWidth) {
9093
+ collapsedText = collapsedInternal;
9094
+ }
9095
+ }
9096
+ textNode.textContent = originalText;
9097
+ return collapsedText;
9098
+ };
9099
+
9100
+ // src/drawing/text/find-line-breaks.text.ts
9101
+ function findLineBreaks(span, rtl) {
9102
+ const textNode = span.childNodes[0];
9103
+ const originalText = textNode.textContent;
9104
+ const originalRect = span.getBoundingClientRect();
9105
+ const computedStyle = getComputedStyle(span);
9106
+ const segmenter = new Intl.Segmenter("en", { granularity: "word" });
9107
+ const segments = segmenter.segment(originalText);
9108
+ const words = Array.from(segments).map((s) => s.segment);
9109
+ const lines = [];
9110
+ let currentLine = "";
9111
+ let testText = "";
9112
+ let previousRect = originalRect;
9113
+ textNode.textContent = "";
9114
+ for (let i = 0;i < words.length; i += 1) {
9115
+ const word = words[i];
9116
+ testText += word;
9117
+ let wordsToAdd = word;
9118
+ while (typeof words[i + 1] !== "undefined" && words[i + 1].trim() === "") {
9119
+ testText += words[i + 1];
9120
+ wordsToAdd += words[i + 1];
9121
+ i++;
9122
+ }
9123
+ previousRect = span.getBoundingClientRect();
9124
+ textNode.textContent = testText;
9125
+ const collapsedText = getCollapsedText(span);
9126
+ textNode.textContent = collapsedText;
9127
+ const rect = span.getBoundingClientRect();
9128
+ const currentHeight = rect.height;
9129
+ if (previousRect && previousRect.height !== 0 && Math.abs(currentHeight - previousRect.height) > 2) {
9130
+ const offsetHorizontal = rtl ? previousRect.right - originalRect.right : previousRect.left - originalRect.left;
9131
+ const shouldCollapse = !computedStyle.whiteSpaceCollapse.includes("preserve");
9132
+ lines.push({
9133
+ text: shouldCollapse ? currentLine.trim() : currentLine,
9134
+ offsetTop: currentHeight - previousRect.height,
9135
+ offsetHorizontal
9136
+ });
9137
+ currentLine = wordsToAdd;
9138
+ } else {
9139
+ currentLine += wordsToAdd;
9140
+ }
9141
+ }
9142
+ if (currentLine) {
9143
+ textNode.textContent = testText;
9144
+ const rects = span.getClientRects();
9145
+ const rect = span.getBoundingClientRect();
9146
+ const lastRect = rects[rects.length - 1];
9147
+ const offsetHorizontal = rtl ? lastRect.right - originalRect.right : lastRect.left - originalRect.left;
9148
+ lines.push({
9149
+ text: currentLine,
9150
+ offsetTop: rect.height - previousRect.height,
9151
+ offsetHorizontal
9152
+ });
9153
+ }
9154
+ textNode.textContent = originalText;
9155
+ return lines;
9156
+ }
9157
+
9158
+ // src/drawing/text/handle-text-node.ts
9159
+ var applyTextTransform = (text, transform) => {
9160
+ if (transform === "uppercase") {
9161
+ return text.toUpperCase();
9162
+ }
9163
+ if (transform === "lowercase") {
9164
+ return text.toLowerCase();
9165
+ }
9166
+ if (transform === "capitalize") {
9167
+ return text.replace(/\b\w/g, (char) => char.toUpperCase());
9168
+ }
9169
+ return text;
9170
+ };
9171
+ var handleTextNode = async (node, context) => {
9172
+ const span = document.createElement("span");
9173
+ const parent = node.parentNode;
9174
+ if (!parent) {
9175
+ throw new Error("Text node has no parent");
9176
+ }
9177
+ parent.insertBefore(span, node);
9178
+ span.appendChild(node);
9179
+ await drawElementToCanvas({
9180
+ context,
9181
+ element: span,
9182
+ draw(rect, style) {
9183
+ const {
9184
+ fontFamily,
9185
+ fontSize,
9186
+ fontWeight,
9187
+ color,
9188
+ lineHeight,
9189
+ direction,
9190
+ writingMode,
9191
+ letterSpacing,
9192
+ textTransform
9193
+ } = style;
9194
+ const isVertical = writingMode !== "horizontal-tb";
9195
+ if (isVertical) {
9196
+ Internals3.Log.warn({
9197
+ logLevel: "warn",
9198
+ tag: "@remotion/web-renderer"
9199
+ }, 'Detected "writing-mode" CSS property. Vertical text is not yet supported in @remotion/web-renderer');
9200
+ return;
9201
+ }
9202
+ context.save();
9203
+ context.font = `${fontWeight} ${fontSize} ${fontFamily}`;
9204
+ context.fillStyle = color;
9205
+ context.letterSpacing = letterSpacing;
9206
+ const fontSizePx = parseFloat(fontSize);
9207
+ const lineHeightPx = lineHeight === "normal" ? 1.2 * fontSizePx : parseFloat(lineHeight);
9208
+ const baselineOffset = (lineHeightPx - fontSizePx) / 2;
9209
+ const isRTL = direction === "rtl";
9210
+ context.textAlign = isRTL ? "right" : "left";
9211
+ context.textBaseline = "top";
9212
+ const originalText = span.textContent;
9213
+ const collapsedText = getCollapsedText(span);
9214
+ const transformedText = applyTextTransform(collapsedText, textTransform);
9215
+ span.textContent = transformedText;
9216
+ const xPosition = isRTL ? rect.right : rect.left;
9217
+ const lines = findLineBreaks(span, isRTL);
9218
+ let offsetTop = 0;
9219
+ for (const line of lines) {
9220
+ context.fillText(line.text, xPosition + line.offsetHorizontal, rect.top + baselineOffset + offsetTop);
9221
+ offsetTop += line.offsetTop;
9222
+ }
9223
+ span.textContent = originalText;
9224
+ context.restore();
9225
+ }
9226
+ });
9227
+ parent.insertBefore(node, span);
9228
+ parent.removeChild(span);
9229
+ };
9230
+
9231
+ // src/drawing/turn-svg-into-drawable.ts
9232
+ var turnSvgIntoDrawable = (svg) => {
9233
+ const originalTransform = svg.style.transform;
9234
+ const originalTransformOrigin = svg.style.transformOrigin;
9235
+ const originalMarginLeft = svg.style.marginLeft;
9236
+ const originalMarginRight = svg.style.marginRight;
9237
+ const originalMarginTop = svg.style.marginTop;
9238
+ const originalMarginBottom = svg.style.marginBottom;
9239
+ svg.style.transform = "none";
9240
+ svg.style.transformOrigin = "";
9241
+ svg.style.marginLeft = "0";
9242
+ svg.style.marginRight = "0";
9243
+ svg.style.marginTop = "0";
9244
+ svg.style.marginBottom = "0";
9245
+ const svgData = new XMLSerializer().serializeToString(svg);
9246
+ svg.style.marginLeft = originalMarginLeft;
9247
+ svg.style.marginRight = originalMarginRight;
9248
+ svg.style.marginTop = originalMarginTop;
9249
+ svg.style.marginBottom = originalMarginBottom;
9250
+ svg.style.transform = originalTransform;
9251
+ svg.style.transformOrigin = originalTransformOrigin;
9252
+ return new Promise((resolve, reject) => {
9253
+ const image = new Image;
9254
+ const url2 = `data:image/svg+xml;base64,${btoa(svgData)}`;
9255
+ image.onload = function() {
9256
+ resolve(image);
9257
+ };
9258
+ image.onerror = () => {
9259
+ reject(new Error("Failed to convert SVG to image"));
9260
+ };
9261
+ image.src = url2;
9262
+ });
9263
+ };
9264
+
9086
9265
  // src/compose.ts
9087
9266
  var compose = async (element, context) => {
9088
9267
  const treeWalker = document.createTreeWalker(element, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, (node) => {
9089
9268
  if (node instanceof Element) {
9269
+ if (node.parentElement instanceof SVGSVGElement) {
9270
+ return NodeFilter.FILTER_REJECT;
9271
+ }
9090
9272
  const computedStyle = getComputedStyle(node);
9091
9273
  return computedStyle.display === "none" ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
9092
9274
  }
@@ -9095,7 +9277,18 @@ var compose = async (element, context) => {
9095
9277
  while (treeWalker.nextNode()) {
9096
9278
  const node = treeWalker.currentNode;
9097
9279
  if (node instanceof HTMLElement || node instanceof SVGElement) {
9098
- await drawElementToCanvas({ element: node, context });
9280
+ await drawElementToCanvas({
9281
+ element: node,
9282
+ context,
9283
+ draw: async (dimensions) => {
9284
+ const drawable = await (node instanceof SVGSVGElement ? turnSvgIntoDrawable(node) : node instanceof HTMLImageElement ? node : node instanceof HTMLCanvasElement ? node : null);
9285
+ if (drawable) {
9286
+ context.drawImage(drawable, dimensions.left, dimensions.top, dimensions.width, dimensions.height);
9287
+ }
9288
+ }
9289
+ });
9290
+ } else if (node instanceof Text) {
9291
+ await handleTextNode(node, context);
9099
9292
  }
9100
9293
  }
9101
9294
  };
@@ -9256,7 +9449,7 @@ var internalRenderMediaOnWeb = async ({
9256
9449
  if (codec && !format.getSupportedCodecs().includes(codecToMediabunnyCodec(codec))) {
9257
9450
  return Promise.reject(new Error(`Codec ${codec} is not supported for container ${container}`));
9258
9451
  }
9259
- const resolved = await Internals3.resolveVideoConfig({
9452
+ const resolved = await Internals4.resolveVideoConfig({
9260
9453
  calculateMetadata: composition.calculateMetadata ?? null,
9261
9454
  signal: signal ?? new AbortController().signal,
9262
9455
  defaultProps: composition.defaultProps ?? {},
@@ -9445,7 +9638,7 @@ var renderMediaOnWeb = (options) => {
9445
9638
  };
9446
9639
  // src/render-still-on-web.tsx
9447
9640
  import {
9448
- Internals as Internals4
9641
+ Internals as Internals5
9449
9642
  } from "remotion";
9450
9643
  async function internalRenderStillOnWeb({
9451
9644
  frame,
@@ -9459,7 +9652,7 @@ async function internalRenderStillOnWeb({
9459
9652
  signal,
9460
9653
  onArtifact
9461
9654
  }) {
9462
- const resolved = await Internals4.resolveVideoConfig({
9655
+ const resolved = await Internals5.resolveVideoConfig({
9463
9656
  calculateMetadata: composition.calculateMetadata ?? null,
9464
9657
  signal: signal ?? new AbortController().signal,
9465
9658
  defaultProps: composition.defaultProps ?? {},
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "url": "https://github.com/remotion-dev/remotion/tree/main/packages/web-renderer"
4
4
  },
5
5
  "name": "@remotion/web-renderer",
6
- "version": "4.0.388",
6
+ "version": "4.0.390",
7
7
  "main": "dist/index.js",
8
8
  "sideEffects": false,
9
9
  "scripts": {
@@ -16,13 +16,13 @@
16
16
  "author": "Remotion <jonny@remotion.dev>",
17
17
  "license": "UNLICENSED",
18
18
  "dependencies": {
19
- "remotion": "4.0.388",
19
+ "remotion": "4.0.390",
20
20
  "mediabunny": "1.25.8"
21
21
  },
22
22
  "devDependencies": {
23
- "@remotion/eslint-config-internal": "4.0.388",
24
- "@remotion/player": "4.0.388",
25
- "@remotion/media": "4.0.388",
23
+ "@remotion/eslint-config-internal": "4.0.390",
24
+ "@remotion/player": "4.0.390",
25
+ "@remotion/media": "4.0.390",
26
26
  "@vitejs/plugin-react": "4.1.0",
27
27
  "@vitest/browser-playwright": "4.0.9",
28
28
  "playwright": "1.55.1",
@@ -1,9 +0,0 @@
1
- export declare const calculateTransforms: (element: HTMLElement | SVGSVGElement) => {
2
- dimensions: DOMRect;
3
- totalMatrix: DOMMatrix;
4
- reset: () => void;
5
- nativeTransformOrigin: {
6
- x: number;
7
- y: number;
8
- };
9
- };
@@ -1,74 +0,0 @@
1
- import { parseTransformOrigin } from './parse-transform-origin';
2
- const getInternalTransformOrigin = (transform) => {
3
- var _a;
4
- const centerX = transform.boundingClientRect.width / 2;
5
- const centerY = transform.boundingClientRect.height / 2;
6
- const origin = (_a = parseTransformOrigin(transform.transformOrigin)) !== null && _a !== void 0 ? _a : {
7
- x: centerX,
8
- y: centerY,
9
- };
10
- return origin;
11
- };
12
- const getGlobalTransformOrigin = (transform) => {
13
- const { x: originX, y: originY } = getInternalTransformOrigin(transform);
14
- return {
15
- x: originX + transform.boundingClientRect.left,
16
- y: originY + transform.boundingClientRect.top,
17
- };
18
- };
19
- export const calculateTransforms = (element) => {
20
- // Compute the cumulative transform by traversing parent nodes
21
- let parent = element;
22
- const transforms = [];
23
- const toReset = [];
24
- while (parent) {
25
- const computedStyle = getComputedStyle(parent);
26
- if ((computedStyle.transform && computedStyle.transform !== 'none') ||
27
- parent === element) {
28
- const toParse = computedStyle.transform === 'none' || computedStyle.transform === ''
29
- ? undefined
30
- : computedStyle.transform;
31
- const matrix = new DOMMatrix(toParse);
32
- const { transform } = parent.style;
33
- parent.style.transform = 'none';
34
- transforms.push({
35
- matrix,
36
- rect: parent,
37
- transformOrigin: computedStyle.transformOrigin,
38
- boundingClientRect: null,
39
- });
40
- const parentRef = parent;
41
- toReset.push(() => {
42
- parentRef.style.transform = transform;
43
- });
44
- }
45
- parent = parent.parentElement;
46
- }
47
- for (const transform of transforms) {
48
- transform.boundingClientRect = transform.rect.getBoundingClientRect();
49
- }
50
- const dimensions = transforms[0].boundingClientRect;
51
- const nativeTransformOrigin = getInternalTransformOrigin(transforms[0]);
52
- const totalMatrix = new DOMMatrix();
53
- for (const transform of transforms.slice().reverse()) {
54
- if (!transform.boundingClientRect) {
55
- throw new Error('Bounding client rect not found');
56
- }
57
- const globalTransformOrigin = getGlobalTransformOrigin(transform);
58
- const transformMatrix = new DOMMatrix()
59
- .translate(globalTransformOrigin.x, globalTransformOrigin.y)
60
- .multiply(transform.matrix)
61
- .translate(-globalTransformOrigin.x, -globalTransformOrigin.y);
62
- totalMatrix.multiplySelf(transformMatrix);
63
- }
64
- return {
65
- dimensions,
66
- totalMatrix,
67
- reset: () => {
68
- for (const reset of toReset) {
69
- reset();
70
- }
71
- },
72
- nativeTransformOrigin,
73
- };
74
- };
@@ -1,10 +0,0 @@
1
- export type Composable = {
2
- type: 'canvas';
3
- element: HTMLCanvasElement;
4
- } | {
5
- type: 'svg';
6
- element: SVGSVGElement;
7
- } | {
8
- type: 'img';
9
- element: HTMLImageElement;
10
- };
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export declare const composeCanvas: (canvas: HTMLCanvasElement | HTMLImageElement | SVGSVGElement, context: OffscreenCanvasRenderingContext2D) => Promise<void>;
@@ -1,12 +0,0 @@
1
- import { calculateTransforms } from './calculate-transforms';
2
- import { turnSvgIntoDrawable } from './compose-svg';
3
- export const composeCanvas = async (canvas, context) => {
4
- const { totalMatrix, reset, dimensions } = calculateTransforms(canvas);
5
- context.setTransform(totalMatrix);
6
- const drawable = canvas instanceof SVGSVGElement
7
- ? await turnSvgIntoDrawable(canvas)
8
- : canvas;
9
- context.drawImage(drawable, dimensions.left, dimensions.top, dimensions.width, dimensions.height);
10
- context.setTransform(new DOMMatrix());
11
- reset();
12
- };
@@ -1 +0,0 @@
1
- export declare const turnSvgIntoDrawable: (svg: SVGSVGElement) => Promise<HTMLImageElement>;
@@ -1,34 +0,0 @@
1
- export const turnSvgIntoDrawable = (svg) => {
2
- const originalTransform = svg.style.transform;
3
- const originalTransformOrigin = svg.style.transformOrigin;
4
- const originalMarginLeft = svg.style.marginLeft;
5
- const originalMarginRight = svg.style.marginRight;
6
- const originalMarginTop = svg.style.marginTop;
7
- const originalMarginBottom = svg.style.marginBottom;
8
- svg.style.transform = 'none';
9
- svg.style.transformOrigin = '';
10
- // Margins were already included in the positioning calculation,
11
- // so we need to remove them to avoid double counting.
12
- svg.style.marginLeft = '0';
13
- svg.style.marginRight = '0';
14
- svg.style.marginTop = '0';
15
- svg.style.marginBottom = '0';
16
- const svgData = new XMLSerializer().serializeToString(svg);
17
- svg.style.marginLeft = originalMarginLeft;
18
- svg.style.marginRight = originalMarginRight;
19
- svg.style.marginTop = originalMarginTop;
20
- svg.style.marginBottom = originalMarginBottom;
21
- svg.style.transform = originalTransform;
22
- svg.style.transformOrigin = originalTransformOrigin;
23
- return new Promise((resolve, reject) => {
24
- const image = new Image();
25
- const url = `data:image/svg+xml;base64,${btoa(svgData)}`;
26
- image.onload = function () {
27
- resolve(image);
28
- };
29
- image.onerror = () => {
30
- reject(new Error('Failed to convert SVG to image'));
31
- };
32
- image.src = url;
33
- });
34
- };
@@ -1,2 +0,0 @@
1
- import type { Composable } from './composable';
2
- export declare const findCapturableElements: (element: HTMLDivElement) => Composable[];
@@ -1,28 +0,0 @@
1
- export const findCapturableElements = (element) => {
2
- const canvasAndSvgElements = element.querySelectorAll('svg,canvas,img');
3
- const composables = [];
4
- Array.from(canvasAndSvgElements).forEach((capturableElement) => {
5
- if (capturableElement.tagName.toLocaleLowerCase() === 'canvas') {
6
- const canvas = capturableElement;
7
- composables.push({
8
- type: 'canvas',
9
- element: canvas,
10
- });
11
- }
12
- else if (capturableElement.tagName.toLocaleLowerCase() === 'svg') {
13
- const svg = capturableElement;
14
- composables.push({
15
- type: 'svg',
16
- element: svg,
17
- });
18
- }
19
- else if (capturableElement.tagName.toLocaleLowerCase() === 'img') {
20
- const img = capturableElement;
21
- composables.push({
22
- type: 'img',
23
- element: img,
24
- });
25
- }
26
- });
27
- return composables;
28
- };
@@ -1,4 +0,0 @@
1
- export declare const parseTransformOrigin: (transformOrigin: string) => {
2
- x: number;
3
- y: number;
4
- } | null;
@@ -1,7 +0,0 @@
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
- };