Skip to content

Commit e747ee4

Browse files
committed
feat: Creates new 3D camera position sample.
1 parent 072acf3 commit e747ee4

6 files changed

Lines changed: 588 additions & 0 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Google Maps JavaScript Sample
2+
3+
## 3d-camera-position
4+
5+
An interactive playground designed to help developers understand and experiment with camera positioning in Google Maps 3D.
6+
7+
## Features
8+
- **Live Camera Controls**: Real-time sliders for adjusting `heading`, `tilt`, `range`, and `fov` properties.
9+
- **Coordinate Mapping**: Direct controls to set the camera's focal point via `Latitude`, `Longitude`, and `Altitude`.
10+
- **Code Generator**: Dynamically generates the resulting `<gmp-map-3d>` HTML tag with mapped properties for easy copying and pasting.
11+
- **Continuous Event Syncing**: Listens to map interaction events (like dragging and panning) to keep UI readouts strictly synchronized with live map state.
12+
13+
## Setup
14+
15+
### Before starting run:
16+
17+
`npm i`
18+
19+
### Run an example on a local web server
20+
21+
`cd samples/3d-camera-position`
22+
`npm start`
23+
24+
### Build an individual example
25+
26+
`cd samples/3d-camera-position`
27+
`npm run build`
28+
29+
From 'samples':
30+
31+
`npm run build --workspace=3d-camera-position/`
32+
33+
### Build all of the examples.
34+
35+
From 'samples':
36+
37+
`npm run build-all`
38+
39+
### Run lint to check for problems
40+
41+
`cd samples/3d-camera-position`
42+
`npx eslint index.ts`
43+
44+
## Feedback
45+
46+
For feedback related to this sample, please open a new issue on
47+
[GitHub](https://github.com/googlemaps-samples/js-api-samples/issues).
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<!doctype html>
2+
<!--
3+
@license
4+
Copyright 2026 Google LLC. All Rights Reserved.
5+
SPDX-License-Identifier: Apache-2.0
6+
-->
7+
<!-- [START maps_3d_camera_position] -->
8+
<html>
9+
<head>
10+
<title>Google Maps 3D - Camera Position Controller</title>
11+
<link rel="stylesheet" type="text/css" href="./style.css" />
12+
<script type="module" src="./index.js"></script>
13+
<!-- prettier-ignore -->
14+
<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)) })
15+
({ key: "AIzaSyA6myHzS10YXdcazAFalmXvDkrYCp5cLc8", v: "weekly" });</script>
16+
</head>
17+
18+
<body>
19+
<gmp-map-3d
20+
center="40.7811,-73.9599,0"
21+
mode="HYBRID"
22+
tilt="76"
23+
range="3270"
24+
heading="-154"
25+
fov="35"></gmp-map-3d>
26+
<div id="ui-container">
27+
<div class="panel">
28+
<div class="control-group">
29+
<label for="heading"
30+
>Heading: <span id="heading-val">0</span>&deg;</label
31+
>
32+
<input
33+
type="range"
34+
id="heading"
35+
min="-180"
36+
max="180"
37+
value="0"
38+
step="1" />
39+
</div>
40+
41+
<div class="control-group">
42+
<label for="tilt"
43+
>Tilt: <span id="tilt-val">45</span>&deg;</label
44+
>
45+
<input
46+
type="range"
47+
id="tilt"
48+
min="0"
49+
max="90"
50+
value="45"
51+
step="1" />
52+
</div>
53+
54+
<div class="control-group">
55+
<label for="range"
56+
>Range: <span id="range-val">1000</span>m</label
57+
>
58+
<input
59+
type="range"
60+
id="range"
61+
min="100"
62+
max="10000"
63+
value="1000"
64+
step="100" />
65+
</div>
66+
67+
<div class="control-group row">
68+
<div class="col">
69+
<label for="lat">Latitude</label>
70+
<input
71+
type="number"
72+
id="lat"
73+
min="-90"
74+
max="90"
75+
value="40.7040"
76+
step="0.0001" />
77+
</div>
78+
<div class="col">
79+
<label for="lng">Longitude</label>
80+
<input
81+
type="number"
82+
id="lng"
83+
min="-180"
84+
max="180"
85+
value="-74.0180"
86+
step="0.0001" />
87+
</div>
88+
</div>
89+
90+
<div class="control-group">
91+
<label for="altitude"
92+
>Altitude: <span id="altitude-val">30</span>m</label
93+
>
94+
<input
95+
type="range"
96+
id="altitude"
97+
min="0"
98+
max="5000"
99+
value="30"
100+
step="10" />
101+
</div>
102+
103+
<div class="control-group">
104+
<label for="fov"
105+
>FOV: <span id="fov-val">45</span>&deg;</label
106+
>
107+
<input
108+
type="range"
109+
id="fov"
110+
min="5"
111+
max="80"
112+
value="45"
113+
step="1" />
114+
</div>
115+
116+
<div class="control-group">
117+
<label for="roll"
118+
>Roll: <span id="roll-val">0</span>&deg;</label
119+
>
120+
<input
121+
type="range"
122+
id="roll"
123+
min="-180"
124+
max="180"
125+
value="0"
126+
step="1" />
127+
</div>
128+
129+
<div class="status-group">
130+
<div class="code-box">
131+
<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>
132+
</div>
133+
<button id="copy-btn">Copy HTML</button>
134+
</div>
135+
</div>
136+
</div>
137+
</body>
138+
</html>
139+
<!-- [END maps_3d_camera_position] -->
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*
2+
* @license
3+
* Copyright 2026 Google LLC. All Rights Reserved.
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
// [START maps_3d_camera_position]
7+
async function initMap(): Promise<void> {
8+
// Declare the needed libraries.
9+
await google.maps.importLibrary('maps3d');
10+
11+
// prettier-ignore
12+
// @ts-ignore
13+
const map3DElement = document.querySelector('gmp-map-3d') as google.maps.Map3DElement;
14+
15+
// Elements from HTML
16+
const headingSlider = document.getElementById(
17+
'heading'
18+
) as HTMLInputElement;
19+
const tiltSlider = document.getElementById('tilt') as HTMLInputElement;
20+
const rangeSlider = document.getElementById('range') as HTMLInputElement;
21+
const latSlider = document.getElementById('lat') as HTMLInputElement;
22+
const lngSlider = document.getElementById('lng') as HTMLInputElement;
23+
const altitudeSlider = document.getElementById(
24+
'altitude'
25+
) as HTMLInputElement;
26+
const fovSlider = document.getElementById('fov') as HTMLInputElement;
27+
const rollSlider = document.getElementById('roll') as HTMLInputElement;
28+
29+
const headingVal = document.getElementById('heading-val') as HTMLElement;
30+
const tiltVal = document.getElementById('tilt-val') as HTMLElement;
31+
const rangeVal = document.getElementById('range-val') as HTMLElement;
32+
const altitudeVal = document.getElementById('altitude-val') as HTMLElement;
33+
const fovVal = document.getElementById('fov-val') as HTMLElement;
34+
const rollVal = document.getElementById('roll-val') as HTMLElement;
35+
const codeElem = document.getElementById('generated-code') as HTMLElement;
36+
const copyBtn = document.getElementById('copy-btn') as HTMLButtonElement;
37+
38+
let currentAltitude = 30;
39+
let isUserInteracting = false;
40+
41+
// Update values on UI when the map changes.
42+
const updateUI = () => {
43+
const heading = map3DElement.heading.toFixed(0);
44+
const tilt = map3DElement.tilt.toFixed(0);
45+
const range = map3DElement.range.toFixed(0);
46+
const rawFov = parseFloat(map3DElement.fov.toFixed(0));
47+
const fovClamped = Math.min(80, Math.max(5, rawFov));
48+
const fov = fovClamped.toString();
49+
const roll = map3DElement.roll ? map3DElement.roll.toFixed(0) : '0';
50+
const center = map3DElement.center;
51+
const mode = map3DElement.mode;
52+
53+
headingVal.textContent = heading;
54+
tiltVal.textContent = tilt;
55+
rangeVal.textContent = range;
56+
fovVal.textContent = fov;
57+
rollVal.textContent = roll;
58+
59+
if (!isUserInteracting) {
60+
fovSlider.value = fov;
61+
headingSlider.value = heading;
62+
tiltSlider.value = tilt;
63+
rangeSlider.value = Math.min(10000, parseFloat(range)).toString();
64+
rollSlider.value = roll;
65+
}
66+
67+
if (center) {
68+
const lat = center.lat.toFixed(4);
69+
const lng = center.lng.toFixed(4);
70+
const alt = currentAltitude.toFixed(0);
71+
72+
latSlider.value = lat;
73+
lngSlider.value = lng;
74+
altitudeVal.textContent = alt;
75+
76+
codeElem.textContent = `<gmp-map-3d center="${lat},${lng},${alt}" mode="${mode}" tilt="${tilt}" range="${range}" heading="${heading}" fov="${fov}" roll="${roll}"></gmp-map-3d>`;
77+
}
78+
};
79+
80+
// Copy generated HTML to clipboard.
81+
copyBtn.addEventListener('click', () => {
82+
navigator.clipboard.writeText(codeElem.textContent || '');
83+
copyBtn.textContent = 'Copied!';
84+
setTimeout(() => {
85+
copyBtn.textContent = 'Copy HTML';
86+
}, 2000);
87+
});
88+
89+
// Listen to slider changes.
90+
const handleSliderInput = (e: Event, prop: string) => {
91+
isUserInteracting = true;
92+
const val = parseFloat((e.target as HTMLInputElement).value);
93+
map3DElement[prop] = val;
94+
updateUI();
95+
};
96+
97+
const resetInteraction = () => {
98+
isUserInteracting = false;
99+
};
100+
101+
headingSlider.addEventListener('input', (e) =>
102+
handleSliderInput(e, 'heading')
103+
);
104+
headingSlider.addEventListener('change', resetInteraction);
105+
106+
tiltSlider.addEventListener('input', (e) => handleSliderInput(e, 'tilt'));
107+
tiltSlider.addEventListener('change', resetInteraction);
108+
109+
rangeSlider.addEventListener('input', (e) => handleSliderInput(e, 'range'));
110+
rangeSlider.addEventListener('change', resetInteraction);
111+
112+
fovSlider.addEventListener('input', (e) => handleSliderInput(e, 'fov'));
113+
fovSlider.addEventListener('change', resetInteraction);
114+
115+
rollSlider.addEventListener('input', (e) => handleSliderInput(e, 'roll'));
116+
rollSlider.addEventListener('change', resetInteraction);
117+
118+
latSlider.addEventListener('input', (e) => {
119+
const val = parseFloat((e.target as HTMLInputElement).value);
120+
const currentCenter = map3DElement.center;
121+
map3DElement.center = {
122+
lat: val,
123+
lng: currentCenter.lng,
124+
altitude: currentCenter.altitude,
125+
};
126+
updateUI();
127+
});
128+
129+
lngSlider.addEventListener('input', (e) => {
130+
const val = parseFloat((e.target as HTMLInputElement).value);
131+
const currentCenter = map3DElement.center;
132+
map3DElement.center = {
133+
lat: currentCenter.lat,
134+
lng: val,
135+
altitude: currentCenter.altitude,
136+
};
137+
updateUI();
138+
});
139+
140+
altitudeSlider.addEventListener('input', (e) => {
141+
const val = parseFloat((e.target as HTMLInputElement).value);
142+
currentAltitude = val;
143+
const currentCenter = map3DElement.center;
144+
map3DElement.center = {
145+
lat: currentCenter.lat,
146+
lng: currentCenter.lng,
147+
altitude: val,
148+
};
149+
updateUI();
150+
});
151+
152+
// Update UI on camera change events.
153+
map3DElement.addEventListener('gmp-animationend', updateUI);
154+
map3DElement.addEventListener('gmp-click', updateUI);
155+
map3DElement.addEventListener('gmp-centerchange', updateUI);
156+
map3DElement.addEventListener('gmp-headingchange', updateUI);
157+
map3DElement.addEventListener('gmp-tiltchange', updateUI);
158+
map3DElement.addEventListener('gmp-rangechange', updateUI);
159+
map3DElement.addEventListener('gmp-fovchange', updateUI);
160+
161+
// Initial UI sync
162+
setTimeout(updateUI, 500);
163+
}
164+
165+
initMap();
166+
// [END maps_3d_camera_position]
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "@js-api-samples/3d-camera-position",
3+
"version": "1.0.0",
4+
"scripts": {
5+
"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",
6+
"test": "tsc && npm run build:vite --workspace=.",
7+
"start": "tsc && vite build --base './' && vite",
8+
"build:vite": "vite build --base './'",
9+
"preview": "vite preview"
10+
},
11+
"dependencies": {}
12+
}

0 commit comments

Comments
 (0)