@justai/pano
v0.1.1
Published
Moving a Street View panorama the way a head moves — the short way round, with weight at both ends, and arriving on foot.
Readme
@justai/pano
Moving a Street View panorama the way a head moves — the short way round, with weight at both ends, and arriving on foot rather than appearing.
Lifted from City in Motion's CMS, where the playlist editor had it right and had it welded into an eight-hundred-line React component. It carries no key, loads no script, and knows nothing about Google: it moves whatever panorama it is handed.
Why it is a package
Six of these questions stopped belonging to one product the moment a second one needed them:
| subpath | the question |
|---|---|
| turn | which way should a camera rotate, and with what weight |
| geo | which way is that place from here, and how far |
| timing | how long each move takes — tuned by eye against a real city |
| load | get Google's script onto the page once, whoever asks first |
| camera | move a live panorama's pose smoothly, and know when it has landed |
| walk | how to get from this panorama to that one along the link graph |
| pano | what a panorama IS, structurally — so the rest can be checked alone |
The trigger was the ecosystem's own: a second consumer reaching for it, and the tell that you are
about to copy rather than to import. guess-back wanted shortestDelta, easeInOutCubic and
tweenPov verbatim, and the pano-graph walk is the household's stated differentiator — so a copy
would have been O(N) from its first day.
The panorama is a structure, not a class
google.maps.StreetViewPanorama satisfies the Pano interface with no adapter and no cast, and
nothing here imports a Google type. That is what lets the camera and the walk be checked alone:
the test suite drives them against a forty-line fake with no key, no network and no browser, and
builds three little graphs to prove the walk's three refusals —
- a link pointing away from the target is refused rather than followed;
- two panoramas pointing at each other do not make an infinite corridor;
- and after the flourish, the target is set outright, because the walk is not the transport.
The failure that has no error
fires when a request FAILS. It does not fire when a request SUCCEEDS with an empty body —
which is what an ad blocker, a corporate proxy or a test harness intercepting third-party hosts
does: 204, no content, no error. The script "loads", google.maps is never defined, the callback
never runs, and a loader without a ceiling stays pending forever. Nothing throws and nothing logs;
the page simply never opens.
loadMaps therefore has a timeout, not as belt and braces but as the guard that catches the
common case, and a rejected load clears itself so the next attempt is a real attempt.
A heading always comes back as a compass reads it
shortestDelta returns a signed rotation, so adding it to a heading can land on -160 or 370.
Both are correct angles; neither is a bearing. A live panorama normalises and reports [0, 360), so
the camera does too — every frame, not just the last. Without it a consumer that clamps a heading
behaves differently against a real panorama than against a fake, which is the kind of difference
that is found in production. It was found here by the package's own test.
Every duration is a dial
timing exports named constants rather than hiding numbers in the functions, because those numbers
are not defaults in the sense of something had to go here — they are what the founder arrived at
watching Leiden go past. They travel with the package so the next persona does not re-guess them,
and any of them can be passed per call.
Usage
import { goTo } from "@justai/pano/walk";
import { TURN_IN_PLACE_MS } from "@justai/pano/timing";
// turn to face what is behind you — the short way, over 1.4s
await goTo(panorama, { ...pose, heading: (pose.heading + 180) % 360 });