Kameraposition steuern

Die Kartenansicht wird so modelliert, als wäre eine Kamera auf eine ebene Fläche ausgerichtet. Die Position der Kamera (und damit das Rendern der Karte) wird durch folgende Eigenschaften festgelegt: latitude, longitude, altitude, heading, tilt, range und fov.

Das Kameraziel ist der Standort in der Kartenmitte, angegeben in Breiten- und Längengradkoordinaten. Sie können die Position der Kamera auf zwei Arten angeben:

  • Nach Zielpunkt: Verwenden Sie das Attribut center, um eine Koordinate auf der Karte anzugeben, auf die die Kamera gerichtet sein soll. Das ist am besten, wenn Sie sicherstellen möchten, dass eine Sehenswürdigkeit oder ein Bereich im Fokus ist.
  • Anhand von Kamerakoordinaten: Mit der Property cameraPosition können Sie die Kamera an bestimmten Koordinaten für Breiten- und Längengrad sowie Höhe platzieren. Das ist ideal, um einen genauen Blickwinkel zu definieren.

Logisch gesehen sind center und cameraPosition verknüpft. Wenn Sie einen Wert festlegen, wird der andere automatisch anhand der Ausrichtung und Entfernung der Kamera berechnet. Mit Properties wie tilt, heading und roll wird die Ausrichtung der Kamera gesteuert, unabhängig davon, welche Positionierungsmethode Sie verwenden.

Im folgenden Beispiel können Sie zwischen center (Kamera auf die Kartenmitte gerichtet) und cameraPosition (Kamera in der Kartenmitte positioniert) wechseln.

Vollständigen Quellcode des Beispiels ansehen

TypeScript

async function init(): Promise<void> {
    // Import the needed libraries.
    await google.maps.importLibrary('maps3d');

    const map3DElement = document.querySelector('gmp-map-3d')!;
    const btn = document.getElementById('switch-mode-btn') as HTMLButtonElement;

    const initialCenter = { lat: 40.7860524, lng: -73.9634983, altitude: 0 };
    let isCenterMode = true;

    btn.addEventListener('click', () => {
        if (isCenterMode) {
            // Switch to Camera Position Mode.
            // Place the camera at the marker's location, but 50m up in the air
            map3DElement.cameraPosition = { ...initialCenter, altitude: 50 };
            map3DElement.tilt = 80;

            btn.textContent = 'Switch to Center Mode';
            isCenterMode = false;
        } else {
            // Revert back to Center Mode (looking AT the marker)
            map3DElement.center = initialCenter;
            map3DElement.tilt = 70;
            map3DElement.range = 1500; // Restore the original range value.

            btn.textContent = 'Switch to Camera Position';
            isCenterMode = true;
        }
    });
}

void init();

JavaScript

async function init() {
    // Import the needed libraries.
    await google.maps.importLibrary('maps3d');

    const map3DElement = document.querySelector('gmp-map-3d');
    const btn = document.getElementById('switch-mode-btn');

    const initialCenter = { lat: 40.7860524, lng: -73.9634983, altitude: 0 };
    let isCenterMode = true;

    btn.addEventListener('click', () => {
        if (isCenterMode) {
            // Switch to Camera Position Mode.
            // Place the camera at the marker's location, but 50m up in the air
            map3DElement.cameraPosition = { ...initialCenter, altitude: 50 };
            map3DElement.tilt = 80;

            btn.textContent = 'Switch to Center Mode';
            isCenterMode = false;
        } else {
            // Revert back to Center Mode (looking AT the marker)
            map3DElement.center = initialCenter;
            map3DElement.tilt = 70;
            map3DElement.range = 1500; // Restore the original range value.

            btn.textContent = 'Switch to Camera Position';
            isCenterMode = true;
        }
    });
}

void init();

CSS

html,
body {
    height: 100%;
    margin: 0;
    padding: 0;
}

#ui-container {
    position: absolute;
    top: 20px;
    left: 20px;
    z-index: 10;
}

button {
    background: rgba(15, 23, 42, 0.75);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border: 1px solid rgba(255, 255, 255, 0.1);
    color: #f8fafc;
    padding: 12px 20px;
    border-radius: 8px;
    cursor: pointer;
    font-size: 0.9rem;
    font-weight: 600;
    transition: all 0.2s ease;
    box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
}

