@remotion/web-renderer 4.0.387 → 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.
Files changed (43) hide show
  1. package/dist/compose.js +26 -1
  2. package/dist/drawing/calculate-transforms.d.ts +1 -0
  3. package/dist/drawing/calculate-transforms.js +8 -0
  4. package/dist/drawing/draw-element-to-canvas.d.ts +2 -1
  5. package/dist/drawing/draw-element-to-canvas.js +3 -14
  6. package/dist/drawing/text/find-line-breaks.text.d.ts +5 -0
  7. package/dist/drawing/text/find-line-breaks.text.js +67 -0
  8. package/dist/drawing/text/get-collapsed-text.d.ts +1 -0
  9. package/dist/drawing/text/get-collapsed-text.js +46 -0
  10. package/dist/drawing/text/handle-text-node.d.ts +1 -0
  11. package/dist/drawing/text/handle-text-node.js +81 -0
  12. package/dist/esm/index.mjs +240 -47
  13. package/package.json +5 -5
  14. package/dist/border-radius.d.ts +0 -31
  15. package/dist/border-radius.js +0 -152
  16. package/dist/calculate-transforms.d.ts +0 -11
  17. package/dist/calculate-transforms.js +0 -91
  18. package/dist/composable.d.ts +0 -4
  19. package/dist/composable.js +0 -1
  20. package/dist/compose-canvas.d.ts +0 -1
  21. package/dist/compose-canvas.js +0 -36
  22. package/dist/compose-svg.d.ts +0 -1
  23. package/dist/compose-svg.js +0 -34
  24. package/dist/drawing/compose-canvas.d.ts +0 -1
  25. package/dist/drawing/compose-canvas.js +0 -36
  26. package/dist/drawing/compose-svg.d.ts +0 -1
  27. package/dist/drawing/compose-svg.js +0 -34
  28. package/dist/drawing/compose.d.ts +0 -5
  29. package/dist/drawing/compose.js +0 -6
  30. package/dist/drawing/get-computed-style-cache.d.ts +0 -0
  31. package/dist/drawing/get-computed-style-cache.js +0 -1
  32. package/dist/drawing/text/draw-text.d.ts +0 -1
  33. package/dist/drawing/text/draw-text.js +0 -57
  34. package/dist/find-canvas-elements.d.ts +0 -1
  35. package/dist/find-canvas-elements.js +0 -13
  36. package/dist/find-capturable-elements.d.ts +0 -2
  37. package/dist/find-capturable-elements.js +0 -26
  38. package/dist/opacity.d.ts +0 -4
  39. package/dist/opacity.js +0 -7
  40. package/dist/parse-transform-origin.d.ts +0 -4
  41. package/dist/parse-transform-origin.js +0 -7
  42. package/dist/transform.d.ts +0 -4
  43. package/dist/transform.js +0 -6
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
+ };