@zseven-w/pen-renderer 0.6.0 → 0.7.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/LICENSE +21 -0
- package/README.md +153 -27
- package/package.json +23 -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/node-renderer.ts
CHANGED
|
@@ -1,74 +1,107 @@
|
|
|
1
|
-
import type { CanvasKit, Canvas, Paint, Font, Typeface } from 'canvaskit-wasm'
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import type { CanvasKit, Canvas, Paint, Font, Typeface } from 'canvaskit-wasm';
|
|
2
|
+
import type {
|
|
3
|
+
PenNode,
|
|
4
|
+
ContainerProps,
|
|
5
|
+
EllipseNode,
|
|
6
|
+
LineNode,
|
|
7
|
+
PolygonNode,
|
|
8
|
+
PathNode,
|
|
9
|
+
ImageNode,
|
|
10
|
+
IconFontNode,
|
|
11
|
+
} from '@zseven-w/pen-types';
|
|
12
|
+
import type { PenFill, PenStroke, PenEffect, ShadowEffect, ImageFill } from '@zseven-w/pen-types';
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_FILL,
|
|
15
|
+
DEFAULT_STROKE,
|
|
16
|
+
DEFAULT_STROKE_WIDTH,
|
|
17
|
+
buildEllipseArcPath,
|
|
18
|
+
getPathBoundsFromAnchors,
|
|
19
|
+
isArcEllipse,
|
|
20
|
+
pathDataToAnchors,
|
|
21
|
+
} from '@zseven-w/pen-core';
|
|
22
|
+
import {
|
|
23
|
+
parseColor,
|
|
24
|
+
cornerRadiusValue,
|
|
25
|
+
cornerRadii,
|
|
26
|
+
resolveFillColor,
|
|
27
|
+
shouldUseTransparentFallbackFill,
|
|
28
|
+
resolveStrokeColor,
|
|
29
|
+
resolveStrokeWidth,
|
|
30
|
+
} from './paint-utils.js';
|
|
31
|
+
import { sanitizeSvgPath, hasInvalidNumbers, tryManualPathParse } from './path-utils.js';
|
|
32
|
+
import { SkiaImageLoader } from './image-loader.js';
|
|
33
|
+
import { SkiaTextRenderer } from './text-renderer.js';
|
|
34
|
+
import type { SkiaFontManager, FontManagerOptions } from './font-manager.js';
|
|
35
|
+
import type { RenderNode, IconLookupFn } from './types.js';
|
|
36
|
+
|
|
37
|
+
const FALLBACK_ICON_D = 'M12 12m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0';
|
|
13
38
|
|
|
14
39
|
/**
|
|
15
40
|
* Core node renderer for CanvasKit/Skia. Draws PenNode shapes, fills,
|
|
16
41
|
* strokes, effects, text, and images. No editor overlays or store dependencies.
|
|
17
42
|
*/
|
|
18
43
|
export class SkiaNodeRenderer {
|
|
19
|
-
protected ck: CanvasKit
|
|
20
|
-
private defaultTypeface: Typeface | null = null
|
|
21
|
-
private defaultFont: Font | null = null
|
|
44
|
+
protected ck: CanvasKit;
|
|
45
|
+
private defaultTypeface: Typeface | null = null;
|
|
46
|
+
private defaultFont: Font | null = null;
|
|
22
47
|
|
|
23
48
|
// Current viewport zoom (set by engine before each render frame)
|
|
24
|
-
zoom = 1
|
|
49
|
+
zoom = 1;
|
|
25
50
|
|
|
26
51
|
// Device pixel ratio
|
|
27
|
-
devicePixelRatio: number | undefined
|
|
52
|
+
devicePixelRatio: number | undefined;
|
|
28
53
|
|
|
29
54
|
// Sub-renderers
|
|
30
|
-
private textRenderer: SkiaTextRenderer
|
|
31
|
-
imageLoader: SkiaImageLoader
|
|
55
|
+
private textRenderer: SkiaTextRenderer;
|
|
56
|
+
imageLoader: SkiaImageLoader;
|
|
32
57
|
|
|
33
58
|
// Injectable icon lookup
|
|
34
|
-
private iconLookup: IconLookupFn | null = null
|
|
59
|
+
private iconLookup: IconLookupFn | null = null;
|
|
35
60
|
|
|
36
61
|
/** Font manager — delegates to text renderer */
|
|
37
62
|
get fontManager(): SkiaFontManager {
|
|
38
|
-
return this.textRenderer.fontManager
|
|
63
|
+
return this.textRenderer.fontManager;
|
|
39
64
|
}
|
|
40
65
|
|
|
41
66
|
constructor(ck: CanvasKit, fontOptions?: FontManagerOptions) {
|
|
42
|
-
this.ck = ck
|
|
43
|
-
this.imageLoader = new SkiaImageLoader(ck)
|
|
44
|
-
this.textRenderer = new SkiaTextRenderer(ck, fontOptions)
|
|
67
|
+
this.ck = ck;
|
|
68
|
+
this.imageLoader = new SkiaImageLoader(ck);
|
|
69
|
+
this.textRenderer = new SkiaTextRenderer(ck, fontOptions);
|
|
45
70
|
}
|
|
46
71
|
|
|
47
72
|
init() {
|
|
48
|
-
this.defaultFont = new this.ck.Font(null, 16)
|
|
73
|
+
this.defaultFont = new this.ck.Font(null, 16);
|
|
49
74
|
}
|
|
50
75
|
|
|
51
76
|
/** Set callback to trigger re-render when async images finish loading. */
|
|
52
77
|
setRedrawCallback(cb: () => void) {
|
|
53
|
-
this.imageLoader.setOnLoaded(cb)
|
|
78
|
+
this.imageLoader.setOnLoaded(cb);
|
|
54
79
|
}
|
|
55
80
|
|
|
56
81
|
/** Set injectable icon lookup function. */
|
|
57
82
|
setIconLookup(fn: IconLookupFn) {
|
|
58
|
-
this.iconLookup = fn
|
|
83
|
+
this.iconLookup = fn;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
setImageSourceResolver(resolver: (src: string) => { cacheKey: string; loadUrl: string | null }) {
|
|
87
|
+
this.imageLoader.setSourceResolver(resolver);
|
|
59
88
|
}
|
|
60
89
|
|
|
61
90
|
dispose() {
|
|
62
|
-
this.defaultFont?.delete()
|
|
63
|
-
this.defaultFont = null
|
|
64
|
-
this.defaultTypeface?.delete()
|
|
65
|
-
this.defaultTypeface = null
|
|
66
|
-
this.textRenderer.dispose()
|
|
67
|
-
this.imageLoader.dispose()
|
|
91
|
+
this.defaultFont?.delete();
|
|
92
|
+
this.defaultFont = null;
|
|
93
|
+
this.defaultTypeface?.delete();
|
|
94
|
+
this.defaultTypeface = null;
|
|
95
|
+
this.textRenderer.dispose();
|
|
96
|
+
this.imageLoader.dispose();
|
|
68
97
|
}
|
|
69
98
|
|
|
70
|
-
clearTextCache() {
|
|
71
|
-
|
|
99
|
+
clearTextCache() {
|
|
100
|
+
this.textRenderer.clearTextCache();
|
|
101
|
+
}
|
|
102
|
+
clearParaCache() {
|
|
103
|
+
this.textRenderer.clearParaCache();
|
|
104
|
+
}
|
|
72
105
|
|
|
73
106
|
// ---------------------------------------------------------------------------
|
|
74
107
|
// Fill paint
|
|
@@ -76,146 +109,304 @@ export class SkiaNodeRenderer {
|
|
|
76
109
|
|
|
77
110
|
private makeFillPaint(
|
|
78
111
|
fills: PenFill[] | string | undefined,
|
|
79
|
-
w: number,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
112
|
+
w: number,
|
|
113
|
+
h: number,
|
|
114
|
+
opacity: number,
|
|
115
|
+
absX: number,
|
|
116
|
+
absY: number,
|
|
117
|
+
): {
|
|
118
|
+
paint: Paint;
|
|
119
|
+
imageFillDraw?: {
|
|
120
|
+
fill: ImageFill;
|
|
121
|
+
w: number;
|
|
122
|
+
h: number;
|
|
123
|
+
absX: number;
|
|
124
|
+
absY: number;
|
|
125
|
+
opacity: number;
|
|
126
|
+
};
|
|
127
|
+
} {
|
|
128
|
+
const ck = this.ck;
|
|
129
|
+
const paint = new ck.Paint();
|
|
130
|
+
paint.setStyle(ck.PaintStyle.Fill);
|
|
131
|
+
paint.setAntiAlias(true);
|
|
85
132
|
|
|
86
133
|
if (typeof fills === 'string') {
|
|
87
|
-
const c = parseColor(ck, fills);
|
|
88
|
-
|
|
134
|
+
const c = parseColor(ck, fills);
|
|
135
|
+
c[3] *= opacity;
|
|
136
|
+
paint.setColor(c);
|
|
137
|
+
return { paint };
|
|
89
138
|
}
|
|
90
139
|
if (!fills || fills.length === 0) {
|
|
91
|
-
const c = parseColor(ck, DEFAULT_FILL);
|
|
92
|
-
|
|
140
|
+
const c = parseColor(ck, DEFAULT_FILL);
|
|
141
|
+
c[3] *= opacity;
|
|
142
|
+
paint.setColor(c);
|
|
143
|
+
return { paint };
|
|
93
144
|
}
|
|
94
145
|
|
|
95
|
-
const first = fills[0]
|
|
146
|
+
const first = fills[0];
|
|
96
147
|
if (first.type === 'solid') {
|
|
97
|
-
const c = parseColor(ck, first.color);
|
|
148
|
+
const c = parseColor(ck, first.color);
|
|
149
|
+
c[3] *= (first.opacity ?? 1) * opacity;
|
|
150
|
+
paint.setColor(c);
|
|
98
151
|
} else if (first.type === 'linear_gradient') {
|
|
99
|
-
const stops = first.stops ?? []
|
|
100
|
-
const fillOpacity = (first.opacity ?? 1) * opacity
|
|
152
|
+
const stops = first.stops ?? [];
|
|
153
|
+
const fillOpacity = (first.opacity ?? 1) * opacity;
|
|
101
154
|
if (stops.length >= 2) {
|
|
102
|
-
const rad = ((first.angle ?? 0) - 90) * Math.PI / 180
|
|
103
|
-
const cos = Math.cos(rad),
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
155
|
+
const rad = (((first.angle ?? 0) - 90) * Math.PI) / 180;
|
|
156
|
+
const cos = Math.cos(rad),
|
|
157
|
+
sin = Math.sin(rad);
|
|
158
|
+
const x1 = absX + w / 2 - (cos * w) / 2,
|
|
159
|
+
y1 = absY + h / 2 - (sin * h) / 2;
|
|
160
|
+
const x2 = absX + w / 2 + (cos * w) / 2,
|
|
161
|
+
y2 = absY + h / 2 + (sin * h) / 2;
|
|
162
|
+
const colors = stops.map((s) => {
|
|
163
|
+
const c = parseColor(ck, s.color);
|
|
164
|
+
c[3] *= fillOpacity;
|
|
165
|
+
return c;
|
|
166
|
+
});
|
|
167
|
+
const positions = stops.map((s) => Math.max(0, Math.min(1, s.offset)));
|
|
168
|
+
const shader = ck.Shader.MakeLinearGradient(
|
|
169
|
+
[x1, y1],
|
|
170
|
+
[x2, y2],
|
|
171
|
+
colors,
|
|
172
|
+
positions,
|
|
173
|
+
ck.TileMode.Clamp,
|
|
174
|
+
);
|
|
175
|
+
if (shader) paint.setShader(shader);
|
|
110
176
|
} else {
|
|
111
|
-
const c = parseColor(ck, stops[0]?.color ?? DEFAULT_FILL);
|
|
177
|
+
const c = parseColor(ck, stops[0]?.color ?? DEFAULT_FILL);
|
|
178
|
+
c[3] *= fillOpacity;
|
|
179
|
+
paint.setColor(c);
|
|
112
180
|
}
|
|
113
181
|
} else if (first.type === 'radial_gradient') {
|
|
114
|
-
const stops = first.stops ?? []
|
|
115
|
-
const fillOpacity = (first.opacity ?? 1) * opacity
|
|
182
|
+
const stops = first.stops ?? [];
|
|
183
|
+
const fillOpacity = (first.opacity ?? 1) * opacity;
|
|
116
184
|
if (stops.length >= 2) {
|
|
117
|
-
const cx = absX + (first.cx ?? 0.5) * w,
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
185
|
+
const cx = absX + (first.cx ?? 0.5) * w,
|
|
186
|
+
cy = absY + (first.cy ?? 0.5) * h;
|
|
187
|
+
const r = (first.radius ?? 0.5) * Math.max(w, h);
|
|
188
|
+
const colors = stops.map((s) => {
|
|
189
|
+
const c = parseColor(ck, s.color);
|
|
190
|
+
c[3] *= fillOpacity;
|
|
191
|
+
return c;
|
|
192
|
+
});
|
|
193
|
+
const positions = stops.map((s) => Math.max(0, Math.min(1, s.offset)));
|
|
194
|
+
const shader = ck.Shader.MakeRadialGradient(
|
|
195
|
+
[cx, cy],
|
|
196
|
+
r,
|
|
197
|
+
colors,
|
|
198
|
+
positions,
|
|
199
|
+
ck.TileMode.Clamp,
|
|
200
|
+
);
|
|
201
|
+
if (shader) paint.setShader(shader);
|
|
123
202
|
} else {
|
|
124
|
-
const c = parseColor(ck, stops[0]?.color ?? DEFAULT_FILL);
|
|
203
|
+
const c = parseColor(ck, stops[0]?.color ?? DEFAULT_FILL);
|
|
204
|
+
c[3] *= fillOpacity;
|
|
205
|
+
paint.setColor(c);
|
|
125
206
|
}
|
|
126
207
|
} else if (first.type === 'image') {
|
|
127
|
-
const result = this.applyImageFillToPaint(paint, first, w, h, opacity, absX, absY)
|
|
208
|
+
const result = this.applyImageFillToPaint(paint, first, w, h, opacity, absX, absY);
|
|
128
209
|
if (result.needsDrawImageRect && result.fill) {
|
|
129
|
-
return {
|
|
210
|
+
return {
|
|
211
|
+
paint,
|
|
212
|
+
imageFillDraw: {
|
|
213
|
+
fill: result.fill,
|
|
214
|
+
w: result.w!,
|
|
215
|
+
h: result.h!,
|
|
216
|
+
absX: result.absX!,
|
|
217
|
+
absY: result.absY!,
|
|
218
|
+
opacity: result.opacity!,
|
|
219
|
+
},
|
|
220
|
+
};
|
|
130
221
|
}
|
|
131
222
|
}
|
|
132
223
|
|
|
133
|
-
return { paint }
|
|
224
|
+
return { paint };
|
|
134
225
|
}
|
|
135
226
|
|
|
136
227
|
private applyImageFillToPaint(
|
|
137
|
-
paint: Paint,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
228
|
+
paint: Paint,
|
|
229
|
+
fill: ImageFill,
|
|
230
|
+
w: number,
|
|
231
|
+
h: number,
|
|
232
|
+
opacity: number,
|
|
233
|
+
absX: number,
|
|
234
|
+
absY: number,
|
|
235
|
+
): {
|
|
236
|
+
needsDrawImageRect: boolean;
|
|
237
|
+
fill?: ImageFill;
|
|
238
|
+
w?: number;
|
|
239
|
+
h?: number;
|
|
240
|
+
absX?: number;
|
|
241
|
+
absY?: number;
|
|
242
|
+
opacity?: number;
|
|
243
|
+
} {
|
|
244
|
+
const ck = this.ck;
|
|
245
|
+
const fillOpacity = (fill.opacity ?? 1) * opacity;
|
|
246
|
+
const url = fill.url;
|
|
247
|
+
if (!url) {
|
|
248
|
+
const c = parseColor(ck, '#e5e7eb');
|
|
249
|
+
c[3] *= fillOpacity;
|
|
250
|
+
paint.setColor(c);
|
|
251
|
+
return { needsDrawImageRect: false };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const cached = this.imageLoader.get(url);
|
|
255
|
+
if (cached === undefined) this.imageLoader.request(url);
|
|
256
|
+
if (!cached) {
|
|
257
|
+
const isMissing = this.imageLoader.getStatus(url)?.state === 'missing';
|
|
258
|
+
const c = parseColor(ck, isMissing ? '#f1d7d7' : '#e5e7eb');
|
|
259
|
+
c[3] *= fillOpacity;
|
|
260
|
+
paint.setColor(c);
|
|
261
|
+
return { needsDrawImageRect: false };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const imgW = cached.width(),
|
|
265
|
+
imgH = cached.height();
|
|
266
|
+
if (imgW <= 0 || imgH <= 0) return { needsDrawImageRect: false };
|
|
267
|
+
|
|
268
|
+
const mode = fill.mode ?? 'fill';
|
|
153
269
|
if (mode === 'tile') {
|
|
154
|
-
const dispX = absX + (w - imgW) / 2,
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
270
|
+
const dispX = absX + (w - imgW) / 2,
|
|
271
|
+
dispY = absY + (h - imgH) / 2;
|
|
272
|
+
const localMatrix = Float32Array.of(1, 0, -dispX, 0, 1, -dispY, 0, 0, 1);
|
|
273
|
+
const shader = cached.makeShaderOptions(
|
|
274
|
+
ck.TileMode.Repeat,
|
|
275
|
+
ck.TileMode.Repeat,
|
|
276
|
+
ck.FilterMode.Linear,
|
|
277
|
+
ck.MipmapMode.None,
|
|
278
|
+
localMatrix,
|
|
279
|
+
);
|
|
280
|
+
if (shader) {
|
|
281
|
+
paint.setShader(shader);
|
|
282
|
+
if (fillOpacity < 1) paint.setAlphaf(fillOpacity);
|
|
283
|
+
const cf = this.buildImageAdjustmentFilter(fill);
|
|
284
|
+
if (cf) paint.setColorFilter(cf);
|
|
285
|
+
}
|
|
286
|
+
return { needsDrawImageRect: false };
|
|
159
287
|
}
|
|
160
288
|
|
|
161
|
-
paint.setColor(Float32Array.of(0, 0, 0, 0))
|
|
162
|
-
return { needsDrawImageRect: true, fill, w, h, absX, absY, opacity: fillOpacity }
|
|
289
|
+
paint.setColor(Float32Array.of(0, 0, 0, 0));
|
|
290
|
+
return { needsDrawImageRect: true, fill, w, h, absX, absY, opacity: fillOpacity };
|
|
163
291
|
}
|
|
164
292
|
|
|
165
|
-
private drawImageFillRect(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
293
|
+
private drawImageFillRect(
|
|
294
|
+
canvas: Canvas,
|
|
295
|
+
fill: ImageFill,
|
|
296
|
+
w: number,
|
|
297
|
+
h: number,
|
|
298
|
+
absX: number,
|
|
299
|
+
absY: number,
|
|
300
|
+
fillOpacity: number,
|
|
301
|
+
) {
|
|
302
|
+
const ck = this.ck;
|
|
303
|
+
const url = fill.url;
|
|
304
|
+
if (!url) return;
|
|
305
|
+
const cached = this.imageLoader.get(url);
|
|
306
|
+
if (!cached) return;
|
|
307
|
+
const imgW = cached.width(),
|
|
308
|
+
imgH = cached.height();
|
|
309
|
+
if (imgW <= 0 || imgH <= 0) return;
|
|
310
|
+
|
|
311
|
+
const mode = fill.mode ?? 'fill';
|
|
312
|
+
const paint = new ck.Paint();
|
|
313
|
+
paint.setAntiAlias(true);
|
|
314
|
+
if (fillOpacity < 1) paint.setAlphaf(fillOpacity);
|
|
315
|
+
const adjFilter = this.buildImageAdjustmentFilter(fill);
|
|
316
|
+
if (adjFilter) paint.setColorFilter(adjFilter);
|
|
180
317
|
|
|
181
318
|
if (mode === 'fit') {
|
|
182
|
-
const scale = Math.min(w / imgW, h / imgH)
|
|
183
|
-
const dw = imgW * scale,
|
|
184
|
-
|
|
185
|
-
|
|
319
|
+
const scale = Math.min(w / imgW, h / imgH);
|
|
320
|
+
const dw = imgW * scale,
|
|
321
|
+
dh = imgH * scale;
|
|
322
|
+
const dx = absX + (w - dw) / 2,
|
|
323
|
+
dy = absY + (h - dh) / 2;
|
|
324
|
+
canvas.drawImageRect(
|
|
325
|
+
cached,
|
|
326
|
+
ck.LTRBRect(0, 0, imgW, imgH),
|
|
327
|
+
ck.LTRBRect(dx, dy, dx + dw, dy + dh),
|
|
328
|
+
paint,
|
|
329
|
+
);
|
|
186
330
|
} else if (mode === 'stretch') {
|
|
187
|
-
canvas.drawImageRect(
|
|
331
|
+
canvas.drawImageRect(
|
|
332
|
+
cached,
|
|
333
|
+
ck.LTRBRect(0, 0, imgW, imgH),
|
|
334
|
+
ck.LTRBRect(absX, absY, absX + w, absY + h),
|
|
335
|
+
paint,
|
|
336
|
+
);
|
|
188
337
|
} else {
|
|
189
|
-
const scale = Math.max(w / imgW, h / imgH)
|
|
190
|
-
const dw = imgW * scale,
|
|
191
|
-
|
|
192
|
-
|
|
338
|
+
const scale = Math.max(w / imgW, h / imgH);
|
|
339
|
+
const dw = imgW * scale,
|
|
340
|
+
dh = imgH * scale;
|
|
341
|
+
const dx = absX + (w - dw) / 2,
|
|
342
|
+
dy = absY + (h - dh) / 2;
|
|
343
|
+
canvas.drawImageRect(
|
|
344
|
+
cached,
|
|
345
|
+
ck.LTRBRect(0, 0, imgW, imgH),
|
|
346
|
+
ck.LTRBRect(dx, dy, dx + dw, dy + dh),
|
|
347
|
+
paint,
|
|
348
|
+
);
|
|
193
349
|
}
|
|
194
|
-
paint.delete()
|
|
350
|
+
paint.delete();
|
|
195
351
|
}
|
|
196
352
|
|
|
197
|
-
private buildImageAdjustmentFilter(adj: {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
353
|
+
private buildImageAdjustmentFilter(adj: {
|
|
354
|
+
exposure?: number;
|
|
355
|
+
contrast?: number;
|
|
356
|
+
saturation?: number;
|
|
357
|
+
temperature?: number;
|
|
358
|
+
tint?: number;
|
|
359
|
+
highlights?: number;
|
|
360
|
+
shadows?: number;
|
|
361
|
+
}) {
|
|
362
|
+
const ck = this.ck;
|
|
363
|
+
const exp = (adj.exposure ?? 0) / 100,
|
|
364
|
+
con = (adj.contrast ?? 0) / 100,
|
|
365
|
+
sat = (adj.saturation ?? 0) / 100;
|
|
366
|
+
const temp = (adj.temperature ?? 0) / 100,
|
|
367
|
+
tintVal = (adj.tint ?? 0) / 100;
|
|
368
|
+
const hi = (adj.highlights ?? 0) / 100,
|
|
369
|
+
sh = (adj.shadows ?? 0) / 100;
|
|
370
|
+
if (exp === 0 && con === 0 && sat === 0 && temp === 0 && tintVal === 0 && hi === 0 && sh === 0)
|
|
371
|
+
return null;
|
|
372
|
+
|
|
373
|
+
const e = 1 + exp * 1.5,
|
|
374
|
+
c = 1 + con,
|
|
375
|
+
cOff = 0.5 * (1 - c);
|
|
376
|
+
const s = 1 + sat;
|
|
377
|
+
const lr = 0.2126,
|
|
378
|
+
lg = 0.7152,
|
|
379
|
+
lb = 0.0722;
|
|
380
|
+
const sr = (1 - s) * lr,
|
|
381
|
+
sg = (1 - s) * lg,
|
|
382
|
+
sb = (1 - s) * lb;
|
|
383
|
+
const f = c * e;
|
|
384
|
+
const offR = cOff + temp * 0.15 + (hi + sh * 0.5) * 0.1;
|
|
385
|
+
const offG = cOff + tintVal * 0.15 + (hi + sh * 0.5) * 0.1;
|
|
386
|
+
const offB = cOff - temp * 0.15 + (hi + sh * 0.5) * 0.1;
|
|
212
387
|
|
|
213
388
|
return ck.ColorFilter.MakeMatrix([
|
|
214
|
-
f * (sr + s),
|
|
215
|
-
f *
|
|
216
|
-
f *
|
|
217
|
-
0,
|
|
218
|
-
|
|
389
|
+
f * (sr + s),
|
|
390
|
+
f * sg,
|
|
391
|
+
f * sb,
|
|
392
|
+
0,
|
|
393
|
+
offR,
|
|
394
|
+
f * sr,
|
|
395
|
+
f * (sg + s),
|
|
396
|
+
f * sb,
|
|
397
|
+
0,
|
|
398
|
+
offG,
|
|
399
|
+
f * sr,
|
|
400
|
+
f * sg,
|
|
401
|
+
f * (sb + s),
|
|
402
|
+
0,
|
|
403
|
+
offB,
|
|
404
|
+
0,
|
|
405
|
+
0,
|
|
406
|
+
0,
|
|
407
|
+
1,
|
|
408
|
+
0,
|
|
409
|
+
]);
|
|
219
410
|
}
|
|
220
411
|
|
|
221
412
|
// ---------------------------------------------------------------------------
|
|
@@ -223,51 +414,65 @@ export class SkiaNodeRenderer {
|
|
|
223
414
|
// ---------------------------------------------------------------------------
|
|
224
415
|
|
|
225
416
|
private makeStrokePaint(stroke: PenStroke | undefined, opacity: number): Paint | null {
|
|
226
|
-
if (!stroke) return null
|
|
227
|
-
const strokeColor = resolveStrokeColor(stroke)
|
|
228
|
-
const strokeWidth = resolveStrokeWidth(stroke)
|
|
229
|
-
if (!strokeColor || strokeWidth <= 0) return null
|
|
230
|
-
|
|
231
|
-
const ck = this.ck
|
|
232
|
-
const paint = new ck.Paint()
|
|
233
|
-
paint.setStyle(ck.PaintStyle.Stroke)
|
|
234
|
-
paint.setAntiAlias(true)
|
|
235
|
-
paint.setStrokeWidth(strokeWidth)
|
|
236
|
-
const c = parseColor(ck, strokeColor);
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (stroke.
|
|
241
|
-
else if (stroke.
|
|
417
|
+
if (!stroke) return null;
|
|
418
|
+
const strokeColor = resolveStrokeColor(stroke);
|
|
419
|
+
const strokeWidth = resolveStrokeWidth(stroke);
|
|
420
|
+
if (!strokeColor || strokeWidth <= 0) return null;
|
|
421
|
+
|
|
422
|
+
const ck = this.ck;
|
|
423
|
+
const paint = new ck.Paint();
|
|
424
|
+
paint.setStyle(ck.PaintStyle.Stroke);
|
|
425
|
+
paint.setAntiAlias(true);
|
|
426
|
+
paint.setStrokeWidth(strokeWidth);
|
|
427
|
+
const c = parseColor(ck, strokeColor);
|
|
428
|
+
c[3] *= opacity;
|
|
429
|
+
paint.setColor(c);
|
|
430
|
+
|
|
431
|
+
if (stroke.join === 'round') paint.setStrokeJoin(ck.StrokeJoin.Round);
|
|
432
|
+
else if (stroke.join === 'bevel') paint.setStrokeJoin(ck.StrokeJoin.Bevel);
|
|
433
|
+
if (stroke.cap === 'round') paint.setStrokeCap(ck.StrokeCap.Round);
|
|
434
|
+
else if (stroke.cap === 'square') paint.setStrokeCap(ck.StrokeCap.Square);
|
|
242
435
|
if (stroke.dashPattern && stroke.dashPattern.length >= 2) {
|
|
243
|
-
const effect = ck.PathEffect.MakeDash(stroke.dashPattern, 0)
|
|
244
|
-
if (effect) paint.setPathEffect(effect)
|
|
436
|
+
const effect = ck.PathEffect.MakeDash(stroke.dashPattern, stroke.dashOffset ?? 0);
|
|
437
|
+
if (effect) paint.setPathEffect(effect);
|
|
245
438
|
}
|
|
246
439
|
|
|
247
|
-
return paint
|
|
440
|
+
return paint;
|
|
248
441
|
}
|
|
249
442
|
|
|
250
443
|
// ---------------------------------------------------------------------------
|
|
251
444
|
// Shadow
|
|
252
445
|
// ---------------------------------------------------------------------------
|
|
253
446
|
|
|
254
|
-
private applyShadowDirect(
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
paint.
|
|
270
|
-
|
|
447
|
+
private applyShadowDirect(
|
|
448
|
+
canvas: Canvas,
|
|
449
|
+
effects: PenEffect[] | undefined,
|
|
450
|
+
x: number,
|
|
451
|
+
y: number,
|
|
452
|
+
w: number,
|
|
453
|
+
h: number,
|
|
454
|
+
): boolean {
|
|
455
|
+
if (!effects) return false;
|
|
456
|
+
const shadow = effects.find((e): e is ShadowEffect => e.type === 'shadow');
|
|
457
|
+
if (!shadow) return false;
|
|
458
|
+
|
|
459
|
+
const ck = this.ck;
|
|
460
|
+
const paint = new ck.Paint();
|
|
461
|
+
paint.setStyle(ck.PaintStyle.Fill);
|
|
462
|
+
paint.setAntiAlias(true);
|
|
463
|
+
paint.setColor(parseColor(ck, shadow.color));
|
|
464
|
+
paint.setMaskFilter(ck.MaskFilter.MakeBlur(ck.BlurStyle.Normal, shadow.blur / 2, true));
|
|
465
|
+
canvas.drawRect(
|
|
466
|
+
ck.LTRBRect(
|
|
467
|
+
x + shadow.offsetX - shadow.spread,
|
|
468
|
+
y + shadow.offsetY - shadow.spread,
|
|
469
|
+
x + w + shadow.offsetX + shadow.spread,
|
|
470
|
+
y + h + shadow.offsetY + shadow.spread,
|
|
471
|
+
),
|
|
472
|
+
paint,
|
|
473
|
+
);
|
|
474
|
+
paint.delete();
|
|
475
|
+
return true;
|
|
271
476
|
}
|
|
272
477
|
|
|
273
478
|
// ---------------------------------------------------------------------------
|
|
@@ -275,325 +480,691 @@ export class SkiaNodeRenderer {
|
|
|
275
480
|
// ---------------------------------------------------------------------------
|
|
276
481
|
|
|
277
482
|
drawNode(canvas: Canvas, rn: RenderNode) {
|
|
278
|
-
const { node, absX, absY, absW, absH, clipRect } = rn
|
|
279
|
-
const ck = this.ck
|
|
280
|
-
const opacity = typeof node.opacity === 'number' ? node.opacity : 1
|
|
483
|
+
const { node, absX, absY, absW, absH, clipRect } = rn;
|
|
484
|
+
const ck = this.ck;
|
|
485
|
+
const opacity = typeof node.opacity === 'number' ? node.opacity : 1;
|
|
281
486
|
|
|
282
|
-
if (('visible' in node ? node.visible : undefined) === false) return
|
|
487
|
+
if (('visible' in node ? node.visible : undefined) === false) return;
|
|
283
488
|
|
|
284
489
|
// Pass zoom to text renderer
|
|
285
|
-
this.textRenderer.zoom = this.zoom
|
|
286
|
-
this.textRenderer.devicePixelRatio = this.devicePixelRatio
|
|
490
|
+
this.textRenderer.zoom = this.zoom;
|
|
491
|
+
this.textRenderer.devicePixelRatio = this.devicePixelRatio;
|
|
287
492
|
|
|
288
493
|
// Apply clipping from parent frame
|
|
289
|
-
let clipped = false
|
|
494
|
+
let clipped = false;
|
|
290
495
|
if (clipRect) {
|
|
291
|
-
canvas.save();
|
|
496
|
+
canvas.save();
|
|
497
|
+
clipped = true;
|
|
292
498
|
if (clipRect.rx > 0) {
|
|
293
|
-
canvas.clipRRect(
|
|
499
|
+
canvas.clipRRect(
|
|
500
|
+
ck.RRectXY(
|
|
501
|
+
ck.LTRBRect(clipRect.x, clipRect.y, clipRect.x + clipRect.w, clipRect.y + clipRect.h),
|
|
502
|
+
clipRect.rx,
|
|
503
|
+
clipRect.rx,
|
|
504
|
+
),
|
|
505
|
+
ck.ClipOp.Intersect,
|
|
506
|
+
true,
|
|
507
|
+
);
|
|
294
508
|
} else {
|
|
295
|
-
canvas.clipRect(
|
|
509
|
+
canvas.clipRect(
|
|
510
|
+
ck.LTRBRect(clipRect.x, clipRect.y, clipRect.x + clipRect.w, clipRect.y + clipRect.h),
|
|
511
|
+
ck.ClipOp.Intersect,
|
|
512
|
+
true,
|
|
513
|
+
);
|
|
296
514
|
}
|
|
297
515
|
}
|
|
298
516
|
|
|
299
517
|
// Apply flip
|
|
300
|
-
const flipX = node.flipX === true,
|
|
518
|
+
const flipX = node.flipX === true,
|
|
519
|
+
flipY = node.flipY === true;
|
|
301
520
|
if (flipX || flipY) {
|
|
302
|
-
canvas.save()
|
|
303
|
-
canvas.translate(absX + absW / 2, absY + absH / 2)
|
|
304
|
-
canvas.scale(flipX ? -1 : 1, flipY ? -1 : 1)
|
|
305
|
-
canvas.translate(-(absX + absW / 2), -(absY + absH / 2))
|
|
521
|
+
canvas.save();
|
|
522
|
+
canvas.translate(absX + absW / 2, absY + absH / 2);
|
|
523
|
+
canvas.scale(flipX ? -1 : 1, flipY ? -1 : 1);
|
|
524
|
+
canvas.translate(-(absX + absW / 2), -(absY + absH / 2));
|
|
306
525
|
}
|
|
307
526
|
|
|
308
527
|
// Apply rotation
|
|
309
|
-
const rotation = node.rotation ?? 0
|
|
528
|
+
const rotation = node.rotation ?? 0;
|
|
310
529
|
if (rotation !== 0) {
|
|
311
|
-
canvas.save()
|
|
312
|
-
canvas.rotate(rotation, absX + absW / 2, absY + absH / 2)
|
|
530
|
+
canvas.save();
|
|
531
|
+
canvas.rotate(rotation, absX + absW / 2, absY + absH / 2);
|
|
313
532
|
}
|
|
314
533
|
|
|
315
534
|
// Apply shadow (text uses glyph-shaped shadow, not rectangle)
|
|
316
|
-
const effects =
|
|
535
|
+
const effects =
|
|
536
|
+
'effects' in node ? (node as PenNode & { effects?: PenEffect[] }).effects : undefined;
|
|
317
537
|
if (node.type !== 'text') {
|
|
318
|
-
this.applyShadowDirect(canvas, effects, absX, absY, absW, absH)
|
|
538
|
+
this.applyShadowDirect(canvas, effects, absX, absY, absW, absH);
|
|
319
539
|
}
|
|
320
540
|
|
|
321
541
|
switch (node.type) {
|
|
322
|
-
case 'frame':
|
|
323
|
-
|
|
542
|
+
case 'frame':
|
|
543
|
+
case 'rectangle':
|
|
544
|
+
case 'group':
|
|
545
|
+
this.drawRect(canvas, node, absX, absY, absW, absH, opacity);
|
|
546
|
+
break;
|
|
324
547
|
case 'ellipse':
|
|
325
|
-
this.drawEllipse(canvas, node, absX, absY, absW, absH, opacity);
|
|
548
|
+
this.drawEllipse(canvas, node, absX, absY, absW, absH, opacity);
|
|
549
|
+
break;
|
|
326
550
|
case 'line':
|
|
327
|
-
this.drawLine(canvas, node, absX, absY, opacity);
|
|
551
|
+
this.drawLine(canvas, node, absX, absY, opacity);
|
|
552
|
+
break;
|
|
328
553
|
case 'polygon':
|
|
329
|
-
this.drawPolygon(canvas, node, absX, absY, absW, absH, opacity);
|
|
554
|
+
this.drawPolygon(canvas, node, absX, absY, absW, absH, opacity);
|
|
555
|
+
break;
|
|
330
556
|
case 'path':
|
|
331
|
-
this.drawPath(canvas, node, absX, absY, absW, absH, opacity);
|
|
557
|
+
this.drawPath(canvas, node, absX, absY, absW, absH, opacity);
|
|
558
|
+
break;
|
|
332
559
|
case 'icon_font':
|
|
333
|
-
this.drawIconFont(canvas, node, absX, absY, absW, absH, opacity);
|
|
560
|
+
this.drawIconFont(canvas, node, absX, absY, absW, absH, opacity);
|
|
561
|
+
break;
|
|
334
562
|
case 'text':
|
|
335
|
-
this.textRenderer.drawText(canvas, node, absX, absY, absW, absH, opacity, effects);
|
|
563
|
+
this.textRenderer.drawText(canvas, node, absX, absY, absW, absH, opacity, effects);
|
|
564
|
+
break;
|
|
336
565
|
case 'image':
|
|
337
|
-
this.drawImage(canvas, node, absX, absY, absW, absH, opacity);
|
|
566
|
+
this.drawImage(canvas, node, absX, absY, absW, absH, opacity);
|
|
567
|
+
break;
|
|
338
568
|
}
|
|
339
569
|
|
|
340
|
-
if (rotation !== 0) canvas.restore()
|
|
341
|
-
if (flipX || flipY) canvas.restore()
|
|
342
|
-
if (clipped) canvas.restore()
|
|
570
|
+
if (rotation !== 0) canvas.restore();
|
|
571
|
+
if (flipX || flipY) canvas.restore();
|
|
572
|
+
if (clipped) canvas.restore();
|
|
343
573
|
}
|
|
344
574
|
|
|
345
575
|
// ---------------------------------------------------------------------------
|
|
346
576
|
// Shape drawing
|
|
347
577
|
// ---------------------------------------------------------------------------
|
|
348
578
|
|
|
349
|
-
private drawRect(
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
const
|
|
579
|
+
private drawRect(
|
|
580
|
+
canvas: Canvas,
|
|
581
|
+
node: PenNode,
|
|
582
|
+
x: number,
|
|
583
|
+
y: number,
|
|
584
|
+
w: number,
|
|
585
|
+
h: number,
|
|
586
|
+
opacity: number,
|
|
587
|
+
) {
|
|
588
|
+
const ck = this.ck;
|
|
589
|
+
const container = node as PenNode & ContainerProps;
|
|
590
|
+
const cr = cornerRadii(container.cornerRadius);
|
|
591
|
+
const fills = container.fill;
|
|
592
|
+
const stroke = container.stroke;
|
|
593
|
+
const hasFill = fills && fills.length > 0;
|
|
594
|
+
const isContainer = node.type === 'frame' || node.type === 'group';
|
|
595
|
+
|
|
596
|
+
const { paint: fillPaint, imageFillDraw } = this.makeFillPaint(
|
|
597
|
+
hasFill || !shouldUseTransparentFallbackFill(fills, stroke, isContainer)
|
|
598
|
+
? fills
|
|
599
|
+
: 'transparent',
|
|
600
|
+
w,
|
|
601
|
+
h,
|
|
602
|
+
opacity,
|
|
603
|
+
x,
|
|
604
|
+
y,
|
|
605
|
+
);
|
|
606
|
+
const hasRoundedCorners = cr.some((r) => r > 0);
|
|
360
607
|
if (hasRoundedCorners) {
|
|
361
|
-
const maxR = Math.min(w / 2, h / 2)
|
|
362
|
-
canvas.drawRRect(
|
|
608
|
+
const maxR = Math.min(w / 2, h / 2);
|
|
609
|
+
canvas.drawRRect(
|
|
610
|
+
ck.RRectXY(ck.LTRBRect(x, y, x + w, y + h), Math.min(cr[0], maxR), Math.min(cr[0], maxR)),
|
|
611
|
+
fillPaint,
|
|
612
|
+
);
|
|
363
613
|
} else {
|
|
364
|
-
canvas.drawRect(ck.LTRBRect(x, y, x + w, y + h), fillPaint)
|
|
614
|
+
canvas.drawRect(ck.LTRBRect(x, y, x + w, y + h), fillPaint);
|
|
365
615
|
}
|
|
366
|
-
fillPaint.delete()
|
|
616
|
+
fillPaint.delete();
|
|
367
617
|
|
|
368
618
|
if (imageFillDraw) {
|
|
369
|
-
canvas.save()
|
|
619
|
+
canvas.save();
|
|
370
620
|
if (hasRoundedCorners) {
|
|
371
|
-
const maxR = Math.min(w / 2, h / 2)
|
|
372
|
-
canvas.clipRRect(
|
|
621
|
+
const maxR = Math.min(w / 2, h / 2);
|
|
622
|
+
canvas.clipRRect(
|
|
623
|
+
ck.RRectXY(ck.LTRBRect(x, y, x + w, y + h), Math.min(cr[0], maxR), Math.min(cr[0], maxR)),
|
|
624
|
+
ck.ClipOp.Intersect,
|
|
625
|
+
true,
|
|
626
|
+
);
|
|
373
627
|
} else {
|
|
374
|
-
canvas.clipRect(ck.LTRBRect(x, y, x + w, y + h), ck.ClipOp.Intersect, true)
|
|
628
|
+
canvas.clipRect(ck.LTRBRect(x, y, x + w, y + h), ck.ClipOp.Intersect, true);
|
|
375
629
|
}
|
|
376
|
-
this.drawImageFillRect(
|
|
377
|
-
|
|
630
|
+
this.drawImageFillRect(
|
|
631
|
+
canvas,
|
|
632
|
+
imageFillDraw.fill,
|
|
633
|
+
imageFillDraw.w,
|
|
634
|
+
imageFillDraw.h,
|
|
635
|
+
imageFillDraw.absX,
|
|
636
|
+
imageFillDraw.absY,
|
|
637
|
+
imageFillDraw.opacity,
|
|
638
|
+
);
|
|
639
|
+
canvas.restore();
|
|
378
640
|
}
|
|
379
641
|
|
|
380
|
-
const strokePaint = this.makeStrokePaint(stroke, opacity)
|
|
642
|
+
const strokePaint = this.makeStrokePaint(stroke, opacity);
|
|
381
643
|
if (strokePaint) {
|
|
382
644
|
if (hasRoundedCorners) {
|
|
383
|
-
const maxR = Math.min(w / 2, h / 2)
|
|
384
|
-
canvas.drawRRect(
|
|
645
|
+
const maxR = Math.min(w / 2, h / 2);
|
|
646
|
+
canvas.drawRRect(
|
|
647
|
+
ck.RRectXY(ck.LTRBRect(x, y, x + w, y + h), Math.min(cr[0], maxR), Math.min(cr[0], maxR)),
|
|
648
|
+
strokePaint,
|
|
649
|
+
);
|
|
385
650
|
} else {
|
|
386
|
-
canvas.drawRect(ck.LTRBRect(x, y, x + w, y + h), strokePaint)
|
|
651
|
+
canvas.drawRect(ck.LTRBRect(x, y, x + w, y + h), strokePaint);
|
|
387
652
|
}
|
|
388
|
-
strokePaint.delete()
|
|
653
|
+
strokePaint.delete();
|
|
389
654
|
}
|
|
390
655
|
}
|
|
391
656
|
|
|
392
|
-
private drawEllipse(
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
657
|
+
private drawEllipse(
|
|
658
|
+
canvas: Canvas,
|
|
659
|
+
node: PenNode,
|
|
660
|
+
x: number,
|
|
661
|
+
y: number,
|
|
662
|
+
w: number,
|
|
663
|
+
h: number,
|
|
664
|
+
opacity: number,
|
|
665
|
+
) {
|
|
666
|
+
const ck = this.ck;
|
|
667
|
+
const eNode = node as EllipseNode;
|
|
668
|
+
const fills = eNode.fill,
|
|
669
|
+
stroke = eNode.stroke;
|
|
670
|
+
const cr = cornerRadiusValue(eNode.cornerRadius);
|
|
671
|
+
const fillSource = shouldUseTransparentFallbackFill(fills, stroke) ? 'transparent' : fills;
|
|
397
672
|
|
|
398
673
|
if (isArcEllipse(eNode.startAngle, eNode.sweepAngle, eNode.innerRadius)) {
|
|
399
|
-
const arcD = buildEllipseArcPath(
|
|
400
|
-
|
|
674
|
+
const arcD = buildEllipseArcPath(
|
|
675
|
+
w,
|
|
676
|
+
h,
|
|
677
|
+
eNode.startAngle ?? 0,
|
|
678
|
+
eNode.sweepAngle ?? 360,
|
|
679
|
+
eNode.innerRadius ?? 0,
|
|
680
|
+
);
|
|
681
|
+
const path = ck.Path.MakeFromSVGString(arcD);
|
|
401
682
|
if (path) {
|
|
402
|
-
path.offset(x, y)
|
|
403
|
-
const { paint: fillPaint } = this.makeFillPaint(
|
|
404
|
-
if (cr > 0) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
683
|
+
path.offset(x, y);
|
|
684
|
+
const { paint: fillPaint } = this.makeFillPaint(fillSource, w, h, opacity, x, y);
|
|
685
|
+
if (cr > 0) {
|
|
686
|
+
const effect = ck.PathEffect.MakeCorner(cr);
|
|
687
|
+
if (effect) fillPaint.setPathEffect(effect);
|
|
688
|
+
}
|
|
689
|
+
canvas.drawPath(path, fillPaint);
|
|
690
|
+
fillPaint.delete();
|
|
691
|
+
const strokePaint = this.makeStrokePaint(stroke, opacity);
|
|
692
|
+
if (strokePaint) {
|
|
693
|
+
if (cr > 0) {
|
|
694
|
+
const effect = ck.PathEffect.MakeCorner(cr);
|
|
695
|
+
if (effect) strokePaint.setPathEffect(effect);
|
|
696
|
+
}
|
|
697
|
+
canvas.drawPath(path, strokePaint);
|
|
698
|
+
strokePaint.delete();
|
|
699
|
+
}
|
|
700
|
+
path.delete();
|
|
409
701
|
}
|
|
410
|
-
return
|
|
702
|
+
return;
|
|
411
703
|
}
|
|
412
704
|
|
|
413
|
-
const { paint: fillPaint } = this.makeFillPaint(
|
|
414
|
-
canvas.drawOval(ck.LTRBRect(x, y, x + w, y + h), fillPaint);
|
|
415
|
-
|
|
416
|
-
|
|
705
|
+
const { paint: fillPaint } = this.makeFillPaint(fillSource, w, h, opacity, x, y);
|
|
706
|
+
canvas.drawOval(ck.LTRBRect(x, y, x + w, y + h), fillPaint);
|
|
707
|
+
fillPaint.delete();
|
|
708
|
+
const strokePaint = this.makeStrokePaint(stroke, opacity);
|
|
709
|
+
if (strokePaint) {
|
|
710
|
+
canvas.drawOval(ck.LTRBRect(x, y, x + w, y + h), strokePaint);
|
|
711
|
+
strokePaint.delete();
|
|
712
|
+
}
|
|
417
713
|
}
|
|
418
714
|
|
|
419
715
|
private drawLine(canvas: Canvas, node: PenNode, x: number, y: number, opacity: number) {
|
|
420
|
-
const ck = this.ck
|
|
421
|
-
const lNode = node as LineNode
|
|
422
|
-
const x2 = lNode.x2 ?? x + 100,
|
|
423
|
-
|
|
424
|
-
const
|
|
425
|
-
const
|
|
426
|
-
paint
|
|
427
|
-
|
|
428
|
-
|
|
716
|
+
const ck = this.ck;
|
|
717
|
+
const lNode = node as LineNode;
|
|
718
|
+
const x2 = lNode.x2 ?? x + 100,
|
|
719
|
+
y2 = lNode.y2 ?? y;
|
|
720
|
+
const strokeColor = resolveStrokeColor(lNode.stroke) ?? DEFAULT_STROKE;
|
|
721
|
+
const strokeWidth = resolveStrokeWidth(lNode.stroke) || DEFAULT_STROKE_WIDTH;
|
|
722
|
+
const paint = new ck.Paint();
|
|
723
|
+
paint.setStyle(ck.PaintStyle.Stroke);
|
|
724
|
+
paint.setAntiAlias(true);
|
|
725
|
+
paint.setStrokeWidth(strokeWidth);
|
|
726
|
+
const c = parseColor(ck, strokeColor);
|
|
727
|
+
c[3] *= opacity;
|
|
728
|
+
paint.setColor(c);
|
|
729
|
+
canvas.drawLine(x, y, x2, y2, paint);
|
|
730
|
+
paint.delete();
|
|
429
731
|
}
|
|
430
732
|
|
|
431
|
-
private drawPolygon(
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
733
|
+
private drawPolygon(
|
|
734
|
+
canvas: Canvas,
|
|
735
|
+
node: PenNode,
|
|
736
|
+
x: number,
|
|
737
|
+
y: number,
|
|
738
|
+
w: number,
|
|
739
|
+
h: number,
|
|
740
|
+
opacity: number,
|
|
741
|
+
) {
|
|
742
|
+
const ck = this.ck;
|
|
743
|
+
const pNode = node as PolygonNode;
|
|
744
|
+
const count = pNode.polygonCount || 6;
|
|
745
|
+
const fills = pNode.fill,
|
|
746
|
+
stroke = pNode.stroke;
|
|
747
|
+
const cr = cornerRadiusValue(pNode.cornerRadius);
|
|
748
|
+
const fillSource = shouldUseTransparentFallbackFill(fills, stroke) ? 'transparent' : fills;
|
|
749
|
+
|
|
750
|
+
const raw: [number, number][] = [];
|
|
439
751
|
for (let i = 0; i < count; i++) {
|
|
440
|
-
const angle = (i * 2 * Math.PI) / count - Math.PI / 2
|
|
441
|
-
raw.push([Math.cos(angle), Math.sin(angle)])
|
|
752
|
+
const angle = (i * 2 * Math.PI) / count - Math.PI / 2;
|
|
753
|
+
raw.push([Math.cos(angle), Math.sin(angle)]);
|
|
754
|
+
}
|
|
755
|
+
let minX = Infinity,
|
|
756
|
+
maxX = -Infinity,
|
|
757
|
+
minY = Infinity,
|
|
758
|
+
maxY = -Infinity;
|
|
759
|
+
for (const [rx, ry] of raw) {
|
|
760
|
+
if (rx < minX) minX = rx;
|
|
761
|
+
if (rx > maxX) maxX = rx;
|
|
762
|
+
if (ry < minY) minY = ry;
|
|
763
|
+
if (ry > maxY) maxY = ry;
|
|
442
764
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const rawW = maxX - minX, rawH = maxY - minY
|
|
765
|
+
const rawW = maxX - minX,
|
|
766
|
+
rawH = maxY - minY;
|
|
446
767
|
|
|
447
|
-
const path = new ck.Path()
|
|
768
|
+
const path = new ck.Path();
|
|
448
769
|
for (let i = 0; i < count; i++) {
|
|
449
|
-
const px = x + ((raw[i][0] - minX) / rawW) * w,
|
|
450
|
-
|
|
770
|
+
const px = x + ((raw[i][0] - minX) / rawW) * w,
|
|
771
|
+
py = y + ((raw[i][1] - minY) / rawH) * h;
|
|
772
|
+
if (i === 0) path.moveTo(px, py);
|
|
773
|
+
else path.lineTo(px, py);
|
|
451
774
|
}
|
|
452
|
-
path.close()
|
|
775
|
+
path.close();
|
|
453
776
|
|
|
454
|
-
const { paint: fillPaint } = this.makeFillPaint(
|
|
455
|
-
if (cr > 0) {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
777
|
+
const { paint: fillPaint } = this.makeFillPaint(fillSource, w, h, opacity, x, y);
|
|
778
|
+
if (cr > 0) {
|
|
779
|
+
const effect = ck.PathEffect.MakeCorner(cr);
|
|
780
|
+
if (effect) fillPaint.setPathEffect(effect);
|
|
781
|
+
}
|
|
782
|
+
canvas.drawPath(path, fillPaint);
|
|
783
|
+
fillPaint.delete();
|
|
784
|
+
const strokePaint = this.makeStrokePaint(stroke, opacity);
|
|
785
|
+
if (strokePaint) {
|
|
786
|
+
if (cr > 0) {
|
|
787
|
+
const effect = ck.PathEffect.MakeCorner(cr);
|
|
788
|
+
if (effect) strokePaint.setPathEffect(effect);
|
|
789
|
+
}
|
|
790
|
+
canvas.drawPath(path, strokePaint);
|
|
791
|
+
strokePaint.delete();
|
|
792
|
+
}
|
|
793
|
+
path.delete();
|
|
460
794
|
}
|
|
461
795
|
|
|
462
|
-
private drawPath(
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
796
|
+
private drawPath(
|
|
797
|
+
canvas: Canvas,
|
|
798
|
+
node: PenNode,
|
|
799
|
+
x: number,
|
|
800
|
+
y: number,
|
|
801
|
+
w: number,
|
|
802
|
+
h: number,
|
|
803
|
+
opacity: number,
|
|
804
|
+
) {
|
|
805
|
+
const ck = this.ck;
|
|
806
|
+
const pNode = node as PathNode;
|
|
807
|
+
const rawD = typeof pNode.d === 'string' && pNode.d.trim().length > 0 ? pNode.d : 'M0 0 L0 0';
|
|
808
|
+
const fills = pNode.fill,
|
|
809
|
+
stroke = pNode.stroke;
|
|
810
|
+
|
|
811
|
+
let path: ReturnType<typeof ck.Path.MakeFromSVGString> = null;
|
|
812
|
+
if (hasInvalidNumbers(rawD)) {
|
|
813
|
+
path = tryManualPathParse(ck, rawD);
|
|
814
|
+
} else {
|
|
815
|
+
const d = sanitizeSvgPath(rawD);
|
|
816
|
+
path = ck.Path.MakeFromSVGString(d);
|
|
817
|
+
if (!path && d !== rawD) path = ck.Path.MakeFromSVGString(rawD);
|
|
818
|
+
if (!path) path = tryManualPathParse(ck, rawD);
|
|
475
819
|
}
|
|
476
820
|
if (!path) {
|
|
477
|
-
if (w > 0 && h > 0) {
|
|
478
|
-
|
|
821
|
+
if (w > 0 && h > 0) {
|
|
822
|
+
const { paint: fp } = this.makeFillPaint(fills, w, h, opacity, x, y);
|
|
823
|
+
canvas.drawRect(ck.LTRBRect(x, y, x + w, y + h), fp);
|
|
824
|
+
fp.delete();
|
|
825
|
+
}
|
|
826
|
+
return;
|
|
479
827
|
}
|
|
480
828
|
|
|
481
|
-
const
|
|
482
|
-
|
|
829
|
+
const parsedAnchors = pNode.anchors
|
|
830
|
+
? {
|
|
831
|
+
anchors: pNode.anchors,
|
|
832
|
+
closed: pNode.closed ?? /[Zz]\s*$/.test(rawD),
|
|
833
|
+
}
|
|
834
|
+
: pathDataToAnchors(rawD);
|
|
835
|
+
const geometryBounds = parsedAnchors
|
|
836
|
+
? getPathBoundsFromAnchors(parsedAnchors.anchors, parsedAnchors.closed)
|
|
837
|
+
: null;
|
|
838
|
+
const bounds = geometryBounds
|
|
839
|
+
? Float32Array.of(
|
|
840
|
+
geometryBounds.x,
|
|
841
|
+
geometryBounds.y,
|
|
842
|
+
geometryBounds.x + geometryBounds.width,
|
|
843
|
+
geometryBounds.y + geometryBounds.height,
|
|
844
|
+
)
|
|
845
|
+
: path.getBounds();
|
|
846
|
+
const nativeW = bounds[2] - bounds[0],
|
|
847
|
+
nativeH = bounds[3] - bounds[1];
|
|
483
848
|
if (w > 0 && h > 0 && nativeW > 0.01 && nativeH > 0.01) {
|
|
484
|
-
const isIcon = !!pNode.iconId
|
|
485
|
-
const sx = isIcon ? Math.min(w / nativeW, h / nativeH) : w / nativeW
|
|
486
|
-
const sy = isIcon ? sx : h / nativeH
|
|
487
|
-
path.transform(
|
|
849
|
+
const isIcon = !!pNode.iconId;
|
|
850
|
+
const sx = isIcon ? Math.min(w / nativeW, h / nativeH) : w / nativeW;
|
|
851
|
+
const sy = isIcon ? sx : h / nativeH;
|
|
852
|
+
path.transform(
|
|
853
|
+
ck.Matrix.multiply(
|
|
854
|
+
ck.Matrix.translated(x - bounds[0] * sx, y - bounds[1] * sy),
|
|
855
|
+
ck.Matrix.scaled(sx, sy),
|
|
856
|
+
),
|
|
857
|
+
);
|
|
488
858
|
} else if (nativeW > 0.01 || nativeH > 0.01) {
|
|
489
|
-
const sx = nativeW > 0.01 && w > 0 ? w / nativeW : 1,
|
|
490
|
-
|
|
491
|
-
|
|
859
|
+
const sx = nativeW > 0.01 && w > 0 ? w / nativeW : 1,
|
|
860
|
+
sy = nativeH > 0.01 && h > 0 ? h / nativeH : 1;
|
|
861
|
+
path.transform(
|
|
862
|
+
ck.Matrix.multiply(
|
|
863
|
+
ck.Matrix.translated(x - bounds[0] * sx, y - bounds[1] * sy),
|
|
864
|
+
ck.Matrix.scaled(sx, sy),
|
|
865
|
+
),
|
|
866
|
+
);
|
|
867
|
+
} else {
|
|
868
|
+
path.offset(x, y);
|
|
869
|
+
}
|
|
492
870
|
|
|
493
|
-
const hasExplicitFill = fills && fills.length > 0
|
|
494
|
-
const strokeColor = resolveStrokeColor(stroke),
|
|
495
|
-
|
|
871
|
+
const hasExplicitFill = fills && fills.length > 0;
|
|
872
|
+
const strokeColor = resolveStrokeColor(stroke),
|
|
873
|
+
strokeWidth = resolveStrokeWidth(stroke);
|
|
874
|
+
const hasVisibleStroke = strokeWidth > 0 && !!strokeColor;
|
|
496
875
|
|
|
497
876
|
if (hasExplicitFill || !hasVisibleStroke) {
|
|
498
|
-
const { paint: fillPaint } = this.makeFillPaint(
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
877
|
+
const { paint: fillPaint } = this.makeFillPaint(
|
|
878
|
+
hasExplicitFill ? fills : undefined,
|
|
879
|
+
w,
|
|
880
|
+
h,
|
|
881
|
+
opacity,
|
|
882
|
+
x,
|
|
883
|
+
y,
|
|
884
|
+
);
|
|
885
|
+
const closeCount = (rawD.match(/Z/gi) || []).length;
|
|
886
|
+
path.setFillType(closeCount > 1 ? ck.FillType.EvenOdd : ck.FillType.Winding);
|
|
887
|
+
canvas.drawPath(path, fillPaint);
|
|
888
|
+
fillPaint.delete();
|
|
889
|
+
}
|
|
890
|
+
if (hasVisibleStroke) {
|
|
891
|
+
const sp = this.makeStrokePaint(stroke, opacity);
|
|
892
|
+
if (sp) {
|
|
893
|
+
canvas.drawPath(path, sp);
|
|
894
|
+
sp.delete();
|
|
895
|
+
}
|
|
502
896
|
}
|
|
503
|
-
|
|
504
|
-
path.delete()
|
|
897
|
+
path.delete();
|
|
505
898
|
}
|
|
506
899
|
|
|
507
|
-
private drawIconFont(
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
900
|
+
private drawIconFont(
|
|
901
|
+
canvas: Canvas,
|
|
902
|
+
node: PenNode,
|
|
903
|
+
x: number,
|
|
904
|
+
y: number,
|
|
905
|
+
w: number,
|
|
906
|
+
h: number,
|
|
907
|
+
opacity: number,
|
|
908
|
+
) {
|
|
909
|
+
const ck = this.ck;
|
|
910
|
+
const iNode = node as IconFontNode;
|
|
911
|
+
const iconName = iNode.iconFontName ?? iNode.name ?? '';
|
|
912
|
+
const iconMatch = this.iconLookup?.(iconName) ?? null;
|
|
913
|
+
const iconD = iconMatch?.d ?? FALLBACK_ICON_D;
|
|
914
|
+
const iconStyle = iconMatch?.style ?? 'stroke';
|
|
915
|
+
|
|
916
|
+
const rawFill = iNode.fill;
|
|
917
|
+
const iconFillColor =
|
|
918
|
+
typeof rawFill === 'string'
|
|
919
|
+
? rawFill
|
|
920
|
+
: Array.isArray(iNode.fill) && iNode.fill.length > 0
|
|
921
|
+
? resolveFillColor(iNode.fill)
|
|
922
|
+
: '#64748B';
|
|
923
|
+
|
|
924
|
+
const sanitizedIconD = sanitizeSvgPath(iconD);
|
|
925
|
+
let path = ck.Path.MakeFromSVGString(sanitizedIconD);
|
|
926
|
+
if (!path && sanitizedIconD !== iconD) path = ck.Path.MakeFromSVGString(iconD);
|
|
927
|
+
if (!path) path = tryManualPathParse(ck, iconD);
|
|
928
|
+
if (!path) return;
|
|
929
|
+
|
|
930
|
+
const bounds = path.getBounds();
|
|
931
|
+
const nativeW = bounds[2] - bounds[0],
|
|
932
|
+
nativeH = bounds[3] - bounds[1];
|
|
527
933
|
if (w > 0 && h > 0 && nativeW > 0 && nativeH > 0) {
|
|
528
|
-
const s = Math.min(w / nativeW, h / nativeH)
|
|
529
|
-
path.transform(
|
|
530
|
-
|
|
934
|
+
const s = Math.min(w / nativeW, h / nativeH);
|
|
935
|
+
path.transform(
|
|
936
|
+
ck.Matrix.multiply(
|
|
937
|
+
ck.Matrix.translated(x - bounds[0] * s, y - bounds[1] * s),
|
|
938
|
+
ck.Matrix.scaled(s, s),
|
|
939
|
+
),
|
|
940
|
+
);
|
|
941
|
+
} else {
|
|
942
|
+
path.offset(x, y);
|
|
943
|
+
}
|
|
531
944
|
|
|
532
|
-
const paint = new ck.Paint()
|
|
533
|
-
paint.setAntiAlias(true)
|
|
534
|
-
const c = parseColor(ck, iconFillColor);
|
|
945
|
+
const paint = new ck.Paint();
|
|
946
|
+
paint.setAntiAlias(true);
|
|
947
|
+
const c = parseColor(ck, iconFillColor);
|
|
948
|
+
c[3] *= opacity;
|
|
949
|
+
paint.setColor(c);
|
|
535
950
|
if (iconStyle === 'stroke') {
|
|
536
|
-
paint.setStyle(ck.PaintStyle.Stroke);
|
|
537
|
-
paint.
|
|
951
|
+
paint.setStyle(ck.PaintStyle.Stroke);
|
|
952
|
+
paint.setStrokeWidth(2);
|
|
953
|
+
paint.setStrokeCap(ck.StrokeCap.Round);
|
|
954
|
+
paint.setStrokeJoin(ck.StrokeJoin.Round);
|
|
538
955
|
} else {
|
|
539
|
-
paint.setStyle(ck.PaintStyle.Fill);
|
|
956
|
+
paint.setStyle(ck.PaintStyle.Fill);
|
|
957
|
+
path.setFillType(ck.FillType.EvenOdd);
|
|
540
958
|
}
|
|
541
|
-
canvas.drawPath(path, paint);
|
|
959
|
+
canvas.drawPath(path, paint);
|
|
960
|
+
paint.delete();
|
|
961
|
+
path.delete();
|
|
542
962
|
}
|
|
543
963
|
|
|
544
964
|
// ---------------------------------------------------------------------------
|
|
545
965
|
// Image drawing
|
|
546
966
|
// ---------------------------------------------------------------------------
|
|
547
967
|
|
|
548
|
-
private drawImage(
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
968
|
+
private drawImage(
|
|
969
|
+
canvas: Canvas,
|
|
970
|
+
node: PenNode,
|
|
971
|
+
x: number,
|
|
972
|
+
y: number,
|
|
973
|
+
w: number,
|
|
974
|
+
h: number,
|
|
975
|
+
opacity: number,
|
|
976
|
+
) {
|
|
977
|
+
const ck = this.ck;
|
|
978
|
+
const iNode = node as ImageNode;
|
|
979
|
+
const src: string | undefined = iNode.src;
|
|
980
|
+
const cr = cornerRadiusValue(iNode.cornerRadius);
|
|
981
|
+
|
|
982
|
+
if (!src) {
|
|
983
|
+
this.drawImageFallback(canvas, x, y, w, h, cr, opacity, false);
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
559
986
|
|
|
560
|
-
const
|
|
987
|
+
const cached = this.imageLoader.get(src);
|
|
988
|
+
if (cached === undefined) {
|
|
989
|
+
this.imageLoader.request(src);
|
|
990
|
+
this.drawImageFallback(canvas, x, y, w, h, cr, opacity, false);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (!cached) {
|
|
994
|
+
const status = this.imageLoader.getStatus(src);
|
|
995
|
+
this.drawImageFallback(
|
|
996
|
+
canvas,
|
|
997
|
+
x,
|
|
998
|
+
y,
|
|
999
|
+
w,
|
|
1000
|
+
h,
|
|
1001
|
+
cr,
|
|
1002
|
+
opacity,
|
|
1003
|
+
status?.state === 'missing' || status?.state === 'error',
|
|
1004
|
+
);
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
561
1007
|
|
|
562
|
-
|
|
563
|
-
|
|
1008
|
+
const imgW = cached.width(),
|
|
1009
|
+
imgH = cached.height();
|
|
1010
|
+
|
|
1011
|
+
if (cr > 0) {
|
|
1012
|
+
canvas.save();
|
|
1013
|
+
const maxR = Math.min(cr, w / 2, h / 2);
|
|
1014
|
+
canvas.clipRRect(
|
|
1015
|
+
ck.RRectXY(ck.LTRBRect(x, y, x + w, y + h), maxR, maxR),
|
|
1016
|
+
ck.ClipOp.Intersect,
|
|
1017
|
+
true,
|
|
1018
|
+
);
|
|
1019
|
+
} else {
|
|
1020
|
+
canvas.save();
|
|
1021
|
+
canvas.clipRect(ck.LTRBRect(x, y, x + w, y + h), ck.ClipOp.Intersect, true);
|
|
1022
|
+
}
|
|
564
1023
|
|
|
565
|
-
const paint = new ck.Paint()
|
|
566
|
-
paint.setAntiAlias(true)
|
|
567
|
-
if (opacity < 1) paint.setAlphaf(opacity)
|
|
568
|
-
const adjFilter = this.buildImageAdjustmentFilter(iNode)
|
|
569
|
-
if (adjFilter) paint.setColorFilter(adjFilter)
|
|
1024
|
+
const paint = new ck.Paint();
|
|
1025
|
+
paint.setAntiAlias(true);
|
|
1026
|
+
if (opacity < 1) paint.setAlphaf(opacity);
|
|
1027
|
+
const adjFilter = this.buildImageAdjustmentFilter(iNode);
|
|
1028
|
+
if (adjFilter) paint.setColorFilter(adjFilter);
|
|
570
1029
|
|
|
571
|
-
const fit = iNode.objectFit ?? 'fill'
|
|
1030
|
+
const fit = iNode.objectFit ?? 'fill';
|
|
572
1031
|
if (fit === 'tile') {
|
|
573
|
-
const tileMatrix = Float32Array.of(1, 0, -x, 0, 1, -y, 0, 0, 1)
|
|
574
|
-
const shader = cached.makeShaderOptions(
|
|
575
|
-
|
|
1032
|
+
const tileMatrix = Float32Array.of(1, 0, -x, 0, 1, -y, 0, 0, 1);
|
|
1033
|
+
const shader = cached.makeShaderOptions(
|
|
1034
|
+
ck.TileMode.Repeat,
|
|
1035
|
+
ck.TileMode.Repeat,
|
|
1036
|
+
ck.FilterMode.Linear,
|
|
1037
|
+
ck.MipmapMode.None,
|
|
1038
|
+
tileMatrix,
|
|
1039
|
+
);
|
|
1040
|
+
if (shader) {
|
|
1041
|
+
paint.setShader(shader);
|
|
1042
|
+
canvas.drawRect(ck.LTRBRect(x, y, x + w, y + h), paint);
|
|
1043
|
+
}
|
|
576
1044
|
} else if (fit === 'fit') {
|
|
577
|
-
const bgPaint = new ck.Paint();
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
canvas.
|
|
1045
|
+
const bgPaint = new ck.Paint();
|
|
1046
|
+
bgPaint.setStyle(ck.PaintStyle.Fill);
|
|
1047
|
+
bgPaint.setColor(parseColor(ck, '#f3f4f6'));
|
|
1048
|
+
if (opacity < 1) bgPaint.setAlphaf(opacity * 0.3);
|
|
1049
|
+
else bgPaint.setAlphaf(0.3);
|
|
1050
|
+
canvas.drawRect(ck.LTRBRect(x, y, x + w, y + h), bgPaint);
|
|
1051
|
+
bgPaint.delete();
|
|
1052
|
+
const scale = Math.min(w / imgW, h / imgH),
|
|
1053
|
+
dw = imgW * scale,
|
|
1054
|
+
dh = imgH * scale;
|
|
1055
|
+
const dx = x + (w - dw) / 2,
|
|
1056
|
+
dy = y + (h - dh) / 2;
|
|
1057
|
+
canvas.drawImageRect(
|
|
1058
|
+
cached,
|
|
1059
|
+
ck.LTRBRect(0, 0, imgW, imgH),
|
|
1060
|
+
ck.LTRBRect(dx, dy, dx + dw, dy + dh),
|
|
1061
|
+
paint,
|
|
1062
|
+
);
|
|
583
1063
|
} else {
|
|
584
|
-
const scale = Math.max(w / imgW, h / imgH),
|
|
585
|
-
|
|
586
|
-
|
|
1064
|
+
const scale = Math.max(w / imgW, h / imgH),
|
|
1065
|
+
dw = imgW * scale,
|
|
1066
|
+
dh = imgH * scale;
|
|
1067
|
+
const dx = x + (w - dw) / 2,
|
|
1068
|
+
dy = y + (h - dh) / 2;
|
|
1069
|
+
canvas.drawImageRect(
|
|
1070
|
+
cached,
|
|
1071
|
+
ck.LTRBRect(0, 0, imgW, imgH),
|
|
1072
|
+
ck.LTRBRect(dx, dy, dx + dw, dy + dh),
|
|
1073
|
+
paint,
|
|
1074
|
+
);
|
|
587
1075
|
}
|
|
588
|
-
paint.delete();
|
|
1076
|
+
paint.delete();
|
|
1077
|
+
canvas.restore();
|
|
589
1078
|
}
|
|
590
1079
|
|
|
591
|
-
private drawImageFallback(
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
1080
|
+
private drawImageFallback(
|
|
1081
|
+
canvas: Canvas,
|
|
1082
|
+
x: number,
|
|
1083
|
+
y: number,
|
|
1084
|
+
w: number,
|
|
1085
|
+
h: number,
|
|
1086
|
+
cr: number,
|
|
1087
|
+
opacity: number,
|
|
1088
|
+
emphasizeMissing: boolean,
|
|
1089
|
+
) {
|
|
1090
|
+
const ck = this.ck;
|
|
1091
|
+
const rect = ck.LTRBRect(x, y, x + w, y + h);
|
|
1092
|
+
const maxR = Math.min(cr, w / 2, h / 2);
|
|
1093
|
+
|
|
1094
|
+
const fillPaint = new ck.Paint();
|
|
1095
|
+
fillPaint.setStyle(ck.PaintStyle.Fill);
|
|
1096
|
+
fillPaint.setAntiAlias(true);
|
|
1097
|
+
const fillColor = parseColor(ck, emphasizeMissing ? '#f8d7da' : '#e5e7eb');
|
|
1098
|
+
fillColor[3] *= opacity;
|
|
1099
|
+
fillPaint.setColor(fillColor);
|
|
1100
|
+
|
|
1101
|
+
if (cr > 0) {
|
|
1102
|
+
canvas.drawRRect(ck.RRectXY(rect, maxR, maxR), fillPaint);
|
|
1103
|
+
} else {
|
|
1104
|
+
canvas.drawRect(rect, fillPaint);
|
|
1105
|
+
}
|
|
1106
|
+
fillPaint.delete();
|
|
1107
|
+
|
|
1108
|
+
const strokePaint = new ck.Paint();
|
|
1109
|
+
strokePaint.setStyle(ck.PaintStyle.Stroke);
|
|
1110
|
+
strokePaint.setAntiAlias(true);
|
|
1111
|
+
strokePaint.setStrokeWidth(Math.max(1, Math.min(w, h) * 0.02));
|
|
1112
|
+
const strokeColor = parseColor(ck, emphasizeMissing ? '#c2410c' : '#94a3b8');
|
|
1113
|
+
strokeColor[3] *= opacity;
|
|
1114
|
+
strokePaint.setColor(strokeColor);
|
|
1115
|
+
const dash = ck.PathEffect.MakeDash([8, 6], 0);
|
|
1116
|
+
if (dash) strokePaint.setPathEffect(dash);
|
|
1117
|
+
|
|
1118
|
+
if (cr > 0) {
|
|
1119
|
+
canvas.drawRRect(ck.RRectXY(rect, maxR, maxR), strokePaint);
|
|
1120
|
+
} else {
|
|
1121
|
+
canvas.drawRect(rect, strokePaint);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const iconPaint = new ck.Paint();
|
|
1125
|
+
iconPaint.setStyle(ck.PaintStyle.Stroke);
|
|
1126
|
+
iconPaint.setAntiAlias(true);
|
|
1127
|
+
iconPaint.setStrokeWidth(Math.max(1.25, Math.min(w, h) * 0.03));
|
|
1128
|
+
iconPaint.setStrokeCap(ck.StrokeCap.Round);
|
|
1129
|
+
iconPaint.setStrokeJoin(ck.StrokeJoin.Round);
|
|
1130
|
+
iconPaint.setColor(strokeColor);
|
|
1131
|
+
|
|
1132
|
+
const inset = Math.max(10, Math.min(w, h) * 0.18);
|
|
1133
|
+
const iconLeft = x + inset;
|
|
1134
|
+
const iconTop = y + inset;
|
|
1135
|
+
const iconRight = x + w - inset;
|
|
1136
|
+
const iconBottom = y + h - inset;
|
|
1137
|
+
const iconPath = new ck.Path();
|
|
1138
|
+
iconPath.moveTo(iconLeft, iconBottom);
|
|
1139
|
+
iconPath.lineTo(x + w * 0.42, y + h * 0.58);
|
|
1140
|
+
iconPath.lineTo(x + w * 0.58, y + h * 0.72);
|
|
1141
|
+
iconPath.lineTo(iconRight, y + h * 0.42);
|
|
1142
|
+
iconPath.moveTo(iconLeft, iconTop);
|
|
1143
|
+
iconPath.lineTo(iconRight, iconTop);
|
|
1144
|
+
iconPath.lineTo(iconRight, iconBottom);
|
|
1145
|
+
iconPath.lineTo(iconLeft, iconBottom);
|
|
1146
|
+
iconPath.close();
|
|
1147
|
+
canvas.drawPath(iconPath, iconPaint);
|
|
1148
|
+
iconPath.delete();
|
|
1149
|
+
|
|
1150
|
+
const dotPaint = new ck.Paint();
|
|
1151
|
+
dotPaint.setStyle(ck.PaintStyle.Fill);
|
|
1152
|
+
dotPaint.setAntiAlias(true);
|
|
1153
|
+
dotPaint.setColor(strokeColor);
|
|
1154
|
+
canvas.drawCircle(x + w * 0.35, y + h * 0.38, Math.max(3, Math.min(w, h) * 0.05), dotPaint);
|
|
1155
|
+
|
|
1156
|
+
if (emphasizeMissing) {
|
|
1157
|
+
canvas.drawLine(
|
|
1158
|
+
x + inset * 0.9,
|
|
1159
|
+
y + h - inset * 0.9,
|
|
1160
|
+
x + w - inset * 0.9,
|
|
1161
|
+
y + inset * 0.9,
|
|
1162
|
+
iconPaint,
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
strokePaint.delete();
|
|
1167
|
+
iconPaint.delete();
|
|
1168
|
+
dotPaint.delete();
|
|
598
1169
|
}
|
|
599
1170
|
}
|