clone

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

feat: a jump from a panel gives the view back

Dami committed 016b45e
Opening a feature from the results panel or the legend flies the camera to it,
which is the point of the row -- but it also spends the view the reader was
looking at, and the only way back was to find their place again by hand. A
list is for glancing at things; glancing should not cost a place.

The camera each jump leaves is held for as long as the card is open, and
closing the card hands it back. Each jump holds afresh over the last, so
closing undoes that card's own move and nothing older: reading three rows in
turn steps back through them one view at a time. A hand on the map -- a drag,
the wheel, the zoom buttons, the overview, descending into a cell -- lets the
hold go, because after that the view the reader is in is the view they chose
and returning from it would be the map arguing with them. A jump into another
world or another lens is not this camera's to give back either.

A location clicked on the map holds nothing: it was already in front of the
reader, and no camera moved to show it.
commit
016b45e8898349d9109d6f74a9b537dc47ef25ab
parent
c6eb7d60297312761334d566936ce7a9fa8d933e
author
Dami <dami.etoile@gmail.com>
kind
feat risk score 1.8 of 3, Jev's estimate
7 files changed, +82 -5
Mfrontend/src/areas.js5++-
Mfrontend/src/detail.js10++++++-
Mfrontend/src/engine.js8++++-
Mfrontend/src/grid.js6+++-
Mfrontend/src/navigation.js49++++++++++++++++++++++++++++++
Mfrontend/src/overview.js5++-
Mfrontend/src/state.js4++
M frontend/src/areas.js +4 -1
M frontend/src/detail.js +9 -1
M frontend/src/engine.js +7 -1
M frontend/src/grid.js +5 -1
M frontend/src/navigation.js +49 -0
@@ -111,6 +111,8 @@ export async function selectWorld(slug) {
111111 state.search = "";112112 elements.search.value = "";113113 elements.dock.hidden = false;114+ // A way back into the map being left behind is no way back at all.115+ releaseCameraReturn();114116 closeDetail();115117 renderLegend();116118 renderShapes();
@@ -324,6 +326,7 @@ export function activeExtent() {
324326 325327 export function fitMap() {326328 if (!state.engine || !state.lens) return;329+ releaseCameraReturn();327330 const view = state.engine.getView();328331 view.fit(activeExtent(), {329332 size: state.engine.getSize(),
@@ -336,6 +339,7 @@ export function fitMap() {
336339 }337340 338341 export function changeZoom(delta) {342+ releaseCameraReturn();339343 const view = state.engine.getView();340344 const current = view.getZoom() || 0;341345 view.animate({
@@ -379,3 +383,48 @@ export function updateVisibleCount() {
379383 elements.visibleCount.textContent = `${formatNumber(inView)} of ` +380384 `${formatNumber(shown)} ${shown === 1 ? "feature" : "features"} in view`;381385 }386+387+// A jump made from a panel is a detour. The reader asked to look at one388+// feature, not to give up the view they were reading it against -- and until389+// now the only way back from a row in the list was to find their place again390+// by hand. The camera the jump left is held for as long as the card is open,391+// and closing the card hands it back.392+//393+// Each jump holds afresh, overwriting whatever the last one left: closing a394+// card undoes that card's own move and nothing older. Reading three rows in395+// turn steps back through them one view at a time, and none of the three can396+// hand back a place from before the reader went looking. Anything that steers397+// the map -- a drag, the wheel, the zoom buttons, the overview, a cell -- lets398+// the hold go entirely, because after that the view they are in is the view399+// they chose, and returning from it would be the map arguing with them.400+export function holdCameraReturn() {401+ if (!state.engine) return;402+ const view = state.engine.getView();403+ const center = view.getCenter();404+ const zoom = view.getZoom();405+ if (!center || !Number.isFinite(zoom)) return;406+ state.cameraReturn = {407+ center: center.slice(),408+ zoom,409+ world: state.world,410+ lens: state.lens,411+ };412+}413+414+export function releaseCameraReturn() {415+ state.cameraReturn = null;416+}417+418+export function returnCamera() {419+ const held = state.cameraReturn;420+ state.cameraReturn = null;421+ if (!held || !state.engine) return;422+ // A view belonging to another map, or to another layer of this one, is not423+ // this camera's to give back.424+ if (held.world !== state.world || held.lens !== state.lens) return;425+ state.engine.getView().animate({426+ center: held.center,427+ zoom: held.zoom,428+ duration: 220,429+ });430+}
M frontend/src/overview.js +4 -1
M frontend/src/state.js +4 -0