Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions samples/3d-camera-position/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Google Maps JavaScript Sample

## 3d-camera-position

An interactive playground designed to help developers understand and experiment with camera positioning in Google Maps 3D.

## Features
- **Live Camera Controls**: Real-time sliders for adjusting `heading`, `tilt`, `range`, and `fov` properties.
- **Coordinate Mapping**: Direct controls to set the camera's focal point via `Latitude`, `Longitude`, and `Altitude`.
- **Code Generator**: Dynamically generates the resulting `<gmp-map-3d>` HTML tag with mapped properties for easy copying and pasting.
- **Continuous Event Syncing**: Listens to map interaction events (like dragging and panning) to keep UI readouts strictly synchronized with live map state.

## Setup

### Before starting run:

`npm i`

### Run an example on a local web server

`cd samples/3d-camera-position`
`npm start`

### Build an individual example

`cd samples/3d-camera-position`
`npm run build`

From 'samples':

`npm run build --workspace=3d-camera-position/`

### Build all of the examples.

From 'samples':

`npm run build-all`

### Run lint to check for problems

`cd samples/3d-camera-position`
`npx eslint index.ts`

## Feedback

For feedback related to this sample, please open a new issue on
[GitHub](https://github.com/googlemaps-samples/js-api-samples/issues).
139 changes: 139 additions & 0 deletions samples/3d-camera-position/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<!doctype html>
<!--
@license
Copyright 2026 Google LLC. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
-->
<!-- [START maps_3d_camera_position] -->
<html>
<head>
<title>Google Maps 3D - Camera Position Controller</title>
<link rel="stylesheet" type="text/css" href="./style.css" />
<script type="module" src="./index.js"></script>
<!-- prettier-ignore -->
<script>(g => { var h, a, k, p = "The Google Maps JavaScript API", c = "google", l = "importLibrary", q = "__ib__", m = document, b = window; b = b[c] || (b[c] = {}); var d = b.maps || (b.maps = {}), r = new Set, e = new URLSearchParams, u = () => h || (h = new Promise(async (f, n) => { await (a = m.createElement("script")); e.set("libraries", [...r] + ""); for (k in g) e.set(k.replace(/[A-Z]/g, t => "_" + t[0].toLowerCase()), g[k]); e.set("callback", c + ".maps." + q); a.src = `https://maps.${c}apis.com/maps/api/js?` + e; d[q] = f; a.onerror = () => h = n(Error(p + " could not load.")); a.nonce = m.querySelector("script[nonce]")?.nonce || ""; m.head.append(a) })); d[l] ? console.warn(p + " only loads once. Ignoring:", g) : d[l] = (f, ...n) => r.add(f) && u().then(() => d[l](f, ...n)) })
({ key: "AIzaSyA6myHzS10YXdcazAFalmXvDkrYCp5cLc8", v: "weekly" });</script>
</head>

<body>
<gmp-map-3d
center="40.7811,-73.9599,0"
mode="HYBRID"
tilt="76"
range="3270"
heading="-154"
fov="35"></gmp-map-3d>
Comment thread
willum070 marked this conversation as resolved.
Outdated
<div id="ui-container">
<div class="panel">
<div class="control-group">
<label for="heading"
>Heading: <span id="heading-val">0</span>&deg;</label
>
<input
type="range"
id="heading"
min="-180"
max="180"
value="0"
step="1" />
</div>

<div class="control-group">
<label for="tilt"
>Tilt: <span id="tilt-val">45</span>&deg;</label
>
<input
type="range"
id="tilt"
min="0"
max="90"
value="45"
step="1" />
</div>

<div class="control-group">
<label for="range"
>Range: <span id="range-val">1000</span>m</label
>
<input
type="range"
id="range"
min="100"
max="10000"
value="1000"
step="100" />
</div>

<div class="control-group row">
<div class="col">
<label for="lat">Latitude</label>
<input
type="number"
id="lat"
min="-90"
max="90"
value="40.7040"
step="0.0001" />
</div>
<div class="col">
<label for="lng">Longitude</label>
<input
type="number"
id="lng"
min="-180"
max="180"
value="-74.0180"
step="0.0001" />
</div>
</div>

<div class="control-group">
<label for="altitude"
>Altitude: <span id="altitude-val">30</span>m</label
>
<input
type="range"
id="altitude"
min="0"
max="5000"
value="30"
step="10" />
</div>

<div class="control-group">
<label for="fov"
>FOV: <span id="fov-val">45</span>&deg;</label
>
<input
type="range"
id="fov"
min="5"
max="80"
value="45"
Comment thread
willum070 marked this conversation as resolved.
Outdated
step="1" />
</div>

<div class="control-group">
<label for="roll"
>Roll: <span id="roll-val">0</span>&deg;</label
>
<input
type="range"
id="roll"
min="-180"
max="180"
value="0"
step="1" />
</div>

<div class="status-group">
<div class="code-box">
<pre><code id="generated-code">&lt;gmp-map-3d center="40.704,-74.018,1000" mode="hybrid" tilt="45" range="1000" heading="0"&gt;&lt;/gmp-map-3d&gt;</code></pre>
Comment thread
willum070 marked this conversation as resolved.
Outdated
</div>
<button id="copy-btn">Copy HTML</button>
</div>
</div>
</div>
</body>
</html>
<!-- [END maps_3d_camera_position] -->
166 changes: 166 additions & 0 deletions samples/3d-camera-position/index.ts
Comment thread
willum070 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* @license
* Copyright 2026 Google LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
// [START maps_3d_camera_position]
async function initMap(): Promise<void> {
// Declare the needed libraries.
await google.maps.importLibrary('maps3d');

// prettier-ignore
// @ts-ignore
const map3DElement = document.querySelector('gmp-map-3d') as google.maps.Map3DElement;

// Elements from HTML
const headingSlider = document.getElementById(
'heading'
) as HTMLInputElement;
const tiltSlider = document.getElementById('tilt') as HTMLInputElement;
const rangeSlider = document.getElementById('range') as HTMLInputElement;
const latSlider = document.getElementById('lat') as HTMLInputElement;
const lngSlider = document.getElementById('lng') as HTMLInputElement;
const altitudeSlider = document.getElementById(
'altitude'
) as HTMLInputElement;
const fovSlider = document.getElementById('fov') as HTMLInputElement;
const rollSlider = document.getElementById('roll') as HTMLInputElement;

const headingVal = document.getElementById('heading-val') as HTMLElement;
const tiltVal = document.getElementById('tilt-val') as HTMLElement;
const rangeVal = document.getElementById('range-val') as HTMLElement;
const altitudeVal = document.getElementById('altitude-val') as HTMLElement;
const fovVal = document.getElementById('fov-val') as HTMLElement;
const rollVal = document.getElementById('roll-val') as HTMLElement;
const codeElem = document.getElementById('generated-code') as HTMLElement;
const copyBtn = document.getElementById('copy-btn') as HTMLButtonElement;

let currentAltitude = 30;
let isUserInteracting = false;

// Update values on UI when the map changes.
const updateUI = () => {
const heading = map3DElement.heading.toFixed(0);
const tilt = map3DElement.tilt.toFixed(0);
const range = map3DElement.range.toFixed(0);
const rawFov = parseFloat(map3DElement.fov.toFixed(0));
const fovClamped = Math.min(80, Math.max(5, rawFov));
const fov = fovClamped.toString();
const roll = map3DElement.roll ? map3DElement.roll.toFixed(0) : '0';
const center = map3DElement.center;
const mode = map3DElement.mode;

headingVal.textContent = heading;
tiltVal.textContent = tilt;
rangeVal.textContent = range;
fovVal.textContent = fov;
rollVal.textContent = roll;

if (!isUserInteracting) {
Comment thread
willum070 marked this conversation as resolved.
fovSlider.value = fov;
headingSlider.value = heading;
tiltSlider.value = tilt;
rangeSlider.value = Math.min(10000, parseFloat(range)).toString();
rollSlider.value = roll;
}

if (center) {
const lat = center.lat.toFixed(4);
const lng = center.lng.toFixed(4);
const alt = currentAltitude.toFixed(0);

latSlider.value = lat;
lngSlider.value = lng;
altitudeVal.textContent = alt;

codeElem.textContent = `<gmp-map-3d center="${lat},${lng},${alt}" mode="${mode}" tilt="${tilt}" range="${range}" heading="${heading}" fov="${fov}" roll="${roll}"></gmp-map-3d>`;
}
};

// Copy generated HTML to clipboard.
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(codeElem.textContent || '');
copyBtn.textContent = 'Copied!';
setTimeout(() => {
copyBtn.textContent = 'Copy HTML';
}, 2000);
});

