@versatiles/svg-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/README.md +20 -10
- package/dist/index.cjs +153 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +153 -32
- package/dist/index.js.map +1 -1
- package/dist/maplibre.cjs +153 -32
- package/dist/maplibre.cjs.map +1 -1
- package/dist/maplibre.js +153 -32
- package/dist/maplibre.js.map +1 -1
- package/dist/maplibre.umd.js +153 -32
- package/dist/maplibre.umd.js.map +1 -1
- package/package.json +6 -4
package/dist/maplibre.cjs
CHANGED
|
@@ -9444,6 +9444,9 @@ class SVGRenderer {
|
|
|
9444
9444
|
height;
|
|
9445
9445
|
#svg;
|
|
9446
9446
|
#backgroundColor;
|
|
9447
|
+
#spriteSheetDefs = new Map();
|
|
9448
|
+
#spriteSymbolDefs = new Map();
|
|
9449
|
+
#sdfFilterDefs = new Map();
|
|
9447
9450
|
constructor(opt) {
|
|
9448
9451
|
this.width = opt.width;
|
|
9449
9452
|
this.height = opt.height;
|
|
@@ -9636,7 +9639,7 @@ class SVGRenderer {
|
|
|
9636
9639
|
drawIcons(id, features, spriteAtlas) {
|
|
9637
9640
|
if (features.length === 0)
|
|
9638
9641
|
return;
|
|
9639
|
-
|
|
9642
|
+
const elements = [];
|
|
9640
9643
|
for (const [feature, style] of features) {
|
|
9641
9644
|
if (style.opacity <= 0)
|
|
9642
9645
|
continue;
|
|
@@ -9653,27 +9656,102 @@ class SVGRenderer {
|
|
|
9653
9656
|
const [anchorDx, anchorDy] = mapIconAnchor(style.anchor, iconW, iconH);
|
|
9654
9657
|
const ox = style.offset[0] * style.size + anchorDx;
|
|
9655
9658
|
const oy = style.offset[1] * style.size + anchorDy;
|
|
9656
|
-
const
|
|
9657
|
-
|
|
9658
|
-
const
|
|
9659
|
-
const
|
|
9660
|
-
const
|
|
9661
|
-
|
|
9662
|
-
|
|
9663
|
-
|
|
9664
|
-
|
|
9665
|
-
|
|
9666
|
-
|
|
9667
|
-
|
|
9668
|
-
|
|
9659
|
+
const [iconXr, iconYr] = roundXY(point.x + ox, point.y + oy);
|
|
9660
|
+
// Register sprite sheet in global defs (once per unique data URI)
|
|
9661
|
+
const imgW = Math.round(sprite.sheetWidth * 10);
|
|
9662
|
+
const imgH = Math.round(sprite.sheetHeight * 10);
|
|
9663
|
+
const sheetKey = sprite.sheetDataUri;
|
|
9664
|
+
if (!this.#spriteSheetDefs.has(sheetKey)) {
|
|
9665
|
+
this.#spriteSheetDefs.set(sheetKey, {
|
|
9666
|
+
defId: `sprite-sheet-${String(this.#spriteSheetDefs.size)}`,
|
|
9667
|
+
width: imgW,
|
|
9668
|
+
height: imgH,
|
|
9669
|
+
href: sprite.sheetDataUri,
|
|
9670
|
+
});
|
|
9671
|
+
}
|
|
9672
|
+
const sheetDef = this.#spriteSheetDefs.get(sheetKey);
|
|
9673
|
+
// Register symbol for this sprite (once per sprite name + sheet)
|
|
9674
|
+
const sprX = Math.round(sprite.x * 10);
|
|
9675
|
+
const sprY = Math.round(sprite.y * 10);
|
|
9676
|
+
const sprW = Math.round(sprite.width * 10);
|
|
9677
|
+
const sprH = Math.round(sprite.height * 10);
|
|
9678
|
+
const symKey = `${style.image}\0${sheetKey}`;
|
|
9679
|
+
if (!this.#spriteSymbolDefs.has(symKey)) {
|
|
9680
|
+
this.#spriteSymbolDefs.set(symKey, {
|
|
9681
|
+
symbolId: `sprite-${escapeXml(style.image)}`,
|
|
9682
|
+
sheetDefId: sheetDef.defId,
|
|
9683
|
+
x: sprX,
|
|
9684
|
+
y: sprY,
|
|
9685
|
+
width: sprW,
|
|
9686
|
+
height: sprH,
|
|
9687
|
+
});
|
|
9688
|
+
}
|
|
9689
|
+
const symDef = this.#spriteSymbolDefs.get(symKey);
|
|
9690
|
+
// Build instance: translate to position, scale from native to desired size
|
|
9691
|
+
const scaleStr = scale === 1 ? '' : ` scale(${formatScale(scale)})`;
|
|
9692
|
+
const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
|
|
9693
|
+
// SDF filter for colorable icons
|
|
9694
|
+
let filterAttr = '';
|
|
9695
|
+
if (style.sdf) {
|
|
9696
|
+
const iconColor = new Color(style.color);
|
|
9697
|
+
const haloColor = new Color(style.haloColor);
|
|
9698
|
+
const hasHalo = style.haloWidth > 0 && haloColor.alpha > 0;
|
|
9699
|
+
const filterKey = hasHalo
|
|
9700
|
+
? `sdf\0${iconColor.hex}\0${haloColor.hex}\0${String(style.haloWidth)}`
|
|
9701
|
+
: `sdf\0${iconColor.hex}`;
|
|
9702
|
+
if (!this.#sdfFilterDefs.has(filterKey)) {
|
|
9703
|
+
const filterId = `sdf-${String(this.#sdfFilterDefs.size)}`;
|
|
9704
|
+
const iconFloodOpacity = iconColor.alpha < 255 ? ` flood-opacity="${iconColor.opacity.toFixed(3)}"` : '';
|
|
9705
|
+
let content;
|
|
9706
|
+
if (hasHalo) {
|
|
9707
|
+
const haloRadius = formatScale(style.haloWidth);
|
|
9708
|
+
const haloFloodOpacity = haloColor.alpha < 255 ? ` flood-opacity="${haloColor.opacity.toFixed(3)}"` : '';
|
|
9709
|
+
content =
|
|
9710
|
+
`<filter id="${filterId}" color-interpolation-filters="sRGB">` +
|
|
9711
|
+
// Threshold alpha at 0.75 (MapLibre SDF edge) to get sharp icon mask
|
|
9712
|
+
`<feComponentTransfer in="SourceGraphic" result="sharp"><feFuncA type="discrete" tableValues="0 0 0 1" /></feComponentTransfer>` +
|
|
9713
|
+
// Dilate sharp mask for halo
|
|
9714
|
+
`<feMorphology in="sharp" operator="dilate" radius="${haloRadius}" result="dilated" />` +
|
|
9715
|
+
`<feFlood flood-color="${haloColor.rgb}"${haloFloodOpacity} result="haloColor" />` +
|
|
9716
|
+
`<feComposite in="haloColor" in2="dilated" operator="in" result="halo" />` +
|
|
9717
|
+
// Color the sharp icon
|
|
9718
|
+
`<feFlood flood-color="${iconColor.rgb}"${iconFloodOpacity} result="iconColor" />` +
|
|
9719
|
+
`<feComposite in="iconColor" in2="sharp" operator="in" result="colored" />` +
|
|
9720
|
+
`<feComposite in="colored" in2="halo" operator="over" />` +
|
|
9721
|
+
`</filter>`;
|
|
9722
|
+
}
|
|
9723
|
+
else {
|
|
9724
|
+
content =
|
|
9725
|
+
`<filter id="${filterId}" x="0" y="0" width="1" height="1" color-interpolation-filters="sRGB">` +
|
|
9726
|
+
// Threshold alpha at 0.75 (MapLibre SDF edge) to get sharp mask
|
|
9727
|
+
`<feComponentTransfer in="SourceGraphic" result="sharp"><feFuncA type="discrete" tableValues="0 0 0 1" /></feComponentTransfer>` +
|
|
9728
|
+
// Replace color while keeping sharp alpha
|
|
9729
|
+
`<feFlood flood-color="${iconColor.rgb}"${iconFloodOpacity} result="color" />` +
|
|
9730
|
+
`<feComposite in="color" in2="sharp" operator="in" />` +
|
|
9731
|
+
`</filter>`;
|
|
9732
|
+
}
|
|
9733
|
+
this.#sdfFilterDefs.set(filterKey, { filterId, content });
|
|
9734
|
+
}
|
|
9735
|
+
const { filterId } = this.#sdfFilterDefs.get(filterKey);
|
|
9736
|
+
filterAttr = ` filter="url(#${filterId})"`;
|
|
9737
|
+
}
|
|
9669
9738
|
if (style.rotate !== 0) {
|
|
9670
9739
|
const [cx, cy] = roundXY(point.x + style.offset[0] * style.size, point.y + style.offset[1] * style.size);
|
|
9671
|
-
|
|
9740
|
+
elements.push(`<g transform="rotate(${String(style.rotate)},${formatNum(cx)},${formatNum(cy)})">` +
|
|
9741
|
+
`<g transform="translate(${formatNum(iconXr)},${formatNum(iconYr)})${scaleStr}"${opacityAttr}${filterAttr}>` +
|
|
9742
|
+
`<use href="#${escapeXml(symDef.symbolId)}" />` +
|
|
9743
|
+
`</g></g>`);
|
|
9744
|
+
}
|
|
9745
|
+
else {
|
|
9746
|
+
elements.push(`<g transform="translate(${formatNum(iconXr)},${formatNum(iconYr)})${scaleStr}"${opacityAttr}${filterAttr}>` +
|
|
9747
|
+
`<use href="#${escapeXml(symDef.symbolId)}" />` +
|
|
9748
|
+
`</g>`);
|
|
9672
9749
|
}
|
|
9673
|
-
this.#svg.push(`<svg ${attrs.join(' ')} viewBox="${viewBox}" xmlns="http://www.w3.org/2000/svg">` +
|
|
9674
|
-
`<image width="${String(sprite.sheetWidth)}" height="${String(sprite.sheetHeight)}" href="${sprite.sheetDataUri}" />` +
|
|
9675
|
-
`</svg>`);
|
|
9676
9750
|
}
|
|
9751
|
+
if (elements.length === 0)
|
|
9752
|
+
return;
|
|
9753
|
+
this.#svg.push(`<g id="${escapeXml(id)}">`);
|
|
9754
|
+
this.#svg.push(...elements);
|
|
9677
9755
|
this.#svg.push('</g>');
|
|
9678
9756
|
}
|
|
9679
9757
|
drawRasterTiles(id, tiles, style) {
|
|
@@ -9709,9 +9787,21 @@ class SVGRenderer {
|
|
|
9709
9787
|
getString() {
|
|
9710
9788
|
const w = this.width.toFixed(0);
|
|
9711
9789
|
const h = this.height.toFixed(0);
|
|
9790
|
+
// Build defs content
|
|
9791
|
+
const defsContent = [`<clipPath id="vb"><rect width="${w}" height="${h}"/></clipPath>`];
|
|
9792
|
+
for (const sheet of this.#spriteSheetDefs.values()) {
|
|
9793
|
+
defsContent.push(`<image id="${escapeXml(sheet.defId)}" width="${formatNum(sheet.width)}" height="${formatNum(sheet.height)}" href="${escapeXml(sheet.href)}" />`);
|
|
9794
|
+
}
|
|
9795
|
+
for (const sym of this.#spriteSymbolDefs.values()) {
|
|
9796
|
+
const clipId = `${sym.symbolId}-clip`;
|
|
9797
|
+
defsContent.push(`<clipPath id="${escapeXml(clipId)}"><rect width="${formatNum(sym.width)}" height="${formatNum(sym.height)}" /></clipPath>`, `<symbol id="${escapeXml(sym.symbolId)}"><g clip-path="url(#${escapeXml(clipId)})"><use href="#${escapeXml(sym.sheetDefId)}" x="${formatNum(-sym.x)}" y="${formatNum(-sym.y)}" /></g></symbol>`);
|
|
9798
|
+
}
|
|
9799
|
+
for (const { content } of this.#sdfFilterDefs.values()) {
|
|
9800
|
+
defsContent.push(content);
|
|
9801
|
+
}
|
|
9712
9802
|
const parts = [
|
|
9713
9803
|
`<svg viewBox="0 0 ${w} ${h}" width="${w}" height="${h}" xmlns="http://www.w3.org/2000/svg">`,
|
|
9714
|
-
`<defs
|
|
9804
|
+
`<defs>\n ${defsContent.join('\n ')}\n</defs>`,
|
|
9715
9805
|
`<g clip-path="url(#vb)">`,
|
|
9716
9806
|
];
|
|
9717
9807
|
if (this.#backgroundColor.alpha > 0) {
|
|
@@ -9736,6 +9826,9 @@ function strokeAttr(color, width) {
|
|
|
9736
9826
|
function formatScaled(v) {
|
|
9737
9827
|
return formatNum(Math.round(v * 10));
|
|
9738
9828
|
}
|
|
9829
|
+
function formatScale(v) {
|
|
9830
|
+
return (Math.round(v * 10000) / 10000).toString();
|
|
9831
|
+
}
|
|
9739
9832
|
function roundXY(x, y) {
|
|
9740
9833
|
return [Math.round(x * 10), Math.round(y * 10)];
|
|
9741
9834
|
}
|
|
@@ -16198,14 +16291,29 @@ async function loadVectorSource(source, job, layerFeatures) {
|
|
|
16198
16291
|
list = features.polygons;
|
|
16199
16292
|
break;
|
|
16200
16293
|
}
|
|
16201
|
-
|
|
16202
|
-
|
|
16203
|
-
geometry
|
|
16204
|
-
|
|
16205
|
-
|
|
16206
|
-
|
|
16207
|
-
|
|
16208
|
-
|
|
16294
|
+
// Split MultiPoint into individual Point features
|
|
16295
|
+
if (type === 'Point' && geometry.length > 1) {
|
|
16296
|
+
for (const ring of geometry) {
|
|
16297
|
+
const feature = new Feature({
|
|
16298
|
+
type,
|
|
16299
|
+
geometry: [ring],
|
|
16300
|
+
id: featureSrc.id,
|
|
16301
|
+
properties: featureSrc.properties,
|
|
16302
|
+
});
|
|
16303
|
+
if (feature.doesOverlap([0, 0, width, height]))
|
|
16304
|
+
list.push(feature);
|
|
16305
|
+
}
|
|
16306
|
+
}
|
|
16307
|
+
else {
|
|
16308
|
+
const feature = new Feature({
|
|
16309
|
+
type,
|
|
16310
|
+
geometry,
|
|
16311
|
+
id: featureSrc.id,
|
|
16312
|
+
properties: featureSrc.properties,
|
|
16313
|
+
});
|
|
16314
|
+
if (feature.doesOverlap([0, 0, width, height]))
|
|
16315
|
+
list.push(feature);
|
|
16316
|
+
}
|
|
16209
16317
|
}
|
|
16210
16318
|
}
|
|
16211
16319
|
}));
|
|
@@ -16342,6 +16450,14 @@ async function getRasterTiles(job, sourceName) {
|
|
|
16342
16450
|
return rasterTiles.filter((tile) => tile !== null);
|
|
16343
16451
|
}
|
|
16344
16452
|
|
|
16453
|
+
async function fetchSpritePair(url) {
|
|
16454
|
+
const [jsonResponse, imageResponse] = await Promise.all([
|
|
16455
|
+
fetch(`${url}.json`),
|
|
16456
|
+
fetch(`${url}.png`),
|
|
16457
|
+
]);
|
|
16458
|
+
if (jsonResponse.ok && imageResponse.ok)
|
|
16459
|
+
return { jsonResponse, imageResponse };
|
|
16460
|
+
}
|
|
16345
16461
|
async function loadSpriteAtlas(style) {
|
|
16346
16462
|
const atlas = new Map();
|
|
16347
16463
|
const sprite = style.sprite;
|
|
@@ -16361,12 +16477,11 @@ async function loadSpriteAtlas(style) {
|
|
|
16361
16477
|
}
|
|
16362
16478
|
await Promise.all(sources.map(async ({ id, url }) => {
|
|
16363
16479
|
try {
|
|
16364
|
-
|
|
16365
|
-
|
|
16366
|
-
|
|
16367
|
-
]);
|
|
16368
|
-
if (!jsonResponse.ok || !imageResponse.ok)
|
|
16480
|
+
// Try @2x retina sprites first, fall back to 1x
|
|
16481
|
+
const spritePair = (await fetchSpritePair(`${url}@2x`)) ?? (await fetchSpritePair(url));
|
|
16482
|
+
if (!spritePair)
|
|
16369
16483
|
return;
|
|
16484
|
+
const { jsonResponse, imageResponse } = spritePair;
|
|
16370
16485
|
const json = (await jsonResponse.json());
|
|
16371
16486
|
const imageBuffer = await imageResponse.arrayBuffer();
|
|
16372
16487
|
const base64 = typeof Buffer !== 'undefined'
|
|
@@ -16388,6 +16503,7 @@ async function loadSpriteAtlas(style) {
|
|
|
16388
16503
|
x: entry.x,
|
|
16389
16504
|
y: entry.y,
|
|
16390
16505
|
pixelRatio: entry.pixelRatio ?? 1,
|
|
16506
|
+
sdf: entry.sdf ?? false,
|
|
16391
16507
|
sheetDataUri,
|
|
16392
16508
|
sheetWidth,
|
|
16393
16509
|
sheetHeight,
|
|
@@ -16721,6 +16837,7 @@ async function render(job) {
|
|
|
16721
16837
|
: '';
|
|
16722
16838
|
if (!iconName || !spriteAtlas.has(iconName))
|
|
16723
16839
|
return [];
|
|
16840
|
+
const spriteEntry = spriteAtlas.get(iconName);
|
|
16724
16841
|
return [
|
|
16725
16842
|
[
|
|
16726
16843
|
feature,
|
|
@@ -16731,6 +16848,10 @@ async function render(job) {
|
|
|
16731
16848
|
offset: getLayout('icon-offset', feature),
|
|
16732
16849
|
rotate: getLayout('icon-rotate', feature),
|
|
16733
16850
|
opacity: getPaint('icon-opacity', feature),
|
|
16851
|
+
sdf: spriteEntry.sdf,
|
|
16852
|
+
color: getPaint('icon-color', feature),
|
|
16853
|
+
haloColor: getPaint('icon-halo-color', feature),
|
|
16854
|
+
haloWidth: getPaint('icon-halo-width', feature),
|
|
16734
16855
|
},
|
|
16735
16856
|
],
|
|
16736
16857
|
];
|