Files
Hist/src/scene/timeline.js
T
fukunee 13d3ef180a Replace Three.js globe with Cesium
Migrate the globe view to a Cesium viewer with Esri satellite
imagery and terrain, dropping the custom blue-marble texture and
dot-bead rendering. Add vite-plugin-cesium and a container element
for the viewer.

Also rework the binder leather and suede materials with dedicated
normal maps, and refine timeline labels and page hole sizing.
2026-09-18 21:29:30 +08:00

98 lines
3.4 KiB
JavaScript

import * as THREE from 'three';
import { ENTRIES } from '../data/entries.js';
import { getPageTextures } from '../textures/pages.js';
import { makeTextSprite } from '../utils/labels.js';
export const TIMELINE_SPREAD = 6.2; // 横轴范围 ±6.2(超出视口,可横向滚动)
const CARD_FLOAT = 0.14; // 选中时上浮高度
const NAME_Y = 0.42; // 主题标签基准高度(活页上方)
function cardX(i, n) {
return (i / (n - 1)) * 2 * TIMELINE_SPREAD - TIMELINE_SPREAD;
}
export class Timeline {
constructor() {
this.group = new THREE.Group();
this.cards = [];
this.selected = -1;
this._buildLine();
this._buildCards();
}
_buildLine() {
const n = ENTRIES.length;
const min = cardX(0, n) - 0.4;
const max = cardX(n - 1, n) + 0.4;
const lineMat = new THREE.LineBasicMaterial({ color: 0xc9a227, transparent: true, opacity: 0.7 });
const lineGeo = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(min, -0.85, 0), new THREE.Vector3(max, -0.85, 0),
]);
this.group.add(new THREE.Line(lineGeo, lineMat));
const glowMat = new THREE.LineBasicMaterial({ color: 0xe0c46a, transparent: true, opacity: 0.22 });
const glowGeo = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(min, -0.855, -0.03), new THREE.Vector3(max, -0.855, -0.03),
]);
this.group.add(new THREE.Line(glowGeo, glowMat));
// 刻度:向上指向卡片、向下指向时间标签
const tickMat = new THREE.LineBasicMaterial({ color: 0xc9a227, transparent: true, opacity: 0.5 });
ENTRIES.forEach((e, i) => {
const x = cardX(i, n);
const g = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(x, -0.98, 0), new THREE.Vector3(x, -0.7, 0),
]);
this.group.add(new THREE.Line(g, tickMat));
});
}
_buildCards() {
const n = ENTRIES.length;
const cardGeo = new THREE.PlaneGeometry(0.4, 0.565);
ENTRIES.forEach((entry, i) => {
const x = cardX(i, n);
const { front } = getPageTextures(entry);
const mat = new THREE.MeshStandardMaterial({ map: front, roughness: 0.85, metalness: 0.0 });
const card = new THREE.Mesh(cardGeo, mat);
card.position.set(x, 0, 0);
card.userData = { view: 'timeline', index: i };
card.castShadow = true;
const outline = new THREE.Mesh(
new THREE.PlaneGeometry(0.4 * 1.1, 0.565 * 1.1),
new THREE.MeshBasicMaterial({ color: 0xc9a227, transparent: true, opacity: 0.9, side: THREE.BackSide })
);
outline.position.z = -0.01;
outline.visible = false;
card.add(outline);
// 活页上方:主题(常显,随选中上浮,超宽居中换行)
const name = makeTextSprite(entry.name, { fontSize: 17, color: '#e8d9b4', scale: 0.13, maxWidth: 1.0 });
name.position.set(x, NAME_Y, 0);
// 轴下方:时间(常显,固定不动)
const time = makeTextSprite(entry.time, { fontSize: 13, color: '#c9a227', scale: 0.11 });
time.position.set(x, -1.06, 0);
this.group.add(card, name, time);
this.cards.push({ card, outline, name, nameBaseY: NAME_Y, index: i });
});
}
setSelected(index) {
this.selected = index;
this.cards.forEach((c) => {
const on = c.index === index;
c.outline.visible = on;
c.card.position.y = on ? CARD_FLOAT : 0;
c.name.position.y = c.nameBaseY + (on ? CARD_FLOAT : 0);
});
}
}