Tune camera and timeline controls, add globe fly-to
- Clamp binder/timeline zoom ranges and disable OrbitControls zoom, using custom smooth zoom instead - Disable middle/right mouse buttons; reduce rotate speed - Fly camera to picked marker on globe click - Rebuild leather surfaces with a regular grid for smooth curvature - Refine suede and kraft paper textures, punch real page holes - Reposition axis, time labels, and detail card; speed up iris fade
This commit is contained in:
+23
-31
@@ -7,9 +7,9 @@ export const PAGE_W = 1.6; // 世界单位:页宽
|
||||
export const PAGE_H = 2.24; // 世界单位:页高
|
||||
export const TEX_W = 768;
|
||||
export const TEX_H = 1075;
|
||||
export const HOLE_U = [0.085, 0.24, 0.4, 0.56, 0.72, 0.855]; // 活页孔(自顶部归一化)
|
||||
export const HOLE_U = [0.1, 0.26, 0.42, 0.58, 0.74, 0.9]; // 活页孔(自顶部归一化,6孔等距)
|
||||
export const HOLE_X = 0.065; // 孔中心相对页内缘(环前交点 0.085 − 页缘空隙 0.02)
|
||||
export const HOLE_R = 0.03; // 孔半径(世界,略大于环丝,留出穿环空隙)
|
||||
export const HOLE_R = 0.029; // 孔半径 ≈5.5mm 直径(真实活页本孔径)
|
||||
|
||||
const SERIF = '"Noto Serif SC","Songti SC","STSong","SimSun",Georgia,serif';
|
||||
const KAI = '"Kaiti SC","STKaiti","KaiTi","Noto Serif SC",serif';
|
||||
@@ -21,34 +21,34 @@ const CONTENT_L = MARGIN.left + 18; // 内容左界
|
||||
const CONTENT_R = TEX_W - MARGIN.right - 18; // 内容右界
|
||||
const CONTENT_W = CONTENT_R - CONTENT_L; // 内容宽度
|
||||
|
||||
// —— 基础纸张 ——
|
||||
// —— 基础纸张(牛皮纸)——
|
||||
export function makePaper(w, h, { tone = 0, ruled = false, margin = false, seed = 0 } = {}) {
|
||||
const c = document.createElement('canvas');
|
||||
c.width = w; c.height = h;
|
||||
const ctx = c.getContext('2d');
|
||||
|
||||
// 底色(暖黄纸)
|
||||
const base = [232 - tone, 216 - tone, 182 - tone];
|
||||
// 底色(牛皮纸棕黄)
|
||||
const base = [201 - tone, 169 - tone, 126 - tone];
|
||||
const g = ctx.createLinearGradient(0, 0, 0, h);
|
||||
g.addColorStop(0, `rgb(${base[0] + 6},${base[1] + 5},${base[2] + 4})`);
|
||||
g.addColorStop(0, `rgb(${base[0] + 7},${base[1] + 6},${base[2] + 5})`);
|
||||
g.addColorStop(0.5, `rgb(${base[0]},${base[1]},${base[2]})`);
|
||||
g.addColorStop(1, `rgb(${base[0] - 8},${base[1] - 7},${base[2] - 6})`);
|
||||
g.addColorStop(1, `rgb(${base[0] - 9},${base[1] - 8},${base[2] - 7})`);
|
||||
ctx.fillStyle = g;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
// 随机纤维与斑点
|
||||
// 纤维斑点(更粗粝)
|
||||
const rand = mulberry(seed + 1);
|
||||
for (let i = 0; i < w * h / 90; i++) {
|
||||
for (let i = 0; i < w * h / 70; i++) {
|
||||
const x = rand() * w, y = rand() * h;
|
||||
const dark = rand() > 0.5;
|
||||
ctx.fillStyle = dark ? 'rgba(120,95,60,0.10)' : 'rgba(255,250,230,0.14)';
|
||||
ctx.fillRect(x, y, rand() * 2 + 0.4, rand() * 2 + 0.4);
|
||||
ctx.fillStyle = dark ? 'rgba(110,80,48,0.14)' : 'rgba(240,220,185,0.13)';
|
||||
ctx.fillRect(x, y, rand() * 2.4 + 0.5, rand() * 2.4 + 0.5);
|
||||
}
|
||||
// 纤维线条
|
||||
ctx.strokeStyle = 'rgba(140,110,70,0.07)';
|
||||
ctx.lineWidth = 0.7;
|
||||
for (let i = 0; i < w * h / 2600; i++) {
|
||||
const x = rand() * w, y = rand() * h, a = rand() * Math.PI, l = rand() * 22 + 6;
|
||||
// 长纤维线条(牛皮纸特征)
|
||||
ctx.strokeStyle = 'rgba(120,85,50,0.13)';
|
||||
ctx.lineWidth = 0.8;
|
||||
for (let i = 0; i < w * h / 1400; i++) {
|
||||
const x = rand() * w, y = rand() * h, a = rand() * Math.PI, l = rand() * 50 + 12;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
ctx.lineTo(x + Math.cos(a) * l, y + Math.sin(a) * l);
|
||||
@@ -59,7 +59,7 @@ export function makePaper(w, h, { tone = 0, ruled = false, margin = false, seed
|
||||
const x = rand() * w, y = rand() * h, r = rand() * 140 + 40;
|
||||
const gr = ctx.createRadialGradient(x, y, 0, x, y, r);
|
||||
const warm = rand() > 0.5;
|
||||
gr.addColorStop(0, warm ? 'rgba(180,150,95,0.05)' : 'rgba(120,100,70,0.05)');
|
||||
gr.addColorStop(0, warm ? 'rgba(170,135,90,0.06)' : 'rgba(110,80,50,0.06)');
|
||||
gr.addColorStop(1, 'rgba(0,0,0,0)');
|
||||
ctx.fillStyle = gr;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
@@ -103,24 +103,16 @@ function mulberry(seed) {
|
||||
function drawHoles(ctx, w, h) {
|
||||
const hx = (HOLE_X / PAGE_W) * w;
|
||||
const hr = (HOLE_R / PAGE_W) * w;
|
||||
ctx.save();
|
||||
ctx.fillStyle = '#000'; // 确保不透明,destination-out 才能完全抠掉
|
||||
for (const u of HOLE_U) {
|
||||
const y = u * h;
|
||||
// 孔内阴影(穿过去的感觉)
|
||||
const g = ctx.createRadialGradient(hx, y, 0, hx, y, hr);
|
||||
g.addColorStop(0, '#2b1f13');
|
||||
g.addColorStop(0.62, '#4a3826');
|
||||
g.addColorStop(0.82, '#6b5740');
|
||||
g.addColorStop(1, '#8a735a');
|
||||
ctx.fillStyle = g;
|
||||
// 真实打孔:抠出透明孔洞(配合材质 alphaTest 成为真洞)
|
||||
ctx.globalCompositeOperation = 'destination-out';
|
||||
ctx.beginPath(); ctx.arc(hx, y, hr, 0, Math.PI * 2); ctx.fill();
|
||||
// 孔缘
|
||||
ctx.strokeStyle = 'rgba(70,50,32,0.85)';
|
||||
ctx.lineWidth = 2.2;
|
||||
ctx.beginPath(); ctx.arc(hx, y, hr, 0, Math.PI * 2); ctx.stroke();
|
||||
ctx.strokeStyle = 'rgba(255,244,214,0.35)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath(); ctx.arc(hx, y, hr - 2, Math.PI * 0.15, Math.PI * 0.85); ctx.stroke();
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// 描边 + 填充的通用文字
|
||||
|
||||
Reference in New Issue
Block a user