Add 3D loose-leaf chronicle of human civilisation

A Three.js site presenting 20 world history milestones as
double-sided loose-leaf pages, with three views: a leather
binder with page-flip animation, a golden timeline, and a
dotted globe with great-circle arcs.

All textures (paper, leather, registration cards, and 18
hand-drawn sketches) are generated procedurally via Canvas
2D, so no external image assets are needed beyond the Earth
map. Includes dev helper scripts for screenshots, ASCII
previews, and headless verification.
This commit is contained in:
2026-09-18 19:19:53 +08:00
commit 4e818d53e3
24 changed files with 4374 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
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(超出视口,可横向滚动)
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: 30, color: '#e8d9b4' });
name.scale.multiplyScalar(0.22);
name.position.set(x, 0.42, 0);
// 轴下方:时间(常显)
const time = makeTextSprite(entry.time, { fontSize: 22, color: '#c9a227' });
time.scale.multiplyScalar(0.18);
time.position.set(x, -1.06, 0);
this.group.add(card, name, time);
this.cards.push({ card, outline, 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 ? 0.14 : 0;
});
}
}