OnlyMapJS Latest release
GitHub Docs

← 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.

Open full screen ↗ View source ↗
<!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.