ArcGIS JS 基础教程(10):Camera 相机控制

简介: ArcGIS JS 基础教程(10):Camera 相机控制

零、写在前面

📌 本系列教程完整目录:ArcGIS JS 系列基础教程(100个项目常用热门功能)

💡 在线示例:完整可运行的 HTML 示例,无需任何环境配置,可直接在浏览器中打开体验
🗂️ 专栏导航:收藏 + 关注,专栏文章第一时间送达
❤️ 一键三连:点赞(给教程充电)+ 评论(提问必回)+ 收藏(下次再看)


一、功能介绍

在三维场景中,Camera(相机) 相当于用户的「眼睛」,决定了你从哪个位置、以什么角度观察三维世界。Camera 类用四个核心属性描述观察视角:

  • position(位置):经纬度 + 海拔高度(z 值,单位米),你在哪里看
  • heading(朝向):水平旋转角度,0° 为正北,顺时针 0~360°,你朝哪个方向看
  • tilt(俯仰角):垂直倾斜角度,0° 为垂直俯视地面,90° 为水平平视,你以什么仰角看
  • fov(视场角):对角线视野角度,范围 1~170°,默认 55°,决定视野多「广」

掌握 Camera 是三维开发的基础:设置初始视角、建筑漫游、环绕观察地标、保存恢复书签视角,都离不开它。


二、功能实现

核心 API: Camera 类(@arcgis/core/Camera.js),通过 view.camera 读取或设置。

2.1 Camera 对象基础

Camera 通过四个属性完整定义一个三维观察视角:

const Camera = await $arcgis.import("@arcgis/core/Camera.js");
const Point = await $arcgis.import("@arcgis/core/geometry/Point.js");

// 创建相机实例
const camera = new Camera({
   
    position: new Point({
   
        longitude: 116.39,   // 经度
        latitude: 39.9,      // 纬度
        z: 500               // 海拔高度(米)
    }),
    heading: 0,    // 朝向:0=正北, 90=正东, 180=正南, 270=正西
    tilt: 45,      // 俯仰:0=垂直俯视, 45=倾斜观察, 90=水平平视
    fov: 55        // 视场角:越小越长焦,越大越广角
});

heading 方向对照:

heading 值 朝向 说明
0° 正北 ↑ 默认朝向
90° 正东 → 顺时针旋转
180° 正南 ↓ —
270° 正西 ← —

tilt 角度对照:

tilt 值 视觉效果 典型场景
0° 完全垂直俯视 二维地图视角、卫星图
30°~45° 倾斜鸟瞰 城市规划、园区总览
60°~75° 近地面视角 建筑立面展示、街景
90° 水平平视 第一人称漫游

2.2 设置相机位置

有两种方式设置相机:

// 方式一:通过 SceneView 构造函数
const view = new SceneView({
   
    container: "mapContainer",
    map: map,
    camera: {
                             // 直接传 camera 配置对象
        position: {
    longitude: 116.39, latitude: 39.9, z: 2000 },
        heading: 0,
        tilt: 45
    }
});

// 方式二:直接赋值 camera 对象(运行时切换视角)
view.camera = new Camera({
   
    position: {
    longitude: 116.39, latitude: 39.9, z: 500 },
    heading: 90,
    tilt: 65
});

💡 注意: 直接赋值 view.camera 是瞬间切换(无动画),后文(第11课)将介绍使用 goTo() 实现平滑过渡。

2.3 相机旋转(heading)

heading 控制相机的水平朝向,范围 0~360°,顺时针旋转:

// 注意:view.camera 返回对象的属性不能直接改,必须整体赋值。
// 以「看向正东」为例:
const cam = view.camera.clone();
cam.heading = 90;
view.camera = cam;

// 每帧旋转 0.5°,实现环绕效果
setInterval(() => {
   
    const cam = view.camera.clone();
    cam.heading = (cam.heading + 0.5) % 360;
    view.camera = cam;
}, 16);

