virtual-tree-canvas 0.3.9 → 0.4.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 +164 -225
- package/docs/inspector-demo.png +0 -0
- package/docs/tree-demo.png +0 -0
- package/package.json +2 -1
- package/src/assets/icons/air.svg +1 -0
- package/src/assets/icons/aircraft.svg +1 -0
- package/src/assets/icons/bus.svg +1 -0
- package/src/assets/icons/control.svg +1 -0
- package/src/assets/icons/damage.svg +1 -0
- package/src/assets/icons/error.svg +1 -0
- package/src/assets/icons/folder.svg +1 -0
- package/src/assets/icons/ground.svg +1 -0
- package/src/assets/icons/inspector-array.svg +1 -0
- package/src/assets/icons/inspector-object.svg +1 -0
- package/src/assets/icons/inspector-value.svg +1 -0
- package/src/assets/icons/munition.svg +1 -0
- package/src/assets/icons/placeholder.svg +1 -0
- package/src/assets/icons/point.svg +1 -0
- package/src/assets/icons/radar.svg +1 -0
- package/src/assets/icons/situation.svg +1 -0
- package/src/assets/icons/space.svg +1 -0
- package/src/assets/icons/subsurface.svg +1 -0
- package/src/assets/icons/surface.svg +1 -0
- package/src/assets/icons/task.svg +1 -0
- package/src/assets/icons/track.svg +1 -0
- package/src/assets/icons/warning.svg +1 -0
- package/src/assets/icons.js +25 -0
- package/src/core/icon-registry.js +173 -495
- package/src/core/tree-worker-client.js +17 -0
- package/src/index.d.ts +13 -0
- package/src/renderers/tree-row-renderer.js +35 -0
- package/src/tree-view-controller.js +9 -2
|
@@ -1,550 +1,228 @@
|
|
|
1
|
+
import { builtinIconUrls } from '../assets/icons.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Icon source registry for the Canvas2D tree renderer.
|
|
5
|
+
*
|
|
6
|
+
* SVG icons remain editable files, but are decoded to an ImageBitmap (or a
|
|
7
|
+
* small canvas fallback) only once for each size, device-pixel-ratio and
|
|
8
|
+
* colour combination. The render hot path is therefore a single drawImage.
|
|
9
|
+
*/
|
|
1
10
|
export class IconRegistry {
|
|
2
|
-
constructor() {
|
|
11
|
+
constructor({ pixelRatio } = {}) {
|
|
3
12
|
this.icons = new Map();
|
|
4
|
-
this.
|
|
13
|
+
this.listeners = new Set();
|
|
14
|
+
this.pixelRatio = pixelRatio ?? devicePixelRatio();
|
|
5
15
|
this.#registerBuiltIns();
|
|
6
16
|
}
|
|
7
17
|
|
|
18
|
+
/** Register an SVG string/URL, an image URL, ImageBitmap, or legacy Canvas draw function. */
|
|
8
19
|
register(name, icon) {
|
|
9
20
|
if (!name) throw new Error('Icon name is required');
|
|
10
21
|
if (this.icons.has(name)) return this.icons.get(name);
|
|
22
|
+
|
|
11
23
|
if (typeof icon === 'function') {
|
|
12
|
-
|
|
13
|
-
|
|
24
|
+
const entry = { kind: 'vector', draw: icon };
|
|
25
|
+
this.icons.set(name, entry);
|
|
26
|
+
return entry;
|
|
14
27
|
}
|
|
28
|
+
|
|
15
29
|
if (typeof icon === 'string') {
|
|
16
|
-
|
|
17
|
-
return this.#
|
|
30
|
+
const source = icon.trim();
|
|
31
|
+
if (source.startsWith('<svg')) return this.#registerSvg(name, { source });
|
|
32
|
+
if (isSvgUrl(source)) return this.#registerSvg(name, { url: source });
|
|
33
|
+
return this.#registerImageUrl(name, source);
|
|
18
34
|
}
|
|
19
|
-
|
|
20
|
-
|
|
35
|
+
|
|
36
|
+
const entry = { kind: 'image', image: icon, loaded: true };
|
|
37
|
+
this.icons.set(name, entry);
|
|
38
|
+
return entry;
|
|
21
39
|
}
|
|
22
40
|
|
|
23
41
|
get(name) {
|
|
24
42
|
return this.icons.get(name) ?? this.icons.get('placeholder');
|
|
25
43
|
}
|
|
26
44
|
|
|
45
|
+
/** Listen for a decoded icon becoming ready, so hosts can repaint once. */
|
|
46
|
+
onChange(listener) {
|
|
47
|
+
this.listeners.add(listener);
|
|
48
|
+
return () => this.listeners.delete(listener);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Starts loading and rasterising a known set of icons ahead of first paint.
|
|
53
|
+
* A colour is part of the cache key because theme colours are baked into SVG
|
|
54
|
+
* assets that use currentColor.
|
|
55
|
+
*/
|
|
56
|
+
prepare({ icons = this.icons.keys(), size = 15, color = '#94a3b8', pixelRatio = this.pixelRatio } = {}) {
|
|
57
|
+
return Promise.all([...icons].map((name) => this.#requestRaster(this.get(name), size, color, pixelRatio)));
|
|
58
|
+
}
|
|
59
|
+
|
|
27
60
|
draw(ctx, name, x, y, size, color) {
|
|
28
61
|
const icon = this.get(name);
|
|
29
62
|
if (!icon) return;
|
|
30
63
|
if (icon.kind === 'vector') {
|
|
64
|
+
// Kept for backwards compatibility. Built-in icons are all SVG assets.
|
|
31
65
|
icon.draw(ctx, x, y, size, color);
|
|
32
66
|
return;
|
|
33
67
|
}
|
|
34
|
-
if (icon.
|
|
35
|
-
ctx.drawImage(icon.image, x, y, size, size);
|
|
68
|
+
if (icon.kind === 'image') {
|
|
69
|
+
if (icon.loaded && icon.image) ctx.drawImage(icon.image, x, y, size, size);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const pixelRatio = canvasPixelRatio(ctx, this.pixelRatio);
|
|
74
|
+
const key = rasterKey(size, color, pixelRatio);
|
|
75
|
+
const raster = icon.rasters.get(key);
|
|
76
|
+
if (raster) {
|
|
77
|
+
ctx.drawImage(raster, x, y, size, size);
|
|
36
78
|
return;
|
|
37
79
|
}
|
|
38
|
-
this
|
|
80
|
+
this.#requestRaster(icon, size, color, pixelRatio);
|
|
39
81
|
}
|
|
40
82
|
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
83
|
+
#registerSvg(name, { source = null, url = null }) {
|
|
84
|
+
const entry = {
|
|
85
|
+
kind: 'svg',
|
|
86
|
+
source,
|
|
87
|
+
url,
|
|
88
|
+
rasters: new Map(),
|
|
89
|
+
pending: new Map(),
|
|
90
|
+
sourcePromise: null,
|
|
91
|
+
error: null,
|
|
92
|
+
};
|
|
44
93
|
this.icons.set(name, entry);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
image.onload = () => {
|
|
49
|
-
entry.image = image;
|
|
50
|
-
entry.loaded = true;
|
|
51
|
-
this.loading.delete(url);
|
|
52
|
-
};
|
|
53
|
-
image.src = url;
|
|
54
|
-
this.loading.set(url, image);
|
|
55
|
-
}
|
|
94
|
+
// Fetching source eagerly moves network and SVG parsing out of scrolling.
|
|
95
|
+
// Avoid file-URL fetch attempts while the package is exercised in Node.
|
|
96
|
+
if (typeof window !== 'undefined') this.#loadSvg(entry);
|
|
56
97
|
return entry;
|
|
57
98
|
}
|
|
58
99
|
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
100
|
+
#registerImageUrl(name, url) {
|
|
101
|
+
const entry = { kind: 'image', image: null, loaded: false, url };
|
|
102
|
+
this.icons.set(name, entry);
|
|
103
|
+
if (typeof Image === 'undefined') return entry;
|
|
104
|
+
const image = new Image();
|
|
105
|
+
image.decoding = 'async';
|
|
106
|
+
image.onload = () => {
|
|
107
|
+
entry.image = image;
|
|
108
|
+
entry.loaded = true;
|
|
109
|
+
this.#notify();
|
|
110
|
+
};
|
|
111
|
+
image.onerror = () => { entry.error = new Error(`Unable to load icon: ${url}`); };
|
|
112
|
+
image.src = url;
|
|
113
|
+
return entry;
|
|
66
114
|
}
|
|
67
115
|
|
|
68
|
-
#
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
this
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
this.register('inspector-value', drawInspectorValue);
|
|
116
|
+
async #requestRaster(icon, size, color, pixelRatio) {
|
|
117
|
+
if (!icon || icon.kind !== 'svg') return null;
|
|
118
|
+
const key = rasterKey(size, color, pixelRatio);
|
|
119
|
+
if (icon.rasters.has(key)) return icon.rasters.get(key);
|
|
120
|
+
if (icon.pending.has(key)) return icon.pending.get(key);
|
|
121
|
+
|
|
122
|
+
const pending = this.#loadSvg(icon)
|
|
123
|
+
.then((source) => source && rasterizeSvg(source, size, color, pixelRatio))
|
|
124
|
+
.then((raster) => {
|
|
125
|
+
if (raster) {
|
|
126
|
+
icon.rasters.set(key, raster);
|
|
127
|
+
this.#notify();
|
|
128
|
+
}
|
|
129
|
+
return raster;
|
|
130
|
+
})
|
|
131
|
+
.catch((error) => {
|
|
132
|
+
icon.error = error;
|
|
133
|
+
return null;
|
|
134
|
+
})
|
|
135
|
+
.finally(() => icon.pending.delete(key));
|
|
136
|
+
icon.pending.set(key, pending);
|
|
137
|
+
return pending;
|
|
91
138
|
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function drawPlaceholder(ctx, x, y, size, color) {
|
|
95
|
-
ctx.strokeStyle = color;
|
|
96
|
-
ctx.strokeRect(x + 2.5, y + 2.5, size - 5, size - 5);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function drawFolder(ctx, x, y, size, color) {
|
|
100
|
-
ctx.fillStyle = color;
|
|
101
|
-
ctx.beginPath();
|
|
102
|
-
ctx.moveTo(x + 1, y + 5);
|
|
103
|
-
ctx.lineTo(x + size * 0.38, y + 5);
|
|
104
|
-
ctx.lineTo(x + size * 0.48, y + 8);
|
|
105
|
-
ctx.lineTo(x + size - 1, y + 8);
|
|
106
|
-
ctx.lineTo(x + size - 1, y + size - 2);
|
|
107
|
-
ctx.lineTo(x + 1, y + size - 2);
|
|
108
|
-
ctx.closePath();
|
|
109
|
-
ctx.fill();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function drawAircraft(ctx, x, y, size, color) {
|
|
113
|
-
const cx = x + size * 0.5;
|
|
114
|
-
ctx.save();
|
|
115
|
-
ctx.strokeStyle = color;
|
|
116
|
-
ctx.fillStyle = color;
|
|
117
|
-
ctx.lineWidth = 1.35;
|
|
118
|
-
ctx.lineCap = 'round';
|
|
119
|
-
ctx.lineJoin = 'round';
|
|
120
|
-
|
|
121
|
-
ctx.beginPath();
|
|
122
|
-
ctx.moveTo(cx, y + 1.5);
|
|
123
|
-
ctx.lineTo(cx + size * 0.08, y + size * 0.56);
|
|
124
|
-
ctx.lineTo(cx + size * 0.05, y + size - 2);
|
|
125
|
-
ctx.lineTo(cx - size * 0.05, y + size - 2);
|
|
126
|
-
ctx.lineTo(cx - size * 0.08, y + size * 0.56);
|
|
127
|
-
ctx.closePath();
|
|
128
|
-
ctx.fill();
|
|
129
|
-
|
|
130
|
-
ctx.beginPath();
|
|
131
|
-
ctx.moveTo(cx - size * 0.08, y + size * 0.54);
|
|
132
|
-
ctx.lineTo(x + 1.5, y + size * 0.76);
|
|
133
|
-
ctx.lineTo(cx - size * 0.03, y + size * 0.68);
|
|
134
|
-
ctx.lineTo(cx + size * 0.03, y + size * 0.68);
|
|
135
|
-
ctx.lineTo(x + size - 1.5, y + size * 0.76);
|
|
136
|
-
ctx.lineTo(cx + size * 0.08, y + size * 0.54);
|
|
137
|
-
ctx.stroke();
|
|
138
|
-
|
|
139
|
-
ctx.beginPath();
|
|
140
|
-
ctx.moveTo(cx - size * 0.05, y + size * 0.82);
|
|
141
|
-
ctx.lineTo(x + size * 0.25, y + size - 1.5);
|
|
142
|
-
ctx.moveTo(cx + size * 0.05, y + size * 0.82);
|
|
143
|
-
ctx.lineTo(x + size * 0.75, y + size - 1.5);
|
|
144
|
-
ctx.stroke();
|
|
145
|
-
ctx.restore();
|
|
146
|
-
}
|
|
147
139
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
ctx.stroke();
|
|
168
|
-
|
|
169
|
-
ctx.beginPath();
|
|
170
|
-
ctx.moveTo(x + size * 0.62, y + size * 0.6);
|
|
171
|
-
ctx.lineTo(x + size - 4.5, y + size * 0.6);
|
|
172
|
-
ctx.moveTo(x + 5, y + size * 0.78);
|
|
173
|
-
ctx.lineTo(x + size - 4.5, y + size * 0.78);
|
|
174
|
-
ctx.stroke();
|
|
175
|
-
ctx.restore();
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function roundIconRect(ctx, x, y, width, height, radius) {
|
|
179
|
-
ctx.beginPath();
|
|
180
|
-
if (typeof ctx.roundRect === 'function') {
|
|
181
|
-
ctx.roundRect(x, y, width, height, radius);
|
|
182
|
-
return;
|
|
140
|
+
#loadSvg(icon) {
|
|
141
|
+
if (icon.source) return Promise.resolve(icon.source);
|
|
142
|
+
if (icon.sourcePromise) return icon.sourcePromise;
|
|
143
|
+
if (!icon.url || typeof fetch !== 'function') return Promise.resolve(null);
|
|
144
|
+
|
|
145
|
+
icon.sourcePromise = fetch(icon.url)
|
|
146
|
+
.then((response) => {
|
|
147
|
+
if (!response.ok) throw new Error(`Unable to load SVG icon: ${icon.url}`);
|
|
148
|
+
return response.text();
|
|
149
|
+
})
|
|
150
|
+
.then((source) => {
|
|
151
|
+
icon.source = source;
|
|
152
|
+
return source;
|
|
153
|
+
})
|
|
154
|
+
.catch((error) => {
|
|
155
|
+
icon.error = error;
|
|
156
|
+
return null;
|
|
157
|
+
});
|
|
158
|
+
return icon.sourcePromise;
|
|
183
159
|
}
|
|
184
|
-
ctx.moveTo(x + radius, y);
|
|
185
|
-
ctx.lineTo(x + width - radius, y);
|
|
186
|
-
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
187
|
-
ctx.lineTo(x + width, y + height - radius);
|
|
188
|
-
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
189
|
-
ctx.lineTo(x + radius, y + height);
|
|
190
|
-
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
191
|
-
ctx.lineTo(x, y + radius);
|
|
192
|
-
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function drawRadar(ctx, x, y, size, color) {
|
|
196
|
-
ctx.strokeStyle = color;
|
|
197
|
-
ctx.lineWidth = 1.5;
|
|
198
|
-
ctx.beginPath();
|
|
199
|
-
ctx.arc(x + size / 2, y + size / 2, size * 0.34, -0.4, Math.PI * 1.4);
|
|
200
|
-
ctx.moveTo(x + size / 2, y + size / 2);
|
|
201
|
-
ctx.lineTo(x + size * 0.84, y + size * 0.28);
|
|
202
|
-
ctx.stroke();
|
|
203
|
-
ctx.fillStyle = color;
|
|
204
|
-
ctx.beginPath();
|
|
205
|
-
ctx.arc(x + size / 2, y + size / 2, 2, 0, Math.PI * 2);
|
|
206
|
-
ctx.fill();
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function drawWarning(ctx, x, y, size, color) {
|
|
210
|
-
ctx.fillStyle = color;
|
|
211
|
-
ctx.beginPath();
|
|
212
|
-
ctx.moveTo(x + size / 2, y + 1);
|
|
213
|
-
ctx.lineTo(x + size - 1, y + size - 2);
|
|
214
|
-
ctx.lineTo(x + 1, y + size - 2);
|
|
215
|
-
ctx.closePath();
|
|
216
|
-
ctx.fill();
|
|
217
|
-
}
|
|
218
160
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
ctx.beginPath();
|
|
222
|
-
ctx.arc(x + size / 2, y + size / 2, size * 0.42, 0, Math.PI * 2);
|
|
223
|
-
ctx.fill();
|
|
224
|
-
ctx.strokeStyle = '#ffffff';
|
|
225
|
-
ctx.beginPath();
|
|
226
|
-
ctx.moveTo(x + 5, y + 5);
|
|
227
|
-
ctx.lineTo(x + size - 5, y + size - 5);
|
|
228
|
-
ctx.moveTo(x + size - 5, y + 5);
|
|
229
|
-
ctx.lineTo(x + 5, y + size - 5);
|
|
230
|
-
ctx.stroke();
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function drawBus(ctx, x, y, size, color) {
|
|
234
|
-
const left = x + size * 0.22;
|
|
235
|
-
const right = x + size * 0.82;
|
|
236
|
-
const ys = [y + size * 0.32, y + size * 0.5, y + size * 0.68];
|
|
237
|
-
ctx.strokeStyle = color;
|
|
238
|
-
ctx.fillStyle = color;
|
|
239
|
-
ctx.lineWidth = 1.25;
|
|
240
|
-
|
|
241
|
-
ctx.beginPath();
|
|
242
|
-
ctx.moveTo(left, ys[0]);
|
|
243
|
-
ctx.lineTo(right, ys[0]);
|
|
244
|
-
ctx.moveTo(left, ys[1]);
|
|
245
|
-
ctx.lineTo(right, ys[1]);
|
|
246
|
-
ctx.moveTo(left, ys[2]);
|
|
247
|
-
ctx.lineTo(right, ys[2]);
|
|
248
|
-
ctx.moveTo(left, ys[0]);
|
|
249
|
-
ctx.lineTo(left, ys[2]);
|
|
250
|
-
ctx.stroke();
|
|
251
|
-
|
|
252
|
-
for (const cy of ys) {
|
|
253
|
-
ctx.beginPath();
|
|
254
|
-
ctx.arc(left, cy, size * 0.075, 0, Math.PI * 2);
|
|
255
|
-
ctx.arc(right, cy, size * 0.075, 0, Math.PI * 2);
|
|
256
|
-
ctx.fill();
|
|
161
|
+
#notify() {
|
|
162
|
+
for (const listener of this.listeners) listener();
|
|
257
163
|
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function drawTrack(ctx, x, y, size, color) {
|
|
261
|
-
ctx.strokeStyle = color;
|
|
262
|
-
ctx.lineWidth = 1.5;
|
|
263
|
-
ctx.beginPath();
|
|
264
|
-
ctx.arc(x + size / 2, y + size / 2, size * 0.34, 0, Math.PI * 2);
|
|
265
|
-
ctx.moveTo(x + size / 2, y + 1);
|
|
266
|
-
ctx.lineTo(x + size / 2, y + size - 1);
|
|
267
|
-
ctx.moveTo(x + 1, y + size / 2);
|
|
268
|
-
ctx.lineTo(x + size - 1, y + size / 2);
|
|
269
|
-
ctx.stroke();
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function drawPoint(ctx, x, y, size, color) {
|
|
273
|
-
const cx = x + size / 2;
|
|
274
|
-
const cy = y + size / 2;
|
|
275
|
-
ctx.strokeStyle = color;
|
|
276
|
-
ctx.fillStyle = color;
|
|
277
|
-
ctx.lineWidth = 1.4;
|
|
278
|
-
ctx.beginPath();
|
|
279
|
-
ctx.arc(cx, cy, size * 0.22, 0, Math.PI * 2);
|
|
280
|
-
ctx.fill();
|
|
281
|
-
ctx.beginPath();
|
|
282
|
-
ctx.arc(cx, cy, size * 0.42, 0, Math.PI * 2);
|
|
283
|
-
ctx.stroke();
|
|
284
|
-
ctx.beginPath();
|
|
285
|
-
ctx.moveTo(cx, y + 1);
|
|
286
|
-
ctx.lineTo(cx, y + size * 0.22);
|
|
287
|
-
ctx.moveTo(cx, y + size * 0.78);
|
|
288
|
-
ctx.lineTo(cx, y + size - 1);
|
|
289
|
-
ctx.moveTo(x + 1, cy);
|
|
290
|
-
ctx.lineTo(x + size * 0.22, cy);
|
|
291
|
-
ctx.moveTo(x + size * 0.78, cy);
|
|
292
|
-
ctx.lineTo(x + size - 1, cy);
|
|
293
|
-
ctx.stroke();
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function drawMunition(ctx, x, y, size, color) {
|
|
297
|
-
const cx = x + size * 0.5;
|
|
298
|
-
const bodyTop = y + size * 0.25;
|
|
299
|
-
const bodyBottom = y + size * 0.76;
|
|
300
|
-
ctx.fillStyle = color;
|
|
301
|
-
|
|
302
|
-
ctx.beginPath();
|
|
303
|
-
ctx.moveTo(cx, y + 1);
|
|
304
|
-
ctx.lineTo(cx + size * 0.17, bodyTop);
|
|
305
|
-
ctx.lineTo(cx - size * 0.17, bodyTop);
|
|
306
|
-
ctx.closePath();
|
|
307
|
-
ctx.fill();
|
|
308
|
-
|
|
309
|
-
ctx.fillRect(cx - size * 0.11, bodyTop, size * 0.22, bodyBottom - bodyTop);
|
|
310
|
-
|
|
311
|
-
ctx.beginPath();
|
|
312
|
-
ctx.moveTo(cx - size * 0.11, y + size * 0.63);
|
|
313
|
-
ctx.lineTo(x + 1, y + size - 2);
|
|
314
|
-
ctx.lineTo(cx - size * 0.11, y + size * 0.82);
|
|
315
|
-
ctx.closePath();
|
|
316
|
-
ctx.fill();
|
|
317
|
-
|
|
318
|
-
ctx.beginPath();
|
|
319
|
-
ctx.moveTo(cx + size * 0.11, y + size * 0.63);
|
|
320
|
-
ctx.lineTo(x + size - 1, y + size - 2);
|
|
321
|
-
ctx.lineTo(cx + size * 0.11, y + size * 0.82);
|
|
322
|
-
ctx.closePath();
|
|
323
|
-
ctx.fill();
|
|
324
|
-
|
|
325
|
-
ctx.strokeStyle = 'rgba(255,255,255,.35)';
|
|
326
|
-
ctx.lineWidth = 1;
|
|
327
|
-
ctx.beginPath();
|
|
328
|
-
ctx.moveTo(cx, bodyTop + 1);
|
|
329
|
-
ctx.lineTo(cx, bodyBottom - 1);
|
|
330
|
-
ctx.stroke();
|
|
331
|
-
|
|
332
|
-
ctx.strokeStyle = color;
|
|
333
|
-
ctx.beginPath();
|
|
334
|
-
ctx.moveTo(cx - size * 0.12, bodyBottom);
|
|
335
|
-
ctx.lineTo(cx, y + size - 1);
|
|
336
|
-
ctx.lineTo(cx + size * 0.12, bodyBottom);
|
|
337
|
-
ctx.stroke();
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function drawGroundVehicle(ctx, x, y, size, color) {
|
|
341
|
-
ctx.fillStyle = color;
|
|
342
|
-
const bodyY = y + size * 0.42;
|
|
343
|
-
ctx.fillRect(x + size * 0.16, bodyY, size * 0.68, size * 0.26);
|
|
344
|
-
ctx.fillRect(x + size * 0.34, y + size * 0.26, size * 0.28, size * 0.2);
|
|
345
|
-
ctx.fillRect(x + size * 0.62, y + size * 0.34, size * 0.3, size * 0.06);
|
|
346
|
-
ctx.beginPath();
|
|
347
|
-
ctx.arc(x + size * 0.28, y + size * 0.76, size * 0.09, 0, Math.PI * 2);
|
|
348
|
-
ctx.arc(x + size * 0.5, y + size * 0.76, size * 0.09, 0, Math.PI * 2);
|
|
349
|
-
ctx.arc(x + size * 0.72, y + size * 0.76, size * 0.09, 0, Math.PI * 2);
|
|
350
|
-
ctx.fill();
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function drawSurfaceVehicle(ctx, x, y, size, color) {
|
|
354
|
-
ctx.fillStyle = color;
|
|
355
|
-
ctx.beginPath();
|
|
356
|
-
ctx.moveTo(x + 1, y + size * 0.58);
|
|
357
|
-
ctx.lineTo(x + size * 0.82, y + size * 0.58);
|
|
358
|
-
ctx.lineTo(x + size - 1, y + size * 0.72);
|
|
359
|
-
ctx.lineTo(x + size * 0.18, y + size * 0.82);
|
|
360
|
-
ctx.closePath();
|
|
361
|
-
ctx.fill();
|
|
362
164
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
ctx.lineWidth = 1.2;
|
|
367
|
-
ctx.beginPath();
|
|
368
|
-
ctx.moveTo(x + size * 0.18, y + size * 0.88);
|
|
369
|
-
ctx.quadraticCurveTo(x + size * 0.34, y + size * 0.78, x + size * 0.5, y + size * 0.88);
|
|
370
|
-
ctx.quadraticCurveTo(x + size * 0.66, y + size * 0.98, x + size * 0.82, y + size * 0.88);
|
|
371
|
-
ctx.stroke();
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function drawSubsurfaceVehicle(ctx, x, y, size, color) {
|
|
375
|
-
ctx.fillStyle = color;
|
|
376
|
-
ctx.beginPath();
|
|
377
|
-
ctx.ellipse(x + size * 0.5, y + size * 0.58, size * 0.38, size * 0.18, 0, 0, Math.PI * 2);
|
|
378
|
-
ctx.fill();
|
|
379
|
-
ctx.fillRect(x + size * 0.44, y + size * 0.28, size * 0.12, size * 0.18);
|
|
380
|
-
ctx.fillRect(x + size * 0.38, y + size * 0.26, size * 0.24, size * 0.06);
|
|
381
|
-
ctx.strokeStyle = color;
|
|
382
|
-
ctx.lineWidth = 1.2;
|
|
383
|
-
ctx.beginPath();
|
|
384
|
-
ctx.moveTo(x + size * 0.16, y + size * 0.86);
|
|
385
|
-
ctx.quadraticCurveTo(x + size * 0.32, y + size * 0.78, x + size * 0.5, y + size * 0.86);
|
|
386
|
-
ctx.quadraticCurveTo(x + size * 0.68, y + size * 0.94, x + size * 0.84, y + size * 0.86);
|
|
387
|
-
ctx.stroke();
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function drawSpaceVehicle(ctx, x, y, size, color) {
|
|
391
|
-
const cx = x + size * 0.5;
|
|
392
|
-
const cy = y + size * 0.5;
|
|
393
|
-
ctx.strokeStyle = color;
|
|
394
|
-
ctx.fillStyle = color;
|
|
395
|
-
ctx.lineWidth = 1.2;
|
|
396
|
-
ctx.beginPath();
|
|
397
|
-
ctx.arc(cx, cy, size * 0.16, 0, Math.PI * 2);
|
|
398
|
-
ctx.fill();
|
|
399
|
-
ctx.strokeRect(x + size * 0.12, y + size * 0.32, size * 0.24, size * 0.36);
|
|
400
|
-
ctx.strokeRect(x + size * 0.64, y + size * 0.32, size * 0.24, size * 0.36);
|
|
401
|
-
ctx.beginPath();
|
|
402
|
-
ctx.moveTo(x + size * 0.36, cy);
|
|
403
|
-
ctx.lineTo(x + size * 0.64, cy);
|
|
404
|
-
ctx.stroke();
|
|
405
|
-
ctx.beginPath();
|
|
406
|
-
ctx.ellipse(cx, cy, size * 0.42, size * 0.18, -0.45, 0, Math.PI * 2);
|
|
407
|
-
ctx.stroke();
|
|
165
|
+
#registerBuiltIns() {
|
|
166
|
+
for (const [name, url] of Object.entries(builtinIconUrls)) this.register(name, url);
|
|
167
|
+
}
|
|
408
168
|
}
|
|
409
169
|
|
|
410
|
-
function
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
ctx.lineTo(x + size * 0.42, y + size * 0.32);
|
|
428
|
-
ctx.stroke();
|
|
429
|
-
|
|
430
|
-
ctx.beginPath();
|
|
431
|
-
ctx.arc(x + size * 0.4, y + size * 0.28, size * 0.13, 0, Math.PI * 2);
|
|
432
|
-
ctx.fill();
|
|
170
|
+
async function rasterizeSvg(source, size, color, pixelRatio) {
|
|
171
|
+
const pixelSize = Math.max(1, Math.round(size * pixelRatio));
|
|
172
|
+
const svg = source.replaceAll('currentColor', color);
|
|
173
|
+
const blob = new Blob([svg], { type: 'image/svg+xml' });
|
|
174
|
+
|
|
175
|
+
if (typeof createImageBitmap === 'function') {
|
|
176
|
+
try {
|
|
177
|
+
return await createImageBitmap(blob, {
|
|
178
|
+
resizeWidth: pixelSize,
|
|
179
|
+
resizeHeight: pixelSize,
|
|
180
|
+
resizeQuality: 'high',
|
|
181
|
+
});
|
|
182
|
+
} catch {
|
|
183
|
+
// Safari and older Chromium versions can reject SVG blobs here. The
|
|
184
|
+
// canvas fallback below still creates one fixed-size raster per key.
|
|
185
|
+
}
|
|
186
|
+
}
|
|
433
187
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
188
|
+
if (typeof Image === 'undefined' || typeof document === 'undefined' || typeof URL === 'undefined') return null;
|
|
189
|
+
const url = URL.createObjectURL(blob);
|
|
190
|
+
try {
|
|
191
|
+
const image = await loadImage(url);
|
|
192
|
+
const canvas = document.createElement('canvas');
|
|
193
|
+
canvas.width = pixelSize;
|
|
194
|
+
canvas.height = pixelSize;
|
|
195
|
+
const ctx = canvas.getContext('2d');
|
|
196
|
+
ctx?.drawImage(image, 0, 0, pixelSize, pixelSize);
|
|
197
|
+
return canvas;
|
|
198
|
+
} finally {
|
|
199
|
+
URL.revokeObjectURL(url);
|
|
200
|
+
}
|
|
438
201
|
}
|
|
439
202
|
|
|
440
|
-
function
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
ctx.arc(cx, cy, size * 0.42, 0, Math.PI * 2);
|
|
449
|
-
ctx.moveTo(cx - size * 0.42, cy);
|
|
450
|
-
ctx.lineTo(cx + size * 0.42, cy);
|
|
451
|
-
ctx.moveTo(cx, cy - size * 0.42);
|
|
452
|
-
ctx.lineTo(cx, cy + size * 0.42);
|
|
453
|
-
ctx.stroke();
|
|
454
|
-
|
|
455
|
-
ctx.globalAlpha = 0.55;
|
|
456
|
-
ctx.beginPath();
|
|
457
|
-
ctx.arc(cx, cy, size * 0.25, 0, Math.PI * 2);
|
|
458
|
-
ctx.stroke();
|
|
459
|
-
ctx.globalAlpha = 1;
|
|
460
|
-
|
|
461
|
-
ctx.beginPath();
|
|
462
|
-
ctx.moveTo(cx, cy);
|
|
463
|
-
ctx.lineTo(x + size * 0.82, y + size * 0.26);
|
|
464
|
-
ctx.stroke();
|
|
465
|
-
|
|
466
|
-
ctx.beginPath();
|
|
467
|
-
ctx.arc(x + size * 0.68, y + size * 0.38, size * 0.055, 0, Math.PI * 2);
|
|
468
|
-
ctx.arc(x + size * 0.36, y + size * 0.62, size * 0.045, 0, Math.PI * 2);
|
|
469
|
-
ctx.fill();
|
|
203
|
+
function loadImage(url) {
|
|
204
|
+
return new Promise((resolve, reject) => {
|
|
205
|
+
const image = new Image();
|
|
206
|
+
image.decoding = 'async';
|
|
207
|
+
image.onload = () => resolve(image);
|
|
208
|
+
image.onerror = () => reject(new Error(`Unable to decode SVG icon: ${url}`));
|
|
209
|
+
image.src = url;
|
|
210
|
+
});
|
|
470
211
|
}
|
|
471
212
|
|
|
472
|
-
function
|
|
473
|
-
|
|
474
|
-
const cy = y + size * 0.52;
|
|
475
|
-
const r = size * 0.42;
|
|
476
|
-
|
|
477
|
-
ctx.beginPath();
|
|
478
|
-
ctx.moveTo(cx, y + 1);
|
|
479
|
-
ctx.lineTo(cx + r * 0.28, cy - r * 0.28);
|
|
480
|
-
ctx.lineTo(x + size - 1, y + size * 0.2);
|
|
481
|
-
ctx.lineTo(cx + r * 0.55, cy + r * 0.05);
|
|
482
|
-
ctx.lineTo(x + size * 0.92, y + size * 0.76);
|
|
483
|
-
ctx.lineTo(cx + r * 0.22, cy + r * 0.32);
|
|
484
|
-
ctx.lineTo(cx + r * 0.08, y + size - 1);
|
|
485
|
-
ctx.lineTo(cx - r * 0.18, cy + r * 0.34);
|
|
486
|
-
ctx.lineTo(x + size * 0.14, y + size * 0.86);
|
|
487
|
-
ctx.lineTo(cx - r * 0.42, cy + r * 0.12);
|
|
488
|
-
ctx.lineTo(x + 1, y + size * 0.38);
|
|
489
|
-
ctx.lineTo(cx - r * 0.3, cy - r * 0.18);
|
|
490
|
-
ctx.closePath();
|
|
491
|
-
ctx.fillStyle = color;
|
|
492
|
-
ctx.fill();
|
|
493
|
-
|
|
494
|
-
ctx.fillStyle = '#fff7ed';
|
|
495
|
-
ctx.beginPath();
|
|
496
|
-
ctx.arc(cx, cy, size * 0.13, 0, Math.PI * 2);
|
|
497
|
-
ctx.fill();
|
|
498
|
-
|
|
499
|
-
ctx.strokeStyle = color;
|
|
500
|
-
ctx.lineWidth = 1.1;
|
|
501
|
-
ctx.beginPath();
|
|
502
|
-
ctx.moveTo(x + size * 0.08, y + size * 0.08);
|
|
503
|
-
ctx.lineTo(x + size * 0.2, y + size * 0.2);
|
|
504
|
-
ctx.moveTo(x + size * 0.84, y + size * 0.88);
|
|
505
|
-
ctx.lineTo(x + size * 0.94, y + size * 0.98);
|
|
506
|
-
ctx.moveTo(x + size * 0.9, y + size * 0.06);
|
|
507
|
-
ctx.lineTo(x + size * 0.82, y + size * 0.18);
|
|
508
|
-
ctx.stroke();
|
|
213
|
+
function isSvgUrl(value) {
|
|
214
|
+
return /^data:image\/svg\+xml/i.test(value) || /\.svg(?:[?#].*)?$/i.test(value);
|
|
509
215
|
}
|
|
510
216
|
|
|
511
|
-
function
|
|
512
|
-
|
|
513
|
-
ctx.lineWidth = 1.2;
|
|
514
|
-
const left = x + size * 0.24;
|
|
515
|
-
const top = y + size * 0.22;
|
|
516
|
-
const width = size * 0.52;
|
|
517
|
-
const height = size * 0.56;
|
|
518
|
-
ctx.strokeRect(left, top, width, height);
|
|
519
|
-
ctx.beginPath();
|
|
520
|
-
ctx.moveTo(left + width * 0.28, top);
|
|
521
|
-
ctx.lineTo(left + width * 0.28, top + height);
|
|
522
|
-
ctx.moveTo(left + width * 0.72, top);
|
|
523
|
-
ctx.lineTo(left + width * 0.72, top + height);
|
|
524
|
-
ctx.stroke();
|
|
217
|
+
function rasterKey(size, color, pixelRatio) {
|
|
218
|
+
return `${size}|${pixelRatio}|${color}`;
|
|
525
219
|
}
|
|
526
220
|
|
|
527
|
-
function
|
|
528
|
-
|
|
529
|
-
ctx.lineWidth = 1.2;
|
|
530
|
-
const cx = x + size * 0.5;
|
|
531
|
-
const cy = y + size * 0.5;
|
|
532
|
-
ctx.beginPath();
|
|
533
|
-
ctx.arc(cx - size * 0.18, cy, size * 0.18, 0, Math.PI * 2);
|
|
534
|
-
ctx.arc(cx + size * 0.18, cy, size * 0.18, 0, Math.PI * 2);
|
|
535
|
-
ctx.stroke();
|
|
221
|
+
function devicePixelRatio() {
|
|
222
|
+
return Math.max(1, globalThis.devicePixelRatio || 1);
|
|
536
223
|
}
|
|
537
224
|
|
|
538
|
-
function
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
ctx.strokeStyle = color;
|
|
542
|
-
ctx.fillStyle = color;
|
|
543
|
-
ctx.lineWidth = 1.25;
|
|
544
|
-
ctx.beginPath();
|
|
545
|
-
ctx.arc(cx, cy, size * 0.3, 0, Math.PI * 2);
|
|
546
|
-
ctx.stroke();
|
|
547
|
-
ctx.beginPath();
|
|
548
|
-
ctx.arc(cx, cy, size * 0.11, 0, Math.PI * 2);
|
|
549
|
-
ctx.fill();
|
|
225
|
+
function canvasPixelRatio(ctx, fallback) {
|
|
226
|
+
const transform = ctx.getTransform?.();
|
|
227
|
+
return Math.max(1, Math.abs(transform?.a) || fallback || 1);
|
|
550
228
|
}
|