button:hover {
    background: rgba(56, 189, 248, 0.2);
    border-color: rgba(56, 189, 248, 0.4);
    transform: translateY(-1px);
}

button:active {
    transform: translateY(0);
}

HTML

<html>
    <head>
        <title>3D Camera Position</title>

        <link rel="stylesheet" type="text/css" href="./style.css" />
        <script type="module" src="./index.js"></script>
        <script>
            // prettier-ignore
            (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: "GOOGLE_MAPS_API_KEY" 
            });
        </script>
    </head>

    <body>
        <gmp-map-3d
            center="40.7860524,-73.9634983"
            range="1500"
            tilt="70"
            heading="-150"
            mode="satellite">
            <gmp-marker
                position="40.7860524,-73.9634983"
                altitude-mode="clamp-to-ground"></gmp-marker>
        </gmp-map-3d>

        <div id="ui-container">
            <button id="switch-mode-btn" type="button">
                Switch to Camera Position
            </button>
        </div>
    </body>
</html>

Sichtfeld und Reichweite

In einer 3D-Umgebung wird das Konzept des „Zooms“ durch zwei verschiedene Parameter gesteuert: range und fov (Sichtfeld). Beide beeinflussen, wie groß Objekte auf der Karte dargestellt werden, verwenden dafür aber unterschiedliche Mechanismen:

  • range: Der physische Abstand zwischen der Kamera und ihrem Mittelpunkt. Das Anpassen des Bereichs ist so, als würde man eine Kamera näher an ein Motiv heran- oder weiter davon wegbewegen.
  • fov: Der vertikale Winkel des Kameraobjektivs, gemessen in Grad. Das Anpassen des Sichtfelds entspricht dem Wechseln des Objektivs einer Kamera. Ein höherer Wert (bis zu 80 Grad) wirkt wie ein Weitwinkelobjektiv und zeigt mehr von der Peripherie, während ein niedrigerer Wert (bis zu 5 Grad) wie ein Teleobjektiv wirkt und den Fokus verengt.

Im folgenden Beispiel können Sie ausprobieren, wie die verschiedenen Optionen für die Karten- und Kamerapositionierung zusammenwirken. Sie können Parameter über die Schieberegler in der Benutzeroberfläche festlegen oder direkt mit der Karte und den Kartensteuerelementen interagieren. Die resultierenden Parameter werden einem gmp-map-3d-Element hinzugefügt, das Sie kopieren und wiederverwenden können.

Vollständigen Quellcode des Beispiels ansehen

TypeScript

async function init(): Promise<void> {
    // Declare the needed libraries.
    await google.maps.importLibrary('maps3d');

    const map3DElement = document.querySelector('gmp-map-3d')!;

    // 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 fovSlider = document.getElementById('fov') as HTMLInputElement;
    const rollSlider = document.getElementById('roll') as HTMLInputElement;

    const headingVal = document.getElementById('heading-val')!;
    const tiltVal = document.getElementById('tilt-val')!;
    const rangeVal = document.getElementById('range-val')!;
    const altitudeVal = document.getElementById('altitude-val')!;
    const fovVal = document.getElementById('fov-val')!;
    const rollVal = document.getElementById('roll-val')!;
    const codeElem = document.getElementById('generated-code')!;
    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) ?? '0';
        const tilt = map3DElement.tilt?.toFixed(0) ?? '0';
        const range = map3DElement.range?.toFixed(0) ?? '0';
        const rawFov = parseFloat(map3DElement.fov?.toFixed(0) ?? '45');
        const fovClamped = Math.min(80, Math.max(5, rawFov));
        const fov = fovClamped.toString();
        const 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) {
            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', () => {
        void navigator.clipboard.writeText(codeElem.textContent || '');
        copyBtn.textContent = 'Copied!';
        setTimeout(() => {
            copyBtn.textContent = 'Copy HTML';
        }, 2000);
    });

    // Listen to slider changes using event delegation.
    const panel = document.querySelector('.panel')!;

    panel.addEventListener('input', (e) => {
        const target = e.target as HTMLInputElement;
        if (target.tagName !== 'INPUT') return;

        isUserInteracting = true;
        const prop = target.name;
        const val = parseFloat(target.value);

        if (prop === 'lat') {
            const currentCenter = map3DElement.center;
            if (currentCenter) {
                map3DElement.center = {
                    lat: val,
                    lng: currentCenter.lng,
                    altitude: currentCenter.altitude,
                };
            }
        } else if (prop === 'lng') {
            const currentCenter = map3DElement.center;
            if (currentCenter) {
                map3DElement.center = {
                    lat: currentCenter.lat,
                    lng: val,
                    altitude: currentCenter.altitude,
                };
            }
        } else if (prop === 'altitude') {
            currentAltitude = val;
            const currentCenter = map3DElement.center;
            if (currentCenter) {
                map3DElement.center = {
                    lat: currentCenter.lat,
                    lng: currentCenter.lng,
                    altitude: val,
                };
            }
        } else if (prop === 'tilt') {
            map3DElement.tilt = Math.max(0, val);
        } else if (prop === 'fov') {
            map3DElement.fov = Math.min(80, Math.max(5, val));
        } else {
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            (map3DElement as any)[prop] = val;
        }
        updateUI();
    });

    panel.addEventListener('change', (e) => {
        const target = e.target as HTMLInputElement;
        if (target.tagName === 'INPUT') {
            isUserInteracting = false;
        }
    });

    // Update UI on camera change events.
    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);
}