应用场景: 环绕观察地标建筑、旋转查看全景、无人机环绕航拍模拟。

2.4 相机俯仰(tilt)

tilt 控制相机向下倾斜的角度:

// 以「45° 鸟瞰」(最常用)为例,其余角度同理
const cam = view.camera.clone();
cam.tilt = 45;
view.camera = cam;

⚠️ 注意: tilt 的有效范围是 0~90°。0° 是垂直向下看;90° 是水平看。不存在大于 90° 的值(无法"仰头向上看")。

2.5 视野角度(fov)

fov 控制视场角,默认 55°,范围 1~170°:

// 以「长焦 20°」为例,其余角度同理
const cam = view.camera.clone();
cam.fov = 20;
view.camera = cam;
fov 值 效果 类比
20°~30° 长焦,放大远处 望远镜
55° 默认标准视角 人眼自然视野
80°~100° 广角,视野更大 超广角镜头
120°+ 鱼眼,显著畸变 鱼眼镜头

2.6 获取当前相机状态

随时可以读取 view.camera 的当前属性:

// 读取相机状态
const cam = view.camera;

console.log("位置:", cam.position.longitude, cam.position.latitude, cam.position.z);
console.log("朝向:", cam.heading, "°");
console.log("俯仰:", cam.tilt, "°");
console.log("视场角:", cam.fov, "°");

⚠️ 重要: 直接修改 view.camera 返回对象的属性(如 cam.heading = 90)不会更新视图,必须把整个 Camera 重新赋给 view.camera。保存快照时用 clone() 创建副本。

2.7 相机克隆与保存

// 保存当前视角(深拷贝)
const savedCamera = view.camera.clone();

// ... 用户操作,视角改变 ...

// 恢复之前保存的视角
view.camera = savedCamera;

三、功能应用

应用场景 position.z (高度) heading tilt fov 说明
城市规划总览 2000~5000m 180°(南向) 30°~45° 55° 鸟瞰全城,俯瞰建筑肌理
建筑立面展示 200~500m 正对建筑 60°~75° 55° 近地观察建筑外观
环绕地标观察 500~1000m 动态旋转 45°~60° 55° heading 循环旋转
第一人称漫游 10~50m 行走方向 80°~90° 55°~70° 低空贴近地面
卫星图视角 10000m+ 任意 0° 55° tilt=0 垂直俯视
远眺山脉 2000~5000m 朝向山脉 15°~30° 30°~40° 长焦拉近远景
视角书签保存 任意 任意 任意 任意 clone() 保存,随时恢复

四、核心代码

