From ac12405bdfae76a6dde1ebf5c4cdc31342afd331 Mon Sep 17 00:00:00 2001 From: fukunee Date: Fri, 18 Sep 2026 19:30:34 +0800 Subject: [PATCH] Make page curl lift across full width Replace localized spine curl with whole-page arc so the free edge rises highest, and float the topic label with the selected card. --- src/scene/binder.js | 18 +++++++----------- src/scene/timeline.js | 14 +++++++++----- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/scene/binder.js b/src/scene/binder.js index e7d8136..bf6f4db 100644 --- a/src/scene/binder.js +++ b/src/scene/binder.js @@ -236,25 +236,21 @@ export class Binder { } _updateCurvature() { - // 翻页时:靠近书脊的一段平滑卷曲,顶边卷曲略强于底边(斜向扭转),静止时完全平整 + // 翻页时整页弧形抬升:自由边最高、书脊侧贴环(如帆鼓风),顶边略强于底边;静止时完全平整 this.pages.forEach((p) => { const pos = p.geometry.attributes.position; const arr = pos.array; const theta = Math.abs(p.angle); // 0..π const c = Math.sin(theta); // 0 静止,1 翻页中途 - const L = 0.16 * c; // 卷曲延伸长度 - const h = 0.15 * c; // 卷曲抬升高度 + const A = 0.42 * c; // 抬升幅度(整页弧) for (let i = 0; i < arr.length; i += 3) { const x = p.base[i]; const y = p.base[i + 1]; - const s = Math.max(0, x - PAGE_GAP); // 距页内缘 0..W(防浮点误差产生负值→NaN) - let z = 0; - if (s < L) { - const t = s / L; - const yNorm = (y + PAGE_H / 2) / PAGE_H; // 0 底边 .. 1 顶边 - const twist = 0.55 + 0.45 * yNorm; - z = h * twist * Math.sin((Math.PI * t) / 2); - } + const s = Math.max(0, x - PAGE_GAP); // 距页内缘 0..W(防浮点误差产生负值→NaN) + const u = Math.min(1, s / PAGE_W); // 0 内缘 .. 1 自由边 + const yNorm = (y + PAGE_H / 2) / PAGE_H; // 0 底边 .. 1 顶边 + const twist = 0.7 + 0.3 * yNorm; // 顶边抬升略强 + const z = A * twist * Math.sin((Math.PI * u) / 2); arr[i + 2] = p.base[i + 2] + z; } pos.needsUpdate = true; diff --git a/src/scene/timeline.js b/src/scene/timeline.js index 46cd527..6586e44 100644 --- a/src/scene/timeline.js +++ b/src/scene/timeline.js @@ -5,6 +5,9 @@ import { makeTextSprite } from '../utils/labels.js'; export const TIMELINE_SPREAD = 6.2; // 横轴范围 ±6.2(超出视口,可横向滚动) +const CARD_FLOAT = 0.14; // 选中时上浮高度 +const NAME_Y = 0.42; // 主题标签基准高度(活页上方) + function cardX(i, n) { return (i / (n - 1)) * 2 * TIMELINE_SPREAD - TIMELINE_SPREAD; } @@ -69,18 +72,18 @@ export class Timeline { 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); + name.position.set(x, NAME_Y, 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 }); + this.cards.push({ card, outline, name, nameBaseY: NAME_Y, index: i }); }); } @@ -89,7 +92,8 @@ export class Timeline { this.cards.forEach((c) => { const on = c.index === index; c.outline.visible = on; - c.card.position.y = on ? 0.14 : 0; + c.card.position.y = on ? CARD_FLOAT : 0; + c.name.position.y = c.nameBaseY + (on ? CARD_FLOAT : 0); }); } }