Ethscriptions commited on
Commit
ae1fc55
·
verified ·
1 Parent(s): b826b6a

E4-E5: 以太坊简史 渲染引擎 + 13 章分镜

Browse files
.gitattributes CHANGED
@@ -1,2 +1,3 @@
1
  shouhouzi/covers/cover-landscape.png filter=lfs diff=lfs merge=lfs -text
2
  shouhouzi/covers/cover-portrait.png filter=lfs diff=lfs merge=lfs -text
 
 
1
  shouhouzi/covers/cover-landscape.png filter=lfs diff=lfs merge=lfs -text
2
  shouhouzi/covers/cover-portrait.png filter=lfs diff=lfs merge=lfs -text
3
+ ethereum/fonts/notosc.woff2 filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -149,8 +149,8 @@ python shouhouzi/tools/subset_font.py <ZCOOLKuaiLe-Regular.ttf> # 重新做字
149
  | E1 | 写计划(本节)+ 作品导航首页 | ✅ 完成 |
150
  | E2 | 史实核对(众筹、ETF 首日、各升级时间、Glamsterdam 测试网)+ 真实 ETH 日线入库(2015-08 → 2026-10,1361 点)+ 13 章 63 句文案 | ✅ 完成 |
151
  | E3 | TTS 配音(晓晓·纪录片声线,逐词时间戳)→ 自动解析 120+ 个画面同步点,语音约 357 秒 | ✅ 完成 |
152
- | E4 | 渲染引擎:3D 投影、发光合成、连续镜头系统、响应式构图(横/竖) | ⏳ |
153
- | E5 | 13 个章节的分镜动画 | ⏳ |
154
  | E6 | 音频:Web Worker 内纯 DSP 合成配乐与音效 + 卷积混响 + 母带处理 | ⏳ |
155
  | E7 | 播放器 + MP4 导出(Mediabunny:H.264/AAC 优先,自动回退)+ 24/30/60fps + 横竖屏 | ⏳ |
156
  | E8 | 封面图 + 抖音 / B站发布文案 | ⏳ |
 
149
  | E1 | 写计划(本节)+ 作品导航首页 | ✅ 完成 |
150
  | E2 | 史实核对(众筹、ETF 首日、各升级时间、Glamsterdam 测试网)+ 真实 ETH 日线入库(2015-08 → 2026-10,1361 点)+ 13 章 63 句文案 | ✅ 完成 |
151
  | E3 | TTS 配音(晓晓·纪录片声线,逐词时间戳)→ 自动解析 120+ 个画面同步点,语音约 357 秒 | ✅ 完成 |
152
+ | E4 | 渲染引擎:3D 投影晶体/区块链/地球、加法发光、星空跃迁转场、调色板渐变、响应式构图(横/竖)、卡拉 OK 字幕 | ✅ 完成 |
153
+ | E5 | 13 个章节的分镜动画(含真实价格曲线 4 次递进出现),横竖屏逐句截图检查并修正 | ✅ 完成 |
154
  | E6 | 音频:Web Worker 内纯 DSP 合成配乐与音效 + 卷积混响 + 母带处理 | ⏳ |
155
  | E7 | 播放器 + MP4 导出(Mediabunny:H.264/AAC 优先,自动回退)+ 24/30/60fps + 横竖屏 | ⏳ |
156
  | E8 | 封面图 + 抖音 / B站发布文案 | ⏳ |
ethereum/fonts/deyi.woff2 ADDED
Binary file (95 kB). View file
 
ethereum/fonts/grotesk.woff2 ADDED
Binary file (19.3 kB). View file
 
