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.
This commit is contained in:
+73
-17
@@ -1,5 +1,43 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
// 超采样倍数:文字以更高分辨率栅格化,拉近/放大后依然清晰
|
||||
const SS = 4;
|
||||
|
||||
// 折行:尽量均分两行(标题较短),均分不满足时逐字折行
|
||||
function wrapText(ctx, text, maxW) {
|
||||
if (maxW === Infinity || ctx.measureText(text).width <= maxW) return [text];
|
||||
|
||||
let best = 1;
|
||||
let bestDiff = Infinity;
|
||||
for (let i = 1; i < text.length; i++) {
|
||||
const a = ctx.measureText(text.slice(0, i)).width;
|
||||
const b = ctx.measureText(text.slice(i)).width;
|
||||
if (a > maxW || b > maxW) continue;
|
||||
const diff = Math.abs(a - b);
|
||||
if (diff < bestDiff) { bestDiff = diff; best = i; }
|
||||
}
|
||||
const first = text.slice(0, best);
|
||||
const second = text.slice(best);
|
||||
if (ctx.measureText(first).width <= maxW && ctx.measureText(second).width <= maxW) {
|
||||
return [first, second];
|
||||
}
|
||||
|
||||
// 兜底:逐字折行
|
||||
const lines = [];
|
||||
let line = '';
|
||||
for (const ch of text) {
|
||||
if (line && ctx.measureText(line + ch).width > maxW) {
|
||||
lines.push(line);
|
||||
line = ch;
|
||||
} else {
|
||||
line += ch;
|
||||
}
|
||||
}
|
||||
if (line) lines.push(line);
|
||||
if (!lines.length) lines.push('');
|
||||
return lines;
|
||||
}
|
||||
|
||||
// 用 Canvas 生成文字精灵(始终面向相机的标签)
|
||||
export function makeTextSprite(text, {
|
||||
fontSize = 42,
|
||||
@@ -9,36 +47,54 @@ export function makeTextSprite(text, {
|
||||
padding = 24,
|
||||
font = 'Georgia, "Noto Serif SC", serif',
|
||||
weight = '600',
|
||||
letterSpacing = '0.08em',
|
||||
scale = 1, // 单行文字的精灵世界高度
|
||||
maxWidth = Infinity, // 精灵世界最大宽度,超宽则居中换行
|
||||
lineHeight = 1.6, // 行高倍数(相对字号)
|
||||
} = {}) {
|
||||
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 fontStr = `${weight} ${fontSize}px ${font}`;
|
||||
ctx.font = fontStr;
|
||||
|
||||
const lineH = Math.ceil(fontSize * lineHeight);
|
||||
// 每行文本像素预算:保证该行显示宽度 ≤ maxWidth 世界单位
|
||||
const textBudget = maxWidth === Infinity ? Infinity : (maxWidth * lineH) / scale - padding * 2;
|
||||
|
||||
const lines = wrapText(ctx, text, textBudget);
|
||||
const maxLineW = Math.max(0, ...lines.map((l) => ctx.measureText(l).width));
|
||||
const w = Math.ceil(maxLineW) + padding * 2;
|
||||
const h = lineH * lines.length;
|
||||
|
||||
canvas.width = Math.ceil(w * SS);
|
||||
canvas.height = Math.ceil(h * SS);
|
||||
|
||||
const c2 = canvas.getContext('2d');
|
||||
c2.font = ctx.font;
|
||||
c2.scale(SS, SS);
|
||||
c2.font = fontStr;
|
||||
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);
|
||||
|
||||
lines.forEach((line, i) => {
|
||||
const y = lineH * (i + 0.5);
|
||||
if (stroke) {
|
||||
c2.lineWidth = strokeWidth;
|
||||
c2.strokeStyle = stroke;
|
||||
c2.strokeText(line, w / 2, y);
|
||||
}
|
||||
c2.fillStyle = color;
|
||||
c2.fillText(line, w / 2, y);
|
||||
});
|
||||
|
||||
const tex = new THREE.CanvasTexture(canvas);
|
||||
tex.colorSpace = THREE.SRGBColorSpace;
|
||||
tex.anisotropy = 4;
|
||||
tex.anisotropy = 8;
|
||||
tex.minFilter = THREE.LinearMipmapLinearFilter;
|
||||
tex.generateMipmaps = true;
|
||||
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);
|
||||
// 单行世界高度 = scale;多行总高度 = numLines * scale(宽度按 w/lineH 同步换算)
|
||||
sprite.scale.set((w / lineH) * scale, lines.length * scale, 1);
|
||||
return sprite;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user