From 2f591236e100676e784f52b8ad258e9802eee2b8 Mon Sep 17 00:00:00 2001 From: Ari Oppenheimer Date: Wed, 27 May 2026 18:27:03 -0700 Subject: [PATCH 1/4] added hero sample --- samples/3d-hero-showcase/README.md | 41 ++ samples/3d-hero-showcase/index.html | 190 ++++++++ samples/3d-hero-showcase/index.ts | 587 ++++++++++++++++++++++ samples/3d-hero-showcase/package.json | 11 + samples/3d-hero-showcase/style.css | 649 +++++++++++++++++++++++++ samples/3d-hero-showcase/tsconfig.json | 7 + 6 files changed, 1485 insertions(+) create mode 100644 samples/3d-hero-showcase/README.md create mode 100644 samples/3d-hero-showcase/index.html create mode 100644 samples/3d-hero-showcase/index.ts create mode 100644 samples/3d-hero-showcase/package.json create mode 100644 samples/3d-hero-showcase/style.css create mode 100644 samples/3d-hero-showcase/tsconfig.json diff --git a/samples/3d-hero-showcase/README.md b/samples/3d-hero-showcase/README.md new file mode 100644 index 000000000..389f62bc0 --- /dev/null +++ b/samples/3d-hero-showcase/README.md @@ -0,0 +1,41 @@ +# Google Maps JavaScript Sample + +## 3d-hero-showcase + +Add a meaningful description for 3d-hero-showcase here... + +## Setup + +### Before starting run: + +`npm i` + +### Run an example on a local web server + +`cd samples/3d-hero-showcase` +`npm start` + +### Build an individual example + +`cd samples/3d-hero-showcase` +`npm run build` + +From 'samples': + +`npm run build --workspace=3d-hero-showcase/` + +### Build all of the examples. + +From 'samples': + +`npm run build-all` + +### Run lint to check for problems + +`cd samples/3d-hero-showcase` +`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). diff --git a/samples/3d-hero-showcase/index.html b/samples/3d-hero-showcase/index.html new file mode 100644 index 000000000..fb50f3cf5 --- /dev/null +++ b/samples/3d-hero-showcase/index.html @@ -0,0 +1,190 @@ + + + + + + Amsterdam 3D Explorer + + + + + + + + + + + + + + +
+
+
+ + A + m + s + t + e + r + d + a + m + + 3D +
+

Rijksmuseum & attractions

+
+ +
+ +
+ + + + +
+ + +
+

Map Mode

+
+ + +
+
+ + +
+

Map Layers

+
+ + + + + + + + + +
+
+
+ + +
+ + +
+
+

Explore Amsterdam in 3D

+

+ Start the tour to fly around the Rijksmuseum, Vondelpark, + Prinsengracht, and De Gooyer Windmill. +

