html2canvas-pro 2.1.0 → 2.1.1
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/html2canvas-pro.esm.js +21 -7
- package/dist/html2canvas-pro.esm.js.map +1 -1
- package/dist/html2canvas-pro.js +21 -7
- package/dist/html2canvas-pro.js.map +1 -1
- package/dist/html2canvas-pro.min.js +3 -3
- package/dist/lib/core/cache-storage.js +2 -2
- package/dist/lib/core/features.js +2 -2
- package/dist/lib/render/canvas/background-renderer.js +6 -0
- package/dist/lib/render/canvas/canvas-renderer.js +5 -1
- package/dist/lib/render/canvas/foreignobject-renderer.js +5 -1
- package/package.json +3 -11
- package/dist/lib/invariant.js +0 -9
- package/dist/types/invariant.d.ts +0 -1
- package/src/__tests__/index.ts +0 -99
- package/src/config.ts +0 -107
- package/src/core/__mocks__/cache-storage.ts +0 -1
- package/src/core/__mocks__/context.ts +0 -19
- package/src/core/__mocks__/features.ts +0 -8
- package/src/core/__mocks__/logger.ts +0 -17
- package/src/core/__tests__/cache-storage.test.ts +0 -205
- package/src/core/__tests__/cache-storage.ts +0 -278
- package/src/core/__tests__/logger.ts +0 -29
- package/src/core/__tests__/validator.ts +0 -359
- package/src/core/bitwise.ts +0 -1
- package/src/core/cache-storage.ts +0 -315
- package/src/core/context.ts +0 -31
- package/src/core/debugger.ts +0 -32
- package/src/core/features.ts +0 -222
- package/src/core/logger.ts +0 -64
- package/src/core/origin-checker.ts +0 -57
- package/src/core/performance-monitor.ts +0 -241
- package/src/core/render-element.ts +0 -272
- package/src/core/util.ts +0 -1
- package/src/core/validator.ts +0 -593
- package/src/css/index.ts +0 -427
- package/src/css/layout/__mocks__/bounds.ts +0 -6
- package/src/css/layout/bounds.ts +0 -79
- package/src/css/layout/text.ts +0 -161
- package/src/css/property-descriptor.ts +0 -49
- package/src/css/property-descriptors/__tests__/background-tests.ts +0 -65
- package/src/css/property-descriptors/__tests__/clip-path.test.ts +0 -280
- package/src/css/property-descriptors/__tests__/font-family.ts +0 -25
- package/src/css/property-descriptors/__tests__/image-rendering-integration.test.ts +0 -153
- package/src/css/property-descriptors/__tests__/image-rendering-performance.test.ts +0 -175
- package/src/css/property-descriptors/__tests__/image-rendering.test.ts +0 -72
- package/src/css/property-descriptors/__tests__/paint-order.ts +0 -87
- package/src/css/property-descriptors/__tests__/text-shadow.ts +0 -94
- package/src/css/property-descriptors/__tests__/transform-tests.ts +0 -18
- package/src/css/property-descriptors/background-clip.ts +0 -30
- package/src/css/property-descriptors/background-color.ts +0 -9
- package/src/css/property-descriptors/background-image.ts +0 -27
- package/src/css/property-descriptors/background-origin.ts +0 -31
- package/src/css/property-descriptors/background-position.ts +0 -38
- package/src/css/property-descriptors/background-repeat.ts +0 -44
- package/src/css/property-descriptors/background-size.ts +0 -27
- package/src/css/property-descriptors/border-color.ts +0 -13
- package/src/css/property-descriptors/border-radius.ts +0 -19
- package/src/css/property-descriptors/border-style.ts +0 -34
- package/src/css/property-descriptors/border-width.ts +0 -20
- package/src/css/property-descriptors/box-shadow.ts +0 -60
- package/src/css/property-descriptors/clip-path.ts +0 -271
- package/src/css/property-descriptors/color.ts +0 -9
- package/src/css/property-descriptors/content.ts +0 -26
- package/src/css/property-descriptors/counter-increment.ts +0 -43
- package/src/css/property-descriptors/counter-reset.ts +0 -36
- package/src/css/property-descriptors/direction.ts +0 -23
- package/src/css/property-descriptors/display.ts +0 -117
- package/src/css/property-descriptors/duration.ts +0 -14
- package/src/css/property-descriptors/float.ts +0 -29
- package/src/css/property-descriptors/font-family.ts +0 -38
- package/src/css/property-descriptors/font-size.ts +0 -9
- package/src/css/property-descriptors/font-style.ts +0 -25
- package/src/css/property-descriptors/font-variant.ts +0 -12
- package/src/css/property-descriptors/font-weight.ts +0 -26
- package/src/css/property-descriptors/image-rendering.ts +0 -33
- package/src/css/property-descriptors/letter-spacing.ts +0 -25
- package/src/css/property-descriptors/line-break.ts +0 -22
- package/src/css/property-descriptors/line-height.ts +0 -22
- package/src/css/property-descriptors/list-style-image.ts +0 -19
- package/src/css/property-descriptors/list-style-position.ts +0 -22
- package/src/css/property-descriptors/list-style-type.ts +0 -179
- package/src/css/property-descriptors/margin.ts +0 -13
- package/src/css/property-descriptors/mix-blend-mode.ts +0 -35
- package/src/css/property-descriptors/object-fit.ts +0 -39
- package/src/css/property-descriptors/opacity.ts +0 -15
- package/src/css/property-descriptors/overflow-wrap.ts +0 -22
- package/src/css/property-descriptors/overflow.ts +0 -34
- package/src/css/property-descriptors/padding.ts +0 -14
- package/src/css/property-descriptors/paint-order.ts +0 -42
- package/src/css/property-descriptors/position.ts +0 -30
- package/src/css/property-descriptors/quotes.ts +0 -57
- package/src/css/property-descriptors/rotate.ts +0 -34
- package/src/css/property-descriptors/text-align.ts +0 -26
- package/src/css/property-descriptors/text-decoration-color.ts +0 -9
- package/src/css/property-descriptors/text-decoration-line.ts +0 -38
- package/src/css/property-descriptors/text-decoration-style.ts +0 -32
- package/src/css/property-descriptors/text-decoration-thickness.ts +0 -30
- package/src/css/property-descriptors/text-overflow.ts +0 -23
- package/src/css/property-descriptors/text-shadow.ts +0 -52
- package/src/css/property-descriptors/text-transform.ts +0 -27
- package/src/css/property-descriptors/text-underline-offset.ts +0 -27
- package/src/css/property-descriptors/transform-origin.ts +0 -29
- package/src/css/property-descriptors/transform.ts +0 -74
- package/src/css/property-descriptors/visibility.ts +0 -25
- package/src/css/property-descriptors/webkit-line-clamp.ts +0 -30
- package/src/css/property-descriptors/webkit-text-stroke-color.ts +0 -8
- package/src/css/property-descriptors/webkit-text-stroke-width.ts +0 -15
- package/src/css/property-descriptors/word-break.ts +0 -25
- package/src/css/property-descriptors/writing-mode.ts +0 -37
- package/src/css/property-descriptors/z-index.ts +0 -27
- package/src/css/syntax/__tests__/tokernizer-tests.ts +0 -29
- package/src/css/syntax/parser.ts +0 -188
- package/src/css/syntax/tokenizer.ts +0 -822
- package/src/css/type-descriptor.ts +0 -7
- package/src/css/types/__tests__/color-tests.ts +0 -147
- package/src/css/types/__tests__/image-tests.ts +0 -239
- package/src/css/types/angle.ts +0 -86
- package/src/css/types/color-math.ts +0 -22
- package/src/css/types/color-spaces/a98.ts +0 -86
- package/src/css/types/color-spaces/p3.ts +0 -92
- package/src/css/types/color-spaces/pro-photo.ts +0 -87
- package/src/css/types/color-spaces/rec2020.ts +0 -90
- package/src/css/types/color-spaces/srgb.ts +0 -87
- package/src/css/types/color-utilities.ts +0 -452
- package/src/css/types/color.ts +0 -485
- package/src/css/types/functions/-prefix-linear-gradient.ts +0 -35
- package/src/css/types/functions/-prefix-radial-gradient.ts +0 -106
- package/src/css/types/functions/-webkit-gradient.ts +0 -69
- package/src/css/types/functions/__tests__/radial-gradient.ts +0 -69
- package/src/css/types/functions/counter.ts +0 -511
- package/src/css/types/functions/gradient.ts +0 -206
- package/src/css/types/functions/linear-gradient.ts +0 -28
- package/src/css/types/functions/radial-gradient.ts +0 -101
- package/src/css/types/image.ts +0 -120
- package/src/css/types/index.ts +0 -1
- package/src/css/types/length-percentage.ts +0 -137
- package/src/css/types/length.ts +0 -7
- package/src/css/types/time.ts +0 -20
- package/src/dom/__mocks__/document-cloner.ts +0 -22
- package/src/dom/__tests__/dom-normalizer.test.ts +0 -133
- package/src/dom/__tests__/element-container.test.ts +0 -129
- package/src/dom/document-cloner.ts +0 -929
- package/src/dom/dom-normalizer.ts +0 -133
- package/src/dom/element-container.ts +0 -75
- package/src/dom/elements/li-element-container.ts +0 -10
- package/src/dom/elements/ol-element-container.ts +0 -12
- package/src/dom/elements/select-element-container.ts +0 -10
- package/src/dom/elements/textarea-element-container.ts +0 -9
- package/src/dom/node-parser.ts +0 -177
- package/src/dom/node-type-guards.ts +0 -70
- package/src/dom/replaced-elements/canvas-element-container.ts +0 -15
- package/src/dom/replaced-elements/iframe-element-container.ts +0 -55
- package/src/dom/replaced-elements/image-element-container.ts +0 -16
- package/src/dom/replaced-elements/index.ts +0 -5
- package/src/dom/replaced-elements/input-element-container.ts +0 -105
- package/src/dom/replaced-elements/pseudo-elements.ts +0 -0
- package/src/dom/replaced-elements/svg-element-container.ts +0 -23
- package/src/dom/text-container.ts +0 -42
- package/src/global.d.ts +0 -19
- package/src/index.ts +0 -82
- package/src/invariant.ts +0 -5
- package/src/options.ts +0 -55
- package/src/render/__tests__/object-fit.test.ts +0 -85
- package/src/render/background.ts +0 -298
- package/src/render/bezier-curve.ts +0 -47
- package/src/render/border.ts +0 -165
- package/src/render/bound-curves.ts +0 -388
- package/src/render/box-sizing.ts +0 -31
- package/src/render/canvas/__tests__/background-renderer.test.ts +0 -72
- package/src/render/canvas/__tests__/border-renderer.test.ts +0 -24
- package/src/render/canvas/__tests__/effects-renderer.test.ts +0 -32
- package/src/render/canvas/__tests__/text-renderer.test.ts +0 -471
- package/src/render/canvas/background-renderer.ts +0 -271
- package/src/render/canvas/border-renderer.ts +0 -224
- package/src/render/canvas/canvas-path.ts +0 -31
- package/src/render/canvas/canvas-renderer.ts +0 -641
- package/src/render/canvas/effects-renderer.ts +0 -130
- package/src/render/canvas/foreignobject-renderer.ts +0 -53
- package/src/render/canvas/text-renderer.ts +0 -700
- package/src/render/effects.ts +0 -75
- package/src/render/font-metrics.ts +0 -72
- package/src/render/object-fit.ts +0 -100
- package/src/render/path.ts +0 -37
- package/src/render/renderer-interface.ts +0 -28
- package/src/render/stacking-context.ts +0 -386
- package/src/render/vector.ts +0 -19
|
@@ -1,700 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Text Renderer
|
|
3
|
-
*
|
|
4
|
-
* Handles rendering of text content including:
|
|
5
|
-
* - Text with letter spacing
|
|
6
|
-
* - Text decorations (underline, overline, line-through)
|
|
7
|
-
* - Text shadows
|
|
8
|
-
* - Webkit line clamp
|
|
9
|
-
* - Text overflow ellipsis
|
|
10
|
-
* - Paint order (fill/stroke)
|
|
11
|
-
* - Font styles
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { Context } from '../../core/context';
|
|
15
|
-
import { TextContainer } from '../../dom/text-container';
|
|
16
|
-
import { CSSParsedDeclaration } from '../../css';
|
|
17
|
-
import { Bounds } from '../../css/layout/bounds';
|
|
18
|
-
import { TextBounds, segmentGraphemes } from '../../css/layout/text';
|
|
19
|
-
import { asString } from '../../css/types/color-utilities';
|
|
20
|
-
import { TEXT_DECORATION_LINE } from '../../css/property-descriptors/text-decoration-line';
|
|
21
|
-
import { TEXT_DECORATION_STYLE } from '../../css/property-descriptors/text-decoration-style';
|
|
22
|
-
import { PAINT_ORDER_LAYER } from '../../css/property-descriptors/paint-order';
|
|
23
|
-
import { DIRECTION } from '../../css/property-descriptors/direction';
|
|
24
|
-
import { DISPLAY } from '../../css/property-descriptors/display';
|
|
25
|
-
import { TEXT_OVERFLOW } from '../../css/property-descriptors/text-overflow';
|
|
26
|
-
import { OVERFLOW } from '../../css/property-descriptors/overflow';
|
|
27
|
-
import { isDimensionToken } from '../../css/syntax/parser';
|
|
28
|
-
import { TextShadow } from '../../css/property-descriptors/text-shadow';
|
|
29
|
-
import {
|
|
30
|
-
isSidewaysWritingMode,
|
|
31
|
-
isVerticalWritingMode,
|
|
32
|
-
WRITING_MODE
|
|
33
|
-
} from '../../css/property-descriptors/writing-mode';
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Dependencies required for TextRenderer
|
|
37
|
-
*/
|
|
38
|
-
export interface TextRendererDependencies {
|
|
39
|
-
ctx: CanvasRenderingContext2D;
|
|
40
|
-
context: Context;
|
|
41
|
-
options: {
|
|
42
|
-
scale: number;
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// iOS font fix - see https://github.com/niklasvh/html2canvas/pull/2645
|
|
47
|
-
const iOSBrokenFonts = ['-apple-system', 'system-ui'];
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Detect CJK (Chinese, Japanese, Korean) characters in a string.
|
|
51
|
-
* CJK characters use the ideographic baseline in browsers, which differs
|
|
52
|
-
* from the alphabetic baseline used for Latin script.
|
|
53
|
-
*
|
|
54
|
-
* Covers:
|
|
55
|
-
* U+2E80–U+2FFF CJK Radicals Supplement, Kangxi Radicals
|
|
56
|
-
* U+3000–U+30FF CJK Symbols & Punctuation (。、「」…), Hiragana, Katakana
|
|
57
|
-
* U+3400–U+4DBF CJK Extension A
|
|
58
|
-
* U+4E00–U+9FFF CJK Unified Ideographs (most common Chinese/Japanese/Korean)
|
|
59
|
-
* U+AC00–U+D7AF Hangul Syllables
|
|
60
|
-
* U+F900–U+FAFF CJK Compatibility Ideographs
|
|
61
|
-
* U+FF01–U+FFEF Halfwidth and Fullwidth Forms (A B 1 2 ! ? etc.)
|
|
62
|
-
*/
|
|
63
|
-
const CJK_CHAR_REGEX = /[\u2E80-\u2FFF\u3000-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7AF\uF900-\uFAFF\uFF01-\uFFEF]/;
|
|
64
|
-
|
|
65
|
-
export const hasCJKCharacters = (text: string): boolean => CJK_CHAR_REGEX.test(text);
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Detect iOS version from user agent
|
|
69
|
-
* Returns null if not iOS or version cannot be determined
|
|
70
|
-
*/
|
|
71
|
-
const getIOSVersion = (): number | null => {
|
|
72
|
-
if (typeof navigator === 'undefined') {
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const userAgent = navigator.userAgent;
|
|
77
|
-
|
|
78
|
-
// Check if it's iOS or iPadOS
|
|
79
|
-
// iPadOS 13+ may identify as Macintosh, check for touch support
|
|
80
|
-
const isIOS = /iPhone|iPad|iPod/.test(userAgent);
|
|
81
|
-
const isIPadOS = /Macintosh/.test(userAgent) && navigator.maxTouchPoints && navigator.maxTouchPoints > 1;
|
|
82
|
-
|
|
83
|
-
if (!isIOS && !isIPadOS) {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Extract version number from various iOS user agent formats:
|
|
88
|
-
// - "iPhone OS 15_0" or "iPhone OS 15_0_1"
|
|
89
|
-
// - "CPU OS 15_0 like Mac OS X"
|
|
90
|
-
// - "CPU iPhone OS 15_0 like Mac OS X"
|
|
91
|
-
// - "Version/15.0" (for iPadOS)
|
|
92
|
-
const patterns = [
|
|
93
|
-
/(?:iPhone|CPU(?:\siPhone)?)\sOS\s(\d+)[\._](\d+)/, // iPhone OS, CPU OS, CPU iPhone OS
|
|
94
|
-
/Version\/(\d+)\.(\d+)/ // Version/15.0 (iPadOS)
|
|
95
|
-
];
|
|
96
|
-
|
|
97
|
-
for (const pattern of patterns) {
|
|
98
|
-
const match = userAgent.match(pattern);
|
|
99
|
-
if (match && match[1]) {
|
|
100
|
-
return parseInt(match[1], 10);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return null;
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const fixIOSSystemFonts = (fontFamilies: string[]): string[] => {
|
|
108
|
-
const iosVersion = getIOSVersion();
|
|
109
|
-
|
|
110
|
-
// On iOS 15.0 and 15.1, system fonts have rendering issues
|
|
111
|
-
// Fixed in iOS 17+
|
|
112
|
-
if (iosVersion !== null && iosVersion >= 15 && iosVersion < 17) {
|
|
113
|
-
return fontFamilies.map((fontFamily) =>
|
|
114
|
-
iOSBrokenFonts.indexOf(fontFamily) !== -1
|
|
115
|
-
? `-apple-system, "Helvetica Neue", Arial, sans-serif`
|
|
116
|
-
: fontFamily
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return fontFamilies;
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
const getTextStrokeLineJoin = (): CanvasLineJoin => {
|
|
124
|
-
const currentWindow = typeof window !== 'undefined' ? (window as Window & { chrome?: unknown }) : undefined;
|
|
125
|
-
return currentWindow?.chrome ? 'miter' : 'round';
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Text Renderer
|
|
130
|
-
*
|
|
131
|
-
* Specialized renderer for text content.
|
|
132
|
-
* Extracted from CanvasRenderer to improve code organization and maintainability.
|
|
133
|
-
*/
|
|
134
|
-
export class TextRenderer {
|
|
135
|
-
private readonly ctx: CanvasRenderingContext2D;
|
|
136
|
-
private readonly options: { scale: number };
|
|
137
|
-
|
|
138
|
-
constructor(deps: TextRendererDependencies) {
|
|
139
|
-
this.ctx = deps.ctx;
|
|
140
|
-
// context stored but not used directly in this renderer
|
|
141
|
-
this.options = deps.options;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Iterate grapheme clusters one-by-one, applying correct letter-spacing and
|
|
146
|
-
* per-script baseline for each character.
|
|
147
|
-
*
|
|
148
|
-
* Issue #73: When letter-spacing is non-zero, text must be rendered character by
|
|
149
|
-
* character. This helper centralises two fixes applied during that iteration:
|
|
150
|
-
* 1. Add `letterSpacing` to each character's advance width (was previously
|
|
151
|
-
* omitted, causing characters to render without any spacing).
|
|
152
|
-
* 2. Switch to the ideographic baseline for CJK glyphs so their vertical
|
|
153
|
-
* position matches how browsers lay them out in the DOM.
|
|
154
|
-
*
|
|
155
|
-
* The `renderFn` callback receives (letter, x, y) and performs the actual draw
|
|
156
|
-
* call (fillText or strokeText), allowing fill and stroke paths to share one
|
|
157
|
-
* implementation.
|
|
158
|
-
*/
|
|
159
|
-
private iterateLettersWithLetterSpacing(
|
|
160
|
-
text: TextBounds,
|
|
161
|
-
letterSpacing: number,
|
|
162
|
-
baseline: number,
|
|
163
|
-
writingMode: WRITING_MODE,
|
|
164
|
-
renderFn: (letter: string, x: number, y: number) => void
|
|
165
|
-
): void {
|
|
166
|
-
if (isVerticalWritingMode(writingMode)) {
|
|
167
|
-
this.iterateVerticalGlyphs(text, letterSpacing, baseline, writingMode, renderFn);
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const letters = segmentGraphemes(text.text);
|
|
172
|
-
const y = text.bounds.top + baseline;
|
|
173
|
-
let left = text.bounds.left;
|
|
174
|
-
for (const letter of letters) {
|
|
175
|
-
if (hasCJKCharacters(letter)) {
|
|
176
|
-
const savedBaseline = this.ctx.textBaseline;
|
|
177
|
-
this.ctx.textBaseline = 'ideographic';
|
|
178
|
-
renderFn(letter, left, y);
|
|
179
|
-
this.ctx.textBaseline = savedBaseline;
|
|
180
|
-
} else {
|
|
181
|
-
renderFn(letter, left, y);
|
|
182
|
-
}
|
|
183
|
-
left += this.ctx.measureText(letter).width + letterSpacing;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
private iterateVerticalGlyphs(
|
|
188
|
-
text: TextBounds,
|
|
189
|
-
letterSpacing: number,
|
|
190
|
-
baseline: number,
|
|
191
|
-
writingMode: WRITING_MODE,
|
|
192
|
-
renderFn: (letter: string, x: number, y: number) => void
|
|
193
|
-
): void {
|
|
194
|
-
const letters = segmentGraphemes(text.text);
|
|
195
|
-
let top = text.bounds.top;
|
|
196
|
-
|
|
197
|
-
for (const letter of letters) {
|
|
198
|
-
if (isSidewaysWritingMode(writingMode) || (!hasCJKCharacters(letter) && letter.trim().length > 0)) {
|
|
199
|
-
this.ctx.save();
|
|
200
|
-
this.ctx.translate(text.bounds.left + baseline, top);
|
|
201
|
-
this.ctx.rotate(writingMode === WRITING_MODE.SIDEWAYS_LR ? -Math.PI / 2 : Math.PI / 2);
|
|
202
|
-
renderFn(letter, 0, 0);
|
|
203
|
-
this.ctx.restore();
|
|
204
|
-
} else {
|
|
205
|
-
const savedBaseline = this.ctx.textBaseline;
|
|
206
|
-
if (hasCJKCharacters(letter)) {
|
|
207
|
-
this.ctx.textBaseline = 'ideographic';
|
|
208
|
-
}
|
|
209
|
-
renderFn(letter, text.bounds.left, top + baseline);
|
|
210
|
-
this.ctx.textBaseline = savedBaseline;
|
|
211
|
-
}
|
|
212
|
-
top += this.ctx.measureText(letter).width + letterSpacing;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Render text with letter-spacing applied (fill pass).
|
|
218
|
-
* When letterSpacing is 0 the whole string is drawn in one call; otherwise each
|
|
219
|
-
* grapheme is drawn individually so spacing and CJK baseline are applied correctly.
|
|
220
|
-
*/
|
|
221
|
-
renderTextWithLetterSpacing(
|
|
222
|
-
text: TextBounds,
|
|
223
|
-
letterSpacing: number,
|
|
224
|
-
baseline: number,
|
|
225
|
-
writingMode: WRITING_MODE = WRITING_MODE.HORIZONTAL_TB
|
|
226
|
-
): void {
|
|
227
|
-
this.renderFillText(text, letterSpacing, baseline, writingMode);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
private canRenderWholeText(letterSpacing: number, writingMode: WRITING_MODE): boolean {
|
|
231
|
-
return letterSpacing === 0 && !isVerticalWritingMode(writingMode);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
private renderFillText(text: TextBounds, letterSpacing: number, baseline: number, writingMode: WRITING_MODE): void {
|
|
235
|
-
if (this.canRenderWholeText(letterSpacing, writingMode)) {
|
|
236
|
-
this.ctx.fillText(text.text, text.bounds.left, text.bounds.top + baseline);
|
|
237
|
-
} else {
|
|
238
|
-
this.iterateLettersWithLetterSpacing(text, letterSpacing, baseline, writingMode, (letter, x, y) => {
|
|
239
|
-
this.ctx.fillText(letter, x, y);
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
private renderStrokeText(
|
|
245
|
-
text: TextBounds,
|
|
246
|
-
letterSpacing: number,
|
|
247
|
-
baseline: number,
|
|
248
|
-
writingMode: WRITING_MODE
|
|
249
|
-
): void {
|
|
250
|
-
if (this.canRenderWholeText(letterSpacing, writingMode)) {
|
|
251
|
-
this.ctx.strokeText(text.text, text.bounds.left, text.bounds.top + baseline);
|
|
252
|
-
} else {
|
|
253
|
-
this.iterateLettersWithLetterSpacing(text, letterSpacing, baseline, writingMode, (letter, x, y) => {
|
|
254
|
-
this.ctx.strokeText(letter, x, y);
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
private renderTextStrokeWithStyle(text: TextBounds, styles: CSSParsedDeclaration): void {
|
|
260
|
-
if (!styles.webkitTextStrokeWidth || !text.text.trim().length) {
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
this.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
|
|
265
|
-
this.ctx.lineWidth = styles.webkitTextStrokeWidth;
|
|
266
|
-
this.ctx.lineJoin = getTextStrokeLineJoin();
|
|
267
|
-
this.renderStrokeText(text, styles.letterSpacing, styles.fontSize.number, styles.writingMode);
|
|
268
|
-
this.ctx.strokeStyle = '';
|
|
269
|
-
this.ctx.lineWidth = 0;
|
|
270
|
-
this.ctx.lineJoin = 'miter';
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
private renderTextFillWithShadows(text: TextBounds, styles: CSSParsedDeclaration): void {
|
|
274
|
-
this.ctx.fillStyle = asString(styles.color);
|
|
275
|
-
this.renderTextWithLetterSpacing(text, styles.letterSpacing, styles.fontSize.number, styles.writingMode);
|
|
276
|
-
|
|
277
|
-
const textShadows: TextShadow = styles.textShadow;
|
|
278
|
-
if (textShadows.length && text.text.trim().length) {
|
|
279
|
-
textShadows
|
|
280
|
-
.slice(0)
|
|
281
|
-
.reverse()
|
|
282
|
-
.forEach((textShadow) => {
|
|
283
|
-
this.ctx.shadowColor = asString(textShadow.color);
|
|
284
|
-
this.ctx.shadowOffsetX = textShadow.offsetX.number * this.options.scale;
|
|
285
|
-
this.ctx.shadowOffsetY = textShadow.offsetY.number * this.options.scale;
|
|
286
|
-
this.ctx.shadowBlur = textShadow.blur.number;
|
|
287
|
-
|
|
288
|
-
this.renderTextWithLetterSpacing(
|
|
289
|
-
text,
|
|
290
|
-
styles.letterSpacing,
|
|
291
|
-
styles.fontSize.number,
|
|
292
|
-
styles.writingMode
|
|
293
|
-
);
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
this.ctx.shadowColor = '';
|
|
297
|
-
this.ctx.shadowOffsetX = 0;
|
|
298
|
-
this.ctx.shadowOffsetY = 0;
|
|
299
|
-
this.ctx.shadowBlur = 0;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Helper method to render text with paint order support
|
|
305
|
-
* Reduces code duplication in line-clamp and normal rendering
|
|
306
|
-
*/
|
|
307
|
-
private renderTextBoundWithPaintOrder(
|
|
308
|
-
textBound: TextBounds,
|
|
309
|
-
styles: CSSParsedDeclaration,
|
|
310
|
-
paintOrderLayers: number[]
|
|
311
|
-
): void {
|
|
312
|
-
paintOrderLayers.forEach((paintOrderLayer: number) => {
|
|
313
|
-
switch (paintOrderLayer) {
|
|
314
|
-
case PAINT_ORDER_LAYER.FILL:
|
|
315
|
-
this.ctx.fillStyle = asString(styles.color);
|
|
316
|
-
this.renderTextWithLetterSpacing(
|
|
317
|
-
textBound,
|
|
318
|
-
styles.letterSpacing,
|
|
319
|
-
styles.fontSize.number,
|
|
320
|
-
styles.writingMode
|
|
321
|
-
);
|
|
322
|
-
break;
|
|
323
|
-
case PAINT_ORDER_LAYER.STROKE:
|
|
324
|
-
this.renderTextStrokeWithStyle(textBound, styles);
|
|
325
|
-
break;
|
|
326
|
-
}
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
private renderTextDecoration(bounds: Bounds, styles: CSSParsedDeclaration): void {
|
|
331
|
-
this.ctx.fillStyle = asString(styles.textDecorationColor || styles.color);
|
|
332
|
-
|
|
333
|
-
// Calculate decoration line thickness
|
|
334
|
-
let thickness = 1; // default
|
|
335
|
-
if (typeof styles.textDecorationThickness === 'number') {
|
|
336
|
-
thickness = styles.textDecorationThickness;
|
|
337
|
-
} else if (styles.textDecorationThickness === 'from-font') {
|
|
338
|
-
// Use a reasonable default based on font size
|
|
339
|
-
thickness = Math.max(1, Math.floor(styles.fontSize.number * 0.05));
|
|
340
|
-
}
|
|
341
|
-
// 'auto' uses default thickness of 1
|
|
342
|
-
|
|
343
|
-
// Calculate underline offset
|
|
344
|
-
let underlineOffset = 0;
|
|
345
|
-
if (typeof styles.textUnderlineOffset === 'number') {
|
|
346
|
-
// It's a pixel value
|
|
347
|
-
underlineOffset = styles.textUnderlineOffset;
|
|
348
|
-
}
|
|
349
|
-
// 'auto' uses default offset of 0
|
|
350
|
-
|
|
351
|
-
const decorationStyle = styles.textDecorationStyle;
|
|
352
|
-
|
|
353
|
-
styles.textDecorationLine.forEach((textDecorationLine) => {
|
|
354
|
-
let y = 0;
|
|
355
|
-
|
|
356
|
-
switch (textDecorationLine) {
|
|
357
|
-
case TEXT_DECORATION_LINE.UNDERLINE:
|
|
358
|
-
y = bounds.top + bounds.height - thickness + underlineOffset;
|
|
359
|
-
break;
|
|
360
|
-
case TEXT_DECORATION_LINE.OVERLINE:
|
|
361
|
-
y = bounds.top;
|
|
362
|
-
break;
|
|
363
|
-
case TEXT_DECORATION_LINE.LINE_THROUGH:
|
|
364
|
-
y = bounds.top + (bounds.height / 2 - thickness / 2);
|
|
365
|
-
break;
|
|
366
|
-
default:
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
this.drawDecorationLine(bounds.left, y, bounds.width, thickness, decorationStyle);
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
private drawDecorationLine(x: number, y: number, width: number, thickness: number, style: number): void {
|
|
375
|
-
switch (style) {
|
|
376
|
-
case TEXT_DECORATION_STYLE.SOLID:
|
|
377
|
-
// Solid line (default)
|
|
378
|
-
this.ctx.fillRect(x, y, width, thickness);
|
|
379
|
-
break;
|
|
380
|
-
|
|
381
|
-
case TEXT_DECORATION_STYLE.DOUBLE:
|
|
382
|
-
// Double line
|
|
383
|
-
const gap = Math.max(1, thickness);
|
|
384
|
-
this.ctx.fillRect(x, y, width, thickness);
|
|
385
|
-
this.ctx.fillRect(x, y + thickness + gap, width, thickness);
|
|
386
|
-
break;
|
|
387
|
-
|
|
388
|
-
case TEXT_DECORATION_STYLE.DOTTED:
|
|
389
|
-
// Dotted line
|
|
390
|
-
this.ctx.save();
|
|
391
|
-
this.ctx.beginPath();
|
|
392
|
-
this.ctx.setLineDash([thickness, thickness * 2]);
|
|
393
|
-
this.ctx.lineWidth = thickness;
|
|
394
|
-
this.ctx.strokeStyle = this.ctx.fillStyle;
|
|
395
|
-
this.ctx.moveTo(x, y + thickness / 2);
|
|
396
|
-
this.ctx.lineTo(x + width, y + thickness / 2);
|
|
397
|
-
this.ctx.stroke();
|
|
398
|
-
this.ctx.restore();
|
|
399
|
-
break;
|
|
400
|
-
|
|
401
|
-
case TEXT_DECORATION_STYLE.DASHED:
|
|
402
|
-
// Dashed line
|
|
403
|
-
this.ctx.save();
|
|
404
|
-
this.ctx.beginPath();
|
|
405
|
-
this.ctx.setLineDash([thickness * 3, thickness * 2]);
|
|
406
|
-
this.ctx.lineWidth = thickness;
|
|
407
|
-
this.ctx.strokeStyle = this.ctx.fillStyle;
|
|
408
|
-
this.ctx.moveTo(x, y + thickness / 2);
|
|
409
|
-
this.ctx.lineTo(x + width, y + thickness / 2);
|
|
410
|
-
this.ctx.stroke();
|
|
411
|
-
this.ctx.restore();
|
|
412
|
-
break;
|
|
413
|
-
|
|
414
|
-
case TEXT_DECORATION_STYLE.WAVY:
|
|
415
|
-
// Wavy line (approximation using quadratic curves)
|
|
416
|
-
this.ctx.save();
|
|
417
|
-
this.ctx.beginPath();
|
|
418
|
-
this.ctx.lineWidth = thickness;
|
|
419
|
-
this.ctx.strokeStyle = this.ctx.fillStyle;
|
|
420
|
-
|
|
421
|
-
const amplitude = thickness * 2;
|
|
422
|
-
const wavelength = thickness * 4;
|
|
423
|
-
let currentX = x;
|
|
424
|
-
|
|
425
|
-
this.ctx.moveTo(currentX, y + thickness / 2);
|
|
426
|
-
|
|
427
|
-
while (currentX < x + width) {
|
|
428
|
-
const nextX = Math.min(currentX + wavelength / 2, x + width);
|
|
429
|
-
this.ctx.quadraticCurveTo(
|
|
430
|
-
currentX + wavelength / 4,
|
|
431
|
-
y + thickness / 2 - amplitude,
|
|
432
|
-
nextX,
|
|
433
|
-
y + thickness / 2
|
|
434
|
-
);
|
|
435
|
-
currentX = nextX;
|
|
436
|
-
|
|
437
|
-
if (currentX < x + width) {
|
|
438
|
-
const nextX2 = Math.min(currentX + wavelength / 2, x + width);
|
|
439
|
-
this.ctx.quadraticCurveTo(
|
|
440
|
-
currentX + wavelength / 4,
|
|
441
|
-
y + thickness / 2 + amplitude,
|
|
442
|
-
nextX2,
|
|
443
|
-
y + thickness / 2
|
|
444
|
-
);
|
|
445
|
-
currentX = nextX2;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
this.ctx.stroke();
|
|
450
|
-
this.ctx.restore();
|
|
451
|
-
break;
|
|
452
|
-
|
|
453
|
-
default:
|
|
454
|
-
// Fallback to solid
|
|
455
|
-
this.ctx.fillRect(x, y, width, thickness);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// Helper method to truncate text and add ellipsis if needed
|
|
460
|
-
private truncateTextWithEllipsis(text: string, maxWidth: number, letterSpacing: number): string {
|
|
461
|
-
// Use the Unicode ellipsis character (U+2026) whose width the browser measures
|
|
462
|
-
// as a single glyph, matching native text-overflow behaviour more closely.
|
|
463
|
-
const ellipsis = '\u2026';
|
|
464
|
-
const ellipsisWidth = this.ctx.measureText(ellipsis).width;
|
|
465
|
-
// Segment into grapheme clusters so multi-byte characters (emoji, composed
|
|
466
|
-
// sequences) are never split mid-character.
|
|
467
|
-
const graphemes = segmentGraphemes(text);
|
|
468
|
-
|
|
469
|
-
if (letterSpacing === 0) {
|
|
470
|
-
// Measure the whole candidate string for accuracy: the browser applies
|
|
471
|
-
// kerning and ligatures when rendering multiple glyphs together, so
|
|
472
|
-
// measuring them as one string is more precise than summing individual widths.
|
|
473
|
-
// Binary search reduces measurements from O(n) to O(log n).
|
|
474
|
-
const fits = (n: number) =>
|
|
475
|
-
this.ctx.measureText(graphemes.slice(0, n).join('')).width + ellipsisWidth <= maxWidth;
|
|
476
|
-
let lo = 0;
|
|
477
|
-
let hi = graphemes.length;
|
|
478
|
-
while (lo < hi) {
|
|
479
|
-
const mid = (lo + hi + 1) >> 1;
|
|
480
|
-
if (fits(mid)) {
|
|
481
|
-
lo = mid;
|
|
482
|
-
} else {
|
|
483
|
-
hi = mid - 1;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
return graphemes.slice(0, lo).join('') + ellipsis;
|
|
487
|
-
} else {
|
|
488
|
-
let width = ellipsisWidth;
|
|
489
|
-
const result: string[] = [];
|
|
490
|
-
|
|
491
|
-
for (const letter of graphemes) {
|
|
492
|
-
const glyphWidth = this.ctx.measureText(letter).width;
|
|
493
|
-
// Check against glyph width only (no trailing spacing): letter-spacing
|
|
494
|
-
// is applied *between* characters, not after the final glyph. Using
|
|
495
|
-
// `glyphWidth + letterSpacing` would incorrectly discard letters that
|
|
496
|
-
// fit as the last character before the ellipsis.
|
|
497
|
-
if (width + glyphWidth > maxWidth) {
|
|
498
|
-
break;
|
|
499
|
-
}
|
|
500
|
-
result.push(letter);
|
|
501
|
-
// Accumulate glyph + inter-character spacing for the *next* iteration.
|
|
502
|
-
width += glyphWidth + letterSpacing;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
return result.join('') + ellipsis;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/**
|
|
510
|
-
* Create font style array
|
|
511
|
-
* Public method used by list rendering
|
|
512
|
-
*/
|
|
513
|
-
createFontStyle(styles: CSSParsedDeclaration): string[] {
|
|
514
|
-
const fontVariant = styles.fontVariant
|
|
515
|
-
.filter((variant) => variant === 'normal' || variant === 'small-caps')
|
|
516
|
-
.join('');
|
|
517
|
-
const fontFamily = fixIOSSystemFonts(styles.fontFamily).join(', ');
|
|
518
|
-
const fontSize = isDimensionToken(styles.fontSize)
|
|
519
|
-
? `${styles.fontSize.number}${styles.fontSize.unit}`
|
|
520
|
-
: `${styles.fontSize.number}px`;
|
|
521
|
-
|
|
522
|
-
return [
|
|
523
|
-
[styles.fontStyle, fontVariant, styles.fontWeight, fontSize, fontFamily].join(' '),
|
|
524
|
-
fontFamily,
|
|
525
|
-
fontSize
|
|
526
|
-
];
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
async renderTextNode(text: TextContainer, styles: CSSParsedDeclaration, containerBounds?: Bounds): Promise<void> {
|
|
530
|
-
const [font] = this.createFontStyle(styles);
|
|
531
|
-
|
|
532
|
-
this.ctx.font = font;
|
|
533
|
-
|
|
534
|
-
this.ctx.direction = styles.direction === DIRECTION.RTL ? 'rtl' : 'ltr';
|
|
535
|
-
this.ctx.textAlign = 'left';
|
|
536
|
-
this.ctx.textBaseline = 'alphabetic';
|
|
537
|
-
const paintOrder = styles.paintOrder;
|
|
538
|
-
|
|
539
|
-
// Calculate line height for text layout detection (used by both line-clamp and ellipsis)
|
|
540
|
-
const lineHeight = styles.fontSize.number * 1.5;
|
|
541
|
-
|
|
542
|
-
// Check if we need to apply -webkit-line-clamp
|
|
543
|
-
// This limits text to a specific number of lines with ellipsis
|
|
544
|
-
const shouldApplyLineClamp =
|
|
545
|
-
styles.webkitLineClamp > 0 &&
|
|
546
|
-
(styles.display & DISPLAY.BLOCK) !== 0 &&
|
|
547
|
-
styles.overflowY === OVERFLOW.HIDDEN &&
|
|
548
|
-
text.textBounds.length > 0;
|
|
549
|
-
|
|
550
|
-
if (shouldApplyLineClamp) {
|
|
551
|
-
// Group text bounds by lines based on their Y position
|
|
552
|
-
const lines: TextBounds[][] = [];
|
|
553
|
-
let currentLine: TextBounds[] = [];
|
|
554
|
-
let currentLineTop = text.textBounds[0].bounds.top;
|
|
555
|
-
|
|
556
|
-
text.textBounds.forEach((tb) => {
|
|
557
|
-
// If this text bound is on a different line, start a new line
|
|
558
|
-
if (Math.abs(tb.bounds.top - currentLineTop) >= lineHeight * 0.5) {
|
|
559
|
-
if (currentLine.length > 0) {
|
|
560
|
-
lines.push(currentLine);
|
|
561
|
-
}
|
|
562
|
-
currentLine = [tb];
|
|
563
|
-
currentLineTop = tb.bounds.top;
|
|
564
|
-
} else {
|
|
565
|
-
currentLine.push(tb);
|
|
566
|
-
}
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
// Don't forget the last line
|
|
570
|
-
if (currentLine.length > 0) {
|
|
571
|
-
lines.push(currentLine);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// Only render up to webkitLineClamp lines
|
|
575
|
-
const maxLines = styles.webkitLineClamp;
|
|
576
|
-
if (lines.length > maxLines) {
|
|
577
|
-
// Render only the first (maxLines - 1) complete lines
|
|
578
|
-
for (let i = 0; i < maxLines - 1; i++) {
|
|
579
|
-
lines[i].forEach((textBound) => {
|
|
580
|
-
this.renderTextBoundWithPaintOrder(textBound, styles, paintOrder);
|
|
581
|
-
});
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// For the last line, truncate with ellipsis
|
|
585
|
-
const lastLine = lines[maxLines - 1];
|
|
586
|
-
if (lastLine && lastLine.length > 0 && containerBounds) {
|
|
587
|
-
const lastLineText = lastLine.map((tb) => tb.text).join('');
|
|
588
|
-
const firstBound = lastLine[0];
|
|
589
|
-
const availableWidth = containerBounds.width - (firstBound.bounds.left - containerBounds.left);
|
|
590
|
-
const truncatedText = this.truncateTextWithEllipsis(
|
|
591
|
-
lastLineText,
|
|
592
|
-
availableWidth,
|
|
593
|
-
styles.letterSpacing
|
|
594
|
-
);
|
|
595
|
-
|
|
596
|
-
// Build TextBounds once; reused for fill and stroke without re-allocating.
|
|
597
|
-
const truncatedBounds = new TextBounds(truncatedText, firstBound.bounds);
|
|
598
|
-
|
|
599
|
-
paintOrder.forEach((paintOrderLayer) => {
|
|
600
|
-
switch (paintOrderLayer) {
|
|
601
|
-
case PAINT_ORDER_LAYER.FILL:
|
|
602
|
-
this.ctx.fillStyle = asString(styles.color);
|
|
603
|
-
this.renderTextWithLetterSpacing(
|
|
604
|
-
truncatedBounds,
|
|
605
|
-
styles.letterSpacing,
|
|
606
|
-
styles.fontSize.number,
|
|
607
|
-
styles.writingMode
|
|
608
|
-
);
|
|
609
|
-
break;
|
|
610
|
-
case PAINT_ORDER_LAYER.STROKE:
|
|
611
|
-
this.renderTextStrokeWithStyle(truncatedBounds, styles);
|
|
612
|
-
break;
|
|
613
|
-
}
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
return; // Don't render anything else
|
|
617
|
-
}
|
|
618
|
-
// If lines.length <= maxLines, fall through to normal rendering
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// Check if we need to apply text-overflow: ellipsis
|
|
622
|
-
// Issue #203: Only apply ellipsis for single-line text overflow
|
|
623
|
-
// Multi-line text truncation (like -webkit-line-clamp) should not be affected
|
|
624
|
-
const shouldApplyEllipsis =
|
|
625
|
-
styles.textOverflow === TEXT_OVERFLOW.ELLIPSIS &&
|
|
626
|
-
containerBounds &&
|
|
627
|
-
styles.overflowX === OVERFLOW.HIDDEN &&
|
|
628
|
-
text.textBounds.length > 0;
|
|
629
|
-
|
|
630
|
-
// Calculate total text width if ellipsis might be needed
|
|
631
|
-
let needsEllipsis = false;
|
|
632
|
-
let truncatedText = '';
|
|
633
|
-
if (shouldApplyEllipsis) {
|
|
634
|
-
// Check if all text bounds are on approximately the same line (single-line scenario)
|
|
635
|
-
// For multi-line text (like -webkit-line-clamp), textBounds will have different Y positions
|
|
636
|
-
const firstTop = text.textBounds[0].bounds.top;
|
|
637
|
-
const isSingleLine = text.textBounds.every((tb) => Math.abs(tb.bounds.top - firstTop) < lineHeight * 0.5);
|
|
638
|
-
|
|
639
|
-
if (isSingleLine) {
|
|
640
|
-
// Measure the full text content
|
|
641
|
-
// Note: text.textBounds may contain whitespace characters from HTML formatting
|
|
642
|
-
// We need to collapse them like the browser does for white-space: nowrap
|
|
643
|
-
let fullText = text.textBounds.map((tb) => tb.text).join('');
|
|
644
|
-
|
|
645
|
-
// Collapse whitespace: replace sequences of whitespace (including newlines) with single spaces
|
|
646
|
-
// and trim leading/trailing whitespace
|
|
647
|
-
fullText = fullText.replace(/\s+/g, ' ').trim();
|
|
648
|
-
|
|
649
|
-
const fullTextWidth = this.ctx.measureText(fullText).width;
|
|
650
|
-
const availableWidth = containerBounds.width;
|
|
651
|
-
|
|
652
|
-
if (fullTextWidth > availableWidth) {
|
|
653
|
-
needsEllipsis = true;
|
|
654
|
-
truncatedText = this.truncateTextWithEllipsis(fullText, availableWidth, styles.letterSpacing);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// If ellipsis is needed, render the truncated text once
|
|
660
|
-
if (needsEllipsis) {
|
|
661
|
-
const firstBound = text.textBounds[0];
|
|
662
|
-
// Build TextBounds once; reused across paint layers and every shadow pass
|
|
663
|
-
// to avoid repeated allocation inside forEach callbacks.
|
|
664
|
-
const truncatedBounds = new TextBounds(truncatedText, firstBound.bounds);
|
|
665
|
-
|
|
666
|
-
paintOrder.forEach((paintOrderLayer) => {
|
|
667
|
-
switch (paintOrderLayer) {
|
|
668
|
-
case PAINT_ORDER_LAYER.FILL: {
|
|
669
|
-
this.renderTextFillWithShadows(truncatedBounds, styles);
|
|
670
|
-
break;
|
|
671
|
-
}
|
|
672
|
-
case PAINT_ORDER_LAYER.STROKE:
|
|
673
|
-
this.renderTextStrokeWithStyle(truncatedBounds, styles);
|
|
674
|
-
break;
|
|
675
|
-
}
|
|
676
|
-
});
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
// Normal rendering (no ellipsis needed)
|
|
681
|
-
text.textBounds.forEach((text) => {
|
|
682
|
-
paintOrder.forEach((paintOrderLayer) => {
|
|
683
|
-
switch (paintOrderLayer) {
|
|
684
|
-
case PAINT_ORDER_LAYER.FILL: {
|
|
685
|
-
this.renderTextFillWithShadows(text, styles);
|
|
686
|
-
|
|
687
|
-
if (styles.textDecorationLine.length) {
|
|
688
|
-
this.renderTextDecoration(text.bounds, styles);
|
|
689
|
-
}
|
|
690
|
-
break;
|
|
691
|
-
}
|
|
692
|
-
case PAINT_ORDER_LAYER.STROKE: {
|
|
693
|
-
this.renderTextStrokeWithStyle(text, styles);
|
|
694
|
-
break;
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
});
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
}
|