void init();

JavaScript

async function init() {
    // Declare the needed libraries.
    await google.maps.importLibrary('maps3d');

    const map3DElement = document.querySelector('gmp-map-3d');

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

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

    let currentAltitude = 30;
    let isUserInteracting = false;

    // Update values on UI when the map changes.
    const updateUI = () => {
        const heading = map3DElement.heading?.toFixed(0) ?? '0';
        const tilt = map3DElement.tilt?.toFixed(0) ?? '0';
        const range = map3DElement.range?.toFixed(0) ?? '0';
        const rawFov = parseFloat(map3DElement.fov?.toFixed(0) ?? '45');
        const fovClamped = Math.min(80, Math.max(5, rawFov));
        const fov = fovClamped.toString();
        const 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) {
            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', () => {
        void navigator.clipboard.writeText(codeElem.textContent || '');
        copyBtn.textContent = 'Copied!';
        setTimeout(() => {
            copyBtn.textContent = 'Copy HTML';
        }, 2000);
    });

    // Listen to slider changes using event delegation.
    const panel = document.querySelector('.panel');

    panel.addEventListener('input', (e) => {
        const target = e.target;
        if (target.tagName !== 'INPUT') return;

        isUserInteracting = true;
        const prop = target.name;
        const val = parseFloat(target.value);

        if (prop === 'lat') {
            const currentCenter = map3DElement.center;
            if (currentCenter) {
                map3DElement.center = {
                    lat: val,
                    lng: currentCenter.lng,
                    altitude: currentCenter.altitude,
                };
            }
        } else if (prop === 'lng') {
            const currentCenter = map3DElement.center;
            if (currentCenter) {
                map3DElement.center = {
                    lat: currentCenter.lat,
                    lng: val,
                    altitude: currentCenter.altitude,
                };
            }
        } else if (prop === 'altitude') {
            currentAltitude = val;
            const currentCenter = map3DElement.center;
            if (currentCenter) {
                map3DElement.center = {
                    lat: currentCenter.lat,
                    lng: currentCenter.lng,
                    altitude: val,
                };
            }
        } else if (prop === 'tilt') {
            map3DElement.tilt = Math.max(0, val);
        } else if (prop === 'fov') {
            map3DElement.fov = Math.min(80, Math.max(5, val));
        } else {
            map3DElement[prop] = val;
        }
        updateUI();
    });

    panel.addEventListener('change', (e) => {
        const target = e.target;
        if (target.tagName === 'INPUT') {
            isUserInteracting = false;
        }
    });

    // Update UI on camera change events.
    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);
}

void init();

CSS

html,
body {
    height: 100%;
    margin: 0;
    padding: 0;
    font-family:
        'Inter',
        -apple-system,
        BlinkMacSystemFont,
        'Segoe UI',
        Roboto,
        Oxygen,
        Ubuntu,
        Cantarell,
        'Open Sans',
        'Helvetica Neue',
        sans-serif;
    background-color: #0f172a;
    color: #e2e8f0;
}

gmp-map-3d {
    height: 100%;
    width: 100%;
}

/* Glassmorphism UI Overlay */
#ui-container {
    position: absolute;
    top: 20px;
    left: 20px;
    width: 320px;
    z-index: 10;
}

