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,39 @@
|
||||
// 缓动函数与小工具
|
||||
|
||||
export const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
|
||||
export const lerp = (a, b, t) => a + (b - a) * t;
|
||||
|
||||
export const easeInOutCubic = (t) =>
|
||||
t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||
|
||||
export const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);
|
||||
export const easeOutQuint = (t) => 1 - Math.pow(1 - t, 5);
|
||||
export const easeInOutQuint = (t) =>
|
||||
t < 0.5 ? 16 * t * t * t * t * t : 1 - Math.pow(-2 * t + 2, 5) / 2;
|
||||
|
||||
// 简单的补间器:由调用方按帧推进
|
||||
export class Tween {
|
||||
constructor({ from = 0, to = 1, duration = 1, ease = easeInOutCubic, onUpdate, onDone } = {}) {
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.duration = duration;
|
||||
this.ease = ease;
|
||||
this.onUpdate = onUpdate;
|
||||
this.onDone = onDone;
|
||||
this.t = 0;
|
||||
this.done = false;
|
||||
}
|
||||
|
||||
update(dt) {
|
||||
if (this.done) return true;
|
||||
this.t += dt / this.duration;
|
||||
let k = clamp(this.t, 0, 1);
|
||||
const e = this.ease(k);
|
||||
if (this.onUpdate) this.onUpdate(lerp(this.from, this.to, e), k);
|
||||
if (this.t >= 1) {
|
||||
this.done = true;
|
||||
if (this.onDone) this.onDone();
|
||||
}
|
||||
return this.done;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
// 用 Canvas 生成文字精灵(始终面向相机的标签)
|
||||
export function makeTextSprite(text, {
|
||||
fontSize = 42,
|
||||
color = '#e0c46a',
|
||||
stroke = 'rgba(20,12,6,0.9)',
|
||||
strokeWidth = 6,
|
||||
padding = 24,
|
||||
font = 'Georgia, "Noto Serif SC", serif',
|
||||
weight = '600',
|
||||
letterSpacing = '0.08em',
|
||||
} = {}) {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.font = `${weight} ${fontSize}px ${font}`;
|
||||
const w = Math.ceil(ctx.measureText(text).width) + padding * 2;
|
||||
const h = Math.ceil(fontSize * 1.6);
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
|
||||
const c2 = canvas.getContext('2d');
|
||||
c2.font = ctx.font;
|
||||
c2.textBaseline = 'middle';
|
||||
c2.textAlign = 'center';
|
||||
c2.lineJoin = 'round';
|
||||
if (stroke) {
|
||||
c2.lineWidth = strokeWidth;
|
||||
c2.strokeStyle = stroke;
|
||||
c2.strokeText(text, w / 2, h / 2);
|
||||
}
|
||||
c2.fillStyle = color;
|
||||
c2.fillText(text, w / 2, h / 2);
|
||||
|
||||
const tex = new THREE.CanvasTexture(canvas);
|
||||
tex.colorSpace = THREE.SRGBColorSpace;
|
||||
tex.anisotropy = 4;
|
||||
const mat = new THREE.SpriteMaterial({ map: tex, transparent: true, depthTest: false });
|
||||
const sprite = new THREE.Sprite(mat);
|
||||
const aspect = w / h;
|
||||
sprite.scale.set(aspect, 1, 1);
|
||||
return sprite;
|
||||
}
|
||||
|
||||
export function setSpriteText(sprite, text) {
|
||||
// 惰性:直接重建内部(简单实现:返回新精灵)
|
||||
return makeTextSprite(text);
|
||||
}
|
||||
Reference in New Issue
Block a user