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:
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user