ethereum/fonts/notosc.woff2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:52761f9d352aa46037be3ade47bf9c03cc207b05b144017f3cf26ebf3a3bc386
3
+ size 230800
ethereum/js/chapters.js ADDED
@@ -0,0 +1,801 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ==========================================================================
2
+ * chapters.js — 13 个章节的分镜层
3
+ * 每章:draw(ctx, L, t, H) —— H 提供该章的同步点工具:
4
+ * H.at(name) 标记时刻;H.m(name) 距标记秒数;H.lf(a,b,inD,outD) 生命周期
5
+ * H.bx(lx,ly,px,py) 按横/竖屏返回内容框内坐标(参数为 0..1 比例)
6
+ * crystal(o, t, H):可选,修改晶体姿态
7
+ * ========================================================================== */
8
+ (() => {
9
+ const { C, E } = K;
10
+ const W = window.WORLD;
11
+ const CH = (window.CHAPTERS = {});
12
+
13
+ /* ---------- 小组件 ---------- */
14
+ function pop(ctx, x, y, k, fn) { if (k <= 0.003) return; ctx.save(); ctx.translate(x, y); const s = 0.85 + 0.15 * k; ctx.scale(s, s); ctx.globalAlpha *= k; fn(); ctx.restore(); }
15
+ function chip(ctx, x, y, label, o = {}) {
16
+ const size = o.size || 30; const w = K.measure(ctx, label, size, o.fam || 'body', 700) + size * 1.4; const h = size * 1.9;
17
+ K.glass(ctx, x - w / 2, y - h / 2, w, h, { r: h / 2, tint: o.color || C.eth, tint2: o.color2 || C.mag, alpha: o.alpha ?? 1 });
18
+ K.text(ctx, label, x, y + 1, { size, fam: o.fam || 'body', color: o.textColor || C.white, alpha: o.alpha ?? 1 });
19
+ return w;
20
+ }
21
+ function card(ctx, x, y, w, h, title, lines, o = {}) {
22
+ const a = o.alpha ?? 1; if (a <= 0.003) return;
23
+ ctx.save(); ctx.globalAlpha *= a;
24
+ K.glass(ctx, x - w / 2, y - h / 2, w, h, { tint: o.color || C.eth, tint2: o.color2 || C.mag });
25
+ if (o.tag) K.text(ctx, o.tag, x - w / 2 + 34, y - h / 2 + 40, { size: 22, fam: 'num', align: 'left', color: o.color || C.eth2, weight: 600 });
26
+ K.text(ctx, title, x, y - h / 2 + (o.tag ? 92 : 62), { size: o.ts || 44, fam: 'title', color: C.white, weight: 400 });
27
+ (lines || []).forEach((s, i) => K.text(ctx, s, x, y - h / 2 + (o.tag ? 150 : 120) + i * 46, { size: o.ls || 30, color: K.rgba(C.white, 0.78), weight: 500 }));
28
+ ctx.restore();
29
+ }
30
+ function bigYear(ctx, x, y, str, size, color, a = 1) { K.text(ctx, str, x, y, { size, fam: 'num', weight: 700, color, alpha: a, glow: color, glowR: size * 0.35, ls: size * 0.02 }); }
31
+ function counter(v0, v1, k) { return K.lerp(v0, v1, E.out3(k)); }
32
+ function hexToken(ctx, x, y, r, col, label, a = 1) {
33
+ if (a <= 0.01) return;
34
+ const pts = []; for (let i = 0; i <= 6; i++) { const t = i / 6 * K.TAU + Math.PI / 6; pts.push([x + Math.cos(t) * r, y + Math.sin(t) * r]); }
35
+ ctx.save(); ctx.globalAlpha *= a; ctx.fillStyle = K.rgba(col, 0.18); ctx.beginPath(); pts.forEach((p, i) => (i ? ctx.lineTo(p[0], p[1]) : ctx.moveTo(p[0], p[1]))); ctx.fill(); ctx.restore();
36
+ K.neon(ctx, pts, col, 2, a);
37
+ if (label) K.text(ctx, label, x, y + 1, { size: r * 0.42, fam: 'num', color: C.white, alpha: a });
38
+ }
39
+ function ethGlyph(ctx, x, y, s, col = C.eth2, a = 1) { // 平面以太坊标志
40
+ const P = (pts) => pts.map(([px, py]) => [x + px * s, y + py * s]);
41
+ K.neon(ctx, P([[0, -1], [0.62, 0.05], [0, 0.38], [-0.62, 0.05], [0, -1]]), col, Math.max(1.2, s * 0.05), a);
42
+ K.neon(ctx, P([[-0.62, 0.22], [0, 1], [0.62, 0.22], [0, 0.56], [-0.62, 0.22]]), col, Math.max(1.2, s * 0.05), a);
43
+ }
44
+ function scan(ctx, L, t, col, a) { // 扫描线光带
45
+ if (a <= 0) return; const y = L.H * K.fract(t * 0.25);
46
+ const g = ctx.createLinearGradient(0, y - 80, 0, y + 80); g.addColorStop(0, K.rgba(col, 0)); g.addColorStop(0.5, K.rgba(col, 0.12 * a)); g.addColorStop(1, K.rgba(col, 0));
47
+ ctx.fillStyle = g; ctx.fillRect(0, y - 80, L.W, 160);
48
+ }
49
+ function flash(ctx, L, k, col = '#ffffff') { if (k <= 0) return; ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.fillStyle = K.rgba(col, 0.6 * k); ctx.fillRect(0, 0, L.W, L.H); ctx.restore(); }
50
+ function burst(ctx, x, y, dt, col, n = 28, R = 300) { // 粒子爆散
51
+ if (dt < 0 || dt > 2.2) return; const r = K.rng(Math.floor(x * 7 + y * 3));
52
+ for (let i = 0; i < n; i++) { const a = r() * K.TAU, v = R * (0.4 + r() * 0.8); const d = E.out3(dt / 1.6) * v; K.glow(ctx, x + Math.cos(a) * d, y + Math.sin(a) * d, 10 + r() * 16, i % 3 ? col : C.white, (1 - dt / 2.2) * 0.9); }
53
+ }
54
+
55
+ /* =======================================================================
56
+ * 序章
57
+ * ===================================================================== */
58
+ CH.open = {
59
+ draw(ctx, L, t, H) {
60
+ const P = L.portrait;
61
+ const sp = H.m('spark');
62
+ if (sp > -0.2 && sp < 3.5) {
63
+ // 光点 + 汇聚粒子
64
+ const c = W.crystalPose(t, L);
65
+ K.glow(ctx, c.x, c.y, 60 + 200 * E.out3(sp / 1.5), C.white, K.clamp(1 - (sp - 2) / 1.5));
66
+ const r = K.rng(5);
67
+ for (let i = 0; i < 70; i++) { const a = r() * K.TAU, d0 = 300 + r() * 700; const k = E.io3((sp - r() * 0.8) / 2.2); const d = d0 * (1 - k); if (k <= 0 || k >= 1) continue; K.glow(ctx, c.x + Math.cos(a) * d, c.y + Math.sin(a) * d * 0.8, 8 + r() * 10, i % 2 ? C.cyan : C.eth2, 0.8); }
68
+ }
69
+ // 姓名卡
70
+ const nk = H.lf('name', 'title', 0.7, 0.5);
71
+ if (nk > 0) {
72
+ const [x, y] = H.bx(0.5, 0.97, 0.5, 0.84);
73
+ pop(ctx, x, y, nk, () => {
74
+ K.text(ctx, 'VITALIK BUTERIN', 0, -38, { size: P ? 44 : 52, fam: 'num', ls: 8, color: C.white, glow: C.eth });
75
+ K.text(ctx, '维塔利克 · 布特林 | 以太坊创始人 | 时年 19 岁', 0, 26, { size: P ? 26 : 30, color: K.rgba(C.white, 0.75), weight: 500 });
76
+ });
77
+ }
78
+ // 价值增长:环形扩散
79
+ const g = H.m('grow');
80
+ if (g > 0 && H.m('title') < 0.5) {
81
+ const c = W.crystalPose(t, L);
82
+ for (let i = 0; i < 4; i++) { const k = K.fract(g * 0.45 + i / 4); K.neonCircle(ctx, c.x, c.y, 120 + k * Math.max(L.W, L.H) * 0.5, i % 2 ? C.cyan : C.eth, 2, (1 - k) * 0.6 * K.clamp(g)); }
83
+ }
84
+ // 片名
85
+ const tk = H.lf('title', null, 1.0);
86
+ if (tk > 0) {
87
+ const [x, y] = H.bx(0.5, 0.84, 0.5, 0.82);
88
+ const m = H.m('title');
89
+ ctx.save(); ctx.globalAlpha *= tk;
90
+ const s = P ? 132 : 150;
91
+ K.text(ctx, '以太坊简史', x, y - s * 0.32, { size: s, fam: 'title', weight: 400, color: C.white, glow: C.eth, glowR: 40, ls: 6 * E.out3(m) });
92
+ K.text(ctx, '从一份白皮书,到世界计算机', x, y + s * 0.5, { size: P ? 40 : 44, color: K.rgba(C.white, 0.85), weight: 500, ls: 4 });
93
+ K.text(ctx, '2013 — 2026', x, y + s * 0.5 + 62, { size: 30, fam: 'num', color: C.eth2, weight: 600, ls: 10 });
94
+ ctx.restore();
95
+ flash(ctx, L, K.clamp(1 - m / 0.5) * K.clamp(m * 8));
96
+ }
97
+ },
98
+ crystal(o, t, H) {
99
+ const sp = H.m('spark');
100
+ o.alpha = K.clamp(sp / 0.6);
101
+ o.build = E.io3(sp / 3.2);
102
+ o.fill = K.life(t, H.at('name'), Infinity, 1.6);
103
+ if (H.m('title') > 0) { const k = E.io3(H.m('title') / 1.2); o.y -= o.s * 0.9 * k; o.s *= 1 - 0.38 * k; }
104
+ },
105
+ };
106
+
107
+ /* =======================================================================
108
+ * 起源
109
+ * ===================================================================== */
110
+ CH.origin = {
111
+ draw(ctx, L, t, H) {
112
+ const P = L.portrait;
113
+ // 比特币 → 计算器
114
+ const bk = H.lf(null, 'code', 0.8, 0.6, 0);
115
+ if (bk > 0) {
116
+ const [x, y] = H.bx(0.27, 0.5, 0.5, 0.3);
117
+ const ck = E.io3(H.m('calc') / 0.9);
118
+ pop(ctx, x, y, bk, () => {
119
+ // 比特币币面
120
+ ctx.save(); ctx.globalAlpha *= 1 - ck;
121
+ K.glow(ctx, 0, 0, 260, C.orange, 0.6); K.neonCircle(ctx, 0, 0, 120, C.orange, 4); K.text(ctx, '₿', 0, 6, { size: 150, fam: 'num', color: '#FFB15E' });
122
+ K.text(ctx, 'Bitcoin · 2009', 0, 175, { size: 30, fam: 'num', color: K.rgba(C.white, 0.8), weight: 500 });
123
+ ctx.restore();
124
+ if (ck > 0) {
125
+ ctx.save(); ctx.globalAlpha *= ck;
126
+ K.glass(ctx, -140, -190, 280, 380, { tint: C.orange, tint2: C.gold });
127
+ K.rr(ctx, -110, -160, 220, 84, 12); ctx.fillStyle = 'rgba(0,0,0,.45)'; ctx.fill();
128
+ K.text(ctx, 'A → B : 1 ₿', 92, -117, { size: 30, fam: 'num', align: 'right', color: '#FFB15E' });
129
+ for (let r = 0; r < 4; r++) for (let c = 0; c < 3; c++) { const bx = -100 + c * 72, by = -50 + r * 56; K.rr(ctx, bx, by, 58, 42, 10); ctx.fillStyle = 'rgba(255,177,94,.15)'; ctx.fill(); ctx.strokeStyle = 'rgba(255,177,94,.5)'; ctx.lineWidth = 1.5; ctx.stroke(); }
130
+ ctx.restore();
131
+ K.text(ctx, '计算器:只专注记账与转账', 0, 240, { size: 30, color: C.white, alpha: ck });
132
+ }
133
+ });
134
+ }
135
+ // 代码面板
136
+ const cdk = H.lf('code', 'contract', 0.7, 0.5);
137
+ if (cdk > 0) {
138
+ const [x, y] = H.bx(0.28, 0.5, 0.5, 0.3);
139
+ const lines = ['contract WorldComputer {', ' mapping(address => uint) balance;', ' function anything(bytes code)', ' public returns (bool) {', ' // 任何程序,都能在链上运行', ' return execute(code);', ' }', '}'];
140
+ const w = P ? 800 : 700, h = 420;
141
+ pop(ctx, x, y, cdk, () => {
142
+ K.glass(ctx, -w / 2, -h / 2, w, h, { tint: C.cyan });
143
+ ['#FF5F57', '#FEBC2E', '#28C840'].forEach((c, i) => { ctx.fillStyle = c; ctx.beginPath(); ctx.arc(-w / 2 + 34 + i * 26, -h / 2 + 30, 7, 0, K.TAU); ctx.fill(); });
144
+ const chars = Math.floor(H.m('code') * 42);
145
+ let used = 0;
146
+ lines.forEach((ln, i) => {
147
+ const n = K.clamp(chars - used, 0, ln.length); used += ln.length;
148
+ if (n <= 0) return;
149
+ const col = ln.includes('//') ? '#7DD3A0' : i === 0 || i === 7 ? C.mag : ln.includes('function') ? C.cyan : '#DDE3FF';
150
+ K.text(ctx, ln.slice(0, n), -w / 2 + 34, -h / 2 + 80 + i * 40, { size: P ? 26 : 25, fam: 'num', align: 'left', color: col, weight: 500 });
151
+ });
152
+ });
153
+ }
154
+ // 世界计算机:地球
155
+ const gk = H.lf('world', null, 1.0);
156
+ if (gk > 0) {
157
+ const [x, y] = H.bx(0.72, 0.52, 0.5, 0.74);
158
+ const r = P ? 230 : 270;
159
+ W.globe(ctx, { x, y, r, t, ry: t * 0.25, arcs: 16, alpha: gk, color: C.eth, color2: C.cyan });
160
+ K.text(ctx, '世界计算机', x, y + r + 60, { size: 46, fam: 'title', weight: 400, color: C.white, alpha: gk, glow: C.eth });
161
+ K.text(ctx, 'WORLD COMPUTER', x, y + r + 106, { size: 22, fam: 'num', color: C.eth2, alpha: gk, ls: 8 });
162
+ }
163
+ // 智能合约流程
164
+ const fk = H.lf('contract', null, 0.8);
165
+ if (fk > 0) {
166
+ const [x, y] = H.bx(0.28, 0.5, 0.5, 0.3);
167
+ const steps = [['条件', 'IF'], ['合约代码', 'CODE'], ['自动执行', 'THEN']];
168
+ const gap = P ? 270 : 230;
169
+ pop(ctx, x, y, fk, () => {
170
+ K.text(ctx, '智能合约 · Smart Contract', 0, -150, { size: 36, fam: 'title', weight: 400, color: C.white, glow: C.cyan });
171
+ steps.forEach(([zh, en], i) => {
172
+ const sx = (i - 1) * gap; const done = i === 2 && H.m('auto') > 0.3;
173
+ K.glass(ctx, sx - 95, -60, 190, 120, { tint: done ? C.green : C.cyan, alpha: 1 });
174
+ K.text(ctx, en, sx, -24, { size: 22, fam: 'num', color: done ? C.green : C.cyan, ls: 4 });
175
+ K.text(ctx, zh, sx, 18, { size: 32, color: C.white });
176
+ if (done) K.glow(ctx, sx, 0, 150, C.green, 0.5 * K.clamp(H.m('auto') - 0.3));
177
+ if (i < 2) { K.neon(ctx, [[sx + 100, 0], [sx + gap - 100, 0]], C.cyan, 2, 0.8); const ph = K.fract(t * 0.9 + i * 0.5); K.glow(ctx, sx + 100 + (gap - 200) * ph, 0, 22, C.white, 0.9); }
178
+ });
179
+ K.text(ctx, '不需要任何中间人', 0, 120, { size: 30, color: K.rgba(C.white, 0.75), alpha: K.clamp(H.m('auto')) });
180
+ });
181
+ }
182
+ },
183
+ };
184
+
185
+ /* =======================================================================
186
+ * 众筹
187
+ * ===================================================================== */
188
+ CH.ico = {
189
+ draw(ctx, L, t, H) {
190
+ const P = L.portrait;
191
+ const gk = H.lf(null, 'sale', 0.8, 0.8, 0);
192
+ if (gk > 0) {
193
+ const [x, y] = H.bx(0.5, 0.5, 0.5, 0.48);
194
+ W.globe(ctx, { x, y, r: P ? 300 : 320, t, ry: -1.35 + t * 0.05, rx: -0.35, arcs: 6, alpha: gk, color: C.eth, color2: C.gold, markers: [{ lat: 25.76, lon: -80.19, label: '迈阿密 · 2014.01', color: C.gold, a: K.clamp(H.m('miami')) }] });
195
+ }
196
+ const rk = H.lf('sale', null, 0.9);
197
+ if (rk > 0) {
198
+ const [x, y] = H.bx(0.36, 0.5, 0.5, 0.36);
199
+ const R = P ? 220 : 240;
200
+ const prog = E.io3(H.m('sale') / 6);
201
+ pop(ctx, x, y, rk, () => {
202
+ K.neonCircle(ctx, 0, 0, R, 'rgba(255,255,255,0.15)'.startsWith('#') ? C.dim : C.dim, 1.5, 0.35);
203
+ K.neonCircle(ctx, 0, 0, R, C.gold, 5, 1, -Math.PI / 2, -Math.PI / 2 + K.TAU * prog);
204
+ const hx = Math.cos(-Math.PI / 2 + K.TAU * prog) * R, hy = Math.sin(-Math.PI / 2 + K.TAU * prog) * R; K.glow(ctx, hx, hy, 50, C.white, 0.9);
205
+ K.text(ctx, '公开众筹', 0, -R * 0.5, { size: 32, color: K.rgba(C.white, 0.8) });
206
+ const raised = H.m('raise') > 0 ? counter(0, 18.3, H.m('raise') / 1.6) : counter(0, 18.3, prog) * 0.0;
207
+ if (H.m('raise') > 0) K.text(ctx, '$' + raised.toFixed(1) + 'M', 0, 6, { size: 92, fam: 'num', color: C.gold, glow: C.gold });
208
+ else K.text(ctx, Math.round(prog * 42) + ' 天', 0, 6, { size: 80, fam: 'num', color: C.white });
209
+ K.text(ctx, '2014.07.22 — 09.02', 0, R * 0.48, { size: 26, fam: 'num', color: C.eth2, ls: 3 });
210
+ });
211
+ }
212
+ const pk = H.lf('price', 'found', 0.7, 0.6);
213
+ if (pk > 0) {
214
+ const [x, y] = H.bx(0.75, 0.32, 0.5, 0.8);
215
+ pop(ctx, x, y, pk, () => {
216
+ K.glass(ctx, -230, -90, 460, 180, { tint: C.gold });
217
+ ethGlyph(ctx, -150, 0, 52, C.eth2);
218
+ K.text(ctx, '1 ETH ≈', 30, -30, { size: 30, fam: 'num', color: K.rgba(C.white, 0.75), weight: 500 });
219
+ K.text(ctx, '$0.31', 30, 30, { size: 74, fam: 'num', color: C.white, glow: C.gold });
220
+ });
221
+ }
222
+ const fk = H.lf('found', null, 0.8);
223
+ if (fk > 0) {
224
+ const [x, y] = H.bx(0.75, 0.6, 0.5, 0.82);
225
+ pop(ctx, x, y, fk, () => {
226
+ K.glass(ctx, -260, -110, 520, 220, { tint: C.red, tint2: C.eth });
227
+ K.rr(ctx, -210, -50, 100, 100, 14); ctx.fillStyle = '#E8333A'; ctx.fill();
228
+ ctx.fillStyle = '#fff'; ctx.fillRect(-172, -36, 24, 72); ctx.fillRect(-196, -12, 72, 24);
229
+ K.text(ctx, '以太坊基金会', 40, -22, { size: 44, fam: 'title', weight: 400, color: C.white });
230
+ K.text(ctx, 'Ethereum Foundation · 瑞士', 40, 30, { size: 24, fam: 'num', color: K.rgba(C.white, 0.7), weight: 500 });
231
+ });
232
+ }
233
+ },
234
+ };
235
+
236
+ /* =======================================================================
237
+ * 创世
238
+ * ===================================================================== */
239
+ CH.genesis = {
240
+ draw(ctx, L, t, H) {
241
+ const P = L.portrait;
242
+ const bk = H.lf('boot', null, 0.6);
243
+ if (bk > 0 && H.m('block0') < 1.2) {
244
+ const [x, y] = H.bx(0.5, 0.42, 0.5, 0.36);
245
+ const lines = ['> 连接网络 ……… OK', '> 加载创世配置 ……… OK', '> MAINNET: FRONTIER', '> 2015-07-30'];
246
+ const k = K.clamp(1 - Math.max(0, H.m('block0') + 0.3) / 0.5);
247
+ ctx.save(); ctx.globalAlpha *= bk * k;
248
+ lines.forEach((s, i) => { const n = Math.floor(K.clamp((H.m('boot') - i * 0.45) * 30, 0, s.length)); K.text(ctx, s.slice(0, n), x - (P ? 330 : 360), y - 80 + i * 56, { size: P ? 34 : 38, fam: 'num', align: 'left', color: i === 2 ? C.cyan : K.rgba(C.white, 0.85), weight: 600 }); });
249
+ ctx.restore();
250
+ }
251
+ // 创世区块 → 链条生长
252
+ const b0 = H.m('block0');
253
+ if (b0 > -0.1) {
254
+ const [x, y] = H.bx(0.5, 0.52, 0.5, 0.5);
255
+ const grow = H.m('chain');
256
+ const lit = grow > 0 ? Math.min(40, 1 + grow * 1.8) : K.clamp(b0 / 0.5);
257
+ const off = grow > 0 ? Math.max(0, (lit - 4) * 1.6) : 0;
258
+ W.blockchain(ctx, { x: x + (P ? 0 : 0), y, s: P ? 84 : 112, n: 44, lit, offset: off - (44 - 1) / 2 * 1.6, first: 0, color: C.cyan, alpha: K.clamp(b0 / 0.4), ry: -0.55, rx: -0.22 });
259
+ if (b0 > 0 && grow < 0.6) {
260
+ const k = K.life(t, H.at('block0'), H.at('chain') + 0.2, 0.5, 0.5);
261
+ pop(ctx, x, y - (P ? 230 : 250), k, () => { K.text(ctx, 'BLOCK #0', 0, -26, { size: 64, fam: 'num', color: C.white, glow: C.cyan, ls: 6 }); K.text(ctx, '创世区块 · Genesis', 0, 34, { size: 32, color: C.cyan }); });
262
+ burst(ctx, x, y, b0, C.cyan, 40, 420);
263
+ }
264
+ if (grow > 0) {
265
+ const n = Math.floor(lit) - 1;
266
+ K.text(ctx, '区块高度', x - (P ? 0 : 0), y - (P ? 250 : 270), { size: 28, color: K.rgba(C.white, 0.6), alpha: K.clamp(grow) });
267
+ K.text(ctx, '#' + Math.max(0, Math.floor(grow * 2400)).toLocaleString('en-US'), x, y - (P ? 200 : 214), { size: 56, fam: 'num', color: C.white, alpha: K.clamp(grow), glow: C.cyan });
268
+ }
269
+ }
270
+ const pk = H.lf('p2015', null, 0.8);
271
+ if (pk > 0) {
272
+ const [x, y] = H.bx(0.5, 0.86, 0.5, 0.86);
273
+ pop(ctx, x, y, pk, () => {
274
+ K.glass(ctx, -300, -70, 600, 140, { tint: C.gold });
275
+ K.text(ctx, '2015.08 1 ETH ≈', -40, 0, { size: 32, fam: 'num', color: K.rgba(C.white, 0.75), align: 'right', weight: 500 });
276
+ K.text(ctx, K.money(window.ETH_PRICE.pts[0][1], 2), 150, 2, { size: 64, fam: 'num', color: C.gold, glow: C.gold });
277
+ });
278
+ }
279
+ },
280
+ };
281
+
282
+ /* =======================================================================
283
+ * 至暗时刻
284
+ * ===================================================================== */
285
+ CH.dao = {
286
+ draw(ctx, L, t, H) {
287
+ const P = L.portrait;
288
+ const hk = H.m('hack');
289
+ // 金库
290
+ const vk = H.lf('dao', 'debate', 0.8, 0.6);
291
+ if (vk > 0) {
292
+ const [x, y] = H.bx(0.33, 0.5, 0.5, 0.36);
293
+ const R = P ? 200 : 220;
294
+ const red = K.clamp(hk / 0.4);
295
+ const sh = hk > 0 && hk < 1.2 ? (1 - hk / 1.2) * 16 : 0;
296
+ pop(ctx, x + Math.sin(t * 70) * sh, y + Math.cos(t * 53) * sh, vk, () => {
297
+ const col = K.mix(C.eth, C.red, red);
298
+ const pts = []; for (let i = 0; i <= 6; i++) { const a = i / 6 * K.TAU; pts.push([Math.cos(a) * R, Math.sin(a) * R]); }
299
+ ctx.fillStyle = K.rgba(col, 0.12); ctx.beginPath(); pts.forEach((p, i) => (i ? ctx.lineTo(p[0], p[1]) : ctx.moveTo(p[0], p[1]))); ctx.fill();
300
+ K.neon(ctx, pts, col, 3.5, 1);
301
+ K.neonCircle(ctx, 0, 0, R * 0.62, col, 2, 0.8);
302
+ ctx.save(); ctx.rotate(t * 0.4 + hk * 3); for (let i = 0; i < 6; i++) { ctx.rotate(K.TAU / 6); K.neon(ctx, [[R * 0.2, 0], [R * 0.6, 0]], col, 2.5, 0.8); } ctx.restore();
303
+ K.text(ctx, 'The DAO', 0, -18, { size: 54, fam: 'num', color: C.white, glow: col });
304
+ const eth = hk > 0 ? Math.max(0, 1 - E.out3(H.m('drain') / 4) * 0.3) : 1;
305
+ K.text(ctx, '≈ $150M', 0, 40, { size: 34, fam: 'num', color: red > 0.5 ? '#FF8FA3' : C.eth2, alpha: eth });
306
+ if (hk > 0) { // 裂纹
307
+ const r = K.rng(616); ctx.save(); ctx.globalAlpha *= K.clamp(hk / 0.3);
308
+ for (let i = 0; i < 7; i++) { let px = 0, py = 0; const pts = [[0, 0]]; const a0 = r() * K.TAU; for (let j = 0; j < 5; j++) { const a = a0 + (r() - 0.5) * 1.2; px += Math.cos(a) * R * 0.22; py += Math.sin(a) * R * 0.22; pts.push([px, py]); } K.neon(ctx, pts, C.red, 2, 0.9); }
309
+ ctx.restore();
310
+ }
311
+ });
312
+ if (H.m('nohead') > 0 && hk < 0.3) {
313
+ const [cx, cy] = H.bx(0.74, 0.5, 0.5, 0.78);
314
+ const k = H.lf('nohead', 'hack', 0.6, 0.3);
315
+ pop(ctx, cx, cy, k, () => { chip(ctx, 0, -60, '没有老板', { size: 34, color: C.eth }); chip(ctx, 0, 30, '代码治理的投资基金', { size: 34, color: C.cyan }); });
316
+ }
317
+ // 资金流失
318
+ const dr = H.m('drain');
319
+ if (dr > 0) {
320
+ const [sx, sy] = H.bx(0.76, 0.5, 0.5, 0.8);
321
+ const r = K.rng(99);
322
+ for (let i = 0; i < 60; i++) { const ph = K.fract(dr * 0.6 + r()); const px = K.lerp(x, sx, ph), py = K.lerp(y, sy, ph) - Math.sin(ph * Math.PI) * 140 * (r() - 0.3); K.glow(ctx, px, py, 12, i % 3 ? C.red : C.eth2, 0.8 * vk); }
323
+ const amt = Math.round(counter(0, 3.6, dr / 3.2) * 100) / 100;
324
+ ctx.save(); ctx.globalAlpha *= vk;
325
+ K.text(ctx, '-' + amt.toFixed(2) + 'M ETH', sx, sy, { size: P ? 64 : 72, fam: 'num', color: C.red, glow: C.red });
326
+ K.text(ctx, '约 360 万枚以太币被转走', sx, sy + 60, { size: 28, color: K.rgba(C.white, 0.8) });
327
+ ctx.restore();
328
+ }
329
+ if (hk > 0 && hk < 2.5) { // 故障切片
330
+ const r = K.rng(Math.floor(t * 20)); const g = K.clamp(1 - hk / 2.5);
331
+ for (let i = 0; i < 6; i++) { const yy = r() * L.H, hh = 6 + r() * 30; ctx.fillStyle = K.rgba(i % 2 ? C.red : C.cyan, 0.18 * g); ctx.fillRect(0, yy, L.W, hh); }
332
+ flash(ctx, L, K.clamp(1 - hk / 0.35), C.red);
333
+ }
334
+ }
335
+ // 争论
336
+ const dk = H.lf('debate', 'fork', 0.7, 0.5);
337
+ if (dk > 0) {
338
+ const [x1, y1] = H.bx(0.3, 0.5, 0.5, 0.3), [x2, y2] = H.bx(0.7, 0.5, 0.5, 0.72);
339
+ const w = P ? 600 : 560;
340
+ card(ctx, x1, y1, w, 260, '代码即法律', ['Code is Law', '链上发生的就是最终结果'], { alpha: dk, color: C.violet, tag: 'A' });
341
+ card(ctx, x2, y2, w, 260, '把钱追回来', ['修改协议,挽回损失', '保护普通用户'], { alpha: dk, color: C.cyan, tag: 'B' });
342
+ K.text(ctx, 'VS', (x1 + x2) / 2, (y1 + y2) / 2, { size: 64, fam: 'num', color: C.gold, alpha: dk, glow: C.gold });
343
+ }
344
+ // 分叉
345
+ const fk = H.lf('fork', null, 0.8);
346
+ if (fk > 0) {
347
+ const [x, y] = H.bx(0.5, 0.5, 0.5, 0.48);
348
+ const k = E.io3(H.m('fork') / 2.5);
349
+ W.blockchain(ctx, { x, y, s: P ? 62 : 80, n: 12, first: 1919995, lit: 12, alpha: fk, color: C.eth, labels: false, branch: { at: 5, k: 0.55 * k, c2: C.etc }, ry: -0.35, rx: -0.12 });
350
+ K.text(ctx, '硬分叉 · 2016.07.20', x, y - (P ? 280 : 300), { size: 44, fam: 'title', weight: 400, color: C.white, alpha: fk, glow: C.eth });
351
+ K.text(ctx, '区块 1,920,000', x, y - (P ? 232 : 250), { size: 26, fam: 'num', color: C.eth2, alpha: fk });
352
+ const ek = K.clamp(H.m('etc') / 0.6);
353
+ K.text(ctx, 'ETH 以太坊', x + (P ? 230 : 380), y - (P ? 150 : 170) * k, { size: 34, fam: 'num', color: C.eth2, alpha: fk * k, glow: C.eth });
354
+ K.text(ctx, 'ETC 以太坊经典', x + (P ? 200 : 380), y + (P ? 170 : 190) * k, { size: 34, fam: 'num', color: '#7CE07C', alpha: fk * Math.max(k * 0.4, ek), glow: C.etc });
355
+ }
356
+ },
357
+ crystal(o, t, H) { const hk = H.m('hack'); if (hk > 0) { o.tint = K.mix(C.eth, C.red, K.clamp(hk) * (1 - K.clamp(H.m('fork') / 2))); o.x += hk < 1 ? Math.sin(t * 80) * 6 : 0; } },
358
+ };
359
+
360
+ /* =======================================================================
361
+ * 狂潮
362
+ * ===================================================================== */
363
+ const D0 = K.day('2015-08-07');
364
+ CH.mania = {
365
+ draw(ctx, L, t, H) {
366
+ const P = L.portrait;
367
+ const ek = H.lf('erc20', 'peak18', 0.6, 0.6);
368
+ if (ek > 0) {
369
+ const r = K.rng(2017); const B = L.box;
370
+ const cols = [C.mag, C.gold, C.cyan, C.violet, C.green, C.eth2];
371
+ const n = Math.floor(K.clamp(H.m('erc20') / 3) * 26) + (H.m('ico') > 0 ? 30 : 0);
372
+ for (let i = 0; i < Math.min(56, n); i++) {
373
+ const bx = B.x + B.w * (0.06 + r() * 0.88); let by = B.y + B.h * (0.12 + r() * 0.76);
374
+ const rain = H.m('ico') > 0 && i >= 20; if (rain) by = B.y - 100 + K.fract(r() + H.m('ico') * 0.25) * (B.h + 200);
375
+ hexToken(ctx, bx, by, 34 + r() * 26, cols[i % cols.length], i % 4 === 0 ? 'ERC20' : '', ek * 0.85);
376
+ }
377
+ const [x, y] = H.bx(0.5, 0.48, 0.5, 0.45);
378
+ if (H.m('ico') < 0) pop(ctx, x, y, K.clamp(H.m('erc20') / 0.6), () => { K.glass(ctx, -260, -90, 520, 180, { tint: C.mag }); K.text(ctx, 'ERC-20', 0, -16, { size: 76, fam: 'num', color: C.white, glow: C.mag }); K.text(ctx, '人人都能发行代币', 0, 50, { size: 28, color: K.rgba(C.white, 0.8) }); });
379
+ else if (H.m('cats') < 0) pop(ctx, x, y, K.clamp(H.m('ico') / 0.6), () => { K.text(ctx, 'ICO', 0, -20, { size: 170, fam: 'num', color: C.white, glow: C.mag, glowR: 70 }); K.text(ctx, '2017 · 首次代币发行狂潮', 0, 96, { size: 34, color: C.gold }); });
380
+ }
381
+ const ck = H.lf('cats', 'peak18', 0.6, 0.6);
382
+ if (ck > 0) {
383
+ const [x, y] = H.bx(0.5, 0.5, 0.5, 0.45);
384
+ pop(ctx, x, y, ck, () => {
385
+ K.glass(ctx, -380, -200, 760, 400, { tint: C.orange, tint2: C.mag });
386
+ // 像素猫
387
+ const cat = ['..X...X..', '.XXX.XXX.', '.XXXXXXX.', 'XX.XXX.XX', 'XXXXXXXXX', 'XXX.X.XXX', '.XXXXXXX.', '..XXXXX..'];
388
+ for (let k = 0; k < 3; k++) { const ox = -260 + k * 190, oy = -150 + Math.sin(t * 4 + k) * 6; const col = [C.orange, C.pink || C.mag, C.cyan][k];
389
+ cat.forEach((row, ry) => [...row].forEach((c, rx) => { if (c === 'X') { ctx.fillStyle = col; ctx.fillRect(ox + rx * 13, oy + ry * 13, 12, 12); } })); }
390
+ K.text(ctx, 'CryptoKitties · 加密猫', 0, 18, { size: 36, fam: 'title', weight: 400, color: C.white });
391
+ // 拥堵仪表
392
+ const lv = K.clamp(H.m('cats') / 2.5);
393
+ K.rr(ctx, -300, 80, 600, 26, 13); ctx.fillStyle = 'rgba(255,255,255,.08)'; ctx.fill();
394
+ K.rr(ctx, -300, 80, 600 * lv, 26, 13); ctx.fillStyle = K.mix(C.green, C.red, lv); ctx.fill();
395
+ K.text(ctx, '网络拥堵 ' + Math.round(lv * 100) + '%', 0, 148, { size: 28, fam: 'body', color: lv > 0.8 ? '#FF8FA3' : C.white });
396
+ });
397
+ }
398
+ const pk = H.lf('peak18', null, 0.8);
399
+ if (pk > 0) {
400
+ const B = L.box; const pad = P ? 30 : 90;
401
+ const d1 = K.day('2019-03-01');
402
+ const t18 = K.day('2018-01-13'), c18 = K.day('2018-12-15');
403
+ const up = H.m('crash18') > 0 ? K.lerp(t18, c18, E.io3(H.m('crash18') / 3)) : K.lerp(D0, t18, E.io3(H.m('peak18') / 2.4));
404
+ W.priceChart(ctx, { x: B.x + pad, y: B.y + (P ? 120 : 80), w: B.w - pad * 2, h: B.h - (P ? 260 : 190), d0: D0, d1, upTo: up, alpha: pk, color: C.mag,
405
+ notes: [{ day: t18, label: '2018.01 高点', sub: K.money(K.priceAtDay(t18)), color: C.gold, a: K.clamp((up - t18 + 3) / 6) }, { day: c18, label: '跌幅超 90%', sub: K.money(K.priceAtDay(c18)), color: C.red, a: K.clamp((up - c18 + 3) / 6), below: true }] });
406
+ }
407
+ },
408
+ };
409
+
410
+ /* =======================================================================
411
+ * DeFi 之夏
412
+ * ===================================================================== */
413
+ CH.defi = {
414
+ draw(ctx, L, t, H) {
415
+ const P = L.portrait;
416
+ const bk = H.lf(null, 'defi', 0.8, 0.4, 0);
417
+ if (bk > 0) { const [x, y] = H.bx(0.5, 0.48, 0.5, 0.45); K.text(ctx, 'BUIDL', x, y - 10, { size: P ? 160 : 200, fam: 'num', color: K.rgba(C.white, 0.9), glow: C.green, glowR: 60, alpha: bk * K.clamp(H.m('builders') + 0.6), ls: 20 }); K.text(ctx, '熊市里,继续建设', x, y + 110, { size: 34, color: C.green, alpha: bk }); }
418
+ const dk = H.lf('defi', 'swap', 0.6, 0.5);
419
+ if (dk > 0) { const [x, y] = H.bx(0.5, 0.48, 0.5, 0.45); burst(ctx, x, y, H.m('defi'), C.green, 36, 460); pop(ctx, x, y, dk, () => { K.text(ctx, 'DeFi', 0, -20, { size: 190, fam: 'num', color: C.white, glow: C.green, glowR: 70 }); K.text(ctx, '去中心化金融 · 2020 夏天', 0, 110, { size: 36, color: C.green }); }); }
420
+ // 兑换 + 借贷
421
+ const sk = H.lf('swap', 'lego', 0.7, 0.6);
422
+ if (sk > 0) {
423
+ const [x, y] = H.bx(0.28, 0.5, 0.5, 0.3);
424
+ pop(ctx, x, y, sk, () => {
425
+ K.glass(ctx, -300, -170, 600, 340, { tint: C.mag, tint2: C.cyan });
426
+ K.text(ctx, 'Uniswap · 兑换', 0, -120, { size: 34, fam: 'title', weight: 400, color: C.white });
427
+ const ph = (t * 0.5) % 1; const a = ph * Math.PI;
428
+ const ax = Math.cos(a) * 140, ay = Math.sin(a) * 50;
429
+ K.glow(ctx, -ax, -ay + 20, 70, C.eth, 0.7); K.neonCircle(ctx, -ax, -ay + 20, 54, C.eth2, 3); ethGlyph(ctx, -ax, -ay + 20, 30);
430
+ K.glow(ctx, ax, ay + 20, 70, C.green, 0.7); K.neonCircle(ctx, ax, ay + 20, 54, C.green, 3); K.text(ctx, 'TOKEN', ax, ay + 21, { size: 22, fam: 'num' });
431
+ K.text(ctx, '无需交易所', 0, 130, { size: 28, color: K.rgba(C.white, 0.75) });
432
+ });
433
+ const lk = K.life(t, H.at('lend'), H.at('lego'), 0.7, 0.6);
434
+ const [x2, y2] = H.bx(0.72, 0.5, 0.5, 0.72);
435
+ pop(ctx, x2, y2, lk, () => {
436
+ K.glass(ctx, -300, -170, 600, 340, { tint: C.green, tint2: C.gold });
437
+ K.text(ctx, '借贷协议', 0, -120, { size: 34, fam: 'title', weight: 400, color: C.white });
438
+ K.neonCircle(ctx, 0, 10, 70, C.gold, 3); K.text(ctx, '资金池', 0, 12, { size: 26 });
439
+ const ph = K.fract(t * 0.7);
440
+ K.neon(ctx, [[-240, 10], [-80, 10]], C.green, 2, 0.8); K.glow(ctx, -240 + 160 * ph, 10, 18, C.white, 0.9); K.text(ctx, '存款', -230, -26, { size: 24, color: C.green });
441
+ K.neon(ctx, [[80, 10], [240, 10]], C.gold, 2, 0.8); K.glow(ctx, 80 + 160 * ph, 10, 18, C.white, 0.9); K.text(ctx, '借款', 230, -26, { size: 24, color: C.gold });
442
+ K.text(ctx, '无需银行', 0, 130, { size: 28, color: K.rgba(C.white, 0.75) });
443
+ });
444
+ }
445
+ // 货币乐高
446
+ const gk = H.lf('lego', 'beacon', 0.6, 0.6);
447
+ if (gk > 0) {
448
+ const [x, y] = H.bx(0.5, 0.62, 0.5, 0.6);
449
+ const names = ['Maker', 'Compound', 'Aave', 'Uniswap', 'Curve', 'Yearn'];
450
+ const cols = [C.green, C.cyan, C.violet, C.mag, C.gold, C.eth2];
451
+ const bw = P ? 250 : 280, bh = 74;
452
+ names.forEach((nm, i) => {
453
+ const row = Math.floor(i / 2), c = i % 2;
454
+ const d = H.m('lego') - i * 0.35; if (d <= 0) return;
455
+ const fall = E.out3(d / 0.6);
456
+ const bx = x + (c - 0.5) * (bw + 10) + (row % 2 ? bw * 0.25 : 0), by = y + 120 - row * (bh + 8) - (1 - fall) * 300;
457
+ ctx.save(); ctx.globalAlpha *= gk * K.clamp(d / 0.3);
458
+ K.rr(ctx, bx - bw / 2, by - bh / 2, bw, bh, 10); ctx.fillStyle = K.rgba(cols[i], 0.25); ctx.fill(); ctx.strokeStyle = K.rgba(cols[i], 0.95); ctx.lineWidth = 2.5; ctx.stroke();
459
+ for (let s = 0; s < 4; s++) { K.rr(ctx, bx - bw / 2 + 22 + s * (bw - 40) / 4, by - bh / 2 - 14, 36, 16, 5); ctx.fillStyle = K.rgba(cols[i], 0.55); ctx.fill(); }
460
+ K.text(ctx, nm, bx, by + 2, { size: 30, fam: 'num', color: C.white });
461
+ ctx.restore();
462
+ });
463
+ K.text(ctx, '货币乐高 · Money Legos', x, y - (P ? 250 : 230), { size: 46, fam: 'title', weight: 400, color: C.white, alpha: gk, glow: C.green });
464
+ }
465
+ // 信标链
466
+ const bc = H.lf('beacon', null, 0.8);
467
+ if (bc > 0) {
468
+ const [x, y] = H.bx(0.5, 0.5, 0.5, 0.48);
469
+ const k = E.io3(H.m('beacon') / 1.6);
470
+ W.blockchain(ctx, { x, y: y + (P ? 120 : 110), s: P ? 52 : 64, n: 14, lit: 14, alpha: bc, color: C.orange, labels: false, offset: (t * 0.8) % 1.6, ry: -0.45, rx: -0.2 });
471
+ W.blockchain(ctx, { x, y: y - (P ? 120 : 110), s: P ? 52 : 64, n: 14, lit: 14 * k, alpha: bc, color: C.gold, labels: false, offset: (t * 0.8) % 1.6, ry: -0.45, rx: -0.2 });
472
+ K.text(ctx, '主网 · 工作量证明', x - (P ? 0 : 520), y + (P ? 230 : 200), { size: 30, color: C.orange, alpha: bc });
473
+ K.text(ctx, '信标链 · 权益证明 · 2020.12.01', x - (P ? 0 : 420), y - (P ? 230 : 200), { size: 30, color: C.gold, alpha: bc * k });
474
+ }
475
+ },
476
+ };
477
+
478
+ /* =======================================================================
479
+ * 燃烧与 NFT
480
+ * ===================================================================== */
481
+ CH.burn = {
482
+ draw(ctx, L, t, H) {
483
+ const P = L.portrait; const B = L.box;
484
+ const sp = H.m('spot');
485
+ if (sp > -0.5 && H.m('nft') < 0.5) { const c = W.crystalPose(t, L); const a = K.clamp(sp + 0.5) * K.clamp(1 - H.m('nft') * 2); const g = ctx.createRadialGradient(c.x, c.y, 0, c.x, c.y, 520); g.addColorStop(0, K.rgba(C.gold, 0.35 * a)); g.addColorStop(1, K.rgba(C.gold, 0)); ctx.fillStyle = g; ctx.fillRect(0, 0, L.W, L.H); }
486
+ // NFT 画廊
487
+ const nk = H.lf('nft', 'london', 0.8, 0.6);
488
+ if (nk > 0) {
489
+ const fr = P ? [[0.24, 0.24], [0.76, 0.24], [0.5, 0.78]] : [[0.16, 0.48], [0.5, 0.48], [0.84, 0.48]];
490
+ fr.forEach(([fx, fy], i) => {
491
+ const x = B.x + B.w * fx, y = B.y + B.h * fy; const s = P ? 300 : 340;
492
+ pop(ctx, x, y, K.life(t, H.at('nft') + i * 0.25, H.at('london'), 0.7, 0.5), () => {
493
+ K.rr(ctx, -s / 2 - 16, -s / 2 - 16, s + 32, s + 32, 6); ctx.fillStyle = '#1a1420'; ctx.fill(); ctx.strokeStyle = C.gold; ctx.lineWidth = 4; ctx.stroke();
494
+ const r = K.rng(31 + i); // 生成艺术
495
+ for (let k = 0; k < 14; k++) { const cx = (r() - 0.5) * s * 0.8, cy = (r() - 0.5) * s * 0.8; const rr = 20 + r() * 70; const col = [C.mag, C.cyan, C.gold, C.violet, C.orange][k % 5]; const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, rr); g.addColorStop(0, K.rgba(col, 0.9)); g.addColorStop(1, K.rgba(col, 0)); ctx.fillStyle = g; ctx.fillRect(-s / 2, -s / 2, s, s); }
496
+ ctx.strokeStyle = 'rgba(255,255,255,.35)'; ctx.lineWidth = 2; for (let k = 0; k < 6; k++) { ctx.beginPath(); ctx.arc(0, 0, 20 + k * 22 + Math.sin(t + k) * 4, 0, K.TAU); ctx.stroke(); }
497
+ });
498
+ });
499
+ const [x, y] = P ? [B.x + B.w * 0.5, B.y + B.h * 0.5] : [B.x + B.w * 0.5, B.y + B.h * 0.92];
500
+ pop(ctx, x, y, K.life(t, H.at('nft') + 1.2, H.at('london'), 0.7, 0.5), () => { K.glass(ctx, -300, -60, 600, 120, { tint: C.gold }); K.text(ctx, 'Beeple · 佳士得 ≈ $69M', 0, 2, { size: 36, fam: 'body', color: C.gold }); });
501
+ }
502
+ // 伦敦升级 + 燃烧
503
+ const lk = H.lf('london', 'peak21', 0.7, 0.6);
504
+ if (lk > 0) {
505
+ const [x, y] = H.bx(0.5, 0.12, 0.5, 0.02);
506
+ pop(ctx, x, y, lk, () => { chip(ctx, 0, 0, 'London 升级 · EIP-1559 · 2021.08.05', { size: P ? 30 : 34, color: C.orange, color2: C.gold }); });
507
+ const fk = K.life(t, H.at('fire'), H.at('peak21'), 0.8, 0.6);
508
+ if (fk > 0) {
509
+ const c = W.crystalPose(t, L);
510
+ const r = K.rng(1559);
511
+ for (let i = 0; i < 90; i++) { const ph = K.fract(t * (0.6 + r() * 0.5) + r()); const fx = c.x + (r() - 0.5) * c.s * 2.6 * (1 - ph * 0.6); const fy = c.y + c.s * 2.4 - ph * c.s * 4.2; const col = ph < 0.3 ? '#FFF1B8' : ph < 0.6 ? C.gold : ph < 0.85 ? C.orange : C.red; K.glow(ctx, fx, fy, (18 + r() * 26) * (1 - ph * 0.6), col, (1 - ph) * 0.75 * fk); }
512
+ // 掉入火焰的硬币
513
+ for (let i = 0; i < 5; i++) { const ph = K.fract(t * 0.45 + i / 5); const cx = c.x + (i - 2) * c.s * 0.5, cy = c.y - c.s * 3 + ph * c.s * 4.6; ctx.save(); ctx.globalAlpha *= (1 - ph) * fk; K.neonCircle(ctx, cx, cy, 20, C.gold, 2); ethGlyph(ctx, cx, cy, 12, C.gold, 1); ctx.restore(); }
514
+ const [tx, ty] = H.bx(0.84, 0.5, 0.5, 0.92);
515
+ K.text(ctx, 'BURN', tx, ty - 30, { size: 80, fam: 'num', color: C.orange, glow: C.red, alpha: fk });
516
+ K.text(ctx, '基础手续费直接销毁', tx, ty + 34, { size: 28, color: C.white, alpha: fk });
517
+ const dk = K.life(t, H.at('deflation'), H.at('peak21'), 0.7, 0.5);
518
+ if (dk > 0) { const [ax, ay] = H.bx(0.16, 0.5, 0.5, 0.82); if (!P) { K.text(ctx, '供应', ax, ay - 110, { size: 30, alpha: dk, color: K.rgba(C.white, 0.8) }); K.neon(ctx, [[ax, ay - 70], [ax, ay + 60]], C.cyan, 3, dk); K.neon(ctx, [[ax - 26, ay + 30], [ax, ay + 64], [ax + 26, ay + 30]], C.cyan, 3, dk); K.text(ctx, '通缩力量', ax, ay + 110, { size: 32, color: C.cyan, alpha: dk }); } else { K.text(ctx, '↓ 通缩力量', ax, ay - 160, { size: 36, color: C.cyan, alpha: dk }); } }
519
+ }
520
+ }
521
+ const pk = H.lf('peak21', null, 0.8);
522
+ if (pk > 0) {
523
+ const pad = P ? 30 : 90; const t21 = K.day('2021-11-10');
524
+ const up = K.lerp(K.day('2019-03-01'), t21, E.io3(H.m('peak21') / 2.6));
525
+ W.priceChart(ctx, { x: B.x + pad, y: B.y + (P ? 120 : 80), w: B.w - pad * 2, h: B.h - (P ? 260 : 190), d0: D0, d1: K.day('2022-03-01'), upTo: up, alpha: pk, color: C.orange,
526
+ notes: [{ day: K.day('2018-01-13'), label: '2018 高点', color: C.dim, a: 0.8 }, { day: t21, label: '2021.11 历史新高', sub: K.money(K.priceAtDay(t21)), color: C.gold, a: K.clamp((up - t21 + 4) / 8) }] });
527
+ }
528
+ },
529
+ crystal(o, t, H) { if (H.m('peak21') > 0) o.alpha *= 1 - K.clamp(H.m('peak21') / 0.6); if (H.m('nft') > 0 && H.m('london') < 0) o.alpha *= 0.35; },
530
+ };
531
+
532
+ /* =======================================================================
533
+ * 合并
534
+ * ===================================================================== */
535
+ CH.merge = {
536
+ draw(ctx, L, t, H) {
537
+ const P = L.portrait;
538
+ const pw = H.lf('pow', 'engine', 0.7, 0.6), ps = H.lf('pos', 'engine', 0.7, 0.6);
539
+ const off = K.clamp(H.m('mine') / 1.2);
540
+ const rigs = (x, y, a, offK) => {
541
+ for (let i = 0; i < 3; i++) {
542
+ const bx = x + (i - 1) * 130, by = y; ctx.save(); ctx.globalAlpha *= a;
543
+ K.rr(ctx, bx - 55, by - 70, 110, 140, 10); ctx.fillStyle = 'rgba(20,22,34,.9)'; ctx.fill(); ctx.strokeStyle = K.rgba(C.orange, 0.8 * (1 - offK) + 0.2); ctx.lineWidth = 2; ctx.stroke();
544
+ for (const fy of [-30, 30]) { ctx.save(); ctx.translate(bx, by + fy); ctx.rotate(t * 14 * (1 - offK)); for (let b = 0; b < 4; b++) { ctx.rotate(Math.PI / 2); ctx.fillStyle = K.rgba(C.orange, 0.7 * (1 - offK) + 0.15); ctx.fillRect(-3, 2, 6, 20); } ctx.restore(); }
545
+ ctx.fillStyle = (1 - offK) * K.fract(t * 3 + i * 0.3) > 0.5 ? C.green : '#333'; ctx.beginPath(); ctx.arc(bx + 40, by - 58, 4, 0, K.TAU); ctx.fill();
546
+ ctx.restore();
547
+ if (offK < 0.9) K.glow(ctx, bx, by, 90, C.orange, 0.3 * a * (1 - offK));
548
+ }
549
+ };
550
+ if (pw > 0) { const [x, y] = H.bx(0.22, 0.55, 0.27, 0.86); ctx.save(); if (P) { ctx.translate(x, y); ctx.scale(0.62, 0.62); ctx.translate(-x, -y); } rigs(x, y, pw, 0); K.text(ctx, '工作量证明 · PoW', x, y + 130, { size: 34, color: C.orange, alpha: pw }); K.text(ctx, '矿机算力竞争记账', x, y + 176, { size: 26, color: K.rgba(C.white, 0.7), alpha: pw }); ctx.restore(); }
551
+ if (ps > 0) {
552
+ const [x, y] = H.bx(0.78, 0.55, 0.75, 0.86);
553
+ ctx.save(); if (P) { ctx.translate(x, y); ctx.scale(0.62, 0.62); ctx.translate(-x, -y); } ctx.globalAlpha *= ps;
554
+ for (let i = 0; i < 5; i++) { const a = i / 5 * K.TAU + t * 0.4; const sx = x + Math.cos(a) * 110, sy = y + Math.sin(a) * 60; K.glow(ctx, sx, sy, 50, C.gold, 0.6); const sh = [[0, -26], [22, -16], [18, 12], [0, 28], [-18, 12], [-22, -16], [0, -26]].map(([px, py]) => [sx + px, sy + py]); K.neon(ctx, sh, C.gold, 2, 1); }
555
+ ctx.restore();
556
+ K.text(ctx, '权益证明 · PoS', x, y + 130, { size: 34, color: C.gold, alpha: ps }); K.text(ctx, '质押 ETH 的验证者轮流出块', x, y + 176, { size: 26, color: K.rgba(C.white, 0.7), alpha: ps });
557
+ ctx.restore();
558
+ }
559
+ // 空中换引擎
560
+ const ek = H.lf('engine', 'merge', 0.7, 0.5);
561
+ if (ek > 0) {
562
+ const [x, y] = H.bx(0.5, 0.5, 0.5, 0.48); const m = H.m('engine');
563
+ pop(ctx, x, y + Math.sin(t * 1.6) * 8, ek, () => {
564
+ const S = P ? 0.85 : 1;
565
+ ctx.scale(S, S);
566
+ const body = [[-320, 0], [-260, -30], [240, -30], [330, 0], [240, 30], [-260, 30], [-320, 0]];
567
+ K.neon(ctx, body, C.white, 2.5, 0.9);
568
+ K.neon(ctx, [[-40, -28], [60, -220], [120, -220], [80, -28]], C.eth2, 2.5, 0.9);
569
+ K.neon(ctx, [[-40, 28], [60, 220], [120, 220], [80, 28]], C.eth2, 2.5, 0.9);
570
+ K.neon(ctx, [[-260, -28], [-300, -120], [-250, -120], [-200, -28]], C.eth2, 2, 0.8);
571
+ const swap = E.io3((m - 1.2) / 1.4);
572
+ // 旧引擎(橙)脱落,新引擎(金)就位
573
+ ctx.save(); ctx.translate(0, 120 + swap * 260); ctx.globalAlpha *= 1 - swap; K.rr(ctx, 30, -26, 120, 52, 20); ctx.strokeStyle = C.orange; ctx.lineWidth = 3; ctx.stroke(); K.text(ctx, 'PoW', 90, 1, { size: 24, fam: 'num', color: C.orange }); ctx.restore();
574
+ ctx.save(); ctx.translate(-260 * (1 - swap), 120); ctx.globalAlpha *= swap; K.rr(ctx, 30, -26, 120, 52, 20); ctx.strokeStyle = C.gold; ctx.lineWidth = 3; ctx.stroke(); K.glow(ctx, 90, 0, 80, C.gold, 0.6); K.text(ctx, 'PoS', 90, 1, { size: 24, fam: 'num', color: C.gold }); ctx.restore();
575
+ for (let i = 0; i < 6; i++) { const ph = K.fract(t * 1.5 + i / 6); K.neon(ctx, [[-340 - ph * 300, -10 + i * 4], [-380 - ph * 300, -10 + i * 4]], C.cyan, 1.2, 1 - ph); }
576
+ K.text(ctx, '在飞行中更换引擎', 0, -270, { size: 40, fam: 'title', weight: 400, color: C.white, glow: C.eth });
577
+ });
578
+ }
579
+ // 合并时刻
580
+ const mg = H.m('merge');
581
+ if (mg > -0.2) {
582
+ const [x, y] = H.bx(0.5, 0.97, 0.5, 0.9);
583
+ const k = H.lf('merge', P ? 'energy' : null, 0.8, 0.5);
584
+ pop(ctx, x, y, k, () => { K.text(ctx, 'THE MERGE', 0, -30, { size: P ? 80 : 96, fam: 'num', color: C.white, glow: C.gold, ls: 10 }); K.text(ctx, '2022.09.15 合并完成', 0, 46, { size: 34, color: C.gold }); });
585
+ flash(ctx, L, K.clamp(1 - mg / 0.6) * K.clamp(mg * 6 + 1), C.gold);
586
+ const c = W.crystalPose(t, L); burst(ctx, c.x, c.y, mg, C.gold, 50, 600);
587
+ }
588
+ // 矿机熄灭 + 能耗
589
+ const mk = H.lf('mine', null, 0.6);
590
+ if (mk > 0) {
591
+ const [x, y] = H.bx(0.18, 0.6, 0.27, 0.86);
592
+ ctx.save(); if (P) { ctx.translate(x, y); ctx.scale(0.62, 0.62); ctx.translate(-x, -y); } ctx.globalAlpha *= 1 - K.clamp(H.m('energy') / 1); rigs(x, y, mk, off); ctx.restore();
593
+ const ek2 = H.lf('energy', null, 0.6);
594
+ if (ek2 > 0) {
595
+ const [bx, by] = H.bx(0.8, 0.55, 0.5, 0.86);
596
+ const v = 100 - 99.95 * E.io3(H.m('energy') / 2.2);
597
+ pop(ctx, bx, by, ek2, () => {
598
+ K.glass(ctx, -260, -160, 520, 320, { tint: C.green, tint2: C.gold });
599
+ K.text(ctx, '能耗', 0, -110, { size: 30, color: K.rgba(C.white, 0.75) });
600
+ K.rr(ctx, -200, -60, 400, 36, 18); ctx.fillStyle = 'rgba(255,255,255,.08)'; ctx.fill();
601
+ K.rr(ctx, -200, -60, Math.max(8, 400 * v / 100), 36, 18); ctx.fillStyle = K.mix(C.green, C.orange, v / 100); ctx.fill();
602
+ K.text(ctx, '-99.95%', 0, 50, { size: 84, fam: 'num', color: C.green, glow: C.green, alpha: K.clamp(H.m('energy') / 2.2) });
603
+ });
604
+ }
605
+ }
606
+ },
607
+ crystal(o, t, H) {
608
+ const pre = K.clamp(H.m('pow') / 1.5), m = H.m('merge');
609
+ o.split = pre * 0.9 * (m > 0 ? 1 - E.io5(m / 0.5) : 1);
610
+ if (m < 0) { o.tint = C.eth; }
611
+ else { o.tint = K.mix(C.gold, C.eth, K.clamp(m / 4)); o.glow = 1 + 1.5 * K.clamp(1 - m / 2); }
612
+ if (H.m('engine') > 0 && m < 0) o.alpha *= 0.25;
613
+ if (H.m('energy') > 0) { o.y -= o.s * 0.3 * K.clamp(H.m('energy')); }
614
+ },
615
+ };
616
+
617
+ /* =======================================================================
618
+ * 扩容
619
+ * ===================================================================== */
620
+ CH.scale = {
621
+ draw(ctx, L, t, H) {
622
+ const P = L.portrait; const B = L.box;
623
+ const jk = H.lf('jam', 'l2', 0.6, 0.5);
624
+ if (jk > 0) {
625
+ const [x, y] = H.bx(0.5, 0.5, 0.5, 0.45);
626
+ ctx.save(); ctx.globalAlpha *= jk;
627
+ for (let lane = 0; lane < 3; lane++) { const ly = y - 80 + lane * 80; K.neon(ctx, [[B.x, ly + 34], [B.x + B.w, ly + 34]], C.dim, 1, 0.3); const r = K.rng(lane + 4); for (let i = 0; i < 26; i++) { const px = B.x + K.fract(r() + t * 0.012 * (lane + 1)) * B.w; ctx.fillStyle = K.rgba(C.red, 0.85); K.rr(ctx, px, ly, 46, 22, 6); ctx.fill(); } }
628
+ ctx.restore();
629
+ pop(ctx, x, y - 200, jk, () => { K.text(ctx, '太贵 · 太挤', 0, 0, { size: 76, fam: 'title', weight: 400, color: C.white, glow: C.red }); });
630
+ }
631
+ // 分层:L1 + L2
632
+ const lk = H.lf('l2', 'shapella', 0.8, 0.6);
633
+ if (lk > 0) {
634
+ const [x, y] = H.bx(0.5, 0.52, 0.5, 0.5);
635
+ const cam = { x, y, s: P ? 150 : 175, d: 7, ry: 0.55 + Math.sin(t * 0.2) * 0.1, rx: -0.55 };
636
+ const plane = (h, col, label, a) => {
637
+ const pts = [[-2, h, -1.4], [2, h, -1.4], [2, h, 1.4], [-2, h, 1.4]].map((p) => K.proj(p, cam));
638
+ ctx.save(); ctx.globalAlpha *= a; ctx.fillStyle = K.rgba(col, 0.12); ctx.beginPath(); pts.forEach((p, i) => (i ? ctx.lineTo(p[0], p[1]) : ctx.moveTo(p[0], p[1]))); ctx.closePath(); ctx.fill(); ctx.restore();
639
+ K.neon(ctx, pts.concat([pts[0]]), col, 2, a);
640
+ const lp = K.proj([-2.1, h, 1.4], cam); K.text(ctx, label, lp[0] - 10, lp[1], { size: 30, align: 'right', color: col, alpha: a, stroke: 6 });
641
+ };
642
+ plane(-0.9, C.eth, 'L1 主网', lk);
643
+ const l2a = K.clamp(H.m('l2') / 1.2);
644
+ plane(0.6, C.cyan, 'L2 Rollup', lk * l2a);
645
+ const r = K.rng(42);
646
+ for (let i = 0; i < 26; i++) { const ph = K.fract(t * 0.5 + r()); const px = (r() - 0.5) * 3.6, pz = (r() - 0.5) * 2.4; const p = K.proj([px, 0.6 + 0.02, pz], cam); K.glow(ctx, p[0], p[1], 14, C.cyan, lk * l2a * (0.4 + 0.6 * Math.sin(ph * Math.PI))); }
647
+ const pk = K.clamp(H.m('pack') / 0.5);
648
+ for (let i = 0; i < 3; i++) { const ph = K.fract(t * 0.35 + i / 3); const pz = (i - 1) * 0.8; const p = K.proj([0.3 * i - 0.3, K.lerp(0.6, -0.9, ph), pz], cam); ctx.save(); ctx.globalAlpha *= lk * pk * (1 - ph * 0.3); K.rr(ctx, p[0] - 26, p[1] - 18, 52, 36, 6); ctx.fillStyle = K.rgba(C.gold, 0.3); ctx.fill(); ctx.strokeStyle = C.gold; ctx.lineWidth = 2; ctx.stroke(); ctx.restore(); }
649
+ if (pk > 0) K.text(ctx, '打包 → 交回主网结算', x, y + (P ? 300 : 310), { size: 30, color: C.gold, alpha: lk * pk });
650
+ }
651
+ // 上海 / 坎昆
652
+ const sk = H.lf('shapella', 'city', 0.7, 0.6);
653
+ if (sk > 0) {
654
+ const [x1, y1] = H.bx(0.28, 0.4, 0.5, 0.25), [x2, y2] = H.bx(0.72, 0.4, 0.5, 0.58);
655
+ const w = P ? 640 : 600;
656
+ card(ctx, x1, y1, w, 250, '上海升级 Shapella', ['2023.04 开放质押提款'], { alpha: sk, color: C.green, tag: 'WITHDRAWALS' });
657
+ const dk = K.life(t, H.at('dencun'), H.at('city'), 0.7, 0.6);
658
+ card(ctx, x2, y2, w, 250, '坎昆升级 Dencun', ['2024.03 EIP-4844 · Blob 数据空间'], { alpha: dk, color: C.violet, tag: 'PROTO-DANKSHARDING' });
659
+ if (dk > 0) { for (let i = 0; i < 4; i++) { const bx = x2 - 160 + i * 106, by = y2 + 98 + Math.sin(t * 2 + i) * 6; const g = ctx.createRadialGradient(bx, by, 0, bx, by, 40); g.addColorStop(0, K.rgba(C.violet, 0.8 * dk)); g.addColorStop(1, K.rgba(C.violet, 0)); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(bx, by, 40, 0, K.TAU); ctx.fill(); } }
660
+ const fk = K.life(t, H.at('fees'), H.at('city'), 0.7, 0.6);
661
+ if (fk > 0) {
662
+ const [fx, fy] = H.bx(0.5, 0.86, 0.5, 0.86);
663
+ const v = 1 - 0.9 * E.io3(H.m('fees') / 2);
664
+ pop(ctx, fx, fy, fk, () => { K.text(ctx, 'L2 手续费', -330, 0, { size: 30, align: 'left', color: K.rgba(C.white, 0.8) }); K.rr(ctx, -150, -16, 420, 32, 16); ctx.fillStyle = 'rgba(255,255,255,.08)'; ctx.fill(); K.rr(ctx, -150, -16, 420 * v, 32, 16); ctx.fillStyle = K.mix(C.green, C.red, v); ctx.fill(); K.text(ctx, '↓ 大幅下降', 340, 0, { size: 30, align: 'left', color: C.green }); });
665
+ }
666
+ }
667
+ // 多层城市
668
+ const ck = H.lf('city', null, 0.9);
669
+ if (ck > 0) {
670
+ const r = K.rng(808); const base = B.y + B.h * (P ? 0.85 : 0.9);
671
+ for (let layer = 0; layer < 3; layer++) {
672
+ const col = [C.eth, C.cyan, C.violet][layer]; const yb = base - layer * (P ? 150 : 120);
673
+ for (let i = 0; i < 22; i++) { const bw = 40 + r() * 50, bh = 60 + r() * (P ? 200 : 170); const bx = B.x + (i / 22) * B.w + (r() - 0.5) * 20; const grow = E.out3((H.m('city') - layer * 0.4 - i * 0.02) / 0.9); if (grow <= 0) continue;
674
+ ctx.save(); ctx.globalAlpha *= ck * (1 - layer * 0.22); ctx.fillStyle = K.rgba(col, 0.1); ctx.fillRect(bx, yb - bh * grow, bw, bh * grow); ctx.strokeStyle = K.rgba(col, 0.75); ctx.lineWidth = 1.5; ctx.strokeRect(bx, yb - bh * grow, bw, bh * grow);
675
+ for (let w = 0; w < 5; w++) if (r() > 0.45) { ctx.fillStyle = K.rgba(C.white, 0.5); ctx.fillRect(bx + 8 + (w % 2) * (bw / 2 - 4), yb - bh * grow + 12 + w * 18, 6, 6); }
676
+ ctx.restore(); }
677
+ K.text(ctx, ['L1 · 安全与结算', 'L2 · 速度', 'L2 · 应用'][layer], B.x + B.w - 10, yb - (P ? 140 : 110), { size: 26, align: 'right', color: col, alpha: ck * K.clamp(H.m('city') - layer * 0.4), stroke: 6 });
678
+ }
679
+ }
680
+ },
681
+ };
682
+
683
+ /* =======================================================================
684
+ * 走向主流
685
+ * ===================================================================== */
686
+ CH.wall = {
687
+ draw(ctx, L, t, H) {
688
+ const P = L.portrait; const B = L.box;
689
+ // 行情跑马灯
690
+ const tk = H.lf('wallst', 'ath', 0.6, 0.6);
691
+ if (tk > 0) {
692
+ const items = ['ETH', 'SPOT ETF', 'NYSE', 'NASDAQ', 'CBOE', 'STAKING', 'L2', 'DEFI'];
693
+ const y0 = B.y + (P ? -30 : 10);
694
+ ctx.save(); ctx.globalAlpha *= tk; ctx.fillStyle = 'rgba(46,230,166,.08)'; ctx.fillRect(0, y0 - 28, L.W, 56);
695
+ ctx.font = K.font('num', 28, 600); ctx.textBaseline = 'middle'; let x = -K.fract(t * 0.08) * 1600;
696
+ for (let rep = 0; rep < 3; rep++) items.forEach((s, i) => { ctx.fillStyle = i % 2 ? C.green : C.white; ctx.fillText(s + ' ▲', x, y0); x += ctx.measureText(s + ' ▲').width + 60; });
697
+ ctx.restore();
698
+ }
699
+ const wk = H.lf('wallst', 'etf', 0.7, 0.4);
700
+ if (wk > 0) { const [x, y] = H.bx(0.5, 0.5, 0.5, 0.45); pop(ctx, x, y, wk, () => { K.text(ctx, 'WALL STREET', 0, -20, { size: P ? 110 : 150, fam: 'num', color: C.white, glow: C.green, glowR: 50, ls: 12 }); K.text(ctx, '传统金融入场', 0, 90, { size: 40, color: C.green }); }); }
701
+ const ek = H.lf('etf', 'ath', 0.8, 0.6);
702
+ if (ek > 0) {
703
+ const [x, y] = H.bx(0.5, 0.48, 0.5, 0.42);
704
+ pop(ctx, x, y, ek, () => {
705
+ const w = P ? 820 : 900, h = 380;
706
+ K.glass(ctx, -w / 2, -h / 2, w, h, { tint: C.green, tint2: C.gold });
707
+ K.text(ctx, 'SPOT ETH ETF', 0, -110, { size: P ? 74 : 86, fam: 'num', color: C.white, glow: C.green, ls: 4 });
708
+ K.text(ctx, '美国现货以太坊 ETF 开始交易', 0, -30, { size: 36, color: C.green });
709
+ K.text(ctx, '2024.07.23', 0, 40, { size: 54, fam: 'num', color: C.gold });
710
+ K.text(ctx, '首日净流入 ≈ $106.6M', 0, 120, { size: 30, fam: 'body', color: K.rgba(C.white, 0.75), alpha: K.clamp(H.m('etf') - 1.5) });
711
+ });
712
+ const pk = K.life(t, H.at('pectra'), H.at('ath'), 0.6, 0.5);
713
+ const [px, py] = H.bx(0.5, 0.9, 0.5, 0.84);
714
+ pop(ctx, px, py, pk, () => chip(ctx, 0, 0, 'Pectra 升级 · 2025.05 · 质押与钱包再进化', { size: P ? 28 : 32, color: C.violet, color2: C.cyan }));
715
+ }
716
+ const ak = H.lf('ath', null, 0.8);
717
+ if (ak > 0) {
718
+ const pad = P ? 30 : 90; const tA = K.day('2025-08-23');
719
+ const up = H.m('fusaka') > 0 ? K.lerp(tA, K.day('2025-12-03'), E.io3(H.m('fusaka') / 2)) : K.lerp(K.day('2021-11-10'), tA, E.io3(H.m('ath') / 2.4));
720
+ const top = P ? 120 : 80;
721
+ W.priceChart(ctx, { x: B.x + pad, y: B.y + top, w: B.w - pad * 2, h: B.h - (P ? 300 : 230), d0: D0, d1: K.day('2026-10-07'), upTo: up, alpha: ak, color: C.green,
722
+ notes: [{ day: K.day('2021-11-10'), label: '2021', color: C.dim, a: 0.8 }, { day: tA, label: '2025.08 历史新高(日收盘)', sub: K.money(K.priceAtDay(tA)), color: C.gold, a: K.clamp((up - tA + 3) / 6) }] });
723
+ const fk = K.life(t, H.at('fusaka'), Infinity, 0.6);
724
+ if (fk > 0) { const [fx, fy] = H.bx(0.5, 0.97, 0.5, 0.96); pop(ctx, fx, fy, fk, () => chip(ctx, 0, 0, 'Fusaka 升级 · 2025.12 · PeerDAS', { size: P ? 30 : 32, color: C.cyan, color2: C.violet })); }
725
+ }
726
+ },
727
+ };
728
+
729
+ /* =======================================================================
730
+ * 此刻
731
+ * ===================================================================== */
732
+ CH.now = {
733
+ draw(ctx, L, t, H) {
734
+ const P = L.portrait; const B = L.box;
735
+ const dk = H.lf('today', 'pnow', 0.8, 0.6);
736
+ if (dk > 0) {
737
+ const [x, y] = H.bx(0.5, 0.55, 0.5, 0.5);
738
+ pop(ctx, x, y, dk, () => {
739
+ K.text(ctx, 'TODAY', 0, -150, { size: 30, fam: 'num', color: C.cyan, ls: 14 });
740
+ K.text(ctx, '2026.10.06', 0, -80, { size: P ? 96 : 110, fam: 'num', color: C.white, glow: C.cyan });
741
+ const steps = [['Sepolia 测试网', '今天上线', C.green, true], ['Hoodi 测试网', '下一步', C.gold, false], ['主网', '日期待定', C.dim, false]];
742
+ const sk = K.clamp(H.m('sepolia') / 0.6);
743
+ steps.forEach(([a, b, col, done], i) => {
744
+ const sx = (i - 1) * (P ? 290 : 330), sy = 50;
745
+ ctx.save(); ctx.globalAlpha *= K.clamp(sk * 3 - i * 0.6);
746
+ K.glass(ctx, sx - 140, sy - 50, 280, 110, { tint: col });
747
+ K.text(ctx, a, sx, sy - 12, { size: 30, color: C.white }); K.text(ctx, (done ? '✓ ' : '') + b, sx, sy + 30, { size: 24, color: col });
748
+ ctx.restore();
749
+ if (i < 2) K.neon(ctx, [[sx + 145, sy], [sx + (P ? 290 : 330) - 145, sy]], C.dim, 1.5, 0.6 * sk);
750
+ });
751
+ K.text(ctx, 'Glamsterdam 升级', 0, -200 - 30, { size: 46, fam: 'title', weight: 400, color: C.white, alpha: sk, glow: C.eth });
752
+ const ek = K.clamp(H.m('epbs') / 0.6);
753
+ if (ek > 0) { chip(ctx, -(P ? 210 : 260), 190, '提议者 / 构建者分离 ePBS', { size: P ? 26 : 30, color: C.violet, alpha: ek }); chip(ctx, (P ? 210 : 260), 190, '区块级访问列表 BAL', { size: P ? 26 : 30, color: C.cyan, alpha: K.clamp(ek - 0.4) }); }
754
+ });
755
+ }
756
+ const pk = H.lf('pnow', null, 0.8);
757
+ if (pk > 0) {
758
+ const pad = P ? 30 : 90; const last = window.ETH_PRICE.pts[window.ETH_PRICE.pts.length - 1][0];
759
+ const up = K.lerp(K.day('2025-12-03'), last, E.io3(H.m('pnow') / 2));
760
+ W.priceChart(ctx, { x: B.x + pad, y: B.y + (P ? 120 : 80), w: B.w - pad * 2, h: B.h - (P ? 300 : 230), d0: D0, d1: last + 30, upTo: up, alpha: pk, color: C.cyan,
761
+ notes: [{ day: K.day('2025-08-23'), label: '历史高点', color: C.gold, a: 0.9 }] });
762
+ const bk = K.life(t, H.at('blocks'), Infinity, 0.6);
763
+ if (bk > 0) { const [x, y] = H.bx(0.5, 0.97, 0.5, 0.97); const beat = Math.pow(K.fract(t / 12 * 1), 8); pop(ctx, x, y, bk, () => chip(ctx, 0, 0, '新区块 · 每 12 秒', { size: 32, color: C.cyan, alpha: 1 })); K.glow(ctx, x, y, 140, C.cyan, 0.4 * bk * (0.3 + 0.7 * (1 - K.fract(t / 12 * 12 / 12)))); }
764
+ }
765
+ },
766
+ crystal(o, t, H) { if (H.m('pnow') > 0) o.alpha *= 1 - K.clamp(H.m('pnow') / 0.6); },
767
+ };
768
+
769
+ /* =======================================================================
770
+ * 结语
771
+ * ===================================================================== */
772
+ CH.end = {
773
+ draw(ctx, L, t, H) {
774
+ const P = L.portrait;
775
+ const c = W.crystalPose(t, L);
776
+ const gk = H.lf('globe', 'bye', 1.0, 0.8);
777
+ if (gk > 0) W.globe(ctx, { x: c.x, y: c.y, r: c.s * 2.4, t, ry: t * 0.15, arcs: 22, alpha: gk * 0.55, color: C.eth, color2: C.mag });
778
+ const icons = [['s1', '黑客', '2016'], ['s2', '分裂', 'ETC'], ['s3', '泡沫与寒冬', '2018'], ['s4', '换引擎', '2022']];
779
+ icons.forEach(([m, a, b], i) => {
780
+ const k = K.life(t, H.at(m), H.at('bye'), 0.6, 0.6);
781
+ if (k <= 0) return;
782
+ const x = P ? L.box.x + L.box.w * (0.14 + i * 0.24) : c.x + (i - 1.5) * 300, y = c.y + c.s * (P ? 2.9 : 2.1);
783
+ pop(ctx, x, y, k, () => { K.glass(ctx, -120, -60, 240, 120, { tint: [C.red, C.etc, C.mag, C.gold][i] }); K.text(ctx, a, 0, -14, { size: 32, color: C.white }); K.text(ctx, b, 0, 28, { size: 24, fam: 'num', color: [C.red, '#7CE07C', C.mag, C.gold][i] }); });
784
+ });
785
+ const ok = H.lf('open', 'bye', 0.9, 0.6);
786
+ if (ok > 0) { const [x, y] = H.bx(0.5, 0.12, 0.5, 0.02); K.text(ctx, '未完待续', x, y, { size: P ? 64 : 72, fam: 'title', weight: 400, color: C.white, alpha: ok, glow: C.eth, ls: 12 }); }
787
+ const ck = H.lf('cta', 'bye', 0.7, 0.6);
788
+ if (ck > 0) { const [x, y] = H.bx(0.82, 0.12, 0.5, 0.12); if (!P) pop(ctx, x, y, ck, () => chip(ctx, 0, 0, '💬 评论区见', { size: 32, color: C.mag })); else pop(ctx, x, y + 60, ck, () => chip(ctx, 0, 0, '💬 评论区聊聊:下一个十年?', { size: 30, color: C.mag })); }
789
+ const bk = H.lf('bye', null, 1.0);
790
+ if (bk > 0) {
791
+ const [x, y] = H.bx(0.5, 0.72, 0.5, 0.7);
792
+ pop(ctx, x, y, bk, () => {
793
+ K.text(ctx, '以太坊简史', 0, -40, { size: P ? 96 : 110, fam: 'title', weight: 400, color: C.white, glow: C.eth, ls: 6 });
794
+ K.text(ctx, 'ETHEREUM · 2013 — ∞', 0, 46, { size: 30, fam: 'num', color: C.eth2, ls: 10 });
795
+ K.text(ctx, '本视频仅作科普,不构成任何投资建议 · 价格为 ETH 日收盘价', 0, 112, { size: P ? 22 : 24, color: K.rgba(C.white, 0.6), weight: 500 });
796
+ });
797
+ }
798
+ },
799
+ crystal(o, t, H) { const b = H.m('bye'); o.y -= o.s * 0.25 * Math.sin(t * 0.8); if (b > 0) { o.s *= 1 - 0.35 * E.io3(b / 1.2); o.y -= o.s * 0.9 * E.io3(b / 1.2); } },
800
+ };
801
+ })();
ethereum/js/core.js ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ==========================================================================
2
+ * core.js — 以太坊简史 · 基础库
3
+ * 数学 / 缓动 / 3D 投影 / 发光绘制 / 字体排版 / 时间轴
4
+ * ========================================================================== */
5
+ (() => {
6
+ const K = (window.K = {});
7
+ const TAU = Math.PI * 2;
8
+ K.TAU = TAU;
9
+
10
+ /* ---------- 数学 ---------- */
11
+ K.clamp = (v, a = 0, b = 1) => (v < a ? a : v > b ? b : v);
12
+ K.lerp = (a, b, k) => a + (b - a) * k;
13
+ K.map = (v, a, b, c, d, clamp = true) => { let k = (v - a) / (b - a); if (clamp) k = K.clamp(k); return c + (d - c) * k; };
14
+ K.fract = (x) => x - Math.floor(x);
15
+ K.hash = (n) => K.fract(Math.sin(n * 91.3458 + 47.123) * 43758.5453);
16
+ K.rng = (seed) => { let a = seed >>> 0 || 1; return () => { a |= 0; a = (a + 0x6d2b79f5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; };
17
+ // 平滑噪声(1D)
18
+ K.noise = (x) => { const i = Math.floor(x), f = x - i, u = f * f * (3 - 2 * f); return K.lerp(K.hash(i), K.hash(i + 1), u) * 2 - 1; };
19
+
20
+ /* ---------- 缓动 ---------- */
21
+ const E = (K.E = {
22
+ linear: (x) => K.clamp(x),
23
+ out2: (x) => { x = K.clamp(x); return 1 - (1 - x) * (1 - x); },
24
+ out3: (x) => { x = K.clamp(x); return 1 - Math.pow(1 - x, 3); },
25
+ out5: (x) => { x = K.clamp(x); return 1 - Math.pow(1 - x, 5); },
26
+ in3: (x) => Math.pow(K.clamp(x), 3),
27
+ io3: (x) => { x = K.clamp(x); return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2; },
28
+ io5: (x) => { x = K.clamp(x); return x < 0.5 ? 16 * Math.pow(x, 5) : 1 - Math.pow(-2 * x + 2, 5) / 2; },
29
+ outExpo: (x) => { x = K.clamp(x); return x === 1 ? 1 : 1 - Math.pow(2, -10 * x); },
30
+ outBack: (x) => { x = K.clamp(x); const c = 1.55; return 1 + (c + 1) * Math.pow(x - 1, 3) + c * Math.pow(x - 1, 2); },
31
+ spring: (x) => { x = K.clamp(x); return 1 - Math.exp(-6 * x) * Math.cos(10 * x); },
32
+ });
33
+ // 生命周期:a 时刻进入(inD 秒),b 时刻开始退出(outD 秒)。返回 0..1
34
+ K.life = (t, a, b = Infinity, inD = 0.6, outD = 0.5, ease = E.out3) => {
35
+ if (t < a) return 0;
36
+ let k = ease((t - a) / inD);
37
+ if (t > b) k *= 1 - E.io3((t - b) / outD);
38
+ return K.clamp(k);
39
+ };
40
+
41
+ /* ---------- 颜色 ---------- */
42
+ const hexCache = {};
43
+ K.rgb = (hex) => { if (hexCache[hex]) return hexCache[hex]; const h = hex.replace('#', ''); const v = [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)]; hexCache[hex] = v; return v; };
44
+ K.rgba = (hex, a) => { const [r, g, b] = K.rgb(hex); return `rgba(${r},${g},${b},${a})`; };
45
+ K.mix = (h1, h2, k) => { const a = K.rgb(h1), b = K.rgb(h2); const c = a.map((v, i) => Math.round(K.lerp(v, b[i], K.clamp(k)))); return '#' + c.map((v) => v.toString(16).padStart(2, '0')).join(''); };
46
+ K.C = {
47
+ eth: '#627EEA', eth2: '#8EA2FF', cyan: '#39E4FF', mag: '#FF4FD8', gold: '#FFC857', red: '#FF3B5C', green: '#2EE6A6',
48
+ etc: '#3AB83A', white: '#F4F6FF', dim: '#8A93C7', ink: '#05060F', violet: '#9B6BFF', orange: '#FF8A3D',
49
+ };
50
+
51
+ /* ---------- 字体 ---------- */
52
+ K.FAM = {
53
+ title: '"DeYi","Smiley Sans","NotoSC","PingFang SC","Microsoft YaHei",sans-serif',
54
+ body: '"NotoSC","Noto Sans SC","PingFang SC","Microsoft YaHei",sans-serif',
55
+ num: '"Grotesk","Space Grotesk","NotoSC","DIN Alternate","Helvetica Neue",Arial,sans-serif',
56
+ };
57
+ K.font = (fam, size, weight = 700) => `${weight} ${Math.round(size)}px ${K.FAM[fam] || fam}`;
58
+ /**
59
+ * 文本:o = {size, fam, weight, color, align, base, alpha, glow, ls, shadow}
60
+ * ls = 字间距(px)
61
+ */
62
+ K.text = (ctx, str, x, y, o = {}) => {
63
+ const size = o.size || 40;
64
+ ctx.save();
65
+ ctx.font = K.font(o.fam || 'body', size, o.weight || 700);
66
+ ctx.textAlign = o.align || 'center';
67
+ ctx.textBaseline = o.base || 'middle';
68
+ if (o.alpha != null) ctx.globalAlpha *= o.alpha;
69
+ if (o.ls && 'letterSpacing' in ctx) ctx.letterSpacing = o.ls + 'px';
70
+ if (o.glow) {
71
+ ctx.shadowColor = o.glow; ctx.shadowBlur = o.glowR || size * 0.6;
72
+ ctx.fillStyle = o.color || K.C.white; ctx.fillText(str, x, y);
73
+ ctx.shadowBlur = 0;
74
+ }
75
+ if (o.stroke) { ctx.lineWidth = o.stroke; ctx.strokeStyle = o.strokeColor || 'rgba(5,6,15,.85)'; ctx.lineJoin = 'round'; ctx.strokeText(str, x, y); }
76
+ ctx.fillStyle = o.color || K.C.white;
77
+ ctx.fillText(str, x, y);
78
+ ctx.restore();
79
+ };
80
+ K.measure = (ctx, str, size, fam = 'body', weight = 700) => { ctx.save(); ctx.font = K.font(fam, size, weight); const w = ctx.measureText(str).width; ctx.restore(); return w; };
81
+
82
+ /* ---------- 路径 / 面板 ---------- */
83
+ K.rr = (ctx, x, y, w, h, r) => { r = Math.min(r, w / 2, h / 2); ctx.beginPath(); ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r); ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath(); };
84
+ // 玻璃拟态面板
85
+ K.glass = (ctx, x, y, w, h, o = {}) => {
86
+ const r = o.r != null ? o.r : 22, a = o.alpha != null ? o.alpha : 1, tint = o.tint || K.C.eth;
87
+ ctx.save(); ctx.globalAlpha *= a;
88
+ K.rr(ctx, x, y, w, h, r);
89
+ const g = ctx.createLinearGradient(x, y, x + w * 0.4, y + h);
90
+ g.addColorStop(0, 'rgba(255,255,255,0.13)'); g.addColorStop(1, 'rgba(255,255,255,0.035)');
91
+ ctx.fillStyle = 'rgba(10,12,30,0.55)'; ctx.fill();
92
+ ctx.fillStyle = g; ctx.fill();
93
+ const b = ctx.createLinearGradient(x, y, x + w, y + h);
94
+ b.addColorStop(0, K.rgba(tint, 0.9)); b.addColorStop(0.5, 'rgba(255,255,255,0.12)'); b.addColorStop(1, K.rgba(o.tint2 || K.C.mag, 0.6));
95
+ ctx.lineWidth = o.lw || 2; ctx.strokeStyle = b; ctx.stroke();
96
+ ctx.restore();
97
+ };
98
+
99
+ /* ---------- 发光 ---------- */
100
+ const spriteCache = {};
101
+ K.sprite = (hex) => {
102
+ if (spriteCache[hex]) return spriteCache[hex];
103
+ const c = document.createElement('canvas'); c.width = c.height = 128;
104
+ const g = c.getContext('2d'); const [r, gg, b] = K.rgb(hex);
105
+ const rg = g.createRadialGradient(64, 64, 0, 64, 64, 64);
106
+ rg.addColorStop(0, `rgba(${r},${gg},${b},1)`); rg.addColorStop(0.18, `rgba(${r},${gg},${b},0.55)`);
107
+ rg.addColorStop(0.45, `rgba(${r},${gg},${b},0.14)`); rg.addColorStop(1, `rgba(${r},${gg},${b},0)`);
108
+ g.fillStyle = rg; g.fillRect(0, 0, 128, 128);
109
+ spriteCache[hex] = c; return c;
110
+ };
111
+ K.glow = (ctx, x, y, r, hex, a = 1) => {
112
+ if (a <= 0.003 || r <= 0.5) return;
113
+ ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.globalAlpha *= a;
114
+ ctx.drawImage(K.sprite(hex), x - r, y - r, r * 2, r * 2); ctx.restore();
115
+ };
116
+ // 霓虹线:三层叠加(外晕 / 中层 / 白芯)
117
+ K.neon = (ctx, pts, hex, w = 3, a = 1, closed = false) => {
118
+ if (a <= 0.003 || pts.length < 2) return;
119
+ ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.lineJoin = 'round'; ctx.lineCap = 'round';
120
+ const path = () => { ctx.beginPath(); ctx.moveTo(pts[0][0], pts[0][1]); for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i][0], pts[i][1]); if (closed) ctx.closePath(); };
121
+ path(); ctx.strokeStyle = K.rgba(hex, 0.16 * a); ctx.lineWidth = w * 5; ctx.stroke();
122
+ ctx.strokeStyle = K.rgba(hex, 0.45 * a); ctx.lineWidth = w * 2.2; ctx.stroke();
123
+ ctx.strokeStyle = K.rgba(K.mix(hex, '#ffffff', 0.65), 0.95 * a); ctx.lineWidth = Math.max(1, w * 0.7); ctx.stroke();
124
+ ctx.restore();
125
+ };
126
+ K.neonCircle = (ctx, x, y, r, hex, w = 3, a = 1, a0 = 0, a1 = TAU) => {
127
+ const n = Math.max(12, Math.ceil(r * Math.abs(a1 - a0) / 10)); const pts = [];
128
+ for (let i = 0; i <= n; i++) { const t = a0 + (a1 - a0) * i / n; pts.push([x + Math.cos(t) * r, y + Math.sin(t) * r]); }
129
+ K.neon(ctx, pts, hex, w, a);
130
+ };
131
+
132
+ /* ---------- 3D ---------- */
133
+ K.rotY = (p, a) => { const c = Math.cos(a), s = Math.sin(a); return [p[0] * c + p[2] * s, p[1], -p[0] * s + p[2] * c]; };
134
+ K.rotX = (p, a) => { const c = Math.cos(a), s = Math.sin(a); return [p[0], p[1] * c - p[2] * s, p[1] * s + p[2] * c]; };
135
+ K.rotZ = (p, a) => { const c = Math.cos(a), s = Math.sin(a); return [p[0] * c - p[1] * s, p[0] * s + p[1] * c, p[2]]; };
136
+ // 透视投影:cam = {x,y (屏幕锚点), s (像素/单位), d (相机距离), rx, ry, rz}
137
+ K.proj = (p, cam) => {
138
+ let q = p;
139
+ if (cam.ry) q = K.rotY(q, cam.ry);
140
+ if (cam.rx) q = K.rotX(q, cam.rx);
141
+ if (cam.rz) q = K.rotZ(q, cam.rz);
142
+ const d = cam.d || 6; const z = q[2] + d; const k = d / Math.max(0.05, z);
143
+ return [cam.x + q[0] * cam.s * k, cam.y - q[1] * cam.s * k, z, k];
144
+ };
145
+
146
+ /* ---------- 数字格式 ---------- */
147
+ K.money = (v, dec = 0) => '$' + v.toLocaleString('en-US', { minimumFractionDigits: dec, maximumFractionDigits: dec });
148
+ K.mmss = (t) => { t = Math.max(0, Math.floor(t)); return String(Math.floor(t / 60)).padStart(2, '0') + ':' + String(t % 60).padStart(2, '0'); };
149
+
150
+ /* ---------- 时间轴 ---------- */
151
+ const PRE_FIRST = 1.2, PRE = 2.0, GAP = 0.32, POST = 0.6, TAIL = 3.2;
152
+ const TL = (K.TL = { chapters: [], lines: [], total: 0 });
153
+ let acc = 0;
154
+ window.ETH_SCRIPT.forEach((ch, ci) => {
155
+ const c = { id: ch.id, title: ch.title, year: ch.year, index: ci, start: acc, lines: [], marks: {} };
156
+ let cur = ci === 0 ? PRE_FIRST : PRE;
157
+ ch.lines.forEach((ln, li) => {
158
+ const L = { ci, li, text: ln.text, file: ln.file, dur: ln.dur, words: ln.words, start: acc + cur };
159
+ L.end = L.start + ln.dur;
160
+ Object.entries(ln.marks).forEach(([k, v]) => { c.marks[k] = L.start + v; });
161
+ c.lines.push(L); TL.lines.push(L);
162
+ cur += ln.dur + GAP;
163
+ });
164
+ c.dur = cur + POST + (ci === window.ETH_SCRIPT.length - 1 ? TAIL : 0);
165
+ c.end = acc + c.dur;
166
+ acc += c.dur;
167
+ TL.chapters.push(c);
168
+ });
169
+ TL.total = acc;
170
+ TL.byId = Object.fromEntries(TL.chapters.map((c) => [c.id, c]));
171
+ TL.chapterAt = (t) => { const C = TL.chapters; let i = C.length - 1; while (i > 0 && C[i].start > t) i--; return C[i]; };
172
+ TL.lineAt = (t) => { for (let i = TL.lines.length - 1; i >= 0; i--) { const L = TL.lines[i]; if (t >= L.start - 0.08) return t <= L.end + 0.45 ? L : null; } return null; };
173
+ TL.mark = (id, name) => { const c = TL.byId[id]; if (!(name in c.marks)) { console.warn('missing mark', id, name); return c.start; } return c.marks[name]; };
174
+ // 价格数据工具
175
+ const P = window.ETH_PRICE.pts;
176
+ K.priceAtDay = (day) => { let lo = 0, hi = P.length - 1; if (day <= P[0][0]) return P[0][1]; if (day >= P[hi][0]) return P[hi][1]; while (hi - lo > 1) { const m = (lo + hi) >> 1; if (P[m][0] <= day) lo = m; else hi = m; } const k = (day - P[lo][0]) / (P[hi][0] - P[lo][0]); return K.lerp(P[lo][1], P[hi][1], k); };
177
+ K.day = (iso) => Math.floor(Date.parse(iso + 'T00:00:00Z') / 86400000);
178
+ })();
ethereum/js/fonts.js ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* 字体加载(FontFace API,自托管子集字体) */
2
+ window.loadFonts = async (base = '') => {
3
+ const list = [
4
+ ['DeYi', 'fonts/deyi.woff2', {}],
5
+ ['NotoSC', 'fonts/notosc.woff2', { weight: '100 900' }],
6
+ ['Grotesk', 'fonts/grotesk.woff2', { weight: '300 700' }],
7
+ ];
8
+ await Promise.all(list.map(async ([fam, url, d]) => {
9
+ try { const f = new FontFace(fam, `url(${base}${url}) format("woff2")`, d); await f.load(); document.fonts.add(f); } catch (e) { console.warn('font', fam, e); }
10
+ }));
11
+ };
ethereum/js/render.js ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ==========================================================================
2
+ * render.js — 单帧合成:世界 → 晶体 → 章节层(交叉溶解)→ 章节标题 → HUD → 字幕 → 后期
3
+ * 纯函数:render(ctx, t, L, opts) 在任意时刻得到确定画面(预览与导出共用)
4
+ * ========================================================================== */
5
+ (() => {
6
+ const { C, E } = K;
7
+ const W = window.WORLD;
8
+ const TL = K.TL;
9
+
10
+ function helper(ch, L, t) {
11
+ const at = (n) => (n == null ? ch.start : TL.mark(ch.id, n));
12
+ return {
13
+ at, m: (n) => t - at(n),
14
+ lf: (a, b, i = 0.6, o = 0.5) => K.life(t, at(a), b == null ? Infinity : at(b), i, o),
15
+ bx: (lx, ly, px, py) => { const B = L.box; return L.portrait ? [B.x + B.w * px, B.y + B.h * py] : [B.x + B.w * lx, B.y + B.h * ly]; },
16
+ };
17
+ }
18
+
19
+ /* ---------- 后期素材 ---------- */
20
+ let grain = null;
21
+ function grainCanvas() {
22
+ if (grain) return grain;
23
+ const c = document.createElement('canvas'); c.width = c.height = 256; const g = c.getContext('2d');
24
+ const id = g.createImageData(256, 256); const r = K.rng(9);
25
+ for (let i = 0; i < id.data.length; i += 4) { const v = 110 + r() * 40; id.data[i] = id.data[i + 1] = id.data[i + 2] = v; id.data[i + 3] = 255; }
26
+ g.putImageData(id, 0, 0); grain = c; return c;
27
+ }
28
+
29
+ /* ---------- 章节标题(跃迁时出现) ---------- */
30
+ function chapterCard(ctx, L, ch, t) {
31
+ if (ch.index === 0) return;
32
+ const lt = t - ch.start; const first = ch.lines[0].start - ch.start;
33
+ const k = K.life(lt, -0.4, first - 0.35, 0.7, 0.5);
34
+ if (k <= 0) return;
35
+ const x = L.portrait ? L.cx : L.W / 2, y = L.portrait ? L.cy - 40 : L.cy;
36
+ const pal = W.palette(t);
37
+ ctx.save(); ctx.globalAlpha *= k;
38
+ const yr = ch.year;
39
+ const s = L.portrait ? 210 : 260;
40
+ const rise = (1 - E.out5((lt + 0.4) / 1.1)) * 60;
41
+ K.text(ctx, yr, x, y - 30 + rise, { size: s, fam: 'num', weight: 700, color: K.rgba(C.white, 0.95), glow: pal[0], glowR: 60, ls: 6 + 20 * (1 - k) });
42
+ K.text(ctx, String(ch.index).padStart(2, '0') + ' ' + ch.title, x, y + s * 0.5 + 10, { size: L.portrait ? 54 : 60, fam: 'title', weight: 400, color: pal[0], ls: 10 });
43
+ K.neon(ctx, [[x - 220 * k, y + s * 0.5 + 64], [x + 220 * k, y + s * 0.5 + 64]], pal[1], 2, 0.9);
44
+ ctx.restore();
45
+ }
46
+
47
+ /* ---------- HUD ---------- */
48
+ function hud(ctx, L, t, ch) {
49
+ const pal = W.palette(t);
50
+ const P = L.portrait;
51
+ const fadeIn = K.clamp((t - 6) / 1.5);
52
+ if (fadeIn <= 0) return;
53
+ ctx.save(); ctx.globalAlpha *= fadeIn;
54
+ const bx = P ? 60 : 60, by = P ? 256 : 58;
55
+ K.glow(ctx, bx + 14, by, 40, C.eth, 0.6); K.neon(ctx, [[bx + 14, by - 20], [bx + 26, by], [bx + 14, by + 7], [bx + 2, by], [bx + 14, by - 20]], C.eth2, 1.5, 1); K.neon(ctx, [[bx + 2, by + 4], [bx + 14, by + 21], [bx + 26, by + 4]], C.eth2, 1.5, 1);
56
+ K.text(ctx, '以太坊简史', bx + 48, by + 2, { size: 30, fam: 'title', weight: 400, align: 'left', color: C.white });
57
+ K.text(ctx, String(ch.index).padStart(2, '0') + ' / ' + String(TL.chapters.length - 1).padStart(2, '0') + ' ' + ch.title, bx + 4, by + 46, { size: 24, align: 'left', color: K.rgba(C.white, 0.65), weight: 500 });
58
+ // 年份(滚动)
59
+ const yx = P ? L.W - 60 : L.W - 60, yy = P ? 270 : 70;
60
+ const cur = ch.year, prev = ch.index > 0 ? TL.chapters[ch.index - 1].year : cur;
61
+ const roll = E.io3((t - ch.start + 0.6) / 1.2);
62
+ ctx.save(); ctx.beginPath(); ctx.rect(yx - 260, yy - 40, 270, 80); ctx.clip();
63
+ if (roll < 1 && prev !== cur) K.text(ctx, prev, yx, yy - roll * 70, { size: 64, fam: 'num', align: 'right', color: K.rgba(C.white, 0.9 * (1 - roll)) });
64
+ K.text(ctx, cur, yx, yy + (1 - roll) * 70, { size: 64, fam: 'num', align: 'right', color: C.white, glow: pal[0], glowR: 24 });
65
+ ctx.restore();
66
+ // 时间线
67
+ const x0 = P ? 60 : 120, x1 = P ? 1020 : 1800, ly = P ? 360 : 1052;
68
+ ctx.strokeStyle = 'rgba(255,255,255,.12)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(x0, ly); ctx.lineTo(x1, ly); ctx.stroke();
69
+ const prog = t / TL.total;
70
+ K.neon(ctx, [[x0, ly], [x0 + (x1 - x0) * prog, ly]], pal[0], 1.4, 0.9);
71
+ TL.chapters.forEach((c, i) => { const px = x0 + (x1 - x0) * c.start / TL.total; const on = t >= c.start; ctx.fillStyle = on ? K.rgba(pal[1], 0.95) : 'rgba(255,255,255,.3)'; ctx.beginPath(); ctx.arc(px, ly, on ? 4 : 3, 0, K.TAU); ctx.fill(); });
72
+ K.glow(ctx, x0 + (x1 - x0) * prog, ly, 26, C.white, 0.9);
73
+ ctx.restore();
74
+ }
75
+
76
+ /* ---------- 卡拉 OK 字幕 ---------- */
77
+ function subtitles(ctx, L, t) {
78
+ const ln = TL.lineAt(t); if (!ln) return;
79
+ const a = K.clamp((t - ln.start + 0.1) / 0.2) * K.clamp((ln.end + 0.45 - t) / 0.3);
80
+ if (a <= 0) return;
81
+ const S = L.sub; const lt = t - ln.start;
82
+ const text = ln.text;
83
+ // 每个字符的状态
84
+ const st = new Array(text.length).fill(0); // 0 未读 1 正在读 2 已读
85
+ let last = 0;
86
+ for (const [c0, c1, t0, t1] of ln.words) { const s = lt >= t1 ? 2 : lt >= t0 ? 1 : 0; for (let i = c0; i < c1; i++) st[i] = s; }
87
+ for (let i = 0; i < text.length; i++) { if (ln.words.some((w) => i >= w[0] && i < w[1])) last = st[i]; else st[i] = last === 1 ? 2 : last; }
88
+ if (lt > ln.dur - 0.05) st.fill(2);
89
+ ctx.save(); ctx.globalAlpha *= a;
90
+ ctx.font = K.font('body', S.size, 700);
91
+ // 断行
92
+ const rows = []; let row = '', rs = 0, start = 0;
93
+ for (let i = 0; i < text.length; i++) {
94
+ const ch = text[i]; const w = ctx.measureText(row + ch).width;
95
+ if (w > S.maxW && row && !/[,。!?、:;”),.!?]/.test(ch)) {
96
+ // 不在英文单词/数字中间断行
97
+ let cut = row.length;
98
+ if (/[A-Za-z0-9.\-]/.test(ch)) { let j = row.length; while (j > 0 && /[A-Za-z0-9.\-]/.test(row[j - 1])) j--; if (j > 0) cut = j; }
99
+ rows.push([row.slice(0, cut), start]); start = start + cut; row = row.slice(cut) + ch;
100
+ }
101
+ else row += ch;
102
+ }
103
+ rows.push([row, start]);
104
+ if (rows.length === 2) { // 两行均衡
105
+ const half = Math.ceil(text.length / 2); let cut = half;
106
+ for (let d = 0; d < 6; d++) { if (/[,。:;、]/.test(text[half + d - 1] || '')) { cut = half + d; break; } if (/[,。:;、]/.test(text[half - d - 1] || '')) { cut = half - d; break; } }
107
+ const r1 = text.slice(0, cut), r2 = text.slice(cut);
108
+ const midWord = /[A-Za-z0-9]/.test(text[cut - 1] || '') && /[A-Za-z0-9]/.test(text[cut] || '');
109
+ if (!midWord && ctx.measureText(r1).width <= S.maxW && ctx.measureText(r2).width <= S.maxW) { rows.length = 0; rows.push([r1, 0], [r2, cut]); }
110
+ }
111
+ const lh = S.size * 1.4;
112
+ const y0 = S.y - (L.portrait ? 0 : (rows.length - 1) * lh / 2);
113
+ // 底衬
114
+ const bandH = rows.length * lh + 40;
115
+ const g = ctx.createLinearGradient(0, y0 - lh / 2 - 20, 0, y0 - lh / 2 - 20 + bandH);
116
+ g.addColorStop(0, 'rgba(5,6,15,0)'); g.addColorStop(0.3, 'rgba(5,6,15,.55)'); g.addColorStop(0.7, 'rgba(5,6,15,.55)'); g.addColorStop(1, 'rgba(5,6,15,0)');
117
+ ctx.fillStyle = g; ctx.fillRect(0, y0 - lh / 2 - 20, L.W, bandH);
118
+ ctx.textBaseline = 'middle'; ctx.textAlign = 'left';
119
+ rows.forEach(([r, s0], ri) => {
120
+ const w = ctx.measureText(r).width; let x = S.cx - w / 2; const y = y0 + ri * lh;
121
+ for (let i = 0; i < r.length; i++) {
122
+ const ch = r[i]; const state = st[s0 + i]; const cw = ctx.measureText(ch).width;
123
+ ctx.lineWidth = S.size * 0.16; ctx.strokeStyle = 'rgba(5,6,15,.9)'; ctx.lineJoin = 'round'; ctx.strokeText(ch, x, y);
124
+ if (state === 1) { ctx.shadowColor = C.cyan; ctx.shadowBlur = 18; ctx.fillStyle = '#C9F6FF'; }
125
+ else { ctx.shadowBlur = 0; ctx.fillStyle = state === 2 ? '#FFFFFF' : 'rgba(255,255,255,.5)'; }
126
+ ctx.fillText(ch, x, y); ctx.shadowBlur = 0;
127
+ x += cw;
128
+ }
129
+ });
130
+ ctx.restore();
131
+ }
132
+
133
+ function guides(ctx, L) {
134
+ ctx.save();
135
+ if (L.safe) {
136
+ const s = L.safe; ctx.fillStyle = 'rgba(255,40,90,.2)';
137
+ ctx.fillRect(0, 0, L.W, s.top); ctx.fillRect(0, s.bottom, L.W, L.H - s.bottom); ctx.fillRect(s.right, L.H * 0.45, L.W - s.right, s.bottom - L.H * 0.45);
138
+ ctx.font = '28px sans-serif'; ctx.fillStyle = '#fff'; ctx.textAlign = 'center';
139
+ ctx.fillText('状态栏 / 刘海 / 顶部导航', L.W / 2, s.top / 2); ctx.fillText('账号 / 文案 / 音乐 / 进度条', L.W / 2, s.bottom + 140);
140
+ ctx.save(); ctx.translate(s.right + 70, L.H * 0.66); ctx.rotate(Math.PI / 2); ctx.fillText('点赞 · 评论 · 收藏 · 分享', 0, 0); ctx.restore();
141
+ } else { ctx.setLineDash([14, 10]); ctx.strokeStyle = 'rgba(57,228,255,.8)'; ctx.lineWidth = 2; ctx.strokeRect(L.W * 0.05, L.H * 0.05, L.W * 0.9, L.H * 0.9); }
142
+ ctx.restore();
143
+ }
144
+
145
+ /* ---------- 主渲染 ---------- */
146
+ function render(ctx, t, L, opts = {}) {
147
+ t = K.clamp(t, 0, TL.total - 1e-3);
148
+ const z = opts.scale || 1;
149
+ ctx.setTransform(z, 0, 0, z, 0, 0);
150
+ ctx.globalAlpha = 1; ctx.globalCompositeOperation = 'source-over';
151
+ const ch = TL.chapterAt(t);
152
+ // 序章开头:星空渐显
153
+ const intro = K.clamp(t / 1.5);
154
+ W.background(ctx, L, t, { neb: 0.3 + 0.7 * intro * K.clamp((t - 1) / 3), stars: intro });
155
+ // 晶体
156
+ const pose = W.crystalPose(t, L);
157
+ const co = { x: pose.x, y: pose.y + Math.sin(t * 0.9) * pose.s * 0.06, s: pose.s, ry: t * 0.55, rx: -0.2 + Math.sin(t * 0.3) * 0.08, alpha: 1, build: 1, fill: 1 };
158
+ const H = helper(ch, L, t);
159
+ const mod = window.CHAPTERS[ch.id] && window.CHAPTERS[ch.id].crystal;
160
+ if (mod) mod(co, t, H);
161
+ // 跨章节:前一章的晶体修改在 1.2 秒内淡出
162
+ if (ch.index > 0) {
163
+ const pc = TL.chapters[ch.index - 1]; const pm = window.CHAPTERS[pc.id] && window.CHAPTERS[pc.id].crystal;
164
+ const k = K.clamp((t - ch.start) / 1.2);
165
+ if (pm && k < 1) { const po = Object.assign({}, co); pm(po, t, helper(pc, L, t)); ['alpha', 'split', 'glow', 'x', 'y', 's'].forEach((f) => { if (typeof po[f] === 'number' && typeof co[f] === 'number') co[f] = K.lerp(po[f], co[f], E.io3(k)); }); if (po.tint && k < 0.5) co.tint = po.tint; }
166
+ }
167
+ W.crystal(ctx, co);
168
+ // 章节层(与上一章交叉溶解)
169
+ if (ch.index > 0 && t - ch.start < 0.9) {
170
+ const pc = TL.chapters[ch.index - 1];
171
+ ctx.save(); ctx.globalAlpha = 1 - E.io3((t - ch.start) / 0.9);
172
+ try { window.CHAPTERS[pc.id].draw(ctx, L, t, helper(pc, L, t)); } catch (e) { console.error(pc.id, e); }
173
+ ctx.restore();
174
+ }
175
+ ctx.save(); ctx.globalAlpha = ch.index > 0 ? K.clamp((t - ch.start) / 0.6) : 1;
176
+ try { window.CHAPTERS[ch.id].draw(ctx, L, t, H); } catch (e) { console.error(ch.id, e); }
177
+ ctx.restore();
178
+ chapterCard(ctx, L, ch, t);
179
+ // 后期:暗角 + 颗粒
180
+ const vg = ctx.createRadialGradient(L.W / 2, L.H / 2, Math.min(L.W, L.H) * 0.35, L.W / 2, L.H / 2, Math.hypot(L.W, L.H) * 0.62);
181
+ vg.addColorStop(0, 'rgba(5,6,15,0)'); vg.addColorStop(1, 'rgba(5,6,15,.55)');
182
+ ctx.fillStyle = vg; ctx.fillRect(0, 0, L.W, L.H);
183
+ ctx.save(); ctx.globalAlpha = 0.05; ctx.globalCompositeOperation = 'overlay';
184
+ const gc = grainCanvas(); const ox = Math.floor(K.hash(Math.floor(t * 24)) * 256), oy = Math.floor(K.hash(Math.floor(t * 24) + 7) * 256);
185
+ ctx.translate(-ox, -oy); ctx.fillStyle = ctx.createPattern(gc, 'repeat'); ctx.fillRect(ox, oy, L.W, L.H); ctx.restore();
186
+ hud(ctx, L, t, ch);
187
+ subtitles(ctx, L, t);
188
+ // 片尾渐黑
189
+ const endK = K.clamp((t - (TL.total - 1.2)) / 1.2);
190
+ if (endK > 0) { ctx.fillStyle = `rgba(5,6,15,${endK})`; ctx.fillRect(0, 0, L.W, L.H); }
191
+ if (opts.guides) guides(ctx, L);
192
+ }
193
+
194
+ /* ---------- 封面 ---------- */
195
+ function renderCover(ctx, L, z = 1) {
196
+ ctx.setTransform(z, 0, 0, z, 0, 0);
197
+ const t = 12.5; const P = L.portrait;
198
+ W.background(ctx, L, TL.byId.origin.start - 0.2, {});
199
+ const cx = L.portrait ? L.W / 2 : L.W * 0.7, cy = L.portrait ? L.H * 0.42 : L.H * 0.48;
200
+ const s = P ? 200 : 210;
201
+ W.globe(ctx, { x: cx, y: cy, r: s * 2.2, t: 3, ry: 0.6, arcs: 18, alpha: 0.5, color: C.eth, color2: C.mag });
202
+ W.crystal(ctx, { x: cx, y: cy, s, ry: 0.65, rx: -0.2, alpha: 1, glow: 1.4 });
203
+ if (P) {
204
+ K.text(ctx, '以太坊简史', L.W / 2, 330, { size: 150, fam: 'title', weight: 400, color: C.white, glow: C.eth, glowR: 50, ls: 6 });
205
+ K.text(ctx, '从一份白皮书,到世界计算机', L.W / 2, 450, { size: 50, color: K.rgba(C.white, 0.9), ls: 4 });
206
+ K.text(ctx, '2013 — 2026', L.W / 2, 1330, { size: 64, fam: 'num', color: C.eth2, ls: 14, glow: C.eth });
207
+ ['众筹 0.31 美元', 'The DAO 危机', '合并 · 能耗 -99.95%', 'ETF 时代'].forEach((s2, i) => { const x = 270 + (i % 2) * 540, y = 1450 + Math.floor(i / 2) * 110; K.glass(ctx, x - 240, y - 44, 480, 88, { r: 44 }); K.text(ctx, s2, x, y + 2, { size: 36, color: C.white }); });
208
+ } else {
209
+ K.text(ctx, '以太坊简史', 120, 380, { size: 170, fam: 'title', weight: 400, color: C.white, align: 'left', glow: C.eth, glowR: 50, ls: 6 });
210
+ K.text(ctx, '从一份白皮书,到世界计算机', 128, 520, { size: 54, color: K.rgba(C.white, 0.9), align: 'left', ls: 4 });
211
+ K.text(ctx, '2013 — 2026', 128, 610, { size: 52, fam: 'num', color: C.eth2, align: 'left', ls: 14, glow: C.eth });
212
+ ['0.31 美元众筹', 'The DAO 危机', '合并 -99.95%', 'ETF 时代'].forEach((s2, i) => { const x = 128 + i * 300; K.glass(ctx, x, 700, 280, 84, { r: 42 }); K.text(ctx, s2, x + 140, 743, { size: 32, color: C.white }); });
213
+ }
214
+ const vg = ctx.createRadialGradient(L.W / 2, L.H / 2, Math.min(L.W, L.H) * 0.4, L.W / 2, L.H / 2, Math.hypot(L.W, L.H) * 0.65);
215
+ vg.addColorStop(0, 'rgba(5,6,15,0)'); vg.addColorStop(1, 'rgba(5,6,15,.5)'); ctx.fillStyle = vg; ctx.fillRect(0, 0, L.W, L.H);
216
+ }
217
+
218
+ window.RENDER = { render, renderCover };
219
+ })();
ethereum/js/world.js ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ==========================================================================
2
+ * world.js — 持续存在的"世界":星空隧道、星云、以太坊晶体、通用组件
3
+ * 镜头连贯性的核心:这些元素跨章节连续运动,章节之间不切黑。
4
+ * ========================================================================== */
5
+ (() => {
6
+ const { C, E } = K;
7
+ const W = (window.WORLD = {});
8
+
9
+ /* ---------- 画幅布局(响应式构图) ---------- */
10
+ W.LAYOUTS = {
11
+ landscape: { name: 'landscape', W: 1920, H: 1080, portrait: false, cx: 960, cy: 495, box: { x: 120, y: 120, w: 1680, h: 760 }, sub: { cx: 960, y: 985, maxW: 1560, size: 46 }, u: 1 },
12
+ portrait: { name: 'portrait', W: 1080, H: 1920, portrait: true, cx: 485, cy: 860, box: { x: 50, y: 420, w: 870, h: 860 }, sub: { cx: 485, y: 1345, maxW: 860, size: 48 }, u: 0.92, safe: { top: 210, bottom: 1480, right: 940 } },
13
+ };
14
+
15
+ /* ---------- 章节调色板 ---------- */
16
+ const PAL = {
17
+ open: [C.eth, C.violet], origin: [C.eth, C.cyan], ico: [C.gold, C.eth], genesis: [C.cyan, C.eth], dao: [C.red, C.mag],
18
+ mania: [C.mag, C.gold], defi: [C.green, C.cyan], burn: [C.orange, C.mag], merge: [C.gold, C.eth], scale: [C.cyan, C.violet],
19
+ wall: [C.green, C.gold], now: [C.cyan, C.eth], end: [C.eth, C.mag],
20
+ };
21
+ // 在章节边界附近平滑混合调色板
22
+ W.palette = (t) => {
23
+ const ch = K.TL.chapterAt(t); const i = ch.index;
24
+ const cur = PAL[ch.id];
25
+ if (i === 0) return cur;
26
+ const prev = PAL[K.TL.chapters[i - 1].id];
27
+ const k = E.io3((t - ch.start) / 1.6);
28
+ return [K.mix(prev[0], cur[0], k), K.mix(prev[1], cur[1], k)];
29
+ };
30
+
31
+ /* ---------- 摄像机速度(章节交界处"跃迁") ---------- */
32
+ const BASE = 0.045, WARP = 0.9, WD = 1.8;
33
+ const bounds = () => K.TL.chapters.slice(1).map((c) => c.start - 0.9);
34
+ W.speed = (t) => { let v = BASE; for (const b of bounds()) { const x = t - b; if (x > 0 && x < WD) v += WARP * (1 - Math.cos(K.TAU * x / WD)) / 2; } return v; };
35
+ W.travel = (t) => {
36
+ let s = BASE * t;
37
+ for (const b of bounds()) { const x = t - b; if (x <= 0) continue; if (x >= WD) s += WARP * WD / 2; else s += WARP * (x / 2 - WD / (4 * Math.PI) * Math.sin(K.TAU * x / WD)); }
38
+ return s;
39
+ };
40
+
41
+ /* ---------- 星空 ---------- */
42
+ const STARS = [];
43
+ { const r = K.rng(20131127); for (let i = 0; i < 560; i++) STARS.push({ x: (r() * 2 - 1) * 1.6, y: (r() * 2 - 1) * 1.6, z: r(), m: 0.4 + r() * 0.8, c: r() }); }
44
+ W.background = (ctx, L, t, o = {}) => {
45
+ const [p1, p2] = W.palette(t);
46
+ ctx.fillStyle = C.ink; ctx.fillRect(0, 0, L.W, L.H);
47
+ // 星云:两团缓慢漂移的彩色光
48
+ const R = Math.max(L.W, L.H);
49
+ const nx1 = L.W * (0.25 + 0.08 * Math.sin(t * 0.05)), ny1 = L.H * (0.3 + 0.06 * Math.cos(t * 0.04));
50
+ const nx2 = L.W * (0.78 + 0.06 * Math.cos(t * 0.045)), ny2 = L.H * (0.72 + 0.05 * Math.sin(t * 0.06));
51
+ let g = ctx.createRadialGradient(nx1, ny1, 0, nx1, ny1, R * 0.62);
52
+ g.addColorStop(0, K.rgba(p1, 0.34 * (o.neb ?? 1))); g.addColorStop(0.5, K.rgba(p1, 0.08 * (o.neb ?? 1))); g.addColorStop(1, K.rgba(p1, 0));
53
+ ctx.fillStyle = g; ctx.fillRect(0, 0, L.W, L.H);
54
+ g = ctx.createRadialGradient(nx2, ny2, 0, nx2, ny2, R * 0.55);
55
+ g.addColorStop(0, K.rgba(p2, 0.26 * (o.neb ?? 1))); g.addColorStop(0.55, K.rgba(p2, 0.06 * (o.neb ?? 1))); g.addColorStop(1, K.rgba(p2, 0));
56
+ ctx.fillStyle = g; ctx.fillRect(0, 0, L.W, L.H);
57
+ // 星星(3D 透视 + 跃迁拖尾)
58
+ const trav = W.travel(t), sp = W.speed(t);
59
+ const F = Math.min(L.W, L.H) * 0.62, cx = L.W / 2, cy = L.H * (L.portrait ? 0.44 : 0.47);
60
+ const streak = K.clamp((sp - BASE) / WARP);
61
+ ctx.save(); ctx.globalCompositeOperation = 'lighter';
62
+ for (let i = 0; i < STARS.length; i++) {
63
+ const s = STARS[i];
64
+ const z = 0.03 + K.fract(s.z - trav * 0.35);
65
+ const x = cx + (s.x / z) * F, y = cy + (s.y / z) * F;
66
+ if (x < -60 || x > L.W + 60 || y < -60 || y > L.H + 60) continue;
67
+ const near = 1 - z;
68
+ const a = K.clamp(near * 1.3) * (0.35 + 0.65 * s.m) * (o.stars ?? 1);
69
+ const col = s.c < 0.6 ? '#dfe6ff' : s.c < 0.82 ? p1 : p2;
70
+ if (streak > 0.04) {
71
+ const z2 = z + 0.018 + streak * 0.11;
72
+ const x2 = cx + (s.x / z2) * F, y2 = cy + (s.y / z2) * F;
73
+ ctx.strokeStyle = K.rgba(col, a * 0.9); ctx.lineWidth = 0.6 + near * 2.2;
74
+ ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x2, y2); ctx.stroke();
75
+ } else {
76
+ const r = 0.5 + near * near * 2.6 * s.m;
77
+ ctx.fillStyle = K.rgba(col, a); ctx.beginPath(); ctx.arc(x, y, r, 0, K.TAU); ctx.fill();
78
+ }
79
+ }
80
+ ctx.restore();
81
+ // 跃迁闪光
82
+ if (streak > 0.05) K.glow(ctx, cx, cy, Math.min(L.W, L.H) * (0.3 + streak * 0.5), p1, streak * 0.55);
83
+ };
84
+
85
+ /* ---------- 以太坊晶体 ---------- */
86
+ const RING = (y, r, off = Math.PI / 4) => [0, 1, 2, 3].map((i) => [Math.cos(off + i * Math.PI / 2) * r, y, Math.sin(off + i * Math.PI / 2) * r]);
87
+ const UP_A = [0, 1.75, 0], UP_R = RING(-0.02, 1.0), UP_B = [0, -0.55, 0];
88
+ const LO_C = [0, -1.8, 0], LO_R = RING(-0.62, 1.0), LO_D = [0, -1.02, 0];
89
+ const FACES = [];
90
+ for (let i = 0; i < 4; i++) {
91
+ const j = (i + 1) % 4;
92
+ FACES.push({ part: 0, v: [UP_A, UP_R[i], UP_R[j]], top: true });
93
+ FACES.push({ part: 0, v: [UP_B, UP_R[j], UP_R[i]], top: false });
94
+ FACES.push({ part: 1, v: [LO_C, LO_R[j], LO_R[i]], top: false });
95
+ FACES.push({ part: 1, v: [LO_D, LO_R[i], LO_R[j]], top: true });
96
+ }
97
+ const LIGHT = (() => { const l = [-0.45, 0.75, -0.5]; const n = Math.hypot(...l); return l.map((v) => v / n); })();
98
+ /**
99
+ * o: {x,y,s(像素/单位), ry, rx, alpha, build(0..1 线框绘制进度), fill(0..1 实体程度), split(上下分离), tint, tint2, glow}
100
+ */
101
+ W.crystal = (ctx, o) => {
102
+ const a = o.alpha ?? 1; if (a <= 0.003 || o.s < 1) return;
103
+ const tint = o.tint || C.eth, tint2 = o.tint2 || C.eth2;
104
+ const cam = { x: o.x, y: o.y, s: o.s, d: 7, ry: o.ry || 0, rx: o.rx ?? -0.18 };
105
+ const split = o.split || 0;
106
+ const tr = (p, part) => { let q = [p[0], p[1] + (part ? -split : split * 0.5), p[2]]; if (o.wob) q = [q[0] * (1 + o.wob), q[1], q[2] * (1 + o.wob)]; return q; };
107
+ K.glow(ctx, o.x, o.y - o.s * 0.1, o.s * 3.2, tint, 0.55 * a * (o.glow ?? 1));
108
+ const build = o.build ?? 1, fill = o.fill ?? 1;
109
+ // 面
110
+ const fs = FACES.map((f) => {
111
+ const P3 = f.v.map((p) => { let q = tr(p, f.part); q = K.rotY(q, cam.ry); q = K.rotX(q, cam.rx); return q; });
112
+ const u = [P3[1][0] - P3[0][0], P3[1][1] - P3[0][1], P3[1][2] - P3[0][2]];
113
+ const v = [P3[2][0] - P3[0][0], P3[2][1] - P3[0][1], P3[2][2] - P3[0][2]];
114
+ let n = [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]];
115
+ const nl = Math.hypot(...n) || 1; n = n.map((x) => x / nl);
116
+ const z = (P3[0][2] + P3[1][2] + P3[2][2]) / 3;
117
+ const P2 = f.v.map((p) => K.proj(tr(p, f.part), cam));
118
+ return { f, n, z, P2 };
119
+ }).sort((p, q) => q.z - p.z);
120
+ if (fill > 0.01) {
121
+ ctx.save(); ctx.globalAlpha *= a * fill;
122
+ for (const F of fs) {
123
+ const lit = K.clamp(0.25 + 0.75 * Math.max(0, -(F.n[0] * LIGHT[0] + F.n[1] * LIGHT[1] + F.n[2] * LIGHT[2])));
124
+ const facing = F.n[2] < 0;
125
+ const col = K.mix(K.mix('#1b2350', tint, 0.55), K.mix(tint2, '#ffffff', 0.55), lit);
126
+ ctx.beginPath(); ctx.moveTo(F.P2[0][0], F.P2[0][1]); ctx.lineTo(F.P2[1][0], F.P2[1][1]); ctx.lineTo(F.P2[2][0], F.P2[2][1]); ctx.closePath();
127
+ ctx.fillStyle = K.rgba(col, facing ? 0.78 : 0.32); ctx.fill();
128
+ }
129
+ ctx.restore();
130
+ }
131
+ // 棱线(按 build 逐段出现)
132
+ const edges = [];
133
+ const addTri = (P) => { edges.push([P[0], P[1]], [P[1], P[2]], [P[2], P[0]]); };
134
+ fs.forEach((F) => addTri(F.P2));
135
+ const n = Math.floor(edges.length * build);
136
+ ctx.save(); ctx.globalCompositeOperation = 'lighter';
137
+ for (let i = 0; i < n; i++) {
138
+ const [p, q] = edges[i];
139
+ ctx.strokeStyle = K.rgba(K.mix(tint2, '#ffffff', 0.5), 0.55 * a); ctx.lineWidth = Math.max(1, o.s * 0.018);
140
+ ctx.beginPath(); ctx.moveTo(p[0], p[1]); ctx.lineTo(q[0], q[1]); ctx.stroke();
141
+ }
142
+ ctx.restore();
143
+ // 顶点光点
144
+ if (build < 1) { const e = edges[Math.min(edges.length - 1, n)]; if (e) K.glow(ctx, e[1][0], e[1][1], o.s * 0.5, C.white, a); }
145
+ const top = K.proj(tr(UP_A, 0), cam); K.glow(ctx, top[0], top[1], o.s * 0.45, tint2, 0.6 * a);
146
+ };
147
+
148
+ // 晶体在各章节的"站位"(相对内容框,0..1)。章节交界处平滑过渡。
149
+ const POSE = {
150
+ open: { l: [0.5, 0.42, 0.3], p: [0.5, 0.4, 0.24] },
151
+ origin: { l: [0.86, 0.22, 0.09], p: [0.83, 0.07, 0.07] },
152
+ ico: { l: [0.14, 0.2, 0.08], p: [0.15, 0.06, 0.07] },
153
+ genesis: { l: [0.5, 0.13, 0.07], p: [0.5, 0.05, 0.07] },
154
+ dao: { l: [0.9, 0.2, 0.06], p: [0.86, 0.05, 0.06] },
155
+ mania: { l: [0.08, 0.16, 0.06], p: [0.12, 0.04, 0.06] },
156
+ defi: { l: [0.9, 0.18, 0.07], p: [0.85, 0.05, 0.07] },
157
+ burn: { l: [0.5, 0.5, 0.16], p: [0.5, 0.5, 0.14] },
158
+ merge: { l: [0.5, 0.45, 0.2], p: [0.5, 0.42, 0.17] },
159
+ scale: { l: [0.9, 0.16, 0.06], p: [0.86, 0.04, 0.06] },
160
+ wall: { l: [0.08, 0.16, 0.06], p: [0.12, 0.04, 0.06] },
161
+ now: { l: [0.9, 0.16, 0.06], p: [0.86, 0.04, 0.06] },
162
+ end: { l: [0.5, 0.42, 0.22], p: [0.5, 0.36, 0.18] },
163
+ };
164
+ W.crystalPose = (t, L) => {
165
+ const ch = K.TL.chapterAt(t); const key = L.portrait ? 'p' : 'l';
166
+ const cur = POSE[ch.id][key];
167
+ let v = cur;
168
+ if (ch.index > 0) {
169
+ const prev = POSE[K.TL.chapters[ch.index - 1].id][key];
170
+ const k = E.io5((t - ch.start + 0.9) / 2.2);
171
+ v = prev.map((x, i) => K.lerp(x, cur[i], k));
172
+ }
173
+ const b = L.box;
174
+ return { x: b.x + b.w * v[0], y: b.y + b.h * v[1], s: Math.min(b.w, b.h) * v[2] * (L.portrait ? 1.25 : 1) };
175
+ };
176
+
177
+ /* ---------- 通用组件:3D 区块链 ---------- */
178
+ /**
179
+ * o: {x,y,s,n,spacing,ry,rx,first(起始编号),lit(0..n 点亮数量),color,branch:{at,k,c2},alpha,shake,labels}
180
+ */
181
+ W.blockchain = (ctx, o) => {
182
+ const a = o.alpha ?? 1; if (a <= 0.003) return;
183
+ const cam = { x: o.x, y: o.y, s: o.s, d: 9, ry: o.ry ?? -0.5, rx: o.rx ?? -0.25 };
184
+ const sp = o.spacing || 1.6, n = o.n;
185
+ const lit = o.lit ?? n;
186
+ const off = o.offset || 0; // 链条整体平移(生长感)
187
+ const cube = (cx, cy, cz, h) => {
188
+ const v = [];
189
+ for (const dx of [-h, h]) for (const dy of [-h, h]) for (const dz of [-h, h]) v.push([cx + dx, cy + dy, cz + dz]);
190
+ return v;
191
+ };
192
+ const E12 = [[0, 1], [2, 3], [4, 5], [6, 7], [0, 2], [1, 3], [4, 6], [5, 7], [0, 4], [1, 5], [2, 6], [3, 7]];
193
+ const items = [];
194
+ for (let i = 0; i < n; i++) {
195
+ let bx = (i - (n - 1) / 2) * sp - off, by = 0, bz = 0, col = o.color || C.eth;
196
+ const br = o.branch;
197
+ if (br && i >= br.at) { const d = (i - br.at + 1) * br.k; items.push({ i, bx, by: d, bz, col }); items.push({ i, bx, by: -d, bz, col: br.c2 || C.etc, alt: true }); continue; }
198
+ items.push({ i, bx, by, bz, col });
199
+ }
200
+ ctx.save(); ctx.globalAlpha *= a;
201
+ // 连接线
202
+ let prev = null, prevAlt = null;
203
+ for (const it of items) {
204
+ const p = K.proj([it.bx, it.by, it.bz], cam);
205
+ const ref = it.alt ? prevAlt || prev : prev;
206
+ if (ref && it.i <= lit && it.i > 0) K.neon(ctx, [ref, p], it.col, 1.6 * o.s / 60, 0.8);
207
+ if (it.alt) prevAlt = p; else prev = p;
208
+ }
209
+ for (const it of items) {
210
+ if (it.i >= lit + 1) continue;
211
+ const on = K.clamp(lit - it.i);
212
+ const V = cube(it.bx, it.by, it.bz, 0.42).map((p) => K.proj(p, cam));
213
+ // 面(半透明)
214
+ const c = K.proj([it.bx, it.by, it.bz], cam);
215
+ K.glow(ctx, c[0], c[1], o.s * 1.3, it.col, 0.45 * on);
216
+ ctx.fillStyle = K.rgba(it.col, 0.12 + 0.1 * on);
217
+ ctx.beginPath(); [0, 1, 3, 2].forEach((k, j) => (j ? ctx.lineTo(V[k][0], V[k][1]) : ctx.moveTo(V[k][0], V[k][1]))); ctx.closePath(); ctx.fill();
218
+ ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.strokeStyle = K.rgba(K.mix(it.col, '#fff', 0.5), 0.35 + 0.6 * on); ctx.lineWidth = Math.max(1, o.s * 0.03);
219
+ ctx.beginPath(); E12.forEach(([p, q]) => { ctx.moveTo(V[p][0], V[p][1]); ctx.lineTo(V[q][0], V[q][1]); }); ctx.stroke(); ctx.restore();
220
+ if (o.labels !== false && on > 0.5 && c[3] > 0.6) K.text(ctx, '#' + ((o.first || 0) + it.i).toLocaleString('en-US'), c[0], c[1] + o.s * 0.75 * c[3], { size: o.s * 0.26 * c[3], fam: 'num', color: K.rgba(C.white, 0.75), weight: 600 });
221
+ }
222
+ ctx.restore();
223
+ };
224
+
225
+ /* ---------- 通用组件:点阵地球 ---------- */
226
+ const GPTS = []; { const r = K.rng(77); for (let i = 0; i < 520; i++) { const u = r() * 2 - 1, th = r() * K.TAU; GPTS.push([Math.sqrt(1 - u * u) * Math.cos(th), u, Math.sqrt(1 - u * u) * Math.sin(th)]); } }
227
+ W.globe = (ctx, o) => {
228
+ const a = o.alpha ?? 1; if (a <= 0.003) return;
229
+ const cam = { x: o.x, y: o.y, s: o.r, d: 8, ry: o.ry || 0, rx: o.rx ?? -0.25 };
230
+ K.glow(ctx, o.x, o.y, o.r * 2.1, o.color || C.eth, 0.35 * a);
231
+ ctx.save(); ctx.globalAlpha *= a; ctx.globalCompositeOperation = 'lighter';
232
+ // 经纬线
233
+ for (let lat = -60; lat <= 60; lat += 30) {
234
+ const pts = []; const y = Math.sin(lat * Math.PI / 180), rr = Math.cos(lat * Math.PI / 180);
235
+ for (let k = 0; k <= 48; k++) { const th = k / 48 * K.TAU; const p = K.proj([Math.cos(th) * rr, y, Math.sin(th) * rr], cam); pts.push(p); }
236
+ ctx.strokeStyle = K.rgba(o.color || C.eth, 0.22); ctx.lineWidth = 1; ctx.beginPath(); pts.forEach((p, i) => (i ? ctx.lineTo(p[0], p[1]) : ctx.moveTo(p[0], p[1]))); ctx.stroke();
237
+ }
238
+ for (let lon = 0; lon < 180; lon += 30) {
239
+ const pts = []; for (let k = 0; k <= 48; k++) { const th = k / 48 * K.TAU; const q = K.rotY([Math.cos(th), Math.sin(th), 0], lon * Math.PI / 180); pts.push(K.proj(q, cam)); }
240
+ ctx.strokeStyle = K.rgba(o.color || C.eth, 0.16); ctx.beginPath(); pts.forEach((p, i) => (i ? ctx.lineTo(p[0], p[1]) : ctx.moveTo(p[0], p[1]))); ctx.stroke();
241
+ }
242
+ // 节点
243
+ const nodes = [];
244
+ GPTS.forEach((p, i) => { const q = K.proj(p, cam); const front = q[2] < cam.d; nodes.push(q); const r = front ? 1.8 : 1; ctx.fillStyle = K.rgba(i % 7 ? '#cfd8ff' : o.color2 || C.cyan, front ? 0.85 : 0.2); ctx.fillRect(q[0] - r / 2, q[1] - r / 2, r, r); });
245
+ // 连接弧
246
+ const arcs = o.arcs || 0;
247
+ for (let i = 0; i < arcs; i++) {
248
+ const A = GPTS[(i * 37) % GPTS.length], B = GPTS[(i * 91 + 13) % GPTS.length];
249
+ const pts = []; for (let k = 0; k <= 16; k++) { const s = k / 16; let m = [K.lerp(A[0], B[0], s), K.lerp(A[1], B[1], s), K.lerp(A[2], B[2], s)]; const l = Math.hypot(...m) || 1; const h = 1 + 0.25 * Math.sin(s * Math.PI); m = m.map((v) => v / l * h); pts.push(K.proj(m, cam)); }
250
+ const ph = K.fract((o.t || 0) * 0.4 + i * 0.13);
251
+ const vis = pts.slice(0, Math.max(2, Math.floor(pts.length * K.clamp(ph * 2))));
252
+ K.neon(ctx, vis, i % 2 ? (o.color2 || C.cyan) : (o.color || C.eth), 1.2, 0.7 * (1 - K.clamp((ph - 0.5) * 2)));
253
+ }
254
+ ctx.restore();
255
+ // 标记点
256
+ (o.markers || []).forEach((m) => {
257
+ const la = m.lat * Math.PI / 180, lo = -m.lon * Math.PI / 180;
258
+ const p = K.proj([Math.cos(la) * Math.cos(lo), Math.sin(la), Math.cos(la) * Math.sin(lo)], cam);
259
+ if (p[2] > cam.d + 0.2 || (m.a ?? 1) <= 0) return;
260
+ const ma = m.a ?? 1;
261
+ K.glow(ctx, p[0], p[1], o.r * 0.22, m.color || C.gold, ma);
262
+ K.neonCircle(ctx, p[0], p[1], o.r * 0.06 + K.fract((o.t || 0) * 0.8) * o.r * 0.2, m.color || C.gold, 1.5, ma * (1 - K.fract((o.t || 0) * 0.8)));
263
+ if (m.label) K.text(ctx, m.label, p[0] + o.r * 0.12, p[1] - o.r * 0.12, { size: o.r * 0.13, align: 'left', color: C.white, alpha: ma, stroke: 6 });
264
+ });
265
+ };
266
+
267
+ /* ---------- 通用组件:真实价格曲线(对数坐标) ---------- */
268
+ const PTS = window.ETH_PRICE.pts;
269
+ /**
270
+ * o: {x,y,w,h, d0, d1 (x 轴范围,日序号), upTo(绘制到的日序号), alpha, notes:[{day,label,color,a}], color}
271
+ */
272
+ W.priceChart = (ctx, o) => {
273
+ const a = o.alpha ?? 1; if (a <= 0.003) return;
274
+ const { x, y, w, h } = o; const lmin = Math.log10(1), lmax = Math.log10(10000);
275
+ const X = (d) => x + (d - o.d0) / (o.d1 - o.d0) * w;
276
+ const Y = (v) => y + h - (Math.log10(Math.max(1, v)) - lmin) / (lmax - lmin) * h;
277
+ ctx.save(); ctx.globalAlpha *= a;
278
+ K.glass(ctx, x - 24, y - 50, w + 48, h + 96, { r: 20, tint: o.color || C.eth });
279
+ // 网格
280
+ ctx.font = K.font('num', 20, 500); ctx.textAlign = 'right'; ctx.textBaseline = 'middle';
281
+ [1, 10, 100, 1000, 10000].forEach((v) => { const yy = Y(v); ctx.strokeStyle = 'rgba(255,255,255,.07)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(x, yy); ctx.lineTo(x + w, yy); ctx.stroke(); ctx.fillStyle = 'rgba(220,226,255,.45)'; ctx.fillText('$' + v.toLocaleString('en-US'), x + w, yy - 12); });
282
+ ctx.textAlign = 'center';
283
+ for (let yr = 2015; yr <= 2026; yr++) { const d = K.day(yr + '-01-01'); if (d < o.d0 || d > o.d1) continue; const xx = X(d); ctx.fillStyle = 'rgba(220,226,255,.4)'; ctx.fillText(String(yr), xx, y + h + 26); ctx.strokeStyle = 'rgba(255,255,255,.05)'; ctx.beginPath(); ctx.moveTo(xx, y); ctx.lineTo(xx, y + h); ctx.stroke(); }
284
+ // 曲线
285
+ const pts = [];
286
+ for (const [d, v] of PTS) { if (d < o.d0) continue; if (d > o.upTo) break; pts.push([X(d), Y(v)]); }
287
+ if (o.upTo > (PTS[0][0]) && pts.length) { const v = K.priceAtDay(o.upTo); pts.push([X(o.upTo), Y(v)]); }
288
+ if (pts.length > 1) {
289
+ const g = ctx.createLinearGradient(0, y, 0, y + h); g.addColorStop(0, K.rgba(o.color || C.eth, 0.35)); g.addColorStop(1, K.rgba(o.color || C.eth, 0));
290
+ ctx.beginPath(); ctx.moveTo(pts[0][0], y + h); pts.forEach((p) => ctx.lineTo(p[0], p[1])); ctx.lineTo(pts[pts.length - 1][0], y + h); ctx.closePath(); ctx.fillStyle = g; ctx.fill();
291
+ K.neon(ctx, pts, o.color || C.eth, 2.4, 1);
292
+ const hd = pts[pts.length - 1]; K.glow(ctx, hd[0], hd[1], 40, C.white, 0.9);
293
+ if (o.head !== false) { const v = K.priceAtDay(o.upTo); K.text(ctx, K.money(v, v < 10 ? 2 : 0), hd[0] - 14, hd[1] - 30, { size: 30, fam: 'num', align: 'right', color: C.white, stroke: 6 }); }
294
+ }
295
+ // 标注
296
+ (o.notes || []).forEach((n) => {
297
+ const na = n.a ?? 1; if (na <= 0.01) return;
298
+ const v = n.v ?? K.priceAtDay(n.day); const px = X(n.day), py = Y(v);
299
+ K.glow(ctx, px, py, 34, n.color || C.gold, na);
300
+ ctx.save(); ctx.globalAlpha *= na; ctx.strokeStyle = K.rgba(n.color || C.gold, 0.7); ctx.setLineDash([4, 6]); ctx.beginPath(); ctx.moveTo(px, py); ctx.lineTo(px, py + (n.below ? 70 : -70)); ctx.stroke(); ctx.restore();
301
+ K.text(ctx, n.label, px, py + (n.below ? 96 : -96), { size: 28, color: n.color || C.gold, alpha: na, stroke: 6 });
302
+ if (n.sub) K.text(ctx, n.sub, px, py + (n.below ? 130 : -132), { size: 21, fam: 'num', color: C.white, alpha: na * 0.8, weight: 500, stroke: 5 });
303
+ });
304
+ ctx.restore();
305
+ };
306
+ })();
ethereum/tools/fonts.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 收集 ethereum/ 下所有用字,子集化三款开源字体为 woff2
2
+ import os, glob, sys
3
+ from fontTools import subset
4
+ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
5
+ SRC = sys.argv[1] # 字体源目录
6
+ chars = set()
7
+ for f in glob.glob(ROOT + '/js/*.js') + glob.glob(ROOT + '/*.html'):
8
+ chars |= set(open(f, encoding='utf-8').read())
9
+ chars |= set('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ $%+-−×=.,:;!?()[]/|·…—“”‘’,。!?、:;()《》【】#@&*_<>{}~^\'"⟠₿✓↓↑→▲')
10
+ text = ''.join(sorted(c for c in chars if ord(c) >= 32))
11
+ def make(src, out):
12
+ o = subset.Options(); o.flavor = 'woff2'; o.layout_features = ['*']; o.notdef_outline = True
13
+ f = subset.load_font(src, o); s = subset.Subsetter(o); s.populate(text=text); s.subset(f)
14
+ subset.save_font(f, out, o); print(out, os.path.getsize(out))
15
+ os.makedirs(ROOT + '/fonts', exist_ok=True)
16
+ make(SRC + '/smiley/SmileySans-Oblique.ttf', ROOT + '/fonts/deyi.woff2')
17
+ make(SRC + '/NotoSansSC.ttf', ROOT + '/fonts/notosc.woff2')
18
+ make(SRC + '/SpaceGrotesk.ttf', ROOT + '/fonts/grotesk.woff2')
19
+ print(len(text), 'chars')
ethereum/tools/frames.html ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html><meta charset="utf-8"><body style="margin:0;background:#000">
2
+ <canvas id="c"></canvas>
3
+ <script src="../data/eth-price.js"></script><script src="../js/data-script.js"></script>
4
+ <script src="../js/core.js"></script><script src="../js/world.js"></script><script src="../js/chapters.js"></script><script src="../js/render.js"></script><script src="../js/fonts.js"></script>
5
+ <script>
6
+ window.ready = loadFonts('../');
7
+ window.snap = async (t, mode, guides) => { await ready; const L = WORLD.LAYOUTS[mode]; const c = document.getElementById('c'); c.width = L.W; c.height = L.H; RENDER.render(c.getContext('2d'), t, L, { guides }); return c.toDataURL('image/jpeg', 0.82); };
8
+ window.cover = async (mode) => { await ready; const L = WORLD.LAYOUTS[mode]; const c = document.getElementById('c'); c.width = L.W; c.height = L.H; RENDER.renderCover(c.getContext('2d'), L); return c.toDataURL('image/png'); };
9
+ window.bench = async (n, mode) => { await ready; const L = WORLD.LAYOUTS[mode]; const c = document.getElementById('c'); c.width = L.W; c.height = L.H; const x = c.getContext('2d'); const t0 = performance.now(); for (let i = 0; i < n; i++) RENDER.render(x, i * K.TL.total / n, L); return (performance.now() - t0) / n; };
10
+ window.info = () => ({ total: K.TL.total, lines: K.TL.lines.map(l => [+(l.start).toFixed(2), +(l.end).toFixed(2)]), chapters: K.TL.chapters.map(c => [c.id, +c.start.toFixed(2), +c.dur.toFixed(2)]), marks: K.TL.chapters.map(c => [c.id, Object.fromEntries(Object.entries(c.marks).map(([k, v]) => [k, +v.toFixed(2)]))]) });
11
+ </script>