@carto/api-client 0.5.32-alpha.77957dd.124 → 0.5.32-alpha.b3291bf.129
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/CHANGELOG.md +1 -1
- package/build/api-client.cjs +86 -52
- package/build/api-client.cjs.map +1 -1
- package/build/api-client.js +86 -48
- package/build/api-client.js.map +1 -1
- package/package.json +1 -1
- package/src/fetch-map/parse-map.ts +15 -3
- package/src/fetch-map/pattern-atlas.ts +245 -20
- package/src/fetch-map/pattern-atlas-baked.ts +0 -56
- package/src/fetch-map/pattern-atlas-runtime.ts +0 -134
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"homepage": "https://github.com/CartoDB/carto-api-client#readme",
|
|
9
9
|
"author": "Don McCurdy <donmccurdy@carto.com>",
|
|
10
10
|
"packageManager": "yarn@4.3.1",
|
|
11
|
-
"version": "0.5.32-alpha.
|
|
11
|
+
"version": "0.5.32-alpha.b3291bf.129",
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"publishConfig": {
|
|
14
14
|
"access": "public"
|
|
@@ -34,7 +34,12 @@ import type {
|
|
|
34
34
|
VisualChannelField,
|
|
35
35
|
} from './types.js';
|
|
36
36
|
import {isRemoteCalculationSupported} from './utils.js';
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
getPatternAtlas,
|
|
39
|
+
getPatternAtlasMapping,
|
|
40
|
+
getPatternScaleAdjustment,
|
|
41
|
+
getPatternTextureParameters,
|
|
42
|
+
} from './pattern-atlas.js';
|
|
38
43
|
import {
|
|
39
44
|
getRasterTileLayerStylePropsRgb,
|
|
40
45
|
getRasterTileLayerStylePropsScaledBand,
|
|
@@ -632,9 +637,16 @@ function createChannelProps(
|
|
|
632
637
|
|
|
633
638
|
if (visConfig.filled && visConfig.fillPatternEnabled) {
|
|
634
639
|
result.fillPatternAtlas = getPatternAtlas();
|
|
635
|
-
result.fillPatternMapping =
|
|
640
|
+
result.fillPatternMapping = getPatternAtlasMapping();
|
|
636
641
|
result.fillPatternMask = true;
|
|
637
|
-
|
|
642
|
+
const textureParameters = getPatternTextureParameters();
|
|
643
|
+
if (textureParameters) {
|
|
644
|
+
// Merged into the atlas texture's sampler by deck's image-prop transform.
|
|
645
|
+
result.textureParameters = textureParameters;
|
|
646
|
+
}
|
|
647
|
+
// Scale compensated for the atlas cell size — see getPatternScaleAdjustment.
|
|
648
|
+
result.getFillPatternScale =
|
|
649
|
+
(visConfig.fillPatternSize ?? 1) * getPatternScaleAdjustment();
|
|
638
650
|
// Flat prop for legend consumers (fallback when there is no by-column scale).
|
|
639
651
|
result.fillPattern = visConfig.fillPattern;
|
|
640
652
|
|
|
@@ -1,25 +1,250 @@
|
|
|
1
1
|
// Fill-pattern atlas — internal to carto-api-client.
|
|
2
2
|
//
|
|
3
|
+
// The patterns live as individual, developer-editable tiles (src/fetch-map/patterns/
|
|
4
|
+
// *.png — any resolution; ideally anti-aliased exports from the Design vector master),
|
|
5
|
+
// inlined by tsup's `dataurl` loader and composited into a sprite sheet on a canvas the
|
|
6
|
+
// first time a pattern is needed. Each cell is surrounded by a gutter filled with the
|
|
7
|
+
// tile's own wrapped content, so linear sampling stays seamless at repeat boundaries
|
|
8
|
+
// and never bleeds a neighboring cell. High-res sources are smoothly downscaled;
|
|
9
|
+
// lower-res (pixel-art) sources are upscaled nearest-neighbor.
|
|
10
|
+
//
|
|
3
11
|
// parse-map sets `result.fillPatternAtlas = getPatternAtlas()`. deck.gl's
|
|
4
|
-
// `fillPatternAtlas` prop is async
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// and never sees this choice.
|
|
12
|
+
// `fillPatternAtlas` prop is async — but the Promise must resolve to a decoded image,
|
|
13
|
+
// never a data-URL string: deck URL-loads string prop values, while a promise-resolved
|
|
14
|
+
// string goes straight to texture creation, where luma.gl rejects it.
|
|
8
15
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
// Debug knobs (set in the browser console, then reload the map):
|
|
17
|
+
// globalThis.__CARTO_PATTERN_CELL_SIZE__ = 64|128|256 // atlas cell px, default 128
|
|
18
|
+
// globalThis.__CARTO_PATTERN_TEXTURE_PARAMS__ = {magFilter: 'nearest', ...}
|
|
19
|
+
// // merged into the atlas texture's sampler via deck's `textureParameters`;
|
|
20
|
+
// // unset -> deck defaults (linear).
|
|
21
|
+
|
|
22
|
+
import hlinesLarge from './patterns/hlines-large.png';
|
|
23
|
+
import hlinesMedium from './patterns/hlines-medium.png';
|
|
24
|
+
import hlinesSmall from './patterns/hlines-small.png';
|
|
25
|
+
import vlinesLarge from './patterns/vlines-large.png';
|
|
26
|
+
import vlinesMedium from './patterns/vlines-medium.png';
|
|
27
|
+
import vlinesSmall from './patterns/vlines-small.png';
|
|
28
|
+
import diagLeftLarge from './patterns/diag-left-large.png';
|
|
29
|
+
import diagLeftMedium from './patterns/diag-left-medium.png';
|
|
30
|
+
import diagLeftSmall from './patterns/diag-left-small.png';
|
|
31
|
+
import diagRightLarge from './patterns/diag-right-large.png';
|
|
32
|
+
import diagRightMedium from './patterns/diag-right-medium.png';
|
|
33
|
+
import diagRightSmall from './patterns/diag-right-small.png';
|
|
34
|
+
import crossHatchLarge from './patterns/cross-hatch-large.png';
|
|
35
|
+
import crossHatchMedium from './patterns/cross-hatch-medium.png';
|
|
36
|
+
import crossHatchSmall from './patterns/cross-hatch-small.png';
|
|
37
|
+
import dotsLarge from './patterns/dots-large.png';
|
|
38
|
+
import dotsMedium from './patterns/dots-medium.png';
|
|
39
|
+
import dotsSmall from './patterns/dots-small.png';
|
|
40
|
+
import checkerLarge from './patterns/checker-large.png';
|
|
41
|
+
import checkerMedium from './patterns/checker-medium.png';
|
|
42
|
+
import checkerSmall from './patterns/checker-small.png';
|
|
43
|
+
import solid from './patterns/solid.png';
|
|
44
|
+
|
|
45
|
+
const DEFAULT_CELL_SIZE = 128;
|
|
46
|
+
// px of the authored pattern art; also the reference for on-screen pattern size.
|
|
47
|
+
const SOURCE_TILE_SIZE = 64;
|
|
48
|
+
|
|
49
|
+
type PatternDebugGlobals = {
|
|
50
|
+
__CARTO_PATTERN_CELL_SIZE__?: number;
|
|
51
|
+
__CARTO_PATTERN_TEXTURE_PARAMS__?: Record<string, unknown>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type PatternAtlasFrame = {
|
|
55
|
+
x: number;
|
|
56
|
+
y: number;
|
|
57
|
+
width: number;
|
|
58
|
+
height: number;
|
|
59
|
+
mask: boolean;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Atlas grid: rows follow PATTERN_ROWS, columns are density sparse->dense
|
|
63
|
+
// [large, medium, small]; last row holds `none` (transparent) and `solid`.
|
|
64
|
+
const PATTERN_ROWS = [
|
|
65
|
+
'hlines',
|
|
66
|
+
'vlines',
|
|
67
|
+
'diag-left',
|
|
68
|
+
'diag-right',
|
|
69
|
+
'cross-hatch',
|
|
70
|
+
'dots',
|
|
71
|
+
'checker',
|
|
72
|
+
] as const;
|
|
73
|
+
const DENSITY_COLUMNS = ['large', 'medium', 'small'] as const;
|
|
74
|
+
|
|
75
|
+
// atlas key -> inlined data URL of its editable source tile
|
|
76
|
+
const CELL_URLS: Record<string, string> = {
|
|
77
|
+
'hlines-large': hlinesLarge,
|
|
78
|
+
'hlines-medium': hlinesMedium,
|
|
79
|
+
'hlines-small': hlinesSmall,
|
|
80
|
+
'vlines-large': vlinesLarge,
|
|
81
|
+
'vlines-medium': vlinesMedium,
|
|
82
|
+
'vlines-small': vlinesSmall,
|
|
83
|
+
'diag-left-large': diagLeftLarge,
|
|
84
|
+
'diag-left-medium': diagLeftMedium,
|
|
85
|
+
'diag-left-small': diagLeftSmall,
|
|
86
|
+
'diag-right-large': diagRightLarge,
|
|
87
|
+
'diag-right-medium': diagRightMedium,
|
|
88
|
+
'diag-right-small': diagRightSmall,
|
|
89
|
+
'cross-hatch-large': crossHatchLarge,
|
|
90
|
+
'cross-hatch-medium': crossHatchMedium,
|
|
91
|
+
'cross-hatch-small': crossHatchSmall,
|
|
92
|
+
'dots-large': dotsLarge,
|
|
93
|
+
'dots-medium': dotsMedium,
|
|
94
|
+
'dots-small': dotsSmall,
|
|
95
|
+
'checker-large': checkerLarge,
|
|
96
|
+
'checker-medium': checkerMedium,
|
|
97
|
+
'checker-small': checkerSmall,
|
|
98
|
+
solid,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function debugGlobals(): PatternDebugGlobals {
|
|
102
|
+
return globalThis as PatternDebugGlobals;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function getPatternCellSize(): number {
|
|
106
|
+
const cell = debugGlobals().__CARTO_PATTERN_CELL_SIZE__;
|
|
107
|
+
return typeof cell === 'number' && cell > 0 ? cell : DEFAULT_CELL_SIZE;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Sampler overrides for the atlas texture; undefined keeps deck's defaults (linear). */
|
|
111
|
+
export function getPatternTextureParameters():
|
|
112
|
+
| Record<string, unknown>
|
|
113
|
+
| undefined {
|
|
114
|
+
const params = debugGlobals().__CARTO_PATTERN_TEXTURE_PARAMS__;
|
|
115
|
+
return params && typeof params === 'object' ? params : undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// deck's fill-pattern shader sizes the on-screen repeat proportionally to the mapping
|
|
119
|
+
// frame's texel size (scale = FILL_UV_SCALE * getFillPatternScale * frame.wh), so a
|
|
120
|
+
// bigger cell magnifies the art instead of sharpening it. This factor compensates:
|
|
121
|
+
// getFillPatternScale must be multiplied by it so cell size only changes resolution.
|
|
122
|
+
export function getPatternScaleAdjustment(
|
|
123
|
+
cell: number = getPatternCellSize()
|
|
124
|
+
): number {
|
|
125
|
+
return SOURCE_TILE_SIZE / cell;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Gutter width around each cell; sized so a few mip levels stay bleed-free.
|
|
129
|
+
function getPatternCellPadding(cell: number): number {
|
|
130
|
+
return Math.max(2, Math.round(cell / 16));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function getPatternAtlasMapping(
|
|
134
|
+
cell: number = getPatternCellSize()
|
|
135
|
+
): Record<string, PatternAtlasFrame> {
|
|
136
|
+
const pad = getPatternCellPadding(cell);
|
|
137
|
+
const pitch = cell + 2 * pad;
|
|
138
|
+
const mapping: Record<string, PatternAtlasFrame> = {};
|
|
139
|
+
const frame = (col: number, row: number): PatternAtlasFrame => ({
|
|
140
|
+
x: pad + col * pitch,
|
|
141
|
+
y: pad + row * pitch,
|
|
142
|
+
width: cell,
|
|
143
|
+
height: cell,
|
|
144
|
+
mask: true,
|
|
145
|
+
});
|
|
146
|
+
PATTERN_ROWS.forEach((pattern, row) => {
|
|
147
|
+
DENSITY_COLUMNS.forEach((density, col) => {
|
|
148
|
+
mapping[`${pattern}-${density}`] = frame(col, row);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
// `none` must be a real transparent cell — a null pattern key resolves to atlas
|
|
152
|
+
// bounds [0,0,0,0] and samples the wrong (0,0) cell.
|
|
153
|
+
mapping.none = frame(0, PATTERN_ROWS.length);
|
|
154
|
+
mapping.solid = frame(1, PATTERN_ROWS.length);
|
|
155
|
+
return mapping;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export type AssembledAtlas = ImageBitmap | HTMLCanvasElement;
|
|
159
|
+
|
|
160
|
+
type AnyCanvas = HTMLCanvasElement | OffscreenCanvas;
|
|
161
|
+
|
|
162
|
+
function createCanvas(w: number, h: number): AnyCanvas {
|
|
163
|
+
if (typeof OffscreenCanvas !== 'undefined') return new OffscreenCanvas(w, h);
|
|
164
|
+
if (typeof document !== 'undefined') {
|
|
165
|
+
const c = document.createElement('canvas');
|
|
166
|
+
c.width = w;
|
|
167
|
+
c.height = h;
|
|
168
|
+
return c;
|
|
169
|
+
}
|
|
170
|
+
throw new Error(
|
|
171
|
+
'carto-api-client: no Canvas available to assemble the pattern atlas'
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function loadImage(dataUrl: string): Promise<CanvasImageSource> {
|
|
176
|
+
if (
|
|
177
|
+
typeof createImageBitmap !== 'undefined' &&
|
|
178
|
+
typeof fetch !== 'undefined'
|
|
179
|
+
) {
|
|
180
|
+
const blob = await (await fetch(dataUrl)).blob();
|
|
181
|
+
return createImageBitmap(blob);
|
|
182
|
+
}
|
|
183
|
+
return new Promise((resolve, reject) => {
|
|
184
|
+
const img = new Image();
|
|
185
|
+
img.onload = () => resolve(img);
|
|
186
|
+
img.onerror = reject;
|
|
187
|
+
img.src = dataUrl;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const atlasPromises = new Map<number, Promise<AssembledAtlas>>();
|
|
192
|
+
|
|
193
|
+
/** Assemble the sprite sheet from the individual editable pattern tiles. Memoized. */
|
|
194
|
+
export function getPatternAtlas(
|
|
195
|
+
cell: number = getPatternCellSize()
|
|
196
|
+
): Promise<AssembledAtlas> {
|
|
197
|
+
let promise = atlasPromises.get(cell);
|
|
198
|
+
if (!promise) {
|
|
199
|
+
promise = build(cell);
|
|
200
|
+
// Keep a rejection observed even if no consumer attaches a handler (e.g. Node,
|
|
201
|
+
// where there is no canvas) — the returned promise still rejects for real callers.
|
|
202
|
+
promise.catch(() => {});
|
|
203
|
+
atlasPromises.set(cell, promise);
|
|
204
|
+
}
|
|
205
|
+
return promise;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function build(cell: number): Promise<AssembledAtlas> {
|
|
209
|
+
const mapping = getPatternAtlasMapping(cell);
|
|
210
|
+
const pad = getPatternCellPadding(cell);
|
|
211
|
+
const pitch = cell + 2 * pad;
|
|
212
|
+
const canvas = createCanvas(pitch * 3, pitch * (PATTERN_ROWS.length + 1));
|
|
213
|
+
const ctx = canvas.getContext('2d');
|
|
214
|
+
if (!ctx)
|
|
215
|
+
throw new Error(
|
|
216
|
+
'carto-api-client: 2D context unavailable for pattern atlas'
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const images: Record<string, CanvasImageSource> = {};
|
|
220
|
+
await Promise.all(
|
|
221
|
+
Object.entries(CELL_URLS).map(async ([key, url]) => {
|
|
222
|
+
images[key] = await loadImage(url);
|
|
223
|
+
})
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// `none` is left transparent (no tile). Every other tile is drawn 3x3 clipped to its
|
|
227
|
+
// padded rect, so the gutter holds the tile's own wrapped content.
|
|
228
|
+
for (const [key, frame] of Object.entries(mapping)) {
|
|
229
|
+
const img = images[key];
|
|
230
|
+
if (!img) continue;
|
|
231
|
+
// Smooth downscale for high-res art; nearest upscale for pixel-art sources.
|
|
232
|
+
const srcWidth = (img as ImageBitmap | HTMLImageElement).width;
|
|
233
|
+
ctx.imageSmoothingEnabled = srcWidth > cell;
|
|
234
|
+
if ('imageSmoothingQuality' in ctx) ctx.imageSmoothingQuality = 'high';
|
|
235
|
+
ctx.save();
|
|
236
|
+
ctx.beginPath();
|
|
237
|
+
ctx.rect(frame.x - pad, frame.y - pad, cell + 2 * pad, cell + 2 * pad);
|
|
238
|
+
ctx.clip();
|
|
239
|
+
for (const dx of [-cell, 0, cell]) {
|
|
240
|
+
for (const dy of [-cell, 0, cell]) {
|
|
241
|
+
ctx.drawImage(img, frame.x + dx, frame.y + dy, cell, cell);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
ctx.restore();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (typeof createImageBitmap !== 'undefined')
|
|
248
|
+
return createImageBitmap(canvas);
|
|
249
|
+
return canvas as HTMLCanvasElement;
|
|
25
250
|
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
// Fill-pattern sprite atlas — option A (baked), the default.
|
|
2
|
-
//
|
|
3
|
-
// The definitive Design atlas (192x512, proper space-filling tiles) inlined as a base64
|
|
4
|
-
// data URL, so it ships in the bundle with no asset file and no HTTP request (hence no
|
|
5
|
-
// CORS surface) — the same mechanism api-client already uses for FALLBACK_ICON. deck.gl
|
|
6
|
-
// decodes the string and uploads the GPU texture lazily, only when a FillStyleExtension
|
|
7
|
-
// layer actually draws a pattern. Source of truth is the vector master (kept out of the
|
|
8
|
-
// repo for now) rendered to PNG; the runtime assembler (option B) remains a PoC.
|
|
9
|
-
//
|
|
10
|
-
// 7 patterns x 3 densities = 21 cells, plus a `solid` (opaque, middle of the last row)
|
|
11
|
-
// and a `none` (transparent) cell = 23, each 64x64. Columns are density sparse->dense
|
|
12
|
-
// (large/medium/small at x 0/64/128). Every cell is `mask: true`, so the sprite is a
|
|
13
|
-
// stencil tinted by getFillColor: opaque texels paint the fill color, transparent texels
|
|
14
|
-
// leave gaps. `none` must be a real transparent cell — a null pattern key resolves to
|
|
15
|
-
// atlas bounds [0,0,0,0] and samples the wrong (0,0) cell.
|
|
16
|
-
export const BAKED_ATLAS_URL =
|
|
17
|
-
'data:image/png;base64,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';
|
|
18
|
-
|
|
19
|
-
export type PatternAtlasFrame = {
|
|
20
|
-
x: number;
|
|
21
|
-
y: number;
|
|
22
|
-
width: number;
|
|
23
|
-
height: number;
|
|
24
|
-
mask: boolean;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export const PATTERN_ATLAS_MAPPING: Record<string, PatternAtlasFrame> = {
|
|
28
|
-
'hlines-large': {x: 0, y: 0, width: 64, height: 64, mask: true},
|
|
29
|
-
'hlines-medium': {x: 64, y: 0, width: 64, height: 64, mask: true},
|
|
30
|
-
'hlines-small': {x: 128, y: 0, width: 64, height: 64, mask: true},
|
|
31
|
-
'vlines-large': {x: 0, y: 64, width: 64, height: 64, mask: true},
|
|
32
|
-
'vlines-medium': {x: 64, y: 64, width: 64, height: 64, mask: true},
|
|
33
|
-
'vlines-small': {x: 128, y: 64, width: 64, height: 64, mask: true},
|
|
34
|
-
'diag-left-large': {x: 0, y: 128, width: 64, height: 64, mask: true},
|
|
35
|
-
'diag-left-medium': {x: 64, y: 128, width: 64, height: 64, mask: true},
|
|
36
|
-
'diag-left-small': {x: 128, y: 128, width: 64, height: 64, mask: true},
|
|
37
|
-
'diag-right-large': {x: 0, y: 192, width: 64, height: 64, mask: true},
|
|
38
|
-
'diag-right-medium': {x: 64, y: 192, width: 64, height: 64, mask: true},
|
|
39
|
-
'diag-right-small': {x: 128, y: 192, width: 64, height: 64, mask: true},
|
|
40
|
-
'cross-hatch-large': {x: 0, y: 256, width: 64, height: 64, mask: true},
|
|
41
|
-
'cross-hatch-medium': {x: 64, y: 256, width: 64, height: 64, mask: true},
|
|
42
|
-
'cross-hatch-small': {x: 128, y: 256, width: 64, height: 64, mask: true},
|
|
43
|
-
'dots-large': {x: 0, y: 320, width: 64, height: 64, mask: true},
|
|
44
|
-
'dots-medium': {x: 64, y: 320, width: 64, height: 64, mask: true},
|
|
45
|
-
'dots-small': {x: 128, y: 320, width: 64, height: 64, mask: true},
|
|
46
|
-
'checker-large': {x: 0, y: 384, width: 64, height: 64, mask: true},
|
|
47
|
-
'checker-medium': {x: 64, y: 384, width: 64, height: 64, mask: true},
|
|
48
|
-
'checker-small': {x: 128, y: 384, width: 64, height: 64, mask: true},
|
|
49
|
-
solid: {x: 64, y: 448, width: 64, height: 64, mask: true},
|
|
50
|
-
none: {x: 0, y: 448, width: 64, height: 64, mask: true},
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// Atlas geometry shared by the baked sheet above and the runtime assembler.
|
|
54
|
-
export const CELL = 64;
|
|
55
|
-
export const ATLAS_W = CELL * 3; // 192 — columns: [large, medium, small]
|
|
56
|
-
export const ATLAS_H = CELL * 8; // 512 — 7 pattern rows + 1 solid/none row
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
// Runtime fill-pattern atlas — option B (the proposal).
|
|
2
|
-
//
|
|
3
|
-
// The patterns live as individual, developer-editable assets (src/fetch-map/patterns/*.png,
|
|
4
|
-
// one 64x64 space-filling tile per pattern+density) rather than a compiled base64 blob.
|
|
5
|
-
// tsup inlines each via its `dataurl` loader, and we composite them into the 192x512 sheet
|
|
6
|
-
// on a canvas the first time a pattern is needed. The result is memoized and handed to
|
|
7
|
-
// deck.gl's async `fillPatternAtlas` prop as a Promise of a decoded image (so this stays
|
|
8
|
-
// internal — the consumer just spreads descriptor.props). SVG-vs-PNG is a later experiment.
|
|
9
|
-
|
|
10
|
-
import hlinesLarge from './patterns/hlines-large.png';
|
|
11
|
-
import hlinesMedium from './patterns/hlines-medium.png';
|
|
12
|
-
import hlinesSmall from './patterns/hlines-small.png';
|
|
13
|
-
import vlinesLarge from './patterns/vlines-large.png';
|
|
14
|
-
import vlinesMedium from './patterns/vlines-medium.png';
|
|
15
|
-
import vlinesSmall from './patterns/vlines-small.png';
|
|
16
|
-
import diagLeftLarge from './patterns/diag-left-large.png';
|
|
17
|
-
import diagLeftMedium from './patterns/diag-left-medium.png';
|
|
18
|
-
import diagLeftSmall from './patterns/diag-left-small.png';
|
|
19
|
-
import diagRightLarge from './patterns/diag-right-large.png';
|
|
20
|
-
import diagRightMedium from './patterns/diag-right-medium.png';
|
|
21
|
-
import diagRightSmall from './patterns/diag-right-small.png';
|
|
22
|
-
import crossHatchLarge from './patterns/cross-hatch-large.png';
|
|
23
|
-
import crossHatchMedium from './patterns/cross-hatch-medium.png';
|
|
24
|
-
import crossHatchSmall from './patterns/cross-hatch-small.png';
|
|
25
|
-
import dotsLarge from './patterns/dots-large.png';
|
|
26
|
-
import dotsMedium from './patterns/dots-medium.png';
|
|
27
|
-
import dotsSmall from './patterns/dots-small.png';
|
|
28
|
-
import checkerLarge from './patterns/checker-large.png';
|
|
29
|
-
import checkerMedium from './patterns/checker-medium.png';
|
|
30
|
-
import checkerSmall from './patterns/checker-small.png';
|
|
31
|
-
import solid from './patterns/solid.png';
|
|
32
|
-
|
|
33
|
-
import {
|
|
34
|
-
ATLAS_W,
|
|
35
|
-
ATLAS_H,
|
|
36
|
-
CELL,
|
|
37
|
-
PATTERN_ATLAS_MAPPING,
|
|
38
|
-
} from './pattern-atlas-baked.js';
|
|
39
|
-
|
|
40
|
-
// atlas key -> inlined data URL of its editable source tile
|
|
41
|
-
const CELL_URLS: Record<string, string> = {
|
|
42
|
-
'hlines-large': hlinesLarge,
|
|
43
|
-
'hlines-medium': hlinesMedium,
|
|
44
|
-
'hlines-small': hlinesSmall,
|
|
45
|
-
'vlines-large': vlinesLarge,
|
|
46
|
-
'vlines-medium': vlinesMedium,
|
|
47
|
-
'vlines-small': vlinesSmall,
|
|
48
|
-
'diag-left-large': diagLeftLarge,
|
|
49
|
-
'diag-left-medium': diagLeftMedium,
|
|
50
|
-
'diag-left-small': diagLeftSmall,
|
|
51
|
-
'diag-right-large': diagRightLarge,
|
|
52
|
-
'diag-right-medium': diagRightMedium,
|
|
53
|
-
'diag-right-small': diagRightSmall,
|
|
54
|
-
'cross-hatch-large': crossHatchLarge,
|
|
55
|
-
'cross-hatch-medium': crossHatchMedium,
|
|
56
|
-
'cross-hatch-small': crossHatchSmall,
|
|
57
|
-
'dots-large': dotsLarge,
|
|
58
|
-
'dots-medium': dotsMedium,
|
|
59
|
-
'dots-small': dotsSmall,
|
|
60
|
-
'checker-large': checkerLarge,
|
|
61
|
-
'checker-medium': checkerMedium,
|
|
62
|
-
'checker-small': checkerSmall,
|
|
63
|
-
solid,
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
type AnyCanvas = HTMLCanvasElement | OffscreenCanvas;
|
|
67
|
-
|
|
68
|
-
function createCanvas(w: number, h: number): AnyCanvas {
|
|
69
|
-
if (typeof OffscreenCanvas !== 'undefined') return new OffscreenCanvas(w, h);
|
|
70
|
-
if (typeof document !== 'undefined') {
|
|
71
|
-
const c = document.createElement('canvas');
|
|
72
|
-
c.width = w;
|
|
73
|
-
c.height = h;
|
|
74
|
-
return c;
|
|
75
|
-
}
|
|
76
|
-
throw new Error(
|
|
77
|
-
'carto-api-client: no Canvas available to assemble the pattern atlas'
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function loadImage(dataUrl: string): Promise<CanvasImageSource> {
|
|
82
|
-
if (
|
|
83
|
-
typeof createImageBitmap !== 'undefined' &&
|
|
84
|
-
typeof fetch !== 'undefined'
|
|
85
|
-
) {
|
|
86
|
-
const blob = await (await fetch(dataUrl)).blob();
|
|
87
|
-
return createImageBitmap(blob);
|
|
88
|
-
}
|
|
89
|
-
return new Promise((resolve, reject) => {
|
|
90
|
-
const img = new Image();
|
|
91
|
-
img.onload = () => resolve(img);
|
|
92
|
-
img.onerror = reject;
|
|
93
|
-
img.src = dataUrl;
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export type AssembledAtlas = ImageBitmap | HTMLCanvasElement;
|
|
98
|
-
|
|
99
|
-
let atlasPromise: Promise<AssembledAtlas> | undefined;
|
|
100
|
-
|
|
101
|
-
/** Assemble the sprite sheet from the individual editable pattern tiles. Memoized. */
|
|
102
|
-
export function assemblePatternAtlas(): Promise<AssembledAtlas> {
|
|
103
|
-
if (!atlasPromise) atlasPromise = build();
|
|
104
|
-
return atlasPromise;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Must resolve to a decoded image, never a data-URL string: deck.gl only URL-loads
|
|
108
|
-
// string prop *values* — a string resolved from a Promise goes straight to texture
|
|
109
|
-
// creation, where luma.gl rejects it (1x1 texture, "ArrayBufferView not big enough").
|
|
110
|
-
async function build(): Promise<AssembledAtlas> {
|
|
111
|
-
const canvas = createCanvas(ATLAS_W, ATLAS_H);
|
|
112
|
-
const ctx = canvas.getContext('2d');
|
|
113
|
-
if (!ctx)
|
|
114
|
-
throw new Error(
|
|
115
|
-
'carto-api-client: 2D context unavailable for pattern atlas'
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
const images: Record<string, CanvasImageSource> = {};
|
|
119
|
-
await Promise.all(
|
|
120
|
-
Object.entries(CELL_URLS).map(async ([key, url]) => {
|
|
121
|
-
images[key] = await loadImage(url);
|
|
122
|
-
})
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
// Place each editable 64x64 tile at its slot; `none` is left transparent (no tile).
|
|
126
|
-
for (const [key, frame] of Object.entries(PATTERN_ATLAS_MAPPING)) {
|
|
127
|
-
const img = images[key];
|
|
128
|
-
if (img) ctx.drawImage(img, frame.x, frame.y, CELL, CELL);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (typeof createImageBitmap !== 'undefined')
|
|
132
|
-
return createImageBitmap(canvas);
|
|
133
|
-
return canvas as HTMLCanvasElement;
|
|
134
|
-
}
|