fix(zoom): use transform:scale instead of CSS zoom to kill map tile seams

CSS `zoom` re-lays-out and pixel-rounds each element, which opened ~1px seams
between map tiles (a faint grid over the map when zoomed). Switch the persistent
UI zoom to `transform: scale(z)` from the app root's top-left, with the root
counter-sized to (100/z)vw × (100/z)vh so it reflows to fill and scales back to
the window. transform scales the subtree as one composited layer, so tiles stay
seamless. Removed the now-unneeded tile-overlap CSS hack.
This commit is contained in:
2026-07-25 13:34:53 +02:00
parent b7bfd39652
commit 34b60f9f20
2 changed files with 14 additions and 27 deletions
+12 -15
View File
@@ -845,31 +845,28 @@ export default function App() {
useEffect(() => { wkEsmRef.current = wkEsm; }, [wkEsm]);
// Persistent Ctrl+wheel zoom. The native WebView2 Ctrl+wheel zoom isn't saved,
// so we run our own: CSS `zoom` on the app root, factor stored in localStorage
// and restored at startup. Ctrl+0 resets to 100%.
// so we run our own, factor stored in localStorage and restored at startup;
// Ctrl+0 resets to 100%.
//
// `zoom` scales the element uniformly (grid included) but does NOT rescale
// viewport units, so a 100vh/100vw root would leave an empty strip when zoomed
// out. We counter that by sizing the root to (100/z)vw × (100/z)vh so that,
// after the zoom multiplies it back down, it exactly fills the window.
// We use `transform: scale` (NOT CSS `zoom`): `zoom` re-lays-out and rounds each
// element to the pixel grid, which opens ~1px seams between map tiles. `transform`
// scales the whole subtree as one composited layer, so tiles stay seamless. The
// app root is counter-sized to (100/z)vw × (100/z)vh and scaled from its top-left
// by z, so it reflows to fill that larger box and then scales back to exactly the
// window (more content visible when zoomed out, like a real browser zoom).
const appRootRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const KEY = 'opslog.uiZoom';
let z = parseFloat(localStorage.getItem(KEY) || '1');
if (!Number.isFinite(z) || z <= 0) z = 1;
const apply = () => {
// Zoom the whole document (so portalled menus/modals/tooltips scale too)…
(document.documentElement.style as any).zoom = String(z);
// Flag the zoom state so the Leaflet tile-seam fix (style.css) only kicks in
// when actually zoomed (fractional scaling opens 1px gaps between tiles).
document.documentElement.setAttribute('data-uizoom', String(z));
// …and counter-size the app root: `zoom` doesn't rescale vh/vw, so a 100vh
// root leaves an empty strip when zoomed out. (100/z)vh zoomed by z = 100vh.
const el = appRootRef.current;
if (el) {
if (!el) return;
el.style.transformOrigin = '0 0';
el.style.transform = z === 1 ? '' : `scale(${z})`;
el.style.width = `${100 / z}vw`;
el.style.height = `${100 / z}vh`;
}
document.documentElement.setAttribute('data-uizoom', String(z));
};
apply();
const onWheel = (e: WheelEvent) => {
-10
View File
@@ -652,13 +652,3 @@
border: 2px solid var(--background);
}
::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); }
/* When the UI is CSS-zoomed (our persistent Ctrl+wheel zoom), map tiles are
scaled by a fractional factor, which opens ~1px seams between adjacent tiles
(visible as a faint grid over the map). Enlarging each 256px tile by 1px makes
neighbours overlap just enough to hide the seams. Scoped to the zoomed state so
it never touches the crisp 100% view. */
html[data-uizoom]:not([data-uizoom="1"]) .leaflet-tile {
width: 257px !important;
height: 257px !important;
}