发布:完整项目代码(三视图 + 词条数据 + 加载页等)
This commit is contained in:
@@ -2,3 +2,4 @@ node_modules/
|
|||||||
dist/
|
dist/
|
||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
__pycache__/
|
||||||
|
|||||||
+10
-12
@@ -30,17 +30,17 @@
|
|||||||
|
|
||||||
<!-- 底部:导航 -->
|
<!-- 底部:导航 -->
|
||||||
<footer class="hud bottom-bar">
|
<footer class="hud bottom-bar">
|
||||||
<button class="nav-btn" id="btn-prev" title="上一页 (←)">◀</button>
|
<button class="nav-btn" id="btn-prev" aria-label="上一页">◀</button>
|
||||||
<div class="page-indicator">
|
<div class="page-indicator">
|
||||||
<span id="page-num">01</span><span class="sep">/</span><span id="page-total">20</span>
|
<span id="page-num">01</span><span class="sep">/</span><span id="page-total">20</span>
|
||||||
<span class="page-name" id="page-name">露西 · 南方古猿</span>
|
<span class="page-name" id="page-name">露西 · 南方古猿</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="nav-btn" id="btn-next" title="下一页 (→)">▶</button>
|
<button class="nav-btn" id="btn-next" aria-label="下一页">▶</button>
|
||||||
<div class="view-switch" aria-label="视图切换">
|
<button class="view-btn" id="btn-view" data-view="binder" aria-label="切换视图">
|
||||||
<button class="view-btn active" data-view="binder">活页本</button>
|
<svg class="v-icon v-binder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="3" width="16" height="18" rx="1.5"/><line x1="8" y1="3" x2="8" y2="21"/><circle cx="11.5" cy="8" r="0.6" fill="currentColor"/><circle cx="11.5" cy="12" r="0.6" fill="currentColor"/><circle cx="11.5" cy="16" r="0.6" fill="currentColor"/></svg>
|
||||||
<button class="view-btn" data-view="timeline">时间轴</button>
|
<svg class="v-icon v-timeline" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><polyline points="12 7 12 12 15.5 14"/></svg>
|
||||||
<button class="view-btn" data-view="globe" disabled title="地理视图加载中">地理</button>
|
<svg class="v-icon v-globe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><ellipse cx="12" cy="12" rx="4" ry="9"/><line x1="3" y1="12" x2="21" y2="12"/></svg>
|
||||||
</div>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<!-- 右侧:登记卡详情 -->
|
<!-- 右侧:登记卡详情 -->
|
||||||
@@ -63,12 +63,10 @@
|
|||||||
<div class="hud hint" id="hint">拖动旋转 · 滚轮缩放 · 时间轴视图拖拽左右滑动 · 点击选中 · ← → 翻页</div>
|
<div class="hud hint" id="hint">拖动旋转 · 滚轮缩放 · 时间轴视图拖拽左右滑动 · 点击选中 · ← → 翻页</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 加载界面 -->
|
<!-- 加载界面:OS1 3D 加载环 -->
|
||||||
<div id="loading" aria-hidden="true">
|
<div id="loading" aria-hidden="true">
|
||||||
<div class="loading-box">
|
<div id="loading-canvas"></div>
|
||||||
<div class="spinner"></div>
|
<div class="loading-text">加载中,约等待六秒…</div>
|
||||||
<div class="loading-text">载入中…</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
import math
|
import math
|
||||||
|
|
||||||
|
from region_codes import make_code
|
||||||
|
|
||||||
DATA = [
|
DATA = [
|
||||||
# ===================== 史前 =====================
|
# ===================== 史前 =====================
|
||||||
("~ BC 3,200,000", -3200000, "东非 · 埃塞俄比亚", "露西 · 南方古猿", "人", "fossil", 11.2, 40.5, "1974年在埃塞俄比亚哈达尔发现的一具约40%完整的南方古猿阿法种骨架,是直立行走的最早证据之一,被亲切地称为“人类祖母”。"),
|
("~ BC 3,200,000", -3200000, "东非 · 埃塞俄比亚", "露西 · 南方古猿", "人", "fossil", 11.2, 40.5, "1974年在埃塞俄比亚哈达尔发现的一具约40%完整的南方古猿阿法种骨架,是直立行走的最早证据之一,被亲切地称为“人类祖母”。"),
|
||||||
@@ -360,6 +362,44 @@ def dedupe(entries, km=DEDUP_KM):
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
DEDUPED = dedupe(DATA)
|
DEDUPED = dedupe(DATA)
|
||||||
|
|
||||||
|
# —— 素描多样分配:同类主题轮换到不同画风,避免同一张素描重复出现 ——
|
||||||
|
SKETCH_POOLS = {
|
||||||
|
'sword': ['sword', 'helmet', 'cannon', 'fortress', 'bow', 'shield'],
|
||||||
|
'crown': ['crown', 'throne', 'scepter', 'palace', 'laurel'],
|
||||||
|
'machine': ['machine', 'gear', 'anvil', 'locomotive', 'factory'],
|
||||||
|
'atom': ['atom', 'microscope', 'telescope', 'flask', 'lightbulb'],
|
||||||
|
'scale': ['scale', 'gavel', 'columns', 'document'],
|
||||||
|
'ship': ['ship', 'anchor', 'compass', 'globe'],
|
||||||
|
'fossil': ['fossil', 'dinosaur', 'mountain'],
|
||||||
|
'tablet': ['tablet', 'coin', 'document', 'hourglass'],
|
||||||
|
'scroll': ['scroll', 'book', 'document', 'lyre'],
|
||||||
|
'temple': ['temple', 'columns', 'palace', 'pyramid'],
|
||||||
|
'colosseum': ['colosseum', 'fortress', 'helmet'],
|
||||||
|
'figure': ['figure', 'mask', 'lyre'],
|
||||||
|
'flag': ['flag', 'laurel', 'mountain', 'dove'],
|
||||||
|
'bell': ['bell', 'gavel', 'document'],
|
||||||
|
'rocket': ['rocket', 'telescope', 'lightbulb', 'compass'],
|
||||||
|
'pyramid': ['pyramid', 'temple', 'palace', 'mountain'],
|
||||||
|
'apple': ['apple', 'flask', 'tree', 'flower'],
|
||||||
|
'book': ['book', 'scroll', 'document', 'key'],
|
||||||
|
'tree': ['tree', 'flower', 'dinosaur'],
|
||||||
|
'dove': ['dove', 'laurel', 'flag'],
|
||||||
|
'globe': ['globe', 'compass', 'anchor', 'ship'],
|
||||||
|
'horseman': ['horseman', 'bow', 'fortress', 'helmet'],
|
||||||
|
}
|
||||||
|
_pool_idx = {}
|
||||||
|
def _reassign(e):
|
||||||
|
pool = SKETCH_POOLS.get(e[5])
|
||||||
|
if not pool:
|
||||||
|
return e
|
||||||
|
k = e[5]
|
||||||
|
idx = _pool_idx.get(k, 0)
|
||||||
|
s = pool[idx % len(pool)]
|
||||||
|
_pool_idx[k] = idx + 1
|
||||||
|
return (e[0], e[1], e[2], e[3], e[4], s, e[6], e[7], e[8])
|
||||||
|
|
||||||
|
DEDUPED = [_reassign(e) for e in DEDUPED]
|
||||||
print(f"坐标去重:{len(DATA)} -> {len(DEDUPED)} 条(阈值 {DEDUP_KM} km)")
|
print(f"坐标去重:{len(DATA)} -> {len(DEDUPED)} 条(阈值 {DEDUP_KM} km)")
|
||||||
|
|
||||||
def esc(s):
|
def esc(s):
|
||||||
@@ -397,6 +437,7 @@ lines = ["// 世界历史里程碑条目数据",
|
|||||||
for i, (time, tn, region, name, cat, sketch, lat, lng, desc) in enumerate(DEDUPED, 1):
|
for i, (time, tn, region, name, cat, sketch, lat, lng, desc) in enumerate(DEDUPED, 1):
|
||||||
lines.append(" {")
|
lines.append(" {")
|
||||||
lines.append(f" id: 'No.{i:03d}',")
|
lines.append(f" id: 'No.{i:03d}',")
|
||||||
|
lines.append(f" code: '{esc(make_code(tn, region, lat, lng, i, name, cat))}',")
|
||||||
lines.append(f" time: '{esc(normalize_time(time, tn))}',")
|
lines.append(f" time: '{esc(normalize_time(time, tn))}',")
|
||||||
lines.append(f" timeNumeric: {tn},")
|
lines.append(f" timeNumeric: {tn},")
|
||||||
lines.append(f" region: '{esc(region)}',")
|
lines.append(f" region: '{esc(region)}',")
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
import random
|
import random
|
||||||
import math
|
import math
|
||||||
|
|
||||||
|
from region_codes import make_code
|
||||||
|
|
||||||
N = 1000
|
N = 1000
|
||||||
random.seed(20240101)
|
random.seed(20240101)
|
||||||
|
|
||||||
@@ -90,6 +92,7 @@ lines = ["// 压力测试用合成词条数据(1000 条)",
|
|||||||
for i, (time, tn, region, name, cat, sketch, lat, lng, desc) in enumerate(entries, 1):
|
for i, (time, tn, region, name, cat, sketch, lat, lng, desc) in enumerate(entries, 1):
|
||||||
lines.append(" {")
|
lines.append(" {")
|
||||||
lines.append(f" id: 'No.{i:03d}',")
|
lines.append(f" id: 'No.{i:03d}',")
|
||||||
|
lines.append(f" code: '{esc(make_code(tn, region, lat, lng, i, name, cat))}',")
|
||||||
lines.append(f" time: '{esc(time)}',")
|
lines.append(f" time: '{esc(time)}',")
|
||||||
lines.append(f" timeNumeric: {tn},")
|
lines.append(f" timeNumeric: {tn},")
|
||||||
lines.append(f" region: '{esc(region)}',")
|
lines.append(f" region: '{esc(region)}',")
|
||||||
|
|||||||
@@ -0,0 +1,316 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""地区编码:把 region 字符串映射成「大洲码 + 国家/地区码 + 方位码」,
|
||||||
|
用于生成编目号(如 AFETNE = 非洲 / 埃塞俄比亚 / 东北部)。
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
|
||||||
|
# region -> (大洲码, 国家/地区码, 现代国名[用于查标注点], 固定方位或 None)
|
||||||
|
# 大洲码:AF 非洲 AS 亚洲 EU 欧洲 NA 北美 SA 南美 OC 大洋洲 AN 南极 GL 全球/海域
|
||||||
|
REGION_CC = {
|
||||||
|
# —— 非洲 ——
|
||||||
|
"东非 · 肯尼亚": ("AF", "KE", "肯尼亚", None),
|
||||||
|
"东非 · 埃塞俄比亚": ("AF", "ET", "埃塞俄比亚", None),
|
||||||
|
"东非 · 坦桑尼亚": ("AF", "TZ", "坦桑尼亚", None),
|
||||||
|
"北非 · 摩洛哥": ("AF", "MA", "摩洛哥", None),
|
||||||
|
"古埃及 · 孟菲斯": ("AF", "EG", "埃及", None),
|
||||||
|
"古埃及 · 阿拜多斯": ("AF", "EG", "埃及", None),
|
||||||
|
"古埃及 · 吉萨": ("AF", "EG", "埃及", None),
|
||||||
|
"古埃及 · 阿玛纳": ("AF", "EG", "埃及", None),
|
||||||
|
"古埃及 · 亚历山大港": ("AF", "EG", "埃及", None),
|
||||||
|
"北非 · 迦太基": ("AF", "TN", "突尼斯", None),
|
||||||
|
"葡萄牙 · 休达": ("AF", "MA", "摩洛哥", None),
|
||||||
|
"埃及 · 苏伊士": ("AF", "EG", "埃及", None),
|
||||||
|
"南非 · 开普敦": ("AF", "ZA", "南非", None),
|
||||||
|
"南非 · 比勒陀利亚": ("AF", "ZA", "南非", None),
|
||||||
|
# —— 亚洲 ——
|
||||||
|
"东亚 · 中国周口店": ("AS", "CN", "中国", None),
|
||||||
|
"西亚 · 以色列": ("AS", "IL", "以色列", None),
|
||||||
|
"西亚 · 伊拉克": ("AS", "IQ", "伊拉克", None),
|
||||||
|
"日本 · 本州": ("AS", "JP", "日本", None),
|
||||||
|
"西亚 · 两河流域": ("AS", "IQ", "伊拉克", None),
|
||||||
|
"南亚 · 印度河谷": ("AS", "PK", "巴基斯坦", None),
|
||||||
|
"美索不达米亚 · 两河流域": ("AS", "IQ", "伊拉克", None),
|
||||||
|
"美索不达米亚 · 乌尔": ("AS", "IQ", "伊拉克", None),
|
||||||
|
"美索不达米亚 · 巴比伦": ("AS", "IQ", "伊拉克", None),
|
||||||
|
"中国 · 河南安阳": ("AS", "CN", "中国", None),
|
||||||
|
"古印度 · 恒河流域": ("AS", "IN", "印度", None),
|
||||||
|
"西亚 · 安纳托利亚": ("AS", "TR", "土耳其", None),
|
||||||
|
"地中海 · 东岸": ("AS", "LB", "黎巴嫩", None),
|
||||||
|
"中国 · 咸阳": ("AS", "CN", "中国", None),
|
||||||
|
"古印度 · 蓝毗尼": ("AS", "NP", "尼泊尔", None),
|
||||||
|
"中国 · 鲁国曲阜": ("AS", "CN", "中国", None),
|
||||||
|
"古印度 · 华氏城": ("AS", "IN", "印度", None),
|
||||||
|
"中国 · 北方边塞": ("AS", "CN", "中国", None),
|
||||||
|
"中国 · 陕西西安": ("AS", "CN", "中国", None),
|
||||||
|
"中国 · 陕西汉中": ("AS", "CN", "中国", None),
|
||||||
|
"中国 · 四川": ("AS", "CN", "中国", None),
|
||||||
|
"犹太 · 伯利恒": ("AS", "PS", "巴勒斯坦", None),
|
||||||
|
"中国 · 东汉洛阳": ("AS", "CN", "中国", None),
|
||||||
|
"拜占庭 · 君士坦丁堡": ("AS", "TR", "土耳其", None),
|
||||||
|
"阿拉伯半岛 · 麦加": ("AS", "SA", "沙特阿拉伯", None),
|
||||||
|
"中国 · 建康": ("AS", "CN", "中国", None),
|
||||||
|
"阿拉伯半岛 · 麦地那": ("AS", "SA", "沙特阿拉伯", None),
|
||||||
|
"波斯 · 萨珊": ("AS", "IR", "伊朗", None),
|
||||||
|
"朝鲜半岛 · 百济": ("AS", "KR", "韩国", None),
|
||||||
|
"中亚 · 怛罗斯": ("AS", "KZ", "哈萨克斯坦", None),
|
||||||
|
"日本 · 京都": ("AS", "JP", "日本", None),
|
||||||
|
"中国 · 敦煌": ("AS", "CN", "中国", None),
|
||||||
|
"中国 · 开封": ("AS", "CN", "中国", None),
|
||||||
|
"中国 · 北宋": ("AS", "CN", "中国", None),
|
||||||
|
"柬埔寨 · 吴哥": ("AS", "KH", "柬埔寨", None),
|
||||||
|
"蒙古高原": ("AS", "MN", "蒙古", None),
|
||||||
|
"中国 · 大都": ("AS", "CN", "中国", None),
|
||||||
|
"印度 · 卡利卡特": ("AS", "IN", "印度", None),
|
||||||
|
"印度 · 帕尼帕特": ("AS", "IN", "印度", None),
|
||||||
|
"日本 · 江户": ("AS", "JP", "日本", None),
|
||||||
|
"中国 · 广东虎门": ("AS", "CN", "中国", None),
|
||||||
|
"中国 · 武昌": ("AS", "CN", "中国", None),
|
||||||
|
"中国 · 沈阳": ("AS", "CN", "中国", None),
|
||||||
|
"中国 · 江西": ("AS", "CN", "中国", None),
|
||||||
|
"日本 · 广岛": ("AS", "JP", "日本", None),
|
||||||
|
"印度 · 新德里": ("AS", "IN", "印度", None),
|
||||||
|
"以色列 · 特拉维夫": ("AS", "IL", "以色列", None),
|
||||||
|
"朝鲜半岛": ("AS", "KP", "朝鲜", None),
|
||||||
|
"尼泊尔 · 珠峰": ("AS", "NP", "尼泊尔", None),
|
||||||
|
"印度尼西亚 · 万隆": ("AS", "ID", "印度尼西亚", None),
|
||||||
|
"苏联 · 拜科努尔": ("AS", "KZ", "哈萨克斯坦", None),
|
||||||
|
"中国 · 罗布泊": ("AS", "CN", "中国", None),
|
||||||
|
"美国 · 越南": ("AS", "VN", "越南", None),
|
||||||
|
"中东 · 埃及叙利亚": ("AS", "EG", "埃及", None),
|
||||||
|
"伊朗 · 德黑兰": ("AS", "IR", "伊朗", None),
|
||||||
|
"中国 · 香港": ("AS", "CN", "中国", None),
|
||||||
|
"中国 · 澳门": ("AS", "CN", "中国", None),
|
||||||
|
"伊拉克 · 巴格达": ("AS", "IQ", "伊拉克", None),
|
||||||
|
"中国 · 酒泉": ("AS", "CN", "中国", None),
|
||||||
|
"中国 · 文昌": ("AS", "CN", "中国", None),
|
||||||
|
# —— 欧洲 ——
|
||||||
|
"欧洲 · 德国": ("EU", "DE", "德国", None),
|
||||||
|
"欧洲 · 奥地利": ("EU", "AT", "奥地利", None),
|
||||||
|
"欧洲 · 法国": ("EU", "FR", "法国", None),
|
||||||
|
"不列颠 · 威尔特郡": ("EU", "GB", "英国", None),
|
||||||
|
"爱琴海 · 克里特岛": ("EU", "GR", "希腊", None),
|
||||||
|
"古希腊 · 奥林匹亚": ("EU", "GR", "希腊", None),
|
||||||
|
"罗马帝国 · 罗马": ("EU", "IT", "意大利", None),
|
||||||
|
"古希腊 · 爱奥尼亚": ("EU", "GR", "希腊", None),
|
||||||
|
"古希腊 · 雅典": ("EU", "GR", "希腊", None),
|
||||||
|
"古希腊 · 马拉松": ("EU", "GR", "希腊", None),
|
||||||
|
"古希腊 · 温泉关": ("EU", "GR", "希腊", None),
|
||||||
|
"古希腊 · 斯塔基拉": ("EU", "GR", "希腊", None),
|
||||||
|
"马其顿 · 希腊半岛": ("EU", "GR", "希腊", None),
|
||||||
|
"古希腊 · 叙拉古": ("EU", "IT", "意大利", None),
|
||||||
|
"古罗马 · 卡普阿": ("EU", "IT", "意大利", None),
|
||||||
|
"古罗马 · 庞贝": ("EU", "IT", "意大利", None),
|
||||||
|
"古罗马 · 米兰": ("EU", "IT", "意大利", None),
|
||||||
|
"罗马帝国 · 塞萨洛尼基": ("EU", "GR", "希腊", None),
|
||||||
|
"西罗马 · 拉文纳": ("EU", "IT", "意大利", None),
|
||||||
|
"伊比利亚 · 直布罗陀": ("EU", "GB", "英国", None),
|
||||||
|
"法兰克 · 图尔": ("EU", "FR", "法国", None),
|
||||||
|
"法兰克 · 亚琛": ("EU", "DE", "德国", None),
|
||||||
|
"法兰克 · 凡尔登": ("EU", "FR", "法国", None),
|
||||||
|
"英格兰 · 黑斯廷斯": ("EU", "GB", "英国", None),
|
||||||
|
"意大利 · 博洛尼亚": ("EU", "IT", "意大利", None),
|
||||||
|
"西欧 · 克莱蒙": ("EU", "FR", "法国", None),
|
||||||
|
"法国 · 巴黎": ("EU", "FR", "法国", None),
|
||||||
|
"英格兰 · 兰尼米德": ("EU", "GB", "英国", None),
|
||||||
|
"意大利 · 佛罗伦萨": ("EU", "IT", "意大利", None),
|
||||||
|
"欧洲 · 英法": ("EU", "FR", "法国", None),
|
||||||
|
"欧洲 · 意大利": ("EU", "IT", "意大利", None),
|
||||||
|
"法国 · 鲁昂": ("EU", "FR", "法国", None),
|
||||||
|
"德意志 · 美因茨": ("EU", "DE", "德国", None),
|
||||||
|
"西班牙 · 格拉纳达": ("EU", "ES", "西班牙", None),
|
||||||
|
"德意志 · 维滕贝格": ("EU", "DE", "德国", None),
|
||||||
|
"波兰 · 弗龙堡": ("EU", "PL", "波兰", None),
|
||||||
|
"德意志 · 奥格斯堡": ("EU", "DE", "德国", None),
|
||||||
|
"英吉利海峡": ("EU", "GB", None, "CH"),
|
||||||
|
"英国 · 伦敦": ("EU", "GB", "英国", None),
|
||||||
|
"西班牙 · 马德里": ("EU", "ES", "西班牙", None),
|
||||||
|
"意大利 · 帕多瓦": ("EU", "IT", "意大利", None),
|
||||||
|
"欧洲 · 威斯特伐利亚": ("EU", "DE", "德国", None),
|
||||||
|
"英国 · 剑桥": ("EU", "GB", "英国", None),
|
||||||
|
"法国 · 凡尔赛": ("EU", "FR", "法国", None),
|
||||||
|
"瑞典 · 乌普萨拉": ("EU", "SE", "瑞典", None),
|
||||||
|
"欧洲 · 英格兰": ("EU", "GB", "英国", None),
|
||||||
|
"英国 · 格拉斯哥": ("EU", "GB", "英国", None),
|
||||||
|
"英格兰 · 伯克利": ("EU", "GB", "英国", None),
|
||||||
|
"俄国 · 莫斯科": ("EU", "RU", "俄罗斯", None),
|
||||||
|
"比利时 · 滑铁卢": ("EU", "BE", "比利时", None),
|
||||||
|
"英格兰 · 斯托克顿": ("EU", "GB", "英国", None),
|
||||||
|
"克里米亚 · 塞瓦斯托波尔": ("EU", "UA", "乌克兰", None),
|
||||||
|
"意大利 · 都灵": ("EU", "IT", "意大利", None),
|
||||||
|
"瑞典 · 斯德哥尔摩": ("EU", "SE", "瑞典", None),
|
||||||
|
"俄国 · 圣彼得堡": ("EU", "RU", "俄罗斯", None),
|
||||||
|
"德国 · 柏林": ("EU", "DE", "德国", None),
|
||||||
|
"德国 · 曼海姆": ("EU", "DE", "德国", None),
|
||||||
|
"德国 · 维尔茨堡": ("EU", "DE", "德国", None),
|
||||||
|
"瑞士 · 伯尔尼": ("EU", "CH", "瑞士", None),
|
||||||
|
"欧洲 · 萨拉热窝": ("EU", "BA", "波斯尼亚", None),
|
||||||
|
"欧洲 · 康边": ("EU", "FR", "法国", None),
|
||||||
|
"瑞士 · 日内瓦": ("EU", "CH", "瑞士", None),
|
||||||
|
"欧洲 · 波兰": ("EU", "PL", "波兰", None),
|
||||||
|
"苏联 · 斯大林格勒": ("EU", "RU", "俄罗斯", None),
|
||||||
|
"法国 · 诺曼底": ("EU", "FR", "法国", None),
|
||||||
|
"欧洲 · 布鲁塞尔": ("EU", "BE", "比利时", None),
|
||||||
|
"苏联 · 切尔诺贝利": ("EU", "UA", "乌克兰", None),
|
||||||
|
"欧洲 · 马斯特里赫特": ("EU", "NL", "荷兰", None),
|
||||||
|
"苏格兰 · 爱丁堡": ("EU", "GB", "英国", None),
|
||||||
|
"欧洲 · 法兰克福": ("EU", "DE", "德国", None),
|
||||||
|
# —— 北美 ——
|
||||||
|
"中南美 · 墨西哥湾": ("NA", "MX", "墨西哥", None),
|
||||||
|
"北美 · 普利茅斯": ("NA", "US", "美国", None),
|
||||||
|
"北美 · 费城": ("NA", "US", "美国", None),
|
||||||
|
"世界 · 纽约": ("NA", "US", "美国", None),
|
||||||
|
"海地 · 太子港": ("NA", "HT", "海地", None),
|
||||||
|
"美国 · 萨姆特堡": ("NA", "US", "美国", None),
|
||||||
|
"美国 · 葛底斯堡": ("NA", "US", "美国", None),
|
||||||
|
"美国 · 华盛顿": ("NA", "US", "美国", None),
|
||||||
|
"美国 · 波士顿": ("NA", "US", "美国", None),
|
||||||
|
"美国 · 门洛帕克": ("NA", "US", "美国", None),
|
||||||
|
"美国 · 基蒂霍克": ("NA", "US", "美国", None),
|
||||||
|
"美国 · 纽约": ("NA", "US", "美国", None),
|
||||||
|
"美国 · 珍珠港": ("NA", "US", "美国", None),
|
||||||
|
"美国 · 芝加哥": ("NA", "US", "美国", None),
|
||||||
|
"美国 · 新墨西哥": ("NA", "US", "美国", None),
|
||||||
|
"古巴 · 哈瓦那": ("NA", "CU", "古巴", None),
|
||||||
|
"古巴 · 加勒比": ("NA", "CU", "古巴", None),
|
||||||
|
"美国 · 洛杉矶": ("NA", "US", "美国", None),
|
||||||
|
"美国 · 西雅图": ("NA", "US", "美国", None),
|
||||||
|
"美国 · 库比蒂诺": ("NA", "US", "美国", None),
|
||||||
|
"美国 · 加利福尼亚": ("NA", "US", "美国", None),
|
||||||
|
"美国 · 旧金山": ("NA", "US", "美国", None),
|
||||||
|
"美国 · 卡纳维拉尔角": ("NA", "US", "美国", None),
|
||||||
|
# —— 南美 ——
|
||||||
|
"南美 · 秘鲁": ("SA", "PE", "秘鲁", None),
|
||||||
|
"拉丁美洲 · 委内瑞拉": ("SA", "VE", "委内瑞拉", None),
|
||||||
|
# —— 大洋洲 ——
|
||||||
|
"澳大利亚 · 植物学湾": ("OC", "AU", "澳大利亚", None),
|
||||||
|
# —— 跨区域 / 海域 / 极地 ——
|
||||||
|
"北欧 · 格陵兰": ("NA", "GL", "格陵兰", None),
|
||||||
|
"西班牙 → 美洲": ("NA", "ES", None, "AM"),
|
||||||
|
"大西洋 · 托德西利亚斯": ("GL", "PT", None, "TR"),
|
||||||
|
"欧洲 → 环球": ("GL", "GL", None, "GB"),
|
||||||
|
"大西洋": ("GL", "AT", None, "OC"),
|
||||||
|
"挪威 · 南极": ("AN", "AQ", None, "SP"),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 从 Natural Earth 国家数据读取标注点(按 ISO_A2 编码,用于计算条目在国家内的方位)
|
||||||
|
COUNTRY_LABEL = {}
|
||||||
|
try:
|
||||||
|
with open("public/world-countries.geojson", encoding="utf-8") as _f:
|
||||||
|
_gj = json.load(_f)
|
||||||
|
for _ft in _gj.get("features", []):
|
||||||
|
_p = _ft.get("properties", {})
|
||||||
|
_iso = _p.get("ISO_A2")
|
||||||
|
if _iso and _iso != "-99" and _p.get("LABEL_X") is not None and _p.get("LABEL_Y") is not None:
|
||||||
|
COUNTRY_LABEL[_iso.upper()] = (_p["LABEL_X"], _p["LABEL_Y"])
|
||||||
|
except Exception as _e:
|
||||||
|
print(f"警告:读取国家标注点失败,方位将回退为 XX:{_e}")
|
||||||
|
|
||||||
|
|
||||||
|
def _dir(lat, lng, cx, cy):
|
||||||
|
dlat = lat - cy
|
||||||
|
dlng = lng - cx
|
||||||
|
ns = "N" if dlat > 0.5 else ("S" if dlat < -0.5 else "")
|
||||||
|
ew = "E" if dlng > 0.5 else ("W" if dlng < -0.5 else "")
|
||||||
|
if not ns and not ew:
|
||||||
|
return "CE"
|
||||||
|
if not ns:
|
||||||
|
return "EA" if ew == "E" else "WE"
|
||||||
|
if not ew:
|
||||||
|
return "NO" if ns == "N" else "SO"
|
||||||
|
return ns + ew
|
||||||
|
|
||||||
|
|
||||||
|
# 条目类型枚举:E 事件/战役 P 人物/政党 T 学说/技术 I 文物/艺术品 B 建筑/奇观
|
||||||
|
TYPE_OVERRIDE = {
|
||||||
|
# 化石标本(人 → 文物)
|
||||||
|
"露西 · 南方古猿": "I",
|
||||||
|
"杰贝尔依罗智人化石": "I",
|
||||||
|
# 建筑/奇观(物 → B)
|
||||||
|
"哥贝克力石阵": "B",
|
||||||
|
"加泰土丘聚落": "B",
|
||||||
|
"胡夫金字塔": "B",
|
||||||
|
"巨石阵": "B",
|
||||||
|
"乌尔金字形神塔": "B",
|
||||||
|
"米诺斯宫殿": "B",
|
||||||
|
"罗马斗兽场": "B",
|
||||||
|
"雅典卫城 · 帕特农神庙": "B",
|
||||||
|
"万里长城": "B",
|
||||||
|
"吴哥窟": "B",
|
||||||
|
"巴黎圣母院": "B",
|
||||||
|
"亚历山大图书馆": "B",
|
||||||
|
"博洛尼亚大学": "B",
|
||||||
|
"苏伊士运河通航": "B",
|
||||||
|
# 学说/技术(物 → T)
|
||||||
|
"赫梯铁器": "T",
|
||||||
|
"造纸术萌芽": "T",
|
||||||
|
"毕昇活字印刷": "T",
|
||||||
|
"古腾堡印刷术": "T",
|
||||||
|
"伽利略天文望远镜": "T",
|
||||||
|
"哥白尼日心说": "T",
|
||||||
|
"林奈分类法": "T",
|
||||||
|
"门捷列夫元素周期表": "T",
|
||||||
|
"瓦特改良蒸汽机": "T",
|
||||||
|
"第一条铁路": "T",
|
||||||
|
"诺贝尔炸药": "T",
|
||||||
|
"电话发明": "T",
|
||||||
|
"爱迪生白炽灯": "T",
|
||||||
|
"本茨汽车": "T",
|
||||||
|
"莱特兄弟首飞": "T",
|
||||||
|
"福特T型车": "T",
|
||||||
|
"晶体管发明": "T",
|
||||||
|
"斯普特尼克1号": "T",
|
||||||
|
"iPhone发布": "T",
|
||||||
|
# 学说/技术(事 → T)
|
||||||
|
"蔡伦造纸": "T",
|
||||||
|
"达尔文《物种起源》": "T",
|
||||||
|
"牛顿《自然哲学的数学原理》": "T",
|
||||||
|
"爱因斯坦狭义相对论": "T",
|
||||||
|
"互联网诞生": "T",
|
||||||
|
"科赫发现结核杆菌": "T",
|
||||||
|
"伦琴发现X射线": "T",
|
||||||
|
"琴纳牛痘接种": "T",
|
||||||
|
"首例心脏移植": "T",
|
||||||
|
"克隆羊多利": "T",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def classify(name, category):
|
||||||
|
if name in TYPE_OVERRIDE:
|
||||||
|
return TYPE_OVERRIDE[name]
|
||||||
|
if category == "人":
|
||||||
|
return "P"
|
||||||
|
if category == "事":
|
||||||
|
return "E"
|
||||||
|
return "I" # 物(默认文物/艺术品)
|
||||||
|
|
||||||
|
|
||||||
|
def _time_seg(tn):
|
||||||
|
age = abs(tn)
|
||||||
|
if age < 10000:
|
||||||
|
if tn < 0:
|
||||||
|
return "BC" + f"{math.ceil(age / 100):02d}"
|
||||||
|
return "AD" + f"{(tn - 1) // 100 + 1:02d}"
|
||||||
|
if age < 100000:
|
||||||
|
return "BCE1"
|
||||||
|
if age < 1000000:
|
||||||
|
return "BCE2"
|
||||||
|
if age < 10000000:
|
||||||
|
return "BCE3"
|
||||||
|
return "BCE4"
|
||||||
|
|
||||||
|
|
||||||
|
def make_code(tn, region, lat, lng, idx, name, category):
|
||||||
|
"""生成编目号,如 E3-AFETNE-I0001。"""
|
||||||
|
t = _time_seg(tn)
|
||||||
|
# 地区段:大洲 + 国家/地区 + 方位
|
||||||
|
cont, iso, zh, fixed = REGION_CC.get(region, ("GL", "XX", None, "XX"))
|
||||||
|
if fixed:
|
||||||
|
sub = fixed
|
||||||
|
elif iso.upper() in COUNTRY_LABEL:
|
||||||
|
cx, cy = COUNTRY_LABEL[iso.upper()]
|
||||||
|
sub = _dir(lat, lng, cx, cy)
|
||||||
|
else:
|
||||||
|
sub = "XX"
|
||||||
|
prefix = classify(name, category)
|
||||||
|
return f"{t}-{cont}{iso}{sub}-{prefix}{idx:04d}"
|
||||||
+27
-49
@@ -3,69 +3,47 @@ import puppeteer from 'puppeteer-core';
|
|||||||
const CHROME = 'C:/Program Files/Google/Chrome/Application/chrome.exe';
|
const CHROME = 'C:/Program Files/Google/Chrome/Application/chrome.exe';
|
||||||
const URL = process.argv[2] || 'http://localhost:5199/';
|
const URL = process.argv[2] || 'http://localhost:5199/';
|
||||||
|
|
||||||
|
// 轻量冒烟测试:不做完整渲染流程 / 不触发 Cesium,
|
||||||
|
// 只检查「加载无 JS 报错 + loading 环出现 + 6 秒后淡出 + App 就绪」。
|
||||||
|
// 用真实 GPU(不强制 swiftshader 软件渲染)。
|
||||||
const browser = await puppeteer.launch({
|
const browser = await puppeteer.launch({
|
||||||
executablePath: CHROME,
|
executablePath: CHROME,
|
||||||
headless: 'new',
|
headless: 'new',
|
||||||
args: ['--no-sandbox', '--disable-gpu', '--use-gl=swiftshader', '--enable-unsafe-swiftshader', '--window-size=1440,900'],
|
args: ['--no-sandbox', '--window-size=1440,900'],
|
||||||
});
|
});
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
await page.setViewport({ width: 1440, height: 900 });
|
await page.setViewport({ width: 1440, height: 900 });
|
||||||
|
|
||||||
const errors = [];
|
const errors = [];
|
||||||
page.on('pageerror', (e) => errors.push('pageerror: ' + e.message));
|
page.on('pageerror', (e) => errors.push('pageerror: ' + e.message));
|
||||||
page.on('console', (m) => { if (m.type() === 'error') errors.push('console: ' + m.text()); });
|
page.on('console', (m) => {
|
||||||
|
// preview 模式无 HMR,WebSocket 连接失败是正常现象,过滤掉
|
||||||
await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
if (m.type() === 'error' && !/WebSocket|ERR_CONNECTION/i.test(m.text())) {
|
||||||
await new Promise((r) => setTimeout(r, 6000));
|
errors.push('console: ' + m.text());
|
||||||
|
|
||||||
async function snapshot(label) {
|
|
||||||
const info = await page.evaluate(() => {
|
|
||||||
const app = window.__app;
|
|
||||||
const gl = app.renderer.getContext();
|
|
||||||
const w = gl.drawingBufferWidth, h = gl.drawingBufferHeight;
|
|
||||||
const buf = new Uint8Array(w * h * 4);
|
|
||||||
gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, buf);
|
|
||||||
let sum = 0, n = 0, bright = 0;
|
|
||||||
for (let i = 0; i < buf.length; i += 4) {
|
|
||||||
const lum = 0.299 * buf[i] + 0.587 * buf[i + 1] + 0.114 * buf[i + 2];
|
|
||||||
sum += lum; n++;
|
|
||||||
if (lum > 40) bright++;
|
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
view: app.view,
|
|
||||||
selected: app.selected,
|
|
||||||
binderCurrent: app.binder.current,
|
|
||||||
pages: app.binder.pages.length,
|
|
||||||
timelineCards: app.timeline.cards.length,
|
|
||||||
globeMarkers: app.globe.markers.length,
|
|
||||||
detailName: document.getElementById('detail-name')?.textContent,
|
|
||||||
detailNo: document.getElementById('detail-no')?.textContent,
|
|
||||||
avgLum: +(sum / n).toFixed(2),
|
|
||||||
brightPct: +((bright / n) * 100).toFixed(2),
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
console.log(label, JSON.stringify(info, null, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
await snapshot('binder ');
|
await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
||||||
|
|
||||||
await page.click('.view-btn[data-view="timeline"]');
|
// 阶段一:加载环应已出现且正在展示
|
||||||
await new Promise((r) => setTimeout(r, 2400));
|
await new Promise((r) => setTimeout(r, 2000));
|
||||||
await snapshot('timeline');
|
const loading = await page.evaluate(() => ({
|
||||||
|
hasCanvas: !!document.querySelector('#loading-canvas canvas'),
|
||||||
|
loadingShown: !document.getElementById('loading').classList.contains('hidden'),
|
||||||
|
appReady: !!window.__app,
|
||||||
|
}));
|
||||||
|
|
||||||
await page.click('.view-btn[data-view="globe"]');
|
// 阶段二:约 9 秒后,loading 应已淡出、App 应已就绪
|
||||||
await new Promise((r) => setTimeout(r, 2400));
|
await new Promise((r) => setTimeout(r, 7000));
|
||||||
await snapshot('globe ');
|
const done = await page.evaluate(() => ({
|
||||||
|
loadingHidden: document.getElementById('loading').classList.contains('hidden'),
|
||||||
await page.click('.view-btn[data-view="binder"]');
|
appReady: !!window.__app,
|
||||||
await new Promise((r) => setTimeout(r, 2400));
|
view: window.__app?.view,
|
||||||
await page.click('#btn-next');
|
pages: window.__app?.binder?.pages?.length,
|
||||||
await new Promise((r) => setTimeout(r, 1500));
|
timelineCards: window.__app?.timeline?.cards?.length,
|
||||||
await snapshot('flipped ');
|
}));
|
||||||
|
|
||||||
await page.click('#btn-prev');
|
|
||||||
await new Promise((r) => setTimeout(r, 1500));
|
|
||||||
await snapshot('back ');
|
|
||||||
|
|
||||||
|
console.log('阶段一(加载中):', JSON.stringify(loading));
|
||||||
|
console.log('阶段二(加载后):', JSON.stringify(done));
|
||||||
console.log('ERRORS:', errors.length ? errors.join('\n') : '(none)');
|
console.log('ERRORS:', errors.length ? errors.join('\n') : '(none)');
|
||||||
await browser.close();
|
await browser.close();
|
||||||
|
|||||||
+46
-25
@@ -31,8 +31,8 @@ export class App {
|
|||||||
|
|
||||||
this.overlay = new Overlay({
|
this.overlay = new Overlay({
|
||||||
onView: (v) => this.switchView(v),
|
onView: (v) => this.switchView(v),
|
||||||
onPrev: () => this._step(-1),
|
onPrev: () => this._stepByView(-1),
|
||||||
onNext: () => this._step(1),
|
onNext: () => this._stepByView(1),
|
||||||
onFlip: () => this._step(1),
|
onFlip: () => this._step(1),
|
||||||
});
|
});
|
||||||
this.overlay.setTotal(ENTRIES.length);
|
this.overlay.setTotal(ENTRIES.length);
|
||||||
@@ -59,17 +59,16 @@ export class App {
|
|||||||
requestAnimationFrame(() => requestAnimationFrame(() => this.timeline.initLabels()));
|
requestAnimationFrame(() => requestAnimationFrame(() => this.timeline.initLabels()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 首屏加载完成后由 main.js 调用:后台预载地理视图,就绪后激活“地理”按钮
|
// 首屏加载完成后由 main.js 调用:后台预载地理视图,就绪后允许循环切换到地理
|
||||||
preloadGlobe() {
|
preloadGlobe() {
|
||||||
const btn = document.querySelector('.view-btn[data-view="globe"]');
|
this.globe.viewerReady.then(() => this.overlay.setGlobeReady(true));
|
||||||
this.globe.viewerReady.then(() => { if (btn) btn.disabled = false; });
|
|
||||||
this.globe.ensureCreated();
|
this.globe.ensureCreated();
|
||||||
}
|
}
|
||||||
|
|
||||||
_initRenderer() {
|
_initRenderer() {
|
||||||
const canvas = document.getElementById('scene');
|
const canvas = document.getElementById('scene');
|
||||||
this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
||||||
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
|
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // 恢复高分辨率渲染,避免糊
|
||||||
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
this.renderer.shadowMap.enabled = true;
|
this.renderer.shadowMap.enabled = true;
|
||||||
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||||
@@ -121,16 +120,19 @@ export class App {
|
|||||||
const rim = new THREE.DirectionalLight(0x8fa0d0, 0.7);
|
const rim = new THREE.DirectionalLight(0x8fa0d0, 0.7);
|
||||||
rim.position.set(-2.5, 1.2, -3.5);
|
rim.position.set(-2.5, 1.2, -3.5);
|
||||||
this.scene.add(rim);
|
this.scene.add(rim);
|
||||||
|
this.rimLight = rim;
|
||||||
|
|
||||||
// 低角度掠射光:横扫皮面,凸显皮革纹理凹凸
|
// 低角度掠射光:横扫皮面,凸显皮革纹理凹凸
|
||||||
const graze = new THREE.DirectionalLight(0xffd9a8, 1.1);
|
const graze = new THREE.DirectionalLight(0xffd9a8, 1.1);
|
||||||
graze.position.set(-4.5, 0.7, 2.6);
|
graze.position.set(-4.5, 0.7, 2.6);
|
||||||
this.scene.add(graze);
|
this.scene.add(graze);
|
||||||
|
this.grazeLight = graze;
|
||||||
|
|
||||||
// 背面补光:让外侧牛皮从背面看也可见
|
// 背面补光:让外侧牛皮从背面看也可见
|
||||||
const backLight = new THREE.DirectionalLight(0xffd9a8, 0.55);
|
const backLight = new THREE.DirectionalLight(0xffd9a8, 0.55);
|
||||||
backLight.position.set(0.5, 1.2, -3.5);
|
backLight.position.set(0.5, 1.2, -3.5);
|
||||||
this.scene.add(backLight);
|
this.scene.add(backLight);
|
||||||
|
this.backLight = backLight;
|
||||||
}
|
}
|
||||||
|
|
||||||
_initScene() {
|
_initScene() {
|
||||||
@@ -212,10 +214,11 @@ export class App {
|
|||||||
// 时间轴:按下开始拖拽左右滑动;未移动的抬起才视为点击选中
|
// 时间轴:按下开始拖拽左右滑动;未移动的抬起才视为点击选中
|
||||||
this.timelineSlide = null; // 手动拖拽时取消居中动画
|
this.timelineSlide = null; // 手动拖拽时取消居中动画
|
||||||
this.timeline.setPausedGeneration(true); // 拖拽期间暂停纹理生成,保持流畅
|
this.timeline.setPausedGeneration(true); // 拖拽期间暂停纹理生成,保持流畅
|
||||||
this._drag = { active: true, startX: e.clientX, startY: e.clientY, moved: false, offset: this.timelineOffset };
|
this._drag = { active: true, view: 'timeline', startX: e.clientX, startY: e.clientY, moved: false, offset: this.timelineOffset };
|
||||||
canvas.setPointerCapture(e.pointerId);
|
canvas.setPointerCapture(e.pointerId);
|
||||||
} else {
|
} else if (this.view === 'binder') {
|
||||||
this._pick();
|
// 活页本:按下仅记录,松开且未拖拽(<4px)时才翻页
|
||||||
|
this._drag = { active: true, view: 'binder', startX: e.clientX, startY: e.clientY, moved: false };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -227,25 +230,31 @@ export class App {
|
|||||||
if (!this._drag.moved && (Math.abs(dx) > 4 || Math.abs(dy) > 4)) {
|
if (!this._drag.moved && (Math.abs(dx) > 4 || Math.abs(dy) > 4)) {
|
||||||
this._drag.moved = true;
|
this._drag.moved = true;
|
||||||
}
|
}
|
||||||
if (this._drag.moved) {
|
if (this._drag.view === 'timeline' && this._drag.moved) {
|
||||||
const limit = TIMELINE_SPREAD + 0.6;
|
const limit = TIMELINE_SPREAD + 0.6;
|
||||||
this.timelineOffset = clamp(this._drag.offset + dx * this._worldPerPixel(), -limit, limit);
|
this.timelineOffset = clamp(this._drag.offset + dx * this._worldPerPixel(), -limit, limit);
|
||||||
this.timeline.group.position.x = this.timelineOffset;
|
this.timeline.group.position.x = this.timelineOffset;
|
||||||
canvas.style.cursor = 'grabbing';
|
canvas.style.cursor = 'grabbing';
|
||||||
this._needsRender = true;
|
this._needsRender = true;
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
this._hoverDirty = true; // 射线检测移到 _animate 中每帧至多执行一次
|
this._hoverDirty = true; // 射线检测移到 _animate 中每帧至多执行一次
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener('pointerup', (e) => {
|
canvas.addEventListener('pointerup', (e) => {
|
||||||
if (this._drag && this._drag.active) {
|
if (this._drag && this._drag.active) {
|
||||||
const wasClick = !this._drag.moved;
|
const wasClick = !this._drag.moved;
|
||||||
|
const view = this._drag.view;
|
||||||
this._drag.active = false;
|
this._drag.active = false;
|
||||||
canvas.style.cursor = 'grab';
|
canvas.style.cursor = 'grab';
|
||||||
|
if (view === 'timeline') {
|
||||||
this.timeline.setPausedGeneration(false); // 拖拽结束,恢复纹理生成
|
this.timeline.setPausedGeneration(false); // 拖拽结束,恢复纹理生成
|
||||||
if (wasClick) this._pick();
|
if (wasClick) this._pick();
|
||||||
|
} else if (view === 'binder' && wasClick) {
|
||||||
|
setPointer(e); // 用松开位置做射线,松开时触发翻页
|
||||||
|
this._pick();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -260,8 +269,7 @@ export class App {
|
|||||||
window.addEventListener('keydown', (e) => {
|
window.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
|
if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
|
||||||
const dir = e.key === 'ArrowRight' ? 1 : -1;
|
const dir = e.key === 'ArrowRight' ? 1 : -1;
|
||||||
if (this.view === 'timeline') this._stepTimeline(dir);
|
this._stepByView(dir);
|
||||||
else this._step(dir);
|
|
||||||
}
|
}
|
||||||
else if (e.key === '1') this.switchView('binder');
|
else if (e.key === '1') this.switchView('binder');
|
||||||
else if (e.key === '2') this.switchView('timeline');
|
else if (e.key === '2') this.switchView('timeline');
|
||||||
@@ -303,10 +311,10 @@ export class App {
|
|||||||
_hitTest() {
|
_hitTest() {
|
||||||
let list;
|
let list;
|
||||||
if (this.view === 'binder') {
|
if (this.view === 'binder') {
|
||||||
// 只检测顶层可点击页(左右页 + 翻页缓冲),避免对全部 182 页求交
|
// 只检测顶层可点击页(左右顶页 + 正反翻页缓冲),避免对全部页求交
|
||||||
list = [];
|
list = [];
|
||||||
const cur = this.binder.current;
|
const cur = this.binder.current;
|
||||||
for (let d = -1; d <= 1; d++) {
|
for (let d = -2; d <= 1; d++) {
|
||||||
const i = cur + d;
|
const i = cur + d;
|
||||||
if (i >= 0 && i < this.binder.pages.length) list.push(this.binder.pages[i].mesh);
|
if (i >= 0 && i < this.binder.pages.length) list.push(this.binder.pages[i].mesh);
|
||||||
}
|
}
|
||||||
@@ -333,15 +341,11 @@ export class App {
|
|||||||
if (!hit) return;
|
if (!hit) return;
|
||||||
const { view, index } = hit.object.userData;
|
const { view, index } = hit.object.userData;
|
||||||
if (view === 'binder') {
|
if (view === 'binder') {
|
||||||
const cur = this.binder.current;
|
// 按点击位置的空间左右侧判断翻页方向(翻页过程中索引与左右侧会错位,不能按索引)
|
||||||
const f = this.binder.flip;
|
if (hit.point.x >= 0) {
|
||||||
// 翻页进行中:把“正在翻的页”与“即将露出的下一页”都视为向前翻的目标,点击不阻断
|
this.binder.flipForward(); // 点了右侧 → 向前翻
|
||||||
if (index === cur || (f && index === f.nextCurrent)) {
|
|
||||||
this.binder.flipForward();
|
|
||||||
} else if (index === cur - 1) {
|
|
||||||
this.binder.flipBackward();
|
|
||||||
} else {
|
} else {
|
||||||
this._select(index, { syncBinder: true });
|
this.binder.flipBackward(); // 点了左侧 → 向后翻
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this._select(index, { syncBinder: false });
|
this._select(index, { syncBinder: false });
|
||||||
@@ -368,6 +372,12 @@ export class App {
|
|||||||
else this.binder.flipBackward();
|
else this.binder.flipBackward();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 上一页/下一页(底部按钮与左右键共用):时间轴为选择+居中+翻面,其它为活页本翻页
|
||||||
|
_stepByView(dir) {
|
||||||
|
if (this.view === 'timeline') this._stepTimeline(dir);
|
||||||
|
else this._step(dir);
|
||||||
|
}
|
||||||
|
|
||||||
// 时间轴:左右键选择上一张/下一张,动画与点击一致(居中 + 翻面)
|
// 时间轴:左右键选择上一张/下一张,动画与点击一致(居中 + 翻面)
|
||||||
_stepTimeline(dir) {
|
_stepTimeline(dir) {
|
||||||
const next = clamp(this.selected + dir, 0, ENTRIES.length - 1);
|
const next = clamp(this.selected + dir, 0, ENTRIES.length - 1);
|
||||||
@@ -378,9 +388,13 @@ export class App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_select(index, { syncBinder = false } = {}) {
|
_select(index, { syncBinder = false } = {}) {
|
||||||
|
const isEnd = index >= ENTRIES.length;
|
||||||
this.selected = clamp(index, 0, ENTRIES.length - 1);
|
this.selected = clamp(index, 0, ENTRIES.length - 1);
|
||||||
const entry = ENTRIES[this.selected];
|
if (isEnd) {
|
||||||
this.overlay.setEntry(entry, this.selected, ENTRIES.length);
|
this.overlay.setEnd(ENTRIES[this.selected]);
|
||||||
|
} else {
|
||||||
|
this.overlay.setEntry(ENTRIES[this.selected], this.selected, ENTRIES.length);
|
||||||
|
}
|
||||||
this.timeline.setSelected(this.selected);
|
this.timeline.setSelected(this.selected);
|
||||||
this.globe.setSelected(this.selected);
|
this.globe.setSelected(this.selected);
|
||||||
if (this.view === 'globe') this.globe.flyToEntry(this.selected); // 地球页切换时自动居中
|
if (this.view === 'globe') this.globe.flyToEntry(this.selected); // 地球页切换时自动居中
|
||||||
@@ -428,6 +442,12 @@ export class App {
|
|||||||
// 阴影仅在活页本视图启用:时间轴/地理无投影,降低滑动时阴影贴图重绘开销
|
// 阴影仅在活页本视图启用:时间轴/地理无投影,降低滑动时阴影贴图重绘开销
|
||||||
this.renderer.shadowMap.enabled = view === 'binder';
|
this.renderer.shadowMap.enabled = view === 'binder';
|
||||||
|
|
||||||
|
// 灯光:时间轴只用半球光 + 主方向光;活页本保留全部灯光(减少 PBR 每像素光照计算)
|
||||||
|
const fullLights = view === 'binder';
|
||||||
|
this.rimLight.visible = fullLights;
|
||||||
|
this.grazeLight.visible = fullLights;
|
||||||
|
this.backLight.visible = fullLights;
|
||||||
|
|
||||||
// Cesium 地球:地理视图显示 Cesium 画布,其余视图显示 Three.js 画布
|
// Cesium 地球:地理视图显示 Cesium 画布,其余视图显示 Three.js 画布
|
||||||
document.getElementById('scene').style.display = isGlobe ? 'none' : 'block';
|
document.getElementById('scene').style.display = isGlobe ? 'none' : 'block';
|
||||||
document.getElementById('cesium-container').classList.toggle('visible', isGlobe);
|
document.getElementById('cesium-container').classList.toggle('visible', isGlobe);
|
||||||
@@ -526,6 +546,7 @@ export class App {
|
|||||||
// 是否有进行中的动画(决定本帧是否需要重绘)
|
// 是否有进行中的动画(决定本帧是否需要重绘)
|
||||||
const animating =
|
const animating =
|
||||||
this.binder.flip !== null ||
|
this.binder.flip !== null ||
|
||||||
|
this.binder.resetTween !== null ||
|
||||||
this.binder.isSettling() ||
|
this.binder.isSettling() ||
|
||||||
this.timeline.hasActiveFlip() ||
|
this.timeline.hasActiveFlip() ||
|
||||||
this.cameraTween !== null ||
|
this.cameraTween !== null ||
|
||||||
|
|||||||
+311
-129
File diff suppressed because it is too large
Load Diff
+15
-7
@@ -1,24 +1,32 @@
|
|||||||
import { App } from './core/App.js';
|
import { App } from './core/App.js';
|
||||||
|
import { initLoading } from './ui/loading.js';
|
||||||
|
|
||||||
|
// 计时起点:脚本执行开始(含 App 构造耗时),加载界面强制最短展示 6 秒
|
||||||
|
const t0 = Date.now();
|
||||||
|
const MIN_DISPLAY = 8000;
|
||||||
|
|
||||||
// 全站禁止复制/剪切/右键菜单(配合 CSS user-select: none 禁止选中)
|
// 全站禁止复制/剪切/右键菜单(配合 CSS user-select: none 禁止选中)
|
||||||
for (const ev of ['copy', 'cut', 'contextmenu']) {
|
for (const ev of ['copy', 'cut', 'contextmenu']) {
|
||||||
document.addEventListener(ev, (e) => e.preventDefault());
|
document.addEventListener(ev, (e) => e.preventDefault());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OS1 加载环动画
|
||||||
|
const loadingAnim = initLoading();
|
||||||
|
|
||||||
// 启动应用
|
// 启动应用
|
||||||
const app = new App();
|
const app = new App();
|
||||||
window.__app = app; // 便于调试
|
window.__app = app; // 便于调试
|
||||||
|
|
||||||
// 加载界面:等首帧渲染后立即后台预载地理视图(与 loading 并行,不等其淡出);
|
// 首帧渲染后立即后台预载地理视图(与 loading 并行)
|
||||||
// loading 最少展示 3 秒(期间 Cesium 预载已同步进行)。
|
|
||||||
const t0 = Date.now();
|
|
||||||
const MIN_DISPLAY = 6000;
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
app.preloadGlobe(); // 首帧已渲染,立即预载地理界面
|
app.preloadGlobe();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 强制 6 秒后淡出加载界面
|
||||||
const wait = Math.max(0, MIN_DISPLAY - (Date.now() - t0));
|
const wait = Math.max(0, MIN_DISPLAY - (Date.now() - t0));
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.getElementById('loading').classList.add('hidden');
|
document.getElementById('loading').classList.add('hidden');
|
||||||
|
loadingAnim.dispose();
|
||||||
}, wait);
|
}, wait);
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
+107
-57
@@ -1,7 +1,7 @@
|
|||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { ENTRIES } from '../data/entries.js';
|
import { ENTRIES } from '../data/entries.js';
|
||||||
import { PAGE_W, PAGE_H, HOLE_U, acquirePageTextures, releasePageTextures, getPlaceholderTextures, makeTwoSidedMaterial } from '../textures/pages.js';
|
import { PAGE_W, PAGE_H, HOLE_U, acquirePageTextures, releasePageTextures, getPlaceholderTextures, prefetchPageTextures, makeTwoSidedMaterial } from '../textures/pages.js';
|
||||||
import { clamp, easeOutCubic } from '../utils/easing.js';
|
import { clamp, easeOutCubic, easeInOutCubic } from '../utils/easing.js';
|
||||||
|
|
||||||
const RING_R = 0.085;
|
const RING_R = 0.085;
|
||||||
const RING_TUBE = 0.0115;
|
const RING_TUBE = 0.0115;
|
||||||
@@ -234,7 +234,7 @@ function makeLeatherNormalMap() {
|
|||||||
|
|
||||||
// 内侧绒面法线:密集细绒毛(高频噪点)
|
// 内侧绒面法线:密集细绒毛(高频噪点)
|
||||||
function makeSuedeNormalMap() {
|
function makeSuedeNormalMap() {
|
||||||
const w = 512, h = 640; // 降分辨率省 75% 逐像素计算,视觉几乎无差
|
const w = 512, h = 640;
|
||||||
const c = document.createElement('canvas');
|
const c = document.createElement('canvas');
|
||||||
c.width = w; c.height = h;
|
c.width = w; c.height = h;
|
||||||
const ctx = c.getContext('2d', { willReadFrequently: true });
|
const ctx = c.getContext('2d', { willReadFrequently: true });
|
||||||
@@ -278,6 +278,7 @@ export class Binder {
|
|||||||
this.pages = [];
|
this.pages = [];
|
||||||
this.current = 0;
|
this.current = 0;
|
||||||
this.flip = null; // { index, from, to, t, duration }
|
this.flip = null; // { index, from, to, t, duration }
|
||||||
|
this.resetTween = null; // 回卷重置动画
|
||||||
this.onChange = null;
|
this.onChange = null;
|
||||||
|
|
||||||
this.suedeTex = makeSuedeTexture();
|
this.suedeTex = makeSuedeTexture();
|
||||||
@@ -379,23 +380,25 @@ export class Binder {
|
|||||||
return geo;
|
return geo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ensureHiGeo(p) {
|
||||||
|
if (!p.hiGeo) {
|
||||||
|
p.hiGeo = this._makePageGeometry(96, 8);
|
||||||
|
p.hiBase = Float32Array.from(p.hiGeo.attributes.position.array);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_buildPages() {
|
_buildPages() {
|
||||||
const segX = 96, segY = 8;
|
// 共享极简几何(所有栈内页相同,只露边缘无需弯曲)
|
||||||
|
this.loGeo = this._makePageGeometry(1, 1);
|
||||||
|
|
||||||
ENTRIES.forEach((entry, i) => {
|
ENTRIES.forEach((entry, i) => {
|
||||||
// 高分段几何:翻页/顶层页(弯曲需要足够分段)
|
|
||||||
const hiGeo = this._makePageGeometry(segX, segY);
|
|
||||||
const hiBase = Float32Array.from(hiGeo.attributes.position.array);
|
|
||||||
// 极简几何:栈内页(只露边缘,无需弯曲,2 个三角形)
|
|
||||||
const loGeo = this._makePageGeometry(1, 1);
|
|
||||||
|
|
||||||
// 单 mesh + 双面材质:正面登记卡、背面素描(gl_FrontFacing 自动选择)
|
// 单 mesh + 双面材质:正面登记卡、背面素描(gl_FrontFacing 自动选择)
|
||||||
// 活页孔用 alphaTest 直接丢弃透明像素(不启用 transparent,避免透明+双面拆两次 draw call)
|
// 活页孔用 alphaTest 直接丢弃透明像素(不启用 transparent,避免透明+双面拆两次 draw call)
|
||||||
const mat = makeTwoSidedMaterial(this.placeholder.front, this.placeholder.back, {
|
const mat = makeTwoSidedMaterial(this.placeholder.front, this.placeholder.back, {
|
||||||
alphaTest: 0.5,
|
alphaTest: 0.5,
|
||||||
});
|
});
|
||||||
const mesh = new THREE.Mesh(loGeo, mat); // 初始为栈内页,用极简几何
|
const mesh = new THREE.Mesh(this.loGeo, mat); // 初始为栈内页,用极简几何
|
||||||
mesh.castShadow = false; // 由 _restack 依据是否为顶层页控制
|
mesh.castShadow = false; // 由 _restackState 依据是否为顶层页控制
|
||||||
|
|
||||||
const g = new THREE.Group();
|
const g = new THREE.Group();
|
||||||
g.add(mesh);
|
g.add(mesh);
|
||||||
@@ -404,9 +407,9 @@ export class Binder {
|
|||||||
|
|
||||||
this.pages.push({
|
this.pages.push({
|
||||||
group: g,
|
group: g,
|
||||||
hiGeo,
|
hiGeo: null, // 高分段几何延迟创建(成为顶层页时才需要弯曲)
|
||||||
loGeo,
|
hiBase: null,
|
||||||
hiBase,
|
loGeo: this.loGeo,
|
||||||
material: mat,
|
material: mat,
|
||||||
mesh,
|
mesh,
|
||||||
isPlaceholder: true,
|
isPlaceholder: true,
|
||||||
@@ -428,9 +431,16 @@ export class Binder {
|
|||||||
const angle = j < this.current ? -Math.PI : 0;
|
const angle = j < this.current ? -Math.PI : 0;
|
||||||
p.angle = angle;
|
p.angle = angle;
|
||||||
p.group.rotation.y = angle;
|
p.group.rotation.y = angle;
|
||||||
const onTop = (j === this.current || j === this.current - 1);
|
});
|
||||||
const depth = j < this.current ? (this.current - 1 - j) : (j - this.current);
|
this._restackState(this.current, instant);
|
||||||
// 顶层页抬高,与栈内页拉开深度,避免低精度深度缓冲下的穿模(z-fighting)
|
this._updateCurvature();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 堆叠状态(z 目标/亮度/投影/几何 LOD),不碰角度;可传未来 current 实现与翻页并行
|
||||||
|
_restackState(cur, instant = false) {
|
||||||
|
this.pages.forEach((p, j) => {
|
||||||
|
const onTop = (j === cur || j === cur - 1);
|
||||||
|
const depth = j < cur ? (cur - 1 - j) : (j - cur);
|
||||||
p.targetZ = onTop ? TOP_LIFT : -depth * STACK_GAP;
|
p.targetZ = onTop ? TOP_LIFT : -depth * STACK_GAP;
|
||||||
if (instant) {
|
if (instant) {
|
||||||
p.z = p.targetZ;
|
p.z = p.targetZ;
|
||||||
@@ -438,28 +448,34 @@ export class Binder {
|
|||||||
}
|
}
|
||||||
p.group.position.y = 0;
|
p.group.position.y = 0;
|
||||||
p.group.position.x = 0;
|
p.group.position.x = 0;
|
||||||
// 下层页略暗
|
|
||||||
const dim = 1 - Math.min(depth * 0.06, 0.22);
|
const dim = 1 - Math.min(depth * 0.06, 0.22);
|
||||||
p.material.color.setScalar(dim);
|
p.material.color.setScalar(dim);
|
||||||
// 只让顶层(当前右页 + 左侧页)投影,栈内被遮挡页不投影,省阴影 pass
|
|
||||||
p.mesh.castShadow = onTop;
|
p.mesh.castShadow = onTop;
|
||||||
// 顶层页用高分段几何(翻页弯曲),栈内页用极简几何(省顶点)
|
if (onTop) {
|
||||||
if ((p.mesh.geometry === p.hiGeo) !== onTop) {
|
this._ensureHiGeo(p);
|
||||||
p.mesh.geometry = onTop ? p.hiGeo : p.loGeo;
|
if (p.mesh.geometry !== p.hiGeo) p.mesh.geometry = p.hiGeo;
|
||||||
|
} else if (p.mesh.geometry !== p.loGeo) {
|
||||||
|
p.mesh.geometry = p.loGeo;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this._updateCurvature();
|
|
||||||
this._ensureTextures();
|
this._ensureTextures();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 需要真实纹理的窗口:当前页 ±2(左右可见页 + 翻页时会露出的下一层页 + 缓冲)
|
// 需要真实纹理的窗口:左右可见页 + 翻页时会露出的下一层页(按需,首屏最小)
|
||||||
_pageWindow() {
|
_pageWindow() {
|
||||||
const len = this.pages.length;
|
const len = this.pages.length;
|
||||||
const win = new Set();
|
const win = new Set();
|
||||||
for (let d = -2; d <= 2; d++) {
|
// 左页 + 右页(当前)
|
||||||
|
for (let d = -1; d <= 0; d++) {
|
||||||
const i = this.current + d;
|
const i = this.current + d;
|
||||||
if (i >= 0 && i < len) win.add(i);
|
if (i >= 0 && i < len) win.add(i);
|
||||||
}
|
}
|
||||||
|
// 向前翻页会露出的下一层右页
|
||||||
|
const fwd = this.current + 1;
|
||||||
|
if (fwd >= 0 && fwd < len) win.add(fwd);
|
||||||
|
// 向后翻页会露出的下一层左页
|
||||||
|
const bwd = this.current - 2;
|
||||||
|
if (bwd >= 0 && bwd < len) win.add(bwd);
|
||||||
return win;
|
return win;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,10 +501,17 @@ export class Binder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 预生成即将进入纹理窗口的页:推迟到下一帧,避免占用点击这一帧造成卡顿
|
||||||
|
_prefetchForFlip(index) {
|
||||||
|
if (index < 0 || index >= ENTRIES.length) return;
|
||||||
|
requestAnimationFrame(() => prefetchPageTextures(ENTRIES[index]));
|
||||||
|
}
|
||||||
|
|
||||||
_updateCurvature(index = null) {
|
_updateCurvature(index = null) {
|
||||||
// 翻页时整页柔软弧弯:自由边最高、中段有“腹”、书脊侧贴环;顶边略强于底边;静止时完全平整
|
// 翻页时整页柔软弧弯:自由边最高、中段有“腹”、书脊侧贴环;顶边略强于底边;静止时完全平整
|
||||||
const pages = index === null ? this.pages : [this.pages[index]];
|
const pages = index === null ? this.pages : [this.pages[index]];
|
||||||
pages.forEach((p) => {
|
pages.forEach((p) => {
|
||||||
|
if (!p.hiGeo) return; // 栈内页未创建高分段几何,跳过
|
||||||
// 弯曲只作用于高分段几何(顶层/翻页页);栈内极简几何不弯曲
|
// 弯曲只作用于高分段几何(顶层/翻页页);栈内极简几何不弯曲
|
||||||
const pos = p.hiGeo.attributes.position;
|
const pos = p.hiGeo.attributes.position;
|
||||||
const arr = pos.array;
|
const arr = pos.array;
|
||||||
@@ -514,12 +537,13 @@ export class Binder {
|
|||||||
syncTo(index) {
|
syncTo(index) {
|
||||||
this.current = clamp(index, 0, this.pages.length - 1);
|
this.current = clamp(index, 0, this.pages.length - 1);
|
||||||
this.flip = null;
|
this.flip = null;
|
||||||
|
this.resetTween = null; // 外部跳转时终止回卷/快进动画,避免旧状态残留
|
||||||
this._restack(true);
|
this._restack(true);
|
||||||
if (this.onChange) this.onChange(this.current);
|
if (this.onChange) this.onChange(this.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
canFlipForward() { return !this.flip; }
|
canFlipForward() { return !this.flip && !this.resetTween; }
|
||||||
canFlipBackward() { return !this.flip && this.current > 0; }
|
canFlipBackward() { return !this.flip && !this.resetTween; }
|
||||||
|
|
||||||
// 页面 z 是否仍在向目标位置过渡(用于按需渲染:过渡期间需要持续重绘)
|
// 页面 z 是否仍在向目标位置过渡(用于按需渲染:过渡期间需要持续重绘)
|
||||||
isSettling() {
|
isSettling() {
|
||||||
@@ -535,46 +559,74 @@ export class Binder {
|
|||||||
p.group.rotation.y = f.to;
|
p.group.rotation.y = f.to;
|
||||||
p.z = TOP_LIFT;
|
p.z = TOP_LIFT;
|
||||||
p.group.position.z = TOP_LIFT;
|
p.group.position.z = TOP_LIFT;
|
||||||
if (f.to === -Math.PI) {
|
this.current = f.nextCurrent;
|
||||||
this.current = (this.current + 1) % this.pages.length;
|
|
||||||
} else {
|
|
||||||
this.current -= 1;
|
|
||||||
}
|
|
||||||
this.flip = null;
|
this.flip = null;
|
||||||
this._restack(false);
|
this._restack(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_startWrap(targetCur) {
|
||||||
|
// 回卷/快进:所有页从各自当前角度平滑转到目标角度(左页→右侧 或 右页→左侧),current 跳到目标页
|
||||||
|
this.current = targetCur;
|
||||||
|
this.flip = null;
|
||||||
|
this.resetTween = { t: 0, duration: 0.5 };
|
||||||
|
this.pages.forEach((p, j) => {
|
||||||
|
p._resetFrom = p.angle;
|
||||||
|
p._resetTo = j < targetCur ? -Math.PI : 0;
|
||||||
|
});
|
||||||
|
this._restackState(targetCur, false);
|
||||||
|
if (this.onChange) this.onChange(targetCur);
|
||||||
|
}
|
||||||
|
|
||||||
flipForward() {
|
flipForward() {
|
||||||
if (this.flip) this._finalizeFlip(); // 前一页没翻完也能继续翻
|
if (this.flip) this._finalizeFlip(); // 前一页没翻完也能继续翻
|
||||||
if (!this.canFlipForward()) return;
|
if (!this.canFlipForward()) return;
|
||||||
this.flip = { index: this.current, from: 0, to: -Math.PI, t: 0, duration: 0.38 };
|
// END 页(所有页都已翻到左侧,current = pages.length):向前 → 回卷到第一页
|
||||||
// 提前把即将露出的下一层右页切到顶层状态(高分段几何/亮度/投影),避免翻页完成瞬间跳变
|
if (this.current >= this.pages.length) {
|
||||||
const reveal = this.pages[(this.current + 1) % this.pages.length];
|
this._startWrap(0);
|
||||||
if (reveal) {
|
return;
|
||||||
if (reveal.mesh.geometry !== reveal.hiGeo) reveal.mesh.geometry = reveal.hiGeo;
|
|
||||||
reveal.mesh.castShadow = true;
|
|
||||||
reveal.material.color.setScalar(1.0);
|
|
||||||
}
|
}
|
||||||
|
// 最后一页翻完即进入 END 页:右侧无纸,左页为最后一页的背面
|
||||||
|
const nextCurrent = this.current + 1;
|
||||||
|
this.flip = { index: this.current, from: 0, to: -Math.PI, t: 0, duration: 0.38, nextCurrent };
|
||||||
|
// 堆叠重排与旋转并行:翻页开始即设好目标位置,z-lerp 随旋转同步过渡
|
||||||
|
this._restackState(nextCurrent, false);
|
||||||
|
this._prefetchForFlip((nextCurrent + 2) % this.pages.length);
|
||||||
// 优先更新底部导航与信息卡(翻页开始即切换目标页)
|
// 优先更新底部导航与信息卡(翻页开始即切换目标页)
|
||||||
if (this.onChange) this.onChange((this.current + 1) % this.pages.length);
|
if (this.onChange) this.onChange(nextCurrent);
|
||||||
}
|
}
|
||||||
|
|
||||||
flipBackward() {
|
flipBackward() {
|
||||||
if (this.flip) this._finalizeFlip();
|
if (this.flip) this._finalizeFlip();
|
||||||
if (!this.canFlipBackward()) return;
|
if (!this.canFlipBackward()) return;
|
||||||
this.flip = { index: this.current - 1, from: -Math.PI, to: 0, t: 0, duration: 0.38 };
|
// 第一页向后:快进到 END 页(所有未翻的右页都翻到左侧,展示末页背面)
|
||||||
// 提前把即将露出的下一层左页切到顶层状态,避免翻页完成瞬间跳变
|
if (this.current === 0) {
|
||||||
const reveal = this.pages[this.current - 2];
|
this._startWrap(this.pages.length);
|
||||||
if (reveal) {
|
return;
|
||||||
if (reveal.mesh.geometry !== reveal.hiGeo) reveal.mesh.geometry = reveal.hiGeo;
|
|
||||||
reveal.mesh.castShadow = true;
|
|
||||||
reveal.material.color.setScalar(1.0);
|
|
||||||
}
|
}
|
||||||
// 优先更新底部导航与信息卡
|
const nextCurrent = this.current - 1;
|
||||||
if (this.onChange) this.onChange(this.current - 1);
|
this.flip = { index: this.current - 1, from: -Math.PI, to: 0, t: 0, duration: 0.38, nextCurrent };
|
||||||
|
this._restackState(nextCurrent, false);
|
||||||
|
this._prefetchForFlip((nextCurrent - 2 + this.pages.length) % this.pages.length);
|
||||||
|
if (this.onChange) this.onChange(nextCurrent);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(dt) {
|
update(dt) {
|
||||||
|
// 回卷重置动画:所有页角度从各自起点平滑回到 0(右侧)
|
||||||
|
if (this.resetTween) {
|
||||||
|
const r = this.resetTween;
|
||||||
|
r.t += dt / r.duration;
|
||||||
|
const k = clamp(r.t, 0, 1);
|
||||||
|
const e = easeInOutCubic(k);
|
||||||
|
this.pages.forEach((p) => {
|
||||||
|
p.angle = p._resetFrom + (p._resetTo - p._resetFrom) * e;
|
||||||
|
p.group.rotation.y = p.angle;
|
||||||
|
});
|
||||||
|
if (k >= 1) {
|
||||||
|
this.resetTween = null;
|
||||||
|
this._restack(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.flip) {
|
if (this.flip) {
|
||||||
const f = this.flip;
|
const f = this.flip;
|
||||||
f.t += dt / f.duration;
|
f.t += dt / f.duration;
|
||||||
@@ -591,12 +643,10 @@ export class Binder {
|
|||||||
this._updateCurvature(f.index);
|
this._updateCurvature(f.index);
|
||||||
|
|
||||||
if (k >= 1) {
|
if (k >= 1) {
|
||||||
if (f.to === -Math.PI) {
|
this.current = f.nextCurrent;
|
||||||
// 最后一页翻完后循环回第一页
|
|
||||||
this.current = (this.current + 1) % this.pages.length;
|
|
||||||
} else this.current -= 1;
|
|
||||||
this.flip = null;
|
this.flip = null;
|
||||||
this._restack(false);
|
this._updateCurvature();
|
||||||
|
this._ensureTextures();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -610,9 +660,9 @@ export class Binder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
|
if (this.loGeo) this.loGeo.dispose();
|
||||||
this.pages.forEach((p) => {
|
this.pages.forEach((p) => {
|
||||||
p.hiGeo.dispose();
|
if (p.hiGeo) p.hiGeo.dispose();
|
||||||
p.loGeo.dispose();
|
|
||||||
p.material.dispose();
|
p.material.dispose();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-19
@@ -1,6 +1,6 @@
|
|||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { ENTRIES } from '../data/entries.js';
|
import { ENTRIES } from '../data/entries.js';
|
||||||
import { acquirePageTextures, releasePageTextures, getPlaceholderTextures, makeTwoSidedMaterial } from '../textures/pages.js';
|
import { acquirePageTexturesLow, releasePageTexturesLow, getPlaceholderTexturesLow, makeTwoSidedMaterial } from '../textures/pages.js';
|
||||||
import { makeTextSprite } from '../utils/labels.js';
|
import { makeTextSprite } from '../utils/labels.js';
|
||||||
import { clamp, easeInOutCubic } from '../utils/easing.js';
|
import { clamp, easeInOutCubic } from '../utils/easing.js';
|
||||||
|
|
||||||
@@ -22,12 +22,13 @@ export class Timeline {
|
|||||||
this.group = new THREE.Group();
|
this.group = new THREE.Group();
|
||||||
this.cards = [];
|
this.cards = [];
|
||||||
this.selected = -1;
|
this.selected = -1;
|
||||||
this.placeholder = getPlaceholderTextures();
|
this.placeholder = getPlaceholderTexturesLow();
|
||||||
this._textured = new Set();
|
this._textured = new Set();
|
||||||
this._visRange = null;
|
this._visRange = null;
|
||||||
this._ready = false;
|
this._ready = false;
|
||||||
this._pending = [];
|
this._pending = [];
|
||||||
this._dirty = false;
|
this._dirty = false;
|
||||||
|
this._paused = false;
|
||||||
|
|
||||||
this._buildLine();
|
this._buildLine();
|
||||||
this._buildCards();
|
this._buildCards();
|
||||||
@@ -44,15 +45,16 @@ export class Timeline {
|
|||||||
]);
|
]);
|
||||||
this.group.add(new THREE.Line(lineGeo, lineMat));
|
this.group.add(new THREE.Line(lineGeo, lineMat));
|
||||||
|
|
||||||
// 刻度:向上指向卡片、向下指向时间标签
|
// 刻度:合并为单个 LineSegments,一次绘制,避免上百次 draw call
|
||||||
const tickMat = new THREE.LineBasicMaterial({ color: 0xc9a227, transparent: true, opacity: 0.5 });
|
const tickMat = new THREE.LineBasicMaterial({ color: 0xc9a227, transparent: true, opacity: 0.5 });
|
||||||
|
const tickPts = [];
|
||||||
ENTRIES.forEach((e, i) => {
|
ENTRIES.forEach((e, i) => {
|
||||||
const x = cardX(i, n);
|
const x = cardX(i, n);
|
||||||
const g = new THREE.BufferGeometry().setFromPoints([
|
tickPts.push(x, AXIS_Y - 0.14, 0, x, AXIS_Y + 0.12, 0);
|
||||||
new THREE.Vector3(x, AXIS_Y - 0.14, 0), new THREE.Vector3(x, AXIS_Y + 0.12, 0),
|
|
||||||
]);
|
|
||||||
this.group.add(new THREE.Line(g, tickMat));
|
|
||||||
});
|
});
|
||||||
|
const tickGeo = new THREE.BufferGeometry();
|
||||||
|
tickGeo.setAttribute('position', new THREE.Float32BufferAttribute(tickPts, 3));
|
||||||
|
this.group.add(new THREE.LineSegments(tickGeo, tickMat));
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildCards() {
|
_buildCards() {
|
||||||
@@ -62,7 +64,7 @@ export class Timeline {
|
|||||||
const x = cardX(i, n);
|
const x = cardX(i, n);
|
||||||
|
|
||||||
// 每张卡片独立几何(带分段,便于翻面时弯曲出纸感)
|
// 每张卡片独立几何(带分段,便于翻面时弯曲出纸感)
|
||||||
const cardGeo = new THREE.PlaneGeometry(0.4, 0.565, 24, 6);
|
const cardGeo = new THREE.PlaneGeometry(0.4, 0.565, 12, 3);
|
||||||
const base = Float32Array.from(cardGeo.attributes.position.array);
|
const base = Float32Array.from(cardGeo.attributes.position.array);
|
||||||
|
|
||||||
// 翻转容器:单 mesh + 双面材质(翻面时 gl_FrontFacing 自动切换正/背)
|
// 翻转容器:单 mesh + 双面材质(翻面时 gl_FrontFacing 自动切换正/背)
|
||||||
@@ -76,17 +78,37 @@ export class Timeline {
|
|||||||
|
|
||||||
flipGroup.add(cardMesh);
|
flipGroup.add(cardMesh);
|
||||||
|
|
||||||
// 活页上方:主题(常显,随选中上浮,超宽居中换行)
|
// 文字标签(名称/时间)延迟到首屏后由 initLabels() 分帧生成,避免阻塞首屏
|
||||||
|
this.group.add(flipGroup);
|
||||||
|
this.cards.push({ card: flipGroup, mesh: cardMesh, material: mat, geo: cardGeo, base, name: null, time: null, nameBaseY: NAME_Y, index: i, flipped: false, flipAnim: null, isPlaceholder: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间轴文字标签:延迟到首屏后空闲时分帧生成(364 个 canvas 文字精灵较慢,避免阻塞首屏)
|
||||||
|
initLabels() {
|
||||||
|
if (this._labelsDone) return;
|
||||||
|
this._labelsDone = true;
|
||||||
|
const n = ENTRIES.length;
|
||||||
|
let i = 0;
|
||||||
|
const next = () => window.requestAnimationFrame(step);
|
||||||
|
const step = () => {
|
||||||
|
const budget = 4; // 每帧生成 4 个标签,避免卡帧
|
||||||
|
for (let k = 0; k < budget && i < n; k++, i++) {
|
||||||
|
const c = this.cards[i];
|
||||||
|
const entry = ENTRIES[i];
|
||||||
|
const x = cardX(i, n);
|
||||||
const name = makeTextSprite(entry.name, { fontSize: 17, color: '#e8d9b4', scale: 0.12, maxWidth: 0.9 });
|
const name = makeTextSprite(entry.name, { fontSize: 17, color: '#e8d9b4', scale: 0.12, maxWidth: 0.9 });
|
||||||
name.position.set(x, NAME_Y, 0);
|
name.position.set(x, NAME_Y, 0);
|
||||||
|
|
||||||
// 轴下方:时间(常显,固定不动)
|
|
||||||
const time = makeTextSprite(entry.time, { fontSize: 13, color: '#c9a227', scale: 0.11 });
|
const time = makeTextSprite(entry.time, { fontSize: 13, color: '#c9a227', scale: 0.11 });
|
||||||
time.position.set(x, TIME_Y, 0);
|
time.position.set(x, TIME_Y, 0);
|
||||||
|
this.group.add(name, time);
|
||||||
this.group.add(flipGroup, name, time);
|
c.name = name;
|
||||||
this.cards.push({ card: flipGroup, mesh: cardMesh, material: mat, geo: cardGeo, base, name, nameBaseY: NAME_Y, index: i, flipped: false, flipAnim: null, isPlaceholder: true });
|
c.time = time;
|
||||||
});
|
if (this.selected === i) name.position.y = c.nameBaseY + CARD_FLOAT;
|
||||||
|
}
|
||||||
|
if (i < n) next();
|
||||||
|
};
|
||||||
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelected(index) {
|
setSelected(index) {
|
||||||
@@ -94,7 +116,7 @@ export class Timeline {
|
|||||||
this.cards.forEach((c) => {
|
this.cards.forEach((c) => {
|
||||||
const on = c.index === index;
|
const on = c.index === index;
|
||||||
c.card.position.y = on ? CARD_FLOAT : 0;
|
c.card.position.y = on ? CARD_FLOAT : 0;
|
||||||
c.name.position.y = c.nameBaseY + (on ? CARD_FLOAT : 0);
|
if (c.name) c.name.position.y = c.nameBaseY + (on ? CARD_FLOAT : 0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +128,7 @@ export class Timeline {
|
|||||||
// App 每帧根据可视区域调用:minI/maxI 为可见卡片索引范围(可为小数)
|
// App 每帧根据可视区域调用:minI/maxI 为可见卡片索引范围(可为小数)
|
||||||
setVisibleRange(minI, maxI) {
|
setVisibleRange(minI, maxI) {
|
||||||
const len = this.cards.length;
|
const len = this.cards.length;
|
||||||
const PAD = 4; // 缓冲:可视范围外多生成几页,拖拽时提前就绪
|
const PAD = 6; // 缓冲:可视范围外多生成几页,拖拽时提前就绪
|
||||||
const lo = Math.max(0, Math.floor(minI) - PAD);
|
const lo = Math.max(0, Math.floor(minI) - PAD);
|
||||||
const hi = Math.min(len - 1, Math.ceil(maxI) + PAD);
|
const hi = Math.min(len - 1, Math.ceil(maxI) + PAD);
|
||||||
if (this._visRange && this._visRange.min === lo && this._visRange.max === hi) return;
|
if (this._visRange && this._visRange.min === lo && this._visRange.max === hi) return;
|
||||||
@@ -138,7 +160,7 @@ export class Timeline {
|
|||||||
for (const i of [...this._textured]) {
|
for (const i of [...this._textured]) {
|
||||||
if (win.has(i)) continue;
|
if (win.has(i)) continue;
|
||||||
const c = this.cards[i];
|
const c = this.cards[i];
|
||||||
releasePageTextures(ENTRIES[i]);
|
releasePageTexturesLow(ENTRIES[i]);
|
||||||
c.material.setPageMaps(this.placeholder.front, this.placeholder.back);
|
c.material.setPageMaps(this.placeholder.front, this.placeholder.back);
|
||||||
c.isPlaceholder = true;
|
c.isPlaceholder = true;
|
||||||
this._textured.delete(i);
|
this._textured.delete(i);
|
||||||
@@ -152,8 +174,15 @@ export class Timeline {
|
|||||||
this.drainPending();
|
this.drainPending();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 拖拽期间暂停纹理生成,避免主线程同步绘制导致掉帧
|
||||||
|
setPausedGeneration(paused) {
|
||||||
|
this._paused = paused;
|
||||||
|
if (!paused) this.drainPending(); // 恢复时立即补生成
|
||||||
|
}
|
||||||
|
|
||||||
// 每帧由 App._animate 驱动,逐批生成窗口内纹理(避免阻塞)
|
// 每帧由 App._animate 驱动,逐批生成窗口内纹理(避免阻塞)
|
||||||
drainPending() {
|
drainPending() {
|
||||||
|
if (this._paused) return false;
|
||||||
if (!this._pending || !this._pending.length) return false;
|
if (!this._pending || !this._pending.length) return false;
|
||||||
const budget = 2; // 每帧最多生成 2 页
|
const budget = 2; // 每帧最多生成 2 页
|
||||||
let did = false;
|
let did = false;
|
||||||
@@ -161,7 +190,7 @@ export class Timeline {
|
|||||||
const i = this._pending.shift();
|
const i = this._pending.shift();
|
||||||
const c = this.cards[i];
|
const c = this.cards[i];
|
||||||
if (!c || this._textured.has(i)) continue;
|
if (!c || this._textured.has(i)) continue;
|
||||||
const { front, back } = acquirePageTextures(ENTRIES[i]);
|
const { front, back } = acquirePageTexturesLow(ENTRIES[i]);
|
||||||
c.material.setPageMaps(front, back);
|
c.material.setPageMaps(front, back);
|
||||||
c.isPlaceholder = false;
|
c.isPlaceholder = false;
|
||||||
this._textured.add(i);
|
this._textured.add(i);
|
||||||
|
|||||||
+28
-30
@@ -99,39 +99,35 @@ html, body {
|
|||||||
color: rgba(230,216,182,0.65); font-family: var(--serif);
|
color: rgba(230,216,182,0.65); font-family: var(--serif);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 视图切换(位于底部导航栏内) */
|
/* 视图切换(单按钮,图标循环切换) */
|
||||||
.view-switch {
|
|
||||||
display: flex; align-items: center; gap: 8px;
|
|
||||||
}
|
|
||||||
.view-btn {
|
.view-btn {
|
||||||
font-family: var(--serif); font-size: 14px; letter-spacing: 0.1em;
|
flex-shrink: 0;
|
||||||
padding: 8px 16px; color: var(--paper); cursor: pointer;
|
width: 32px; height: 32px;
|
||||||
background: rgba(20,14,10,0.55); border: 1px solid rgba(201,162,39,0.35);
|
display: flex; align-items: center; justify-content: center;
|
||||||
border-radius: 3px; backdrop-filter: blur(4px);
|
color: var(--gold-soft); cursor: pointer;
|
||||||
transition: all 0.25s ease;
|
background: rgba(255,255,255,0.04); border: 1px solid rgba(201,162,39,0.4);
|
||||||
}
|
border-radius: 50%;
|
||||||
.view-btn:hover { border-color: var(--gold); color: #fff; }
|
transition: all 0.2s ease;
|
||||||
.view-btn:disabled {
|
|
||||||
opacity: 0.45;
|
|
||||||
cursor: not-allowed;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.view-btn.active {
|
|
||||||
background: linear-gradient(180deg, rgba(201,162,39,0.85), rgba(150,115,20,0.85));
|
|
||||||
color: #1c1308; border-color: var(--gold); font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
.view-btn:hover { background: rgba(201,162,39,0.25); color: #fff; }
|
||||||
|
.v-icon { display: none; width: 17px; height: 17px; }
|
||||||
|
.view-btn[data-view="binder"] .v-binder { display: block; }
|
||||||
|
.view-btn[data-view="timeline"] .v-timeline { display: block; }
|
||||||
|
.view-btn[data-view="globe"] .v-globe { display: block; }
|
||||||
|
|
||||||
/* 底部导航 */
|
/* 底部导航 */
|
||||||
.bottom-bar {
|
.bottom-bar {
|
||||||
left: 50%; bottom: 22px; transform: translateX(-50%);
|
left: 50%; bottom: 26px; transform: translateX(-50%);
|
||||||
display: flex; align-items: center; gap: 16px;
|
display: flex; align-items: center; gap: 12px;
|
||||||
padding: 10px 18px; border-radius: 4px;
|
padding: 5px 14px; border-radius: 4px;
|
||||||
background: rgba(20,14,10,0.6); border: 1px solid rgba(201,162,39,0.25);
|
background: rgba(20,14,10,0.6); border: 1px solid rgba(201,162,39,0.25);
|
||||||
backdrop-filter: blur(6px);
|
backdrop-filter: blur(6px);
|
||||||
}
|
}
|
||||||
.nav-btn {
|
.nav-btn {
|
||||||
font-family: var(--serif); font-size: 16px; line-height: 1;
|
flex-shrink: 0;
|
||||||
width: 38px; height: 38px; border-radius: 50%;
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
font-family: var(--serif); font-size: 15px; line-height: 1;
|
||||||
|
width: 32px; height: 32px; border-radius: 50%;
|
||||||
color: var(--gold-soft); cursor: pointer;
|
color: var(--gold-soft); cursor: pointer;
|
||||||
background: rgba(255,255,255,0.04); border: 1px solid rgba(201,162,39,0.4);
|
background: rgba(255,255,255,0.04); border: 1px solid rgba(201,162,39,0.4);
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
@@ -139,13 +135,13 @@ html, body {
|
|||||||
.nav-btn:hover { background: rgba(201,162,39,0.25); color: #fff; transform: scale(1.06); }
|
.nav-btn:hover { background: rgba(201,162,39,0.25); color: #fff; transform: scale(1.06); }
|
||||||
.nav-btn:active { transform: scale(0.96); }
|
.nav-btn:active { transform: scale(0.96); }
|
||||||
.flip-hint { width: auto; border-radius: 20px; padding: 0 18px; letter-spacing: 0.12em; font-size: 14px; }
|
.flip-hint { width: auto; border-radius: 20px; padding: 0 18px; letter-spacing: 0.12em; font-size: 14px; }
|
||||||
.page-indicator { display: flex; align-items: baseline; gap: 6px; min-width: 200px; justify-content: center; }
|
.page-indicator { display: flex; align-items: center; gap: 6px; min-width: 0; justify-content: center; }
|
||||||
#page-num { font-size: 24px; color: var(--gold-soft); font-family: var(--serif); }
|
#page-num { font-size: 22px; color: var(--gold-soft); font-family: var(--serif); }
|
||||||
.sep { color: rgba(230,216,182,0.5); }
|
.sep { color: rgba(230,216,182,0.5); }
|
||||||
#page-total { color: rgba(230,216,182,0.6); font-family: var(--serif); }
|
#page-total { color: rgba(230,216,182,0.6); font-family: var(--serif); }
|
||||||
.page-name {
|
.page-name {
|
||||||
font-size: 14px; color: var(--paper); letter-spacing: 0.05em; margin-left: 10px;
|
font-size: 14px; color: var(--paper); letter-spacing: 0.05em; margin-left: 10px;
|
||||||
flex: 0 0 160px; /* 固定宽度,容纳多数词条名,避免翻页时布局抖动 */
|
flex: 0 0 160px; /* 固定宽度,导航条不随词条名长短变化 */
|
||||||
text-align: center;
|
text-align: center;
|
||||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -161,7 +157,7 @@ html, body {
|
|||||||
}
|
}
|
||||||
.detail-card.hidden { opacity: 0; pointer-events: none; transform: translateY(14px); }
|
.detail-card.hidden { opacity: 0; pointer-events: none; transform: translateY(14px); }
|
||||||
.detail-head { display: flex; justify-content: space-between; align-items: center; }
|
.detail-head { display: flex; justify-content: space-between; align-items: center; }
|
||||||
.detail-no { font-family: var(--serif); font-size: 13px; letter-spacing: 0.14em; color: var(--ink-soft); }
|
.detail-no { font-family: Consolas, "Courier New", "Lucida Console", monospace; font-size: 13px; letter-spacing: 0.02em; color: var(--ink-soft); }
|
||||||
.detail-cat {
|
.detail-cat {
|
||||||
font-size: 12px; padding: 2px 10px; border-radius: 2px;
|
font-size: 12px; padding: 2px 10px; border-radius: 2px;
|
||||||
background: var(--leather); color: var(--gold-soft); letter-spacing: 0.2em;
|
background: var(--leather); color: var(--gold-soft); letter-spacing: 0.2em;
|
||||||
@@ -182,10 +178,12 @@ html, body {
|
|||||||
.detail-sketch { margin-top: 12px; border-top: 1px dashed rgba(90,61,43,0.4); padding-top: 10px; }
|
.detail-sketch { margin-top: 12px; border-top: 1px dashed rgba(90,61,43,0.4); padding-top: 10px; }
|
||||||
.detail-sketch canvas { width: 100%; height: 170px; object-fit: contain; border-radius: 2px; opacity: 0.94; }
|
.detail-sketch canvas { width: 100%; height: 170px; object-fit: contain; border-radius: 2px; opacity: 0.94; }
|
||||||
|
|
||||||
/* 提示 */
|
/* 提示(导航条下方) */
|
||||||
.hint {
|
.hint {
|
||||||
left: 26px; bottom: 26px; font-size: 12px; letter-spacing: 0.06em;
|
left: 50%; transform: translateX(-50%); bottom: 5px;
|
||||||
|
font-size: 12px; letter-spacing: 0.06em;
|
||||||
color: rgba(230,216,182,0.55);
|
color: rgba(230,216,182,0.55);
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
|
|||||||
+118
-71
@@ -7,9 +7,6 @@ export const PAGE_W = 1.6; // 世界单位:页宽
|
|||||||
export const PAGE_H = 2.24; // 世界单位:页高
|
export const PAGE_H = 2.24; // 世界单位:页高
|
||||||
export const TEX_W = 768;
|
export const TEX_W = 768;
|
||||||
export const TEX_H = 1075;
|
export const TEX_H = 1075;
|
||||||
// 输出纹理与绘制分辨率一致,保证文字清晰(显存靠懒加载 + LRU 控制,不牺牲画质)
|
|
||||||
export const TEX_OUT_W = 768;
|
|
||||||
export const TEX_OUT_H = 1075;
|
|
||||||
export const HOLE_U = [0.1, 0.26, 0.42, 0.58, 0.74, 0.9]; // 活页孔(自顶部归一化,6孔等距)
|
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_X = 0.065; // 孔中心相对页内缘(环前交点 0.085 − 页缘空隙 0.02)
|
||||||
export const HOLE_R = 0.029; // 孔半径 ≈5.5mm 直径(真实活页本孔径)
|
export const HOLE_R = 0.029; // 孔半径 ≈5.5mm 直径(真实活页本孔径)
|
||||||
@@ -17,6 +14,7 @@ export const HOLE_R = 0.029; // 孔半径 ≈5.5mm 直径(真实活页
|
|||||||
const SERIF = 'Georgia, "Noto Serif SC", serif'; // 正文与标题字体一致(时间轴字体)
|
const SERIF = 'Georgia, "Noto Serif SC", serif'; // 正文与标题字体一致(时间轴字体)
|
||||||
const KAI = '"Kaiti SC","STKaiti","KaiTi","Noto Serif SC",serif';
|
const KAI = '"Kaiti SC","STKaiti","KaiTi","Noto Serif SC",serif';
|
||||||
const NUM = 'Georgia,"Times New Roman",serif';
|
const NUM = 'Georgia,"Times New Roman",serif';
|
||||||
|
const CODE = 'Consolas, "Courier New", "Lucida Console", monospace'; // 编目号等宽字体
|
||||||
const TITLE_FONT = '"Noto Serif SC", "Songti SC", "STSong", "SimSun", Georgia, serif'; // 与时间轴标题字体一致
|
const TITLE_FONT = '"Noto Serif SC", "Songti SC", "STSong", "SimSun", Georgia, serif'; // 与时间轴标题字体一致
|
||||||
|
|
||||||
// 版面边距(左边缘避开活页孔,右/上/下为常规边距)
|
// 版面边距(左边缘避开活页孔,右/上/下为常规边距)
|
||||||
@@ -69,28 +67,6 @@ export function makePaper(w, h, { tone = 0, ruled = false, margin = false, seed
|
|||||||
ctx.fillRect(0, 0, w, h);
|
ctx.fillRect(0, 0, w, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 烫金斑点:不规则金色斑点(淡、纯色、硬边)
|
|
||||||
ctx.fillStyle = 'rgba(212, 168, 64, 0.5)';
|
|
||||||
const goldBlob = (x, y, r) => {
|
|
||||||
const pts = 8;
|
|
||||||
ctx.beginPath();
|
|
||||||
for (let p = 0; p < pts; p++) {
|
|
||||||
const a = (p / pts) * Math.PI * 2;
|
|
||||||
const rr = r * (0.55 + rand() * 0.9);
|
|
||||||
const px = x + Math.cos(a) * rr;
|
|
||||||
const py = y + Math.sin(a) * rr;
|
|
||||||
if (p === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
|
|
||||||
}
|
|
||||||
ctx.closePath();
|
|
||||||
ctx.fill();
|
|
||||||
};
|
|
||||||
for (let i = 0; i < 70; i++) {
|
|
||||||
goldBlob(rand() * w, rand() * h, rand() * 8 + 3);
|
|
||||||
}
|
|
||||||
for (let i = 0; i < 320; i++) {
|
|
||||||
goldBlob(rand() * w, rand() * h, rand() * 2.4 + 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 边缘做旧(暗角)
|
// 边缘做旧(暗角)
|
||||||
const edge = ctx.createRadialGradient(w / 2, h / 2, Math.min(w, h) * 0.4, w / 2, h / 2, Math.max(w, h) * 0.72);
|
const edge = ctx.createRadialGradient(w / 2, h / 2, Math.min(w, h) * 0.4, w / 2, h / 2, Math.max(w, h) * 0.72);
|
||||||
edge.addColorStop(0, 'rgba(0,0,0,0)');
|
edge.addColorStop(0, 'rgba(0,0,0,0)');
|
||||||
@@ -200,63 +176,56 @@ export function renderFront(entry) {
|
|||||||
drawBorder(ctx, w, h);
|
drawBorder(ctx, w, h);
|
||||||
drawHoles(ctx, w, h);
|
drawHoles(ctx, w, h);
|
||||||
|
|
||||||
// 页眉:编号(去掉右上角印章标记)
|
// 页眉:编目号(档案式编号,Consolas 等宽字体)
|
||||||
text(ctx, '编 号 · ' + entry.id, CONTENT_L, 92, { font: NUM, size: 26, color: '#5a432c' });
|
text(ctx, 'NO.' + (entry.code || entry.id), CONTENT_L, 92, { font: CODE, size: 23, color: '#33271a', weight: '700' });
|
||||||
|
|
||||||
// 顶部分隔线
|
// 顶部分隔线(网格)
|
||||||
ctx.strokeStyle = 'rgba(90,61,43,0.4)';
|
ctx.strokeStyle = 'rgba(90,61,43,0.45)';
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
ctx.beginPath(); ctx.moveTo(CONTENT_L, 160); ctx.lineTo(CONTENT_R, 160); ctx.stroke();
|
ctx.beginPath(); ctx.moveTo(CONTENT_L, 160); ctx.lineTo(CONTENT_R, 160); ctx.stroke();
|
||||||
|
|
||||||
// 名称(大字,居中,标题字体与时间轴一致)
|
// 名称(大字,居中)
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
ctx.font = `700 ${fitFont(ctx, entry.name, CONTENT_W - 40, 36, 86, TITLE_FONT)}px ${TITLE_FONT}`;
|
ctx.font = `700 ${fitFont(ctx, entry.name, CONTENT_W - 40, 36, 86, TITLE_FONT)}px ${TITLE_FONT}`;
|
||||||
ctx.fillStyle = '#33271a';
|
ctx.fillStyle = '#33271a';
|
||||||
ctx.fillText(entry.name, w / 2, 248);
|
ctx.fillText(entry.name, w / 2, 210);
|
||||||
// 名称下划线装饰
|
|
||||||
ctx.strokeStyle = info.color;
|
// —— 时间 / 地区(左对齐,字体同标题)——
|
||||||
ctx.lineWidth = 2.4;
|
const gridTop = 276, rowH = 64;
|
||||||
ctx.beginPath(); ctx.moveTo(w / 2 - 220, 314); ctx.lineTo(w / 2 + 220, 314); ctx.stroke();
|
const gLeft = CONTENT_L, gRight = CONTENT_R;
|
||||||
ctx.fillStyle = info.color;
|
const gBottom = gridTop + rowH * 2;
|
||||||
ctx.beginPath(); ctx.arc(w / 2, 314, 4, 0, Math.PI * 2); ctx.fill();
|
|
||||||
|
ctx.strokeStyle = 'rgba(120,95,60,0.5)';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
for (let r = 0; r <= 2; r++) {
|
||||||
|
const y = gridTop + r * rowH;
|
||||||
|
ctx.beginPath(); ctx.moveTo(gLeft, y); ctx.lineTo(gRight, y); ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
// 登记字段
|
|
||||||
const labelX = CONTENT_L + 56; // 标签右对齐
|
|
||||||
const colonX = CONTENT_L + 64; // 冒号
|
|
||||||
const valX = CONTENT_L + 82; // 值起始
|
|
||||||
const fy = 372, rowH = 62;
|
|
||||||
const fields = [
|
const fields = [
|
||||||
['时 间', entry.time],
|
['时 间', entry.time],
|
||||||
['地 区', entry.region],
|
['地 区', entry.region],
|
||||||
];
|
];
|
||||||
fields.forEach(([label, value], i) => {
|
fields.forEach(([label, value], i) => {
|
||||||
const y = fy + i * rowH;
|
const cy = gridTop + i * rowH + rowH / 2;
|
||||||
text(ctx, label, labelX, y, { font: SERIF, size: 26, color: '#7a5c38', align: 'right' });
|
text(ctx, label, gLeft, cy, { font: TITLE_FONT, size: 26, color: '#7a5c38', baseline: 'middle' });
|
||||||
text(ctx, ':', colonX, y, { font: SERIF, size: 26, color: '#7a5c38' });
|
text(ctx, value, gLeft + 96, cy, { font: TITLE_FONT, size: 26, color: '#33271a', baseline: 'middle' });
|
||||||
text(ctx, value, valX, y, { font: SERIF, size: 26, color: '#33271a' });
|
|
||||||
// 值后虚线延伸至右界
|
|
||||||
const vw = ctx.measureText(value).width;
|
|
||||||
ctx.strokeStyle = 'rgba(120,95,60,0.35)';
|
|
||||||
ctx.lineWidth = 0.9;
|
|
||||||
ctx.setLineDash([2, 5]);
|
|
||||||
ctx.beginPath(); ctx.moveTo(valX + vw + 14, y + 12); ctx.lineTo(CONTENT_R, y + 12); ctx.stroke();
|
|
||||||
ctx.setLineDash([]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 描述区
|
// —— 描述区 ——
|
||||||
const dy = 600;
|
const dy = 482;
|
||||||
text(ctx, '描 述', CONTENT_L, dy, { font: SERIF, size: 26, color: '#7a5c38' });
|
text(ctx, '描 述', CONTENT_L, dy, { font: TITLE_FONT, size: 26, color: '#7a5c38' });
|
||||||
ctx.strokeStyle = 'rgba(120,95,60,0.3)';
|
ctx.strokeStyle = 'rgba(120,95,60,0.45)';
|
||||||
ctx.lineWidth = 0.9;
|
ctx.lineWidth = 1;
|
||||||
ctx.beginPath(); ctx.moveTo(CONTENT_L, dy + 18); ctx.lineTo(CONTENT_R, dy + 18); ctx.stroke();
|
ctx.beginPath(); ctx.moveTo(CONTENT_L, dy + 22); ctx.lineTo(CONTENT_R, dy + 22); ctx.stroke();
|
||||||
|
|
||||||
ctx.font = `400 24px ${SERIF}`;
|
ctx.font = `400 24px ${TITLE_FONT}`;
|
||||||
ctx.fillStyle = '#33271a';
|
ctx.fillStyle = '#33271a';
|
||||||
ctx.textAlign = 'left';
|
ctx.textAlign = 'left';
|
||||||
ctx.textBaseline = 'top';
|
ctx.textBaseline = 'top';
|
||||||
const maxW = CONTENT_W;
|
const maxW = CONTENT_W;
|
||||||
let line = '', yy = dy + 40;
|
let line = '', yy = dy + 44;
|
||||||
for (const ch of entry.description) {
|
for (const ch of entry.description) {
|
||||||
const test = line + ch;
|
const test = line + ch;
|
||||||
if (ctx.measureText(test).width > maxW && line) {
|
if (ctx.measureText(test).width > maxW && line) {
|
||||||
@@ -334,7 +303,9 @@ let placeholder = null;
|
|||||||
function makeTexture(canvas) {
|
function makeTexture(canvas) {
|
||||||
const tex = new THREE.CanvasTexture(canvas);
|
const tex = new THREE.CanvasTexture(canvas);
|
||||||
tex.colorSpace = THREE.SRGBColorSpace;
|
tex.colorSpace = THREE.SRGBColorSpace;
|
||||||
tex.anisotropy = 8; // 保持倾斜视角下的清晰度
|
tex.anisotropy = 4; // 倾斜视角已够用,降低采样带宽
|
||||||
|
tex.generateMipmaps = false; // 页面距离变化小,关 mipmap 省 ~33% 显存
|
||||||
|
tex.minFilter = THREE.LinearFilter;
|
||||||
return tex;
|
return tex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,32 +320,36 @@ function renderPlaceholder() {
|
|||||||
|
|
||||||
export function getPlaceholderTextures() {
|
export function getPlaceholderTextures() {
|
||||||
if (!placeholder) {
|
if (!placeholder) {
|
||||||
const front = makeTexture(downscale(renderPlaceholder()));
|
const front = makeTexture(renderPlaceholder());
|
||||||
const back = makeTexture(downscale(renderPlaceholder()));
|
const back = makeTexture(renderPlaceholder());
|
||||||
placeholder = { front, back };
|
placeholder = { front, back };
|
||||||
}
|
}
|
||||||
return placeholder;
|
return placeholder;
|
||||||
}
|
}
|
||||||
|
|
||||||
function generatePair(entry) {
|
function generatePair(entry) {
|
||||||
const front = makeTexture(downscale(renderFront(entry)));
|
const front = makeTexture(renderFront(entry));
|
||||||
const back = makeTexture(downscale(renderBack(entry)));
|
const back = makeTexture(renderBack(entry));
|
||||||
return { front, back };
|
return { front, back };
|
||||||
}
|
}
|
||||||
|
|
||||||
function evictIfNeeded() {
|
function evictCache(cache, maxPages) {
|
||||||
if (texCache.size <= MAX_CACHED_PAGES) return;
|
if (cache.size <= maxPages) return;
|
||||||
const candidates = [...texCache.values()]
|
const candidates = [...cache.values()]
|
||||||
.filter((r) => r.refs === 0)
|
.filter((r) => r.refs === 0)
|
||||||
.sort((a, b) => a.lastUsed - b.lastUsed);
|
.sort((a, b) => a.lastUsed - b.lastUsed);
|
||||||
while (texCache.size > MAX_CACHED_PAGES && candidates.length) {
|
while (cache.size > maxPages && candidates.length) {
|
||||||
const r = candidates.shift();
|
const r = candidates.shift();
|
||||||
r.front.dispose();
|
r.front.dispose();
|
||||||
r.back.dispose();
|
r.back.dispose();
|
||||||
texCache.delete(r.entry.id);
|
cache.delete(r.entry.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function evictIfNeeded() {
|
||||||
|
evictCache(texCache, MAX_CACHED_PAGES);
|
||||||
|
}
|
||||||
|
|
||||||
export function acquirePageTextures(entry) {
|
export function acquirePageTextures(entry) {
|
||||||
let rec = texCache.get(entry.id);
|
let rec = texCache.get(entry.id);
|
||||||
if (!rec) {
|
if (!rec) {
|
||||||
@@ -392,6 +367,21 @@ export function acquirePageTextures(entry) {
|
|||||||
return { front: rec.front, back: rec.back };
|
return { front: rec.front, back: rec.back };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 预生成纹理(不改变引用计数,供翻页前预热用,避免翻页中途同步生成造成卡顿)
|
||||||
|
export function prefetchPageTextures(entry) {
|
||||||
|
let rec = texCache.get(entry.id);
|
||||||
|
if (!rec) {
|
||||||
|
rec = { entry, front: null, back: null, refs: 0, lastUsed: 0 };
|
||||||
|
texCache.set(entry.id, rec);
|
||||||
|
}
|
||||||
|
if (!rec.front) {
|
||||||
|
const pair = generatePair(entry);
|
||||||
|
rec.front = pair.front;
|
||||||
|
rec.back = pair.back;
|
||||||
|
evictIfNeeded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function releasePageTextures(entry) {
|
export function releasePageTextures(entry) {
|
||||||
const rec = texCache.get(entry.id);
|
const rec = texCache.get(entry.id);
|
||||||
if (!rec) return;
|
if (!rec) return;
|
||||||
@@ -399,6 +389,63 @@ export function releasePageTextures(entry) {
|
|||||||
if (rec.refs === 0) rec.lastUsed = performance.now();
|
if (rec.refs === 0) rec.lastUsed = performance.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// —— 低分辨率纹理池(时间轴卡片用,显示尺寸小,省显存与上传带宽)——
|
||||||
|
const TEX_LOW_W = 256;
|
||||||
|
const TEX_LOW_H = 358;
|
||||||
|
const MAX_CACHED_PAGES_LOW = 40;
|
||||||
|
const texCacheLow = new Map();
|
||||||
|
let placeholderLow = null;
|
||||||
|
|
||||||
|
function downscaleLow(src) {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = TEX_LOW_W;
|
||||||
|
c.height = TEX_LOW_H;
|
||||||
|
const ctx = c.getContext('2d');
|
||||||
|
ctx.imageSmoothingEnabled = true;
|
||||||
|
ctx.imageSmoothingQuality = 'high';
|
||||||
|
ctx.drawImage(src, 0, 0, TEX_LOW_W, TEX_LOW_H);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatePairLow(entry) {
|
||||||
|
const front = makeTexture(downscaleLow(renderFront(entry)));
|
||||||
|
const back = makeTexture(downscaleLow(renderBack(entry)));
|
||||||
|
return { front, back };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPlaceholderTexturesLow() {
|
||||||
|
if (!placeholderLow) {
|
||||||
|
const front = makeTexture(downscaleLow(renderPlaceholder()));
|
||||||
|
const back = makeTexture(downscaleLow(renderPlaceholder()));
|
||||||
|
placeholderLow = { front, back };
|
||||||
|
}
|
||||||
|
return placeholderLow;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function acquirePageTexturesLow(entry) {
|
||||||
|
let rec = texCacheLow.get(entry.id);
|
||||||
|
if (!rec) {
|
||||||
|
rec = { entry, front: null, back: null, refs: 0, lastUsed: 0 };
|
||||||
|
texCacheLow.set(entry.id, rec);
|
||||||
|
}
|
||||||
|
rec.refs++;
|
||||||
|
rec.lastUsed = performance.now();
|
||||||
|
if (!rec.front) {
|
||||||
|
const pair = generatePairLow(entry);
|
||||||
|
rec.front = pair.front;
|
||||||
|
rec.back = pair.back;
|
||||||
|
evictCache(texCacheLow, MAX_CACHED_PAGES_LOW);
|
||||||
|
}
|
||||||
|
return { front: rec.front, back: rec.back };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function releasePageTexturesLow(entry) {
|
||||||
|
const rec = texCacheLow.get(entry.id);
|
||||||
|
if (!rec) return;
|
||||||
|
rec.refs = Math.max(0, rec.refs - 1);
|
||||||
|
if (rec.refs === 0) rec.lastUsed = performance.now();
|
||||||
|
}
|
||||||
|
|
||||||
// 双面材质:同一 mesh 用 gl_FrontFacing 在正/反面显示不同纹理,
|
// 双面材质:同一 mesh 用 gl_FrontFacing 在正/反面显示不同纹理,
|
||||||
// 替代原先 front/back 两个 mesh,把每页 draw call 减半。
|
// 替代原先 front/back 两个 mesh,把每页 draw call 减半。
|
||||||
export function makeTwoSidedMaterial(front, back, {
|
export function makeTwoSidedMaterial(front, back, {
|
||||||
|
|||||||
+513
-47
@@ -581,68 +581,534 @@ export function renderSketch(entry, size = 760) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
book() {
|
book() {
|
||||||
// 摊开的书
|
// 封面(略大一圈,深色底)
|
||||||
fill([[-0.5, -0.1], [0.0, -0.2], [0.5, -0.1], [0.5, 0.4], [0.0, 0.5], [-0.5, 0.4]], 'rgba(210,190,150,0.18)');
|
fill([[-0.55, -0.28], [0.55, -0.28], [0.55, 0.5], [0.0, 0.6], [-0.55, 0.5]], 'rgba(120,80,50,0.10)');
|
||||||
line([[-0.5, -0.1], [0.0, -0.2], [0.5, -0.1], [0.5, 0.4], [0.0, 0.5], [-0.5, 0.4], [-0.5, -0.1]], { lw: 2.4 });
|
line([[-0.55, -0.28], [0.55, -0.28], [0.55, 0.5], [0.0, 0.6], [-0.55, 0.5], [-0.55, -0.28]], { lw: 2.6 });
|
||||||
line([[0.0, -0.2], [0.0, 0.5]], { lw: 2 });
|
// 摊开的书页
|
||||||
for (let i = 0; i < 4; i++) {
|
fill([[-0.5, -0.2], [0.0, -0.3], [0.5, -0.2], [0.5, 0.42], [0.0, 0.52], [-0.5, 0.42]], 'rgba(210,190,150,0.22)');
|
||||||
const y = -0.05 + i * 0.14;
|
line([[-0.5, -0.2], [0.0, -0.3], [0.5, -0.2], [0.5, 0.42], [0.0, 0.52], [-0.5, 0.42], [-0.5, -0.2]], { lw: 2.4 });
|
||||||
line([[-0.4, y], [-0.08, y]], { lw: 1.2, color: INK_SOFT, passes: 1, wobble: 0.004 });
|
// 中缝与页缘叠层
|
||||||
line([[0.08, y], [0.4, y]], { lw: 1.2, color: INK_SOFT, passes: 1, wobble: 0.004 });
|
line([[0.0, -0.3], [0.0, 0.52]], { lw: 1.8, color: INK_SOFT });
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const o = i * 0.016;
|
||||||
|
line([[-0.5 + o, -0.2], [-0.5 + o, 0.42]], { lw: 1, color: INK_FAINT, passes: 1 });
|
||||||
|
line([[0.5 - o, -0.2], [0.5 - o, 0.42]], { lw: 1, color: INK_FAINT, passes: 1 });
|
||||||
}
|
}
|
||||||
hatch([[-0.5, -0.1], [0.0, -0.2], [0.5, -0.1], [0.5, 0.4], [0.0, 0.5], [-0.5, 0.4]], { spacing: 0.04, angle: -45, color: INK_FAINT, lw: 1 });
|
// 左右页文字行
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const y = -0.14 + i * 0.105;
|
||||||
|
line([[-0.42, y], [-0.08, y]], { lw: 1.3, color: INK_SOFT, passes: 1, wobble: 0.004 });
|
||||||
|
line([[0.08, y], [0.42, y]], { lw: 1.3, color: INK_SOFT, passes: 1, wobble: 0.004 });
|
||||||
|
}
|
||||||
|
// 右页小插图
|
||||||
|
circle(0.25, 0.12, 0.09, { lw: 1.4, passes: 1, color: INK_SOFT });
|
||||||
|
line([[0.18, 0.12], [0.32, 0.12]], { lw: 1, color: INK_FAINT, passes: 1 });
|
||||||
|
line([[0.25, 0.05], [0.25, 0.19]], { lw: 1, color: INK_FAINT, passes: 1 });
|
||||||
|
// 书签丝带
|
||||||
|
line([[0.0, 0.52], [0.06, 0.8]], { lw: 1.6, color: '#a03a2a' });
|
||||||
|
line([[0.06, 0.8], [0.0, 0.76], [0.09, 0.7]], { lw: 1.4, color: '#a03a2a', passes: 1 });
|
||||||
|
// 阴影排线
|
||||||
|
hatch([[-0.5, -0.2], [0.0, -0.3], [0.5, -0.2], [0.5, 0.42], [0.0, 0.52], [-0.5, 0.42]], { spacing: 0.045, angle: -45, color: INK_FAINT, lw: 1 });
|
||||||
},
|
},
|
||||||
|
|
||||||
bridge() {
|
bridge() {
|
||||||
line([[-0.7, 0.45], [0.7, 0.45]], { lw: 1.6, color: INK_SOFT, passes: 1 });
|
// 水面波纹
|
||||||
line([[-0.6, 0.6], [0.6, 0.6]], { lw: 1.2, color: INK_FAINT, passes: 1 });
|
line([[-0.72, 0.45], [0.72, 0.45]], { lw: 1.6, color: INK_SOFT, passes: 1 });
|
||||||
fill([[-0.5, 0.45], [-0.5, -0.05], [0.5, -0.05], [0.5, 0.45]], 'rgba(150,140,130,0.10)');
|
line([[-0.6, 0.56], [-0.3, 0.58], [0.0, 0.56], [0.3, 0.58], [0.6, 0.56]], { lw: 1.1, color: INK_FAINT, passes: 1, wobble: 0.01 });
|
||||||
line([[-0.5, 0.45], [-0.5, 0.05], [-0.35, -0.2], [0.0, -0.35], [0.35, -0.2], [0.5, 0.05], [0.5, 0.45]], { lw: 2.6 });
|
// 桥体拱形
|
||||||
line([[-0.55, 0.45], [-0.55, 0.28], [0.55, 0.28], [0.55, 0.45]], { lw: 1.6, color: INK_SOFT });
|
fill([[-0.5, 0.32], [-0.5, 0.06], [-0.34, -0.2], [0.0, -0.3], [0.34, -0.2], [0.5, 0.06], [0.5, 0.32]], 'rgba(150,140,130,0.12)');
|
||||||
for (let i = 0; i < 7; i++) {
|
line([[-0.5, 0.32], [-0.5, 0.06], [-0.34, -0.2], [0.0, -0.3], [0.34, -0.2], [0.5, 0.06], [0.5, 0.32]], { lw: 2.6 });
|
||||||
const x = -0.45 + i * 0.15;
|
// 拱券石缝
|
||||||
line([[x, 0.28], [x, 0.4]], { lw: 1.2, passes: 1, color: INK_SOFT });
|
for (let i = 1; i < 7; i++) {
|
||||||
|
const t = i / 7;
|
||||||
|
const x = -0.5 + t;
|
||||||
|
const y = 0.06 - Math.sin(Math.PI * t) * 0.36;
|
||||||
|
line([[x, y], [x, 0.32]], { lw: 1, color: INK_SOFT, passes: 1 });
|
||||||
}
|
}
|
||||||
line([[-0.3, 0.52], [0.0, 0.56], [0.3, 0.52]], { lw: 1, color: INK_FAINT, passes: 1 });
|
// 桥面与栏杆
|
||||||
|
line([[-0.62, 0.32], [0.62, 0.32]], { lw: 2.6 });
|
||||||
|
line([[-0.64, 0.32], [-0.64, 0.2], [0.64, 0.2], [0.64, 0.32]], { lw: 1.6, color: INK_SOFT, passes: 1 });
|
||||||
|
for (let i = 0; i <= 10; i++) {
|
||||||
|
const x = -0.55 + i * 0.11;
|
||||||
|
line([[x, 0.2], [x, 0.29]], { lw: 1.4, passes: 1 });
|
||||||
|
}
|
||||||
|
line([[-0.6, 0.29], [0.6, 0.29]], { lw: 1.2, passes: 1, color: INK_SOFT });
|
||||||
|
// 倒影
|
||||||
|
line([[-0.4, 0.48], [0.0, 0.52], [0.4, 0.48]], { lw: 1.1, color: INK_FAINT, passes: 1, dash: [6, 5] });
|
||||||
|
// 两岸
|
||||||
|
line([[-0.74, 0.45], [-0.74, 0.32]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
|
line([[0.74, 0.45], [0.74, 0.32]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
|
hatch([[-0.5, 0.32], [-0.5, 0.06], [-0.34, -0.2], [0.0, -0.3], [0.34, -0.2], [0.5, 0.06], [0.5, 0.32]], { spacing: 0.04, angle: -60, color: INK_FAINT, lw: 1 });
|
||||||
},
|
},
|
||||||
|
|
||||||
flag() {
|
flag() {
|
||||||
line([[-0.4, 0.6], [-0.4, -0.7]], { lw: 2.4 });
|
// 地面与旗杆
|
||||||
fill([[-0.4, -0.65], [-0.1, -0.6], [0.05, -0.45], [-0.25, -0.5]], 'rgba(160,58,42,0.16)');
|
line([[-0.62, 0.6], [0.62, 0.6]], { lw: 1.6, color: INK_FAINT });
|
||||||
line([[-0.4, -0.65], [-0.1, -0.6], [0.05, -0.45], [-0.25, -0.5], [-0.4, -0.65]], { lw: 2.2 });
|
line([[-0.42, 0.6], [-0.42, -0.74]], { lw: 2.6 });
|
||||||
line([[-0.32, -0.55], [0.0, -0.48]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
line([[-0.47, 0.6], [-0.47, -0.74]], { lw: 1.2, color: INK_SOFT, passes: 1 });
|
||||||
line([[-0.25, -0.5], [-0.15, -0.32]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
circle(-0.42, -0.77, 0.04, { lw: 1.8, passes: 1 });
|
||||||
line([[-0.6, 0.62], [0.6, 0.62]], { lw: 1.6, color: INK_FAINT });
|
// 飘动的旗面(S 形两褶)
|
||||||
for (let i = 0; i < 7; i++) {
|
fill([[-0.42, -0.7], [-0.1, -0.62], [0.18, -0.5], [0.04, -0.36], [-0.42, -0.42]], 'rgba(160,58,42,0.16)');
|
||||||
const a = -0.5 + i * 0.3;
|
line([[-0.42, -0.7], [-0.1, -0.62], [0.18, -0.5], [0.04, -0.36], [-0.42, -0.42], [-0.42, -0.7]], { lw: 2.4 });
|
||||||
line([[0.05 + Math.cos(a) * 0.3, -0.3 + Math.sin(a) * 0.2], [0.05 + Math.cos(a) * 0.42, -0.3 + Math.sin(a) * 0.3]], { lw: 1.2, color: INK_FAINT, passes: 1 });
|
// 褶皱与星徽
|
||||||
|
line([[-0.42, -0.56], [-0.08, -0.5], [0.1, -0.42]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
|
dot(-0.2, -0.55, 0.035, '#a03a2a');
|
||||||
|
// 底部垂穗
|
||||||
|
line([[0.04, -0.36], [0.06, -0.27]], { lw: 1.2, color: INK_SOFT, passes: 1 });
|
||||||
|
line([[0.18, -0.5], [0.24, -0.43]], { lw: 1.2, color: INK_SOFT, passes: 1 });
|
||||||
|
// 旗面阴影
|
||||||
|
hatch([[-0.42, -0.7], [-0.1, -0.62], [0.18, -0.5], [0.04, -0.36], [-0.42, -0.42]], { spacing: 0.04, angle: -40, color: INK_FAINT, lw: 1 });
|
||||||
|
// 光芒
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const a = -0.9 + i * 0.35;
|
||||||
|
line([[0.1 + Math.cos(a) * 0.28, -0.12 + Math.sin(a) * 0.18], [0.1 + Math.cos(a) * 0.4, -0.12 + Math.sin(a) * 0.26]], { lw: 1.2, color: INK_FAINT, passes: 1 });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
scale() {
|
scale() {
|
||||||
line([[0.0, 0.6], [0.0, -0.15]], { lw: 2.4 });
|
// 三级底座
|
||||||
line([[-0.55, -0.05], [0.55, -0.05]], { lw: 2.2 });
|
line([[-0.34, 0.6], [0.34, 0.6]], { lw: 2.6 });
|
||||||
circle(0.0, -0.1, 0.06, { lw: 1.8, passes: 1 });
|
line([[-0.26, 0.66], [0.26, 0.66]], { lw: 2.2 });
|
||||||
line([[-0.55, -0.05], [-0.55, 0.2]], { lw: 1.6, passes: 1 });
|
line([[-0.18, 0.72], [0.18, 0.72]], { lw: 1.8, color: INK_SOFT });
|
||||||
line([[0.55, -0.05], [0.55, 0.2]], { lw: 1.6, passes: 1 });
|
// 立柱与柱头
|
||||||
line([[-0.72, 0.2], [-0.38, 0.2]], { lw: 2 });
|
line([[-0.05, 0.6], [-0.05, -0.06]], { lw: 2.4 });
|
||||||
line([[0.38, 0.2], [0.72, 0.2]], { lw: 2 });
|
line([[0.05, 0.6], [0.05, -0.06]], { lw: 1.2, color: INK_SOFT, passes: 1 });
|
||||||
line([[-0.7, 0.2], [-0.72, 0.34], [-0.38, 0.34], [-0.4, 0.2]], { lw: 1.6, color: INK_SOFT, passes: 1 });
|
circle(0.0, -0.09, 0.06, { lw: 2, passes: 1 });
|
||||||
line([[0.38, 0.2], [0.4, 0.34], [0.7, 0.34], [0.72, 0.2]], { lw: 1.6, color: INK_SOFT, passes: 1 });
|
// 横梁与支点
|
||||||
line([[-0.3, 0.6], [0.3, 0.6]], { lw: 2.6 });
|
line([[-0.6, -0.02], [0.6, -0.02]], { lw: 2.4 });
|
||||||
line([[-0.2, 0.68], [0.2, 0.68]], { lw: 2 });
|
line([[-0.6, -0.1], [0.6, -0.1]], { lw: 1.2, color: INK_SOFT, passes: 1 });
|
||||||
|
circle(0.0, -0.05, 0.07, { lw: 2, passes: 1 });
|
||||||
|
// 吊链(两段折线)
|
||||||
|
line([[-0.6, -0.02], [-0.6, 0.2]], { lw: 1.6, passes: 1 });
|
||||||
|
line([[-0.6, 0.2], [-0.45, 0.28]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
|
line([[-0.45, 0.28], [-0.45, 0.32]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
|
line([[0.6, -0.02], [0.6, 0.2]], { lw: 1.6, passes: 1 });
|
||||||
|
line([[0.6, 0.2], [0.45, 0.28]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
|
line([[0.45, 0.28], [0.45, 0.32]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
|
// 左托盘
|
||||||
|
fill([[-0.78, 0.3], [-0.12, 0.3], [-0.2, 0.42], [-0.7, 0.42]], 'rgba(150,140,130,0.12)');
|
||||||
|
line([[-0.78, 0.3], [-0.12, 0.3]], { lw: 2.2 });
|
||||||
|
line([[-0.78, 0.3], [-0.7, 0.42], [-0.2, 0.42], [-0.12, 0.3]], { lw: 1.6, color: INK_SOFT, passes: 1 });
|
||||||
|
// 右托盘
|
||||||
|
fill([[0.12, 0.3], [0.78, 0.3], [0.7, 0.42], [0.2, 0.42]], 'rgba(150,140,130,0.12)');
|
||||||
|
line([[0.12, 0.3], [0.78, 0.3]], { lw: 2.2 });
|
||||||
|
line([[0.12, 0.3], [0.2, 0.42], [0.7, 0.42], [0.78, 0.3]], { lw: 1.6, color: INK_SOFT, passes: 1 });
|
||||||
|
// 托盘阴影
|
||||||
|
hatch([[-0.78, 0.3], [-0.12, 0.3], [-0.2, 0.42], [-0.7, 0.42]], { spacing: 0.04, angle: -30, color: INK_FAINT, lw: 1 });
|
||||||
|
hatch([[0.12, 0.3], [0.78, 0.3], [0.7, 0.42], [0.2, 0.42]], { spacing: 0.04, angle: -30, color: INK_FAINT, lw: 1 });
|
||||||
|
// 横梁装饰点
|
||||||
|
dot(-0.3, -0.05, 0.02, INK_SOFT);
|
||||||
|
dot(0.3, -0.05, 0.02, INK_SOFT);
|
||||||
},
|
},
|
||||||
|
|
||||||
sword() {
|
sword() {
|
||||||
line([[0.1, -0.6], [0.0, -0.05], [-0.02, 0.15]], { lw: 2.6 });
|
// 盾牌(背景)
|
||||||
line([[0.1, -0.6], [0.05, -0.05], [0.02, 0.15]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
fill([[-0.45, -0.2], [0.45, -0.2], [0.55, 0.15], [0.45, 0.5], [0.0, 0.68], [-0.45, 0.5], [-0.55, 0.15]], 'rgba(120,95,70,0.10)');
|
||||||
fill([[0.1, -0.6], [0.0, -0.05], [0.05, -0.05]], 'rgba(150,150,160,0.10)');
|
line([[-0.45, -0.2], [0.45, -0.2], [0.55, 0.15], [0.45, 0.5], [0.0, 0.68], [-0.45, 0.5], [-0.55, 0.15], [-0.45, -0.2]], { lw: 2.4 });
|
||||||
line([[-0.25, 0.05], [0.22, -0.02]], { lw: 3 });
|
// 盾面十字纹
|
||||||
line([[-0.02, 0.15], [0.05, 0.42]], { lw: 2.4 });
|
line([[0.0, -0.2], [0.0, 0.5]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
circle(0.02, 0.45, 0.05, { lw: 1.6, passes: 1 });
|
line([[-0.35, 0.15], [0.35, 0.15]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
line([[-0.5, 0.6], [0.5, 0.6]], { lw: 1.4, color: INK_FAINT, passes: 1 });
|
hatch([[-0.45, -0.2], [0.45, -0.2], [0.55, 0.15], [0.45, 0.5], [0.0, 0.68], [-0.45, 0.5], [-0.55, 0.15]], { spacing: 0.035, angle: 60, color: INK_FAINT, lw: 1 });
|
||||||
line([[0.15, -0.5], [0.28, -0.42]], { lw: 1.2, color: INK_FAINT, passes: 1 });
|
// 剑身(竖直插于盾前)
|
||||||
line([[0.18, -0.3], [0.32, -0.22]], { lw: 1, color: INK_FAINT, passes: 1 });
|
fill([[-0.1, -0.7], [0.1, -0.7], [0.1, 0.3], [-0.1, 0.3]], 'rgba(150,150,160,0.14)');
|
||||||
|
line([[-0.1, -0.7], [0.1, -0.7], [0.1, 0.3], [-0.1, 0.3], [-0.1, -0.7]], { lw: 2.6 });
|
||||||
|
line([[-0.1, -0.7], [0.0, -0.88], [0.1, -0.7]], { lw: 2.4 });
|
||||||
|
// 血槽与高光
|
||||||
|
line([[0.0, -0.6], [0.0, 0.2]], { lw: 1.2, color: INK_SOFT, passes: 1 });
|
||||||
|
line([[-0.04, -0.5], [-0.04, 0.1]], { lw: 1, color: INK_FAINT, passes: 1 });
|
||||||
|
// 护手
|
||||||
|
line([[-0.32, 0.3], [0.32, 0.3]], { lw: 3 });
|
||||||
|
// 柄与缠绕纹
|
||||||
|
fill([[-0.05, 0.3], [0.05, 0.3], [0.05, 0.52], [-0.05, 0.52]], 'rgba(90,65,45,0.12)');
|
||||||
|
line([[-0.05, 0.3], [0.05, 0.3], [0.05, 0.52], [-0.05, 0.52]], { lw: 2.2 });
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
const y = 0.34 + i * 0.05;
|
||||||
|
line([[-0.05, y], [0.05, y - 0.025]], { lw: 1, color: INK_SOFT, passes: 1 });
|
||||||
|
}
|
||||||
|
// 柄首
|
||||||
|
circle(0.0, 0.56, 0.05, { lw: 2, passes: 1 });
|
||||||
|
dot(0.0, 0.56, 0.02, '#a03a2a');
|
||||||
|
// 地面与星点
|
||||||
|
line([[-0.6, 0.72], [0.6, 0.72]], { lw: 1.4, color: INK_FAINT, passes: 1 });
|
||||||
|
dot(-0.62, -0.3, 0.02, INK_FAINT);
|
||||||
|
dot(0.62, -0.3, 0.02, INK_FAINT);
|
||||||
|
},
|
||||||
|
|
||||||
|
helmet() {
|
||||||
|
fill([[-0.4, 0.2], [0.4, 0.2], [0.35, -0.15], [0.2, -0.45], [-0.2, -0.45], [-0.35, -0.15]], 'rgba(150,150,160,0.12)');
|
||||||
|
line([[-0.4, 0.2], [0.4, 0.2], [0.35, -0.15], [0.2, -0.45], [-0.2, -0.45], [-0.35, -0.15], [-0.4, 0.2]], { lw: 2.6 });
|
||||||
|
circle(0.0, -0.45, 0.07, { lw: 2, passes: 1 });
|
||||||
|
line([[0.0, -0.5], [0.02, -0.78], [0.14, -0.74], [0.02, -0.68]], { lw: 1.8, passes: 1 });
|
||||||
|
line([[-0.4, 0.2], [-0.5, 0.5]], { lw: 2 });
|
||||||
|
line([[0.4, 0.2], [0.5, 0.5]], { lw: 2 });
|
||||||
|
line([[-0.5, 0.5], [-0.3, 0.5]], { lw: 1.6, color: INK_SOFT });
|
||||||
|
line([[0.5, 0.5], [0.3, 0.5]], { lw: 1.6, color: INK_SOFT });
|
||||||
|
line([[-0.26, 0.1], [-0.08, 0.16]], { lw: 1.8 });
|
||||||
|
line([[0.08, 0.16], [0.26, 0.1]], { lw: 1.8 });
|
||||||
|
hatch([[-0.4, 0.2], [0.4, 0.2], [0.35, -0.15], [0.2, -0.45], [-0.2, -0.45], [-0.35, -0.15]], { spacing: 0.04, angle: -30, color: INK_FAINT, lw: 1 });
|
||||||
|
},
|
||||||
|
|
||||||
|
cannon() {
|
||||||
|
line([[-0.7, 0.5], [0.7, 0.5]], { lw: 1.6, color: INK_FAINT });
|
||||||
|
fill([[-0.45, -0.05], [0.45, -0.05], [0.5, 0.2], [-0.5, 0.2]], 'rgba(90,70,50,0.12)');
|
||||||
|
line([[-0.45, -0.05], [0.45, -0.05], [0.5, 0.2], [-0.5, 0.2], [-0.45, -0.05]], { lw: 2.6 });
|
||||||
|
line([[-0.4, 0.05], [0.4, 0.05]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
|
circle(-0.38, 0.2, 0.22, { lw: 2.4 });
|
||||||
|
circle(-0.38, 0.2, 0.05, { lw: 1.6, passes: 1 });
|
||||||
|
for (let i = 0; i < 6; i++) { const a = (i / 6) * Math.PI * 2; line([[-0.38 + Math.cos(a) * 0.05, 0.2 + Math.sin(a) * 0.05], [-0.38 + Math.cos(a) * 0.22, 0.2 + Math.sin(a) * 0.22]], { lw: 1.2, passes: 1 }); }
|
||||||
|
line([[0.0, -0.05], [0.0, -0.35]], { lw: 2 });
|
||||||
|
line([[0.0, -0.35], [0.18, -0.45], [0.0, -0.4]], { lw: 1.6, passes: 1 });
|
||||||
|
circle(0.55, 0.35, 0.09, { lw: 1.6, passes: 1, fill: 'rgba(150,150,160,0.10)' });
|
||||||
|
hatch([[-0.45, -0.05], [0.45, -0.05], [0.5, 0.2], [-0.5, 0.2]], { spacing: 0.035, angle: -55, color: INK_FAINT, lw: 1 });
|
||||||
|
},
|
||||||
|
|
||||||
|
fortress() {
|
||||||
|
line([[-0.7, 0.55], [0.7, 0.55]], { lw: 1.6, color: INK_FAINT });
|
||||||
|
fill([[-0.55, 0.55], [-0.55, -0.2], [0.55, -0.2], [0.55, 0.55]], 'rgba(150,140,130,0.10)');
|
||||||
|
line([[-0.55, 0.55], [-0.55, -0.2], [0.55, -0.2], [0.55, 0.55]], { lw: 2.4 });
|
||||||
|
line([[-0.55, 0.3], [0.55, 0.3]], { lw: 1.6, color: INK_SOFT, passes: 1 });
|
||||||
|
for (let x = -0.55; x <= 0.55; x += 0.1375) { line([[x, 0.3], [x + 0.07, 0.3], [x + 0.07, 0.42], [x, 0.42]], { lw: 1.4, passes: 1 }); }
|
||||||
|
for (let x = -0.5; x <= 0.5; x += 0.2) { line([[x, 0.42], [x, 0.72]], { lw: 1.8 }); }
|
||||||
|
line([[-0.56, 0.72], [-0.4, 0.72]], { lw: 1.8 });
|
||||||
|
line([[0.4, 0.72], [0.56, 0.72]], { lw: 1.8 });
|
||||||
|
line([[-0.4, -0.2], [-0.32, -0.42], [0.32, -0.42], [0.4, -0.2]], { lw: 1.8 });
|
||||||
|
line([[0.0, -0.42], [0.0, -0.2]], { lw: 1.2, color: INK_SOFT, passes: 1 });
|
||||||
|
hatch([[-0.55, 0.55], [-0.55, 0.3], [0.55, 0.3], [0.55, 0.55]], { spacing: 0.04, angle: 45, color: INK_FAINT, lw: 1 });
|
||||||
|
},
|
||||||
|
|
||||||
|
bow() {
|
||||||
|
line([[-0.6, 0.6], [0.6, 0.6]], { lw: 1.2, color: INK_FAINT, passes: 1 });
|
||||||
|
line([[-0.55, -0.5], [-0.25, -0.1], [0.0, 0.1], [0.25, -0.1], [0.55, -0.5]], { lw: 2.6 });
|
||||||
|
line([[-0.55, -0.5], [0.55, -0.5]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
|
line([[0.0, 0.02], [0.5, -0.32]], { lw: 2 });
|
||||||
|
line([[0.5, -0.32], [0.62, -0.42]], { lw: 1.8, passes: 1 });
|
||||||
|
line([[0.5, -0.32], [0.4, -0.42], [0.5, -0.44]], { lw: 1.6, passes: 1 });
|
||||||
|
dot(0.55, -0.5, 0.03, INK_SOFT);
|
||||||
|
dot(-0.55, -0.5, 0.03, INK_SOFT);
|
||||||
|
},
|
||||||
|
|
||||||
|
shield() {
|
||||||
|
fill([[-0.5, -0.5], [0.5, -0.5], [0.55, -0.1], [0.35, 0.4], [0.0, 0.6], [-0.35, 0.4], [-0.55, -0.1]], 'rgba(150,140,130,0.10)');
|
||||||
|
line([[-0.5, -0.5], [0.5, -0.5], [0.55, -0.1], [0.35, 0.4], [0.0, 0.6], [-0.35, 0.4], [-0.55, -0.1], [-0.5, -0.5]], { lw: 2.6 });
|
||||||
|
line([[-0.5, -0.5], [0.5, -0.5]], { lw: 2 });
|
||||||
|
circle(0.0, -0.05, 0.16, { lw: 2, passes: 1 });
|
||||||
|
dot(0.0, -0.05, 0.05, '#a03a2a');
|
||||||
|
line([[0.0, 0.35], [0.0, 0.55]], { lw: 1.6, color: INK_SOFT, passes: 1 });
|
||||||
|
hatch([[-0.5, -0.5], [0.5, -0.5], [0.55, -0.1], [0.35, 0.4], [0.0, 0.6], [-0.35, 0.4], [-0.55, -0.1]], { spacing: 0.04, angle: -45, color: INK_FAINT, lw: 1 });
|
||||||
|
},
|
||||||
|
|
||||||
|
throne() {
|
||||||
|
line([[-0.6, 0.55], [0.6, 0.55]], { lw: 1.6, color: INK_FAINT });
|
||||||
|
fill([[-0.4, 0.5], [-0.4, -0.4], [0.4, -0.4], [0.4, 0.5]], 'rgba(160,120,50,0.10)');
|
||||||
|
line([[-0.4, 0.5], [-0.4, -0.4], [0.4, -0.4], [0.4, 0.5]], { lw: 2.6 });
|
||||||
|
line([[-0.34, 0.5], [-0.28, 0.72], [0.28, 0.72], [0.34, 0.5]], { lw: 2.2 });
|
||||||
|
circle(0.0, 0.66, 0.09, { lw: 1.8, passes: 1 });
|
||||||
|
line([[-0.2, -0.4], [-0.2, -0.62]], { lw: 2 });
|
||||||
|
line([[0.2, -0.4], [0.2, -0.62]], { lw: 2 });
|
||||||
|
line([[-0.28, 0.1], [0.28, 0.1]], { lw: 1.6, color: INK_SOFT, passes: 1 });
|
||||||
|
hatch([[-0.4, 0.5], [-0.4, -0.4], [0.4, -0.4], [0.4, 0.5]], { spacing: 0.04, angle: -60, color: INK_FAINT, lw: 1 });
|
||||||
|
},
|
||||||
|
|
||||||
|
scepter() {
|
||||||
|
line([[-0.05, 0.6], [0.0, -0.6]], { lw: 2.6 });
|
||||||
|
line([[0.05, 0.6], [0.08, -0.6]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
|
circle(0.0, 0.62, 0.12, { lw: 2.2 });
|
||||||
|
line([[-0.08, 0.7], [-0.16, 0.78], [0.0, 0.82], [0.16, 0.78], [0.08, 0.7]], { lw: 1.8 });
|
||||||
|
dot(0.0, 0.78, 0.05, '#a03a2a');
|
||||||
|
circle(0.03, -0.62, 0.07, { lw: 1.8, passes: 1 });
|
||||||
|
for (let i = 0; i < 5; i++) { const y = -0.4 + i * 0.2; line([[-0.05, y], [0.08, y - 0.04]], { lw: 1.2, color: INK_SOFT, passes: 1 }); }
|
||||||
|
line([[-0.4, 0.75], [0.4, 0.75]], { lw: 1.2, color: INK_FAINT, passes: 1 });
|
||||||
|
},
|
||||||
|
|
||||||
|
palace() {
|
||||||
|
line([[-0.7, 0.5], [0.7, 0.5]], { lw: 1.6, color: INK_FAINT });
|
||||||
|
fill([[-0.55, 0.5], [-0.55, -0.25], [0.55, -0.25], [0.55, 0.5]], 'rgba(160,140,110,0.10)');
|
||||||
|
line([[-0.55, 0.5], [-0.55, -0.25], [0.55, -0.25], [0.55, 0.5]], { lw: 2.4 });
|
||||||
|
line([[-0.55, 0.12], [0.55, 0.12]], { lw: 1.6, color: INK_SOFT, passes: 1 });
|
||||||
|
for (let x = -0.5; x <= 0.5; x += 0.2) { line([[x, -0.25], [x, 0.12]], { lw: 1.8 }); }
|
||||||
|
line([[-0.62, 0.12], [-0.55, 0.4], [-0.4, 0.12]], { lw: 2 });
|
||||||
|
line([[-0.3, 0.12], [-0.18, 0.4], [0.0, 0.12]], { lw: 2 });
|
||||||
|
line([[0.1, 0.12], [0.28, 0.42], [0.42, 0.12]], { lw: 2 });
|
||||||
|
line([[0.5, 0.12], [0.62, 0.4], [0.7, 0.12]], { lw: 2 });
|
||||||
|
line([[-0.32, -0.25], [-0.28, -0.5], [0.28, -0.5], [0.32, -0.25]], { lw: 1.8 });
|
||||||
|
hatch([[-0.55, 0.5], [-0.55, 0.12], [0.55, 0.12], [0.55, 0.5]], { spacing: 0.04, angle: 45, color: INK_FAINT, lw: 1 });
|
||||||
|
},
|
||||||
|
|
||||||
|
laurel() {
|
||||||
|
circle(0, 0, 0.55, { lw: 2.4 });
|
||||||
|
circle(0, 0, 0.42, { lw: 1.2, color: INK_FAINT, passes: 1 });
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
const a = (i / 16) * Math.PI * 2;
|
||||||
|
const x = Math.cos(a), y = Math.sin(a);
|
||||||
|
const px = x * 0.48, py = y * 0.48;
|
||||||
|
line([[px, py], [px - y * 0.1 - x * 0.06, py + x * 0.1 - y * 0.06]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
|
}
|
||||||
|
dot(0, 0.42, 0.03, '#a03a2a');
|
||||||
|
},
|
||||||
|
|
||||||
|
gavel() {
|
||||||
|
line([[-0.6, 0.4], [0.6, 0.4]], { lw: 1.4, color: INK_FAINT, passes: 1 });
|
||||||
|
fill([[-0.4, 0.1], [0.4, 0.1], [0.42, 0.28], [-0.42, 0.28]], 'rgba(120,90,60,0.14)');
|
||||||
|
line([[-0.4, 0.1], [0.4, 0.1], [0.42, 0.28], [-0.42, 0.28], [-0.4, 0.1]], { lw: 2.6 });
|
||||||
|
line([[-0.32, 0.16], [0.32, 0.16]], { lw: 1.2, color: INK_SOFT, passes: 1 });
|
||||||
|
line([[-0.05, 0.28], [0.0, 0.58]], { lw: 2.2 });
|
||||||
|
circle(0.0, 0.62, 0.05, { lw: 1.6, passes: 1 });
|
||||||
|
fill([[-0.6, 0.4], [0.6, 0.4], [0.55, 0.5], [-0.55, 0.5]], 'rgba(150,140,130,0.10)');
|
||||||
|
line([[-0.6, 0.4], [0.6, 0.4], [0.55, 0.5], [-0.55, 0.5], [-0.6, 0.4]], { lw: 1.8 });
|
||||||
|
hatch([[-0.4, 0.1], [0.4, 0.1], [0.42, 0.28], [-0.42, 0.28]], { spacing: 0.04, angle: -30, color: INK_FAINT, lw: 1 });
|
||||||
|
},
|
||||||
|
|
||||||
|
columns() {
|
||||||
|
line([[-0.7, 0.55], [0.7, 0.55]], { lw: 2 });
|
||||||
|
line([[-0.66, 0.62], [0.66, 0.62]], { lw: 1.8 });
|
||||||
|
line([[-0.7, -0.5], [0.7, -0.5]], { lw: 2 });
|
||||||
|
line([[-0.66, -0.57], [0.66, -0.57]], { lw: 1.8 });
|
||||||
|
for (let x = -0.55; x <= 0.55; x += 0.22) {
|
||||||
|
line([[x, 0.55], [x, -0.5]], { lw: 2 });
|
||||||
|
line([[x - 0.02, 0.55], [x - 0.02, -0.5]], { lw: 1, color: INK_SOFT, passes: 1 });
|
||||||
|
line([[x - 0.09, 0.42], [x + 0.09, 0.42]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
|
line([[x - 0.09, -0.38], [x + 0.09, -0.38]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
document() {
|
||||||
|
fill([[-0.45, -0.55], [0.45, -0.55], [0.5, -0.2], [0.45, 0.5], [-0.45, 0.5], [-0.5, -0.2]], 'rgba(210,190,150,0.20)');
|
||||||
|
line([[-0.45, -0.55], [0.45, -0.55], [0.5, -0.2], [0.45, 0.5], [-0.45, 0.5], [-0.5, -0.2], [-0.45, -0.55]], { lw: 2.4 });
|
||||||
|
for (let i = 0; i < 6; i++) { const y = -0.4 + i * 0.15; line([[-0.34, y], [0.34, y]], { lw: 1.3, color: INK_SOFT, passes: 1, wobble: 0.004 }); }
|
||||||
|
line([[0.05, 0.0], [0.2, 0.1], [0.05, 0.2], [0.05, 0.0]], { lw: 1.6, passes: 1 });
|
||||||
|
dot(0.05, 0.1, 0.03, '#a03a2a');
|
||||||
|
line([[-0.05, 0.5], [0.0, 0.72]], { lw: 1.6, color: '#a03a2a' });
|
||||||
|
line([[0.0, 0.72], [-0.06, 0.68], [0.04, 0.64]], { lw: 1.4, color: '#a03a2a', passes: 1 });
|
||||||
|
hatch([[-0.45, -0.55], [0.45, -0.55], [0.5, -0.2], [0.45, 0.5], [-0.45, 0.5], [-0.5, -0.2]], { spacing: 0.045, angle: -40, color: INK_FAINT, lw: 1 });
|
||||||
|
},
|
||||||
|
|
||||||
|
gear() {
|
||||||
|
circle(0, 0, 0.5, { lw: 2.6 });
|
||||||
|
circle(0, 0, 0.32, { lw: 2 });
|
||||||
|
circle(0, 0, 0.12, { lw: 1.8, passes: 1 });
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
const a = (i / 12) * Math.PI * 2;
|
||||||
|
const x1 = Math.cos(a) * 0.5, y1 = Math.sin(a) * 0.5;
|
||||||
|
const x2 = Math.cos(a) * 0.62, y2 = Math.sin(a) * 0.62;
|
||||||
|
line([[x1, y1], [x2, y2]], { lw: 1.8, passes: 1 });
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 6; i++) { const a = (i / 6) * Math.PI * 2 + 0.3; line([[Math.cos(a) * 0.12, Math.sin(a) * 0.12], [Math.cos(a) * 0.32, Math.sin(a) * 0.32]], { lw: 1.2, color: INK_SOFT, passes: 1 }); }
|
||||||
|
hatch([[Math.cos(0) * 0.32, Math.sin(0) * 0.32], [Math.cos(2) * 0.32, Math.sin(2) * 0.32], [Math.cos(4) * 0.32, Math.sin(4) * 0.32]], { spacing: 0.03, angle: -45, color: INK_FAINT, lw: 1 });
|
||||||
|
},
|
||||||
|
|
||||||
|
anvil() {
|
||||||
|
line([[-0.6, 0.5], [0.6, 0.5]], { lw: 1.6, color: INK_FAINT });
|
||||||
|
fill([[-0.4, 0.5], [-0.4, 0.15], [-0.25, -0.05], [0.25, -0.05], [0.4, 0.15], [0.4, 0.5]], 'rgba(90,70,50,0.14)');
|
||||||
|
line([[-0.4, 0.5], [-0.4, 0.15], [-0.25, -0.05], [0.25, -0.05], [0.4, 0.15], [0.4, 0.5]], { lw: 2.4 });
|
||||||
|
line([[-0.28, 0.5], [-0.28, 0.2], [0.28, 0.2], [0.28, 0.5]], { lw: 2 });
|
||||||
|
line([[-0.2, 0.5], [-0.2, 0.28], [0.2, 0.28], [0.2, 0.5]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
|
line([[-0.05, 0.28], [0.0, 0.72]], { lw: 2.2 });
|
||||||
|
line([[0.0, 0.72], [0.1, 0.62], [-0.05, 0.55]], { lw: 1.6, passes: 1 });
|
||||||
|
hatch([[-0.4, 0.5], [-0.4, 0.15], [-0.25, -0.05], [0.25, -0.05], [0.4, 0.15], [0.4, 0.5]], { spacing: 0.04, angle: 45, color: INK_FAINT, lw: 1 });
|
||||||
|
},
|
||||||
|
|
||||||
|
locomotive() {
|
||||||
|
line([[-0.7, 0.55], [0.7, 0.55]], { lw: 1.6, color: INK_FAINT });
|
||||||
|
fill([[-0.6, 0.1], [0.35, 0.1], [0.4, 0.35], [-0.55, 0.35]], 'rgba(90,70,50,0.14)');
|
||||||
|
line([[-0.6, 0.1], [0.35, 0.1], [0.4, 0.35], [-0.55, 0.35], [-0.6, 0.1]], { lw: 2.4 });
|
||||||
|
fill([[-0.3, 0.1], [-0.1, 0.1], [-0.1, -0.2], [-0.3, -0.2]], 'rgba(120,90,60,0.12)');
|
||||||
|
line([[-0.3, 0.1], [-0.1, 0.1], [-0.1, -0.2], [-0.3, -0.2], [-0.3, 0.1]], { lw: 2 });
|
||||||
|
line([[-0.2, -0.2], [-0.2, -0.42]], { lw: 1.8 });
|
||||||
|
circle(-0.3, 0.4, 0.08, { lw: 1.6, passes: 1 });
|
||||||
|
circle(0.15, 0.4, 0.08, { lw: 1.6, passes: 1 });
|
||||||
|
circle(0.55, 0.35, 0.28, { lw: 2.2 });
|
||||||
|
circle(0.55, 0.35, 0.06, { lw: 1.4, passes: 1 });
|
||||||
|
line([[-0.5, 0.1], [-0.5, 0.0]], { lw: 2 });
|
||||||
|
line([[0.28, 0.1], [0.28, 0.0]], { lw: 2 });
|
||||||
|
line([[-0.1, 0.28], [-0.02, 0.34], [-0.1, 0.36], [-0.16, 0.28]], { lw: 1.4, passes: 1 });
|
||||||
|
hatch([[-0.6, 0.1], [0.35, 0.1], [0.4, 0.35], [-0.55, 0.35]], { spacing: 0.04, angle: -55, color: INK_FAINT, lw: 1 });
|
||||||
|
},
|
||||||
|
|
||||||
|
factory() {
|
||||||
|
line([[-0.7, 0.5], [0.7, 0.5]], { lw: 1.6, color: INK_FAINT });
|
||||||
|
fill([[-0.55, 0.5], [-0.55, -0.3], [0.55, -0.3], [0.55, 0.5]], 'rgba(120,110,100,0.10)');
|
||||||
|
line([[-0.55, 0.5], [-0.55, -0.3], [0.55, -0.3], [0.55, 0.5]], { lw: 2.4 });
|
||||||
|
line([[-0.5, 0.5], [-0.5, -0.15]], { lw: 2 });
|
||||||
|
line([[-0.15, 0.5], [-0.15, -0.2]], { lw: 2 });
|
||||||
|
line([[0.2, 0.5], [0.2, -0.15]], { lw: 2 });
|
||||||
|
for (const x of [-0.4, 0.05, 0.35]) { line([[x, 0.1], [x - 0.1, 0.1], [x - 0.06, -0.3], [x + 0.06, -0.3], [x + 0.1, 0.1]], { lw: 1.6, passes: 1 }); }
|
||||||
|
for (let i = 0; i < 5; i++) { const a = -0.35 + i * 0.2; line([[a + 0.02, 0.3], [a, 0.12], [a + 0.06, -0.02], [a, -0.16]], { lw: 1.2, color: INK_SOFT, passes: 1 }); }
|
||||||
|
hatch([[-0.55, 0.5], [-0.55, -0.3], [0.55, -0.3], [0.55, 0.5]], { spacing: 0.04, angle: 45, color: INK_FAINT, lw: 1 });
|
||||||
|
},
|
||||||
|
|
||||||
|
microscope() {
|
||||||
|
line([[-0.5, 0.6], [0.5, 0.6]], { lw: 1.6, color: INK_FAINT });
|
||||||
|
line([[-0.15, -0.35], [-0.15, -0.2], [-0.5, -0.2]], { lw: 2.2 });
|
||||||
|
line([[-0.35, -0.35], [-0.35, 0.0]], { lw: 2.2 });
|
||||||
|
line([[-0.55, 0.0], [-0.35, 0.0]], { lw: 2.2 });
|
||||||
|
line([[-0.6, 0.0], [-0.6, -0.55]], { lw: 2.2 });
|
||||||
|
line([[-0.6, -0.55], [-0.05, -0.55]], { lw: 2.2 });
|
||||||
|
line([[-0.05, -0.55], [0.15, -0.55]], { lw: 2.2 });
|
||||||
|
circle(-0.12, -0.05, 0.18, { lw: 2 });
|
||||||
|
line([[-0.12, -0.2], [0.2, -0.5]], { lw: 1.6, color: INK_SOFT, passes: 1 });
|
||||||
|
fill([[-0.18, 0.3], [-0.06, 0.3], [-0.06, 0.1], [-0.18, 0.1]], 'rgba(150,150,160,0.10)');
|
||||||
|
line([[-0.18, 0.3], [-0.06, 0.3], [-0.06, 0.1], [-0.18, 0.1], [-0.18, 0.3]], { lw: 1.6, passes: 1 });
|
||||||
|
dot(-0.12, 0.2, 0.03, '#a03a2a');
|
||||||
|
},
|
||||||
|
|
||||||
|
telescope() {
|
||||||
|
line([[-0.4, 0.5], [0.0, 0.5], [0.0, 0.6]], { lw: 1.6, color: INK_FAINT });
|
||||||
|
fill([[-0.3, -0.3], [0.3, -0.3], [0.38, 0.05], [-0.38, 0.05]], 'rgba(150,150,160,0.12)');
|
||||||
|
line([[-0.3, -0.3], [0.3, -0.3], [0.38, 0.05], [-0.38, 0.05], [-0.3, -0.3]], { lw: 2.4 });
|
||||||
|
line([[-0.42, -0.28], [-0.32, -0.5], [-0.5, -0.5]], { lw: 2 });
|
||||||
|
line([[-0.42, -0.28], [-0.3, -0.5], [-0.5, -0.5]], { lw: 2 });
|
||||||
|
circle(0.32, -0.28, 0.1, { lw: 1.8, passes: 1 });
|
||||||
|
circle(-0.32, -0.28, 0.1, { lw: 1.8, passes: 1 });
|
||||||
|
line([[-0.35, -0.1], [0.35, -0.1]], { lw: 1.2, color: INK_SOFT, passes: 1 });
|
||||||
|
line([[0.0, -0.5], [0.0, 0.5]], { lw: 2 });
|
||||||
|
line([[-0.5, -0.5], [0.5, -0.5]], { lw: 2 });
|
||||||
|
line([[-0.4, -0.5], [-0.4, -0.7]], { lw: 1.8 });
|
||||||
|
line([[0.4, -0.5], [0.4, -0.7]], { lw: 1.8 });
|
||||||
|
hatch([[-0.3, -0.3], [0.3, -0.3], [0.38, 0.05], [-0.38, 0.05]], { spacing: 0.04, angle: -50, color: INK_FAINT, lw: 1 });
|
||||||
|
},
|
||||||
|
|
||||||
|
flask() {
|
||||||
|
line([[-0.5, 0.55], [0.5, 0.55]], { lw: 1.4, color: INK_FAINT, passes: 1 });
|
||||||
|
line([[-0.1, 0.1], [-0.1, -0.3]], { lw: 2 });
|
||||||
|
line([[0.1, 0.1], [0.1, -0.3]], { lw: 2 });
|
||||||
|
fill([[-0.1, -0.3], [0.1, -0.3], [0.32, 0.28], [0.12, 0.42], [-0.12, 0.42], [-0.32, 0.28]], 'rgba(150,150,160,0.10)');
|
||||||
|
line([[-0.1, -0.3], [0.1, -0.3], [0.32, 0.28], [0.12, 0.42], [-0.12, 0.42], [-0.32, 0.28], [-0.1, -0.3]], { lw: 2.2 });
|
||||||
|
line([[-0.16, 0.34], [0.16, 0.34]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
|
line([[0.05, -0.45], [0.15, -0.7]], { lw: 1.2, color: INK_FAINT, passes: 1, dash: [6, 5] });
|
||||||
|
dot(0.2, -0.74, 0.02, INK_FAINT);
|
||||||
|
hatch([[-0.1, -0.3], [0.1, -0.3], [0.32, 0.28], [0.12, 0.42], [-0.12, 0.42], [-0.32, 0.28]], { spacing: 0.04, angle: 30, color: INK_FAINT, lw: 1 });
|
||||||
|
},
|
||||||
|
|
||||||
|
lightbulb() {
|
||||||
|
fill([[0, -0.6], [0.28, -0.4], [0.34, -0.1], [0.22, 0.15], [0.0, 0.3], [-0.22, 0.15], [-0.34, -0.1], [-0.28, -0.4]], 'rgba(200,170,80,0.14)');
|
||||||
|
line([[0, -0.6], [0.28, -0.4], [0.34, -0.1], [0.22, 0.15], [0.0, 0.3], [-0.22, 0.15], [-0.34, -0.1], [-0.28, -0.4], [0, -0.6]], { lw: 2.4 });
|
||||||
|
line([[0, -0.6], [0.02, -0.85]], { lw: 1.8 });
|
||||||
|
line([[-0.16, 0.12], [0.16, 0.12]], { lw: 1.6, color: INK_SOFT, passes: 1 });
|
||||||
|
line([[-0.1, 0.24], [0.1, 0.24]], { lw: 1.6, color: INK_SOFT, passes: 1 });
|
||||||
|
line([[-0.2, 0.36], [0.2, 0.36]], { lw: 1.6, color: INK_SOFT, passes: 1 });
|
||||||
|
for (let i = 0; i < 6; i++) { const a = -1.2 + i * 0.4; line([[Math.cos(a) * 0.4, Math.sin(a) * 0.4], [Math.cos(a) * 0.52, Math.sin(a) * 0.52]], { lw: 1.2, color: INK_FAINT, passes: 1 }); }
|
||||||
|
},
|
||||||
|
|
||||||
|
dinosaur() {
|
||||||
|
line([[-0.6, 0.6], [0.6, 0.6]], { lw: 1.4, color: INK_FAINT, passes: 1 });
|
||||||
|
line([[-0.55, 0.35], [-0.45, 0.1], [-0.3, -0.1], [-0.1, -0.2], [0.1, -0.2], [0.3, -0.1], [0.42, 0.0], [0.5, 0.2], [0.52, 0.4]], { lw: 2.4 });
|
||||||
|
line([[-0.45, 0.1], [-0.6, 0.0], [-0.65, 0.2]], { lw: 2 });
|
||||||
|
line([[0.42, 0.0], [0.55, -0.1], [0.58, 0.1]], { lw: 2 });
|
||||||
|
line([[-0.3, -0.1], [-0.32, 0.3]], { lw: 1.6, color: INK_SOFT, passes: 1 });
|
||||||
|
line([[-0.1, -0.2], [-0.1, 0.25]], { lw: 1.6, color: INK_SOFT, passes: 1 });
|
||||||
|
line([[0.1, -0.2], [0.12, 0.25]], { lw: 1.6, color: INK_SOFT, passes: 1 });
|
||||||
|
line([[0.3, -0.1], [0.32, 0.25]], { lw: 1.6, color: INK_SOFT, passes: 1 });
|
||||||
|
line([[-0.15, -0.16], [0.05, -0.08], [0.15, -0.16]], { lw: 1.4, passes: 1 });
|
||||||
|
line([[0.45, 0.3], [0.35, 0.42], [0.5, 0.5]], { lw: 1.6, color: INK_SOFT, passes: 1 });
|
||||||
|
},
|
||||||
|
|
||||||
|
flower() {
|
||||||
|
circle(0, 0, 0.12, { lw: 2, passes: 1 });
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const a = (i / 6) * Math.PI * 2 + 0.3;
|
||||||
|
const x = Math.cos(a) * 0.24, y = Math.sin(a) * 0.24;
|
||||||
|
fill([[x - 0.05, y - 0.05], [x + 0.16, y - 0.02], [x + 0.2, y + 0.08], [x + 0.02, y + 0.18], [x - 0.12, y + 0.14], [x - 0.1, y + 0.02]], 'rgba(150,60,40,0.10)');
|
||||||
|
line([[x - 0.05, y - 0.05], [x + 0.16, y - 0.02], [x + 0.2, y + 0.08], [x + 0.02, y + 0.18], [x - 0.12, y + 0.14], [x - 0.1, y + 0.02], [x - 0.05, y - 0.05]], { lw: 1.8 });
|
||||||
|
}
|
||||||
|
line([[0.0, 0.1], [0.0, 0.75]], { lw: 2 });
|
||||||
|
fill([[0.0, 0.4], [0.18, 0.5], [0.1, 0.62]], 'rgba(60,90,50,0.16)');
|
||||||
|
line([[0.0, 0.4], [0.18, 0.5], [0.1, 0.62], [0.0, 0.4]], { lw: 1.6 });
|
||||||
|
dot(0.0, 0.0, 0.04, '#c9a227');
|
||||||
|
},
|
||||||
|
|
||||||
|
mountain() {
|
||||||
|
line([[-0.7, 0.45], [0.7, 0.45]], { lw: 1.8 });
|
||||||
|
fill([[-0.6, 0.45], [-0.15, -0.5], [0.1, -0.2], [0.6, 0.45]], 'rgba(150,140,130,0.10)');
|
||||||
|
line([[-0.6, 0.45], [-0.15, -0.5], [0.1, -0.2], [0.6, 0.45]], { lw: 2.4 });
|
||||||
|
line([[-0.15, -0.5], [-0.15, -0.6]], { lw: 2 });
|
||||||
|
line([[-0.6, 0.45], [-0.35, 0.05], [-0.15, -0.5]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
|
line([[-0.15, -0.5], [0.0, -0.15], [0.1, -0.2]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
|
line([[0.1, -0.2], [0.3, 0.05], [0.6, 0.45]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
|
circle(0.55, -0.5, 0.14, { lw: 2, passes: 1 });
|
||||||
|
line([[-0.55, 0.6], [-0.1, 0.6]], { lw: 1.2, color: INK_FAINT, passes: 1 });
|
||||||
|
},
|
||||||
|
|
||||||
|
mask() {
|
||||||
|
circle(0, 0, 0.6, { lw: 2.4 });
|
||||||
|
circle(-0.14, -0.1, 0.12, { lw: 1.8, passes: 1 });
|
||||||
|
circle(0.14, -0.1, 0.12, { lw: 1.8, passes: 1 });
|
||||||
|
line([[-0.32, -0.3], [-0.05, -0.18], [-0.05, -0.5], [-0.18, -0.5], [-0.18, -0.18]], { lw: 1.8 });
|
||||||
|
line([[0.32, -0.3], [0.05, -0.18], [0.05, -0.5], [0.18, -0.5], [0.18, -0.18]], { lw: 1.8 });
|
||||||
|
line([[-0.2, 0.25], [0.2, 0.25]], { lw: 2, passes: 1 });
|
||||||
|
line([[-0.1, 0.25], [0.0, 0.4], [0.1, 0.25]], { lw: 1.8, passes: 1 });
|
||||||
|
line([[-0.3, 0.42], [-0.5, 0.5]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
|
line([[0.3, 0.42], [0.5, 0.5]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
|
hatch([[Math.cos(0.8) * 0.6, Math.sin(0.8) * 0.6], [Math.cos(2.3) * 0.6, Math.sin(2.3) * 0.6], [Math.cos(4) * 0.6, Math.sin(4) * 0.6]], { spacing: 0.04, angle: 45, color: INK_FAINT, lw: 1 });
|
||||||
|
},
|
||||||
|
|
||||||
|
lyre() {
|
||||||
|
line([[-0.5, 0.5], [-0.3, -0.1], [-0.1, -0.5]], { lw: 2.2 });
|
||||||
|
line([[0.5, 0.5], [0.3, -0.1], [0.1, -0.5]], { lw: 2.2 });
|
||||||
|
line([[-0.5, 0.5], [0.5, 0.5]], { lw: 2.2 });
|
||||||
|
fill([[-0.3, -0.1], [0.3, -0.1], [0.15, 0.35], [-0.15, 0.35]], 'rgba(200,180,140,0.14)');
|
||||||
|
line([[-0.3, -0.1], [0.3, -0.1], [0.15, 0.35], [-0.15, 0.35], [-0.3, -0.1]], { lw: 1.8 });
|
||||||
|
for (let i = 0; i < 4; i++) { const y = -0.0 + i * 0.1; line([[-0.28 + i * 0.02, y], [0.28 - i * 0.02, y]], { lw: 1.2, color: INK_SOFT, passes: 1 }); }
|
||||||
|
line([[-0.1, -0.5], [0.0, -0.6], [0.1, -0.5]], { lw: 1.8 });
|
||||||
|
},
|
||||||
|
|
||||||
|
coin() {
|
||||||
|
circle(0, 0, 0.6, { lw: 2.6 });
|
||||||
|
circle(0, 0, 0.5, { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
|
circle(0, 0, 0.32, { lw: 1.8, passes: 1 });
|
||||||
|
// 侧面像(简化)
|
||||||
|
fill([[0.0, -0.2], [0.02, -0.02], [0.06, 0.1], [0.02, 0.2], [-0.08, 0.22], [-0.1, 0.1], [-0.08, -0.02], [-0.02, -0.2]], 'rgba(150,140,130,0.10)');
|
||||||
|
line([[0.0, -0.2], [0.02, -0.02], [0.06, 0.1], [0.02, 0.2], [-0.08, 0.22], [-0.1, 0.1], [-0.08, -0.02], [-0.02, -0.2], [0.0, -0.2]], { lw: 1.8 });
|
||||||
|
line([[-0.45, 0.05], [0.45, 0.05]], { lw: 1.2, color: INK_FAINT, passes: 1 });
|
||||||
|
line([[0.0, -0.5], [0.0, 0.5]], { lw: 1.2, color: INK_FAINT, passes: 1 });
|
||||||
|
},
|
||||||
|
|
||||||
|
key() {
|
||||||
|
circle(0.5, 0.2, 0.22, { lw: 2.4 });
|
||||||
|
circle(0.5, 0.2, 0.1, { lw: 1.6, passes: 1 });
|
||||||
|
line([[-0.55, -0.1], [0.28, 0.1]], { lw: 2.6 });
|
||||||
|
line([[0.28, 0.1], [0.5, 0.16]], { lw: 2.4 });
|
||||||
|
line([[-0.55, -0.1], [-0.55, 0.1]], { lw: 1.8 });
|
||||||
|
line([[-0.4, -0.05], [-0.4, 0.05]], { lw: 1.8 });
|
||||||
|
line([[-0.25, -0.02], [-0.25, 0.05]], { lw: 1.8 });
|
||||||
|
dot(-0.55, 0.0, 0.03, INK_SOFT);
|
||||||
|
},
|
||||||
|
|
||||||
|
hourglass() {
|
||||||
|
line([[-0.4, -0.6], [0.4, -0.6]], { lw: 2 });
|
||||||
|
line([[-0.4, 0.6], [0.4, 0.6]], { lw: 2 });
|
||||||
|
line([[-0.4, -0.6], [-0.4, 0.6]], { lw: 2.2 });
|
||||||
|
line([[0.4, -0.6], [0.4, 0.6]], { lw: 2.2 });
|
||||||
|
line([[-0.4, -0.5], [-0.1, 0.0], [-0.4, 0.5]], { lw: 2.2 });
|
||||||
|
line([[0.4, -0.5], [0.1, 0.0], [0.4, 0.5]], { lw: 2.2 });
|
||||||
|
fill([[-0.1, -0.1], [0.1, -0.1], [0.0, 0.3], [-0.1, 0.3]], 'rgba(160,120,50,0.14)');
|
||||||
|
line([[-0.1, -0.1], [0.1, -0.1], [0.1, 0.3], [-0.1, 0.3], [-0.1, -0.1]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
|
for (let i = 0; i < 5; i++) { dot(-0.03 + (i % 2) * 0.05, -0.05 + i * 0.07, 0.015, INK_SOFT); }
|
||||||
|
},
|
||||||
|
|
||||||
|
compass() {
|
||||||
|
circle(0, 0, 0.6, { lw: 2.6 });
|
||||||
|
circle(0, 0, 0.5, { lw: 1.2, color: INK_FAINT, passes: 1 });
|
||||||
|
line([[0, -0.5], [0, 0.5]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
|
line([[-0.5, 0], [0.5, 0]], { lw: 1.4, color: INK_SOFT, passes: 1 });
|
||||||
|
fill([[0, 0.35], [0.35, 0], [0, -0.2]], 'rgba(150,60,40,0.12)');
|
||||||
|
fill([[0, -0.35], [-0.35, 0], [0, 0.2]], 'rgba(150,150,160,0.12)');
|
||||||
|
line([[0, 0.35], [0.35, 0], [0, -0.2], [0, 0.35]], { lw: 1.8 });
|
||||||
|
line([[0, -0.35], [-0.35, 0], [0, 0.2], [0, -0.35]], { lw: 1.8 });
|
||||||
|
dot(0, 0, 0.05, '#a03a2a');
|
||||||
|
dot(0, 0.5, 0.03, INK_SOFT);
|
||||||
|
dot(0, -0.5, 0.03, INK_SOFT);
|
||||||
|
},
|
||||||
|
|
||||||
|
anchor() {
|
||||||
|
circle(0, -0.5, 0.1, { lw: 2.4, passes: 1 });
|
||||||
|
line([[0, -0.4], [0, 0.5]], { lw: 2.6 });
|
||||||
|
line([[-0.45, -0.2], [0.45, -0.2]], { lw: 2.4 });
|
||||||
|
line([[-0.45, -0.2], [-0.45, 0.1]], { lw: 2.2 });
|
||||||
|
line([[0.45, -0.2], [0.45, 0.1]], { lw: 2.2 });
|
||||||
|
line([[-0.5, 0.35], [0.0, 0.5], [0.5, 0.35]], { lw: 2.2 });
|
||||||
|
line([[-0.45, 0.1], [-0.62, 0.42]], { lw: 2 });
|
||||||
|
line([[0.45, 0.1], [0.62, 0.42]], { lw: 2 });
|
||||||
|
line([[-0.7, 0.5], [0.7, 0.5]], { lw: 1.2, color: INK_FAINT, passes: 1, wobble: 0.008 });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
// OS1 加载环(参考 Her - OS1 Loading):白色扭曲环在陶土橙红背景上旋转,
|
||||||
|
// 按下加速转向正面、淡出并显露完成圆环。
|
||||||
|
const BG = '#14100d';
|
||||||
|
|
||||||
|
class OS1Curve extends THREE.Curve {
|
||||||
|
constructor(length, radius) {
|
||||||
|
super();
|
||||||
|
this.length = length;
|
||||||
|
this.radius = radius;
|
||||||
|
this.pi2 = Math.PI * 2;
|
||||||
|
}
|
||||||
|
getPoint(percent) {
|
||||||
|
const { length, radius, pi2 } = this;
|
||||||
|
let t = (percent % 0.25) / 0.25;
|
||||||
|
t = (percent % 0.25) - (2 * (1 - t) * t * -0.0185 + t * t * 0.25);
|
||||||
|
if (Math.floor(percent / 0.25) === 0 || Math.floor(percent / 0.25) === 2) t *= -1;
|
||||||
|
const x = length * Math.sin(pi2 * percent);
|
||||||
|
const y = radius * Math.cos(pi2 * 3 * percent);
|
||||||
|
const z = radius * Math.sin(pi2 * 2 * (percent - t));
|
||||||
|
return new THREE.Vector3(x, y, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initLoading() {
|
||||||
|
const wrap = document.getElementById('loading-canvas');
|
||||||
|
if (!wrap) return { dispose() {} };
|
||||||
|
|
||||||
|
const canvassize = 500;
|
||||||
|
const length = 30;
|
||||||
|
const radius = 5.6;
|
||||||
|
const rotatevalue = 0.035;
|
||||||
|
let acceleration = 0;
|
||||||
|
let animatestep = 0;
|
||||||
|
let toend = false;
|
||||||
|
let disposed = false;
|
||||||
|
let rafId = 0;
|
||||||
|
|
||||||
|
const group = new THREE.Group();
|
||||||
|
const scene = new THREE.Scene();
|
||||||
|
scene.add(group);
|
||||||
|
|
||||||
|
const camera = new THREE.PerspectiveCamera(65, 1, 1, 10000);
|
||||||
|
camera.position.z = 150;
|
||||||
|
|
||||||
|
// 白色扭曲环
|
||||||
|
const mesh = new THREE.Mesh(
|
||||||
|
new THREE.TubeGeometry(new OS1Curve(length, radius), 120, 1.1, 2, true),
|
||||||
|
new THREE.MeshBasicMaterial({ color: 0xe0c46a, transparent: true }),
|
||||||
|
);
|
||||||
|
group.add(mesh);
|
||||||
|
|
||||||
|
// 缺口遮挡(背景色)
|
||||||
|
const ringcover = new THREE.Mesh(
|
||||||
|
new THREE.PlaneGeometry(50, 15, 1),
|
||||||
|
new THREE.MeshBasicMaterial({ color: 0x14100d, transparent: true, opacity: 0 }),
|
||||||
|
);
|
||||||
|
ringcover.position.x = length + 1;
|
||||||
|
ringcover.rotation.y = Math.PI / 2;
|
||||||
|
group.add(ringcover);
|
||||||
|
|
||||||
|
// 完成圆环
|
||||||
|
const ring = new THREE.Mesh(
|
||||||
|
new THREE.RingGeometry(4.3, 5.55, 32),
|
||||||
|
new THREE.MeshBasicMaterial({ color: 0xe0c46a, transparent: true, opacity: 0 }),
|
||||||
|
);
|
||||||
|
ring.position.x = length + 1.1;
|
||||||
|
ring.rotation.y = Math.PI / 2;
|
||||||
|
group.add(ring);
|
||||||
|
|
||||||
|
const renderer = new THREE.WebGLRenderer({ antialias: false });
|
||||||
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
|
||||||
|
renderer.setSize(canvassize, canvassize);
|
||||||
|
renderer.setClearColor(BG);
|
||||||
|
wrap.appendChild(renderer.domElement);
|
||||||
|
|
||||||
|
function easing(t, b, c, d) {
|
||||||
|
if ((t /= d / 2) < 1) return (c / 2) * t * t + b;
|
||||||
|
return (c / 2) * ((t -= 2) * t * t + 2) + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
animatestep = Math.max(0, Math.min(240, toend ? animatestep + 1 : animatestep - 4));
|
||||||
|
acceleration = easing(animatestep, 0, 1, 240);
|
||||||
|
|
||||||
|
if (acceleration > 0.35) {
|
||||||
|
let progress = (acceleration - 0.35) / 0.65;
|
||||||
|
group.rotation.y = (-Math.PI / 2) * progress;
|
||||||
|
group.position.z = 50 * progress;
|
||||||
|
progress = Math.max(0, (acceleration - 0.97) / 0.03);
|
||||||
|
mesh.material.opacity = 1 - progress;
|
||||||
|
ringcover.material.opacity = ring.material.opacity = progress;
|
||||||
|
ring.scale.x = ring.scale.y = 0.9 + 0.1 * progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
function animate() {
|
||||||
|
mesh.rotation.x += rotatevalue + acceleration;
|
||||||
|
render();
|
||||||
|
if (!disposed) rafId = requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
animate();
|
||||||
|
|
||||||
|
const press = () => { toend = true; };
|
||||||
|
const release = () => { toend = false; };
|
||||||
|
document.body.addEventListener('mousedown', press);
|
||||||
|
document.body.addEventListener('touchstart', press);
|
||||||
|
document.body.addEventListener('mouseup', release);
|
||||||
|
document.body.addEventListener('touchend', release);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dispose() {
|
||||||
|
disposed = true;
|
||||||
|
cancelAnimationFrame(rafId);
|
||||||
|
document.body.removeEventListener('mousedown', press);
|
||||||
|
document.body.removeEventListener('touchstart', press);
|
||||||
|
document.body.removeEventListener('mouseup', release);
|
||||||
|
document.body.removeEventListener('touchend', release);
|
||||||
|
renderer.dispose();
|
||||||
|
if (renderer.domElement.parentNode === wrap) wrap.removeChild(renderer.domElement);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
+37
-7
@@ -7,9 +7,12 @@ export class Overlay {
|
|||||||
this.onPrev = onPrev;
|
this.onPrev = onPrev;
|
||||||
this.onNext = onNext;
|
this.onNext = onNext;
|
||||||
this.onFlip = onFlip;
|
this.onFlip = onFlip;
|
||||||
|
this.view = 'binder';
|
||||||
|
this.globeReady = false;
|
||||||
|
|
||||||
this.els = {
|
this.els = {
|
||||||
iris: document.getElementById('iris'),
|
iris: document.getElementById('iris'),
|
||||||
|
btnView: document.getElementById('btn-view'),
|
||||||
pageNum: document.getElementById('page-num'),
|
pageNum: document.getElementById('page-num'),
|
||||||
pageTotal: document.getElementById('page-total'),
|
pageTotal: document.getElementById('page-total'),
|
||||||
pageName: document.getElementById('page-name'),
|
pageName: document.getElementById('page-name'),
|
||||||
@@ -27,22 +30,31 @@ export class Overlay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_wire() {
|
_wire() {
|
||||||
document.querySelectorAll('.view-btn').forEach((btn) => {
|
this.els.btnView.addEventListener('click', () => this._cycleView());
|
||||||
btn.addEventListener('click', () => this.onView(btn.dataset.view));
|
|
||||||
});
|
|
||||||
document.getElementById('btn-prev').addEventListener('click', () => this.onPrev());
|
document.getElementById('btn-prev').addEventListener('click', () => this.onPrev());
|
||||||
document.getElementById('btn-next').addEventListener('click', () => this.onNext());
|
document.getElementById('btn-next').addEventListener('click', () => this.onNext());
|
||||||
document.getElementById('btn-flip')?.addEventListener('click', () => this.onFlip());
|
document.getElementById('btn-flip')?.addEventListener('click', () => this.onFlip());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 视图循环切换:活页本 → 时间轴 → 地理 → 活页本(地理未就绪时跳过)
|
||||||
|
_cycleView() {
|
||||||
|
const order = ['binder', 'timeline', 'globe'];
|
||||||
|
let next = order[(order.indexOf(this.view) + 1) % order.length];
|
||||||
|
if (next === 'globe' && !this.globeReady) next = 'binder';
|
||||||
|
this.onView(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
setGlobeReady(ready) {
|
||||||
|
this.globeReady = ready;
|
||||||
|
}
|
||||||
|
|
||||||
setTotal(n) {
|
setTotal(n) {
|
||||||
this.els.pageTotal.textContent = String(n).padStart(2, '0');
|
this.els.pageTotal.textContent = String(n).padStart(2, '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
setView(view) {
|
setView(view) {
|
||||||
document.querySelectorAll('.view-btn').forEach((btn) => {
|
this.view = view;
|
||||||
btn.classList.toggle('active', btn.dataset.view === view);
|
this.els.btnView.dataset.view = view;
|
||||||
});
|
|
||||||
const HINTS = {
|
const HINTS = {
|
||||||
binder: '拖动旋转 · 滚轮缩放 · 点击书页选中 · ← → 翻页(翻面查看素描)',
|
binder: '拖动旋转 · 滚轮缩放 · 点击书页选中 · ← → 翻页(翻面查看素描)',
|
||||||
timeline: '拖拽左右滑动 · 滚轮缩放 · 点击卡片查看详情',
|
timeline: '拖拽左右滑动 · 滚轮缩放 · 点击卡片查看详情',
|
||||||
@@ -52,15 +64,33 @@ export class Overlay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setEntry(entry, index, total) {
|
setEntry(entry, index, total) {
|
||||||
|
this.els.detailCard.classList.remove('hidden');
|
||||||
this.els.pageNum.textContent = String(index + 1).padStart(2, '0');
|
this.els.pageNum.textContent = String(index + 1).padStart(2, '0');
|
||||||
this.els.pageName.textContent = entry.name;
|
this.els.pageName.textContent = entry.name;
|
||||||
this.els.detailNo.textContent = entry.id;
|
this.els.detailNo.textContent = 'NO.' + (entry.code || entry.id);
|
||||||
this.els.detailName.textContent = entry.name;
|
this.els.detailName.textContent = entry.name;
|
||||||
this.els.detailTime.textContent = entry.time;
|
this.els.detailTime.textContent = entry.time;
|
||||||
this.els.detailRegion.textContent = entry.region;
|
this.els.detailRegion.textContent = entry.region;
|
||||||
this.els.detailDesc.textContent = entry.description;
|
this.els.detailDesc.textContent = entry.description;
|
||||||
|
|
||||||
// 素描缩略图(等比放大填满,裁掉上下多余部分)
|
// 素描缩略图(等比放大填满,裁掉上下多余部分)
|
||||||
|
this._drawSketch(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// END 页:展示最后一页背面(末页素描)与结束标记
|
||||||
|
setEnd(entry) {
|
||||||
|
this.els.detailCard.classList.remove('hidden');
|
||||||
|
this.els.pageNum.textContent = 'END';
|
||||||
|
this.els.pageName.textContent = '完 · 人类文明里程碑';
|
||||||
|
this.els.detailNo.textContent = 'END';
|
||||||
|
this.els.detailName.textContent = '完 · The End';
|
||||||
|
this.els.detailTime.textContent = '—';
|
||||||
|
this.els.detailRegion.textContent = '—';
|
||||||
|
this.els.detailDesc.textContent = '人类文明里程碑 · 活页编年史 · 全书完';
|
||||||
|
this._drawSketch(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
_drawSketch(entry) {
|
||||||
const c = this.els.detailSketch;
|
const c = this.els.detailSketch;
|
||||||
const ctx = c.getContext('2d');
|
const ctx = c.getContext('2d');
|
||||||
ctx.clearRect(0, 0, c.width, c.height);
|
ctx.clearRect(0, 0, c.width, c.height);
|
||||||
|
|||||||
Reference in New Issue
Block a user