Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions frontend/app/components/map/TileLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,11 @@ function TileLayer({

// Snap tile edges to whole screen pixels so neighbours meet without gaps
// or overlaps, which would darken see-through rasters such as prosperity.
// The position goes in a transform, not left/top: at a fractional device
// pixel ratio (Windows at 125 %, say) Chrome painted a box placed by
// left/top up to a quarter pixel off its snapped edge, so the first column
// and row of every tile let the page through and the map showed a grid of
// dark lines. A translate keeps the exact position.
const screenDpr = typeof window === "undefined" ? 1 : window.devicePixelRatio || 1;
const mapPxPerScreenPx = 1 / (view.displayScale * screenDpr);
const snap = (value: number) =>
Expand Down Expand Up @@ -234,10 +239,9 @@ function TileLayer({
decoding="sync"
onLoad={(event) => handleLoad(event.currentTarget, loadedKey(tile))}
onError={reportTileError}
className="absolute max-w-none select-none"
className="absolute left-0 top-0 max-w-none select-none"
style={{
left,
top,
transform: `translate(${left}px, ${top}px)`,
width: snap(tile.left + tile.width) - left,
height: snap(tile.top + tile.height) - top,
opacity: !fadeIn || loaded ? 1 : 0,
Expand Down
27 changes: 18 additions & 9 deletions frontend/app/hooks/useMapViewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@ function prefersReducedMotion(): boolean {
);
}

/** `value` (CSS pixels) moved onto the nearest whole device pixel. */
function onDevicePixel(value: number): number {
const ratio = typeof window === "undefined" ? 1 : window.devicePixelRatio || 1;
return Math.round(value * ratio) / ratio;
}

export function readViewportSize(element: HTMLElement): Size {
const rect = element.getBoundingClientRect();
return { w: rect.width, h: rect.height };
Expand Down Expand Up @@ -204,6 +210,8 @@ export function useMapViewport({
const appliedZoom = restingZoom ? zoomBase : 1;
const appliedZoomRef = useRef(appliedZoom);
appliedZoomRef.current = appliedZoom;
const restingZoomRef = useRef(restingZoom);
restingZoomRef.current = restingZoom;
const liveActiveRef = useRef(false);
if (!liveActiveRef.current) transformRef.current = transform;
const onLiveTransformRef = useRef(onLiveTransform);
Expand Down Expand Up @@ -268,10 +276,15 @@ export function useMapViewport({
// outline filter) stand down until the map settles.
content.setAttribute("data-gesturing", "");
content.style.transition = "none";
// A drag only moves the map: like the resting position (see below) it
// sits on whole device pixels, or every tile edge straddles a pixel and
// the seams between tiles show as a grid of dark lines while it moves.
const liveScale = displayScale / appliedZoomRef.current;
const pixelAligned = restingZoomRef.current && liveScale === 1;
content.style.transform = viewportTransformStyle(
displayScale / appliedZoomRef.current,
next.translateX,
next.translateY
liveScale,
pixelAligned ? onDevicePixel(next.translateX) : next.translateX,
pixelAligned ? onDevicePixel(next.translateY) : next.translateY
);
onLiveTransformRef.current?.({
displayScale,
Expand Down Expand Up @@ -807,14 +820,10 @@ export function useMapViewport({
// edges (see TileLayer) land on pixel boundaries too; a sub-pixel shift no
// one can see.
const restingOnZoom = restingZoom && appliedZoom === displayScale;
const devicePixelRatio =
typeof window === "undefined" ? 1 : window.devicePixelRatio || 1;
const onDevicePixel = (value: number) =>
restingOnZoom ? Math.round(value * devicePixelRatio) / devicePixelRatio : value;
const transformStyle = viewportTransformStyle(
displayScale / appliedZoom,
onDevicePixel(transform.translateX),
onDevicePixel(transform.translateY)
restingOnZoom ? onDevicePixel(transform.translateX) : transform.translateX,
restingOnZoom ? onDevicePixel(transform.translateY) : transform.translateY
);
const cursorClassName = isPanning ? "cursor-grabbing" : "cursor-grab";
const transformTransition = isPanning ? undefined : transition;
Expand Down