@zseven-w/pen-renderer 0.6.0 → 0.7.0
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 +6 -6
- package/package.json +9 -9
- package/src/__tests__/document-flattener.test.ts +166 -90
- package/src/__tests__/font-manager.test.ts +65 -0
- package/src/__tests__/image-loader.test.ts +136 -0
- package/src/__tests__/paint-utils.test.ts +61 -0
- package/src/__tests__/render-node-thumbnail.test.ts +312 -0
- package/src/document-flattener.ts +222 -159
- package/src/font-manager.ts +221 -190
- package/src/image-loader.ts +138 -51
- package/src/index.ts +18 -17
- package/src/init.ts +50 -21
- package/src/node-renderer.ts +957 -386
- package/src/paint-utils.ts +99 -74
- package/src/path-utils.ts +235 -115
- package/src/render-node-thumbnail.ts +155 -0
- package/src/renderer.ts +196 -175
- package/src/spatial-index.ts +139 -27
- package/src/text-renderer.ts +360 -302
- package/src/types.ts +18 -22
- package/src/viewport.ts +28 -29
package/src/text-renderer.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { CanvasKit, Canvas, Image as SkImage, Paragraph } from 'canvaskit-wasm'
|
|
2
|
-
import type { PenNode, TextNode } from '@zseven-w/pen-types'
|
|
3
|
-
import type { PenEffect, ShadowEffect } from '@zseven-w/pen-types'
|
|
4
|
-
import { defaultLineHeight, cssFontFamily } from '@zseven-w/pen-core'
|
|
5
|
-
import { parseColor, resolveFillColor, wrapLine } from './paint-utils.js'
|
|
6
|
-
import { SkiaFontManager, type FontManagerOptions } from './font-manager.js'
|
|
1
|
+
import type { CanvasKit, Canvas, Image as SkImage, Paragraph } from 'canvaskit-wasm';
|
|
2
|
+
import type { PenNode, TextNode } from '@zseven-w/pen-types';
|
|
3
|
+
import type { PenEffect, ShadowEffect } from '@zseven-w/pen-types';
|
|
4
|
+
import { defaultLineHeight, cssFontFamily } from '@zseven-w/pen-core';
|
|
5
|
+
import { parseColor, resolveFillColor, wrapLine } from './paint-utils.js';
|
|
6
|
+
import { SkiaFontManager, type FontManagerOptions } from './font-manager.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Text rendering sub-system for SkiaNodeRenderer.
|
|
@@ -11,131 +11,146 @@ import { SkiaFontManager, type FontManagerOptions } from './font-manager.js'
|
|
|
11
11
|
* with caching for performance.
|
|
12
12
|
*/
|
|
13
13
|
export class SkiaTextRenderer {
|
|
14
|
-
private ck: CanvasKit
|
|
14
|
+
private ck: CanvasKit;
|
|
15
15
|
|
|
16
16
|
// Text rasterization cache (Canvas 2D -> CanvasKit Image)
|
|
17
17
|
// FIFO eviction via Map insertion order; bytes tracked separately against TEXT_CACHE_BYTE_LIMIT.
|
|
18
|
-
private textCache = new Map<string, SkImage | null>()
|
|
19
|
-
private textCacheBytes = 0
|
|
18
|
+
private textCache = new Map<string, SkImage | null>();
|
|
19
|
+
private textCacheBytes = 0;
|
|
20
20
|
// 256 MB — each bitmap entry is ~cw*ch*4 bytes (RGBA pixels)
|
|
21
|
-
private static TEXT_CACHE_BYTE_LIMIT = 256 * 1024 * 1024
|
|
21
|
+
private static TEXT_CACHE_BYTE_LIMIT = 256 * 1024 * 1024;
|
|
22
22
|
|
|
23
23
|
// Paragraph cache for vector text (keyed by content+style)
|
|
24
24
|
// FIFO eviction via Map insertion order; bytes estimated from content length against PARA_CACHE_BYTE_LIMIT.
|
|
25
|
-
private paraCache = new Map<string, Paragraph | null>()
|
|
26
|
-
private paraCacheBytes = 0
|
|
25
|
+
private paraCache = new Map<string, Paragraph | null>();
|
|
26
|
+
private paraCacheBytes = 0;
|
|
27
27
|
// 64 MB — each entry is estimated as content.length*64+4096 bytes (WASM heap approximation)
|
|
28
|
-
private static PARA_CACHE_BYTE_LIMIT = 64 * 1024 * 1024
|
|
28
|
+
private static PARA_CACHE_BYTE_LIMIT = 64 * 1024 * 1024;
|
|
29
29
|
|
|
30
30
|
// Pre-rasterized paragraph image cache (SkImage, same key as paraCache, zoom-independent)
|
|
31
31
|
// Allows drawImageRect instead of drawParagraph on every frame — avoids per-frame glyph rasterization.
|
|
32
|
-
private paraImageCache = new Map<string, SkImage | null>()
|
|
33
|
-
private paraImageCacheBytes = 0
|
|
32
|
+
private paraImageCache = new Map<string, SkImage | null>();
|
|
33
|
+
private paraImageCacheBytes = 0;
|
|
34
34
|
// 128 MB — each entry is sw*sh*4 bytes (RGBA pixels at up to 2x DPR scale)
|
|
35
|
-
private static PARA_IMAGE_CACHE_BYTE_LIMIT = 128 * 1024 * 1024
|
|
35
|
+
private static PARA_IMAGE_CACHE_BYTE_LIMIT = 128 * 1024 * 1024;
|
|
36
36
|
|
|
37
37
|
private static estimateParaBytes(content: string): number {
|
|
38
|
-
return content.length * 64 + 4096
|
|
38
|
+
return content.length * 64 + 4096;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
// Current viewport zoom (set by engine before each render frame)
|
|
42
|
-
zoom = 1
|
|
42
|
+
zoom = 1;
|
|
43
43
|
|
|
44
44
|
// Device pixel ratio override
|
|
45
|
-
devicePixelRatio: number | undefined
|
|
45
|
+
devicePixelRatio: number | undefined;
|
|
46
46
|
|
|
47
47
|
private get _dpr(): number {
|
|
48
|
-
return
|
|
48
|
+
return (
|
|
49
|
+
this.devicePixelRatio ?? (typeof window !== 'undefined' ? window.devicePixelRatio : 1) ?? 1
|
|
50
|
+
);
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
// Font manager for vector text rendering
|
|
52
|
-
fontManager: SkiaFontManager
|
|
54
|
+
fontManager: SkiaFontManager;
|
|
53
55
|
|
|
54
56
|
constructor(ck: CanvasKit, fontOptions?: FontManagerOptions) {
|
|
55
|
-
this.ck = ck
|
|
56
|
-
this.fontManager = new SkiaFontManager(ck, fontOptions)
|
|
57
|
+
this.ck = ck;
|
|
58
|
+
this.fontManager = new SkiaFontManager(ck, fontOptions);
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
clearTextCache() {
|
|
60
62
|
for (const img of this.textCache.values()) {
|
|
61
|
-
img?.delete()
|
|
63
|
+
img?.delete();
|
|
62
64
|
}
|
|
63
|
-
this.textCache.clear()
|
|
64
|
-
this.textCacheBytes = 0
|
|
65
|
+
this.textCache.clear();
|
|
66
|
+
this.textCacheBytes = 0;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
clearParaCache() {
|
|
68
70
|
for (const p of this.paraCache.values()) {
|
|
69
|
-
p?.delete()
|
|
71
|
+
p?.delete();
|
|
70
72
|
}
|
|
71
|
-
this.paraCache.clear()
|
|
72
|
-
this.paraCacheBytes = 0
|
|
73
|
+
this.paraCache.clear();
|
|
74
|
+
this.paraCacheBytes = 0;
|
|
73
75
|
for (const img of this.paraImageCache.values()) {
|
|
74
|
-
img?.delete()
|
|
76
|
+
img?.delete();
|
|
75
77
|
}
|
|
76
|
-
this.paraImageCache.clear()
|
|
77
|
-
this.paraImageCacheBytes = 0
|
|
78
|
+
this.paraImageCache.clear();
|
|
79
|
+
this.paraImageCacheBytes = 0;
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
// Evict oldest entries (Map head = first inserted) until there is room for `incoming` bytes.
|
|
81
83
|
private evictParaCache(incoming: number) {
|
|
82
|
-
while (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
while (
|
|
85
|
+
this.paraCacheBytes + incoming > SkiaTextRenderer.PARA_CACHE_BYTE_LIMIT &&
|
|
86
|
+
this.paraCache.size > 0
|
|
87
|
+
) {
|
|
88
|
+
const [key, para] = this.paraCache.entries().next().value!;
|
|
89
|
+
para?.delete();
|
|
90
|
+
this.paraCache.delete(key);
|
|
91
|
+
this.paraCacheBytes -= SkiaTextRenderer.estimateParaBytes(key.split('|')[1] ?? '');
|
|
87
92
|
}
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
private evictParaImageCache(incoming: number) {
|
|
91
|
-
while (
|
|
92
|
-
|
|
96
|
+
while (
|
|
97
|
+
this.paraImageCacheBytes + incoming > SkiaTextRenderer.PARA_IMAGE_CACHE_BYTE_LIMIT &&
|
|
98
|
+
this.paraImageCache.size > 0
|
|
99
|
+
) {
|
|
100
|
+
const [key, img] = this.paraImageCache.entries().next().value!;
|
|
93
101
|
if (img) {
|
|
94
|
-
this.paraImageCacheBytes -= img.width() * img.height() * 4
|
|
95
|
-
img.delete()
|
|
102
|
+
this.paraImageCacheBytes -= img.width() * img.height() * 4;
|
|
103
|
+
img.delete();
|
|
96
104
|
}
|
|
97
|
-
this.paraImageCache.delete(key)
|
|
105
|
+
this.paraImageCache.delete(key);
|
|
98
106
|
}
|
|
99
107
|
}
|
|
100
108
|
|
|
101
109
|
private evictTextCache(incoming: number) {
|
|
102
|
-
while (
|
|
103
|
-
|
|
110
|
+
while (
|
|
111
|
+
this.textCacheBytes + incoming > SkiaTextRenderer.TEXT_CACHE_BYTE_LIMIT &&
|
|
112
|
+
this.textCache.size > 0
|
|
113
|
+
) {
|
|
114
|
+
const [key, img] = this.textCache.entries().next().value!;
|
|
104
115
|
if (img) {
|
|
105
|
-
this.textCacheBytes -= img.width() * img.height() * 4
|
|
106
|
-
img.delete()
|
|
116
|
+
this.textCacheBytes -= img.width() * img.height() * 4;
|
|
117
|
+
img.delete();
|
|
107
118
|
}
|
|
108
|
-
this.textCache.delete(key)
|
|
119
|
+
this.textCache.delete(key);
|
|
109
120
|
}
|
|
110
121
|
}
|
|
111
122
|
|
|
112
123
|
dispose() {
|
|
113
|
-
this.clearTextCache()
|
|
114
|
-
this.clearParaCache()
|
|
115
|
-
this.fontManager.dispose()
|
|
124
|
+
this.clearTextCache();
|
|
125
|
+
this.clearParaCache();
|
|
126
|
+
this.fontManager.dispose();
|
|
116
127
|
}
|
|
117
128
|
|
|
118
129
|
/**
|
|
119
130
|
* Main text drawing entry — tries vector, falls back to bitmap.
|
|
120
131
|
*/
|
|
121
132
|
drawText(
|
|
122
|
-
canvas: Canvas,
|
|
123
|
-
|
|
133
|
+
canvas: Canvas,
|
|
134
|
+
node: PenNode,
|
|
135
|
+
x: number,
|
|
136
|
+
y: number,
|
|
137
|
+
w: number,
|
|
138
|
+
h: number,
|
|
124
139
|
opacity: number,
|
|
125
140
|
effects?: PenEffect[],
|
|
126
141
|
) {
|
|
127
142
|
// Draw text shadow as blurred copy of the text glyphs (not a rectangle)
|
|
128
|
-
const shadow = effects?.find((e): e is ShadowEffect => e.type === 'shadow')
|
|
143
|
+
const shadow = effects?.find((e): e is ShadowEffect => e.type === 'shadow');
|
|
129
144
|
if (shadow) {
|
|
130
|
-
this.drawTextShadow(canvas, node, x, y, w, h, opacity, shadow)
|
|
145
|
+
this.drawTextShadow(canvas, node, x, y, w, h, opacity, shadow);
|
|
131
146
|
}
|
|
132
147
|
|
|
133
148
|
// Try vector text first (true Skia Paragraph API)
|
|
134
|
-
const vectorOk = this.drawTextVector(canvas, node, x, y, w, h, opacity)
|
|
135
|
-
if (vectorOk) return
|
|
149
|
+
const vectorOk = this.drawTextVector(canvas, node, x, y, w, h, opacity);
|
|
150
|
+
if (vectorOk) return;
|
|
136
151
|
|
|
137
152
|
// Fallback to bitmap text rendering
|
|
138
|
-
this.drawTextBitmap(canvas, node, x, y, w, h, opacity)
|
|
153
|
+
this.drawTextBitmap(canvas, node, x, y, w, h, opacity);
|
|
139
154
|
}
|
|
140
155
|
|
|
141
156
|
/**
|
|
@@ -143,73 +158,79 @@ export class SkiaTextRenderer {
|
|
|
143
158
|
* Returns true if rendered, false if font not available (caller should fallback).
|
|
144
159
|
*/
|
|
145
160
|
drawTextVector(
|
|
146
|
-
canvas: Canvas,
|
|
147
|
-
|
|
161
|
+
canvas: Canvas,
|
|
162
|
+
node: PenNode,
|
|
163
|
+
x: number,
|
|
164
|
+
y: number,
|
|
165
|
+
w: number,
|
|
166
|
+
_h: number,
|
|
148
167
|
opacity: number,
|
|
149
168
|
): boolean {
|
|
150
|
-
const ck = this.ck
|
|
151
|
-
const tNode = node as TextNode
|
|
152
|
-
const content =
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
+
const ck = this.ck;
|
|
170
|
+
const tNode = node as TextNode;
|
|
171
|
+
const content =
|
|
172
|
+
typeof tNode.content === 'string'
|
|
173
|
+
? tNode.content
|
|
174
|
+
: Array.isArray(tNode.content)
|
|
175
|
+
? tNode.content.map((s) => s.text ?? '').join('')
|
|
176
|
+
: (((tNode as unknown as Record<string, unknown>).text as string) ?? '');
|
|
177
|
+
if (!content) return true;
|
|
178
|
+
|
|
179
|
+
const fontSize = tNode.fontSize ?? 16;
|
|
180
|
+
const fillColor = resolveFillColor(tNode.fill);
|
|
181
|
+
const fontWeight = tNode.fontWeight ?? '400';
|
|
182
|
+
const fontFamily = tNode.fontFamily ?? 'Inter';
|
|
183
|
+
const textAlign: string = tNode.textAlign ?? 'left';
|
|
184
|
+
const lineHeightMul = tNode.lineHeight ?? defaultLineHeight(fontSize);
|
|
185
|
+
const textGrowth = tNode.textGrowth;
|
|
186
|
+
const letterSpacing = tNode.letterSpacing ?? 0;
|
|
187
|
+
|
|
188
|
+
const primaryFamily = fontFamily.split(',')[0].trim().replace(/['"]/g, '');
|
|
169
189
|
if (!this.fontManager.isFontReady(primaryFamily)) {
|
|
170
190
|
if (this.fontManager.isSystemFont(primaryFamily)) {
|
|
171
|
-
return false
|
|
191
|
+
return false;
|
|
172
192
|
}
|
|
173
193
|
this.fontManager.ensureFont(primaryFamily).then((ok) => {
|
|
174
194
|
if (ok) {
|
|
175
|
-
this.clearParaCache()
|
|
176
|
-
|
|
195
|
+
this.clearParaCache();
|
|
196
|
+
(this as any)._onFontLoaded?.();
|
|
177
197
|
}
|
|
178
|
-
})
|
|
198
|
+
});
|
|
179
199
|
if (!this.fontManager.hasAnyFallback(primaryFamily)) {
|
|
180
|
-
return false
|
|
200
|
+
return false;
|
|
181
201
|
}
|
|
182
202
|
}
|
|
183
203
|
|
|
184
|
-
const isFixedWidth = textGrowth === 'fixed-width' || textGrowth === 'fixed-width-height'
|
|
185
|
-
const fwTolerance = isFixedWidth ? Math.min(Math.ceil(w * 0.05), Math.ceil(fontSize * 0.5)) : 0
|
|
186
|
-
const layoutWidth = isFixedWidth && w > 0 ? w + fwTolerance : 1e6
|
|
187
|
-
const effectiveAlign = isFixedWidth ? textAlign : 'left'
|
|
204
|
+
const isFixedWidth = textGrowth === 'fixed-width' || textGrowth === 'fixed-width-height';
|
|
205
|
+
const fwTolerance = isFixedWidth ? Math.min(Math.ceil(w * 0.05), Math.ceil(fontSize * 0.5)) : 0;
|
|
206
|
+
const layoutWidth = isFixedWidth && w > 0 ? w + fwTolerance : 1e6;
|
|
207
|
+
const effectiveAlign = isFixedWidth ? textAlign : 'left';
|
|
188
208
|
|
|
189
|
-
const cacheKey = `p|${content}|${fontSize}|${fillColor}|${fontWeight}|${fontFamily}|${effectiveAlign}|${Math.round(layoutWidth)}|${letterSpacing}|${lineHeightMul}
|
|
209
|
+
const cacheKey = `p|${content}|${fontSize}|${fillColor}|${fontWeight}|${fontFamily}|${effectiveAlign}|${Math.round(layoutWidth)}|${letterSpacing}|${lineHeightMul}`;
|
|
190
210
|
|
|
191
|
-
let para = this.paraCache.get(cacheKey)
|
|
211
|
+
let para = this.paraCache.get(cacheKey);
|
|
192
212
|
if (para === undefined) {
|
|
193
|
-
const color = parseColor(ck, fillColor)
|
|
194
|
-
|
|
195
|
-
let ckAlign = ck.TextAlign.Left
|
|
196
|
-
if (effectiveAlign === 'center') ckAlign = ck.TextAlign.Center
|
|
197
|
-
else if (effectiveAlign === 'right') ckAlign = ck.TextAlign.Right
|
|
198
|
-
else if (effectiveAlign === 'justify') ckAlign = ck.TextAlign.Justify
|
|
199
|
-
|
|
200
|
-
const weightNum =
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
else if (weightNum <=
|
|
205
|
-
else if (weightNum <=
|
|
206
|
-
else if (weightNum <=
|
|
207
|
-
else if (weightNum <=
|
|
208
|
-
else if (weightNum <=
|
|
209
|
-
else if (weightNum <=
|
|
210
|
-
else ckWeight = ck.FontWeight.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
+
const color = parseColor(ck, fillColor);
|
|
214
|
+
|
|
215
|
+
let ckAlign = ck.TextAlign.Left;
|
|
216
|
+
if (effectiveAlign === 'center') ckAlign = ck.TextAlign.Center;
|
|
217
|
+
else if (effectiveAlign === 'right') ckAlign = ck.TextAlign.Right;
|
|
218
|
+
else if (effectiveAlign === 'justify') ckAlign = ck.TextAlign.Justify;
|
|
219
|
+
|
|
220
|
+
const weightNum =
|
|
221
|
+
typeof fontWeight === 'number' ? fontWeight : parseInt(fontWeight as string, 10) || 400;
|
|
222
|
+
let ckWeight = ck.FontWeight.Normal;
|
|
223
|
+
if (weightNum <= 100) ckWeight = ck.FontWeight.Thin;
|
|
224
|
+
else if (weightNum <= 200) ckWeight = ck.FontWeight.ExtraLight;
|
|
225
|
+
else if (weightNum <= 300) ckWeight = ck.FontWeight.Light;
|
|
226
|
+
else if (weightNum <= 400) ckWeight = ck.FontWeight.Normal;
|
|
227
|
+
else if (weightNum <= 500) ckWeight = ck.FontWeight.Medium;
|
|
228
|
+
else if (weightNum <= 600) ckWeight = ck.FontWeight.SemiBold;
|
|
229
|
+
else if (weightNum <= 700) ckWeight = ck.FontWeight.Bold;
|
|
230
|
+
else if (weightNum <= 800) ckWeight = ck.FontWeight.ExtraBold;
|
|
231
|
+
else ckWeight = ck.FontWeight.Black;
|
|
232
|
+
|
|
233
|
+
const fallbackFamilies = this.fontManager.getFallbackChain(primaryFamily);
|
|
213
234
|
|
|
214
235
|
const paraStyle = new ck.ParagraphStyle({
|
|
215
236
|
textAlign: ckAlign,
|
|
@@ -222,315 +243,352 @@ export class SkiaTextRenderer {
|
|
|
222
243
|
heightMultiplier: lineHeightMul,
|
|
223
244
|
halfLeading: true,
|
|
224
245
|
},
|
|
225
|
-
})
|
|
246
|
+
});
|
|
226
247
|
|
|
227
248
|
try {
|
|
228
249
|
const builder = ck.ParagraphBuilder.MakeFromFontProvider(
|
|
229
250
|
paraStyle,
|
|
230
251
|
this.fontManager.getProvider(),
|
|
231
|
-
)
|
|
252
|
+
);
|
|
232
253
|
|
|
233
254
|
// Handle styled segments
|
|
234
|
-
if (
|
|
255
|
+
if (
|
|
256
|
+
Array.isArray(tNode.content) &&
|
|
257
|
+
tNode.content.some((s) => s.fontFamily || s.fontSize || s.fontWeight || s.fill)
|
|
258
|
+
) {
|
|
235
259
|
for (const seg of tNode.content) {
|
|
236
260
|
if (seg.fontFamily || seg.fontSize || seg.fontWeight || seg.fill) {
|
|
237
|
-
const segColor = seg.fill ? parseColor(ck, seg.fill) : color
|
|
261
|
+
const segColor = seg.fill ? parseColor(ck, seg.fill) : color;
|
|
238
262
|
const segWeight = seg.fontWeight
|
|
239
|
-
?
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
263
|
+
? typeof seg.fontWeight === 'number'
|
|
264
|
+
? seg.fontWeight
|
|
265
|
+
: parseInt(seg.fontWeight as string, 10) || weightNum
|
|
266
|
+
: weightNum;
|
|
267
|
+
const segPrimary =
|
|
268
|
+
seg.fontFamily?.split(',')[0].trim().replace(/['"]/g, '') ?? primaryFamily;
|
|
269
|
+
builder.pushStyle(
|
|
270
|
+
new ck.TextStyle({
|
|
271
|
+
color: segColor,
|
|
272
|
+
fontSize: seg.fontSize ?? fontSize,
|
|
273
|
+
fontFamilies: this.fontManager.getFallbackChain(segPrimary),
|
|
274
|
+
fontStyle: { weight: segWeight as any },
|
|
275
|
+
letterSpacing,
|
|
276
|
+
heightMultiplier: lineHeightMul,
|
|
277
|
+
halfLeading: true,
|
|
278
|
+
}),
|
|
279
|
+
);
|
|
280
|
+
builder.addText(seg.text ?? '');
|
|
281
|
+
builder.pop();
|
|
253
282
|
} else {
|
|
254
|
-
builder.addText(seg.text ?? '')
|
|
283
|
+
builder.addText(seg.text ?? '');
|
|
255
284
|
}
|
|
256
285
|
}
|
|
257
286
|
} else {
|
|
258
|
-
builder.addText(content)
|
|
287
|
+
builder.addText(content);
|
|
259
288
|
}
|
|
260
289
|
|
|
261
|
-
para = builder.build()
|
|
262
|
-
para.layout(layoutWidth)
|
|
263
|
-
builder.delete()
|
|
264
|
-
const entryBytes = SkiaTextRenderer.estimateParaBytes(content)
|
|
265
|
-
this.evictParaCache(entryBytes)
|
|
266
|
-
this.paraCacheBytes += entryBytes
|
|
290
|
+
para = builder.build();
|
|
291
|
+
para.layout(layoutWidth);
|
|
292
|
+
builder.delete();
|
|
293
|
+
const entryBytes = SkiaTextRenderer.estimateParaBytes(content);
|
|
294
|
+
this.evictParaCache(entryBytes);
|
|
295
|
+
this.paraCacheBytes += entryBytes;
|
|
267
296
|
} catch {
|
|
268
|
-
para = null
|
|
297
|
+
para = null;
|
|
269
298
|
}
|
|
270
299
|
|
|
271
|
-
this.paraCache.set(cacheKey, para ?? null)
|
|
300
|
+
this.paraCache.set(cacheKey, para ?? null);
|
|
272
301
|
}
|
|
273
302
|
|
|
274
|
-
if (!para) return false
|
|
303
|
+
if (!para) return false;
|
|
275
304
|
|
|
276
305
|
// Compute drawX and surface dimensions
|
|
277
|
-
let drawX = x
|
|
278
|
-
let surfaceW: number
|
|
306
|
+
let drawX = x;
|
|
307
|
+
let surfaceW: number;
|
|
279
308
|
if (!isFixedWidth) {
|
|
280
|
-
const longestLine = para.getLongestLine()
|
|
281
|
-
surfaceW = longestLine + 2
|
|
309
|
+
const longestLine = para.getLongestLine();
|
|
310
|
+
surfaceW = longestLine + 2;
|
|
282
311
|
if (w > 0 && textAlign !== 'left') {
|
|
283
|
-
if (textAlign === 'center') drawX = x + Math.max(0, (w - longestLine) / 2)
|
|
284
|
-
else if (textAlign === 'right') drawX = x + Math.max(0, w - longestLine)
|
|
312
|
+
if (textAlign === 'center') drawX = x + Math.max(0, (w - longestLine) / 2);
|
|
313
|
+
else if (textAlign === 'right') drawX = x + Math.max(0, w - longestLine);
|
|
285
314
|
}
|
|
286
315
|
} else {
|
|
287
|
-
surfaceW = layoutWidth
|
|
316
|
+
surfaceW = layoutWidth;
|
|
288
317
|
}
|
|
289
|
-
const surfaceH = para.getHeight() + 2
|
|
318
|
+
const surfaceH = para.getHeight() + 2;
|
|
290
319
|
|
|
291
320
|
// Try paragraph image cache: drawImageRect is far cheaper than drawParagraph per frame.
|
|
292
321
|
// Skip cache when zoomed in (> 1x) or significantly zoomed out (< 0.5x) — cached
|
|
293
322
|
// bitmaps are at fixed DPR resolution and produce jagged edges when scaled by the
|
|
294
323
|
// viewport transform. At normal zoom (0.5–1x), bitmap cache is safe and fast.
|
|
295
|
-
const useParaImageCache = this.zoom >= 0.5 && this.zoom <= 1
|
|
324
|
+
const useParaImageCache = this.zoom >= 0.5 && this.zoom <= 1;
|
|
296
325
|
// Always rasterize at 2x minimum — 1x bitmaps produce jagged text on low-DPR displays
|
|
297
|
-
const imgScale = Math.max(this._dpr, 2)
|
|
298
|
-
let cachedImg: any = useParaImageCache ? this.paraImageCache.get(cacheKey) : null
|
|
326
|
+
const imgScale = Math.max(this._dpr, 2);
|
|
327
|
+
let cachedImg: any = useParaImageCache ? this.paraImageCache.get(cacheKey) : null;
|
|
299
328
|
if (useParaImageCache && cachedImg === undefined) {
|
|
300
|
-
cachedImg = null
|
|
301
|
-
const sw = Math.min(Math.ceil(surfaceW * imgScale), 4096)
|
|
302
|
-
const sh = Math.min(Math.ceil(surfaceH * imgScale), 4096)
|
|
329
|
+
cachedImg = null;
|
|
330
|
+
const sw = Math.min(Math.ceil(surfaceW * imgScale), 4096);
|
|
331
|
+
const sh = Math.min(Math.ceil(surfaceH * imgScale), 4096);
|
|
303
332
|
if (sw > 0 && sh > 0) {
|
|
304
|
-
const surf: any = (ck as any).MakeSurface?.(sw, sh)
|
|
333
|
+
const surf: any = (ck as any).MakeSurface?.(sw, sh);
|
|
305
334
|
if (surf) {
|
|
306
|
-
const offCanvas = surf.getCanvas()
|
|
307
|
-
offCanvas.scale(imgScale, imgScale)
|
|
308
|
-
offCanvas.drawParagraph(para, 0, 0)
|
|
309
|
-
cachedImg = (surf.makeImageSnapshot() as SkImage | null) ?? null
|
|
310
|
-
surf.delete()
|
|
335
|
+
const offCanvas = surf.getCanvas();
|
|
336
|
+
offCanvas.scale(imgScale, imgScale);
|
|
337
|
+
offCanvas.drawParagraph(para, 0, 0);
|
|
338
|
+
cachedImg = (surf.makeImageSnapshot() as SkImage | null) ?? null;
|
|
339
|
+
surf.delete();
|
|
311
340
|
if (cachedImg) {
|
|
312
|
-
const imgBytes = sw * sh * 4
|
|
313
|
-
this.evictParaImageCache(imgBytes)
|
|
314
|
-
this.paraImageCacheBytes += imgBytes
|
|
341
|
+
const imgBytes = sw * sh * 4;
|
|
342
|
+
this.evictParaImageCache(imgBytes);
|
|
343
|
+
this.paraImageCacheBytes += imgBytes;
|
|
315
344
|
}
|
|
316
345
|
}
|
|
317
346
|
}
|
|
318
|
-
if (useParaImageCache) this.paraImageCache.set(cacheKey, cachedImg)
|
|
347
|
+
if (useParaImageCache) this.paraImageCache.set(cacheKey, cachedImg);
|
|
319
348
|
}
|
|
320
349
|
|
|
321
350
|
if (cachedImg) {
|
|
322
|
-
const imgW = cachedImg.width() / imgScale
|
|
323
|
-
const imgH = cachedImg.height() / imgScale
|
|
324
|
-
const paint = new ck.Paint()
|
|
325
|
-
paint.setAntiAlias(true)
|
|
326
|
-
if (opacity < 1) paint.setAlphaf(opacity)
|
|
351
|
+
const imgW = cachedImg.width() / imgScale;
|
|
352
|
+
const imgH = cachedImg.height() / imgScale;
|
|
353
|
+
const paint = new ck.Paint();
|
|
354
|
+
paint.setAntiAlias(true);
|
|
355
|
+
if (opacity < 1) paint.setAlphaf(opacity);
|
|
327
356
|
canvas.drawImageRect(
|
|
328
357
|
cachedImg,
|
|
329
358
|
ck.LTRBRect(0, 0, cachedImg.width(), cachedImg.height()),
|
|
330
359
|
ck.LTRBRect(drawX, y, drawX + imgW, y + imgH),
|
|
331
360
|
paint,
|
|
332
|
-
)
|
|
333
|
-
paint.delete()
|
|
334
|
-
return true
|
|
361
|
+
);
|
|
362
|
+
paint.delete();
|
|
363
|
+
return true;
|
|
335
364
|
}
|
|
336
365
|
|
|
337
366
|
// Fallback: surface creation failed, draw directly
|
|
338
367
|
if (opacity < 1) {
|
|
339
|
-
const paint = new ck.Paint()
|
|
340
|
-
paint.setAlphaf(opacity)
|
|
341
|
-
canvas.saveLayer(paint)
|
|
342
|
-
paint.delete()
|
|
343
|
-
canvas.drawParagraph(para, drawX, y)
|
|
344
|
-
canvas.restore()
|
|
368
|
+
const paint = new ck.Paint();
|
|
369
|
+
paint.setAlphaf(opacity);
|
|
370
|
+
canvas.saveLayer(paint);
|
|
371
|
+
paint.delete();
|
|
372
|
+
canvas.drawParagraph(para, drawX, y);
|
|
373
|
+
canvas.restore();
|
|
345
374
|
} else {
|
|
346
|
-
canvas.drawParagraph(para, drawX, y)
|
|
375
|
+
canvas.drawParagraph(para, drawX, y);
|
|
347
376
|
}
|
|
348
377
|
|
|
349
|
-
return true
|
|
378
|
+
return true;
|
|
350
379
|
}
|
|
351
380
|
|
|
352
381
|
/**
|
|
353
382
|
* Draw text shadow as a blurred copy of the actual text glyphs.
|
|
354
383
|
*/
|
|
355
384
|
private drawTextShadow(
|
|
356
|
-
canvas: Canvas,
|
|
357
|
-
|
|
385
|
+
canvas: Canvas,
|
|
386
|
+
node: PenNode,
|
|
387
|
+
x: number,
|
|
388
|
+
y: number,
|
|
389
|
+
w: number,
|
|
390
|
+
h: number,
|
|
358
391
|
opacity: number,
|
|
359
392
|
shadow: ShadowEffect,
|
|
360
393
|
) {
|
|
361
|
-
const ck = this.ck
|
|
362
|
-
const tNode = node as TextNode
|
|
363
|
-
const shadowFillColor = shadow.color ?? '#00000066'
|
|
394
|
+
const ck = this.ck;
|
|
395
|
+
const tNode = node as TextNode;
|
|
396
|
+
const shadowFillColor = shadow.color ?? '#00000066';
|
|
364
397
|
const shadowNode = {
|
|
365
398
|
...tNode,
|
|
366
399
|
fill: [{ type: 'solid' as const, color: shadowFillColor }],
|
|
367
|
-
} as PenNode
|
|
400
|
+
} as PenNode;
|
|
368
401
|
|
|
369
|
-
const sx = x + shadow.offsetX
|
|
370
|
-
const sy = y + shadow.offsetY
|
|
402
|
+
const sx = x + shadow.offsetX;
|
|
403
|
+
const sy = y + shadow.offsetY;
|
|
371
404
|
|
|
372
405
|
if (shadow.blur > 0) {
|
|
373
|
-
const paint = new ck.Paint()
|
|
374
|
-
if (opacity < 1) paint.setAlphaf(opacity)
|
|
375
|
-
const sigma = shadow.blur / 2
|
|
376
|
-
const filter = ck.ImageFilter.MakeBlur(sigma, sigma, ck.TileMode.Decal, null)
|
|
377
|
-
paint.setImageFilter(filter)
|
|
378
|
-
canvas.saveLayer(paint)
|
|
379
|
-
paint.delete()
|
|
380
|
-
|
|
381
|
-
const vectorOk = this.drawTextVector(canvas, shadowNode, sx, sy, w, h, 1)
|
|
406
|
+
const paint = new ck.Paint();
|
|
407
|
+
if (opacity < 1) paint.setAlphaf(opacity);
|
|
408
|
+
const sigma = shadow.blur / 2;
|
|
409
|
+
const filter = ck.ImageFilter.MakeBlur(sigma, sigma, ck.TileMode.Decal, null);
|
|
410
|
+
paint.setImageFilter(filter);
|
|
411
|
+
canvas.saveLayer(paint);
|
|
412
|
+
paint.delete();
|
|
413
|
+
|
|
414
|
+
const vectorOk = this.drawTextVector(canvas, shadowNode, sx, sy, w, h, 1);
|
|
382
415
|
if (!vectorOk) {
|
|
383
|
-
this.drawTextBitmap(canvas, shadowNode, sx, sy, w, h, 1)
|
|
416
|
+
this.drawTextBitmap(canvas, shadowNode, sx, sy, w, h, 1);
|
|
384
417
|
}
|
|
385
418
|
|
|
386
|
-
canvas.restore()
|
|
419
|
+
canvas.restore();
|
|
387
420
|
} else {
|
|
388
|
-
const vectorOk = this.drawTextVector(canvas, shadowNode, sx, sy, w, h, opacity)
|
|
421
|
+
const vectorOk = this.drawTextVector(canvas, shadowNode, sx, sy, w, h, opacity);
|
|
389
422
|
if (!vectorOk) {
|
|
390
|
-
this.drawTextBitmap(canvas, shadowNode, sx, sy, w, h, opacity)
|
|
423
|
+
this.drawTextBitmap(canvas, shadowNode, sx, sy, w, h, opacity);
|
|
391
424
|
}
|
|
392
425
|
}
|
|
393
426
|
}
|
|
394
427
|
|
|
395
428
|
/** Bitmap text rendering fallback — supports all system fonts via Canvas 2D API. */
|
|
396
429
|
drawTextBitmap(
|
|
397
|
-
canvas: Canvas,
|
|
398
|
-
|
|
430
|
+
canvas: Canvas,
|
|
431
|
+
node: PenNode,
|
|
432
|
+
x: number,
|
|
433
|
+
y: number,
|
|
434
|
+
w: number,
|
|
435
|
+
h: number,
|
|
399
436
|
opacity: number,
|
|
400
437
|
) {
|
|
401
|
-
const ck = this.ck
|
|
402
|
-
const tNode = node as TextNode
|
|
403
|
-
const content =
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const
|
|
413
|
-
const
|
|
414
|
-
const
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
438
|
+
const ck = this.ck;
|
|
439
|
+
const tNode = node as TextNode;
|
|
440
|
+
const content =
|
|
441
|
+
typeof tNode.content === 'string'
|
|
442
|
+
? tNode.content
|
|
443
|
+
: Array.isArray(tNode.content)
|
|
444
|
+
? tNode.content.map((s) => s.text ?? '').join('')
|
|
445
|
+
: (((tNode as unknown as Record<string, unknown>).text as string) ?? '');
|
|
446
|
+
|
|
447
|
+
if (!content) return;
|
|
448
|
+
|
|
449
|
+
const fontSize = tNode.fontSize ?? 16;
|
|
450
|
+
const fillColor = resolveFillColor(tNode.fill);
|
|
451
|
+
const fontWeight = tNode.fontWeight ?? '400';
|
|
452
|
+
const fontFamily =
|
|
453
|
+
tNode.fontFamily ??
|
|
454
|
+
'Inter, -apple-system, "Noto Sans SC", "PingFang SC", system-ui, sans-serif';
|
|
455
|
+
const textAlign: string = tNode.textAlign ?? 'left';
|
|
456
|
+
const lineHeightMul = tNode.lineHeight ?? defaultLineHeight(fontSize);
|
|
457
|
+
const lineHeight = lineHeightMul * fontSize;
|
|
458
|
+
const textGrowth = tNode.textGrowth;
|
|
459
|
+
|
|
460
|
+
const isFixedWidth =
|
|
461
|
+
textGrowth === 'fixed-width' ||
|
|
462
|
+
textGrowth === 'fixed-width-height' ||
|
|
463
|
+
(textGrowth !== 'auto' && textAlign !== 'left' && textAlign !== undefined);
|
|
464
|
+
const shouldWrap = isFixedWidth && w > 0;
|
|
465
|
+
|
|
466
|
+
const measureCanvas = document.createElement('canvas');
|
|
467
|
+
const mCtx = measureCanvas.getContext('2d')!;
|
|
468
|
+
mCtx.font = `${fontWeight} ${fontSize}px ${cssFontFamily(fontFamily)}`;
|
|
469
|
+
|
|
470
|
+
const rawLines = content.split('\n');
|
|
471
|
+
let wrappedLines: string[];
|
|
472
|
+
let renderW: number;
|
|
431
473
|
|
|
432
474
|
if (shouldWrap) {
|
|
433
|
-
renderW = Math.max(w + fontSize * 0.2, 10)
|
|
434
|
-
wrappedLines = []
|
|
475
|
+
renderW = Math.max(w + fontSize * 0.2, 10);
|
|
476
|
+
wrappedLines = [];
|
|
435
477
|
for (const raw of rawLines) {
|
|
436
|
-
if (!raw) {
|
|
437
|
-
|
|
478
|
+
if (!raw) {
|
|
479
|
+
wrappedLines.push('');
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
wrapLine(mCtx, raw, renderW, wrappedLines);
|
|
438
483
|
}
|
|
439
484
|
} else {
|
|
440
|
-
wrappedLines = rawLines.length > 0 ? rawLines : ['']
|
|
441
|
-
let maxLineWidth = 0
|
|
485
|
+
wrappedLines = rawLines.length > 0 ? rawLines : [''];
|
|
486
|
+
let maxLineWidth = 0;
|
|
442
487
|
for (const line of wrappedLines) {
|
|
443
|
-
if (line) maxLineWidth = Math.max(maxLineWidth, mCtx.measureText(line).width)
|
|
488
|
+
if (line) maxLineWidth = Math.max(maxLineWidth, mCtx.measureText(line).width);
|
|
444
489
|
}
|
|
445
|
-
renderW = Math.max(maxLineWidth + 2, w, 10)
|
|
490
|
+
renderW = Math.max(maxLineWidth + 2, w, 10);
|
|
446
491
|
}
|
|
447
492
|
|
|
448
|
-
const FABRIC_FONT_MULT = 1.13
|
|
449
|
-
const glyphH = fontSize * FABRIC_FONT_MULT
|
|
450
|
-
const textH = Math.max(
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
)
|
|
493
|
+
const FABRIC_FONT_MULT = 1.13;
|
|
494
|
+
const glyphH = fontSize * FABRIC_FONT_MULT;
|
|
495
|
+
const textH = Math.max(
|
|
496
|
+
h,
|
|
497
|
+
wrappedLines.length <= 1 ? glyphH + 2 : (wrappedLines.length - 1) * lineHeight + glyphH + 2,
|
|
498
|
+
);
|
|
455
499
|
|
|
456
|
-
const rawScale = this.zoom * this._dpr
|
|
457
|
-
const scale = rawScale <= 2 ? 2 : rawScale <= 4 ? 4 : 8
|
|
500
|
+
const rawScale = this.zoom * this._dpr;
|
|
501
|
+
const scale = rawScale <= 2 ? 2 : rawScale <= 4 ? 4 : 8;
|
|
458
502
|
|
|
459
|
-
const cacheKey = `${content}|${fontSize}|${fillColor}|${fontWeight}|${fontFamily}|${textAlign}|${Math.round(renderW)}|${Math.round(textH)}|${scale}
|
|
503
|
+
const cacheKey = `${content}|${fontSize}|${fillColor}|${fontWeight}|${fontFamily}|${textAlign}|${Math.round(renderW)}|${Math.round(textH)}|${scale}`;
|
|
460
504
|
|
|
461
|
-
let img = this.textCache.get(cacheKey)
|
|
505
|
+
let img = this.textCache.get(cacheKey);
|
|
462
506
|
if (img === undefined) {
|
|
463
|
-
let effectiveScale = scale
|
|
464
|
-
let cw = Math.ceil(renderW * effectiveScale)
|
|
465
|
-
let ch = Math.ceil(textH * effectiveScale)
|
|
466
|
-
if (cw <= 0 || ch <= 0) {
|
|
467
|
-
|
|
507
|
+
let effectiveScale = scale;
|
|
508
|
+
let cw = Math.ceil(renderW * effectiveScale);
|
|
509
|
+
let ch = Math.ceil(textH * effectiveScale);
|
|
510
|
+
if (cw <= 0 || ch <= 0) {
|
|
511
|
+
this.textCache.set(cacheKey, null);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const MAX_TEX = 4096;
|
|
468
515
|
if (cw > MAX_TEX || ch > MAX_TEX) {
|
|
469
|
-
effectiveScale = Math.min(MAX_TEX / renderW, MAX_TEX / textH, effectiveScale)
|
|
470
|
-
cw = Math.ceil(renderW * effectiveScale)
|
|
471
|
-
ch = Math.ceil(textH * effectiveScale)
|
|
516
|
+
effectiveScale = Math.min(MAX_TEX / renderW, MAX_TEX / textH, effectiveScale);
|
|
517
|
+
cw = Math.ceil(renderW * effectiveScale);
|
|
518
|
+
ch = Math.ceil(textH * effectiveScale);
|
|
472
519
|
}
|
|
473
520
|
|
|
474
|
-
const tmp = document.createElement('canvas')
|
|
475
|
-
tmp.width = cw
|
|
476
|
-
tmp.height = ch
|
|
477
|
-
const ctx = tmp.getContext('2d')
|
|
478
|
-
ctx.scale(effectiveScale, effectiveScale)
|
|
479
|
-
ctx.font = `${fontWeight} ${fontSize}px ${cssFontFamily(fontFamily)}
|
|
480
|
-
ctx.fillStyle = fillColor
|
|
481
|
-
ctx.textBaseline = 'top'
|
|
482
|
-
ctx.textAlign = (textAlign || 'left') as CanvasTextAlign
|
|
483
|
-
|
|
484
|
-
let cy = 0
|
|
521
|
+
const tmp = document.createElement('canvas');
|
|
522
|
+
tmp.width = cw;
|
|
523
|
+
tmp.height = ch;
|
|
524
|
+
const ctx = tmp.getContext('2d')!;
|
|
525
|
+
ctx.scale(effectiveScale, effectiveScale);
|
|
526
|
+
ctx.font = `${fontWeight} ${fontSize}px ${cssFontFamily(fontFamily)}`;
|
|
527
|
+
ctx.fillStyle = fillColor;
|
|
528
|
+
ctx.textBaseline = 'top';
|
|
529
|
+
ctx.textAlign = (textAlign || 'left') as CanvasTextAlign;
|
|
530
|
+
|
|
531
|
+
let cy = 0;
|
|
485
532
|
for (const line of wrappedLines) {
|
|
486
|
-
if (!line) {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
533
|
+
if (!line) {
|
|
534
|
+
cy += lineHeight;
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
let tx = 0;
|
|
538
|
+
if (textAlign === 'center') tx = renderW / 2;
|
|
539
|
+
else if (textAlign === 'right') tx = renderW;
|
|
540
|
+
ctx.fillText(line, tx, cy);
|
|
541
|
+
cy += lineHeight;
|
|
492
542
|
}
|
|
493
543
|
|
|
494
|
-
const imageData = ctx.getImageData(0, 0, cw, ch)
|
|
544
|
+
const imageData = ctx.getImageData(0, 0, cw, ch);
|
|
495
545
|
// Premultiply alpha for correct CanvasKit texture blending
|
|
496
|
-
const premul = new Uint8Array(imageData.data.length)
|
|
546
|
+
const premul = new Uint8Array(imageData.data.length);
|
|
497
547
|
for (let p = 0; p < premul.length; p += 4) {
|
|
498
|
-
const a = imageData.data[p + 3]
|
|
548
|
+
const a = imageData.data[p + 3];
|
|
499
549
|
if (a === 255) {
|
|
500
|
-
premul[p] = imageData.data[p]
|
|
501
|
-
premul[p + 1] = imageData.data[p + 1]
|
|
502
|
-
premul[p + 2] = imageData.data[p + 2]
|
|
503
|
-
premul[p + 3] = 255
|
|
550
|
+
premul[p] = imageData.data[p];
|
|
551
|
+
premul[p + 1] = imageData.data[p + 1];
|
|
552
|
+
premul[p + 2] = imageData.data[p + 2];
|
|
553
|
+
premul[p + 3] = 255;
|
|
504
554
|
} else if (a > 0) {
|
|
505
|
-
const f = a / 255
|
|
506
|
-
premul[p] = Math.round(imageData.data[p] * f)
|
|
507
|
-
premul[p + 1] = Math.round(imageData.data[p + 1] * f)
|
|
508
|
-
premul[p + 2] = Math.round(imageData.data[p + 2] * f)
|
|
509
|
-
premul[p + 3] = a
|
|
555
|
+
const f = a / 255;
|
|
556
|
+
premul[p] = Math.round(imageData.data[p] * f);
|
|
557
|
+
premul[p + 1] = Math.round(imageData.data[p + 1] * f);
|
|
558
|
+
premul[p + 2] = Math.round(imageData.data[p + 2] * f);
|
|
559
|
+
premul[p + 3] = a;
|
|
510
560
|
}
|
|
511
561
|
}
|
|
512
|
-
img =
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
562
|
+
img =
|
|
563
|
+
ck.MakeImage(
|
|
564
|
+
{
|
|
565
|
+
width: cw,
|
|
566
|
+
height: ch,
|
|
567
|
+
alphaType: ck.AlphaType.Premul,
|
|
568
|
+
colorType: ck.ColorType.RGBA_8888,
|
|
569
|
+
colorSpace: ck.ColorSpace.SRGB,
|
|
570
|
+
},
|
|
571
|
+
premul,
|
|
572
|
+
cw * 4,
|
|
573
|
+
) ?? null;
|
|
574
|
+
|
|
575
|
+
const imgBytes = img ? cw * ch * 4 : 0;
|
|
576
|
+
this.evictTextCache(imgBytes);
|
|
577
|
+
this.textCache.set(cacheKey, img);
|
|
578
|
+
this.textCacheBytes += imgBytes;
|
|
521
579
|
}
|
|
522
580
|
|
|
523
|
-
if (!img) return
|
|
581
|
+
if (!img) return;
|
|
524
582
|
|
|
525
|
-
const paint = new ck.Paint()
|
|
526
|
-
paint.setAntiAlias(true)
|
|
527
|
-
if (opacity < 1) paint.setAlphaf(opacity)
|
|
583
|
+
const paint = new ck.Paint();
|
|
584
|
+
paint.setAntiAlias(true);
|
|
585
|
+
if (opacity < 1) paint.setAlphaf(opacity);
|
|
528
586
|
canvas.drawImageRect(
|
|
529
587
|
img,
|
|
530
588
|
ck.LTRBRect(0, 0, img.width(), img.height()),
|
|
531
589
|
ck.LTRBRect(x, y, x + renderW, y + textH),
|
|
532
590
|
paint,
|
|
533
|
-
)
|
|
534
|
-
paint.delete()
|
|
591
|
+
);
|
|
592
|
+
paint.delete();
|
|
535
593
|
}
|
|
536
594
|
}
|