ArcGIS JS 基础教程(10):Camera 相机控制
-
- 零、写在前面
- 一、功能介绍
- 二、功能实现
-
- [2.1 Camera 对象基础](#2.1 Camera 对象基础)
- [2.2 设置相机位置](#2.2 设置相机位置)
- [2.3 相机旋转(heading)](#2.3 相机旋转(heading))
- [2.4 相机俯仰(tilt)](#2.4 相机俯仰(tilt))
- [2.5 视野角度(fov)](#2.5 视野角度(fov))
- [2.6 获取当前相机状态](#2.6 获取当前相机状态)
- [2.7 相机克隆与保存](#2.7 相机克隆与保存)
- 三、功能应用
- 四、核心代码
- 五、在线示例
- 六、关键API说明
-
- [Camera vs goTo 对比](#Camera vs goTo 对比)
- 常见坑点
- 七、系列导航
零、写在前面
📌 本系列教程完整目录 :ArcGIS JS 系列基础教程(100个项目常用热门功能)
💡 在线示例 :完整可运行的 HTML 示例,无需任何环境配置,可直接在浏览器中打开体验
🗂️ 专栏导航 :收藏 + 关注,专栏文章第一时间送达
❤️ 一键三连:点赞(给教程充电)+ 评论(提问必回)+ 收藏(下次再看)
一、功能介绍
在 ArcGIS Maps SDK for JavaScript 的三维场景中,Camera(相机) 相当于用户的"眼睛"------它决定了你从哪个位置、以什么角度观察三维世界。Camera 类提供了四个核心属性来控制观察视角:
position(位置):经纬度 + 海拔高度(z 值,单位米)------你在哪里看heading(朝向):水平旋转角度,0° 为正北,顺时针 0~360°------你朝哪个方向看tilt(俯仰角):垂直倾斜角度,0° 为垂直俯视地面,90° 为水平平视 ------你以什么仰角看fov(视场角):对角线视野角度,范围 1~170°,默认 55°------你看得多"广"
掌握 Camera API 是三维场景开发的基础------无论是设置初始视角、实现建筑漫游、环绕观察地标,还是保存和恢复书签视角,都离不开对相机的精准控制。
二、功能实现
核心 API: Camera 类(@arcgis/core/Camera.js),通过 view.camera 读取或设置。
2.1 Camera 对象基础
Camera 通过四个属性完整定义一个三维观察视角:
javascript
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 设置相机位置
有两种方式设置相机:
javascript
// 方式一:通过 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°,顺时针旋转:
javascript
// 看向正北
view.camera.heading = 0;
// 看向正东
view.camera.heading = 90;
// 看向正南
view.camera.heading = 180;
// 看向正西
view.camera.heading = 270;
// 每帧旋转 0.5°,实现环绕效果
setInterval(() => {
view.camera.heading = (view.camera.heading + 0.5) % 360;
}, 16);
应用场景: 环绕观察地标建筑、旋转查看全景、无人机环绕航拍模拟。
2.4 相机俯仰(tilt)
tilt 控制相机向下倾斜的角度:
javascript
// 垂直俯视(2D 地图风格)
view.camera.tilt = 0;
// 45° 鸟瞰(最常用)
view.camera.tilt = 45;
// 近地面观察建筑立面
view.camera.tilt = 75;
// 水平平视(第一人称视角)
view.camera.tilt = 90;
⚠️ 注意: tilt 的有效范围是 0~90°。0° 是垂直向下看;90° 是水平看。不存在大于 90° 的值(无法"仰头向上看")。
2.5 视野角度(fov)
fov 控制视场角,默认 55°,范围 1~170°:
javascript
// 默认视场角(标准视角)
view.camera.fov = 55;
// 长焦效果(放大远处细节)
view.camera.fov = 20;
// 广角效果(更大视野)
view.camera.fov = 90;
// 鱼眼效果(极端广角)
view.camera.fov = 120;
| fov 值 | 效果 | 类比 |
|---|---|---|
| 20°~30° | 长焦,放大远处 | 望远镜 |
| 55° | 默认标准视角 | 人眼自然视野 |
| 80°~100° | 广角,视野更大 | 超广角镜头 |
| 120°+ | 鱼眼,显著畸变 | 鱼眼镜头 |
2.6 获取当前相机状态
随时可以读取 view.camera 的当前属性:
javascript
// 读取相机状态
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 会同步影响 view.camera。如需保存快照,使用 clone() 创建副本。
2.7 相机克隆与保存
javascript
// 保存当前视角(深拷贝)
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,可直接在浏览器打开。
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
});
window.view = view;
let savedCamera = null;
let autoRotateInterval = null;
// ===== 添加 3D 参照物 =====
function addReferenceObjects() {
const layer = new GraphicsLayer({
title: "参照物体",
castShadows: true, receiveShadows: true
});
// 中心参照物
const centerBox = new Graphic({
geometry: Mesh.createBox(
new Point({ longitude: presets[currentPreset].lng, latitude: presets[currentPreset].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;
}
// ===== 初始化 =====
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", "heading", (v) => {
view.camera.heading = v; 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 + "°";
view.camera.heading = v;
updateAll();
});
});
// Tilt 滑块
initSlider("sliderTilt", "valTilt", "tilt", (v) => {
view.camera.tilt = v; 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 + "°";
view.camera.tilt = v;
updateAll();
});
});
// FOV 滑块
initSlider("sliderFov", "valFov", "fov", (v) => {
view.camera.fov = v; 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 + "°";
view.camera.fov = v;
updateAll();
});
});
// Altitude 滑块
initSlider("sliderAltitude", "valAltitude", "altitude", (v) => {
view.camera.position.z = v; 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(() => {
view.camera.heading = (view.camera.heading + 0.5) % 360;
document.getElementById("sliderHeading").value = Math.round(view.camera.heading);
document.getElementById("valHeading").textContent = Math.round(view.camera.heading) + "°";
updateInfo();
updateStatusText();
}, 50);
document.getElementById("btnAutoRotate").textContent = "⏸ 停止旋转";
document.getElementById("btnAutoRotate").classList.add("primary");
}
});
}
function initSlider(sliderId, valId, type, callback) {
const slider = document.getElementById(sliderId);
const valSpan = document.getElementById(valId);
slider.addEventListener("input", () => {
const v = type === "altitude" ? parseInt(slider.value) : parseInt(slider.value);
valSpan.textContent = v + (type === "altitude" ? "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

操作说明:
- 点击位置预设按钮(北京故宫/上海外滩/广州塔/黄山),切换观察目标地点
- 拖动「朝向 heading」滑块 0~360°,或点击 N/E/S/W 快速设置方向(🔴红块在东、🟢绿块在南,帮助判断朝向)
- 拖动「俯仰 tilt」滑块 0~90°,对比俯视(0°) / 倾斜(45°) / 近地(70°) / 平视(90°) 的效果
- 拖动「视场角 fov」滑块 1~170°,体验长焦放大(20°) / 默认(55°) / 广角(100°) / 鱼眼(150°)
- 拖动「海拔高度」滑块 50~10000m,升空俯瞰或低空贴近
- 点击「💾 保存视角」保存当前相机,浏览后点击「📂 恢复视角」回到保存点
- 点击「🔄 自动旋转」让 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 没反应 |
view.camera 返回引用,需整体赋值 |
使用 view.camera = cam 或 view.camera.heading = 90 |
| 旋转 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 加上平滑的飞行动画。