发布:完整项目代码(三视图 + 词条数据 + 加载页等)
This commit is contained in:
+48
-27
@@ -31,8 +31,8 @@ export class App {
|
||||
|
||||
this.overlay = new Overlay({
|
||||
onView: (v) => this.switchView(v),
|
||||
onPrev: () => this._step(-1),
|
||||
onNext: () => this._step(1),
|
||||
onPrev: () => this._stepByView(-1),
|
||||
onNext: () => this._stepByView(1),
|
||||
onFlip: () => this._step(1),
|
||||
});
|
||||
this.overlay.setTotal(ENTRIES.length);
|
||||
@@ -59,17 +59,16 @@ export class App {
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => this.timeline.initLabels()));
|
||||
}
|
||||
|
||||
// 首屏加载完成后由 main.js 调用:后台预载地理视图,就绪后激活“地理”按钮
|
||||
// 首屏加载完成后由 main.js 调用:后台预载地理视图,就绪后允许循环切换到地理
|
||||
preloadGlobe() {
|
||||
const btn = document.querySelector('.view-btn[data-view="globe"]');
|
||||
this.globe.viewerReady.then(() => { if (btn) btn.disabled = false; });
|
||||
this.globe.viewerReady.then(() => this.overlay.setGlobeReady(true));
|
||||
this.globe.ensureCreated();
|
||||
}
|
||||
|
||||
_initRenderer() {
|
||||
const canvas = document.getElementById('scene');
|
||||
this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
||||
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
|
||||
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // 恢复高分辨率渲染,避免糊
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
this.renderer.shadowMap.enabled = true;
|
||||
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||
@@ -121,16 +120,19 @@ export class App {
|
||||
const rim = new THREE.DirectionalLight(0x8fa0d0, 0.7);
|
||||
rim.position.set(-2.5, 1.2, -3.5);
|
||||
this.scene.add(rim);
|
||||
this.rimLight = rim;
|
||||
|
||||
// 低角度掠射光:横扫皮面,凸显皮革纹理凹凸
|
||||
const graze = new THREE.DirectionalLight(0xffd9a8, 1.1);
|
||||
graze.position.set(-4.5, 0.7, 2.6);
|
||||
this.scene.add(graze);
|
||||
this.grazeLight = graze;
|
||||
|
||||
// 背面补光:让外侧牛皮从背面看也可见
|
||||
const backLight = new THREE.DirectionalLight(0xffd9a8, 0.55);
|
||||
backLight.position.set(0.5, 1.2, -3.5);
|
||||
this.scene.add(backLight);
|
||||
this.backLight = backLight;
|
||||
}
|
||||
|
||||
_initScene() {
|
||||
@@ -212,10 +214,11 @@ export class App {
|
||||
// 时间轴:按下开始拖拽左右滑动;未移动的抬起才视为点击选中
|
||||
this.timelineSlide = null; // 手动拖拽时取消居中动画
|
||||
this.timeline.setPausedGeneration(true); // 拖拽期间暂停纹理生成,保持流畅
|
||||
this._drag = { active: true, startX: e.clientX, startY: e.clientY, moved: false, offset: this.timelineOffset };
|
||||
this._drag = { active: true, view: 'timeline', startX: e.clientX, startY: e.clientY, moved: false, offset: this.timelineOffset };
|
||||
canvas.setPointerCapture(e.pointerId);
|
||||
} else {
|
||||
this._pick();
|
||||
} else if (this.view === 'binder') {
|
||||
// 活页本:按下仅记录,松开且未拖拽(<4px)时才翻页
|
||||
this._drag = { active: true, view: 'binder', startX: e.clientX, startY: e.clientY, moved: false };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -227,14 +230,14 @@ export class App {
|
||||
if (!this._drag.moved && (Math.abs(dx) > 4 || Math.abs(dy) > 4)) {
|
||||
this._drag.moved = true;
|
||||
}
|
||||
if (this._drag.moved) {
|
||||
if (this._drag.view === 'timeline' && this._drag.moved) {
|
||||
const limit = TIMELINE_SPREAD + 0.6;
|
||||
this.timelineOffset = clamp(this._drag.offset + dx * this._worldPerPixel(), -limit, limit);
|
||||
this.timeline.group.position.x = this.timelineOffset;
|
||||
canvas.style.cursor = 'grabbing';
|
||||
this._needsRender = true;
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this._hoverDirty = true; // 射线检测移到 _animate 中每帧至多执行一次
|
||||
});
|
||||
@@ -242,10 +245,16 @@ export class App {
|
||||
canvas.addEventListener('pointerup', (e) => {
|
||||
if (this._drag && this._drag.active) {
|
||||
const wasClick = !this._drag.moved;
|
||||
const view = this._drag.view;
|
||||
this._drag.active = false;
|
||||
canvas.style.cursor = 'grab';
|
||||
this.timeline.setPausedGeneration(false); // 拖拽结束,恢复纹理生成
|
||||
if (wasClick) this._pick();
|
||||
if (view === 'timeline') {
|
||||
this.timeline.setPausedGeneration(false); // 拖拽结束,恢复纹理生成
|
||||
if (wasClick) this._pick();
|
||||
} else if (view === 'binder' && wasClick) {
|
||||
setPointer(e); // 用松开位置做射线,松开时触发翻页
|
||||
this._pick();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -260,8 +269,7 @@ export class App {
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
|
||||
const dir = e.key === 'ArrowRight' ? 1 : -1;
|
||||
if (this.view === 'timeline') this._stepTimeline(dir);
|
||||
else this._step(dir);
|
||||
this._stepByView(dir);
|
||||
}
|
||||
else if (e.key === '1') this.switchView('binder');
|
||||
else if (e.key === '2') this.switchView('timeline');
|
||||
@@ -303,10 +311,10 @@ export class App {
|
||||
_hitTest() {
|
||||
let list;
|
||||
if (this.view === 'binder') {
|
||||
// 只检测顶层可点击页(左右页 + 翻页缓冲),避免对全部 182 页求交
|
||||
// 只检测顶层可点击页(左右顶页 + 正反翻页缓冲),避免对全部页求交
|
||||
list = [];
|
||||
const cur = this.binder.current;
|
||||
for (let d = -1; d <= 1; d++) {
|
||||
for (let d = -2; d <= 1; d++) {
|
||||
const i = cur + d;
|
||||
if (i >= 0 && i < this.binder.pages.length) list.push(this.binder.pages[i].mesh);
|
||||
}
|
||||
@@ -333,15 +341,11 @@ export class App {
|
||||
if (!hit) return;
|
||||
const { view, index } = hit.object.userData;
|
||||
if (view === 'binder') {
|
||||
const cur = this.binder.current;
|
||||
const f = this.binder.flip;
|
||||
// 翻页进行中:把“正在翻的页”与“即将露出的下一页”都视为向前翻的目标,点击不阻断
|
||||
if (index === cur || (f && index === f.nextCurrent)) {
|
||||
this.binder.flipForward();
|
||||
} else if (index === cur - 1) {
|
||||
this.binder.flipBackward();
|
||||
// 按点击位置的空间左右侧判断翻页方向(翻页过程中索引与左右侧会错位,不能按索引)
|
||||
if (hit.point.x >= 0) {
|
||||
this.binder.flipForward(); // 点了右侧 → 向前翻
|
||||
} else {
|
||||
this._select(index, { syncBinder: true });
|
||||
this.binder.flipBackward(); // 点了左侧 → 向后翻
|
||||
}
|
||||
} else {
|
||||
this._select(index, { syncBinder: false });
|
||||
@@ -368,6 +372,12 @@ export class App {
|
||||
else this.binder.flipBackward();
|
||||
}
|
||||
|
||||
// 上一页/下一页(底部按钮与左右键共用):时间轴为选择+居中+翻面,其它为活页本翻页
|
||||
_stepByView(dir) {
|
||||
if (this.view === 'timeline') this._stepTimeline(dir);
|
||||
else this._step(dir);
|
||||
}
|
||||
|
||||
// 时间轴:左右键选择上一张/下一张,动画与点击一致(居中 + 翻面)
|
||||
_stepTimeline(dir) {
|
||||
const next = clamp(this.selected + dir, 0, ENTRIES.length - 1);
|
||||
@@ -378,9 +388,13 @@ export class App {
|
||||
}
|
||||
|
||||
_select(index, { syncBinder = false } = {}) {
|
||||
const isEnd = index >= ENTRIES.length;
|
||||
this.selected = clamp(index, 0, ENTRIES.length - 1);
|
||||
const entry = ENTRIES[this.selected];
|
||||
this.overlay.setEntry(entry, this.selected, ENTRIES.length);
|
||||
if (isEnd) {
|
||||
this.overlay.setEnd(ENTRIES[this.selected]);
|
||||
} else {
|
||||
this.overlay.setEntry(ENTRIES[this.selected], this.selected, ENTRIES.length);
|
||||
}
|
||||
this.timeline.setSelected(this.selected);
|
||||
this.globe.setSelected(this.selected);
|
||||
if (this.view === 'globe') this.globe.flyToEntry(this.selected); // 地球页切换时自动居中
|
||||
@@ -428,6 +442,12 @@ export class App {
|
||||
// 阴影仅在活页本视图启用:时间轴/地理无投影,降低滑动时阴影贴图重绘开销
|
||||
this.renderer.shadowMap.enabled = view === 'binder';
|
||||
|
||||
// 灯光:时间轴只用半球光 + 主方向光;活页本保留全部灯光(减少 PBR 每像素光照计算)
|
||||
const fullLights = view === 'binder';
|
||||
this.rimLight.visible = fullLights;
|
||||
this.grazeLight.visible = fullLights;
|
||||
this.backLight.visible = fullLights;
|
||||
|
||||
// Cesium 地球:地理视图显示 Cesium 画布,其余视图显示 Three.js 画布
|
||||
document.getElementById('scene').style.display = isGlobe ? 'none' : 'block';
|
||||
document.getElementById('cesium-container').classList.toggle('visible', isGlobe);
|
||||
@@ -526,6 +546,7 @@ export class App {
|
||||
// 是否有进行中的动画(决定本帧是否需要重绘)
|
||||
const animating =
|
||||
this.binder.flip !== null ||
|
||||
this.binder.resetTween !== null ||
|
||||
this.binder.isSettling() ||
|
||||
this.timeline.hasActiveFlip() ||
|
||||
this.cameraTween !== null ||
|
||||
|
||||
Reference in New Issue
Block a user