Spaces:
Running
Running
E4-E5: 以太坊简史 渲染引擎 + 13 章分镜
Browse files- .gitattributes +1 -0
- README.md +2 -2
- ethereum/fonts/deyi.woff2 +0 -0
- ethereum/fonts/grotesk.woff2 +0 -0
- ethereum/fonts/notosc.woff2 +3 -0
- ethereum/js/chapters.js +801 -0
- ethereum/js/core.js +178 -0
- ethereum/js/fonts.js +11 -0
- ethereum/js/render.js +219 -0
- ethereum/js/world.js +306 -0
- ethereum/tools/fonts.py +19 -0
- ethereum/tools/frames.html +11 -0
.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>
|