Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db8b14705d | ||
|
|
f11dcb0ea5 | ||
|
|
6d46d51685 | ||
|
|
4cfeb7cba0 | ||
|
|
929e55c32f | ||
|
|
847d361431 | ||
|
|
30e2f562bc | ||
|
|
46c6d2f34a | ||
|
|
d327ab5bc1 | ||
|
|
0215fa71d4 | ||
|
|
d0732a9ac3 | ||
|
|
52b09f5049 | ||
|
|
63777b83ce | ||
|
|
46f6681b6a | ||
|
|
3522ece7b3 | ||
|
|
1fd386b6cf | ||
|
|
10b2b77887 | ||
|
|
d4e0999506 | ||
|
|
0e150f09df | ||
|
|
5b71877ae3 | ||
|
|
89d51a7f59 | ||
|
|
9df646c91c | ||
|
|
d02e7da220 | ||
|
|
6256098a94 | ||
|
|
174f162565 |
@@ -8,7 +8,19 @@
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| **活页本** | 皮革封面 + 金属活页环 + 20 张双面活页,翻页动画、纸张弯曲、堆叠阴影 |
|
| **活页本** | 皮革封面 + 金属活页环 + 20 张双面活页,翻页动画、纸张弯曲、堆叠阴影 |
|
||||||
| **时间轴** | 20 张活页沿金色时间轴一字排开,选中卡片上浮并显示年代 |
|
| **时间轴** | 20 张活页沿金色时间轴一字排开,选中卡片上浮并显示年代 |
|
||||||
| **地理** | 点阵地球 + 经纬网 + 事件标记 + 相邻事件间的金色大圆航线,缓慢自转 |
|
| **地理** | Cesium 三维地球:卫星影像 + 真实地形 + 国界/城市标注 + 词条便签标记 |
|
||||||
|
|
||||||
|
## 地理视图(Cesium)
|
||||||
|
|
||||||
|
地理视图使用 **Cesium**(懒加载,进入该视图才加载脚本),免费、无需 Ion token:
|
||||||
|
|
||||||
|
- **影像/地形**:ESRI World Imagery 卫星影像(限制 ±85°)+ Esri World Elevation 3D 真实地形。
|
||||||
|
- **国界**:Natural Earth 50m,黑色单线。
|
||||||
|
- **国家/城市名**:Natural Earth + GeoNames 中文名,黑边黄字(`FILL_AND_OUTLINE`),按距离淡出,背面经深度测试隐藏。
|
||||||
|
- **词条便签**:DOM 覆盖层(非 billboard),贴地形高度,逆时针 45° 斜放、平滑曲线箭头;未选中金底黑字带黑边,选中黑底金字。
|
||||||
|
- **交互**:左键旋转、滚轮缩放(500 m ~ 8000 km)、右键原生 tilt(松手不回正)、中键回正(flyTo 动画)。
|
||||||
|
- **画质**:按设备像素比渲染(高分屏清晰)、FXAA + MSAA、地形 `maximumScreenSpaceError=2.0`。
|
||||||
|
- **兜底**:丢弃 ESRI 灰色 "Map data not yet available" 错误瓦片,自动回退父级放大。
|
||||||
|
|
||||||
## 技术要点
|
## 技术要点
|
||||||
|
|
||||||
@@ -18,6 +30,18 @@
|
|||||||
- **双面活页**:正面用 `FrontSide`、背面用 `BackSide`(内容预先水平镜像),配合纸张柔性弯曲,翻页时自然显露背面素描。
|
- **双面活页**:正面用 `FrontSide`、背面用 `BackSide`(内容预先水平镜像),配合纸张柔性弯曲,翻页时自然显露背面素描。
|
||||||
- 数据(`src/data/entries.js`)包含 20 条真实历史里程碑,含经纬度与分类(人 / 事 / 物)。
|
- 数据(`src/data/entries.js`)包含 20 条真实历史里程碑,含经纬度与分类(人 / 事 / 物)。
|
||||||
|
|
||||||
|
## 性能优化(已完成)
|
||||||
|
|
||||||
|
- **纹理懒加载 + LRU**:页面 / 卡片纹理按需生成,引用计数 + LRU 释放(上限 40 页),启动只为可见窗口生成;时间轴卡片用独立低分辨率纹理池(256×358)。
|
||||||
|
- **按需渲染**:静止时停止重绘,动画 / 交互 / 状态变化时才渲染(含页面 z 过渡的 `isSettling` 判定)。
|
||||||
|
- **几何降级**:栈内页用极简几何(2 三角形),顶层 / 翻页页才用高分段;封面皮革曲面、活页环、铆钉均降低分段;时间轴卡片 24×6 → 12×3。
|
||||||
|
- **双面材质合并**:页面正反面合并为单 mesh(`gl_FrontFacing` 双纹理),draw call 减半;活页孔用 alphaTest 免透明排序。
|
||||||
|
- **Cesium 懒加载 + 后台预载**:首屏不加载 6MB 脚本,首帧后后台预载,地理按钮就绪前在循环切换中自动跳过。
|
||||||
|
- **地理数据 primitive 化**:7342 城市点/标签与约 240 国家名标签由 entity 改为 `PointPrimitiveCollection` / `LabelCollection`。
|
||||||
|
- **渲染分辨率**:DPR 上限 1.5、Cesium 关闭超采样、页面纹理关闭 mipmap、anisotropy 8→4。
|
||||||
|
- **射线检测节流**:hover 合帧到渲染循环,只测可见窗口对象(binder 当前页 ±1、时间轴可视窗口)。
|
||||||
|
- **gzip**:Nginx 开启 gzip(含 `application/octet-stream`),geojson 3MB→1MB、城市 641KB→182KB。
|
||||||
|
|
||||||
## 运行
|
## 运行
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -71,6 +95,7 @@ curl -s -o /dev/null -w "%{http_code}\n" https://hist.fukun.net/
|
|||||||
## 操作
|
## 操作
|
||||||
|
|
||||||
- **拖动** 旋转视角,**滚轮** 缩放
|
- **拖动** 旋转视角,**滚轮** 缩放
|
||||||
|
- 地理视图:**左键拖拽** 旋转、**滚轮** 缩放、**右键拖拽** 倾斜环视(松手不回正)、**中键** 回正
|
||||||
- **← →** 或底部按钮 翻页(翻面查看背面素描)
|
- **← →** 或底部按钮 翻页(翻面查看背面素描)
|
||||||
- 点击 活页 / 时间轴卡片 / 地球标记 选中词条
|
- 点击 活页 / 时间轴卡片 / 地球标记 选中词条
|
||||||
- 顶部按钮或按键 **1 / 2 / 3** 切换 活页本 / 时间轴 / 地理 视图
|
- 顶部按钮或按键 **1 / 2 / 3** 切换 活页本 / 时间轴 / 地理 视图
|
||||||
@@ -79,18 +104,18 @@ curl -s -o /dev/null -w "%{http_code}\n" https://hist.fukun.net/
|
|||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
main.js 入口
|
main.js 入口
|
||||||
core/App.js 编排器:渲染、灯光、相机、视图切换、拾取
|
core/App.js 编排器:渲染、灯光、相机、视图切换、拾取
|
||||||
scene/Binder.js 活页本(封面、环、双面活页、翻页动画)
|
scene/binder.js 活页本(封面、环、双面活页、翻页动画)
|
||||||
scene/Timeline.js 时间轴视图
|
scene/timeline.js 时间轴视图
|
||||||
scene/Globe.js 地理视图(点阵地球、弧线、标记)
|
scene/CesiumGlobe.js 地理视图(Cesium 地球、国界、城市、词条便签)
|
||||||
textures/pages.js 纸张 + 登记卡/素描两面纹理
|
textures/pages.js 纸张 + 登记卡/素描两面纹理
|
||||||
textures/sketch.js 程序化素描生成器
|
textures/sketch.js 程序化素描生成器
|
||||||
data/entries.js 历史词条数据
|
data/entries.js 历史词条数据
|
||||||
data/worldMap.js 简化大陆轮廓
|
ui/overlay.js HTML 覆盖层
|
||||||
ui/overlay.js HTML 覆盖层
|
ui/loading.js 加载遮罩
|
||||||
utils/ 缓动 / 文字精灵
|
utils/ 缓动 / 文字精灵
|
||||||
scripts/ 截图与验证辅助(开发用)
|
scripts/ 截图与验证辅助(开发用)
|
||||||
```
|
```
|
||||||
|
|
||||||
## TODO / 后续规划
|
## TODO / 后续规划
|
||||||
@@ -100,3 +125,6 @@ scripts/ 截图与验证辅助(开发用)
|
|||||||
- [ ] **时间轴时期划分**:时间轴轴线上标注中西方历史时期刻度(如中国朝代:夏商周 / 秦汉 / 隋唐 / 宋元明清;西方:古典时代 / 中世纪 / 文艺复兴 / 启蒙运动 / 工业革命等),作为背景衬托。
|
- [ ] **时间轴时期划分**:时间轴轴线上标注中西方历史时期刻度(如中国朝代:夏商周 / 秦汉 / 隋唐 / 宋元明清;西方:古典时代 / 中世纪 / 文艺复兴 / 启蒙运动 / 工业革命等),作为背景衬托。
|
||||||
- [ ] **词条插入时间轴**:词条按其 `timeNumeric` 真实时间位置插入时间轴(当前为等距排列),使时期刻度与词条位置相互对应。
|
- [ ] **词条插入时间轴**:词条按其 `timeNumeric` 真实时间位置插入时间轴(当前为等距排列),使时期刻度与词条位置相互对应。
|
||||||
- [ ] **词条结构考究与排版优化**:重新审视 `entries.js` 数据模型(补充年代精度、文明圈、影响、关键词等字段),并优化登记卡正面排版(信息层级、留白、装饰细节)。
|
- [ ] **词条结构考究与排版优化**:重新审视 `entries.js` 数据模型(补充年代精度、文明圈、影响、关键词等字段),并优化登记卡正面排版(信息层级、留白、装饰细节)。
|
||||||
|
- [ ] **性能进一步优化**:继续排查渲染 / 显存 / 加载瓶颈(已做:纹理懒加载、按需渲染、几何降级、双面材质、gzip 等)。
|
||||||
|
- [ ] **边界测试修复**:尾页(第一页 / 最后一页)前后的边界情况,在地理视图与时间轴视图存在 bug,待修。
|
||||||
|
- [ ] **时间轴清晰度**:时间轴视图当前偏模糊,需排查(可能是卡片低分辨率纹理 256×358 或渲染分辨率导致)。
|
||||||
|
|||||||
+164
-44
@@ -55,7 +55,7 @@ function sphericalAreaKm2(polys) {
|
|||||||
// 生成便签 canvas:黄底圆角 + 左侧箭头 + 黑色加粗文字(整张图,箭头尖端指向坐标)
|
// 生成便签 canvas:黄底圆角 + 左侧箭头 + 黑色加粗文字(整张图,箭头尖端指向坐标)
|
||||||
// 选中态:先描粗边再填充,得到一条干净的 2px 外描边(四周预留 B 的 padding 防止裁切)
|
// 选中态:先描粗边再填充,得到一条干净的 2px 外描边(四周预留 B 的 padding 防止裁切)
|
||||||
function makeStickyNote(text, fontSize, selected = false) {
|
function makeStickyNote(text, fontSize, selected = false) {
|
||||||
const padX = 8, padY = 5, radius = 7, arrowW = 10, arrowH = 14, S = 2, B = 2; // S: 2x 超采样,B: 边框预留
|
const padX = 7, padY = 4, radius = 2, arrowW = 11, arrowH = 14, S = 2, B = 2; // S: 2x 超采样,B: 边框预留
|
||||||
const font = `bold ${fontSize}px "Noto Serif SC", "Songti SC", "STSong", "SimSun", Georgia, serif`;
|
const font = `bold ${fontSize}px "Noto Serif SC", "Songti SC", "STSong", "SimSun", Georgia, serif`;
|
||||||
const m = document.createElement('canvas');
|
const m = document.createElement('canvas');
|
||||||
const mctx = m.getContext('2d');
|
const mctx = m.getContext('2d');
|
||||||
@@ -77,16 +77,15 @@ function makeStickyNote(text, fontSize, selected = false) {
|
|||||||
// 箭头 + 圆角矩形一体化路径(箭头尖端在 (ox, oy + h/2),指向坐标)
|
// 箭头 + 圆角矩形一体化路径(箭头尖端在 (ox, oy + h/2),指向坐标)
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(ox, oy + h / 2);
|
ctx.moveTo(ox, oy + h / 2);
|
||||||
ctx.lineTo(ox + arrowW, oy + h / 2 - arrowH / 2);
|
// 上侧:一条曲线从尖端平滑过渡到上边缘(终点水平切线,无折角)
|
||||||
ctx.lineTo(ox + arrowW, oy + radius);
|
ctx.quadraticCurveTo(ox + arrowW * 0.7, oy, ox + arrowW, oy);
|
||||||
ctx.arcTo(ox + arrowW, oy, ox + arrowW + radius, oy, radius);
|
|
||||||
ctx.lineTo(ox + w - radius, oy);
|
ctx.lineTo(ox + w - radius, oy);
|
||||||
ctx.arcTo(ox + w, oy, ox + w, oy + radius, radius);
|
ctx.arcTo(ox + w, oy, ox + w, oy + radius, radius);
|
||||||
ctx.lineTo(ox + w, oy + h - radius);
|
ctx.lineTo(ox + w, oy + h - radius);
|
||||||
ctx.arcTo(ox + w, oy + h, ox + w - radius, oy + h, radius);
|
ctx.arcTo(ox + w, oy + h, ox + w - radius, oy + h, radius);
|
||||||
ctx.lineTo(ox + arrowW + radius, oy + h);
|
ctx.lineTo(ox + arrowW, oy + h);
|
||||||
ctx.arcTo(ox + arrowW, oy + h, ox + arrowW, oy + h - radius, radius);
|
// 下侧:一条曲线从下边缘平滑过渡回尖端(起点水平切线)
|
||||||
ctx.lineTo(ox + arrowW, oy + h / 2 + arrowH / 2);
|
ctx.quadraticCurveTo(ox + arrowW * 0.7, oy + h, ox, oy + h / 2);
|
||||||
ctx.closePath();
|
ctx.closePath();
|
||||||
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
@@ -117,7 +116,7 @@ function makeStickyNote(text, fontSize, selected = false) {
|
|||||||
ctx.font = font;
|
ctx.font = font;
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.fillText(text, ox + arrowW + bodyW / 2, oy + h / 2 + 1);
|
ctx.fillText(text, ox + arrowW + bodyW / 2 - 2, oy + h / 2 + 1);
|
||||||
|
|
||||||
return { canvas: c, scale: 1 / S, width: cw, height: ch, tipX: B, tipY: B + h / 2 };
|
return { canvas: c, scale: 1 / S, width: cw, height: ch, tipX: B, tipY: B + h / 2 };
|
||||||
}
|
}
|
||||||
@@ -203,8 +202,8 @@ export class CesiumGlobe {
|
|||||||
scene.skyBox.show = false;
|
scene.skyBox.show = false;
|
||||||
scene.sun.show = false;
|
scene.sun.show = false;
|
||||||
scene.moon.show = false;
|
scene.moon.show = false;
|
||||||
scene.globe.enableLighting = true; // 地形法线明暗 + 水面高光/波纹
|
scene.globe.enableLighting = false;
|
||||||
scene.globe.showGroundAtmosphere = true; // 地平线大气辉光
|
scene.globe.showGroundAtmosphere = false;
|
||||||
scene.globe.baseColor = Cesium.Color.fromCssColorString('#d9e6f0'); // 冰盖浅色:影像/地形未覆盖的南北极区域显示为冰盖,而非黑洞
|
scene.globe.baseColor = Cesium.Color.fromCssColorString('#d9e6f0'); // 冰盖浅色:影像/地形未覆盖的南北极区域显示为冰盖,而非黑洞
|
||||||
|
|
||||||
// 相机交互:限制滚轮缩放范围,避免过近/过远
|
// 相机交互:限制滚轮缩放范围,避免过近/过远
|
||||||
@@ -228,11 +227,12 @@ export class CesiumGlobe {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 阻止右键弹出浏览器菜单
|
// 阻止右键弹出浏览器菜单;阻止中键触发浏览器自动滚动
|
||||||
scene.canvas.addEventListener('contextmenu', (e) => e.preventDefault());
|
scene.canvas.addEventListener('contextmenu', (e) => e.preventDefault());
|
||||||
|
scene.canvas.addEventListener('mousedown', (e) => { if (e.button === 1) e.preventDefault(); });
|
||||||
|
|
||||||
// 精细化渲染:高清 + 精细地形
|
// 精细化渲染:高清 + 精细地形
|
||||||
scene.globe.maximumScreenSpaceError = 1.0; // 更精细的地形起伏
|
scene.globe.maximumScreenSpaceError = 2.0; // 地形细分
|
||||||
scene.globe.tileCacheSize = 600; // 更大的瓦片缓存,减少细节刷新
|
scene.globe.tileCacheSize = 600; // 更大的瓦片缓存,减少细节刷新
|
||||||
scene.fxaa = true; // 后处理抗锯齿(MSAA 默认已 4x)
|
scene.fxaa = true; // 后处理抗锯齿(MSAA 默认已 4x)
|
||||||
viewer.useBrowserRecommendedResolution = false; // 按设备像素比渲染,高分屏清晰
|
viewer.useBrowserRecommendedResolution = false; // 按设备像素比渲染,高分屏清晰
|
||||||
@@ -240,10 +240,45 @@ export class CesiumGlobe {
|
|||||||
scene.maximumRenderTimeChange = Infinity; // 配合 requestRenderMode:静止时停止重绘,大幅省电省帧
|
scene.maximumRenderTimeChange = Infinity; // 配合 requestRenderMode:静止时停止重绘,大幅省电省帧
|
||||||
|
|
||||||
// 卫星影像
|
// 卫星影像
|
||||||
|
// 检测 ESRI "Map data not yet available" 灰色错误瓦片并丢弃,自动回退父级放大
|
||||||
|
const discardNotAvailableTile = {
|
||||||
|
_canvas: document.createElement('canvas'),
|
||||||
|
_ctx: null,
|
||||||
|
isReady() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
shouldDiscardImage(image) {
|
||||||
|
try {
|
||||||
|
if (!this._ctx) {
|
||||||
|
this._ctx = this._canvas.getContext('2d', { willReadFrequently: true });
|
||||||
|
}
|
||||||
|
this._canvas.width = 8;
|
||||||
|
this._canvas.height = 8;
|
||||||
|
this._ctx.drawImage(image, 0, 0, 8, 8);
|
||||||
|
const d = this._ctx.getImageData(0, 0, 8, 8).data;
|
||||||
|
let gray = 0;
|
||||||
|
let total = 0;
|
||||||
|
for (let i = 0; i < d.length; i += 4) {
|
||||||
|
const r = d[i];
|
||||||
|
const g = d[i + 1];
|
||||||
|
const b = d[i + 2];
|
||||||
|
const mx = Math.max(r, g, b);
|
||||||
|
const mn = Math.min(r, g, b);
|
||||||
|
if (mx - mn < 15 && r > 110 && r < 240) gray++;
|
||||||
|
total++;
|
||||||
|
}
|
||||||
|
return total > 0 && gray / total > 0.92;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const imagery = new Cesium.UrlTemplateImageryProvider({
|
const imagery = new Cesium.UrlTemplateImageryProvider({
|
||||||
url: IMAGERY_URL,
|
url: IMAGERY_URL,
|
||||||
credit: 'Imagery © Esri, Maxar, Earthstar Geographics',
|
credit: 'Imagery © Esri, Maxar, Earthstar Geographics',
|
||||||
maximumLevel: 19,
|
maximumLevel: 19,
|
||||||
|
tileDiscardPolicy: discardNotAvailableTile,
|
||||||
});
|
});
|
||||||
const imageryLayer = new Cesium.ImageryLayer(imagery, {
|
const imageryLayer = new Cesium.ImageryLayer(imagery, {
|
||||||
// 卫星影像不覆盖极区(约 ±85° 外返回黑块),限制范围让极区用冰盖底色
|
// 卫星影像不覆盖极区(约 ±85° 外返回黑块),限制范围让极区用冰盖底色
|
||||||
@@ -263,6 +298,7 @@ export class CesiumGlobe {
|
|||||||
viewer.clock.shouldAnimate = false;
|
viewer.clock.shouldAnimate = false;
|
||||||
|
|
||||||
this._buildMarkers();
|
this._buildMarkers();
|
||||||
|
this._clampMarkersToTerrain();
|
||||||
this._setupPick();
|
this._setupPick();
|
||||||
this._setupRightRestore();
|
this._setupRightRestore();
|
||||||
this._loadBoundaries();
|
this._loadBoundaries();
|
||||||
@@ -288,9 +324,11 @@ export class CesiumGlobe {
|
|||||||
_buildMarkers() {
|
_buildMarkers() {
|
||||||
ENTRIES.forEach((entry, i) => {
|
ENTRIES.forEach((entry, i) => {
|
||||||
const position = Cesium.Cartesian3.fromDegrees(entry.lng, entry.lat);
|
const position = Cesium.Cartesian3.fromDegrees(entry.lng, entry.lat);
|
||||||
const noteNormal = makeStickyNote(entry.name, 16, false);
|
const noteNormal = makeStickyNote(entry.name, 14, false);
|
||||||
const noteSelected = makeStickyNote(entry.name, 16, true);
|
const noteSelected = makeStickyNote(entry.name, 14, true);
|
||||||
this.markers.push({
|
this.markers.push({
|
||||||
|
lng: entry.lng,
|
||||||
|
lat: entry.lat,
|
||||||
position,
|
position,
|
||||||
r2: Cesium.Cartesian3.magnitudeSquared(position),
|
r2: Cesium.Cartesian3.magnitudeSquared(position),
|
||||||
noteNormal,
|
noteNormal,
|
||||||
@@ -301,6 +339,64 @@ export class CesiumGlobe {
|
|||||||
this._initMarkerLayer();
|
this._initMarkerLayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 把便签锚点贴到地形表面(否则在椭球面高度 0 上,放大旋转时会飘)
|
||||||
|
async _clampMarkersToTerrain() {
|
||||||
|
const scene = this.viewer.scene;
|
||||||
|
if (!scene) return;
|
||||||
|
|
||||||
|
// 等待地形 provider 就绪(setTerrain 是异步的,调用时 provider 可能还没准备好)
|
||||||
|
let tp = scene.terrainProvider;
|
||||||
|
if (!tp) {
|
||||||
|
tp = await new Promise((resolve) => {
|
||||||
|
const started = Date.now();
|
||||||
|
const poll = () => {
|
||||||
|
const p = scene.terrainProvider;
|
||||||
|
if (p) { resolve(p); return; }
|
||||||
|
if (Date.now() - started > 30000) { resolve(undefined); return; }
|
||||||
|
setTimeout(poll, 500);
|
||||||
|
};
|
||||||
|
poll();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!tp) {
|
||||||
|
console.warn('[CesiumGlobe] 地形 provider 未就绪,便签保持椭球面高度');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cartos = this.markers.map((m) =>
|
||||||
|
Cesium.Cartographic.fromDegrees(m.lng, m.lat),
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
// 固定层级 15(约 30m):ArcGIS 高程服务无 availability,不用 sampleTerrainMostDetailed
|
||||||
|
await Cesium.sampleTerrain(tp, 15, cartos);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[CesiumGlobe] 地形高度采样失败:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 失败的点逐级降级重试(更粗的层级更容易成功)
|
||||||
|
for (const level of [12, 8, 4]) {
|
||||||
|
const retry = cartos.filter((c) => c.height == null || !isFinite(c.height));
|
||||||
|
if (retry.length === 0) break;
|
||||||
|
try {
|
||||||
|
await Cesium.sampleTerrain(tp, level, retry);
|
||||||
|
} catch (err) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let clamped = 0;
|
||||||
|
for (let i = 0; i < this.markers.length; i++) {
|
||||||
|
const h = cartos[i] && cartos[i].height;
|
||||||
|
if (h != null && isFinite(h)) {
|
||||||
|
const m = this.markers[i];
|
||||||
|
m.position = Cesium.Cartesian3.fromDegrees(m.lng, m.lat, h);
|
||||||
|
m.r2 = Cesium.Cartesian3.magnitudeSquared(m.position);
|
||||||
|
clamped++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`[CesiumGlobe] 便签已贴附地形:${clamped}/${this.markers.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
// 便签用 DOM 覆盖层渲染:始终在城市/国家名上层,且缩放到最近也不会漂移
|
// 便签用 DOM 覆盖层渲染:始终在城市/国家名上层,且缩放到最近也不会漂移
|
||||||
_initMarkerLayer() {
|
_initMarkerLayer() {
|
||||||
const container = document.getElementById('cesium-container');
|
const container = document.getElementById('cesium-container');
|
||||||
@@ -319,11 +415,11 @@ export class CesiumGlobe {
|
|||||||
m.tipY = m.noteNormal.tipY;
|
m.tipY = m.noteNormal.tipY;
|
||||||
m.normalUrl = m.noteNormal.canvas.toDataURL();
|
m.normalUrl = m.noteNormal.canvas.toDataURL();
|
||||||
m.selectedUrl = m.noteSelected.canvas.toDataURL();
|
m.selectedUrl = m.noteSelected.canvas.toDataURL();
|
||||||
|
m.screenX = -1e9;
|
||||||
|
m.screenY = -1e9;
|
||||||
el.style.backgroundImage = `url("${m.normalUrl}")`;
|
el.style.backgroundImage = `url("${m.normalUrl}")`;
|
||||||
el.addEventListener('click', (e) => {
|
el.style.transform = 'rotate(-45deg)';
|
||||||
e.stopPropagation();
|
el.style.transformOrigin = `${m.tipX}px ${m.tipY}px`;
|
||||||
this.onPick?.(m.index);
|
|
||||||
});
|
|
||||||
this._markerLayer.appendChild(el);
|
this._markerLayer.appendChild(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,48 +447,68 @@ export class CesiumGlobe {
|
|||||||
|
|
||||||
el.style.display = 'block';
|
el.style.display = 'block';
|
||||||
el.style.opacity = '1';
|
el.style.opacity = '1';
|
||||||
el.style.left = `${win.x - m.tipX}px`;
|
m.screenX = win.x - m.tipX;
|
||||||
el.style.top = `${win.y - m.tipY}px`;
|
m.screenY = win.y - m.tipY;
|
||||||
|
el.style.left = `${m.screenX}px`;
|
||||||
|
el.style.top = `${m.screenY}px`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_setupPick() {
|
_setupPick() {
|
||||||
const viewer = this.viewer;
|
const viewer = this.viewer;
|
||||||
// 移除 Viewer 默认的单击选中与双击“追踪实体”:否则双击后会锁定相机(定住)
|
// 移除 Viewer 默认的单击选中与双击“追踪实体”
|
||||||
// 标记便签已改为 DOM 覆盖层,点击由 DOM 元素自行处理
|
|
||||||
viewer.screenSpaceEventHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
viewer.screenSpaceEventHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
||||||
viewer.screenSpaceEventHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK);
|
viewer.screenSpaceEventHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK);
|
||||||
|
|
||||||
|
// 便签层 pointer-events:none 后,左键通过 canvas 命中测试选中便签
|
||||||
|
viewer.screenSpaceEventHandler.setInputAction((movement) => {
|
||||||
|
const x = movement.position.x;
|
||||||
|
const y = movement.position.y;
|
||||||
|
const canvasRect = viewer.canvas.getBoundingClientRect();
|
||||||
|
for (const m of this.markers) {
|
||||||
|
const el = m.el;
|
||||||
|
if (!el || el.style.display === 'none') continue;
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
if (
|
||||||
|
x >= r.left - canvasRect.left && x <= r.right - canvasRect.left &&
|
||||||
|
y >= r.top - canvasRect.top && y <= r.bottom - canvasRect.top
|
||||||
|
) {
|
||||||
|
this.onPick?.(m.index);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 右键原生 tilt 松开后自动回位:按下记录相机状态,松开恢复(并清掉 tilt 惯性余力)
|
// 右键原生 tilt:按下记录位置;松开不回正。中键:回正到“不倾斜不旋转”(天底 + 北朝上)
|
||||||
_setupRightRestore() {
|
_setupRightRestore() {
|
||||||
const scene = this.viewer.scene;
|
const scene = this.viewer.scene;
|
||||||
const camera = this.viewer.camera;
|
const camera = this.viewer.camera;
|
||||||
const handler = new Cesium.ScreenSpaceEventHandler(scene.canvas);
|
const handler = new Cesium.ScreenSpaceEventHandler(scene.canvas);
|
||||||
this._rightRestore = null;
|
this._rightHome = null;
|
||||||
|
|
||||||
handler.setInputAction(() => {
|
handler.setInputAction(() => {
|
||||||
camera.cancelFlight(); // 取消可能仍在进行的回正动画
|
camera.cancelFlight(); // 取消可能仍在进行的回正动画
|
||||||
this._rightRestore = {
|
this._rightHome = Cesium.Cartesian3.clone(camera.position);
|
||||||
position: Cesium.Cartesian3.clone(camera.position),
|
|
||||||
direction: Cesium.Cartesian3.clone(camera.direction),
|
|
||||||
up: Cesium.Cartesian3.clone(camera.up),
|
|
||||||
};
|
|
||||||
}, Cesium.ScreenSpaceEventType.RIGHT_DOWN);
|
}, Cesium.ScreenSpaceEventType.RIGHT_DOWN);
|
||||||
|
|
||||||
handler.setInputAction(() => {
|
handler.setInputAction(() => {
|
||||||
if (!this._rightRestore) return;
|
// 松开右键:只清掉 tilt 惯性余力,不回正
|
||||||
const s = this._rightRestore;
|
|
||||||
this._rightRestore = null;
|
|
||||||
const tiltState = scene.screenSpaceCameraController._lastInertiaTiltMovement;
|
const tiltState = scene.screenSpaceCameraController._lastInertiaTiltMovement;
|
||||||
if (tiltState) tiltState.inertiaEnabled = false;
|
if (tiltState) tiltState.inertiaEnabled = false;
|
||||||
// 回正改用 flyTo 渐变,而不是瞬间 setView
|
}, Cesium.ScreenSpaceEventType.RIGHT_UP);
|
||||||
|
|
||||||
|
handler.setInputAction(() => {
|
||||||
|
// 中键:回正(天底 + 北朝上,保持原注视点)
|
||||||
|
if (!this._rightHome) return;
|
||||||
|
const pos = this._rightHome;
|
||||||
|
this._rightHome = null;
|
||||||
camera.flyTo({
|
camera.flyTo({
|
||||||
destination: s.position,
|
destination: pos,
|
||||||
orientation: { direction: s.direction, up: s.up },
|
orientation: { heading: 0, pitch: Cesium.Math.toRadians(-90), roll: 0 },
|
||||||
duration: 0.5,
|
duration: 0.5,
|
||||||
});
|
});
|
||||||
}, Cesium.ScreenSpaceEventType.RIGHT_UP);
|
}, Cesium.ScreenSpaceEventType.MIDDLE_DOWN);
|
||||||
|
|
||||||
this._rightRestoreHandler = handler;
|
this._rightRestoreHandler = handler;
|
||||||
}
|
}
|
||||||
@@ -441,13 +557,15 @@ export class CesiumGlobe {
|
|||||||
let cname = props.NAME_ZH || props.NAME;
|
let cname = props.NAME_ZH || props.NAME;
|
||||||
if (cname === '中华民国') cname = '中国台湾';
|
if (cname === '中华民国') cname = '中国台湾';
|
||||||
if (cname && props.LABEL_X != null && props.LABEL_Y != null) {
|
if (cname && props.LABEL_X != null && props.LABEL_Y != null) {
|
||||||
const fontSize = areaKm2 > 5e6 ? 22 : areaKm2 > 1.5e6 ? 15 : areaKm2 > 3e5 ? 12 : 10;
|
const fontSize = areaKm2 > 5e6 ? 28 : areaKm2 > 1.5e6 ? 21 : areaKm2 > 3e5 ? 18 : 16;
|
||||||
countryLabels.add({
|
countryLabels.add({
|
||||||
position: Cesium.Cartesian3.fromDegrees(props.LABEL_X, props.LABEL_Y),
|
position: Cesium.Cartesian3.fromDegrees(props.LABEL_X, props.LABEL_Y),
|
||||||
text: cname,
|
text: cname,
|
||||||
font: `bold ${fontSize}px "Noto Serif SC", "Songti SC", "STSong", "SimSun", Georgia, serif`,
|
font: `bold ${fontSize}px "Noto Serif SC", "Songti SC", "STSong", "SimSun", Georgia, serif`,
|
||||||
fillColor: black,
|
fillColor: textGold,
|
||||||
style: Cesium.LabelStyle.FILL,
|
outlineColor: black,
|
||||||
|
outlineWidth: 2,
|
||||||
|
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
|
||||||
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
||||||
disableDepthTestDistance: 1000, // 1km 内不深度测试(近处不消失),远处仍遮挡背面
|
disableDepthTestDistance: 1000, // 1km 内不深度测试(近处不消失),远处仍遮挡背面
|
||||||
distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, 1e7),
|
distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, 1e7),
|
||||||
@@ -491,8 +609,8 @@ export class CesiumGlobe {
|
|||||||
const scene = this.viewer.scene;
|
const scene = this.viewer.scene;
|
||||||
const textGold = Cesium.Color.fromCssColorString('#e0c46a');
|
const textGold = Cesium.Color.fromCssColorString('#e0c46a');
|
||||||
const black = Cesium.Color.fromCssColorString('#14100d');
|
const black = Cesium.Color.fromCssColorString('#14100d');
|
||||||
const ddc = new Cesium.DistanceDisplayCondition(0, 4e6);
|
const ddc = new Cesium.DistanceDisplayCondition(0, 2.5e6);
|
||||||
const tdc = new Cesium.NearFarScalar(2e6, 1.0, 4e6, 0.0);
|
const tdc = new Cesium.NearFarScalar(1.25e6, 1.0, 2.5e6, 0.0);
|
||||||
|
|
||||||
let cities = [];
|
let cities = [];
|
||||||
try {
|
try {
|
||||||
@@ -522,9 +640,11 @@ export class CesiumGlobe {
|
|||||||
labels.add({
|
labels.add({
|
||||||
position,
|
position,
|
||||||
text: c.name,
|
text: c.name,
|
||||||
font: 'bold 11px "Noto Serif SC", "Songti SC", "STSong", "SimSun", Georgia, serif',
|
font: 'bold 17px "Noto Serif SC", "Songti SC", "STSong", "SimSun", Georgia, serif',
|
||||||
fillColor: black,
|
fillColor: textGold,
|
||||||
style: Cesium.LabelStyle.FILL,
|
outlineColor: black,
|
||||||
|
outlineWidth: 2,
|
||||||
|
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
|
||||||
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
|
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
|
||||||
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
||||||
pixelOffset: new Cesium.Cartesian2(0, -6),
|
pixelOffset: new Cesium.Cartesian2(0, -6),
|
||||||
|
|||||||
@@ -44,10 +44,8 @@ html, body {
|
|||||||
}
|
}
|
||||||
.marker-note {
|
.marker-note {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
pointer-events: auto;
|
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: 100% 100%;
|
background-size: 100% 100%;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 气氛遮罩 */
|
/* 气氛遮罩 */
|
||||||
|
|||||||
Reference in New Issue
Block a user