1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
import { BaseNode } from "./base-node";
import type { TextElement } from "@/lib/timeline";
import type { EffectPass } from "@/lib/effects/types";
import type { Transform } from "@/lib/rendering";
import {
CORNER_RADIUS_MAX,
CORNER_RADIUS_MIN,
} from "@/constants/text-constants";
import {
drawTextDecoration,
getTextBackgroundRect,
setCanvasLetterSpacing,
} from "@/lib/text/layout";
import type { MeasuredTextElement } from "@/lib/text/measure-element";
import { clamp } from "@/utils/math";
export type TextNodeParams = TextElement & {
canvasCenter: { x: number; y: number };
canvasHeight: number;
textBaseline?: CanvasTextBaseline;
};
export interface ResolvedTextNodeState {
transform: Transform;
opacity: number;
textColor: string;
backgroundColor: string;
effectPasses: EffectPass[][];
measuredText: MeasuredTextElement;
}
export class TextNode extends BaseNode<TextNodeParams, ResolvedTextNodeState> {}
export function renderTextToContext({
node,
ctx,
}: {
node: TextNode;
ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
}): void {
const resolved = node.resolved;
if (!resolved) {
return;
}
const x = resolved.transform.position.x + node.params.canvasCenter.x;
const y = resolved.transform.position.y + node.params.canvasCenter.y;
const baseline = node.params.textBaseline ?? "middle";
const {
scaledFontSize,
fontString,
letterSpacing,
lineHeightPx,
lines,
lineMetrics,
block,
fontSizeRatio,
resolvedBackground,
} = resolved.measuredText;
const lineCount = lines.length;
const resolvedBackgroundWithColor = {
...resolvedBackground,
color: resolved.backgroundColor,
};
ctx.save();
ctx.translate(x, y);
ctx.scale(resolved.transform.scaleX, resolved.transform.scaleY);
if (resolved.transform.rotate) {
ctx.rotate((resolved.transform.rotate * Math.PI) / 180);
}
ctx.font = fontString;
ctx.textAlign = node.params.textAlign;
ctx.textBaseline = baseline;
ctx.fillStyle = resolved.textColor;
setCanvasLetterSpacing({ ctx, letterSpacingPx: letterSpacing });
if (
node.params.background.enabled &&
node.params.background.color &&
node.params.background.color !== "transparent" &&
lineCount > 0
) {
const backgroundRect = getTextBackgroundRect({
textAlign: node.params.textAlign,
block,
background: resolvedBackgroundWithColor,
fontSizeRatio,
});
if (backgroundRect) {
const p =
clamp({
value: resolvedBackgroundWithColor.cornerRadius,
min: CORNER_RADIUS_MIN,
max: CORNER_RADIUS_MAX,
}) / 100;
const radius =
(Math.min(backgroundRect.width, backgroundRect.height) / 2) * p;
ctx.fillStyle = resolvedBackgroundWithColor.color;
ctx.beginPath();
ctx.roundRect(
backgroundRect.left,
backgroundRect.top,
backgroundRect.width,
backgroundRect.height,
radius,
);
ctx.fill();
ctx.fillStyle = resolved.textColor;
}
}
for (let index = 0; index < lineCount; index++) {
const lineY = index * lineHeightPx - block.visualCenterOffset;
ctx.fillText(lines[index], 0, lineY);
drawTextDecoration({
ctx,
textDecoration: node.params.textDecoration ?? "none",
lineWidth: lineMetrics[index].width,
lineY,
metrics: lineMetrics[index],
scaledFontSize,
textAlign: node.params.textAlign,
});
}
ctx.restore();
}
|