// Listen to slider changes.
const handleSliderInput = (e: Event, prop: string) => {
isUserInteracting = true;
const val = parseFloat((e.target as HTMLInputElement).value);
map3DElement[prop] = val;
updateUI();
};

const resetInteraction = () => {
isUserInteracting = false;
};

headingSlider.addEventListener('input', (e) =>
handleSliderInput(e, 'heading')
);
headingSlider.addEventListener('change', resetInteraction);

tiltSlider.addEventListener('input', (e) => handleSliderInput(e, 'tilt'));
tiltSlider.addEventListener('change', resetInteraction);

rangeSlider.addEventListener('input', (e) => handleSliderInput(e, 'range'));
rangeSlider.addEventListener('change', resetInteraction);

fovSlider.addEventListener('input', (e) => handleSliderInput(e, 'fov'));
fovSlider.addEventListener('change', resetInteraction);

rollSlider.addEventListener('input', (e) => handleSliderInput(e, 'roll'));
rollSlider.addEventListener('change', resetInteraction);

latSlider.addEventListener('input', (e) => {
const val = parseFloat((e.target as HTMLInputElement).value);
const currentCenter = map3DElement.center;
map3DElement.center = {
lat: val,
lng: currentCenter.lng,
altitude: currentCenter.altitude,
};
updateUI();
});

