@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.
- package/dist/compose.js +26 -1
- package/dist/drawing/calculate-transforms.d.ts +1 -0
- package/dist/drawing/calculate-transforms.js +8 -0
- package/dist/drawing/draw-element-to-canvas.d.ts +2 -1
- package/dist/drawing/draw-element-to-canvas.js +3 -14
- package/dist/drawing/text/find-line-breaks.text.d.ts +5 -0
- package/dist/drawing/text/find-line-breaks.text.js +67 -0
- package/dist/drawing/text/get-collapsed-text.d.ts +1 -0
- package/dist/drawing/text/get-collapsed-text.js +46 -0
- package/dist/drawing/text/handle-text-node.d.ts +1 -0
- package/dist/drawing/text/handle-text-node.js +81 -0
- package/dist/esm/index.mjs +240 -47
- package/package.json +5 -5
- package/dist/border-radius.d.ts +0 -31
- package/dist/border-radius.js +0 -152
- package/dist/calculate-transforms.d.ts +0 -11
- package/dist/calculate-transforms.js +0 -91
- package/dist/composable.d.ts +0 -4
- package/dist/composable.js +0 -1
- package/dist/compose-canvas.d.ts +0 -1
- package/dist/compose-canvas.js +0 -36
- package/dist/compose-svg.d.ts +0 -1
- package/dist/compose-svg.js +0 -34
- package/dist/drawing/compose-canvas.d.ts +0 -1
- package/dist/drawing/compose-canvas.js +0 -36
- package/dist/drawing/compose-svg.d.ts +0 -1
- package/dist/drawing/compose-svg.js +0 -34
- package/dist/drawing/compose.d.ts +0 -5
- package/dist/drawing/compose.js +0 -6
- package/dist/drawing/get-computed-style-cache.d.ts +0 -0
- package/dist/drawing/get-computed-style-cache.js +0 -1
- package/dist/drawing/text/draw-text.d.ts +0 -1
- package/dist/drawing/text/draw-text.js +0 -57
- package/dist/find-canvas-elements.d.ts +0 -1
- package/dist/find-canvas-elements.js +0 -13
- package/dist/find-capturable-elements.d.ts +0 -2
- package/dist/find-capturable-elements.js +0 -26
- package/dist/opacity.d.ts +0 -4
- package/dist/opacity.js +0 -7
- package/dist/parse-transform-origin.d.ts +0 -4
- package/dist/parse-transform-origin.js +0 -7
- package/dist/transform.d.ts +0 -4
- 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({
|
|
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
|
};
|
|
@@ -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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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,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
|
+
};
|