性能优化:Cesium 懒加载+后台预载、按需渲染、射线检测节流、城市标签 primitive 化、时间轴纹理跟随可视区域
- Cesium 改为动态加载(vite 插件不再注入 6MB 脚本),首屏零 Cesium 请求 - 首帧渲染后后台预载地理视图,loading 展示 8 秒期间并行,就绪前地理按钮置灰 - Three.js 按需渲染:静止停止重绘,动画/交互/状态变化时渲染 - 射线检测合帧节流,binder 只测当前页±1、时间轴只测可视窗口 - 移除 vignette 的 mix-blend-mode 全屏合成 - 7342 个城市从 entity 改为 PointPrimitiveCollection + LabelCollection - 时间轴懒加载窗口跟随可视区域(拖拽/缩放实时跟随)
This commit is contained in:
+1
-1
@@ -39,7 +39,7 @@
|
|||||||
<div class="view-switch" aria-label="视图切换">
|
<div class="view-switch" aria-label="视图切换">
|
||||||
<button class="view-btn active" data-view="binder">活页本</button>
|
<button class="view-btn active" data-view="binder">活页本</button>
|
||||||
<button class="view-btn" data-view="timeline">时间轴</button>
|
<button class="view-btn" data-view="timeline">时间轴</button>
|
||||||
<button class="view-btn" data-view="globe">地理</button>
|
<button class="view-btn" data-view="globe" disabled title="地理视图加载中">地理</button>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
+70
-11
@@ -21,6 +21,7 @@ export class App {
|
|||||||
this.timelineSlide = null;
|
this.timelineSlide = null;
|
||||||
this.zoomTarget = null;
|
this.zoomTarget = null;
|
||||||
this.cameraMemory = {};
|
this.cameraMemory = {};
|
||||||
|
this._needsRender = true; // 按需渲染:首帧必渲染
|
||||||
|
|
||||||
this._initRenderer();
|
this._initRenderer();
|
||||||
this._initLights();
|
this._initLights();
|
||||||
@@ -36,6 +37,14 @@ export class App {
|
|||||||
|
|
||||||
this._initEvents();
|
this._initEvents();
|
||||||
this._applyRotationLimits(CAMERA_POSES.binder);
|
this._applyRotationLimits(CAMERA_POSES.binder);
|
||||||
|
// 默认新打开时:活页本放到最小(相机沿默认视角拉到最远)
|
||||||
|
{
|
||||||
|
const bp = CAMERA_POSES.binder;
|
||||||
|
const dir = bp.pos.clone().sub(bp.target).normalize();
|
||||||
|
this.camera.position.copy(bp.target).addScaledVector(dir, bp.max);
|
||||||
|
this.controls.target.copy(bp.target);
|
||||||
|
this.controls.update();
|
||||||
|
}
|
||||||
this._select(0, { syncBinder: false });
|
this._select(0, { syncBinder: false });
|
||||||
this.binder.syncTo(0);
|
this.binder.syncTo(0);
|
||||||
|
|
||||||
@@ -45,6 +54,13 @@ export class App {
|
|||||||
requestAnimationFrame(this._animate);
|
requestAnimationFrame(this._animate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 首屏加载完成后由 main.js 调用:后台预载地理视图,就绪后激活“地理”按钮
|
||||||
|
preloadGlobe() {
|
||||||
|
const btn = document.querySelector('.view-btn[data-view="globe"]');
|
||||||
|
this.globe.viewerReady.then(() => { if (btn) btn.disabled = false; });
|
||||||
|
this.globe.ensureCreated();
|
||||||
|
}
|
||||||
|
|
||||||
_initRenderer() {
|
_initRenderer() {
|
||||||
const canvas = document.getElementById('scene');
|
const canvas = document.getElementById('scene');
|
||||||
this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
||||||
@@ -74,6 +90,7 @@ export class App {
|
|||||||
this.controls.mouseButtons.MIDDLE = -1;
|
this.controls.mouseButtons.MIDDLE = -1;
|
||||||
this.controls.mouseButtons.RIGHT = -1;
|
this.controls.mouseButtons.RIGHT = -1;
|
||||||
this.controls.rotateSpeed = 0.4; // 整体降速(无边缘减速)
|
this.controls.rotateSpeed = 0.4; // 整体降速(无边缘减速)
|
||||||
|
this.controls.addEventListener('change', () => { this._needsRender = true; });
|
||||||
this.controls.update();
|
this.controls.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,15 +101,16 @@ export class App {
|
|||||||
const key = new THREE.DirectionalLight(0xffe6c0, 2.4);
|
const key = new THREE.DirectionalLight(0xffe6c0, 2.4);
|
||||||
key.position.set(3.2, 4.4, 3.6);
|
key.position.set(3.2, 4.4, 3.6);
|
||||||
key.castShadow = true;
|
key.castShadow = true;
|
||||||
key.shadow.mapSize.set(1024, 1024);
|
key.shadow.mapSize.set(2048, 2048);
|
||||||
key.shadow.camera.near = 0.5;
|
key.shadow.camera.near = 0.5;
|
||||||
key.shadow.camera.far = 30;
|
key.shadow.camera.far = 30;
|
||||||
key.shadow.camera.left = -5;
|
key.shadow.camera.left = -3.5;
|
||||||
key.shadow.camera.right = 5;
|
key.shadow.camera.right = 3.5;
|
||||||
key.shadow.camera.top = 6;
|
key.shadow.camera.top = 4;
|
||||||
key.shadow.camera.bottom = -6;
|
key.shadow.camera.bottom = -4;
|
||||||
key.shadow.bias = -0.0002;
|
key.shadow.bias = -0.0005;
|
||||||
this.scene.add(key);
|
this.scene.add(key);
|
||||||
|
this.scene.add(key.target);
|
||||||
this.keyLight = key;
|
this.keyLight = key;
|
||||||
|
|
||||||
const rim = new THREE.DirectionalLight(0x8fa0d0, 0.7);
|
const rim = new THREE.DirectionalLight(0x8fa0d0, 0.7);
|
||||||
@@ -116,7 +134,7 @@ export class App {
|
|||||||
|
|
||||||
this.timeline = new Timeline();
|
this.timeline = new Timeline();
|
||||||
this.globe = new CesiumGlobe({ onPick: (i) => this._pickGlobe(i) });
|
this.globe = new CesiumGlobe({ onPick: (i) => this._pickGlobe(i) });
|
||||||
this.globe.ensureCreated(); // 打开页面时就渲染缓存好三个视图
|
// Cesium 懒加载:首次进入“地理”视图时才真正创建 viewer
|
||||||
|
|
||||||
this.scene.add(this.binder.group);
|
this.scene.add(this.binder.group);
|
||||||
this.scene.add(this.timeline.group);
|
this.scene.add(this.timeline.group);
|
||||||
@@ -173,6 +191,7 @@ export class App {
|
|||||||
this.raycaster = new THREE.Raycaster();
|
this.raycaster = new THREE.Raycaster();
|
||||||
this.pointer = new THREE.Vector2();
|
this.pointer = new THREE.Vector2();
|
||||||
this._drag = null;
|
this._drag = null;
|
||||||
|
this._hoverDirty = false;
|
||||||
const canvas = this.renderer.domElement;
|
const canvas = this.renderer.domElement;
|
||||||
|
|
||||||
const setPointer = (e) => {
|
const setPointer = (e) => {
|
||||||
@@ -207,11 +226,11 @@ export class App {
|
|||||||
this.timelineOffset = clamp(this._drag.offset + dx * this._worldPerPixel(), -limit, limit);
|
this.timelineOffset = clamp(this._drag.offset + dx * this._worldPerPixel(), -limit, limit);
|
||||||
this.timeline.group.position.x = this.timelineOffset;
|
this.timeline.group.position.x = this.timelineOffset;
|
||||||
canvas.style.cursor = 'grabbing';
|
canvas.style.cursor = 'grabbing';
|
||||||
|
this._needsRender = true;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const hit = this._hitTest();
|
this._hoverDirty = true; // 射线检测移到 _animate 中每帧至多执行一次
|
||||||
canvas.style.cursor = hit ? 'pointer' : 'grab';
|
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener('pointerup', (e) => {
|
canvas.addEventListener('pointerup', (e) => {
|
||||||
@@ -271,8 +290,21 @@ export class App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_hitTest() {
|
_hitTest() {
|
||||||
const list = this.interactives[this.view];
|
let list;
|
||||||
if (!list || !list.length) return null;
|
if (this.view === 'binder') {
|
||||||
|
// 只检测顶层可点击页(左右页 + 翻页缓冲),避免对全部 182 页求交
|
||||||
|
list = [];
|
||||||
|
const cur = this.binder.current;
|
||||||
|
for (let d = -1; d <= 1; d++) {
|
||||||
|
const i = cur + d;
|
||||||
|
if (i >= 0 && i < this.binder.pages.length) list.push(this.binder.pages[i].mesh);
|
||||||
|
}
|
||||||
|
} else if (this.view === 'timeline') {
|
||||||
|
list = this.timeline.pickableMeshes();
|
||||||
|
} else {
|
||||||
|
return null; // 地理视图由 Cesium 自行拾取
|
||||||
|
}
|
||||||
|
if (!list.length) return null;
|
||||||
this.raycaster.setFromCamera(this.pointer, this.camera);
|
this.raycaster.setFromCamera(this.pointer, this.camera);
|
||||||
const hits = this.raycaster.intersectObjects(list, false);
|
const hits = this.raycaster.intersectObjects(list, false);
|
||||||
return hits.length ? hits[0] : null;
|
return hits.length ? hits[0] : null;
|
||||||
@@ -331,10 +363,13 @@ export class App {
|
|||||||
this.globe.setSelected(this.selected);
|
this.globe.setSelected(this.selected);
|
||||||
if (this.view === 'globe') this.globe.flyToEntry(this.selected); // 地球页切换时自动居中
|
if (this.view === 'globe') this.globe.flyToEntry(this.selected); // 地球页切换时自动居中
|
||||||
if (syncBinder) this.binder.syncTo(this.selected);
|
if (syncBinder) this.binder.syncTo(this.selected);
|
||||||
|
this._needsRender = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
switchView(view) {
|
switchView(view) {
|
||||||
if (view === this.view || !CAMERA_POSES[view]) return;
|
if (view === this.view || !CAMERA_POSES[view]) return;
|
||||||
|
// 地理视图尚未就绪(按钮仍置灰)时忽略切换,键盘也无法绕过
|
||||||
|
if (view === 'globe' && !this.globe.viewer) return;
|
||||||
// 离开当前视图前,记住它的相机位置
|
// 离开当前视图前,记住它的相机位置
|
||||||
if (this.view !== 'globe') {
|
if (this.view !== 'globe') {
|
||||||
this.cameraMemory[this.view] = {
|
this.cameraMemory[this.view] = {
|
||||||
@@ -416,6 +451,7 @@ export class App {
|
|||||||
this.controls.target.copy(pose.target);
|
this.controls.target.copy(pose.target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this._needsRender = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_applyRotationLimits(pose) {
|
_applyRotationLimits(pose) {
|
||||||
@@ -449,16 +485,27 @@ export class App {
|
|||||||
this.camera.updateProjectionMatrix();
|
this.camera.updateProjectionMatrix();
|
||||||
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
if (this.globe) this.globe.resize();
|
if (this.globe) this.globe.resize();
|
||||||
|
this._needsRender = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_animate() {
|
_animate() {
|
||||||
requestAnimationFrame(this._animate);
|
requestAnimationFrame(this._animate);
|
||||||
const dt = Math.min(this.clock.getDelta(), 0.05);
|
const dt = Math.min(this.clock.getDelta(), 0.05);
|
||||||
|
|
||||||
|
// 是否有进行中的动画(决定本帧是否需要重绘)
|
||||||
|
const animating =
|
||||||
|
this.binder.flip !== null ||
|
||||||
|
this.timeline.hasActiveFlip() ||
|
||||||
|
this.cameraTween !== null ||
|
||||||
|
this.zoomTarget !== null ||
|
||||||
|
this.timelineSlide !== null;
|
||||||
|
if (animating) this._needsRender = true;
|
||||||
|
|
||||||
this.binder.update(dt);
|
this.binder.update(dt);
|
||||||
this.timeline.update(dt);
|
this.timeline.update(dt);
|
||||||
this.timeline.drainPending();
|
this.timeline.drainPending();
|
||||||
this._updateTimelineTextures();
|
this._updateTimelineTextures();
|
||||||
|
if (this.timeline.consumeDirty()) this._needsRender = true;
|
||||||
|
|
||||||
if (this.cameraTween) {
|
if (this.cameraTween) {
|
||||||
const t = this.cameraTween;
|
const t = this.cameraTween;
|
||||||
@@ -492,7 +539,19 @@ export class App {
|
|||||||
if (k >= 1) this.timelineSlide = null;
|
if (k >= 1) this.timelineSlide = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 鼠标悬停检测:每帧至多一次(pointermove 高频事件被合帧)
|
||||||
|
if (this._hoverDirty) {
|
||||||
|
this._hoverDirty = false;
|
||||||
|
const hit = this._hitTest();
|
||||||
|
this.renderer.domElement.style.cursor = hit ? 'pointer' : 'grab';
|
||||||
|
}
|
||||||
|
|
||||||
this.controls.update();
|
this.controls.update();
|
||||||
|
|
||||||
|
// 按需渲染:仅在状态/动画/交互变化时重绘,静止时停止渲染
|
||||||
|
if (this._needsRender) {
|
||||||
|
this._needsRender = false;
|
||||||
this.renderer.render(this.scene, this.camera);
|
this.renderer.render(this.scene, this.camera);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
+7
-6
@@ -9,15 +9,16 @@ for (const ev of ['copy', 'cut', 'contextmenu']) {
|
|||||||
const app = new App();
|
const app = new App();
|
||||||
window.__app = app; // 便于调试
|
window.__app = app; // 便于调试
|
||||||
|
|
||||||
// 加载界面:等三个视图初始化完成(最少展示 1 秒,最多等 8 秒)后淡出
|
// 加载界面:等首帧渲染后立即后台预载地理视图(与 loading 并行,不等其淡出);
|
||||||
|
// loading 最少展示 3 秒(期间 Cesium 预载已同步进行)。
|
||||||
const t0 = Date.now();
|
const t0 = Date.now();
|
||||||
const MIN_DISPLAY = 1000;
|
const MIN_DISPLAY = 8000;
|
||||||
Promise.race([
|
requestAnimationFrame(() => {
|
||||||
app.globe.ready,
|
requestAnimationFrame(() => {
|
||||||
new Promise((r) => setTimeout(r, 8000)),
|
app.preloadGlobe(); // 首帧已渲染,立即预载地理界面
|
||||||
]).then(() => {
|
|
||||||
const wait = Math.max(0, MIN_DISPLAY - (Date.now() - t0));
|
const wait = Math.max(0, MIN_DISPLAY - (Date.now() - t0));
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.getElementById('loading').classList.add('hidden');
|
document.getElementById('loading').classList.add('hidden');
|
||||||
}, wait);
|
}, wait);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -124,13 +124,20 @@ export class CesiumGlobe {
|
|||||||
this._visible = false;
|
this._visible = false;
|
||||||
this._pendingData = 2; // 国家边界 + 城市数据
|
this._pendingData = 2; // 国家边界 + 城市数据
|
||||||
this.ready = new Promise((resolve) => { this._resolveReady = resolve; });
|
this.ready = new Promise((resolve) => { this._resolveReady = resolve; });
|
||||||
|
this.viewerReady = new Promise((resolve) => { this._resolveViewerReady = resolve; });
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureCreated() {
|
async ensureCreated() {
|
||||||
if (this._created) return;
|
if (this._created) return;
|
||||||
this._created = true;
|
this._created = true;
|
||||||
|
|
||||||
|
try {
|
||||||
await loadCesium();
|
await loadCesium();
|
||||||
|
} catch (err) {
|
||||||
|
this._created = false; // 失败可重试
|
||||||
|
console.warn('[CesiumGlobe] Cesium 脚本加载失败:', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 调整实体渲染顺序:先渲染标签(城市/国家名),再渲染 Billboard(标记便签),
|
// 调整实体渲染顺序:先渲染标签(城市/国家名),再渲染 Billboard(标记便签),
|
||||||
// 让词条标记始终显示在城市/国家名的上层。
|
// 让词条标记始终显示在城市/国家名的上层。
|
||||||
@@ -255,6 +262,12 @@ export class CesiumGlobe {
|
|||||||
this.setVisible(this._visible);
|
this.setVisible(this._visible);
|
||||||
this.resize();
|
this.resize();
|
||||||
this.flyToEntry(this.selected);
|
this.flyToEntry(this.selected);
|
||||||
|
|
||||||
|
// 地球已就绪(影像/地形/城市数据继续后台加载),通知 App 隐藏加载遮罩
|
||||||
|
if (this._resolveViewerReady) {
|
||||||
|
this._resolveViewerReady();
|
||||||
|
this._resolveViewerReady = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildMarkers() {
|
_buildMarkers() {
|
||||||
|
|||||||
+3
-3
@@ -5,7 +5,7 @@ import { clamp, easeOutCubic } from '../utils/easing.js';
|
|||||||
|
|
||||||
const RING_R = 0.085;
|
const RING_R = 0.085;
|
||||||
const RING_TUBE = 0.0115;
|
const RING_TUBE = 0.0115;
|
||||||
const STACK_GAP = 0.004;
|
const STACK_GAP = 0.002;
|
||||||
const PAGE_GAP = 0.02; // 页内缘离书脊的小空隙
|
const PAGE_GAP = 0.02; // 页内缘离书脊的小空隙
|
||||||
|
|
||||||
function makeLeatherTexture({ emblem = false } = {}) {
|
function makeLeatherTexture({ emblem = false } = {}) {
|
||||||
@@ -533,7 +533,7 @@ export class Binder {
|
|||||||
flipForward() {
|
flipForward() {
|
||||||
if (this.flip) this._finalizeFlip(); // 前一页没翻完也能继续翻
|
if (this.flip) this._finalizeFlip(); // 前一页没翻完也能继续翻
|
||||||
if (!this.canFlipForward()) return;
|
if (!this.canFlipForward()) return;
|
||||||
this.flip = { index: this.current, from: 0, to: -Math.PI, t: 0, duration: 0.6 };
|
this.flip = { index: this.current, from: 0, to: -Math.PI, t: 0, duration: 0.38 };
|
||||||
// 优先更新底部导航与信息卡(翻页开始即切换目标页)
|
// 优先更新底部导航与信息卡(翻页开始即切换目标页)
|
||||||
if (this.onChange) this.onChange((this.current + 1) % this.pages.length);
|
if (this.onChange) this.onChange((this.current + 1) % this.pages.length);
|
||||||
}
|
}
|
||||||
@@ -541,7 +541,7 @@ export class Binder {
|
|||||||
flipBackward() {
|
flipBackward() {
|
||||||
if (this.flip) this._finalizeFlip();
|
if (this.flip) this._finalizeFlip();
|
||||||
if (!this.canFlipBackward()) return;
|
if (!this.canFlipBackward()) return;
|
||||||
this.flip = { index: this.current - 1, from: -Math.PI, to: 0, t: 0, duration: 0.6 };
|
this.flip = { index: this.current - 1, from: -Math.PI, to: 0, t: 0, duration: 0.38 };
|
||||||
// 优先更新底部导航与信息卡
|
// 优先更新底部导航与信息卡
|
||||||
if (this.onChange) this.onChange(this.current - 1);
|
if (this.onChange) this.onChange(this.current - 1);
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-2
@@ -27,6 +27,7 @@ export class Timeline {
|
|||||||
this._visRange = null;
|
this._visRange = null;
|
||||||
this._ready = false;
|
this._ready = false;
|
||||||
this._pending = [];
|
this._pending = [];
|
||||||
|
this._dirty = false;
|
||||||
|
|
||||||
this._buildLine();
|
this._buildLine();
|
||||||
this._buildCards();
|
this._buildCards();
|
||||||
@@ -141,17 +142,21 @@ export class Timeline {
|
|||||||
c.material.setPageMaps(this.placeholder.front, this.placeholder.back);
|
c.material.setPageMaps(this.placeholder.front, this.placeholder.back);
|
||||||
c.isPlaceholder = true;
|
c.isPlaceholder = true;
|
||||||
this._textured.delete(i);
|
this._textured.delete(i);
|
||||||
|
this._dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 进入窗口:分帧异步生成,避免一次性生成 25 页阻塞主线程
|
// 进入窗口:分帧异步生成,避免一次性生成 25 页阻塞主线程
|
||||||
this._pending = [...win].filter((i) => !this._textured.has(i) && this.cards[i].isPlaceholder);
|
const next = [...win].filter((i) => !this._textured.has(i) && this.cards[i].isPlaceholder);
|
||||||
|
if (next.length) this._dirty = true;
|
||||||
|
this._pending = next;
|
||||||
this.drainPending();
|
this.drainPending();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 每帧由 App._animate 驱动,逐批生成窗口内纹理(避免阻塞)
|
// 每帧由 App._animate 驱动,逐批生成窗口内纹理(避免阻塞)
|
||||||
drainPending() {
|
drainPending() {
|
||||||
if (!this._pending || !this._pending.length) return;
|
if (!this._pending || !this._pending.length) return false;
|
||||||
const budget = 2; // 每帧最多生成 2 页
|
const budget = 2; // 每帧最多生成 2 页
|
||||||
|
let did = false;
|
||||||
for (let k = 0; k < budget && this._pending.length; k++) {
|
for (let k = 0; k < budget && this._pending.length; k++) {
|
||||||
const i = this._pending.shift();
|
const i = this._pending.shift();
|
||||||
const c = this.cards[i];
|
const c = this.cards[i];
|
||||||
@@ -160,7 +165,21 @@ export class Timeline {
|
|||||||
c.material.setPageMaps(front, back);
|
c.material.setPageMaps(front, back);
|
||||||
c.isPlaceholder = false;
|
c.isPlaceholder = false;
|
||||||
this._textured.add(i);
|
this._textured.add(i);
|
||||||
|
this._dirty = true;
|
||||||
|
did = true;
|
||||||
}
|
}
|
||||||
|
return did;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasActiveFlip() {
|
||||||
|
return this.cards.some((c) => c.flipAnim);
|
||||||
|
}
|
||||||
|
|
||||||
|
// App 每帧消费“纹理窗口变化”标志,用于按需渲染
|
||||||
|
consumeDirty() {
|
||||||
|
const d = this._dirty;
|
||||||
|
this._dirty = false;
|
||||||
|
return d;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回某张卡片在时间轴组内的横向坐标(用于点击后居中)
|
// 返回某张卡片在时间轴组内的横向坐标(用于点击后居中)
|
||||||
@@ -168,6 +187,16 @@ export class Timeline {
|
|||||||
return cardX(index, ENTRIES.length);
|
return cardX(index, ENTRIES.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 射线检测用:只返回可视窗口内的卡片 mesh(减少求交对象)
|
||||||
|
pickableMeshes() {
|
||||||
|
if (!this._visRange) return this.cards.map((c) => c.mesh);
|
||||||
|
const list = [];
|
||||||
|
for (let i = this._visRange.min; i <= this._visRange.max; i++) {
|
||||||
|
if (this.cards[i]) list.push(this.cards[i].mesh);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
// 点击翻转:每次点击切换正/反面;选中另一张时,其它翻到背面的卡片先翻回正面
|
// 点击翻转:每次点击切换正/反面;选中另一张时,其它翻到背面的卡片先翻回正面
|
||||||
flip(index) {
|
flip(index) {
|
||||||
this.cards.forEach((o) => {
|
this.cards.forEach((o) => {
|
||||||
|
|||||||
+6
-1
@@ -55,7 +55,7 @@ html, body {
|
|||||||
.vignette {
|
.vignette {
|
||||||
position: absolute; inset: 0; pointer-events: none;
|
position: absolute; inset: 0; pointer-events: none;
|
||||||
background: radial-gradient(ellipse at 50% 42%, transparent 40%, rgba(8, 5, 3, 0.55) 100%);
|
background: radial-gradient(ellipse at 50% 42%, transparent 40%, rgba(8, 5, 3, 0.55) 100%);
|
||||||
mix-blend-mode: multiply;
|
/* 不启用 mix-blend-mode,避免每帧全屏合成开销(深色半透明覆盖即可) */
|
||||||
}
|
}
|
||||||
.grain {
|
.grain {
|
||||||
position: absolute; inset: 0; pointer-events: none; opacity: 0.06;
|
position: absolute; inset: 0; pointer-events: none; opacity: 0.06;
|
||||||
@@ -114,6 +114,11 @@ html, body {
|
|||||||
transition: all 0.25s ease;
|
transition: all 0.25s ease;
|
||||||
}
|
}
|
||||||
.view-btn:hover { border-color: var(--gold); color: #fff; }
|
.view-btn:hover { border-color: var(--gold); color: #fff; }
|
||||||
|
.view-btn:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
.view-btn.active {
|
.view-btn.active {
|
||||||
background: linear-gradient(180deg, rgba(201,162,39,0.85), rgba(150,115,20,0.85));
|
background: linear-gradient(180deg, rgba(201,162,39,0.85), rgba(150,115,20,0.85));
|
||||||
color: #1c1308; border-color: var(--gold); font-weight: 600;
|
color: #1c1308; border-color: var(--gold); font-weight: 600;
|
||||||
|
|||||||
+38
-2
@@ -1,6 +1,42 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import cesium from 'vite-plugin-cesium';
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
import serveStatic from 'serve-static';
|
||||||
|
|
||||||
|
// Cesium 懒加载:复制静态资源、注入 CSS,但不注入 6MB 的 Cesium.js 脚本——
|
||||||
|
// 脚本由 CesiumGlobe 首次进入“地理”视图时动态加载,避免阻塞首屏。
|
||||||
|
const CESIUM_URL = '/cesium/';
|
||||||
|
|
||||||
|
function cesiumLazy() {
|
||||||
|
let isBuild = false;
|
||||||
|
return {
|
||||||
|
name: 'cesium-lazy',
|
||||||
|
config(_c, { command }) {
|
||||||
|
isBuild = command === 'build';
|
||||||
|
},
|
||||||
|
configureServer({ middlewares }) {
|
||||||
|
// dev:直接静态服务 Cesium 资源(未压缩版便于调试)
|
||||||
|
const dir = path.join(process.cwd(), 'node_modules/cesium/Build/CesiumUnminified');
|
||||||
|
middlewares.use(CESIUM_URL, serveStatic(dir));
|
||||||
|
},
|
||||||
|
async closeBundle() {
|
||||||
|
if (!isBuild) return;
|
||||||
|
const src = path.join(process.cwd(), 'node_modules/cesium/Build/Cesium');
|
||||||
|
const out = path.join(process.cwd(), 'dist', 'cesium');
|
||||||
|
for (const d of ['Assets', 'ThirdParty', 'Workers', 'Widgets']) {
|
||||||
|
await fs.cp(path.join(src, d), path.join(out, d), { recursive: true });
|
||||||
|
}
|
||||||
|
await fs.copyFile(path.join(src, 'Cesium.js'), path.join(out, 'Cesium.js'));
|
||||||
|
},
|
||||||
|
transformIndexHtml() {
|
||||||
|
return [{
|
||||||
|
tag: 'link',
|
||||||
|
attrs: { rel: 'stylesheet', href: `${CESIUM_URL}Widgets/widgets.css` },
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [cesium()],
|
plugins: [cesiumLazy()],
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user