📦 完整代码 已保存至 sample/lesson10_camera_control.html,可直接在浏览器打开。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>第10课:Camera 相机控制</title>
    <link rel="stylesheet" href="https://js.arcgis.com/5.0/esri/themes/light/main.css">
    <script type="module" src="https://js.arcgis.com/5.0/"></script>
    <style>
        * {
     margin: 0; padding: 0; box-sizing: border-box; }
        body {
     font-family: "Microsoft YaHei", sans-serif; }
        #mapContainer {
     width: 100vw; height: 100vh; }
        .page-title {
    
            position: absolute;
            top: 20px; left: 50%;
            transform: translateX(-50%);
            background: rgba(255,255,255,0.95);
            padding: 10px 24px; border-radius: 6px;
            font-size: 18px; font-weight: bold;
            z-index: 100;
            box-shadow: 0 2px 8px rgba(0,0,0,0.15);
        }
        .control-panel {
    
            position: absolute;
            top: 80px; right: 20px;
            background: rgba(255,255,255,0.95);
            padding: 16px; border-radius: 8px;
            box-shadow: 0 2px 12px rgba(0,0,0,0.15);
            z-index: 100; min-width: 320px; max-height: 85vh; overflow-y: auto;
        }
        .control-panel h3 {
     margin: 0 0 8px 0; font-size: 14px; color: #333; }
        .section {
     margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #eee; }
        .section:last-child {
     border-bottom: none; margin-bottom: 0; }
        .form-group {
     margin-bottom: 10px; }
        .form-group label {
     display: block; font-size: 12px; color: #666; margin-bottom: 4px; }
        .form-group label span {
     float: right; font-weight: bold; color: #1890ff; }
        .form-group input[type="range"] {
     width: 100%; }
        .preset-row {
     display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
        .preset-row button {
    
            padding: 5px 10px; font-size: 12px;
            border: 1px solid #d9d9d9; border-radius: 4px;
            background: white; cursor: pointer;
            transition: all 0.2s;
        }
        .preset-row button:hover {
     border-color: #1890ff; color: #1890ff; }
        .preset-row button.active {
     background: #1890ff; color: white; border-color: #1890ff; }
        .btn-row {
     display: flex; gap: 8px; margin-top: 8px; }
        .btn-row button {
    
            flex: 1; padding: 7px 0;
            border: 1px solid #d9d9d9; border-radius: 4px;
            background: white; cursor: pointer; font-size: 12px;
        }
        .btn-row button:hover {
     border-color: #1890ff; color: #1890ff; }
        .btn-row button.primary {
     background: #1890ff; color: white; border-color: #1890ff; }
        .info-card {
    
            margin-top: 10px; padding: 10px 12px;
            background: #f0f5ff; border-radius: 6px;
            border-left: 3px solid #1890ff;
        }
        .info-card .row {
     font-size: 12px; color: #333; margin-bottom: 2px; line-height: 1.6; }
        .info-card .row:last-child {
     margin-bottom: 0; }
        .info-card .val {
     font-weight: bold; color: #1890ff; }
        .status-text {
    
            position: absolute; bottom: 20px; left: 50%;
            transform: translateX(-50%);
            background: rgba(0,0,0,0.7); color: white;
            padding: 8px 20px; border-radius: 20px;
            font-size: 13px; z-index: 100; pointer-events: none;
            white-space: nowrap;
        }
    </style>
</head>
<body>
<h1 class="page-title">第10课:Camera 相机控制</h1>

<div class="control-panel">
    <div class="section">
        <h3>📍 位置预设</h3>
        <div class="preset-row">
            <button data-preset="beijing" class="active">🏯 北京故宫</button>
            <button data-preset="shanghai">🌆 上海外滩</button>
            <button data-preset="guangzhou">🗼 广州塔</button>
            <button data-preset="huangshan">⛰️ 黄山</button>
        </div>
    </div>

    <div class="section">
        <h3>🧭 朝向 heading(0~360°)</h3>
        <div class="form-group">
            <label>朝向:<span id="valHeading">0°</span></label>
            <input type="range" id="sliderHeading" min="0" max="360" value="0" step="1">
        </div>
        <div class="preset-row">
            <button data-heading="0">N 北</button>
            <button data-heading="90">E 东</button>
            <button data-heading="180">S 南</button>
            <button data-heading="270">W 西</button>
        </div>
    </div>

    <div class="section">
        <h3>📐 俯仰 tilt(0~90°)</h3>
        <div class="form-group">
            <label>俯仰:<span id="valTilt">45°</span></label>
            <input type="range" id="sliderTilt" min="0" max="90" value="45" step="1">
        </div>
        <div class="preset-row">
            <button data-tilt="0">0° 俯视</button>
            <button data-tilt="45">45° 倾斜</button>
            <button data-tilt="70">70° 近地</button>
            <button data-tilt="90">90° 平视</button>
        </div>
    </div>

    <div class="section">
        <h3>🔍 视场角 fov(1~170°)</h3>
        <div class="form-group">
            <label>视场角:<span id="valFov">55°</span></label>
            <input type="range" id="sliderFov" min="1" max="170" value="55" step="1">
        </div>
        <div class="preset-row">
            <button data-fov="20">20° 长焦</button>
            <button data-fov="55">55° 默认</button>
            <button data-fov="100">100° 广角</button>
            <button data-fov="150">150° 鱼眼</button>
        </div>
    </div>

    <div class="section">
        <h3>📏 高度 altitude(m)</h3>
        <div class="form-group">
            <label>海拔高度:<span id="valAltitude">2000m</span></label>
            <input type="range" id="sliderAltitude" min="50" max="10000" value="2000" step="10">
        </div>
    </div>

    <div class="btn-row">
        <button id="btnSave" class="primary">💾 保存视角</button>
        <button id="btnRestore">📂 恢复视角</button>
    </div>
    <div class="btn-row">
        <button id="btnReset">🔄 重置视角</button>
        <button id="btnAutoRotate">🔄 自动旋转</button>
    </div>

    <div class="info-card" id="infoCard">
        <div class="row">📍 位置:<span class="val" id="infoPos">经度 116.39, 纬度 39.91</span></div>
        <div class="row">📏 高度:<span class="val" id="infoAlt">2000 m</span></div>
        <div class="row">🧭 heading:<span class="val" id="infoHeading">0°</span></div>
        <div class="row">📐 tilt:<span class="val" id="infoTilt">45°</span></div>
        <div class="row">🔍 fov:<span class="val" id="infoFov">55°</span></div>
    </div>
</div>

<div class="status-text" id="statusText">Camera | 朝向:0° | 俯仰:45° | 高度:2000m | FOV:55°</div>

<div id="mapContainer"></div>

<script type="module">
    const Map = await $arcgis.import("@arcgis/core/Map.js");
    const SceneView = await $arcgis.import("@arcgis/core/views/SceneView.js");
    const Camera = await $arcgis.import("@arcgis/core/Camera.js");
    const Point = await $arcgis.import("@arcgis/core/geometry/Point.js");
    const GraphicsLayer = await $arcgis.import("@arcgis/core/layers/GraphicsLayer.js");
    const Graphic = await $arcgis.import("@arcgis/core/Graphic.js");
    const Mesh = await $arcgis.import("@arcgis/core/geometry/Mesh.js");
    const getTianditu = await $arcgis.import("https://openlayers.vip/examples/resources/tianditu.js");

    // 天地图底图 + 高程
    const vecLayers = getTianditu.default({
     type: "vec_w" });
    const map = new Map({
    
        basemap: {
     baseLayers: [vecLayers.base, vecLayers.anno] },
        ground: {
    
            surface: {
    
                elevationLayers: [{
    
                    url: "https://www.geosceneonline.cn/image/rest/services/OpenData/ChinaTerrain3D/ImageServer/"
                }]
            }
        }
    });

    // ===== 位置预设 =====
    const presets = {
    
        beijing:   {
     lng: 116.397, lat: 39.917, zoom: 17, alt: 2000 },
        shanghai:  {
     lng: 121.491, lat: 31.240, zoom: 17, alt: 2000 },
        guangzhou: {
     lng: 113.324, lat: 23.109, zoom: 17, alt: 2000 },
        huangshan: {
     lng: 118.168, lat: 30.133, zoom: 16, alt: 3000 }
    };

    let currentPreset = "beijing";
    const p = presets[currentPreset];

    const view = new SceneView({
    
        container: "mapContainer",
        map: map,
        camera: {
    
            position: {
     longitude: p.lng, latitude: p.lat, z: p.alt },
            heading: 0,
            tilt: 45
        },
        zoom: p.zoom
    });

    let savedCamera = null;
    let autoRotateInterval = null;

    // ===== 添加 3D 参照物 =====
    function addReferenceObjects() {
    
        const layer = new GraphicsLayer({
    
            title: "参照物体"
        });

        const p = presets[currentPreset];

        // 中心参照物(金色)
        const centerBox = new Graphic({
    
            geometry: Mesh.createBox(
                new Point({
     longitude: p.lng, latitude: p.lat, z: 0 }),
                {
     size: {
     width: 200, height: 300, depth: 200 } }
            ),
            symbol: {
    
                type: "mesh-3d",
                symbolLayers: [{
     type: "fill", material: {
     color: [220, 180, 100, 0.9] } }]
            }
        });

        // 东侧方向指示物(红色)
        const eastBox = new Graphic({
    
            geometry: Mesh.createBox(
                new Point({
     longitude: p.lng + 0.003, latitude: p.lat, z: 0 }),
                {
     size: {
     width: 80, height: 100, depth: 80 } }
            ),
            symbol: {
    
                type: "mesh-3d",
                symbolLayers: [{
     type: "fill", material: {
     color: [255, 80, 80, 0.9] } }]
            }
        });

        // 南侧方向指示物(绿色)
        const southBox = new Graphic({
    
            geometry: Mesh.createBox(
                new Point({
     longitude: p.lng, latitude: p.lat - 0.003, z: 0 }),
                {
     size: {
     width: 80, height: 100, depth: 80 } }
            ),
            symbol: {
    
                type: "mesh-3d",
                symbolLayers: [{
     type: "fill", material: {
     color: [80, 200, 80, 0.9] } }]
            }
        });

        layer.addMany([centerBox, eastBox, southBox]);
        map.add(layer);
    }

    // ===== 刷新参照物位置 =====
    function refreshReferences() {
    
        map.layers.filter(l => l.title === "参照物体").forEach(l => map.remove(l));
        addReferenceObjects();
    }

    // ===== 应用相机参数 =====
    function applyCamera(heading, tilt, fov, altitude) {
    
        const cam = view.camera.clone();
        cam.heading = heading;
        cam.tilt = tilt;
        cam.fov = fov;
        cam.position.z = altitude;
        view.camera = cam;
    }

    // ===== 初始化 UI =====
    view.when(() => {
    
        addReferenceObjects();
        initUI();
        updateInfo();
    });

    function initUI() {
    
        // 位置预设按钮
        document.querySelectorAll("[data-preset]").forEach(btn => {
    
            btn.addEventListener("click", () => {
    
                stopAutoRotate();
                currentPreset = btn.dataset.preset;
                const p = presets[currentPreset];

                view.camera = new Camera({
    
                    position: new Point({
     longitude: p.lng, latitude: p.lat, z: p.alt }),
                    heading: parseInt(document.getElementById("sliderHeading").value),
                    tilt: parseInt(document.getElementById("sliderTilt").value)
                });

                document.querySelectorAll("[data-preset]").forEach(b => b.classList.remove("active"));
                btn.classList.add("active");
                refreshReferences();
                updateAll();
            });
        });

        // Heading 滑块
        initSlider("sliderHeading", "valHeading", (v) => {
    
            const cam = view.camera.clone();
            cam.heading = v;
            view.camera = cam;
            updateAll();
        });

        // Heading 方向预设
        document.querySelectorAll("[data-heading]").forEach(btn => {
    
            btn.addEventListener("click", () => {
    
                const v = parseInt(btn.dataset.heading);
                document.getElementById("sliderHeading").value = v;
                document.getElementById("valHeading").textContent = v + "°";
                const cam = view.camera.clone();
                cam.heading = v;
                view.camera = cam;
                updateAll();
            });
        });

        // Tilt 滑块
        initSlider("sliderTilt", "valTilt", (v) => {
    
            const cam = view.camera.clone();
            cam.tilt = v;
            view.camera = cam;
            updateAll();
        });

        document.querySelectorAll("[data-tilt]").forEach(btn => {
    
            btn.addEventListener("click", () => {
    
                const v = parseInt(btn.dataset.tilt);
                document.getElementById("sliderTilt").value = v;
                document.getElementById("valTilt").textContent = v + "°";
                const cam = view.camera.clone();
                cam.tilt = v;
                view.camera = cam;
                updateAll();
            });
        });

        // FOV 滑块
        initSlider("sliderFov", "valFov", (v) => {
    
            const cam = view.camera.clone();
            cam.fov = v;
            view.camera = cam;
            updateAll();
        });

        document.querySelectorAll("[data-fov]").forEach(btn => {
    
            btn.addEventListener("click", () => {
    
                const v = parseInt(btn.dataset.fov);
                document.getElementById("sliderFov").value = v;
                document.getElementById("valFov").textContent = v + "°";
                const cam = view.camera.clone();
                cam.fov = v;
                view.camera = cam;
                updateAll();
            });
        });

        // Altitude 滑块
        initSlider("sliderAltitude", "valAltitude", (v) => {
    
            const cam = view.camera.clone();
            cam.position.z = v;
            view.camera = cam;
            updateAll();
        });

        // 保存视角
        document.getElementById("btnSave").addEventListener("click", () => {
    
            savedCamera = view.camera.clone();
            document.getElementById("statusText").textContent = "💾 视角已保存!";
            setTimeout(updateStatusText, 1500);
        });

        // 恢复视角
        document.getElementById("btnRestore").addEventListener("click", () => {
    
            stopAutoRotate();
            if (savedCamera) {
    
                view.camera = savedCamera;
                syncSlidersFromCamera();
                updateAll();
                document.getElementById("statusText").textContent = "📂 视角已恢复!";
                setTimeout(updateStatusText, 1500);
            } else {
    
                document.getElementById("statusText").textContent = "⚠️ 尚未保存视角,请先点击「保存视角」";
                setTimeout(updateStatusText, 2000);
            }
        });

        // 重置视角
        document.getElementById("btnReset").addEventListener("click", () => {
    
            stopAutoRotate();
            const p = presets[currentPreset];
            view.camera = new Camera({
    
                position: new Point({
     longitude: p.lng, latitude: p.lat, z: 2000 }),
                heading: 0,
                tilt: 45,
                fov: 55
            });
            syncSlidersFromCamera();
            updateAll();
        });

        // 自动旋转
        document.getElementById("btnAutoRotate").addEventListener("click", () => {
    
            if (autoRotateInterval) {
    
                stopAutoRotate();
                document.getElementById("btnAutoRotate").textContent = "🔄 自动旋转";
                document.getElementById("btnAutoRotate").classList.remove("primary");
            } else {
    
                autoRotateInterval = setInterval(() => {
    
                    const cam = view.camera.clone();
                    cam.heading = (cam.heading + 0.5) % 360;
                    view.camera = cam;
                    document.getElementById("sliderHeading").value = Math.round(cam.heading);
                    document.getElementById("valHeading").textContent = Math.round(cam.heading) + "°";
                    updateInfo();
                    updateStatusText();
                }, 50);
                document.getElementById("btnAutoRotate").textContent = "⏸ 停止旋转";
                document.getElementById("btnAutoRotate").classList.add("primary");
            }
        });
    }

    function initSlider(sliderId, valId, callback) {
    
        const slider = document.getElementById(sliderId);
        const valSpan = document.getElementById(valId);
        slider.addEventListener("input", () => {
    
            const v = parseInt(slider.value);
            valSpan.textContent = v + (sliderId === "sliderAltitude" ? "m" : "°");
            callback(v);
        });
    }

    function syncSlidersFromCamera() {
    
        const cam = view.camera;
        document.getElementById("sliderHeading").value = Math.round(cam.heading);
        document.getElementById("valHeading").textContent = Math.round(cam.heading) + "°";
        document.getElementById("sliderTilt").value = Math.round(cam.tilt);
        document.getElementById("valTilt").textContent = Math.round(cam.tilt) + "°";
        document.getElementById("sliderFov").value = Math.round(cam.fov);
        document.getElementById("valFov").textContent = Math.round(cam.fov) + "°";
        document.getElementById("sliderAltitude").value = Math.round(cam.position.z);
        document.getElementById("valAltitude").textContent = Math.round(cam.position.z) + "m";
    }

    function stopAutoRotate() {
    
        if (autoRotateInterval) {
    
            clearInterval(autoRotateInterval);
            autoRotateInterval = null;
            document.getElementById("btnAutoRotate").textContent = "🔄 自动旋转";
            document.getElementById("btnAutoRotate").classList.remove("primary");
        }
    }

    function updateAll() {
    
        updateInfo();
        updateStatusText();
    }

    function updateInfo() {
    
        const cam = view.camera;
        document.getElementById("infoPos").textContent =
            `经度 ${
      cam.position.longitude.toFixed(4)}, 纬度 ${
      cam.position.latitude.toFixed(4)}`;
        document.getElementById("infoAlt").textContent = Math.round(cam.position.z) + " m";
        document.getElementById("infoHeading").textContent = Math.round(cam.heading) + "°";
        document.getElementById("infoTilt").textContent = Math.round(cam.tilt) + "°";
        document.getElementById("infoFov").textContent = Math.round(cam.fov) + "°";
    }

    function updateStatusText() {
    
        const cam = view.camera;
        document.getElementById("statusText").textContent =
            `Camera | 朝向:${
      Math.round(cam.heading)}° | 俯仰:${
      Math.round(cam.tilt)}° | 高度:${
      Math.round(cam.position.z)}m | FOV:${
      Math.round(cam.fov)}°`;
    }
</script>
</body>
</html>

五、在线示例

🔗 在线体验地址(GitHub资源,等待时间较长,或者架梯子):https://southjor.github.io/arcgis-examples/lessons/lesson10.html

在这里插入图片描述

操作说明:

  1. 点击位置预设按钮(北京故宫/上海外滩/广州塔/黄山),切换观察目标地点
  2. 拖动「朝向 heading」滑块 0~360°,或点击 N/E/S/W 快速设置方向(🔴红块在东、🟢绿块在南,帮助判断朝向)
  3. 拖动「俯仰 tilt」滑块 0~90°,对比俯视(0°) / 倾斜(45°) / 近地(70°) / 平视(90°) 的效果
  4. 拖动「视场角 fov」滑块 1~170°,体验长焦放大(20°) / 默认(55°) / 广角(100°) / 鱼眼(150°)
  5. 拖动「海拔高度」滑块 50~10000m,升空俯瞰或低空贴近
  6. 点击「💾 保存视角」保存当前相机,浏览后点击「📂 恢复视角」回到保存点
  7. 点击「🔄 自动旋转」让 heading 持续旋转 0.5°/帧,环绕观察地标

六、关键API说明

API 类型 默认值 说明
Camera.position Point — 相机位置:经度 + 纬度 + z(海拔高度,米)
Camera.heading number 0 水平朝向角,0°=正北,顺时针 0~360°
Camera.tilt number 0 俯仰角,0°=垂直俯视,90°=水平平视
Camera.fov number 55 对角线视场角,1~170°,越小越长焦
view.camera Camera — 获取/设置当前相机(直接赋值无动画)
camera.clone() Camera — 深拷贝相机,用于保存/恢复视角

Camera vs goTo 对比

方式 动画 适用场景
view.camera = new Camera(...) ❌ 无动画,瞬间切换 初始设置、书签跳转、程序控制
view.goTo(...) ✅ 平滑过渡动画 用户交互、POI 缩放(见第11课)

常见坑点

问题 原因 解决方案
tilt 设为 120° 无效 tilt 有效范围是 0~90° 使用 0~90 之间的值
修改 cam.heading 没反应 直接修改返回对象的属性不会更新视图 使用 cam = view.camera.clone(); cam.heading=90; view.camera = cam; 模式
旋转 heading 后场景跳变 heading 从 360 回到 0 产生突变 用取模运算 heading % 360
保存的视角恢复后不一样 直接赋值引用而非 clone 使用 saved = view.camera.clone()

七、系列导航

⬅️ 上一篇:ArcGIS JS 基础教程(9):天空盒与大气效果

➡️ 下一篇:ArcGIS JS 基础教程(11):飞行定位 goTo


💡 小贴士:Camera 的四个属性 position、heading、tilt、fov 组合在一起,几乎可以表达任何三维观察视角。掌握它们就掌握了三维场景的"镜头语言":想看宏观总览就用高 altitude + 大 tilt;想看建筑细节就用低 altitude + 小 fov;想环绕观察就用动态 heading。下一课我们将学习 goTo() 方法,为 Camera 加上平滑的飞行动画。

相关文章
|
10天前
|
Linux 开发工具 iOS开发
Codex保姆级教程(下载+安装+登录+使用,含所有版本,2026最新)
Codex是OpenAI推出的开源AI编程智能体,本地运行,可理解整个代码库,支持自然语言指令改代码、执行命令、跑测试、修Bug,兼容Win/macOS/Linux。(239字)
3920 18
|
11天前
|
JavaScript API iOS开发
DeepSeek Harness下载、安装、配置一篇搞定(2026整理,送常用插件)
DeepSeek Harness(DSH)是DeepSeek于2026年开源的MIT协议AI Agent框架,支持插件化组装模型、工具、沙箱与UI,兼容主流大模型。本文提供免Node.js的桌面端安装包(Win/macOS),含详细图文教程及热门插件推荐。
4103 49
|
11天前
|
iOS开发 Docker MacOS
【2026实测】Docker Desktop下载、安装、汉化、配置教程(附官网安装包)
Docker Desktop 是 Docker 官方推出的桌面容器管理工具,集成引擎、CLI、图形界面与 Kubernetes,支持 Win/macOS/Linux。2026 最新版 4.88.1 强化 ARM 原生运行、镜像加速及资源可视化,附中文汉化与国内镜像配置指南。
1934 7
|
5天前
|
云安全 人工智能 安全
|
6天前
|
存储 弹性计算 人工智能
2026 年阿里云双十一活动入口攻略,新客服务器优惠通道、三大会场入口梳理
综合来看,2026年阿里云双十一活动入口清晰: 阿里云轻量应用服务器、阿里云ECS云服务器、阿里云GPU云服务器、阿里云百炼大模型服务平台 活动会场、综合权益中心、99计划专场三大入口覆盖服务器、AI权益与续费特价;新用户通过控制台首页banner、权益中心、产品页三处路径进入专属专场,老用户通过控制台弹窗和费用中心权益中心进入老客专区,企业用户做企业实名后解锁专属满减与阶梯折扣。
817 0
|
5天前
|
Linux 开发工具 iOS开发
2026最新codex下载安装保姆级使用教程(下载+安装+登录+使用,含所有历史版本)
本文详解OpenAI Codex国内快速上手指南:提供Win/macOS/Linux全平台免安装包、npm/Homebrew等多方式安装、账号/API Key双登录、简体中文切换,覆盖下载、安装、汉化、使用及高频避坑(如微软商店失败、npm超时等),10分钟即可跑通。
1033 2
|
11天前
|
安全 算法 定位技术
《高性价比人生指南》完整原版:普通人用最少的资源,过好这一生的底层逻辑~How To Live Better
📖《高性价比人生指南》是一份基于实证、拒绝焦虑的生存手册,倡导以“人生时薪”为视角,重新计算时间、金钱与精力的成本与收益。它不承诺逆袭,只教人算清账:省下的钱若耗掉时间便亏;买体验比买物品更值;睡眠是最高杠杆的健康投资;关系贵在情绪价值而非表面热络;学习重在改变行为而非自我安慰。核心理念:用低成本、高回报的微小行动,换取长期的选择权——不是成为谁,而是掌控自己人生的主动权。
2141 6
|
云安全 安全 网络安全
|
24天前
|
人工智能 API 开发工具
DeepSeek Harness 神插件:白嫖 13 家大厂模型
`dsh-our-free-model` 是 DeepSeek Harness 神级插件:零配置、零API Key、零账号,聚合13家大厂免费模型(含DeepSeek V4.1 Flash、Kimi K3等),支持本地网关、用量看板与自动降级,真正实现“白嫖即用”。
4288 58