@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.
- package/README.md +7 -7
- package/dist/add-sample.js +20 -0
- package/dist/artifact.js +56 -0
- package/dist/audio.js +42 -0
- package/dist/can-use-webfs-target.js +19 -0
- package/dist/compose.js +85 -0
- package/dist/create-scaffold.js +104 -0
- package/dist/drawing/border-radius.js +151 -0
- package/dist/drawing/calculate-object-fit.js +208 -0
- package/dist/drawing/calculate-transforms.js +127 -0
- package/dist/drawing/clamp-rect-to-parent-bounds.js +18 -0
- package/dist/drawing/do-rects-intersect.js +6 -0
- package/dist/drawing/draw-background.js +62 -0
- package/dist/drawing/draw-border.js +353 -0
- package/dist/drawing/draw-box-shadow.js +103 -0
- package/dist/drawing/draw-dom-element.js +85 -0
- package/dist/drawing/draw-element.js +84 -0
- package/dist/drawing/draw-outline.js +93 -0
- package/dist/drawing/draw-rounded.js +34 -0
- package/dist/drawing/drawn-fn.js +1 -0
- package/dist/drawing/fit-svg-into-its-dimensions.js +35 -0
- package/dist/drawing/get-clipped-background.d.ts +8 -0
- package/dist/drawing/get-clipped-background.js +14 -0
- package/dist/drawing/get-padding-box.js +30 -0
- package/dist/drawing/get-pretransform-rect.js +49 -0
- package/dist/drawing/handle-3d-transform.js +26 -0
- package/dist/drawing/handle-mask.js +21 -0
- package/dist/drawing/has-transform.js +14 -0
- package/dist/drawing/mask-image.js +14 -0
- package/dist/drawing/opacity.js +7 -0
- package/dist/drawing/overflow.js +14 -0
- package/dist/drawing/parse-linear-gradient.js +260 -0
- package/dist/drawing/parse-transform-origin.js +7 -0
- package/dist/drawing/precompose.d.ts +11 -0
- package/dist/drawing/precompose.js +14 -0
- package/dist/drawing/process-node.js +122 -0
- package/dist/drawing/round-to-expand-rect.js +7 -0
- package/dist/drawing/text/apply-text-transform.js +12 -0
- package/dist/drawing/text/draw-text.js +53 -0
- package/dist/drawing/text/find-line-breaks.text.js +118 -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.js +24 -0
- package/dist/drawing/text/parse-paint-order.d.ts +8 -0
- package/dist/drawing/transform-in-3d.js +177 -0
- package/dist/drawing/transform-rect-with-matrix.js +19 -0
- package/dist/drawing/transform.js +10 -0
- package/dist/drawing/turn-svg-into-drawable.js +41 -0
- package/dist/esm/index.mjs +37 -4
- package/dist/frame-range.js +15 -0
- package/dist/get-audio-encoding-config.js +18 -0
- package/dist/get-biggest-bounding-client-rect.js +43 -0
- package/dist/index.js +2 -0
- package/dist/internal-state.js +36 -0
- package/dist/mediabunny-mappings.js +63 -0
- package/dist/output-target.js +1 -0
- package/dist/props-if-has-props.js +1 -0
- package/dist/render-media-on-web.js +304 -0
- package/dist/render-operations-queue.js +3 -0
- package/dist/render-still-on-web.js +110 -0
- package/dist/send-telemetry-event.js +22 -0
- package/dist/take-screenshot.js +30 -0
- package/dist/throttle-progress.js +43 -0
- package/dist/tree-walker-cleanup-after-children.js +33 -0
- package/dist/update-time.js +17 -0
- package/dist/validate-video-frame.js +34 -0
- package/dist/wait-for-ready.js +39 -0
- package/dist/walk-tree.js +14 -0
- package/dist/web-fs-target.js +41 -0
- package/dist/with-resolvers.js +9 -0
- package/package.json +10 -9
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Internals } from 'remotion';
|
|
2
|
+
import { calculateTransforms } from './calculate-transforms';
|
|
3
|
+
import { getWiderRectAndExpand } from './clamp-rect-to-parent-bounds';
|
|
4
|
+
import { doRectsIntersect } from './do-rects-intersect';
|
|
5
|
+
import { drawElement } from './draw-element';
|
|
6
|
+
import { getPrecomposeRectFor3DTransform, handle3dTransform, } from './handle-3d-transform';
|
|
7
|
+
import { getPrecomposeRectForMask, handleMask } from './handle-mask';
|
|
8
|
+
import { precomposeDOMElement } from './precompose';
|
|
9
|
+
import { roundToExpandRect } from './round-to-expand-rect';
|
|
10
|
+
import { transformDOMRect } from './transform-rect-with-matrix';
|
|
11
|
+
export const processNode = async ({ element, context, draw, logLevel, parentRect, internalState, rootElement, }) => {
|
|
12
|
+
const { totalMatrix, reset, dimensions, opacity, computedStyle, precompositing, } = calculateTransforms({
|
|
13
|
+
element,
|
|
14
|
+
rootElement,
|
|
15
|
+
});
|
|
16
|
+
if (opacity === 0) {
|
|
17
|
+
reset();
|
|
18
|
+
return { type: 'skip-children' };
|
|
19
|
+
}
|
|
20
|
+
// When backfaceVisibility is 'hidden', don't render if the element is rotated
|
|
21
|
+
// to show its backface. The backface is visible when the z-component of the
|
|
22
|
+
// transformed normal vector (0, 0, 1) is negative, which corresponds to m33 < 0.
|
|
23
|
+
if (computedStyle.backfaceVisibility === 'hidden' && totalMatrix.m33 < 0) {
|
|
24
|
+
reset();
|
|
25
|
+
return { type: 'skip-children' };
|
|
26
|
+
}
|
|
27
|
+
if (dimensions.width <= 0 || dimensions.height <= 0) {
|
|
28
|
+
reset();
|
|
29
|
+
return { type: 'continue', cleanupAfterChildren: null };
|
|
30
|
+
}
|
|
31
|
+
const rect = new DOMRect(dimensions.left - parentRect.x, dimensions.top - parentRect.y, dimensions.width, dimensions.height);
|
|
32
|
+
if (precompositing.needsPrecompositing) {
|
|
33
|
+
const start = Date.now();
|
|
34
|
+
let precomposeRect = null;
|
|
35
|
+
if (precompositing.needsMaskImage) {
|
|
36
|
+
precomposeRect = getWiderRectAndExpand({
|
|
37
|
+
firstRect: precomposeRect,
|
|
38
|
+
secondRect: getPrecomposeRectForMask(element),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (precompositing.needs3DTransformViaWebGL) {
|
|
42
|
+
precomposeRect = getWiderRectAndExpand({
|
|
43
|
+
firstRect: precomposeRect,
|
|
44
|
+
secondRect: getPrecomposeRectFor3DTransform({
|
|
45
|
+
element,
|
|
46
|
+
parentRect,
|
|
47
|
+
matrix: totalMatrix,
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (!precomposeRect) {
|
|
52
|
+
throw new Error('Precompose rect not found');
|
|
53
|
+
}
|
|
54
|
+
if (precomposeRect.width <= 0 || precomposeRect.height <= 0) {
|
|
55
|
+
return { type: 'continue', cleanupAfterChildren: null };
|
|
56
|
+
}
|
|
57
|
+
if (!doRectsIntersect(precomposeRect, parentRect)) {
|
|
58
|
+
return { type: 'continue', cleanupAfterChildren: null };
|
|
59
|
+
}
|
|
60
|
+
const { tempCanvas, tempContext } = await precomposeDOMElement({
|
|
61
|
+
boundingRect: precomposeRect,
|
|
62
|
+
element,
|
|
63
|
+
logLevel,
|
|
64
|
+
internalState,
|
|
65
|
+
});
|
|
66
|
+
let drawable = tempCanvas;
|
|
67
|
+
const rectAfterTransforms = roundToExpandRect(transformDOMRect({
|
|
68
|
+
rect: precomposeRect,
|
|
69
|
+
matrix: totalMatrix,
|
|
70
|
+
}));
|
|
71
|
+
if (precompositing.needsMaskImage) {
|
|
72
|
+
handleMask({
|
|
73
|
+
gradientInfo: precompositing.needsMaskImage,
|
|
74
|
+
rect,
|
|
75
|
+
precomposeRect,
|
|
76
|
+
tempContext,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (precompositing.needs3DTransformViaWebGL) {
|
|
80
|
+
const t = handle3dTransform({
|
|
81
|
+
matrix: totalMatrix,
|
|
82
|
+
precomposeRect,
|
|
83
|
+
tempCanvas: drawable,
|
|
84
|
+
rectAfterTransforms,
|
|
85
|
+
internalState,
|
|
86
|
+
});
|
|
87
|
+
if (t) {
|
|
88
|
+
drawable = t;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const previousTransform = context.getTransform();
|
|
92
|
+
if (drawable) {
|
|
93
|
+
context.setTransform(new DOMMatrix());
|
|
94
|
+
context.drawImage(drawable, 0, drawable.height - rectAfterTransforms.height, rectAfterTransforms.width, rectAfterTransforms.height, rectAfterTransforms.left - parentRect.x, rectAfterTransforms.top - parentRect.y, rectAfterTransforms.width, rectAfterTransforms.height);
|
|
95
|
+
context.setTransform(previousTransform);
|
|
96
|
+
Internals.Log.trace({
|
|
97
|
+
logLevel,
|
|
98
|
+
tag: '@remotion/web-renderer',
|
|
99
|
+
}, `Transforming element in 3D - canvas size: ${precomposeRect.width}x${precomposeRect.height} - compose: ${Date.now() - start}ms`);
|
|
100
|
+
internalState.addPrecompose({
|
|
101
|
+
canvasWidth: precomposeRect.width,
|
|
102
|
+
canvasHeight: precomposeRect.height,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
reset();
|
|
106
|
+
return { type: 'skip-children' };
|
|
107
|
+
}
|
|
108
|
+
const { cleanupAfterChildren } = await drawElement({
|
|
109
|
+
rect,
|
|
110
|
+
computedStyle,
|
|
111
|
+
context,
|
|
112
|
+
draw,
|
|
113
|
+
opacity,
|
|
114
|
+
totalMatrix,
|
|
115
|
+
parentRect,
|
|
116
|
+
logLevel,
|
|
117
|
+
element,
|
|
118
|
+
internalState,
|
|
119
|
+
});
|
|
120
|
+
reset();
|
|
121
|
+
return { type: 'continue', cleanupAfterChildren };
|
|
122
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const applyTextTransform = (text, transform) => {
|
|
2
|
+
if (transform === 'uppercase') {
|
|
3
|
+
return text.toUpperCase();
|
|
4
|
+
}
|
|
5
|
+
if (transform === 'lowercase') {
|
|
6
|
+
return text.toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
if (transform === 'capitalize') {
|
|
9
|
+
return text.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
10
|
+
}
|
|
11
|
+
return text;
|
|
12
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Internals } from 'remotion';
|
|
2
|
+
import { applyTextTransform } from './apply-text-transform';
|
|
3
|
+
import { findLineBreaks } from './find-line-breaks.text';
|
|
4
|
+
import { getCollapsedText } from './get-collapsed-text';
|
|
5
|
+
export const drawText = ({ span, logLevel, onlyBackgroundClip, }) => {
|
|
6
|
+
const drawFn = ({ dimensions: rect, computedStyle, contextToDraw }) => {
|
|
7
|
+
const { fontFamily, fontSize, fontWeight, direction, writingMode, letterSpacing, textTransform, webkitTextFillColor, } = computedStyle;
|
|
8
|
+
const isVertical = writingMode !== 'horizontal-tb';
|
|
9
|
+
if (isVertical) {
|
|
10
|
+
// TODO: Only warn once per render.
|
|
11
|
+
Internals.Log.warn({
|
|
12
|
+
logLevel,
|
|
13
|
+
tag: '@remotion/web-renderer',
|
|
14
|
+
}, 'Detected "writing-mode" CSS property. Vertical text is not yet supported in @remotion/web-renderer');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
contextToDraw.save();
|
|
18
|
+
const fontSizePx = parseFloat(fontSize);
|
|
19
|
+
contextToDraw.font = `${fontWeight} ${fontSizePx}px ${fontFamily}`;
|
|
20
|
+
contextToDraw.fillStyle =
|
|
21
|
+
// If text is being applied with backgroundClipText, we need to use a solid color otherwise it won't get
|
|
22
|
+
// applied in canvas
|
|
23
|
+
onlyBackgroundClip
|
|
24
|
+
? 'black'
|
|
25
|
+
: // -webkit-text-fill-color overrides color, and defaults to the value of `color`
|
|
26
|
+
webkitTextFillColor;
|
|
27
|
+
contextToDraw.letterSpacing = letterSpacing;
|
|
28
|
+
const isRTL = direction === 'rtl';
|
|
29
|
+
contextToDraw.textAlign = isRTL ? 'right' : 'left';
|
|
30
|
+
contextToDraw.textBaseline = 'alphabetic';
|
|
31
|
+
const originalText = span.textContent;
|
|
32
|
+
const collapsedText = getCollapsedText(span);
|
|
33
|
+
const transformedText = applyTextTransform(collapsedText, textTransform);
|
|
34
|
+
span.textContent = transformedText;
|
|
35
|
+
// For RTL text, fill from the right edge instead of left
|
|
36
|
+
const xPosition = isRTL ? rect.right : rect.left;
|
|
37
|
+
const lines = findLineBreaks(span, isRTL);
|
|
38
|
+
let offsetTop = 0;
|
|
39
|
+
const measurements = contextToDraw.measureText(lines[0].text);
|
|
40
|
+
const { fontBoundingBoxDescent, fontBoundingBoxAscent } = measurements;
|
|
41
|
+
const fontHeight = fontBoundingBoxAscent + fontBoundingBoxDescent;
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
// Calculate leading
|
|
44
|
+
const leading = line.height - fontHeight;
|
|
45
|
+
const halfLeading = leading / 2;
|
|
46
|
+
contextToDraw.fillText(line.text, xPosition + line.offsetHorizontal, rect.top + halfLeading + fontBoundingBoxAscent + offsetTop);
|
|
47
|
+
offsetTop += line.height;
|
|
48
|
+
}
|
|
49
|
+
span.textContent = originalText;
|
|
50
|
+
contextToDraw.restore();
|
|
51
|
+
};
|
|
52
|
+
return drawFn;
|
|
53
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { getCollapsedText } from './get-collapsed-text';
|
|
2
|
+
// Punctuation that cannot start a line according to Unicode line breaking rules
|
|
3
|
+
// When these would start a line, the browser moves the preceding word to the new line
|
|
4
|
+
const cannotStartLine = (segment) => {
|
|
5
|
+
if (segment.length === 0)
|
|
6
|
+
return false;
|
|
7
|
+
const firstChar = segment[0];
|
|
8
|
+
const forbiddenLineStarts = [
|
|
9
|
+
'.',
|
|
10
|
+
',',
|
|
11
|
+
';',
|
|
12
|
+
':',
|
|
13
|
+
'!',
|
|
14
|
+
'?',
|
|
15
|
+
')',
|
|
16
|
+
']',
|
|
17
|
+
'}',
|
|
18
|
+
'"',
|
|
19
|
+
"'",
|
|
20
|
+
'"',
|
|
21
|
+
`'`,
|
|
22
|
+
'»',
|
|
23
|
+
'…',
|
|
24
|
+
'‥',
|
|
25
|
+
'·',
|
|
26
|
+
'%',
|
|
27
|
+
'‰',
|
|
28
|
+
];
|
|
29
|
+
return forbiddenLineStarts.includes(firstChar);
|
|
30
|
+
};
|
|
31
|
+
export function findLineBreaks(span, rtl) {
|
|
32
|
+
const textNode = span.childNodes[0];
|
|
33
|
+
const originalText = textNode.textContent;
|
|
34
|
+
const originalRect = span.getBoundingClientRect();
|
|
35
|
+
const computedStyle = getComputedStyle(span);
|
|
36
|
+
const segmenter = new Intl.Segmenter('en', { granularity: 'word' });
|
|
37
|
+
const segments = segmenter.segment(originalText);
|
|
38
|
+
const words = Array.from(segments).map((s) => s.segment);
|
|
39
|
+
// If the text would be centered in a flexbox container
|
|
40
|
+
const lines = [];
|
|
41
|
+
let currentLine = '';
|
|
42
|
+
let testText = '';
|
|
43
|
+
let previousRect = originalRect;
|
|
44
|
+
textNode.textContent = '';
|
|
45
|
+
for (let i = 0; i < words.length; i += 1) {
|
|
46
|
+
const word = words[i];
|
|
47
|
+
testText += word;
|
|
48
|
+
let wordsToAdd = word;
|
|
49
|
+
while (typeof words[i + 1] !== 'undefined' && words[i + 1].trim() === '') {
|
|
50
|
+
testText += words[i + 1];
|
|
51
|
+
wordsToAdd += words[i + 1];
|
|
52
|
+
i++;
|
|
53
|
+
}
|
|
54
|
+
previousRect = span.getBoundingClientRect();
|
|
55
|
+
textNode.textContent = testText;
|
|
56
|
+
const collapsedText = getCollapsedText(span);
|
|
57
|
+
textNode.textContent = collapsedText;
|
|
58
|
+
const rect = span.getBoundingClientRect();
|
|
59
|
+
const currentHeight = rect.height;
|
|
60
|
+
// If height changed significantly, we had a line break
|
|
61
|
+
if (previousRect &&
|
|
62
|
+
previousRect.height !== 0 &&
|
|
63
|
+
Math.abs(currentHeight - previousRect.height) > 2) {
|
|
64
|
+
const offsetHorizontal = rtl
|
|
65
|
+
? previousRect.right - originalRect.right
|
|
66
|
+
: previousRect.left - originalRect.left;
|
|
67
|
+
const shouldCollapse = !computedStyle.whiteSpaceCollapse.includes('preserve');
|
|
68
|
+
let textForPreviousLine = currentLine;
|
|
69
|
+
let textForNewLine = wordsToAdd;
|
|
70
|
+
// If the segment that triggered the break can't start a line (e.g., punctuation),
|
|
71
|
+
// the browser would have moved the preceding word to the new line as well
|
|
72
|
+
if (cannotStartLine(word)) {
|
|
73
|
+
const currentLineSegments = Array.from(segmenter.segment(currentLine)).map((s) => s.segment);
|
|
74
|
+
// Find the last non-whitespace segment (the word to move)
|
|
75
|
+
let lastWordIndex = currentLineSegments.length - 1;
|
|
76
|
+
while (lastWordIndex >= 0 &&
|
|
77
|
+
currentLineSegments[lastWordIndex].trim() === '') {
|
|
78
|
+
lastWordIndex--;
|
|
79
|
+
}
|
|
80
|
+
if (lastWordIndex >= 0) {
|
|
81
|
+
// Move the last word (and any trailing whitespace) to the new line
|
|
82
|
+
textForPreviousLine = currentLineSegments
|
|
83
|
+
.slice(0, lastWordIndex)
|
|
84
|
+
.join('');
|
|
85
|
+
textForNewLine =
|
|
86
|
+
currentLineSegments.slice(lastWordIndex).join('') + wordsToAdd;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
lines.push({
|
|
90
|
+
text: shouldCollapse ? textForPreviousLine.trim() : textForPreviousLine,
|
|
91
|
+
height: currentHeight - previousRect.height,
|
|
92
|
+
offsetHorizontal,
|
|
93
|
+
});
|
|
94
|
+
currentLine = textForNewLine;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
currentLine += wordsToAdd;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Add the last line
|
|
101
|
+
if (currentLine) {
|
|
102
|
+
textNode.textContent = testText;
|
|
103
|
+
const rects = span.getClientRects();
|
|
104
|
+
const rect = span.getBoundingClientRect();
|
|
105
|
+
const lastRect = rects[rects.length - 1];
|
|
106
|
+
const offsetHorizontal = rtl
|
|
107
|
+
? lastRect.right - originalRect.right
|
|
108
|
+
: lastRect.left - originalRect.left;
|
|
109
|
+
lines.push({
|
|
110
|
+
text: currentLine,
|
|
111
|
+
height: rect.height - lines.reduce((acc, curr) => acc + curr.height, 0),
|
|
112
|
+
offsetHorizontal,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
// Reset to original text
|
|
116
|
+
textNode.textContent = originalText;
|
|
117
|
+
return lines;
|
|
118
|
+
}
|
|
@@ -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,24 @@
|
|
|
1
|
+
import { processNode } from '../process-node';
|
|
2
|
+
import { drawText } from './draw-text';
|
|
3
|
+
export const handleTextNode = async ({ node, context, logLevel, parentRect, internalState, rootElement, onlyBackgroundClip, }) => {
|
|
4
|
+
const span = document.createElement('span');
|
|
5
|
+
const parent = node.parentNode;
|
|
6
|
+
if (!parent) {
|
|
7
|
+
throw new Error('Text node has no parent');
|
|
8
|
+
}
|
|
9
|
+
parent.insertBefore(span, node);
|
|
10
|
+
span.appendChild(node);
|
|
11
|
+
const value = await processNode({
|
|
12
|
+
context,
|
|
13
|
+
element: span,
|
|
14
|
+
draw: drawText({ span, logLevel, onlyBackgroundClip }),
|
|
15
|
+
logLevel,
|
|
16
|
+
parentRect,
|
|
17
|
+
internalState,
|
|
18
|
+
rootElement,
|
|
19
|
+
});
|
|
20
|
+
// Undo the layout manipulation
|
|
21
|
+
parent.insertBefore(node, span);
|
|
22
|
+
parent.removeChild(span);
|
|
23
|
+
return value;
|
|
24
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse paint-order CSS property to determine if stroke should be drawn before fill.
|
|
3
|
+
* Default order is: fill, stroke, markers
|
|
4
|
+
* paintOrder can be "normal", "fill", "stroke", "fill stroke", "stroke fill", etc.
|
|
5
|
+
*/
|
|
6
|
+
export declare const parsePaintOrder: (paintOrder: string) => {
|
|
7
|
+
strokeFirst: boolean;
|
|
8
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Vertex shader - now includes texture coordinates
|
|
2
|
+
const vsSource = `
|
|
3
|
+
attribute vec2 aPosition;
|
|
4
|
+
attribute vec2 aTexCoord;
|
|
5
|
+
uniform mat4 uTransform;
|
|
6
|
+
uniform mat4 uProjection;
|
|
7
|
+
varying vec2 vTexCoord;
|
|
8
|
+
|
|
9
|
+
void main() {
|
|
10
|
+
gl_Position = uProjection * uTransform * vec4(aPosition, 0.0, 1.0);
|
|
11
|
+
vTexCoord = aTexCoord;
|
|
12
|
+
}
|
|
13
|
+
`;
|
|
14
|
+
// Fragment shader - samples from texture and unpremultiplies alpha
|
|
15
|
+
const fsSource = `
|
|
16
|
+
precision mediump float;
|
|
17
|
+
uniform sampler2D uTexture;
|
|
18
|
+
varying vec2 vTexCoord;
|
|
19
|
+
|
|
20
|
+
void main() {
|
|
21
|
+
gl_FragColor = texture2D(uTexture, vTexCoord);
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
function compileShader(shaderGl, source, type) {
|
|
25
|
+
const shader = shaderGl.createShader(type);
|
|
26
|
+
if (!shader) {
|
|
27
|
+
throw new Error('Could not create shader');
|
|
28
|
+
}
|
|
29
|
+
shaderGl.shaderSource(shader, source);
|
|
30
|
+
shaderGl.compileShader(shader);
|
|
31
|
+
if (!shaderGl.getShaderParameter(shader, shaderGl.COMPILE_STATUS)) {
|
|
32
|
+
const log = shaderGl.getShaderInfoLog(shader);
|
|
33
|
+
shaderGl.deleteShader(shader);
|
|
34
|
+
throw new Error('Shader compile error: ' + log);
|
|
35
|
+
}
|
|
36
|
+
return shader;
|
|
37
|
+
}
|
|
38
|
+
const createHelperCanvas = ({ canvasWidth, canvasHeight, helperCanvasState, }) => {
|
|
39
|
+
var _a;
|
|
40
|
+
if (helperCanvasState.current) {
|
|
41
|
+
// Resize canvas if dimensions changed
|
|
42
|
+
if (helperCanvasState.current.canvas.width !== canvasWidth ||
|
|
43
|
+
helperCanvasState.current.canvas.height !== canvasHeight) {
|
|
44
|
+
helperCanvasState.current.canvas.width = canvasWidth;
|
|
45
|
+
helperCanvasState.current.canvas.height = canvasHeight;
|
|
46
|
+
}
|
|
47
|
+
// Always reset viewport and clear when reusing
|
|
48
|
+
helperCanvasState.current.gl.viewport(0, 0, canvasWidth, canvasHeight);
|
|
49
|
+
helperCanvasState.current.gl.clearColor(0, 0, 0, 0);
|
|
50
|
+
helperCanvasState.current.gl.clear(helperCanvasState.current.gl.COLOR_BUFFER_BIT);
|
|
51
|
+
return helperCanvasState.current;
|
|
52
|
+
}
|
|
53
|
+
const canvas = new OffscreenCanvas(canvasWidth, canvasHeight);
|
|
54
|
+
const gl = (_a = canvas.getContext('webgl', {
|
|
55
|
+
premultipliedAlpha: true,
|
|
56
|
+
})) !== null && _a !== void 0 ? _a : undefined;
|
|
57
|
+
if (!gl) {
|
|
58
|
+
throw new Error('WebGL not supported');
|
|
59
|
+
}
|
|
60
|
+
// Compile shaders and create program once
|
|
61
|
+
const vertexShader = compileShader(gl, vsSource, gl.VERTEX_SHADER);
|
|
62
|
+
const fragmentShader = compileShader(gl, fsSource, gl.FRAGMENT_SHADER);
|
|
63
|
+
const program = gl.createProgram();
|
|
64
|
+
if (!program) {
|
|
65
|
+
throw new Error('Could not create program');
|
|
66
|
+
}
|
|
67
|
+
gl.attachShader(program, vertexShader);
|
|
68
|
+
gl.attachShader(program, fragmentShader);
|
|
69
|
+
gl.linkProgram(program);
|
|
70
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
71
|
+
throw new Error('Program link error: ' + gl.getProgramInfoLog(program));
|
|
72
|
+
}
|
|
73
|
+
// Get attribute and uniform locations once
|
|
74
|
+
const locations = {
|
|
75
|
+
aPosition: gl.getAttribLocation(program, 'aPosition'),
|
|
76
|
+
aTexCoord: gl.getAttribLocation(program, 'aTexCoord'),
|
|
77
|
+
uTransform: gl.getUniformLocation(program, 'uTransform'),
|
|
78
|
+
uProjection: gl.getUniformLocation(program, 'uProjection'),
|
|
79
|
+
uTexture: gl.getUniformLocation(program, 'uTexture'),
|
|
80
|
+
};
|
|
81
|
+
// Shaders can be deleted after linking
|
|
82
|
+
gl.deleteShader(vertexShader);
|
|
83
|
+
gl.deleteShader(fragmentShader);
|
|
84
|
+
const cleanup = () => {
|
|
85
|
+
gl.deleteProgram(program);
|
|
86
|
+
const loseContext = gl.getExtension('WEBGL_lose_context');
|
|
87
|
+
if (loseContext) {
|
|
88
|
+
loseContext.loseContext();
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
helperCanvasState.current = { canvas, gl, program, locations, cleanup };
|
|
92
|
+
return helperCanvasState.current;
|
|
93
|
+
};
|
|
94
|
+
export const transformIn3d = ({ matrix, sourceCanvas, untransformedRect, rectAfterTransforms, internalState, }) => {
|
|
95
|
+
const { canvas, gl, program, locations } = createHelperCanvas({
|
|
96
|
+
canvasWidth: rectAfterTransforms.width,
|
|
97
|
+
canvasHeight: rectAfterTransforms.height,
|
|
98
|
+
helperCanvasState: internalState.helperCanvasState,
|
|
99
|
+
});
|
|
100
|
+
// Use the cached program
|
|
101
|
+
gl.useProgram(program);
|
|
102
|
+
// Setup viewport and clear (already done in createHelperCanvas, but ensure it's set)
|
|
103
|
+
gl.viewport(0, 0, rectAfterTransforms.width, rectAfterTransforms.height);
|
|
104
|
+
gl.clearColor(0, 0, 0, 0);
|
|
105
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
106
|
+
// Enable blending
|
|
107
|
+
gl.enable(gl.BLEND);
|
|
108
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
109
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
|
|
110
|
+
// Create vertex buffer
|
|
111
|
+
const vertexBuffer = gl.createBuffer();
|
|
112
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
|
|
113
|
+
// prettier-ignore
|
|
114
|
+
const vertices = new Float32Array([
|
|
115
|
+
untransformedRect.x, untransformedRect.y, 0, 0,
|
|
116
|
+
untransformedRect.x + untransformedRect.width, untransformedRect.y, 1, 0,
|
|
117
|
+
untransformedRect.x, untransformedRect.y + untransformedRect.height, 0, 1,
|
|
118
|
+
untransformedRect.x, untransformedRect.y + untransformedRect.height, 0, 1,
|
|
119
|
+
untransformedRect.x + untransformedRect.width, untransformedRect.y, 1, 0,
|
|
120
|
+
untransformedRect.x + untransformedRect.width, untransformedRect.y + untransformedRect.height, 1, 1,
|
|
121
|
+
]);
|
|
122
|
+
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
|
|
123
|
+
// Setup attributes using cached locations
|
|
124
|
+
gl.enableVertexAttribArray(locations.aPosition);
|
|
125
|
+
gl.vertexAttribPointer(locations.aPosition, 2, gl.FLOAT, false, 4 * 4, 0);
|
|
126
|
+
gl.enableVertexAttribArray(locations.aTexCoord);
|
|
127
|
+
gl.vertexAttribPointer(locations.aTexCoord, 2, gl.FLOAT, false, 4 * 4, 2 * 4);
|
|
128
|
+
// Create and configure texture
|
|
129
|
+
const texture = gl.createTexture();
|
|
130
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
131
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
132
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
133
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
134
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
135
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, sourceCanvas);
|
|
136
|
+
// Set uniforms using cached locations
|
|
137
|
+
const transformMatrix = matrix.toFloat32Array();
|
|
138
|
+
const zScale = 1000000000;
|
|
139
|
+
// Projection matrix accounts for the output canvas dimensions
|
|
140
|
+
const projectionMatrix = new Float32Array([
|
|
141
|
+
2 / rectAfterTransforms.width,
|
|
142
|
+
0,
|
|
143
|
+
0,
|
|
144
|
+
0,
|
|
145
|
+
0,
|
|
146
|
+
-2 / rectAfterTransforms.height,
|
|
147
|
+
0,
|
|
148
|
+
0,
|
|
149
|
+
0,
|
|
150
|
+
0,
|
|
151
|
+
-2 / zScale,
|
|
152
|
+
0,
|
|
153
|
+
-1 + (2 * -rectAfterTransforms.x) / rectAfterTransforms.width,
|
|
154
|
+
1 - (2 * -rectAfterTransforms.y) / rectAfterTransforms.height,
|
|
155
|
+
0,
|
|
156
|
+
1,
|
|
157
|
+
]);
|
|
158
|
+
gl.uniformMatrix4fv(locations.uTransform, false, transformMatrix);
|
|
159
|
+
gl.uniformMatrix4fv(locations.uProjection, false, projectionMatrix);
|
|
160
|
+
gl.uniform1i(locations.uTexture, 0);
|
|
161
|
+
// Draw
|
|
162
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
163
|
+
// Clean up per-frame resources only
|
|
164
|
+
gl.disableVertexAttribArray(locations.aPosition);
|
|
165
|
+
gl.disableVertexAttribArray(locations.aTexCoord);
|
|
166
|
+
gl.deleteTexture(texture);
|
|
167
|
+
gl.deleteBuffer(vertexBuffer);
|
|
168
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
169
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
|
170
|
+
// Reset state
|
|
171
|
+
gl.disable(gl.BLEND);
|
|
172
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
173
|
+
return {
|
|
174
|
+
canvas,
|
|
175
|
+
rect: rectAfterTransforms,
|
|
176
|
+
};
|
|
177
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function transformDOMRect({ rect, matrix, }) {
|
|
2
|
+
// Get all four corners of the rectangle
|
|
3
|
+
const topLeft = new DOMPointReadOnly(rect.left, rect.top);
|
|
4
|
+
const topRight = new DOMPointReadOnly(rect.right, rect.top);
|
|
5
|
+
const bottomLeft = new DOMPointReadOnly(rect.left, rect.bottom);
|
|
6
|
+
const bottomRight = new DOMPointReadOnly(rect.right, rect.bottom);
|
|
7
|
+
// Transform all corners
|
|
8
|
+
const transformedTopLeft = topLeft.matrixTransform(matrix);
|
|
9
|
+
const transformedTopRight = topRight.matrixTransform(matrix);
|
|
10
|
+
const transformedBottomLeft = bottomLeft.matrixTransform(matrix);
|
|
11
|
+
const transformedBottomRight = bottomRight.matrixTransform(matrix);
|
|
12
|
+
// Find the bounding box of the transformed points
|
|
13
|
+
const minX = Math.min(transformedTopLeft.x / transformedTopLeft.w, transformedTopRight.x / transformedTopRight.w, transformedBottomLeft.x / transformedBottomLeft.w, transformedBottomRight.x / transformedBottomRight.w);
|
|
14
|
+
const maxX = Math.max(transformedTopLeft.x / transformedTopLeft.w, transformedTopRight.x / transformedTopRight.w, transformedBottomLeft.x / transformedBottomLeft.w, transformedBottomRight.x / transformedBottomRight.w);
|
|
15
|
+
const minY = Math.min(transformedTopLeft.y / transformedTopLeft.w, transformedTopRight.y / transformedTopRight.w, transformedBottomLeft.y / transformedBottomLeft.w, transformedBottomRight.y / transformedBottomRight.w);
|
|
16
|
+
const maxY = Math.max(transformedTopLeft.y / transformedTopLeft.w, transformedTopRight.y / transformedTopRight.w, transformedBottomLeft.y / transformedBottomLeft.w, transformedBottomRight.y / transformedBottomRight.w);
|
|
17
|
+
// Create a new DOMRect from the bounding box
|
|
18
|
+
return new DOMRect(minX, minY, maxX - minX, maxY - minY);
|
|
19
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const setTransform = ({ ctx, transform, parentRect, }) => {
|
|
2
|
+
const offsetMatrix = new DOMMatrix()
|
|
3
|
+
.translate(-parentRect.x, -parentRect.y)
|
|
4
|
+
.multiply(transform)
|
|
5
|
+
.translate(parentRect.x, parentRect.y);
|
|
6
|
+
ctx.setTransform(offsetMatrix);
|
|
7
|
+
return () => {
|
|
8
|
+
ctx.setTransform(new DOMMatrix());
|
|
9
|
+
};
|
|
10
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const turnSvgIntoDrawable = (svg) => {
|
|
2
|
+
const { fill, color } = getComputedStyle(svg);
|
|
3
|
+
const originalTransform = svg.style.transform;
|
|
4
|
+
const originalTransformOrigin = svg.style.transformOrigin;
|
|
5
|
+
const originalMarginLeft = svg.style.marginLeft;
|
|
6
|
+
const originalMarginRight = svg.style.marginRight;
|
|
7
|
+
const originalMarginTop = svg.style.marginTop;
|
|
8
|
+
const originalMarginBottom = svg.style.marginBottom;
|
|
9
|
+
const originalFill = svg.style.fill;
|
|
10
|
+
const originalColor = svg.style.color;
|
|
11
|
+
svg.style.transform = 'none';
|
|
12
|
+
svg.style.transformOrigin = '';
|
|
13
|
+
// Margins were already included in the positioning calculation,
|
|
14
|
+
// so we need to remove them to avoid double counting.
|
|
15
|
+
svg.style.marginLeft = '0';
|
|
16
|
+
svg.style.marginRight = '0';
|
|
17
|
+
svg.style.marginTop = '0';
|
|
18
|
+
svg.style.marginBottom = '0';
|
|
19
|
+
svg.style.fill = fill;
|
|
20
|
+
svg.style.color = color;
|
|
21
|
+
const svgData = new XMLSerializer().serializeToString(svg);
|
|
22
|
+
svg.style.marginLeft = originalMarginLeft;
|
|
23
|
+
svg.style.marginRight = originalMarginRight;
|
|
24
|
+
svg.style.marginTop = originalMarginTop;
|
|
25
|
+
svg.style.marginBottom = originalMarginBottom;
|
|
26
|
+
svg.style.transform = originalTransform;
|
|
27
|
+
svg.style.transformOrigin = originalTransformOrigin;
|
|
28
|
+
svg.style.fill = originalFill;
|
|
29
|
+
svg.style.color = originalColor;
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const image = new Image();
|
|
32
|
+
const url = `data:image/svg+xml;base64,${btoa(svgData)}`;
|
|
33
|
+
image.onload = function () {
|
|
34
|
+
resolve(image);
|
|
35
|
+
};
|
|
36
|
+
image.onerror = () => {
|
|
37
|
+
reject(new Error('Failed to convert SVG to image'));
|
|
38
|
+
};
|
|
39
|
+
image.src = url;
|
|
40
|
+
});
|
|
41
|
+
};
|