← Gallery / Stories & animation
Google 3D Tiles flyby story
Fly a camera across photorealistic 3D Tiles as a scripted story, with a player and a scrubber.
<!DOCTYPE html>
<!--
A story flown through Google Photorealistic 3D Tiles. The tour is nothing
but fly-to steps — the same `<om-story>` any map takes — but over a 3D
tileset every move streams new tiles, so each stop dwells for a few
seconds while the buildings sharpen around the camera. Let a 3D tour pause
at every destination and the detail arrives while the card is read.
The tour: four San Francisco landmarks, Ferry Building to Coit Tower.
Bring a browser-restricted Google API key with the Map Tiles API enabled,
paste it in the panel — or open the page as ?key=YOUR_API_KEY — then
press ▶.
-->
<html>
<head>
<meta charset="utf-8" />
<title>OnlyMapJS — Google 3D Tiles flyby story</title>
<link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap/dist/onlymapjs.css" />
<script type="module" src="https://unpkg.com/@nika-js/onlymap/dist/onlymap.standalone.js"></script>
<style>
html, body { margin: 0; height: 100%; font-family: system-ui, sans-serif; }
om-map { display: block; height: 100vh; }
</style>
<style>
.hud {
position: fixed;
left: 16px;
top: 60px;
z-index: 10001;
width: min(420px, calc(100vw - 32px));
padding: 12px 14px;
border-radius: 8px;
background: rgba(18, 22, 28, .9);
color: #fff;
font: 13px/1.45 system-ui, sans-serif;
box-shadow: 0 10px 28px rgba(0,0,0,.28);
}
.hud code { color: #b9d4ff; }
.hud input {
box-sizing: border-box;
width: 100%;
margin-top: 8px;
padding: 7px 9px;
border: 1px solid rgba(255,255,255,.25);
border-radius: 6px;
background: rgba(255,255,255,.08);
color: #fff;
font: inherit;
}
.hud button {
margin-top: 8px;
padding: 7px 10px;
border: 0;
border-radius: 6px;
background: #4f7cff;
color: #fff;
font: inherit;
cursor: pointer;
}
.hud[data-ready="true"] .setup { display: none; }
.hud[data-ready="false"] .ready { display: none; }
.note { color: rgba(255,255,255,.78); margin-top: 6px; }
.stop-card {
background: rgba(20, 24, 31, .9);
color: #fff;
border: 1px solid rgba(255,255,255,.22);
border-radius: 8px;
padding: 10px 12px;
width: 220px;
box-shadow: 0 8px 24px rgba(0,0,0,.35);
font: 13px/1.35 system-ui, sans-serif;
}
.stop-card h3 { margin: 0 0 4px; font-size: 14px; }
.stop-card p { margin: 0; color: rgba(255,255,255,.78); }
</style>
</head>
<body>
<om-map center="[-122.397, 37.793]" zoom="15.4" pitch="67" bearing="28" basemap="none">
<om-overlay id="stop-ferry" anchor="[-122.3930,37.7955]" visible="false" anchor-offset="bottom-center">
<div class="stop-card"><h3>Ferry Building</h3><p>Dwell here and watch pier/building tiles refine after the camera settles.</p></div>
</om-overlay>
<om-overlay id="stop-transamerica" anchor="[-122.4028,37.7952]" visible="false" anchor-offset="bottom-center">
<div class="stop-card"><h3>Transamerica Pyramid</h3><p>A short lateral flight: useful for spotting tile pop-in along vertical facades.</p></div>
</om-overlay>
<om-overlay id="stop-salesforce" anchor="[-122.3960,37.7897]" visible="false" anchor-offset="bottom-center">
<div class="stop-card"><h3>Salesforce Tower</h3><p>High-rise close-up with aggressive detail settings: watch texture sharpness after motion.</p></div>
</om-overlay>
<om-overlay id="stop-coit" anchor="[-122.4058,37.8024]" visible="false" anchor-offset="bottom-center">
<div class="stop-card"><h3>Coit Tower</h3><p>Longer hop north: this stresses new tile requests and replacement refinement.</p></div>
</om-overlay>
<om-story id="flyby" interrupt="pause">
<om-step duration="4500ms" action="fly-to" center="[-122.3930,37.7955]" zoom="17.2" pitch="68" bearing="55" curve></om-step>
<om-step action="show-overlay" target="stop-ferry" duration="3500ms"></om-step>
<om-step action="hide-overlay" target="stop-ferry"></om-step>
<om-step duration="4500ms" action="fly-to" center="[-122.4028,37.7952]" zoom="17.5" pitch="70" bearing="-35" curve></om-step>
<om-step action="show-overlay" target="stop-transamerica" duration="3500ms"></om-step>
<om-step action="hide-overlay" target="stop-transamerica"></om-step>
<om-step duration="4500ms" action="fly-to" center="[-122.3960,37.7897]" zoom="17.6" pitch="72" bearing="135" curve></om-step>
<om-step action="show-overlay" target="stop-salesforce" duration="3500ms"></om-step>
<om-step action="hide-overlay" target="stop-salesforce"></om-step>
<om-step duration="5500ms" action="fly-to" center="[-122.4058,37.8024]" zoom="16.8" pitch="70" bearing="-145" curve></om-step>
<om-step action="show-overlay" target="stop-coit" duration="4500ms"></om-step>
<om-step action="hide-overlay" target="stop-coit"></om-step>
<om-step duration="4500ms" action="fly-to" center="[-122.397,37.793]" zoom="15.4" pitch="67" bearing="28" curve></om-step>
</om-story>
<om-widget type="player" story="flyby" position="bottom-left"></om-widget>
<om-widget type="zoom-controls" position="top-right"></om-widget>
<om-widget type="attribution"
text="3D tiles © Google. Requires Google Map Tiles API access."
position="bottom-right"></om-widget>
</om-map>
<div class="hud" id="hud" data-ready="false">
<div class="setup">
<strong>Google 3D Tiles Flyby</strong><br />
Add a browser-restricted Map Tiles API key as <code>?key=...</code>, or paste it here. Press play after the layer loads.
<input id="key" autocomplete="off" spellcheck="false" placeholder="Google Map Tiles API key" />
<button id="apply" type="button">Load tiles</button>
</div>
<div class="ready">
Tiles loaded into the story scene. Press play and watch whether each stop sharpens during the dwell.
<div class="note">This demo uses <code>maximum-screen-space-error="2"</code> and <code>maximum-memory-usage="256"</code>.</div>
</div>
</div>
<script type="module">
const STORAGE_KEY = "onlymapjs.google3dtiles.key";
const ROOT = "https://tile.googleapis.com/v1/3dtiles/root.json";
const params = new URLSearchParams(location.search);
const map = document.querySelector("om-map");
const hud = document.querySelector("#hud");
const input = document.querySelector("#key");
const apply = document.querySelector("#apply");
function ensureLayer() {
let layer = document.querySelector("#google-photorealistic");
if (layer) return layer;
layer = document.createElement("om-layer");
layer.id = "google-photorealistic";
layer.setAttribute("type", "Tile3DLayer");
layer.setAttribute("label", "Google Photorealistic 3D Tiles");
layer.setAttribute("opacity", "1");
layer.setAttribute("maximum-screen-space-error", "2");
layer.setAttribute("maximum-memory-usage", "256");
layer.setAttribute("view-distance-scale", "0.85");
map.prepend(layer);
return layer;
}
function useKey(key) {
const trimmed = key.trim();
if (!trimmed) return;
localStorage.setItem(STORAGE_KEY, trimmed);
const layer = ensureLayer();
layer.setAttribute("tileset", `${ROOT}?key=${encodeURIComponent(trimmed)}`);
hud.dataset.ready = "true";
}
input.value = params.get("key") || localStorage.getItem(STORAGE_KEY) || "";
apply.addEventListener("click", () => useKey(input.value));
input.addEventListener("keydown", (event) => {
if (event.key === "Enter") useKey(input.value);
});
if (input.value) useKey(input.value);
</script>
</body>
</html>
The snippet above loads the latest release. The demo above it is pinned to v0.5.2, so it keeps working when a new version ships.