clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

feat: dim the whole map behind a highlighted zone, and let its pins be reached

Dami committed b3439ff
Highlighting a zone shaded the other zones and left the ground between them at
full strength, which demoted the neighbours rather than raising the zone that
was asked for. A scrim now covers the whole map with the highlighted zones cut
out of it, under the zone layer so their outlines and titles are drawn over it
untouched. Rings alternate direction with their nesting depth, so the fill
counts its way in and out whichever way round the archive wound them, and a
zone with a hole in it keeps the hole. The neighbours lose the dark fill they
carried: the scrim has already taken them down with everything else, and a
second wash would single them out again.

While a zone is highlighted its pins move to a layer of their own, and that
layer was not among the ones hit-testing looked at -- so the only pins still
on screen were the ones the pointer could not reach, by hover or by click.
Every layer a pin can be drawn in is named in one place now, and both the
click and the pointer read from it.
commit
b3439fffd3914c6b4d7a18f00d860123a302db43
parent
818c67afca05cbb5baffc3a51c48e73ecbbccbe9
author
Dami <dami.etoile@gmail.com>
1 file changed, +64 -19
Mfrontend/app.js83+++++++++++++++++++++++-------
M frontend/app.js +64 -19
@@ -165,6 +165,7 @@ function initializeMap() {
165165 state.sources = {166166 grid: new VectorSource({ wrapX: false }),167167 gridContext: new VectorSource({ wrapX: false }),168+ zoneScrim: new VectorSource({ wrapX: false }),168169 zones: new VectorSource({ wrapX: false }),169170 zoneTitles: new VectorSource({ wrapX: false }),170171 text: new VectorSource({ wrapX: false }),
@@ -184,6 +185,13 @@ function initializeMap() {
184185 renderBuffer: 40,185186 zIndex: 5,186187 }),188+ // Under the zones and over the map: the dimming is something the map is189+ // seen through, not something drawn on top of the zone that was asked for.190+ zoneScrim: eagerVector({191+ source: state.sources.zoneScrim,192+ style: zoneScrimStyle,193+ zIndex: 6,194+ }),187195 zones: eagerVector({188196 source: state.sources.zones,189197 style: zoneStyle,
@@ -338,21 +346,8 @@ function initializeMap() {
338346 }339347 const hit = state.engine.forEachFeatureAtPixel(340348 event.pixel,341- (feature, layer) => {342- if (layer !== state.layers.pins && layer !== state.layers.pinLabels &&343- layer !== state.layers.text && layer !== state.layers.textDetail &&344- layer !== state.layers.priority) {345- return null;346- }347- return feature.get("pin") || null;348- },349- {350- hitTolerance: 4,351- layerFilter: (layer) =>352- layer === state.layers.pins || layer === state.layers.pinLabels ||353- layer === state.layers.text || layer === state.layers.textDetail ||354- layer === state.layers.priority,355- },349+ (feature, layer) => (isAnnotationLayer(layer) && feature.get("pin")) || null,350+ { hitTolerance: 4, layerFilter: isAnnotationLayer },356351 );357352 const hovered = hit?.category.displayType === "text" ? null : hit;358353 setHoveredPin(hovered || null);
@@ -365,6 +360,16 @@ function initializeMap() {
365360 state.engine.getViewport().addEventListener("pointerleave", () => setHoveredPin(null));366361 }367362 363+// Every layer a pin can be drawn in, including the two it moves to while a zone364+// is highlighted. Leaving those out left the pins of the highlighted zone --365+// the only ones still on screen -- answering neither the pointer nor a click.366+function isAnnotationLayer(layer) {367+ return layer === state.layers.pins || layer === state.layers.zonePins ||368+ layer === state.layers.pinLabels || layer === state.layers.priority ||369+ layer === state.layers.text || layer === state.layers.textDetail ||370+ layer === state.layers.zoneText;371+}372+368373 function createView(maxZoom) {369374 const size = state.catalog.tileGrid.size;370375 return new View({
@@ -1486,9 +1491,48 @@ function renderZones() {
14861491 });14871492 }14881493 state.zoneTitleCount = state.sources.zoneTitles.getFeatures().length;1494+ renderZoneScrim();14891495 renderZoneIndex();14901496 }14911497 1498+// Highlighting a zone is a request to look at one place, so everything else1499+// recedes: the whole map is covered and the zone is cut back out of it.1500+// Shading only the neighbouring zones demoted them rather than raising the1501+// zone asked for, and left the unclaimed space between them at full strength.1502+function renderZoneScrim() {1503+ state.sources.zoneScrim.clear();1504+ if (!state.highlightedZones.size) return;1505+ const [left, bottom, right, top] = state.projection.getExtent();1506+ // Rings alternate direction with their depth so the fill counts its way in1507+ // and out: the map, then the zone as a hole, then any hole of the zone's own1508+ // back to solid.1509+ const rings = [orientRing([1510+ [left, bottom], [right, bottom], [right, top], [left, top], [left, bottom],1511+ ], true)];1512+ for (const zoneID of state.highlightedZones) {1513+ for (const geometry of state.zoneRecords.get(zoneID)?.geometries || []) {1514+ const polygons = geometry.getType() === "MultiPolygon"1515+ ? geometry.getPolygons()1516+ : [geometry];1517+ for (const polygon of polygons) {1518+ polygon.getLinearRings().forEach((ring, index) => {1519+ rings.push(orientRing(ring.getCoordinates(), index > 0));1520+ });1521+ }1522+ }1523+ }1524+ state.sources.zoneScrim.addFeature(new Feature({ geometry: new Polygon(rings) }));1525+}1526+1527+// Twice the signed area of a ring: negative when its points run clockwise.1528+function orientRing(ring, clockwise) {1529+ let area = 0;1530+ for (let index = 0, prior = ring.length - 1; index < ring.length; prior = index++) {1531+ area += ring[prior][0] * ring[index][1] - ring[index][0] * ring[prior][1];1532+ }1533+ return (area < 0) === clockwise ? ring : ring.slice().reverse();1534+}1535+14921536 function renderZoneIndex() {14931537 const fragment = document.createDocumentFragment();14941538 for (const { zone, depth } of orderedZones()) {
@@ -1547,6 +1591,7 @@ function orderedZones() {
15471591 function setZonesVisible(visible) {15481592 state.zonesVisible = visible;15491593 elements.zoneToggle.checked = visible;1594+ state.layers.zoneScrim.setVisible(visible);15501595 state.layers.zones.setVisible(visible);15511596 state.layers.zoneTitles.setVisible(visible);15521597 state.layers.zoneTitleDetail.setVisible(visible);
@@ -1579,6 +1624,7 @@ function toggleZoneHighlight(zoneID) {
15791624 updateZonePinFocus();15801625 refreshPinRendering();15811626 updateZoneIndexState();1627+ renderZoneScrim();15821628 state.layers.zones.changed();15831629 state.layers.zoneTitles.changed();15841630 state.layers.zoneTitleDetail.changed();
@@ -2278,12 +2324,11 @@ function zoneStyle(feature) {
22782324 if (state.styleCache.has(key)) return state.styleCache.get(key);22792325 const color = feature.get("color");22802326 if (dimmed) {2327+ // No fill of its own: the scrim below has already taken this zone down2328+ // with the rest of the map, and a second wash would single it out again.22812329 const dimmedStyle = new Style({2282- fill: new Fill({2283- color: `rgba(3, 5, 3, ${child ? 0.38 : 0.32})`,2284- }),22852330 stroke: new Stroke({2286- color: hexToRGBA(color, child ? 0.24 : 0.32),2331+ color: hexToRGBA(color, child ? 0.34 : 0.44),22872332 width: child ? 1 : 1.3,22882333 lineDash: child ? [3, 4] : [7, 5],22892334 }),