+ +
+
+ + + diff --git a/samples/3d-hero-showcase/index.ts b/samples/3d-hero-showcase/index.ts new file mode 100644 index 000000000..ea1a1af44 --- /dev/null +++ b/samples/3d-hero-showcase/index.ts @@ -0,0 +1,587 @@ +/* + * @license + * Copyright 2026 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// [START maps_3d_hero_showcase] + +interface TourStop { + name: string; + desc: string; + camera: { + center: { lat: number; lng: number; altitude: number }; + range: number; + tilt: number; + heading: number; + }; + stats: { + size: string; + highlight: string; + }; +} + +const TOUR_STOPS: TourStop[] = [ + { + name: 'Rijksmuseum', + desc: 'The national museum of the Netherlands. The footprint of this grand Gothic-Renaissance building is highlighted, and its 3D building mesh can be toggled on or off.', + camera: { + center: { lat: 52.36, lng: 4.8852, altitude: 40 }, + range: 280, + tilt: 45, + heading: 180, + }, + stats: { + size: '🖼 8,000+ objects', + highlight: "Rembrandt's Night Watch", + }, + }, + { + name: 'Vondelpark', + desc: "Amsterdam's largest and most famous public park. A flat translucent green polygon outlines a section of its lush lawns and tranquil lakes.", + camera: { + center: { lat: 52.358, lng: 4.8685, altitude: 50 }, + range: 600, + tilt: 45, + heading: 250, + }, + stats: { + size: '🌳 120 acres', + highlight: 'Open Air Theatre', + }, + }, + { + name: 'Prinsengracht Canal', + desc: "One of Amsterdam's three main belt canals. Traced in Googly orange is a flat polyline showing the canal tour path floating at water level.", + camera: { + center: { lat: 52.3637, lng: 4.8855, altitude: 30 }, + range: 300, + tilt: 45, + heading: 330, + }, + stats: { + size: '⛵ 3.2 km length', + highlight: 'Historic Houseboats', + }, + }, + { + name: 'De Gooyer Windmill', + desc: 'The tallest wooden mill in the Netherlands. We load a dynamic 3D model of the windmill which rotates in real-time, standing next to the canal.', + camera: { + center: { lat: 52.3667, lng: 4.9263, altitude: 30 }, + range: 180, + tilt: 45, + heading: 135, + }, + stats: { + size: '⚙ 26 meters tall', + highlight: 'Built in 1725', + }, + }, +]; + +// Reference to map elements +let map: google.maps.maps3d.Map3DElement; +let windmillModel: google.maps.maps3d.Model3DElement; +let canalPolyline: google.maps.maps3d.Polyline3DElement; +let vondelparkPolygon: google.maps.maps3d.Polygon3DElement; +let museumPolygon: google.maps.maps3d.Polygon3DElement; +let museumFlattener: google.maps.maps3d.FlattenerElement; + +// Collections of pins +const standardMarkers: google.maps.maps3d.Marker3DInteractiveElement[] = []; +const standardPopovers: google.maps.maps3d.PopoverElement[] = []; + +// Tour State +let isTouring = false; +let currentStopIndex = -1; +let isAnimationCallbackActive = false; +let tourAnimationCleanup: (() => void) | null = null; + +async function init() { + const { + Map3DElement, + Polyline3DElement, + Polygon3DElement, + Polygon3DInteractiveElement, + Model3DElement, + Marker3DInteractiveElement, + PopoverElement, + FlattenerElement, + } = await google.maps.importLibrary('maps3d'); + + const { PinElement } = await google.maps.importLibrary('marker'); + + // 1. Initialize the 3D Map (Centered on Rijksmuseum) + map = new Map3DElement({ + center: { lat: 52.36, lng: 4.8852, altitude: 800 }, + tilt: 40, + heading: 0, + range: 4000, + mode: 'SATELLITE', + gestureHandling: 'COOPERATIVE', + }); + document.body.append(map); + + // 2. Create Layers + + // Polyline: Prinsengracht canal route (flat, orange) + canalPolyline = new Polyline3DElement({ + path: [ + { lat: 52.3622, lng: 4.89149 }, + { lat: 52.36276, lng: 4.88788 }, + { lat: 52.36614, lng: 4.88277 }, + { lat: 52.36673, lng: 4.88242 }, + { lat: 52.36673, lng: 4.88242 }, + ], + strokeColor: '#F37021', + strokeWidth: 6, + altitudeMode: 'CLAMP_TO_GROUND', + extruded: false, + drawsOccludedSegments: true, + }); + + // Polygon 1: Vondelpark lake/lawn zone (flat green) + vondelparkPolygon = new Polygon3DInteractiveElement({ + path: [ + { lat: 52.35639, lng: 4.85497 }, + { lat: 52.36108, lng: 4.87449 }, + { lat: 52.3593, lng: 4.87592 }, + { lat: 52.35511, lng: 4.86683 }, + { lat: 52.35457, lng: 4.85623 }, + ], + strokeColor: '#1e8e3e90', + strokeWidth: 3, + fillColor: '#1e8e3e40', + drawsOccludedSegments: false, + }); + + vondelparkPolygon.addEventListener('gmp-click', () => { + alert( + 'Welcome to Vondelpark! Enjoy the open lawns and winding pathways.' + ); + }); + + // Polygon 2: Rijksmuseum Building footprint (extruded) + museumPolygon = new Polygon3DElement({ + path: [ + { lat: 52.36029, lng: 4.88327, altitude: 25 }, + { lat: 52.36092, lng: 4.88502, altitude: 25 }, + { lat: 52.36011, lng: 4.8867, altitude: 25 }, + { lat: 52.35881, lng: 4.88627, altitude: 25 }, + { lat: 52.3592, lng: 4.88412, altitude: 25 }, + ], + strokeColor: '#4285F490', + strokeWidth: 3, + fillColor: '#4285F440', + altitudeMode: 'RELATIVE_TO_GROUND', + extruded: true, + }); + + // Rijksmuseum 3D flattener + museumFlattener = new FlattenerElement({ + path: [ + { lat: 52.36029, lng: 4.88327 }, + { lat: 52.36092, lng: 4.88502 }, + { lat: 52.36011, lng: 4.8867 }, + { lat: 52.35881, lng: 4.88627 }, + { lat: 52.3592, lng: 4.88412 }, + ], + }); + + // 3D Model: Windmill (placed at De Gooyer Windmill site) + windmillModel = new Model3DElement({ + src: 'https://maps-docs-team.web.app/assets/windmill.glb', + position: { lat: 52.3667, lng: 4.9263 }, + orientation: { heading: 0, tilt: 270, roll: 90 }, + scale: 0.15, + altitudeMode: 'CLAMP_TO_GROUND', + }); + + // 3. Create Standard Markers & Popovers with Custom HTML (at each Tour Stop) + const poiLocations = [ + { + id: 'rijksmuseum', + name: 'Rijksmuseum', + lat: 52.36, + lng: 4.8852, + alt: 35, + desc: 'The national museum of the Netherlands, home to masterpieces by Rembrandt and Vermeer.', + glyph: '🖼', + bg: '#4285F4', + highlight: "Rembrandt's Night Watch", + }, + { + id: 'vondelpark', + name: 'Vondelpark', + lat: 52.358, + lng: 4.8685, + alt: 20, + desc: "Amsterdam's historic public park, filled with cafes, ponds, and paths.", + glyph: '🌳', + bg: '#34A853', + highlight: 'Open Air Theatre', + }, + { + id: 'canal', + name: 'Prinsengracht Canal', + lat: 52.36409, + lng: 4.88584, + alt: 10, + desc: 'The longest of the main canal rings, known for its iconic houseboats.', + glyph: '⛵', + bg: '#FBBC05', + highlight: 'Historic Houseboats', + }, + { + id: 'degooyer', + name: 'De Gooyer Windmill', + lat: 52.3667, + lng: 4.9263, + alt: 30, + desc: 'A historic flour mill built in 1725, the tallest wooden mill in the country.', + glyph: '⚙', + bg: '#EA4335', + highlight: 'Built in 1725', + }, + ]; + + poiLocations.forEach((loc, index) => { + const pin = new PinElement({ + background: loc.bg, + glyph: loc.glyph, + borderColor: '#FFFFFF', + }); + + const interactiveMarker = new Marker3DInteractiveElement({ + position: { lat: loc.lat, lng: loc.lng, altitude: loc.alt }, + altitudeMode: 'RELATIVE_TO_GROUND', + extruded: true, + }); + interactiveMarker.append(pin); + + const popover = new PopoverElement({ + open: false, + positionAnchor: interactiveMarker, + }); + + const popoverContent = document.createElement('div'); + popoverContent.className = 'popover-custom-content'; + popoverContent.innerHTML = ` +
+ ${loc.glyph} +

${loc.name}

+
+

${loc.desc}

+
+
+ Highlight + ${loc.highlight} +
+
+ `; + popover.append(popoverContent); + + interactiveMarker.addEventListener('gmp-click', () => { + // Close others + standardPopovers.forEach((p, i) => { + if (i !== index) p.open = false; + }); + popover.open = !popover.open; + }); + + standardMarkers.push(interactiveMarker); + standardPopovers.push(popover); + }); + + // Sync initial states + syncLayers(); + syncRijksmuseumMesh(); + + // 5. Connect UI Event Listeners + setupUIListeners(); +} + +function syncLayers() { + const showMarkers = ( + document.getElementById('toggle-markers') as HTMLInputElement + ).checked; + const showPolyline = ( + document.getElementById('toggle-polyline') as HTMLInputElement + ).checked; + const showPolygons = ( + document.getElementById('toggle-polygons') as HTMLInputElement + ).checked; + const showModel = ( + document.getElementById('toggle-model') as HTMLInputElement + ).checked; + + // Standard Markers & Popovers + standardMarkers.forEach((marker, index) => { + if (showMarkers) { + map.append(marker); + map.append(standardPopovers[index]); + } else { + marker.remove(); + standardPopovers[index].remove(); + } + }); + + // Polyline + if (showPolyline) { + map.append(canalPolyline); + } else { + canalPolyline.remove(); + } + + // Polygons + if (showPolygons) { + map.append(vondelparkPolygon); + map.append(museumPolygon); + } else { + vondelparkPolygon.remove(); + museumPolygon.remove(); + } + + // 3D Model + if (showModel) { + map.append(windmillModel); + } else { + windmillModel.remove(); + } +} + +function syncRijksmuseumMesh() { + const showMesh = ( + document.getElementById('toggle-salesforce-mesh') as HTMLInputElement + ).checked; + if (showMesh) { + museumFlattener.remove(); + } else { + map.append(museumFlattener); + } +} + +function setupUIListeners() { + // Welcome dismiss + const welcome = document.getElementById('welcome-banner') as HTMLDivElement; + document + .getElementById('btn-welcome-dismiss') + ?.addEventListener('click', () => { + welcome.classList.add('hidden'); + }); + + // Tour buttons + const btnStart = document.getElementById( + 'btn-start-tour' + ) as HTMLButtonElement; + const btnStop = document.getElementById( + 'btn-stop-tour' + ) as HTMLButtonElement; + const tourNav = document.getElementById('btn-prev-stop') + ?.parentElement as HTMLDivElement; + + btnStart.addEventListener('click', () => { + isTouring = true; + btnStart.classList.add('hidden'); + btnStop.classList.remove('hidden'); + tourNav.classList.remove('hidden'); + welcome.classList.add('hidden'); + jumpToStop(0); + }); + + btnStop.addEventListener('click', () => { + endTour(); + }); + + document.getElementById('btn-prev-stop')?.addEventListener('click', () => { + if (currentStopIndex > 0) { + jumpToStop(currentStopIndex - 1); + } + }); + + document.getElementById('btn-next-stop')?.addEventListener('click', () => { + if (currentStopIndex < TOUR_STOPS.length - 1) { + jumpToStop(currentStopIndex + 1); + } + }); + + // Layer Toggle Listeners + document + .getElementById('toggle-markers') + ?.addEventListener('change', syncLayers); + + document + .getElementById('toggle-polyline') + ?.addEventListener('change', syncLayers); + document + .getElementById('toggle-polygons') + ?.addEventListener('change', syncLayers); + document + .getElementById('toggle-salesforce-mesh') + ?.addEventListener('change', syncRijksmuseumMesh); + document + .getElementById('toggle-model') + ?.addEventListener('change', syncLayers); + + // Map Mode Selectors + const modeSat = document.getElementById( + 'mode-satellite' + ) as HTMLButtonElement; + const modeHyb = document.getElementById('mode-hybrid') as HTMLButtonElement; + + modeSat.addEventListener('click', () => { + map.mode = 'SATELLITE'; + modeSat.classList.add('active'); + modeHyb.classList.remove('active'); + }); + + modeHyb.addEventListener('click', () => { + map.mode = 'HYBRID'; + modeHyb.classList.add('active'); + modeSat.classList.remove('active'); + }); + + // Listener for camera interrupt + map.addEventListener('gmp-click', () => { + if (isTouring) { + console.log( + 'Tour camera animation interrupted by user interaction.' + ); + } + }); +} + +function jumpToStop(index: number) { + if (tourAnimationCleanup) { + tourAnimationCleanup(); + } + + currentStopIndex = index; + isAnimationCallbackActive = true; + + // Update Tour Nav UI + const prevBtn = document.getElementById( + 'btn-prev-stop' + ) as HTMLButtonElement; + const nextBtn = document.getElementById( + 'btn-next-stop' + ) as HTMLButtonElement; + const progressText = document.getElementById( + 'tour-progress' + ) as HTMLSpanElement; + + prevBtn.disabled = index === 0; + nextBtn.disabled = index === TOUR_STOPS.length - 1; + progressText.textContent = `Stop ${String(index + 1)} of ${String(TOUR_STOPS.length)}`; + + // Open active stop popover and close all others + standardPopovers.forEach((p, i) => { + p.open = i === index; + }); + + const stop = TOUR_STOPS[index]; + const flightStartTime = Date.now(); + + // Perform camera FlyTo Animation (18s duration for cinematic smoothness) + map.flyCameraTo({ + endCamera: stop.camera, + durationMillis: 18000, + }); + + const listener = () => { + const elapsed = Date.now() - flightStartTime; + if ( + isAnimationCallbackActive && + currentStopIndex === index && + elapsed >= 17000 + ) { + if (tourAnimationCleanup === cleanup) { + tourAnimationCleanup = null; + } + map.removeEventListener('gmp-animationend', listener); + flyAroundStop(index); + } + }; + + const cleanup = () => { + map.removeEventListener('gmp-animationend', listener); + }; + tourAnimationCleanup = cleanup; + + map.addEventListener('gmp-animationend', listener); +} + +function flyAroundStop(index: number) { + isAnimationCallbackActive = false; // Prevent loop trigger + const stop = TOUR_STOPS[index]; + const spinStartTime = Date.now(); + + // Slower, smoother rotation (30s duration) + map.flyCameraAround({ + camera: stop.camera, + durationMillis: 30000, + repeatCount: 1, + }); + + const listener = () => { + const elapsed = Date.now() - spinStartTime; + if (isTouring && currentStopIndex === index && elapsed >= 29000) { + if (tourAnimationCleanup === cleanup) { + tourAnimationCleanup = null; + } + map.removeEventListener('gmp-animationend', listener); + // Wait 3 seconds at current view, then fly to the next stop + window.setTimeout(() => { + if (isTouring && currentStopIndex === index) { + if (index < TOUR_STOPS.length - 1) { + jumpToStop(index + 1); + } else { + endTour(); + } + } + }, 3000); + } + }; + + const cleanup = () => { + map.removeEventListener('gmp-animationend', listener); + }; + tourAnimationCleanup = cleanup; + + map.addEventListener('gmp-animationend', listener); +} + +function endTour() { + isTouring = false; + currentStopIndex = -1; + isAnimationCallbackActive = false; + map.stopCameraAnimation(); + + if (tourAnimationCleanup) { + tourAnimationCleanup(); + tourAnimationCleanup = null; + } + + // Close all popovers + standardPopovers.forEach((p) => (p.open = false)); + + const btnStart = document.getElementById( + 'btn-start-tour' + ) as HTMLButtonElement; + const btnStop = document.getElementById( + 'btn-stop-tour' + ) as HTMLButtonElement; + const tourNav = document.getElementById('btn-prev-stop') + ?.parentElement as HTMLDivElement; + + btnStart.classList.remove('hidden'); + btnStop.classList.add('hidden'); + tourNav.classList.add('hidden'); +} + +window.addEventListener('load', () => { + void init(); +}); + +// [END maps_3d_hero_showcase] diff --git a/samples/3d-hero-showcase/package.json b/samples/3d-hero-showcase/package.json new file mode 100644 index 000000000..6e9cc1ad7 --- /dev/null +++ b/samples/3d-hero-showcase/package.json @@ -0,0 +1,11 @@ +{ + "name": "@js-api-samples/3d-hero-showcase", + "version": "1.0.0", + "scripts": { + "build": "bash ../build-single.sh", + "test": "tsc && npm run build:vite --workspace=.", + "start": "tsc && vite build --base './' && vite", + "build:vite": "vite build --base './'", + "preview": "vite preview" + } +} diff --git a/samples/3d-hero-showcase/style.css b/samples/3d-hero-showcase/style.css new file mode 100644 index 000000000..285232b39 --- /dev/null +++ b/samples/3d-hero-showcase/style.css @@ -0,0 +1,649 @@ +/* + * @license + * Copyright 2026 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* [START maps_3d_hero_showcase] */ +:root { + --bg-color: #ffffff; + --border-color: #dadce0; + --text-primary: #202124; + --text-secondary: #5f6368; + --google-blue: #1a73e8; + --google-blue-hover: #1557b0; + --google-blue-light: #e8f0fe; + --google-red: #d93025; + --google-red-hover: #b31412; + --google-green: #1e8e3e; + --google-yellow: #f9ab00; + --light-grey: #f8f9fa; + --card-shadow: + 0 1px 3px rgba(60, 64, 67, 0.3), 0 4px 8px 3px rgba(60, 64, 67, 0.15); + --font-sans: 'Google Sans', 'Outfit', 'Roboto', sans-serif; +} + +html, +body { + height: 100%; + margin: 0; + padding: 0; + font-family: var(--font-sans); + background-color: #f1f3f4; + overflow: hidden; + color: var(--text-primary); +} + +/* Base Map */ +gmp-map-3d { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + z-index: 1; +} + +/* Googly Floating Cards */ +.google-card { + background: var(--bg-color); + border: 1px solid var(--border-color); + border-radius: 20px; + box-shadow: var(--card-shadow); + z-index: 10; + display: flex; + flex-direction: column; + box-sizing: border-box; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + overflow: hidden; +} + +/* Control Panel Sidebar */ +#control-panel { + position: absolute; + top: 16px; + left: 16px; + width: 330px; + max-height: calc(100% - 32px); + padding: 18px; +} + +.card-header { + padding-bottom: 12px; + border-bottom: 1px solid #e8eaed; + margin-bottom: 14px; +} + +.logo-area { + display: flex; + align-items: center; + gap: 8px; +} + +.logo-text { + font-size: 21px; + font-weight: 700; + letter-spacing: -0.5px; +} + +.badge-3d { + background: var(--google-blue-light); + color: var(--google-blue); + font-size: 10px; + font-weight: 700; + padding: 2px 6px; + border-radius: 6px; +} + +/* Google Brand Colors */ +.g-blue { + color: var(--google-blue); +} +.g-red { + color: var(--google-red); +} +.g-yellow { + color: var(--google-yellow); +} +.g-green { + color: var(--google-green); +} + +.subtitle { + font-size: 12px; + color: var(--text-secondary); + margin: 4px 0 0 0; + font-weight: 500; +} + +.scrollable-content { + flex: 1; + overflow-y: auto; + padding-right: 4px; +} + +/* Scrollbar styles */ +.scrollable-content::-webkit-scrollbar { + width: 4px; +} +.scrollable-content::-webkit-scrollbar-track { + background: transparent; +} +.scrollable-content::-webkit-scrollbar-thumb { + background: #dadce0; + border-radius: 10px; +} + +.action-block { + margin-bottom: 16px; +} + +.option-block { + border-top: 1px solid #e8eaed; + padding-top: 14px; + margin-bottom: 16px; +} + +.option-block h3 { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.8px; + color: var(--text-secondary); + margin-top: 0; + margin-bottom: 10px; + font-weight: 700; +} + +/* Google style Buttons */ +.google-btn { + font-family: var(--font-sans); + font-size: 13.5px; + font-weight: 500; + border-radius: 20px; + padding: 9px 18px; + border: 1px solid var(--border-color); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + background: #ffffff; + color: var(--google-blue); + transition: + background-color 0.2s, + border-color 0.2s, + box-shadow 0.2s; + width: 100%; +} + +.google-btn:hover { + background-color: var(--light-grey); + border-color: #dadce0; + box-shadow: 0 1px 2px rgba(60, 64, 67, 0.3); +} + +.primary-btn { + background: var(--google-blue); + color: white; + border: none; +} + +.primary-btn:hover { + background: var(--google-blue-hover); + box-shadow: + 0 1px 3px rgba(60, 64, 67, 0.3), + 0 1px 2px rgba(0, 0, 0, 0.15); +} + +.danger-btn { + background: var(--google-red); + color: white; + border: none; +} + +.danger-btn:hover { + background: var(--google-red-hover); + box-shadow: 0 1px 3px rgba(60, 64, 67, 0.3); +} + +.secondary-btn { + background: #ffffff; + color: var(--text-primary); +} + +.secondary-btn:hover { + background: var(--light-grey); +} + +.text-btn { + background: transparent; + border: none; + color: var(--google-blue); + font-weight: 700; + width: auto; + padding: 6px 12px; +} + +.text-btn:hover { + background: var(--google-blue-light); + box-shadow: none; +} + +.small-btn { + padding: 5px 12px; + font-size: 11.5px; + border-radius: 12px; + width: auto; +} + +/* Tour Nav pills */ +.tour-nav { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 10px; + background: var(--light-grey); + padding: 6px 10px; + border-radius: 14px; + border: 1px solid var(--border-color); +} + +.nav-arrow { + background: transparent; + border: none; + cursor: pointer; + font-size: 11px; + color: var(--text-secondary); + width: 28px; + height: 28px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.nav-arrow:hover:not(:disabled) { + background: #dadce0; + color: var(--text-primary); +} + +.nav-arrow:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +.progress-text { + font-size: 12.5px; + font-weight: 500; + color: var(--text-primary); +} + +/* Switch Rows */ +.toggle-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.switch-row { + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; +} + +.switch-label { + font-size: 13.5px; + font-weight: 500; + color: var(--text-primary); + display: flex; + align-items: center; +} + +.switch-label .emoji { + margin-right: 8px; + font-size: 15px; +} + +/* Lever switches */ +.switch-control { + position: relative; + width: 32px; + height: 14px; +} + +.switch-control input { + opacity: 0; + width: 0; + height: 0; +} + +.lever { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #bdc1c6; + transition: 0.15s; + border-radius: 14px; +} + +.lever:before { + position: absolute; + content: ''; + height: 20px; + width: 20px; + left: -4px; + bottom: -3px; + background-color: #ffffff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); + transition: 0.15s; + border-radius: 50%; +} + +input:checked + .lever { + background-color: #8ab4f8; +} + +input:checked + .lever:before { + transform: translateX(18px); + background-color: var(--google-blue); +} + +/* Segmented view controls */ +.segmented-control { + display: flex; + background: var(--light-grey); + padding: 2px; + border-radius: 12px; + border: 1px solid var(--border-color); +} + +.segment-btn { + flex: 1; + background: transparent; + border: none; + color: var(--text-secondary); + padding: 6px 10px; + border-radius: 10px; + font-size: 12.5px; + font-weight: 500; + cursor: pointer; + font-family: var(--font-sans); + transition: all 0.15s; +} + +.segment-btn.active { + background: #ffffff; + color: var(--google-blue); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); + font-weight: 700; +} + +.card-footer { + border-top: 1px solid #e8eaed; + padding-top: 8px; + margin-top: 12px; + font-size: 10.5px; + color: var(--text-secondary); + text-align: center; + font-weight: 500; +} + +/* Bottom Info Drawer */ +.info-drawer { + position: absolute; + bottom: 20px; + left: calc(50% + 165px); /* Adjusted center offset for Googly panel */ + transform: translateX(-50%); + width: 550px; + padding: 16px 20px; + max-width: calc(100% - 390px); +} + +.drawer-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +#info-title { + font-size: 18px; + font-weight: 700; + margin: 0; + color: var(--text-primary); + letter-spacing: -0.3px; +} + +#info-desc { + font-size: 13.5px; + color: var(--text-secondary); + margin: 0 0 12px 0; + line-height: 1.45; +} + +.metrics-row { + display: flex; + gap: 20px; + background: var(--light-grey); + border-radius: 12px; + padding: 8px 16px; + border: 1px solid var(--border-color); +} + +.metric-item { + flex: 1; + display: flex; + flex-direction: column; +} + +.metric-lbl { + font-size: 10px; + text-transform: uppercase; + color: var(--text-secondary); + margin-bottom: 2px; + font-weight: 700; + letter-spacing: 0.5px; +} + +.metric-val { + font-size: 13.5px; + font-weight: 700; + color: var(--text-primary); +} + +.highlight-val { + color: var(--google-blue); +} + +/* Welcome Overlay */ +.welcome-toast { + position: absolute; + top: 16px; + right: 16px; + width: 280px; + padding: 14px; +} + +.toast-content h3 { + margin-top: 0; + margin-bottom: 4px; + font-size: 14px; + font-weight: 700; +} + +.toast-content p { + font-size: 12.5px; + color: var(--text-secondary); + margin: 0 0 10px 0; + line-height: 1.4; +} + +.toast-content button { + float: right; +} + +/* Helpers */ +.hidden, +.google-card.hidden, +.google-btn.hidden, +.tour-nav.hidden { + display: none; +} + +.google-card.hidden { + opacity: 0; + pointer-events: none; + transform: translate(-50%, 15px); +} + +/* Googly Custom Marker Badge */ +.custom-badge { + display: flex; + align-items: center; + gap: 8px; + background: #ffffff; + border: 1px solid var(--border-color); + border-radius: 20px; + padding: 4px 10px 4px 4px; + color: var(--text-primary); + font-family: var(--font-sans); + font-size: 12px; + font-weight: 700; + box-shadow: + 0 2px 6px rgba(60, 64, 67, 0.15), + 0 1px 2px rgba(60, 64, 67, 0.3); + white-space: nowrap; + user-select: none; + cursor: pointer; + transition: + transform 0.2s, + box-shadow 0.2s; + transform-origin: bottom center; +} + +.custom-badge:hover { + transform: scale(1.05); + box-shadow: + 0 4px 12px rgba(60, 64, 67, 0.2), + 0 1px 3px rgba(60, 64, 67, 0.35); + border-color: #bdc1c6; +} + +.badge-icon { + width: 24px; + height: 24px; + border-radius: 50%; + background: var(--google-blue-light); + color: var(--google-blue); + display: flex; + align-items: center; + justify-content: center; + font-size: 13px; +} + +.badge-details { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.badge-title { + font-size: 9px; + color: var(--text-secondary); + font-weight: 500; + line-height: 1; + margin-bottom: 1px; +} + +.badge-value { + font-size: 11.5px; + font-weight: 700; + color: var(--text-primary); + line-height: 1; +} + +/* Responsive */ +@media (max-width: 900px) { + #control-panel { + top: 10px; + left: 10px; + width: calc(100% - 20px); + max-height: 45%; + } +} + +/* Popover Custom Styling */ +.popover-custom-content { + width: 260px; + color: var(--text-primary); + font-family: var(--font-sans); + background: #ffffff; + padding: 2px; +} + +.popover-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; + border-bottom: 1px solid #e8eaed; + padding-bottom: 6px; +} + +.popover-emoji { + font-size: 18px; +} + +.popover-header h4 { + font-size: 15px; + margin: 0; + font-weight: 700; + color: var(--text-primary); +} + +.popover-desc { + font-size: 12.5px; + color: var(--text-secondary); + margin: 0 0 10px 0; + line-height: 1.4; +} + +.popover-stats { + display: flex; + gap: 12px; + background: var(--light-grey); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 6px 10px; +} + +.popover-stat-item { + flex: 1; + display: flex; + flex-direction: column; +} + +.popover-stat-lbl { + font-size: 8px; + text-transform: uppercase; + color: var(--text-secondary); + font-weight: 700; + letter-spacing: 0.3px; + margin-bottom: 2px; +} + +.popover-stat-val { + font-size: 11.5px; + font-weight: 700; + color: var(--text-primary); + line-height: 1.1; +} + +.popover-stat-val.highlight-val { + color: var(--google-blue); +} +/* [END maps_3d_hero_showcase] */ diff --git a/samples/3d-hero-showcase/tsconfig.json b/samples/3d-hero-showcase/tsconfig.json new file mode 100644 index 000000000..976bcc6ef --- /dev/null +++ b/samples/3d-hero-showcase/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "." + }, + "include": ["./*.ts"] +} From b5509d9ba1683f30ce6dccfb3f4cbe278aa28a63 Mon Sep 17 00:00:00 2001 From: Ari Oppenheimer Date: Thu, 28 May 2026 10:13:35 -0700 Subject: [PATCH 2/4] changed order of toggle options --- samples/3d-hero-showcase/index.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/samples/3d-hero-showcase/index.html b/samples/3d-hero-showcase/index.html index fb50f3cf5..aa292ee28 100644 --- a/samples/3d-hero-showcase/index.html +++ b/samples/3d-hero-showcase/index.html @@ -112,13 +112,13 @@

Map Layers