Stop faking SDF for POI icons, fix the resulting blur
The badge and category glyphs were registered as SDF images (a single alpha shape MapLibre tints at render time via icon-color) purely so one shared sprite could be recolored per category. But SDF rendering assumes the source actually encodes a distance field — a smooth multi-pixel gradient at every edge — and these were just normally antialiased rasters (a sharp ~1px transition). MapLibre's SDF antialiasing pass softened every edge as a result, visibly blurrier than the base map's own icons, worst on the small category glyph. The glyph color never varies (always white), so it never needed runtime tinting at all: _registerCategoryGlyphs now bakes #fff directly into the raster and registers one plain image per category. The badge color does vary (arbitrary per-file hex), but a real POI export only has a handful of distinct category colors, so _registerBadgeColors rasterizes one sharp image per distinct color actually present in the points being shown, cached by color, instead of one shared image tinted at render time.
This commit is contained in:
1 parent
7de8428b05
commit
6f7d332993
2 files changed
+94
-40
No files matched your search
+55
-19
@@ -20,8 +20,8 @@ const TRACK_COLOR = "#e63946";
|
||||
const POI_ICON_KEYS = Object.keys(POI_ICONS);
|
||||
const POI_ICON_IMAGE_SIZE = 48; // rasterized once at this size, rendered much smaller by icon-size below
|
||||
const POI_ICON_SIZE = 0.3; // 48 * 0.3 = ~14px, sized to sit inside the marker badge below
|
||||
const MAP_MARKER_ID = "map-marker";
|
||||
const MAP_MARKER_SIZE = 0.55;
|
||||
const BADGE_IMAGE_PREFIX = "badge-"; // one registered image per distinct badge color actually seen, see _registerBadgeColors
|
||||
|
||||
// Custom drag payload MIME type for moving an already-uploaded file between
|
||||
// folders, as opposed to an OS file drag (type "Files") for uploading a new
|
||||
@@ -41,27 +41,54 @@ export class GpxPanelControl {
|
||||
this._pointsByPath = new Map(); // path -> its waypoints FeatureCollection, for the category legend
|
||||
|
||||
// setStyle() (switching the Online/Offline base map) tears down all
|
||||
// custom sources/layers/images — re-register the POI icon sprites
|
||||
// before putting visible GPX layers back, so a symbol layer added right
|
||||
// away doesn't reference a not-yet-existing image.
|
||||
this._iconsReady = this._registerPoiIcons();
|
||||
// custom sources/layers/images — re-register the category glyphs before
|
||||
// putting visible GPX layers back, so a symbol layer added right away
|
||||
// doesn't reference a not-yet-existing image. Badge images are handled
|
||||
// separately (_registerBadgeColors, called from _addLayers) since which
|
||||
// colors are needed depends on which file/category is being shown.
|
||||
this._iconsReady = this._registerCategoryGlyphs();
|
||||
map.on("style.load", () => {
|
||||
this._iconsReady = this._registerPoiIcons();
|
||||
this._iconsReady = this._registerCategoryGlyphs();
|
||||
this._visible.forEach((path) => this._addLayers(path));
|
||||
});
|
||||
}
|
||||
|
||||
// SDF images (a single alpha shape, tinted at render time via icon-color)
|
||||
// rather than plain color images: the source SVGs are silhouettes with no
|
||||
// color information worth preserving, and SDF lets the same sprite be
|
||||
// reused if a future category needs a different tint.
|
||||
async _registerPoiIcons() {
|
||||
const sources = { ...POI_ICONS, [MAP_MARKER_ID]: MAP_MARKER_ICON };
|
||||
// Registered once, always white, as plain (non-SDF) images: these used to
|
||||
// be SDF images tinted white via icon-color, but MapLibre's SDF rendering
|
||||
// assumes the source encodes an actual distance field (a smooth multi-pixel
|
||||
// gradient at each edge) — feeding it a normally-antialiased raster (a
|
||||
// sharp 1px edge transition) instead made every glyph noticeably softer
|
||||
// than the base map's own icons, especially shrunk down to icon-size 0.3.
|
||||
// Since the glyph color never varies (always white, unlike the badge
|
||||
// below), there's no need for SDF's runtime-tintable alpha mask at all —
|
||||
// baking the color into the raster and skipping SDF renders as sharp as
|
||||
// any ordinary sprite.
|
||||
async _registerCategoryGlyphs() {
|
||||
await Promise.all(
|
||||
Object.keys(sources).map(async (key) => {
|
||||
POI_ICON_KEYS.map(async (key) => {
|
||||
if (this._map.hasImage(key)) return;
|
||||
const imageData = await svgToImageData(sources[key], POI_ICON_IMAGE_SIZE);
|
||||
if (!this._map.hasImage(key)) this._map.addImage(key, imageData, { sdf: true });
|
||||
const whiteSvg = POI_ICONS[key].replace(/currentColor/g, "#fff");
|
||||
const imageData = await svgToImageData(whiteSvg, POI_ICON_IMAGE_SIZE);
|
||||
if (!this._map.hasImage(key)) this._map.addImage(key, imageData);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// The badge's color is per-category data (arbitrary hex from a GPX file),
|
||||
// so unlike the glyphs it can't be a single fixed-color image — but a real
|
||||
// POI export only ever has a handful of distinct category colors, so it's
|
||||
// cheap to rasterize one sharp, non-SDF image per color actually seen
|
||||
// (rather than one shared image SDF-tinted at render time, which is what
|
||||
// made these blurry — see _registerCategoryGlyphs above) and cache them by
|
||||
// color for reuse across files.
|
||||
async _registerBadgeColors(colors) {
|
||||
await Promise.all(
|
||||
[...colors].map(async (color) => {
|
||||
const id = badgeImageId(color);
|
||||
if (this._map.hasImage(id)) return;
|
||||
const coloredSvg = MAP_MARKER_ICON.replace(/currentColor/g, color);
|
||||
const imageData = await svgToImageData(coloredSvg, POI_ICON_IMAGE_SIZE);
|
||||
if (!this._map.hasImage(id)) this._map.addImage(id, imageData);
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -358,6 +385,12 @@ export class GpxPanelControl {
|
||||
for (const feature of geojson.features) {
|
||||
(feature.geometry.type === "Point" ? points : lines).features.push(feature);
|
||||
}
|
||||
// Normalized here (rather than a ["coalesce", ...] in the layer's
|
||||
// icon-image expression) so every point's badge color is known upfront,
|
||||
// before registering the per-color badge images below.
|
||||
for (const feature of points.features) {
|
||||
feature.properties.color ||= TRACK_COLOR;
|
||||
}
|
||||
|
||||
this._map.addSource(linesId, { type: "geojson", data: lines });
|
||||
this._map.addLayer({
|
||||
@@ -372,7 +405,8 @@ export class GpxPanelControl {
|
||||
// clusterRadius/clusterMaxZoom are harmless when there's nothing dense
|
||||
// enough to cluster (a handful of waypoints just render individually).
|
||||
this._map.addSource(pointsId, { type: "geojson", data: points, cluster: true, clusterRadius: 50, clusterMaxZoom: 14 });
|
||||
await this._iconsReady;
|
||||
const badgeColors = new Set(points.features.map((f) => f.properties.color));
|
||||
await Promise.all([this._iconsReady, this._registerBadgeColors(badgeColors)]);
|
||||
if (!this._map.getSource(pointsId)) return true; // a style.load (basemap switch) tore it down while icons were loading
|
||||
|
||||
this._addClusterLayers(pointsId);
|
||||
@@ -424,12 +458,11 @@ export class GpxPanelControl {
|
||||
source: pointsId,
|
||||
filter: ["!", ["has", "point_count"]],
|
||||
layout: {
|
||||
"icon-image": MAP_MARKER_ID,
|
||||
"icon-image": ["concat", BADGE_IMAGE_PREFIX, ["get", "color"]],
|
||||
"icon-size": MAP_MARKER_SIZE,
|
||||
"icon-allow-overlap": true,
|
||||
"icon-ignore-placement": true,
|
||||
},
|
||||
paint: { "icon-color": ["coalesce", ["get", "color"], TRACK_COLOR] },
|
||||
});
|
||||
// Only for the curated POI_ICON_KEYS (see icons.js) — anything else
|
||||
// (a plain hand-placed waypoint, or a category p4n2gpx/OsmAnd hasn't
|
||||
@@ -445,7 +478,6 @@ export class GpxPanelControl {
|
||||
"icon-allow-overlap": true,
|
||||
"icon-ignore-placement": true,
|
||||
},
|
||||
paint: { "icon-color": "#fff" },
|
||||
});
|
||||
|
||||
this._registerClusterInteractivity(pointsId);
|
||||
@@ -703,6 +735,10 @@ async function svgToImageData(svgMarkup, size) {
|
||||
}
|
||||
}
|
||||
|
||||
function badgeImageId(color) {
|
||||
return `${BADGE_IMAGE_PREFIX}${color}`;
|
||||
}
|
||||
|
||||
function linesSourceId(path) {
|
||||
return `gpx-${path}-lines`;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user