lngSlider.addEventListener('input', (e) => {
const val = parseFloat((e.target as HTMLInputElement).value);
const currentCenter = map3DElement.center;
map3DElement.center = {
lat: currentCenter.lat,
lng: val,
altitude: currentCenter.altitude,
};
updateUI();
});

altitudeSlider.addEventListener('input', (e) => {
const val = parseFloat((e.target as HTMLInputElement).value);
currentAltitude = val;
const currentCenter = map3DElement.center;
map3DElement.center = {
lat: currentCenter.lat,
lng: currentCenter.lng,
altitude: val,
};
updateUI();
});

// Update UI on camera change events.
map3DElement.addEventListener('gmp-animationend', updateUI);
map3DElement.addEventListener('gmp-click', updateUI);
Comment thread
willum070 marked this conversation as resolved.
Outdated
map3DElement.addEventListener('gmp-centerchange', updateUI);
Comment thread
willum070 marked this conversation as resolved.
Outdated
map3DElement.addEventListener('gmp-headingchange', updateUI);
map3DElement.addEventListener('gmp-tiltchange', updateUI);
map3DElement.addEventListener('gmp-rangechange', updateUI);
map3DElement.addEventListener('gmp-fovchange', updateUI);

// Initial UI sync
setTimeout(updateUI, 500);
}

initMap();
// [END maps_3d_camera_position]
12 changes: 12 additions & 0 deletions samples/3d-camera-position/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@js-api-samples/3d-camera-position",
"version": "1.0.0",
"scripts": {
"build": "tsc && bash ../jsfiddle.sh 3d-camera-position && bash ../app.sh 3d-camera-position && bash ../docs.sh 3d-camera-position && npm run build:vite --workspace=. && bash ../dist.sh 3d-camera-position",
"test": "tsc && npm run build:vite --workspace=.",
"start": "tsc && vite build --base './' && vite",
"build:vite": "vite build --base './'",
"preview": "vite preview"
},
"dependencies": {}
}
Loading
Loading