.panel {
    background: rgba(15, 23, 42, 0.75);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 16px;
    padding: 24px;
    margin-top: 10px;
    box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
}

h1 {
    font-size: 1.25rem;
    font-weight: 700;
    margin: 0 0 4px 0;
    background: linear-gradient(to right, #38bdf8, #818cf8);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
}

.sub-title {
    font-size: 0.85rem;
    color: #94a3b8;
    margin: 0 0 20px 0;
}

h2 {
    font-size: 0.95rem;
    font-weight: 600;
    margin: 16px 0 8px 0;
    color: #f8fafc;
}

.control-group {
    margin-bottom: 16px;
}

label {
    display: block;
    font-size: 0.85rem;
    margin-bottom: 6px;
    color: #cbd5e1;
}

span {
    font-weight: 600;
    color: #38bdf8;
}

.row {
    display: flex;
    gap: 12px;
}

.col {
    flex: 1;
}

input[type='number'] {
    font-family: 'Fira Code', monospace;
    background: rgba(15, 23, 42, 0.5);
    border: 1px solid rgba(255, 255, 255, 0.1);
    color: #f8fafc;
    padding: 4px 8px;
    border-radius: 6px;
    outline: none;
}

input[type='range'] {
    width: 100%;
    height: 4px;
    background: #334155;
    border-radius: 2px;
    outline: none;
    -webkit-appearance: none;
}

input[type='range']::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: #38bdf8;
    cursor: pointer;
    box-shadow: 0 0 8px rgba(56, 189, 248, 0.5);
    transition: all 0.2s ease;
}

input[type='range']::-webkit-slider-thumb:hover {
    transform: scale(1.2);
    background: #60a5fa;
}

.buttons {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

button {
    background: rgba(51, 65, 85, 0.5);
    border: 1px solid rgba(255, 255, 255, 0.05);
    color: #f8fafc;
    padding: 10px 16px;
    border-radius: 8px;
    cursor: pointer;
    font-size: 0.85rem;
    font-weight: 500;
    transition: all 0.2s ease;
    text-align: left;
}

button:hover {
    background: rgba(56, 189, 248, 0.2);
    border-color: rgba(56, 189, 248, 0.4);
    transform: translateY(-1px);
}

button:active {
    transform: translateY(0);
}

.status-group p {
    font-size: 0.8rem;
    color: #94a3b8;
    margin: 4px 0;
    background: rgba(30, 41, 59, 0.5);
    padding: 6px 10px;
    border-radius: 6px;
    font-family: monospace;
}

.code-box {
    position: relative;
    background: rgba(15, 23, 42, 0.9);
    border-radius: 8px;
    border: 1px solid rgba(255, 255, 255, 0.05);
    margin-top: 8px;
}

pre {
    margin: 0;
    padding: 12px;
    overflow-x: auto;
}

code {
    font-family: 'Fira Code', monospace;
    font-size: 0.75rem;
    color: #38bdf8;
}

#copy-btn {
    display: block;
    width: 100%;
    margin-top: 8px;
    padding: 8px;
    font-size: 0.85rem;
    font-weight: 600;
    background: #334155;
    color: #f8fafc;
    border: none;
    border-radius: 6px;
    text-align: center;
    cursor: pointer;
    transition: all 0.2s ease;
}

#copy-btn:hover {
    background: #38bdf8;
    color: #0f172a;
}

HTML

<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>
        <script>
            // prettier-ignore
            (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: "GOOGLE_MAPS_API_KEY"
            });
        </script>
    </head>

    <body>
        <gmp-map-3d
            center="40.7811,-73.9599,0"
            mode="HYBRID"
            tilt="76"
            range="3270"
            heading="-154"></gmp-map-3d>
        <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"
                        name="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"
                        name="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"
                        name="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"
                            name="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"
                            name="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"
                        name="altitude"
                        min="0"
                        max="5000"
                        value="30"
                        step="10" />
                </div>

                <div class="control-group">
                    <label for="fov">
                        FOV: <span id="fov-val">35</span>&deg;
                    </label>
                    <input
                        type="range"
                        id="fov"
                        name="fov"
                        min="5"
                        max="80"
                        value="35"
                        step="1" />
                </div>

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

                <div class="status-group">
                    <div class="code-box">
                        <pre><code id="generated-code"></code></pre>
                    </div>
                    <button id="copy-btn">Copy HTML</button>
                </div>
            </div>
        </div>
    </body>
</html>