Spaces:
Running
Running
solana-clock: 动画引擎 + 15 个场景 + 播放器
Browse files- .gitattributes +3 -0
- README.md +2 -2
- solana-clock/css/app.css +134 -0
- solana-clock/fonts/sol-grotesk.woff2 +0 -0
- solana-clock/fonts/sol-mono.woff2 +0 -0
- solana-clock/fonts/sol-sc-400.woff2 +3 -0
- solana-clock/fonts/sol-sc-700.woff2 +3 -0
- solana-clock/fonts/sol-sc-900.woff2 +3 -0
- solana-clock/index.html +144 -0
- solana-clock/js/chart.js +95 -0
- solana-clock/js/draw.js +223 -0
- solana-clock/js/engine.js +227 -0
- solana-clock/js/main.js +207 -0
- solana-clock/js/props.js +213 -0
- solana-clock/js/scenes-a.js +746 -0
- solana-clock/js/scenes-b.js +788 -0
- solana-clock/js/scenes.js +3 -0
- solana-clock/js/timeline.js +75 -0
- solana-clock/tools/fonts.py +43 -0
- solana-clock/tools/lint_layout.py +70 -0
- solana-clock/tools/shoot.py +44 -0
.gitattributes
CHANGED
|
@@ -6,3 +6,6 @@ ethereum/covers/cover-portrait.png filter=lfs diff=lfs merge=lfs -text
|
|
| 6 |
ethereum/fonts/deyi.woff2 filter=lfs diff=lfs merge=lfs -text
|
| 7 |
guozu/covers/cover-landscape.png filter=lfs diff=lfs merge=lfs -text
|
| 8 |
guozu/covers/cover-portrait.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
ethereum/fonts/deyi.woff2 filter=lfs diff=lfs merge=lfs -text
|
| 7 |
guozu/covers/cover-landscape.png filter=lfs diff=lfs merge=lfs -text
|
| 8 |
guozu/covers/cover-portrait.png filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
solana-clock/fonts/sol-sc-900.woff2 filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
solana-clock/fonts/sol-sc-700.woff2 filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
solana-clock/fonts/sol-sc-400.woff2 filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -21,8 +21,8 @@ pinned: false
|
|
| 21 |
|---|------|------|
|
| 22 |
| 1 | 写计划 / 大纲 / 步骤(本节) | ✅ 完成 |
|
| 23 |
| 2 | 旁白文案定稿(15 场景 / 72 句)+ TTS 配音(纯语音 370 秒)+ 时长清单 + SOL 真实价格数据(2372 个日线点) | ✅ 完成 |
|
| 24 |
-
| 3 | 动画引擎 + 全部场景(横屏 16:9 / 竖屏 9:16 双布局 + 烧录字幕 + 安全区) |
|
| 25 |
-
| 4 | 程序化音乐 + 合成音效 + 旁白闪避混音 |
|
| 26 |
| 5 | 播放器 UI + 浏览器本地 MP4 导出(24 / 30 / 60 fps,横屏 / 竖屏) | ⬜ 待做 |
|
| 27 |
| 6 | 抖音 / B站 发布标题、简介、标签(一键复制) | ⬜ 待做 |
|
| 28 |
| 7 | 横屏 1920×1080 / 竖屏 1080×1920 封面图 | ⬜ 待做 |
|
|
|
|
| 21 |
|---|------|------|
|
| 22 |
| 1 | 写计划 / 大纲 / 步骤(本节) | ✅ 完成 |
|
| 23 |
| 2 | 旁白文案定稿(15 场景 / 72 句)+ TTS 配音(纯语音 370 秒)+ 时长清单 + SOL 真实价格数据(2372 个日线点) | ✅ 完成 |
|
| 24 |
+
| 3 | 动画引擎 + 全部场景(横屏 16:9 / 竖屏 9:16 双布局 + 烧录字幕 + 安全区)+ 播放器页面 + 字体子集;无头浏览器 90 帧截图 + 逐 0.5 秒排版检查通过 | ✅ 完成 |
|
| 25 |
+
| 4 | 程序化音乐 + 合成音效 + 旁白闪避混音 | ⏳ 进行中 |
|
| 26 |
| 5 | 播放器 UI + 浏览器本地 MP4 导出(24 / 30 / 60 fps,横屏 / 竖屏) | ⬜ 待做 |
|
| 27 |
| 6 | 抖音 / B站 发布标题、简介、标签(一键复制) | ⬜ 待做 |
|
| 28 |
| 7 | 横屏 1920×1080 / 竖屏 1080×1920 封面图 | ⬜ 待做 |
|
solana-clock/css/app.css
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@font-face { font-family: "SolSC"; src: url(../fonts/sol-sc-400.woff2) format("woff2"); font-weight: 100 500; font-display: swap; }
|
| 2 |
+
@font-face { font-family: "SolSC"; src: url(../fonts/sol-sc-700.woff2) format("woff2"); font-weight: 600 800; font-display: swap; }
|
| 3 |
+
@font-face { font-family: "SolSC"; src: url(../fonts/sol-sc-900.woff2) format("woff2"); font-weight: 850 1000; font-display: swap; }
|
| 4 |
+
@font-face { font-family: "SolGrotesk"; src: url(../fonts/sol-grotesk.woff2) format("woff2"); font-weight: 300 700; font-display: swap; }
|
| 5 |
+
@font-face { font-family: "SolMono"; src: url(../fonts/sol-mono.woff2) format("woff2"); font-weight: 100 800; font-display: swap; }
|
| 6 |
+
|
| 7 |
+
:root {
|
| 8 |
+
--bg: #07050d; --panel: #110d1c; --panel2: #171226; --line: rgba(255,255,255,.08);
|
| 9 |
+
--fg: #f4f2ff; --muted: #9a93b8; --purple: #9945FF; --green: #14F195; --pink: #DC1FFF;
|
| 10 |
+
--grad: linear-gradient(90deg, #9945FF, #14F195);
|
| 11 |
+
}
|
| 12 |
+
* { box-sizing: border-box; }
|
| 13 |
+
html, body { margin: 0; background: var(--bg); color: var(--fg); font-family: "SolSC", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; }
|
| 14 |
+
body { min-height: 100vh; background: radial-gradient(1200px 600px at 10% -10%, rgba(153,69,255,.18), transparent 60%), radial-gradient(900px 500px at 110% 10%, rgba(20,241,149,.10), transparent 60%), var(--bg); }
|
| 15 |
+
button { font: inherit; color: inherit; cursor: pointer; }
|
| 16 |
+
.app { max-width: 1600px; margin: 0 auto; padding: 18px 22px 30px; }
|
| 17 |
+
|
| 18 |
+
.top { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 16px; }
|
| 19 |
+
.brand { display: flex; gap: 14px; align-items: center; }
|
| 20 |
+
.brand h1 { margin: 0; font-size: 22px; font-weight: 900; letter-spacing: .5px; background: var(--grad); -webkit-background-clip: text; background-clip: text; color: transparent; }
|
| 21 |
+
.brand p { margin: 3px 0 0; color: var(--muted); font-size: 13px; }
|
| 22 |
+
.logo { width: 34px; height: 30px; display: grid; gap: 4px; transform: skewX(-18deg); }
|
| 23 |
+
.logo i { display: block; border-radius: 2px; background: var(--grad); }
|
| 24 |
+
.back { color: var(--muted); text-decoration: none; font-size: 13px; white-space: nowrap; border: 1px solid var(--line); padding: 7px 12px; border-radius: 999px; }
|
| 25 |
+
.back:hover { color: var(--fg); border-color: rgba(255,255,255,.25); }
|
| 26 |
+
|
| 27 |
+
.layout { display: grid; grid-template-columns: minmax(0, 1fr) 380px; gap: 18px; align-items: start; }
|
| 28 |
+
@media (max-width: 1100px) { .layout { grid-template-columns: 1fr; } }
|
| 29 |
+
|
| 30 |
+
.stage-wrap { display: flex; justify-content: center; align-items: center; background: #000; border-radius: 16px; border: 1px solid var(--line); overflow: hidden; box-shadow: 0 30px 80px -30px rgba(153,69,255,.45); }
|
| 31 |
+
.stage { position: relative; width: 100%; aspect-ratio: 16 / 9; }
|
| 32 |
+
.stage-wrap.portrait { background: repeating-linear-gradient(45deg, #0a0812 0 12px, #0d0a17 12px 24px); }
|
| 33 |
+
.stage-wrap.portrait .stage { width: auto; height: min(78vh, 980px); aspect-ratio: 9 / 16; }
|
| 34 |
+
.stage canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
|
| 35 |
+
.stage-wrap:fullscreen { border-radius: 0; border: 0; height: 100vh; }
|
| 36 |
+
.stage-wrap:fullscreen .stage { width: min(100vw, 100vh * 16 / 9); height: auto; }
|
| 37 |
+
.stage-wrap.portrait:fullscreen .stage { height: 100vh; width: auto; }
|
| 38 |
+
|
| 39 |
+
.big-play { position: absolute; inset: 0; margin: auto; width: max-content; height: max-content; display: flex; align-items: center; gap: 12px; padding: 16px 26px 16px 18px; border-radius: 999px; border: 1px solid rgba(255,255,255,.25); background: rgba(10,6,20,.6); backdrop-filter: blur(10px); font-size: 18px; font-weight: 700; transition: transform .2s, opacity .3s; }
|
| 40 |
+
.big-play svg { width: 40px; height: 40px; fill: #fff; background: var(--grad); border-radius: 50%; padding: 8px; }
|
| 41 |
+
.big-play:hover { transform: scale(1.05); }
|
| 42 |
+
.big-play.hide { opacity: 0; pointer-events: none; }
|
| 43 |
+
.loading { position: absolute; left: 50%; bottom: 14%; transform: translateX(-50%); display: flex; gap: 10px; align-items: center; font-size: 13px; color: var(--muted); background: rgba(0,0,0,.5); padding: 8px 14px; border-radius: 999px; }
|
| 44 |
+
.loading.hide { display: none; }
|
| 45 |
+
.spin { width: 14px; height: 14px; border-radius: 50%; border: 2px solid rgba(255,255,255,.2); border-top-color: var(--green); animation: sp 1s linear infinite; }
|
| 46 |
+
@keyframes sp { to { transform: rotate(360deg); } }
|
| 47 |
+
|
| 48 |
+
.controls { display: flex; align-items: center; gap: 12px; margin-top: 12px; background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 8px 12px; }
|
| 49 |
+
.ctl { width: 38px; height: 38px; border: 0; border-radius: 10px; background: transparent; display: grid; place-items: center; flex: none; }
|
| 50 |
+
.ctl:hover { background: rgba(255,255,255,.07); }
|
| 51 |
+
.ctl svg { width: 24px; height: 24px; fill: currentColor; }
|
| 52 |
+
.time { font-family: "SolMono", ui-monospace, monospace; font-size: 13px; color: var(--muted); white-space: nowrap; min-width: 92px; }
|
| 53 |
+
.time .sep { margin: 0 4px; opacity: .5; }
|
| 54 |
+
.scrub { position: relative; flex: 1; height: 34px; cursor: pointer; touch-action: none; }
|
| 55 |
+
.scrub .track { position: absolute; left: 0; right: 0; top: 15px; height: 4px; border-radius: 4px; background: rgba(255,255,255,.12); overflow: hidden; }
|
| 56 |
+
.scrub .fill { height: 100%; width: 0; background: var(--grad); }
|
| 57 |
+
.scrub:hover .track { height: 6px; top: 14px; }
|
| 58 |
+
.scrub .marks i { position: absolute; top: 12px; width: 2px; height: 10px; background: rgba(255,255,255,.35); border-radius: 1px; }
|
| 59 |
+
.scrub .knob { position: absolute; top: 10px; width: 14px; height: 14px; margin-left: -7px; border-radius: 50%; background: #fff; box-shadow: 0 0 0 4px rgba(20,241,149,.25); left: 0; }
|
| 60 |
+
.scrub .tip { position: absolute; bottom: 34px; transform: translateX(-50%); white-space: nowrap; font-size: 12px; background: #000; border: 1px solid var(--line); padding: 4px 8px; border-radius: 6px; opacity: 0; pointer-events: none; transition: opacity .15s; }
|
| 61 |
+
.scrub:hover .tip { opacity: 1; }
|
| 62 |
+
|
| 63 |
+
.toggles { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 16px; margin-top: 12px; }
|
| 64 |
+
.seg { display: inline-flex; background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 4px; gap: 4px; }
|
| 65 |
+
.seg button { border: 0; background: transparent; padding: 8px 14px; border-radius: 9px; color: var(--muted); font-size: 14px; line-height: 1.25; }
|
| 66 |
+
.seg button.on { background: linear-gradient(135deg, rgba(153,69,255,.35), rgba(20,241,149,.22)); color: var(--fg); box-shadow: inset 0 0 0 1px rgba(255,255,255,.12); }
|
| 67 |
+
.seg.small { display: flex; width: 100%; }
|
| 68 |
+
.seg.small button { flex: 1; font-size: 13px; padding: 8px 6px; }
|
| 69 |
+
.seg em { font-style: normal; font-size: 11px; opacity: .7; }
|
| 70 |
+
.chk { display: inline-flex; align-items: center; gap: 6px; font-size: 14px; color: var(--muted); cursor: pointer; user-select: none; }
|
| 71 |
+
.chk input { accent-color: var(--green); width: 16px; height: 16px; }
|
| 72 |
+
.hint { font-size: 12px; color: #5f5878; margin-left: auto; }
|
| 73 |
+
@media (max-width: 700px) { .hint { display: none; } }
|
| 74 |
+
|
| 75 |
+
.side { background: var(--panel); border: 1px solid var(--line); border-radius: 16px; overflow: hidden; position: sticky; top: 14px; max-height: calc(100vh - 28px); display: flex; flex-direction: column; }
|
| 76 |
+
.tabs { display: flex; border-bottom: 1px solid var(--line); flex: none; }
|
| 77 |
+
.tabs button { flex: 1; border: 0; background: transparent; padding: 13px 4px; color: var(--muted); font-size: 14px; font-weight: 700; border-bottom: 2px solid transparent; }
|
| 78 |
+
.tabs button.on { color: var(--fg); border-bottom-color: var(--green); }
|
| 79 |
+
.panel { display: none; padding: 16px; overflow: auto; }
|
| 80 |
+
.panel.on { display: block; }
|
| 81 |
+
.panel h3 { margin: 0 0 6px; font-size: 16px; }
|
| 82 |
+
.muted { color: var(--muted); font-size: 13px; line-height: 1.6; margin: 0 0 12px; }
|
| 83 |
+
.muted.small { font-size: 12px; margin-top: 10px; }
|
| 84 |
+
|
| 85 |
+
.chapters { list-style: none; margin: 0; padding: 0; counter-reset: c; }
|
| 86 |
+
.chapters li { display: flex; gap: 10px; align-items: center; padding: 9px 10px; border-radius: 10px; cursor: pointer; transition: background .15s; }
|
| 87 |
+
.chapters li:hover { background: rgba(255,255,255,.05); }
|
| 88 |
+
.chapters li.on { background: linear-gradient(90deg, rgba(153,69,255,.25), rgba(20,241,149,.08)); }
|
| 89 |
+
.chapters .n { font-family: "SolMono", monospace; font-size: 11px; color: var(--muted); width: 22px; }
|
| 90 |
+
.chapters .ttl { flex: 1; font-size: 14px; font-weight: 700; }
|
| 91 |
+
.chapters .yr { font-size: 11px; color: var(--green); font-family: "SolMono", monospace; margin-left: 6px; font-weight: 400; }
|
| 92 |
+
.chapters .ts { font-family: "SolMono", monospace; font-size: 12px; color: var(--muted); }
|
| 93 |
+
.chapters .pbar { height: 2px; background: var(--grad); width: 0; border-radius: 2px; margin-top: 4px; }
|
| 94 |
+
|
| 95 |
+
.field { margin-bottom: 12px; }
|
| 96 |
+
.field > span { display: block; font-size: 12px; color: var(--muted); margin-bottom: 6px; }
|
| 97 |
+
.exp-actions { display: flex; gap: 10px; margin-top: 14px; }
|
| 98 |
+
.primary { flex: 1; border: 0; border-radius: 12px; padding: 12px; font-weight: 900; font-size: 15px; color: #08050f; background: var(--grad); box-shadow: 0 10px 30px -10px rgba(20,241,149,.6); }
|
| 99 |
+
.primary:disabled { opacity: .5; cursor: wait; }
|
| 100 |
+
.ghost { border: 1px solid rgba(255,255,255,.18); background: transparent; border-radius: 10px; padding: 10px 14px; color: var(--fg); }
|
| 101 |
+
.ghost:hover { background: rgba(255,255,255,.06); }
|
| 102 |
+
.ghost.small { padding: 4px 10px; font-size: 12px; margin-left: 8px; border-radius: 8px; }
|
| 103 |
+
.progress { margin-top: 14px; }
|
| 104 |
+
.progress .bar { height: 8px; background: rgba(255,255,255,.08); border-radius: 8px; overflow: hidden; }
|
| 105 |
+
.progress .bar div { height: 100%; width: 0; background: var(--grad); transition: width .2s; }
|
| 106 |
+
.ptext { display: flex; justify-content: space-between; font-size: 12px; color: var(--muted); margin-top: 6px; font-family: "SolMono", monospace; }
|
| 107 |
+
.exp-preview { width: 100%; margin-top: 10px; border-radius: 8px; border: 1px solid var(--line); background: #000; }
|
| 108 |
+
.result { margin-top: 14px; padding: 12px; border-radius: 12px; background: rgba(20,241,149,.08); border: 1px solid rgba(20,241,149,.3); font-size: 13px; line-height: 1.6; }
|
| 109 |
+
.result a { color: var(--green); font-weight: 700; }
|
| 110 |
+
.result.err { background: rgba(255,59,92,.08); border-color: rgba(255,59,92,.35); }
|
| 111 |
+
|
| 112 |
+
.pub-block { margin-top: 14px; }
|
| 113 |
+
.pub-head { display: flex; align-items: center; justify-content: space-between; font-size: 12px; color: var(--muted); margin-bottom: 6px; }
|
| 114 |
+
.pub-head b { color: var(--fg); font-size: 13px; }
|
| 115 |
+
.pub-text { white-space: pre-wrap; background: var(--panel2); border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; font-size: 13px; line-height: 1.65; max-height: 260px; overflow: auto; }
|
| 116 |
+
.pub-alt { display: flex; flex-direction: column; gap: 6px; }
|
| 117 |
+
.pub-alt .row { display: flex; gap: 8px; align-items: flex-start; background: var(--panel2); border: 1px solid var(--line); border-radius: 10px; padding: 8px 10px; font-size: 13px; line-height: 1.5; }
|
| 118 |
+
.pub-alt .row span { flex: 1; }
|
| 119 |
+
.tags { display: flex; flex-wrap: wrap; gap: 6px; }
|
| 120 |
+
.tags span { font-size: 12px; padding: 3px 9px; border-radius: 999px; background: rgba(153,69,255,.18); border: 1px solid rgba(153,69,255,.35); }
|
| 121 |
+
.copy { flex: none; border: 1px solid rgba(255,255,255,.18); background: transparent; border-radius: 8px; padding: 3px 10px; font-size: 12px; color: var(--fg); }
|
| 122 |
+
.copy:hover { background: rgba(20,241,149,.12); border-color: rgba(20,241,149,.5); }
|
| 123 |
+
.count { font-family: "SolMono", monospace; font-size: 11px; color: var(--muted); }
|
| 124 |
+
.count.over { color: #ff6b81; }
|
| 125 |
+
|
| 126 |
+
.covers { display: grid; gap: 14px; }
|
| 127 |
+
.covers figure { margin: 0; }
|
| 128 |
+
.covers canvas { width: 100%; border-radius: 10px; border: 1px solid var(--line); display: block; background: #000; }
|
| 129 |
+
.covers .cover-p { width: 56%; margin: 0 auto; }
|
| 130 |
+
.covers figcaption { font-size: 12px; color: var(--muted); margin-top: 6px; display: flex; align-items: center; justify-content: center; }
|
| 131 |
+
|
| 132 |
+
.foot { margin-top: 22px; font-size: 12px; color: #5f5878; text-align: center; line-height: 1.6; }
|
| 133 |
+
.toast { position: fixed; left: 50%; bottom: 30px; transform: translateX(-50%) translateY(20px); background: #111; border: 1px solid rgba(20,241,149,.4); color: var(--fg); padding: 10px 18px; border-radius: 999px; font-size: 14px; opacity: 0; pointer-events: none; transition: all .25s; z-index: 50; }
|
| 134 |
+
.toast.on { opacity: 1; transform: translateX(-50%); }
|
solana-clock/fonts/sol-grotesk.woff2
ADDED
|
Binary file (19.4 kB). View file
|
|
|
solana-clock/fonts/sol-mono.woff2
ADDED
|
Binary file (34.5 kB). View file
|
|
|
solana-clock/fonts/sol-sc-400.woff2
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:710128990e82a6e9b800590829c4aafc04858adb4e5e7d2c759c52d38b5dca93
|
| 3 |
+
size 129180
|
solana-clock/fonts/sol-sc-700.woff2
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7187fcdff06944959bf4237af6c05857fcfee7e2347acf8a90bd6189a7e2c8ef
|
| 3 |
+
size 129048
|
solana-clock/fonts/sol-sc-900.woff2
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2d88e49addb97e22470ed9f8b25b2fa7d3c574f3765eeba7fd3c34f679586750
|
| 3 |
+
size 127684
|
solana-clock/index.html
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
| 6 |
+
<title>SOLANA:给区块链装上时钟的人 · 动画史</title>
|
| 7 |
+
<meta name="description" content="Solana 从 2017 年一个深夜的灵感,到宕机、FTX 暴雷、Meme 狂欢与 ETF 的完整动画史。网页实时播放,可本地导出 B 站横屏 / 抖音竖屏 MP4。">
|
| 8 |
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Ccircle cx='32' cy='32' r='30' fill='%2309060f' stroke='%2314F195' stroke-width='4'/%3E%3Cpath d='M32 12v20l12 8' stroke='%239945FF' stroke-width='6' fill='none' stroke-linecap='round'/%3E%3C/svg%3E">
|
| 9 |
+
<link rel="stylesheet" href="css/app.css">
|
| 10 |
+
</head>
|
| 11 |
+
<body>
|
| 12 |
+
<div class="app">
|
| 13 |
+
<header class="top">
|
| 14 |
+
<div class="brand">
|
| 15 |
+
<span class="logo" aria-hidden="true"><i></i><i></i><i></i></span>
|
| 16 |
+
<div>
|
| 17 |
+
<h1>SOLANA:给区块链装上时钟的人</h1>
|
| 18 |
+
<p>一部约 6 分半钟的动画史 · 旁白 / 配乐 / 音效 · 全部在你的浏览器里实时生成</p>
|
| 19 |
+
</div>
|
| 20 |
+
</div>
|
| 21 |
+
<a class="back" href="../index.html">← 返回动画工作室</a>
|
| 22 |
+
</header>
|
| 23 |
+
|
| 24 |
+
<main class="layout">
|
| 25 |
+
<section class="stage-col">
|
| 26 |
+
<div class="stage-wrap" id="stageWrap">
|
| 27 |
+
<div class="stage" id="stage">
|
| 28 |
+
<canvas id="view"></canvas>
|
| 29 |
+
<button class="big-play" id="bigPlay" aria-label="播放">
|
| 30 |
+
<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
| 31 |
+
<span id="bigPlayText">点击播放(含声音)</span>
|
| 32 |
+
</button>
|
| 33 |
+
<div class="loading" id="loading"><div class="spin"></div><span id="loadingText">正在准备字体与画面…</span></div>
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
|
| 37 |
+
<div class="controls">
|
| 38 |
+
<button class="ctl" id="btnPlay" title="播放 / 暂停(空格)"><svg viewBox="0 0 24 24" id="icoPlay"><path d="M8 5v14l11-7z"/></svg></button>
|
| 39 |
+
<div class="time"><span id="tCur">0:00</span><span class="sep">/</span><span id="tTot">0:00</span></div>
|
| 40 |
+
<div class="scrub" id="scrub">
|
| 41 |
+
<div class="track"><div class="fill" id="scrubFill"></div></div>
|
| 42 |
+
<div class="marks" id="scrubMarks"></div>
|
| 43 |
+
<div class="knob" id="scrubKnob"></div>
|
| 44 |
+
<div class="tip" id="scrubTip"></div>
|
| 45 |
+
</div>
|
| 46 |
+
<button class="ctl" id="btnMute" title="静音(M)"><svg viewBox="0 0 24 24" id="icoVol"><path d="M4 9v6h4l5 5V4L8 9H4zm12.5 3A4.5 4.5 0 0 0 14 8v8a4.5 4.5 0 0 0 2.5-4z"/></svg></button>
|
| 47 |
+
<button class="ctl" id="btnFull" title="全屏(F)"><svg viewBox="0 0 24 24"><path d="M5 5h5v2H7v3H5V5zm9 0h5v5h-2V7h-3V5zM5 14h2v3h3v2H5v-5zm12 3v-3h2v5h-5v-2h3z"/></svg></button>
|
| 48 |
+
</div>
|
| 49 |
+
|
| 50 |
+
<div class="toggles">
|
| 51 |
+
<div class="seg" id="segOrient" role="tablist" aria-label="画面方向">
|
| 52 |
+
<button data-v="landscape" class="on">▭ 横屏 16:9 · B站</button>
|
| 53 |
+
<button data-v="portrait">▯ 竖屏 9:16 · 抖音</button>
|
| 54 |
+
</div>
|
| 55 |
+
<label class="chk"><input type="checkbox" id="chkSubs" checked> 字幕</label>
|
| 56 |
+
<label class="chk" id="safeWrap"><input type="checkbox" id="chkSafe"> 显示抖音安全区</label>
|
| 57 |
+
<span class="hint">快捷键:空格 播放 · ←/→ 跳 5 秒 · [ / ] 切章节</span>
|
| 58 |
+
</div>
|
| 59 |
+
</section>
|
| 60 |
+
|
| 61 |
+
<aside class="side">
|
| 62 |
+
<nav class="tabs" id="tabs">
|
| 63 |
+
<button data-tab="chapters" class="on">章节</button>
|
| 64 |
+
<button data-tab="export">导出 MP4</button>
|
| 65 |
+
<button data-tab="publish">发布文案</button>
|
| 66 |
+
<button data-tab="cover">封面</button>
|
| 67 |
+
</nav>
|
| 68 |
+
|
| 69 |
+
<div class="panel on" data-panel="chapters">
|
| 70 |
+
<ol class="chapters" id="chapters"></ol>
|
| 71 |
+
</div>
|
| 72 |
+
|
| 73 |
+
<div class="panel" data-panel="export">
|
| 74 |
+
<h3>在浏览器里直接导出视频</h3>
|
| 75 |
+
<p class="muted">画面逐帧离线渲染,声音一次性合成,全程不上传任何数据。导出时长取决于电脑性能,通常是视频时长的 0.5–3 倍。</p>
|
| 76 |
+
<div class="field">
|
| 77 |
+
<span>画面</span>
|
| 78 |
+
<div class="seg small" id="segExpOrient">
|
| 79 |
+
<button data-v="landscape" class="on">横屏 1920×1080<br><em>B 站</em></button>
|
| 80 |
+
<button data-v="portrait">竖屏 1080×1920<br><em>抖音(已留安全区)</em></button>
|
| 81 |
+
</div>
|
| 82 |
+
</div>
|
| 83 |
+
<div class="field">
|
| 84 |
+
<span>帧率</span>
|
| 85 |
+
<div class="seg small" id="segFps">
|
| 86 |
+
<button data-v="24">24 fps<br><em>电影感</em></button>
|
| 87 |
+
<button data-v="30" class="on">30 fps<br><em>推荐</em></button>
|
| 88 |
+
<button data-v="60">60 fps<br><em>最丝滑</em></button>
|
| 89 |
+
</div>
|
| 90 |
+
</div>
|
| 91 |
+
<div class="field">
|
| 92 |
+
<span>清晰度</span>
|
| 93 |
+
<div class="seg small" id="segQ">
|
| 94 |
+
<button data-v="std">标准<br><em>体积小</em></button>
|
| 95 |
+
<button data-v="high" class="on">高<br><em>推荐</em></button>
|
| 96 |
+
<button data-v="max">极高<br><em>体积大</em></button>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
<label class="chk"><input type="checkbox" id="expSubs" checked> 烧录字幕</label>
|
| 100 |
+
<div class="exp-actions">
|
| 101 |
+
<button class="primary" id="btnExport">开始导出 MP4</button>
|
| 102 |
+
<button class="ghost" id="btnCancel" hidden>取消</button>
|
| 103 |
+
</div>
|
| 104 |
+
<div class="progress" id="expProg" hidden>
|
| 105 |
+
<div class="bar"><div id="expBar"></div></div>
|
| 106 |
+
<div class="ptext"><span id="expStage">准备中…</span><span id="expPct">0%</span></div>
|
| 107 |
+
<canvas id="expPreview" class="exp-preview"></canvas>
|
| 108 |
+
</div>
|
| 109 |
+
<div class="result" id="expResult" hidden></div>
|
| 110 |
+
<p class="muted small" id="expSupport"></p>
|
| 111 |
+
</div>
|
| 112 |
+
|
| 113 |
+
<div class="panel" data-panel="publish">
|
| 114 |
+
<div class="seg small" id="segPlat">
|
| 115 |
+
<button data-v="douyin" class="on">抖音</button>
|
| 116 |
+
<button data-v="bilibili">B 站</button>
|
| 117 |
+
</div>
|
| 118 |
+
<div id="publishBox"></div>
|
| 119 |
+
</div>
|
| 120 |
+
|
| 121 |
+
<div class="panel" data-panel="cover">
|
| 122 |
+
<p class="muted">封面由同一套画面引擎实时绘制,可直接下载 PNG。</p>
|
| 123 |
+
<div class="covers">
|
| 124 |
+
<figure>
|
| 125 |
+
<canvas id="coverL" class="cover-l"></canvas>
|
| 126 |
+
<figcaption>横屏封面 1920×1080(B 站)<button class="ghost small" data-dl="landscape">下载 PNG</button></figcaption>
|
| 127 |
+
</figure>
|
| 128 |
+
<figure>
|
| 129 |
+
<canvas id="coverP" class="cover-p"></canvas>
|
| 130 |
+
<figcaption>竖屏封面 1080×1920(抖音)<button class="ghost small" data-dl="portrait">下载 PNG</button></figcaption>
|
| 131 |
+
</figure>
|
| 132 |
+
</div>
|
| 133 |
+
</div>
|
| 134 |
+
</aside>
|
| 135 |
+
</main>
|
| 136 |
+
|
| 137 |
+
<footer class="foot">
|
| 138 |
+
价格数据来自公开行情的每日收盘价快照;画面中标注“示意”的内容为演示性可视化,不代表真实链上数据。本片不构成任何投资建议。
|
| 139 |
+
</footer>
|
| 140 |
+
</div>
|
| 141 |
+
<div class="toast" id="toast"></div>
|
| 142 |
+
<script type="module" src="js/main.js"></script>
|
| 143 |
+
</body>
|
| 144 |
+
</html>
|
solana-clock/js/chart.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Real SOL price chart renderer (data snapshot from Surf API)
|
| 2 |
+
import { SOL_PRICES, SOL_T0 } from './data-sol.js';
|
| 3 |
+
import { clamp, lerp, glow, text, hexA, fmtUSD, F_MONO, GREEN, PURPLE, RED, WHITE } from './draw.js';
|
| 4 |
+
|
| 5 |
+
export const idx = (date) => clamp(Math.round((Date.parse(date + 'T00:00:00Z') / 1000 - SOL_T0) / 86400), 0, SOL_PRICES.length - 1);
|
| 6 |
+
export const price = (i) => SOL_PRICES[clamp(Math.round(i), 0, SOL_PRICES.length - 1)];
|
| 7 |
+
export const dateOf = (i) => new Date((SOL_T0 + Math.round(i) * 86400) * 1000).toISOString().slice(0, 10);
|
| 8 |
+
export const LAST = SOL_PRICES.length - 1;
|
| 9 |
+
|
| 10 |
+
/** interpolated price at fractional index */
|
| 11 |
+
export function priceF(f) {
|
| 12 |
+
const i = Math.floor(f), k = f - i;
|
| 13 |
+
return lerp(price(i), price(i + 1), k);
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
/**
|
| 17 |
+
* Draw a chart. rect {x,y,w,h}. from/to: day indices. prog: 0..1 drawn fraction.
|
| 18 |
+
* Returns head {x,y,v,i,slope}.
|
| 19 |
+
*/
|
| 20 |
+
export function drawChart(ctx, rect, o) {
|
| 21 |
+
const { from, to, prog = 1, log = false, color = GREEN, color2 = PURPLE, alpha = 1, grid = true, yMin, yMax, lw = 5, fill = true, head = true, headLabel = true, dates = true, gridVals } = o;
|
| 22 |
+
const n = to - from;
|
| 23 |
+
let lo = Infinity, hi = -Infinity;
|
| 24 |
+
for (let i = from; i <= to; i++) { const v = price(i); if (v < lo) lo = v; if (v > hi) hi = v; }
|
| 25 |
+
lo = yMin ?? lo * (log ? .8 : .9); hi = yMax ?? hi * 1.08;
|
| 26 |
+
if (!log) lo = yMin ?? Math.max(0, lo - (hi - lo) * .05);
|
| 27 |
+
const Y = v => log ? rect.y + rect.h - (Math.log(v) - Math.log(lo)) / (Math.log(hi) - Math.log(lo)) * rect.h : rect.y + rect.h - (v - lo) / (hi - lo) * rect.h;
|
| 28 |
+
const X = i => rect.x + (i - from) / n * rect.w;
|
| 29 |
+
ctx.save(); ctx.globalAlpha *= alpha;
|
| 30 |
+
if (grid) {
|
| 31 |
+
const vals = gridVals || (log ? [1, 10, 100] : niceTicks(lo, hi));
|
| 32 |
+
ctx.lineWidth = 1;
|
| 33 |
+
for (const v of vals) {
|
| 34 |
+
if (v < lo || v > hi) continue;
|
| 35 |
+
const y = Y(v);
|
| 36 |
+
ctx.strokeStyle = 'rgba(255,255,255,.09)'; ctx.setLineDash([6, 8]); ctx.beginPath(); ctx.moveTo(rect.x, y); ctx.lineTo(rect.x + rect.w, y); ctx.stroke(); ctx.setLineDash([]);
|
| 37 |
+
text(ctx, fmtUSD(v), rect.x - 12, y, { size: 20, fam: F_MONO, weight: 500, color: 'rgba(255,255,255,.45)', align: 'right' });
|
| 38 |
+
}
|
| 39 |
+
if (dates) {
|
| 40 |
+
const y0 = new Date((SOL_T0 + from * 86400) * 1000).getUTCFullYear(), y1 = new Date((SOL_T0 + to * 86400) * 1000).getUTCFullYear();
|
| 41 |
+
for (let yr = y0; yr <= y1 + 1; yr++) {
|
| 42 |
+
const i = idx(yr + '-01-01'); if (i <= from || i >= to) continue;
|
| 43 |
+
const x = X(i);
|
| 44 |
+
ctx.strokeStyle = 'rgba(255,255,255,.08)'; ctx.beginPath(); ctx.moveTo(x, rect.y); ctx.lineTo(x, rect.y + rect.h); ctx.stroke();
|
| 45 |
+
text(ctx, String(yr), x, rect.y + rect.h + 22, { size: 20, fam: F_MONO, weight: 600, color: 'rgba(255,255,255,.5)', align: 'center' });
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
const endF = from + n * clamp(prog);
|
| 50 |
+
if (endF > from) {
|
| 51 |
+
const step = Math.max(1, Math.floor(n / 900));
|
| 52 |
+
ctx.beginPath();
|
| 53 |
+
let first = true, lx = 0, ly = 0;
|
| 54 |
+
for (let i = from; i <= Math.floor(endF); i += step) { const x = X(i), y = Y(price(i)); if (first) { ctx.moveTo(x, y); first = false; } else ctx.lineTo(x, y); lx = x; ly = y; }
|
| 55 |
+
const hv = priceF(endF), hx = X(endF), hy = Y(hv); ctx.lineTo(hx, hy);
|
| 56 |
+
const g = ctx.createLinearGradient(rect.x, 0, rect.x + rect.w, 0); g.addColorStop(0, color2); g.addColorStop(1, color);
|
| 57 |
+
// fill
|
| 58 |
+
if (fill) {
|
| 59 |
+
ctx.save(); ctx.lineTo(hx, rect.y + rect.h); ctx.lineTo(X(from), rect.y + rect.h); ctx.closePath();
|
| 60 |
+
const fg = ctx.createLinearGradient(0, rect.y, 0, rect.y + rect.h); fg.addColorStop(0, hexA(color, .28)); fg.addColorStop(1, hexA(color, 0));
|
| 61 |
+
ctx.fillStyle = fg; ctx.fill(); ctx.restore();
|
| 62 |
+
// re-trace stroke path
|
| 63 |
+
ctx.beginPath(); first = true;
|
| 64 |
+
for (let i = from; i <= Math.floor(endF); i += step) { const x = X(i), y = Y(price(i)); if (first) { ctx.moveTo(x, y); first = false; } else ctx.lineTo(x, y); }
|
| 65 |
+
ctx.lineTo(hx, hy);
|
| 66 |
+
}
|
| 67 |
+
ctx.lineJoin = 'round'; ctx.lineCap = 'round';
|
| 68 |
+
ctx.strokeStyle = hexA(color, .25); ctx.lineWidth = lw * 3.2; ctx.stroke();
|
| 69 |
+
ctx.strokeStyle = g; ctx.lineWidth = lw; ctx.stroke();
|
| 70 |
+
void lx; void ly;
|
| 71 |
+
const sl = (Y(priceF(Math.min(endF + 3, to))) - Y(priceF(Math.max(endF - 3, from)))) / (X(Math.min(endF + 3, to)) - X(Math.max(endF - 3, from)) || 1);
|
| 72 |
+
if (head && prog > 0) {
|
| 73 |
+
glow(ctx, hx, hy, 60, color, .9);
|
| 74 |
+
ctx.fillStyle = WHITE; ctx.beginPath(); ctx.arc(hx, hy, 8, 0, 7); ctx.fill();
|
| 75 |
+
if (headLabel) {
|
| 76 |
+
const right = hx < rect.x + rect.w * .75;
|
| 77 |
+
text(ctx, fmtUSD(hv), hx + (right ? 22 : -22), hy - 34, { size: 34, fam: F_MONO, weight: 800, color: WHITE, align: right ? 'left' : 'right', stroke: 6, strokeColor: 'rgba(0,0,0,.7)' });
|
| 78 |
+
text(ctx, dateOf(endF), hx + (right ? 22 : -22), hy - 2, { size: 20, fam: F_MONO, weight: 500, color: 'rgba(255,255,255,.6)', align: right ? 'left' : 'right' });
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
ctx.restore();
|
| 82 |
+
return { x: hx, y: hy, v: hv, i: endF, slope: sl, X, Y };
|
| 83 |
+
}
|
| 84 |
+
ctx.restore();
|
| 85 |
+
return { x: X(from), y: Y(price(from)), v: price(from), i: from, slope: 0, X, Y };
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
function niceTicks(lo, hi) {
|
| 89 |
+
const span = hi - lo, raw = span / 4, mag = Math.pow(10, Math.floor(Math.log10(raw)));
|
| 90 |
+
const step = [1, 2, 2.5, 5, 10].map(k => k * mag).find(s => span / s <= 5);
|
| 91 |
+
const out = []; for (let v = Math.ceil(lo / step) * step; v <= hi; v += step) out.push(+v.toFixed(4));
|
| 92 |
+
return out;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
export const RED_C = RED;
|
solana-clock/js/draw.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Drawing toolkit — everything here is stateless so any frame can be rendered at any time.
|
| 2 |
+
export const PURPLE = '#9945FF', GREEN = '#14F195', TEAL = '#00D1FF', PINK = '#DC1FFF', RED = '#FF3B5C', GOLD = '#FFC94A', WHITE = '#F4F2FF';
|
| 3 |
+
export const F_SC = '"SolSC","PingFang SC","Microsoft YaHei","Noto Sans CJK SC",sans-serif';
|
| 4 |
+
export const F_GR = '"SolGrotesk","SolSC","Helvetica Neue",Arial,sans-serif';
|
| 5 |
+
export const F_MONO = '"SolMono","SFMono-Regular",Menlo,Consolas,monospace';
|
| 6 |
+
|
| 7 |
+
export const clamp = (v, a = 0, b = 1) => Math.max(a, Math.min(b, v));
|
| 8 |
+
export const lerp = (a, b, t) => a + (b - a) * t;
|
| 9 |
+
export const inv = (a, b, v) => clamp((v - a) / (b - a));
|
| 10 |
+
export const eOut = t => 1 - Math.pow(1 - clamp(t), 3);
|
| 11 |
+
export const eIn = t => Math.pow(clamp(t), 3);
|
| 12 |
+
export const eIO = t => { t = clamp(t); return t < .5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; };
|
| 13 |
+
export const eBack = t => { t = clamp(t); const c1 = 1.70158, c3 = c1 + 1; return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2); };
|
| 14 |
+
export const eElastic = t => { t = clamp(t); if (t === 0 || t === 1) return t; return Math.pow(2, -10 * t) * Math.sin((t * 10 - .75) * (2 * Math.PI / 3)) + 1; };
|
| 15 |
+
/** eased progress of time t through window [a, a+d] */
|
| 16 |
+
export const win = (t, a, d = .6, ease = eOut) => ease(inv(a, a + d, t));
|
| 17 |
+
/** fade in at a, fade out at b */
|
| 18 |
+
export const life = (t, a, b, fi = .4, fo = .4) => Math.min(eOut(inv(a, a + fi, t)), 1 - eIn(inv(b - fo, b, t)));
|
| 19 |
+
|
| 20 |
+
export function rng(seed) {
|
| 21 |
+
let a = seed >>> 0;
|
| 22 |
+
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; };
|
| 23 |
+
}
|
| 24 |
+
export const hash1 = (n) => { const x = Math.sin(n * 127.1 + 311.7) * 43758.5453; return x - Math.floor(x); };
|
| 25 |
+
export function hexStr(seed, len = 8) { const r = rng(seed * 7919 + 13); let s = ''; for (let i = 0; i < len; i++) s += '0123456789abcdef'[Math.floor(r() * 16)]; return s; }
|
| 26 |
+
|
| 27 |
+
export function hexA(hex, a) {
|
| 28 |
+
const n = parseInt(hex.slice(1), 16);
|
| 29 |
+
return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`;
|
| 30 |
+
}
|
| 31 |
+
export function mix(h1, h2, t) {
|
| 32 |
+
const a = parseInt(h1.slice(1), 16), b = parseInt(h2.slice(1), 16);
|
| 33 |
+
const r = Math.round(lerp(a >> 16 & 255, b >> 16 & 255, t)), g = Math.round(lerp(a >> 8 & 255, b >> 8 & 255, t)), bl = Math.round(lerp(a & 255, b & 255, t));
|
| 34 |
+
return '#' + ((1 << 24) | (r << 16) | (g << 8) | bl).toString(16).slice(1);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// ---------- glow sprites (cached per color) ----------
|
| 38 |
+
const glowCache = new Map();
|
| 39 |
+
function glowSprite(color) {
|
| 40 |
+
let c = glowCache.get(color);
|
| 41 |
+
if (c) return c;
|
| 42 |
+
c = typeof OffscreenCanvas !== 'undefined' ? new OffscreenCanvas(128, 128) : Object.assign(document.createElement('canvas'), { width: 128, height: 128 });
|
| 43 |
+
const g = c.getContext('2d');
|
| 44 |
+
const gr = g.createRadialGradient(64, 64, 0, 64, 64, 64);
|
| 45 |
+
gr.addColorStop(0, hexA(color, 1)); gr.addColorStop(.18, hexA(color, .55)); gr.addColorStop(.45, hexA(color, .16)); gr.addColorStop(1, hexA(color, 0));
|
| 46 |
+
g.fillStyle = gr; g.fillRect(0, 0, 128, 128);
|
| 47 |
+
glowCache.set(color, c);
|
| 48 |
+
return c;
|
| 49 |
+
}
|
| 50 |
+
export function glow(ctx, x, y, r, color, a = 1) {
|
| 51 |
+
if (a <= 0 || r <= 0) return;
|
| 52 |
+
ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.globalAlpha = Math.min(1, a);
|
| 53 |
+
ctx.drawImage(glowSprite(color), x - r, y - r, r * 2, r * 2);
|
| 54 |
+
ctx.restore();
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// ---------- text ----------
|
| 58 |
+
export function font(size, weight = 700, fam = F_SC) { return `${weight} ${Math.round(size)}px ${fam}`; }
|
| 59 |
+
export function text(ctx, str, x, y, o = {}) {
|
| 60 |
+
const { size = 48, weight = 700, fam = F_SC, color = WHITE, align = 'left', base = 'middle', alpha = 1, shadow = 0, shadowColor = 'rgba(0,0,0,.6)', ls = 0, stroke = 0, strokeColor = 'rgba(0,0,0,.6)', maxW } = o;
|
| 61 |
+
if (alpha <= 0 || !str) return 0;
|
| 62 |
+
ctx.save();
|
| 63 |
+
ctx.globalAlpha *= alpha; ctx.font = font(size, weight, fam); ctx.textAlign = align; ctx.textBaseline = base;
|
| 64 |
+
if ('letterSpacing' in ctx) ctx.letterSpacing = ls + 'px';
|
| 65 |
+
if (maxW) { const w = ctx.measureText(str).width; if (w > maxW) { const k = maxW / w; ctx.translate(x, y); ctx.scale(k, k); x = 0; y = 0; } }
|
| 66 |
+
if (stroke) { ctx.lineJoin = 'round'; ctx.lineWidth = stroke; ctx.strokeStyle = strokeColor; ctx.strokeText(str, x, y); }
|
| 67 |
+
if (shadow) { ctx.shadowBlur = shadow; ctx.shadowColor = shadowColor; }
|
| 68 |
+
ctx.fillStyle = color; ctx.fillText(str, x, y);
|
| 69 |
+
const w = ctx.measureText(str).width;
|
| 70 |
+
ctx.restore();
|
| 71 |
+
return w;
|
| 72 |
+
}
|
| 73 |
+
export function measure(ctx, str, size, weight = 700, fam = F_SC, ls = 0) {
|
| 74 |
+
ctx.save(); ctx.font = font(size, weight, fam); if ('letterSpacing' in ctx) ctx.letterSpacing = ls + 'px';
|
| 75 |
+
const w = ctx.measureText(str).width; ctx.restore(); return w;
|
| 76 |
+
}
|
| 77 |
+
/** Solana gradient text */
|
| 78 |
+
export function gradText(ctx, str, x, y, o = {}) {
|
| 79 |
+
const { size = 80, align = 'left', c1 = PURPLE, c2 = GREEN } = o;
|
| 80 |
+
const w = measure(ctx, str, size, o.weight ?? 900, o.fam ?? F_SC, o.ls ?? 0);
|
| 81 |
+
const x0 = align === 'center' ? x - w / 2 : align === 'right' ? x - w : x;
|
| 82 |
+
const g = ctx.createLinearGradient(x0, y - size / 2, x0 + w, y + size / 2);
|
| 83 |
+
g.addColorStop(0, c1); g.addColorStop(1, c2);
|
| 84 |
+
return text(ctx, str, x, y, { weight: 900, ...o, color: g });
|
| 85 |
+
}
|
| 86 |
+
/** Glowing text: soft colored halo + crisp core */
|
| 87 |
+
export function glowText(ctx, str, x, y, o = {}) {
|
| 88 |
+
const c = o.glowColor || PURPLE;
|
| 89 |
+
text(ctx, str, x, y, { ...o, shadow: o.blur ?? 30, shadowColor: c });
|
| 90 |
+
return text(ctx, str, x, y, o);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
// ---------- shapes ----------
|
| 94 |
+
export function rr(ctx, x, y, w, h, r) {
|
| 95 |
+
r = Math.min(r, w / 2, h / 2);
|
| 96 |
+
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();
|
| 97 |
+
}
|
| 98 |
+
export function glass(ctx, x, y, w, h, o = {}) {
|
| 99 |
+
const { r = 22, alpha = 1, border = PURPLE, fill = 'rgba(20,16,44,.62)', bw = 2 } = o;
|
| 100 |
+
if (alpha <= 0) return;
|
| 101 |
+
ctx.save(); ctx.globalAlpha *= alpha;
|
| 102 |
+
rr(ctx, x, y, w, h, r); ctx.fillStyle = fill; ctx.fill();
|
| 103 |
+
const g = ctx.createLinearGradient(x, y, x + w, y + h); g.addColorStop(0, hexA(border, .9)); g.addColorStop(1, hexA(GREEN, .5));
|
| 104 |
+
ctx.lineWidth = bw; ctx.strokeStyle = g; ctx.stroke();
|
| 105 |
+
// top sheen
|
| 106 |
+
const s = ctx.createLinearGradient(0, y, 0, y + h * .5); s.addColorStop(0, 'rgba(255,255,255,.09)'); s.addColorStop(1, 'rgba(255,255,255,0)');
|
| 107 |
+
rr(ctx, x + 2, y + 2, w - 4, h * .5, r); ctx.fillStyle = s; ctx.fill();
|
| 108 |
+
ctx.restore();
|
| 109 |
+
}
|
| 110 |
+
export function pill(ctx, str, x, y, o = {}) {
|
| 111 |
+
const { size = 30, color = GREEN, alpha = 1, align = 'center', fg = '#0b0820', fam = F_SC } = o;
|
| 112 |
+
const w = measure(ctx, str, size, 800, fam) + size * 1.1, h = size * 1.7;
|
| 113 |
+
const x0 = align === 'center' ? x - w / 2 : align === 'right' ? x - w : x;
|
| 114 |
+
ctx.save(); ctx.globalAlpha *= alpha; rr(ctx, x0, y - h / 2, w, h, h / 2); ctx.fillStyle = color; ctx.fill(); ctx.restore();
|
| 115 |
+
text(ctx, str, x0 + w / 2, y + size * .04, { size, weight: 800, color: fg, align: 'center', alpha, fam });
|
| 116 |
+
return w;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
/** The Solana mark: three slanted bars. p = assemble progress 0..1 */
|
| 120 |
+
export function solLogo(ctx, cx, cy, size, p = 1, alpha = 1) {
|
| 121 |
+
if (alpha <= 0) return;
|
| 122 |
+
const w = size, h = size * .16, gap = size * .085, k = h * .9;
|
| 123 |
+
const ys = [-h * 1.5 - gap, -h / 2, h / 2 + gap];
|
| 124 |
+
ctx.save(); ctx.globalAlpha *= alpha; ctx.translate(cx, cy);
|
| 125 |
+
const g = ctx.createLinearGradient(-w / 2, size * .35, w / 2, -size * .35); g.addColorStop(0, PINK); g.addColorStop(.5, PURPLE); g.addColorStop(1, GREEN);
|
| 126 |
+
for (let i = 0; i < 3; i++) {
|
| 127 |
+
const pi = eBack(inv(i * .18, .5 + i * .18, p));
|
| 128 |
+
const dir = i === 1 ? 1 : -1;
|
| 129 |
+
const off = (1 - pi) * size * 1.6 * dir;
|
| 130 |
+
ctx.save(); ctx.globalAlpha *= clamp(pi * 1.4); ctx.translate(off, 0);
|
| 131 |
+
const y = ys[i], x0 = -w / 2;
|
| 132 |
+
ctx.beginPath();
|
| 133 |
+
if (i === 1) { ctx.moveTo(x0, y); ctx.lineTo(x0 + w - k, y); ctx.lineTo(x0 + w, y + h); ctx.lineTo(x0 + k, y + h); }
|
| 134 |
+
else { ctx.moveTo(x0 + k, y); ctx.lineTo(x0 + w, y); ctx.lineTo(x0 + w - k, y + h); ctx.lineTo(x0, y + h); }
|
| 135 |
+
ctx.closePath(); ctx.fillStyle = g; ctx.fill();
|
| 136 |
+
ctx.restore();
|
| 137 |
+
}
|
| 138 |
+
ctx.restore();
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
/** clock dial; hand angle in turns */
|
| 142 |
+
export function dial(ctx, cx, cy, r, o = {}) {
|
| 143 |
+
const { alpha = 1, hand = 0, ticks = 60, color = WHITE, accent = GREEN, lw = 3, minute = null } = o;
|
| 144 |
+
if (alpha <= 0) return;
|
| 145 |
+
ctx.save(); ctx.globalAlpha *= alpha; ctx.translate(cx, cy);
|
| 146 |
+
ctx.strokeStyle = hexA(color, .35); ctx.lineWidth = lw; ctx.beginPath(); ctx.arc(0, 0, r, 0, Math.PI * 2); ctx.stroke();
|
| 147 |
+
for (let i = 0; i < ticks; i++) {
|
| 148 |
+
const a = i / ticks * Math.PI * 2, big = i % 5 === 0;
|
| 149 |
+
ctx.strokeStyle = hexA(color, big ? .8 : .3); ctx.lineWidth = big ? lw * 1.4 : lw * .6;
|
| 150 |
+
ctx.beginPath(); ctx.moveTo(Math.sin(a) * r * (big ? .86 : .92), -Math.cos(a) * r * (big ? .86 : .92)); ctx.lineTo(Math.sin(a) * r * .97, -Math.cos(a) * r * .97); ctx.stroke();
|
| 151 |
+
}
|
| 152 |
+
if (minute !== null) {
|
| 153 |
+
const a = minute * Math.PI * 2; ctx.strokeStyle = hexA(color, .85); ctx.lineWidth = lw * 2.4; ctx.lineCap = 'round';
|
| 154 |
+
ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(Math.sin(a) * r * .55, -Math.cos(a) * r * .55); ctx.stroke();
|
| 155 |
+
}
|
| 156 |
+
const a = hand * Math.PI * 2;
|
| 157 |
+
ctx.strokeStyle = accent; ctx.lineWidth = lw * 1.3; ctx.lineCap = 'round';
|
| 158 |
+
ctx.beginPath(); ctx.moveTo(-Math.sin(a) * r * .12, Math.cos(a) * r * .12); ctx.lineTo(Math.sin(a) * r * .85, -Math.cos(a) * r * .85); ctx.stroke();
|
| 159 |
+
ctx.fillStyle = accent; ctx.beginPath(); ctx.arc(0, 0, lw * 2.4, 0, Math.PI * 2); ctx.fill();
|
| 160 |
+
ctx.restore();
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
/** burst of rays/particles at time since trigger dt */
|
| 164 |
+
export function burst(ctx, x, y, dt, o = {}) {
|
| 165 |
+
const { n = 26, r = 260, color = GREEN, seed = 1, dur = 1.1, size = 6 } = o;
|
| 166 |
+
if (dt < 0 || dt > dur) return;
|
| 167 |
+
const p = dt / dur, R = rng(seed);
|
| 168 |
+
ctx.save(); ctx.globalCompositeOperation = 'lighter';
|
| 169 |
+
for (let i = 0; i < n; i++) {
|
| 170 |
+
const a = R() * Math.PI * 2, sp = .5 + R() * .7, d = eOut(p) * r * sp;
|
| 171 |
+
const px = x + Math.cos(a) * d, py = y + Math.sin(a) * d + p * p * 60;
|
| 172 |
+
ctx.globalAlpha = (1 - p) * .95; ctx.fillStyle = i % 3 === 0 ? WHITE : (i % 2 ? color : PURPLE);
|
| 173 |
+
ctx.beginPath(); ctx.arc(px, py, size * (1 - p * .6) * (.6 + R() * .8), 0, Math.PI * 2); ctx.fill();
|
| 174 |
+
}
|
| 175 |
+
ctx.restore();
|
| 176 |
+
glow(ctx, x, y, r * .8 * (1 - p), color, (1 - p) * .7);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
/** ring shockwave */
|
| 180 |
+
export function ring(ctx, x, y, dt, o = {}) {
|
| 181 |
+
const { r = 300, color = GREEN, dur = .9, lw = 6 } = o;
|
| 182 |
+
if (dt < 0 || dt > dur) return;
|
| 183 |
+
const p = dt / dur;
|
| 184 |
+
ctx.save(); ctx.globalAlpha = (1 - p); ctx.strokeStyle = color; ctx.lineWidth = lw * (1 - p) + 1;
|
| 185 |
+
ctx.beginPath(); ctx.arc(x, y, eOut(p) * r, 0, Math.PI * 2); ctx.stroke(); ctx.restore();
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
/** split string by fraction for typewriter */
|
| 189 |
+
export const typed = (str, p) => { const a = Array.from(str); return a.slice(0, Math.floor(a.length * clamp(p) + 1e-6)).join(''); };
|
| 190 |
+
|
| 191 |
+
/** fit words into lines by measuring */
|
| 192 |
+
export function wrap(ctx, str, maxW, size, weight = 700, fam = F_SC) {
|
| 193 |
+
ctx.save(); ctx.font = font(size, weight, fam);
|
| 194 |
+
const out = []; let cur = '';
|
| 195 |
+
for (const ch of Array.from(str)) {
|
| 196 |
+
const t = cur + ch;
|
| 197 |
+
if (ctx.measureText(t).width > maxW && cur) {
|
| 198 |
+
// avoid starting a line with punctuation
|
| 199 |
+
if (/[,。;:、?!」』),.!?)]/.test(ch)) { out.push(t); cur = ''; continue; }
|
| 200 |
+
out.push(cur); cur = ch;
|
| 201 |
+
} else cur = t;
|
| 202 |
+
}
|
| 203 |
+
if (cur) out.push(cur);
|
| 204 |
+
ctx.restore(); return out;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
/** cubic camera shake offset */
|
| 208 |
+
export function shake(t, amp, seed = 3) { return [Math.sin(t * 61 + seed) * amp * hash1(Math.floor(t * 30) + seed), Math.cos(t * 53 + seed * 2) * amp * hash1(Math.floor(t * 30) + seed + 7)]; }
|
| 209 |
+
|
| 210 |
+
export function arrow(ctx, x1, y1, x2, y2, color, lw = 4, head = 16, alpha = 1) {
|
| 211 |
+
if (alpha <= 0) return;
|
| 212 |
+
const a = Math.atan2(y2 - y1, x2 - x1);
|
| 213 |
+
ctx.save(); ctx.globalAlpha *= alpha; ctx.strokeStyle = color; ctx.fillStyle = color; ctx.lineWidth = lw; ctx.lineCap = 'round';
|
| 214 |
+
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2 - Math.cos(a) * head * .6, y2 - Math.sin(a) * head * .6); ctx.stroke();
|
| 215 |
+
ctx.beginPath(); ctx.moveTo(x2, y2); ctx.lineTo(x2 - Math.cos(a - .45) * head, y2 - Math.sin(a - .45) * head); ctx.lineTo(x2 - Math.cos(a + .45) * head, y2 - Math.sin(a + .45) * head); ctx.closePath(); ctx.fill();
|
| 216 |
+
ctx.restore();
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
export function fmtUSD(v) {
|
| 220 |
+
if (v < 10) return '$' + v.toFixed(2);
|
| 221 |
+
if (v < 1000) return '$' + v.toFixed(v < 100 ? 1 : 0);
|
| 222 |
+
return '$' + Math.round(v).toLocaleString('en-US');
|
| 223 |
+
}
|
solana-clock/js/engine.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Frame compositor: background, scene, chapter header, subtitles, transitions.
|
| 2 |
+
import { buildTimeline, layout, subChunks } from './timeline.js';
|
| 3 |
+
import { SCENES } from './scenes.js';
|
| 4 |
+
import { clamp, lerp, inv, eOut, eIn, rng, glow, text, gradText, hexA, mix, measure, rr, F_SC, F_GR, F_MONO, PURPLE, GREEN, WHITE, GOLD, wrap } from './draw.js';
|
| 5 |
+
|
| 6 |
+
export const TL = buildTimeline();
|
| 7 |
+
|
| 8 |
+
const MOODS = {
|
| 9 |
+
hook: { top: '#120a2e', bot: '#030209', accent: PURPLE, grid: .0, stars: .9 },
|
| 10 |
+
night: { top: '#0b1030', bot: '#04040c', accent: '#5a6cff', grid: 0, stars: .6 },
|
| 11 |
+
poh: { top: '#0c1a2e', bot: '#02060a', accent: GREEN, grid: .55, stars: .5 },
|
| 12 |
+
name: { top: '#3a1440', bot: '#0a0612', accent: '#ff8a5c', grid: 0, stars: .2 },
|
| 13 |
+
build: { top: '#101a3a', bot: '#03040c', accent: '#00D1FF', grid: .5, stars: .6 },
|
| 14 |
+
mainnet: { top: '#2a0810', bot: '#050205', accent: '#ff3b5c', grid: .3, stars: .3 },
|
| 15 |
+
rise: { top: '#1c0b3a', bot: '#03020a', accent: GREEN, grid: .7, stars: .7 },
|
| 16 |
+
outage: { top: '#2a0a0a', bot: '#060203', accent: '#ff3b5c', grid: .45, stars: .2 },
|
| 17 |
+
ftx: { top: '#0b1220', bot: '#020306', accent: '#7aa0c8', grid: .15, stars: .25 },
|
| 18 |
+
rebuild: { top: '#2a1406', bot: '#070302', accent: '#ffa63d', grid: .35, stars: .5 },
|
| 19 |
+
meme: { top: '#2a0b3a', bot: '#05020a', accent: '#ff5ad1', grid: .6, stars: .7 },
|
| 20 |
+
ath: { top: '#06281f', bot: '#020806', accent: GREEN, grid: .7, stars: .9 },
|
| 21 |
+
wall: { top: '#0d1626', bot: '#030508', accent: GOLD, grid: .35, stars: .3 },
|
| 22 |
+
now: { top: '#140c30', bot: '#030209', accent: PURPLE, grid: .5, stars: .7 },
|
| 23 |
+
outro: { top: '#10082a', bot: '#020106', accent: GREEN, grid: .25, stars: 1 },
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
const STARS = (() => { const r = rng(42); return Array.from({ length: 260 }, () => ({ x: r(), y: r(), s: r() * 1.8 + .4, tw: r() * 6.28, sp: r() * .6 + .1 })); })();
|
| 27 |
+
const subCache = {};
|
| 28 |
+
function chunksFor(orient) {
|
| 29 |
+
if (subCache[orient]) return subCache[orient];
|
| 30 |
+
return (subCache[orient] = TL.lines.flatMap(l => subChunks(l, orient === 'portrait' ? 15 : 25).map(c => ({ ...c, news: l.v === 'news' }))));
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
export function sceneAt(T) {
|
| 34 |
+
const sc = TL.scenes;
|
| 35 |
+
for (let i = sc.length - 1; i >= 0; i--) if (T >= sc[i].start) return sc[i];
|
| 36 |
+
return sc[0];
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
function moodAt(T) {
|
| 40 |
+
const s = sceneAt(T), nx = TL.scenes[s.si + 1];
|
| 41 |
+
const a = MOODS[s.id];
|
| 42 |
+
if (!nx) return a;
|
| 43 |
+
const k = eOut(inv(nx.start - 1.2, nx.start + .3, T));
|
| 44 |
+
if (k <= 0) return a;
|
| 45 |
+
const b = MOODS[nx.id];
|
| 46 |
+
return { top: mix(a.top, b.top, k), bot: mix(a.bot, b.bot, k), accent: mix(a.accent, b.accent, k), grid: lerp(a.grid, b.grid, k), stars: lerp(a.stars, b.stars, k) };
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
function background(ctx, L, T, m) {
|
| 50 |
+
const { W, H } = L;
|
| 51 |
+
const g = ctx.createLinearGradient(0, 0, 0, H); g.addColorStop(0, m.top); g.addColorStop(1, m.bot);
|
| 52 |
+
ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
|
| 53 |
+
// nebulae
|
| 54 |
+
glow(ctx, W * (.25 + .05 * Math.sin(T * .07)), H * .28, Math.max(W, H) * .45, m.accent, .22);
|
| 55 |
+
glow(ctx, W * (.8 + .04 * Math.cos(T * .05)), H * .62, Math.max(W, H) * .4, PURPLE, .16);
|
| 56 |
+
// stars
|
| 57 |
+
if (m.stars > 0) {
|
| 58 |
+
// batched into 5 brightness buckets (one fill per bucket instead of one per star)
|
| 59 |
+
const B = 5, paths = Array.from({ length: B }, () => new Path2D());
|
| 60 |
+
for (const s of STARS) {
|
| 61 |
+
const x = ((s.x * W - T * s.sp * 6) % W + W) % W, y = s.y * H;
|
| 62 |
+
const tw = .5 + .5 * Math.sin(T * 1.3 + s.tw);
|
| 63 |
+
paths[Math.min(B - 1, Math.floor(tw * B))].rect(x, y, s.s, s.s);
|
| 64 |
+
}
|
| 65 |
+
ctx.save(); ctx.fillStyle = '#fff';
|
| 66 |
+
for (let k = 0; k < B; k++) { ctx.globalAlpha = m.stars * (.35 + .65 * (k + .5) / B) * .8; ctx.fill(paths[k]); }
|
| 67 |
+
ctx.restore();
|
| 68 |
+
}
|
| 69 |
+
// perspective time-grid floor
|
| 70 |
+
if (m.grid > .01) {
|
| 71 |
+
const hy = H * (L.portrait ? .7 : .66), cx = W / 2;
|
| 72 |
+
ctx.save(); ctx.globalAlpha = m.grid * .55;
|
| 73 |
+
const fg = ctx.createLinearGradient(0, hy, 0, H); fg.addColorStop(0, hexA(m.accent, 0)); fg.addColorStop(1, hexA(m.accent, .9));
|
| 74 |
+
ctx.strokeStyle = fg; ctx.lineWidth = 1.5;
|
| 75 |
+
for (let i = -14; i <= 14; i++) { ctx.beginPath(); ctx.moveTo(cx + i * 30, hy); ctx.lineTo(cx + i * W * .22, H); ctx.stroke(); }
|
| 76 |
+
const ph = (T * .45) % 1;
|
| 77 |
+
for (let j = 0; j < 14; j++) {
|
| 78 |
+
const z = (j + 1 - ph) / 14, y = hy + (H - hy) * Math.pow(z, 2.4);
|
| 79 |
+
ctx.globalAlpha = m.grid * .55 * z; ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
|
| 80 |
+
}
|
| 81 |
+
ctx.restore();
|
| 82 |
+
glow(ctx, cx, hy, W * .5, m.accent, m.grid * .18);
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
function header(ctx, L, sc, t) {
|
| 87 |
+
if (!sc.year && !sc.chapter) return;
|
| 88 |
+
if (sc.id === 'hook' || sc.id === 'outro') return;
|
| 89 |
+
const a = eOut(inv(.15, .8, t)) * (1 - eIn(inv(sc.dur - .5, sc.dur - .05, t)));
|
| 90 |
+
if (a <= 0) return;
|
| 91 |
+
const { x, y } = L.head;
|
| 92 |
+
const slide = (1 - eOut(inv(.15, .9, t))) * -40;
|
| 93 |
+
ctx.save(); ctx.globalAlpha = a; ctx.translate(slide, 0);
|
| 94 |
+
const ys = L.portrait ? 64 : 58;
|
| 95 |
+
const w = gradText(ctx, sc.year, x, y, { size: ys, fam: F_GR, weight: 700, ls: 2 });
|
| 96 |
+
ctx.fillStyle = hexA(GREEN, .8); ctx.fillRect(x + w + 18, y - 22, 4, 44);
|
| 97 |
+
text(ctx, sc.chapter, x + w + 38, y + 2, { size: L.portrait ? 40 : 36, weight: 800, color: WHITE });
|
| 98 |
+
// chapter index
|
| 99 |
+
text(ctx, `CH.${String(sc.si).padStart(2, '0')} / ${TL.scenes.length - 2}`, x + 2, y + (L.portrait ? 54 : 48), { size: 20, fam: F_MONO, weight: 500, color: hexA(WHITE, .5), ls: 3 });
|
| 100 |
+
ctx.restore();
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
function slotCounter(ctx, L, T, sc) {
|
| 104 |
+
if (sc.id === 'hook' && T < 3) return;
|
| 105 |
+
const slot = Math.floor(T / .4);
|
| 106 |
+
const a = .55;
|
| 107 |
+
const s = 'SLOT ' + String(slot).padStart(6, '0');
|
| 108 |
+
if (L.portrait) text(ctx, s, L.safe.r, L.safe.t + 52, { size: 22, fam: F_MONO, weight: 600, color: hexA(GREEN, a), align: 'right', ls: 2 });
|
| 109 |
+
else text(ctx, s, L.W - 96, 92, { size: 22, fam: F_MONO, weight: 600, color: hexA(GREEN, a), align: 'right', ls: 2 });
|
| 110 |
+
// tick dot pulses every slot
|
| 111 |
+
const ph = (T % .4) / .4;
|
| 112 |
+
const x = L.portrait ? L.safe.r - measure(ctx, s, 22, 600, F_MONO, 2) - 20 : L.W - 96 - measure(ctx, s, 22, 600, F_MONO, 2) - 20;
|
| 113 |
+
const y = L.portrait ? L.safe.t + 52 : 92;
|
| 114 |
+
ctx.save(); ctx.globalAlpha = (1 - ph) * .9; ctx.fillStyle = GREEN; ctx.beginPath(); ctx.arc(x, y, 6, 0, 7); ctx.fill(); ctx.restore();
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
function subtitles(ctx, L, T) {
|
| 118 |
+
const cs = chunksFor(L.orient);
|
| 119 |
+
const c = cs.find(c => T >= c.start - .05 && T < c.end + .12);
|
| 120 |
+
if (!c) return;
|
| 121 |
+
const a = Math.min(eOut(inv(c.start - .05, c.start + .12, T)), 1 - eIn(inv(c.end, c.end + .12, T)));
|
| 122 |
+
const { cx, y, maxW, size } = L.sub;
|
| 123 |
+
const lines = wrap(ctx, c.text, maxW - 40, size, 800);
|
| 124 |
+
const lh = size * 1.3;
|
| 125 |
+
const y0 = y - (lines.length - 1) * lh;
|
| 126 |
+
ctx.save(); ctx.globalAlpha = a;
|
| 127 |
+
let wmax = 0; for (const l of lines) wmax = Math.max(wmax, measure(ctx, l, size, 800));
|
| 128 |
+
rr(ctx, cx - wmax / 2 - 26, y0 - lh / 2 - 6, wmax + 52, lh * lines.length + 12, 16);
|
| 129 |
+
ctx.fillStyle = 'rgba(6,4,16,.55)'; ctx.fill();
|
| 130 |
+
lines.forEach((l, i) => text(ctx, l, cx, y0 + i * lh + 2, { size, weight: 800, align: 'center', color: c.news ? GOLD : '#fff', stroke: 7, strokeColor: 'rgba(0,0,0,.85)' }));
|
| 131 |
+
ctx.restore();
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
function transition(ctx, L, T) {
|
| 135 |
+
for (const sc of TL.scenes) {
|
| 136 |
+
if (sc.si === 0) continue;
|
| 137 |
+
const d = T - sc.start;
|
| 138 |
+
if (d < -.45 || d > .55) continue;
|
| 139 |
+
const p = inv(-.45, .55, d);
|
| 140 |
+
// diagonal light sweep
|
| 141 |
+
const { W, H } = L, x = lerp(-W * .6, W * 1.6, eIO(p));
|
| 142 |
+
ctx.save(); ctx.globalCompositeOperation = 'lighter';
|
| 143 |
+
const g = ctx.createLinearGradient(x - 420, 0, x + 420, H * .3);
|
| 144 |
+
g.addColorStop(0, 'rgba(153,69,255,0)'); g.addColorStop(.45, 'rgba(153,69,255,.35)'); g.addColorStop(.5, 'rgba(255,255,255,.55)'); g.addColorStop(.55, 'rgba(20,241,149,.35)'); g.addColorStop(1, 'rgba(20,241,149,0)');
|
| 145 |
+
ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
|
| 146 |
+
ctx.restore();
|
| 147 |
+
const f = Math.max(0, 1 - Math.abs(d) / .22) * .32;
|
| 148 |
+
if (f > 0) { ctx.save(); ctx.fillStyle = `rgba(255,255,255,${f})`; ctx.fillRect(0, 0, L.W, L.H); ctx.restore(); }
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
function eIO(t) { t = clamp(t); return t < .5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; }
|
| 152 |
+
|
| 153 |
+
const vigCache = {};
|
| 154 |
+
function vignette(ctx, L) {
|
| 155 |
+
const { W, H } = L;
|
| 156 |
+
let c = vigCache[L.orient];
|
| 157 |
+
if (!c) {
|
| 158 |
+
const w = W / 4, h = H / 4;
|
| 159 |
+
c = vigCache[L.orient] = typeof OffscreenCanvas !== 'undefined' ? new OffscreenCanvas(w, h) : Object.assign(document.createElement('canvas'), { width: w, height: h });
|
| 160 |
+
const x = c.getContext('2d');
|
| 161 |
+
const g = x.createRadialGradient(w / 2, h / 2, Math.min(w, h) * .35, w / 2, h / 2, Math.max(w, h) * .75);
|
| 162 |
+
g.addColorStop(0, 'rgba(0,0,0,0)'); g.addColorStop(1, 'rgba(0,0,0,.55)');
|
| 163 |
+
x.fillStyle = g; x.fillRect(0, 0, w, h);
|
| 164 |
+
}
|
| 165 |
+
ctx.drawImage(c, 0, 0, W, H);
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
export function sceneState(sc, T) {
|
| 169 |
+
const t = T - sc.start;
|
| 170 |
+
const cue = i => sc.lines[Math.min(i, sc.lines.length - 1)].local;
|
| 171 |
+
const end = i => { const l = sc.lines[Math.min(i, sc.lines.length - 1)]; return l.local + l.dur; };
|
| 172 |
+
const at = (i, sub) => { const l = sc.lines[i]; const k = Math.max(0, l.t.indexOf(sub)); return l.local + l.dur * k / Array.from(l.t).length; };
|
| 173 |
+
let li = -1; sc.lines.forEach((l, i) => { if (t >= l.local - .05) li = i; });
|
| 174 |
+
return { t, T, d: sc.dur, cue, end, at, li, sc };
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
/** Render one frame. ctx must already be scaled so that 1 unit = 1 design pixel. */
|
| 178 |
+
export function renderFrame(ctx, orient, T, opts = {}) {
|
| 179 |
+
const L = layout(orient);
|
| 180 |
+
T = clamp(T, 0, TL.total - 1e-4);
|
| 181 |
+
const sc = sceneAt(T);
|
| 182 |
+
const m = moodAt(T);
|
| 183 |
+
ctx.save();
|
| 184 |
+
background(ctx, L, T, m);
|
| 185 |
+
const S = SCENES[sc.id];
|
| 186 |
+
const st = sceneState(sc, T);
|
| 187 |
+
// render previous scene's tail briefly under the transition for continuity
|
| 188 |
+
ctx.save(); S.draw(ctx, L, st); ctx.restore();
|
| 189 |
+
vignette(ctx, L);
|
| 190 |
+
if (S.post) S.post(ctx, L, st, opts);
|
| 191 |
+
header(ctx, L, sc, st.t);
|
| 192 |
+
slotCounter(ctx, L, T, sc);
|
| 193 |
+
transition(ctx, L, T);
|
| 194 |
+
if (opts.subs !== false) subtitles(ctx, L, T);
|
| 195 |
+
// global fade in/out
|
| 196 |
+
const fi = 1 - inv(0, .6, T), fo = inv(TL.total - 1.6, TL.total - .1, T);
|
| 197 |
+
const k = Math.max(fi, fo);
|
| 198 |
+
if (k > 0) { ctx.fillStyle = `rgba(0,0,0,${k})`; ctx.fillRect(0, 0, L.W, L.H); }
|
| 199 |
+
if (opts.safe && L.portrait) safeGuides(ctx, L);
|
| 200 |
+
ctx.restore();
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
export function safeGuides(ctx, L) {
|
| 204 |
+
const { W, H, safe } = L;
|
| 205 |
+
ctx.save();
|
| 206 |
+
ctx.fillStyle = 'rgba(255,40,80,.18)';
|
| 207 |
+
ctx.fillRect(0, 0, W, safe.t); ctx.fillRect(0, safe.b, W, H - safe.b); ctx.fillRect(safe.r, safe.t, W - safe.r, safe.b - safe.t);
|
| 208 |
+
ctx.setLineDash([14, 10]); ctx.strokeStyle = '#ff4d6d'; ctx.lineWidth = 3; ctx.strokeRect(safe.l, safe.t, safe.r - safe.l, safe.b - safe.t);
|
| 209 |
+
text(ctx, '刘海 / 状态栏 / 顶部标签', W / 2, safe.t / 2, { size: 30, align: 'center', color: '#ffb3c1' });
|
| 210 |
+
text(ctx, '抖音标题文案 / 音乐条 / 底部导航', W / 2, safe.b + (H - safe.b) / 2, { size: 30, align: 'center', color: '#ffb3c1' });
|
| 211 |
+
ctx.translate(safe.r + (W - safe.r) / 2, (safe.t + safe.b) / 2); ctx.rotate(Math.PI / 2);
|
| 212 |
+
text(ctx, '点赞 / 评论 / 收藏 / 分享', 0, 0, { size: 28, align: 'center', color: '#ffb3c1' });
|
| 213 |
+
ctx.restore();
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
/** all SFX events (absolute time) collected from scenes */
|
| 217 |
+
export function allSfx() {
|
| 218 |
+
const ev = [];
|
| 219 |
+
for (const sc of TL.scenes) {
|
| 220 |
+
const S = SCENES[sc.id];
|
| 221 |
+
const st = sceneState(sc, sc.start);
|
| 222 |
+
const list = S.sfx ? S.sfx(st) : [];
|
| 223 |
+
for (const e of list) ev.push({ ...e, t: sc.start + e.t });
|
| 224 |
+
if (sc.si > 0) ev.push({ t: sc.start - .35, k: 'whoosh', v: .55 });
|
| 225 |
+
}
|
| 226 |
+
return ev.sort((a, b) => a.t - b.t);
|
| 227 |
+
}
|
solana-clock/js/main.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Player controller: canvas preview, transport, chapters, orientation, tabs; lazy-loads audio / export / publish / cover.
|
| 2 |
+
import { TL, renderFrame, sceneAt } from './engine.js';
|
| 3 |
+
import { SIZES } from './timeline.js';
|
| 4 |
+
|
| 5 |
+
const $ = (s) => document.querySelector(s);
|
| 6 |
+
const view = $('#view'), stage = $('#stage'), stageWrap = $('#stageWrap');
|
| 7 |
+
const ctx = view.getContext('2d');
|
| 8 |
+
const fmt = (s) => { s = Math.max(0, s); return `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, '0')}`; };
|
| 9 |
+
|
| 10 |
+
export const state = { T: 0, playing: false, orient: 'landscape', subs: true, safe: false, muted: false, dirty: true };
|
| 11 |
+
const qs = new URLSearchParams(location.search);
|
| 12 |
+
if (qs.get('o') === 'portrait') state.orient = 'portrait';
|
| 13 |
+
if (qs.has('t')) state.T = parseFloat(qs.get('t')) || 0;
|
| 14 |
+
if (qs.get('safe') === '1') state.safe = true;
|
| 15 |
+
|
| 16 |
+
export function toast(msg) { const t = $('#toast'); t.textContent = msg; t.classList.add('on'); clearTimeout(toast._h); toast._h = setTimeout(() => t.classList.remove('on'), 1800); }
|
| 17 |
+
|
| 18 |
+
// ---------- canvas sizing ----------
|
| 19 |
+
function resize() {
|
| 20 |
+
const r = stage.getBoundingClientRect();
|
| 21 |
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
| 22 |
+
const w = Math.max(2, Math.round(r.width * dpr)), h = Math.max(2, Math.round(r.height * dpr));
|
| 23 |
+
if (view.width !== w || view.height !== h) { view.width = w; view.height = h; }
|
| 24 |
+
state.dirty = true;
|
| 25 |
+
}
|
| 26 |
+
new ResizeObserver(resize).observe(stage);
|
| 27 |
+
|
| 28 |
+
function draw() {
|
| 29 |
+
const [W] = SIZES[state.orient];
|
| 30 |
+
ctx.setTransform(view.width / W, 0, 0, view.width / W, 0, 0);
|
| 31 |
+
renderFrame(ctx, state.orient, state.T, { subs: state.subs, safe: state.safe });
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// ---------- audio (lazy) ----------
|
| 35 |
+
let audioMod = null, mixBuf = null, actx = null, srcNode = null, gainNode = null, clock0 = 0, mixPromise = null;
|
| 36 |
+
async function ensureMix() {
|
| 37 |
+
if (mixBuf) return mixBuf;
|
| 38 |
+
if (!mixPromise) {
|
| 39 |
+
mixPromise = (async () => {
|
| 40 |
+
audioMod = await import('./audio.js');
|
| 41 |
+
setLoading('正在合成配乐、音效与旁白…');
|
| 42 |
+
mixBuf = await audioMod.buildMix(p => setLoading(`正在合成声音… ${Math.round(p * 100)}%`));
|
| 43 |
+
setLoading(null);
|
| 44 |
+
return mixBuf;
|
| 45 |
+
})().catch(e => { console.error(e); setLoading(null); toast('声音加载失败,将静音播放'); mixPromise = null; return null; });
|
| 46 |
+
}
|
| 47 |
+
return mixPromise;
|
| 48 |
+
}
|
| 49 |
+
export const getMix = ensureMix;
|
| 50 |
+
|
| 51 |
+
function stopSrc() { if (srcNode) { try { srcNode.onended = null; srcNode.stop(); } catch { } srcNode.disconnect(); srcNode = null; } }
|
| 52 |
+
function startSrc() {
|
| 53 |
+
stopSrc();
|
| 54 |
+
if (!mixBuf || !actx) return;
|
| 55 |
+
srcNode = actx.createBufferSource(); srcNode.buffer = mixBuf;
|
| 56 |
+
srcNode.connect(gainNode);
|
| 57 |
+
srcNode.start(0, Math.min(state.T, mixBuf.duration - .01));
|
| 58 |
+
clock0 = actx.currentTime - state.T;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
let wall0 = 0;
|
| 62 |
+
async function play() {
|
| 63 |
+
if (state.T >= TL.total - .05) state.T = 0;
|
| 64 |
+
$('#bigPlay').classList.add('hide');
|
| 65 |
+
if (!actx) {
|
| 66 |
+
const AC = window.AudioContext || window.webkitAudioContext;
|
| 67 |
+
actx = new AC({ sampleRate: 48000, latencyHint: 'playback' });
|
| 68 |
+
gainNode = actx.createGain(); gainNode.gain.value = state.muted ? 0 : 1; gainNode.connect(actx.destination);
|
| 69 |
+
}
|
| 70 |
+
await actx.resume().catch(() => { });
|
| 71 |
+
state.playing = true; setPlayIcon();
|
| 72 |
+
wall0 = performance.now() / 1000 - state.T;
|
| 73 |
+
if (!mixBuf) {
|
| 74 |
+
// run silently until the mix is ready, then attach audio at the current position
|
| 75 |
+
ensureMix().then(b => { if (b && state.playing) startSrc(); });
|
| 76 |
+
} else startSrc();
|
| 77 |
+
}
|
| 78 |
+
function pause() { state.playing = false; stopSrc(); setPlayIcon(); }
|
| 79 |
+
export function seek(T) {
|
| 80 |
+
state.T = Math.max(0, Math.min(TL.total - .01, T)); state.dirty = true;
|
| 81 |
+
wall0 = performance.now() / 1000 - state.T;
|
| 82 |
+
if (state.playing && mixBuf) startSrc();
|
| 83 |
+
}
|
| 84 |
+
function toggle() { state.playing ? pause() : play(); }
|
| 85 |
+
export const pausePlayer = pause;
|
| 86 |
+
|
| 87 |
+
function now() {
|
| 88 |
+
if (srcNode && actx) return actx.currentTime - clock0;
|
| 89 |
+
return performance.now() / 1000 - wall0;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
function setPlayIcon() {
|
| 93 |
+
$('#icoPlay').innerHTML = state.playing ? '<path d="M7 5h4v14H7zm6 0h4v14h-4z"/>' : '<path d="M8 5v14l11-7z"/>';
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
// ---------- loop ----------
|
| 97 |
+
let lastUI = -1;
|
| 98 |
+
function loop() {
|
| 99 |
+
if (state.playing) {
|
| 100 |
+
state.T = now();
|
| 101 |
+
if (state.T >= TL.total) { state.T = TL.total - .01; pause(); $('#bigPlay').classList.remove('hide'); $('#bigPlayText').textContent = '再看一遍'; }
|
| 102 |
+
state.dirty = true;
|
| 103 |
+
}
|
| 104 |
+
if (state.dirty) { draw(); state.dirty = false; updateUI(); }
|
| 105 |
+
requestAnimationFrame(loop);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
// ---------- scrubber + chapters ----------
|
| 109 |
+
const scrub = $('#scrub');
|
| 110 |
+
$('#tTot').textContent = fmt(TL.total);
|
| 111 |
+
$('#scrubMarks').innerHTML = TL.scenes.slice(1).map(s => `<i style="left:${s.start / TL.total * 100}%"></i>`).join('');
|
| 112 |
+
const chapEl = $('#chapters');
|
| 113 |
+
chapEl.innerHTML = TL.scenes.map((s, i) => `<li data-i="${i}"><span class="n">${String(i + 1).padStart(2, '0')}</span><div style="flex:1"><div class="ttl">${s.chapter}${s.year ? `<span class="yr">${s.year}</span>` : ''}</div><div class="pbar"></div></div><span class="ts">${fmt(s.start)}</span></li>`).join('');
|
| 114 |
+
chapEl.addEventListener('click', e => { const li = e.target.closest('li'); if (!li) return; seek(TL.scenes[+li.dataset.i].start + .01); if (!state.playing) play(); });
|
| 115 |
+
|
| 116 |
+
function updateUI() {
|
| 117 |
+
const p = state.T / TL.total;
|
| 118 |
+
if (Math.abs(p - lastUI) < 1e-5) return; lastUI = p;
|
| 119 |
+
$('#scrubFill').style.width = `${p * 100}%`;
|
| 120 |
+
$('#scrubKnob').style.left = `${p * 100}%`;
|
| 121 |
+
$('#tCur').textContent = fmt(state.T);
|
| 122 |
+
const cur = sceneAt(state.T);
|
| 123 |
+
chapEl.querySelectorAll('li').forEach((li, i) => {
|
| 124 |
+
const s = TL.scenes[i];
|
| 125 |
+
li.classList.toggle('on', i === cur.si);
|
| 126 |
+
li.querySelector('.pbar').style.width = i === cur.si ? `${(state.T - s.start) / s.dur * 100}%` : '0';
|
| 127 |
+
});
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
function scrubAt(e) { const r = scrub.getBoundingClientRect(); return Math.max(0, Math.min(1, (e.clientX - r.left) / r.width)) * TL.total; }
|
| 131 |
+
let dragging = false;
|
| 132 |
+
scrub.addEventListener('pointerdown', e => { dragging = true; scrub.setPointerCapture(e.pointerId); seek(scrubAt(e)); });
|
| 133 |
+
scrub.addEventListener('pointermove', e => {
|
| 134 |
+
const T = scrubAt(e), r = scrub.getBoundingClientRect();
|
| 135 |
+
const tip = $('#scrubTip'); tip.style.left = `${e.clientX - r.left}px`; tip.textContent = `${fmt(T)} · ${sceneAt(T).chapter}`;
|
| 136 |
+
if (dragging) seek(T);
|
| 137 |
+
});
|
| 138 |
+
scrub.addEventListener('pointerup', () => { dragging = false; });
|
| 139 |
+
|
| 140 |
+
// ---------- buttons ----------
|
| 141 |
+
$('#btnPlay').onclick = toggle;
|
| 142 |
+
$('#bigPlay').onclick = play;
|
| 143 |
+
view.addEventListener('click', toggle);
|
| 144 |
+
$('#btnMute').onclick = () => {
|
| 145 |
+
state.muted = !state.muted; if (gainNode) gainNode.gain.value = state.muted ? 0 : 1;
|
| 146 |
+
$('#icoVol').innerHTML = state.muted ? '<path d="M4 9v6h4l5 5V4L8 9H4zm12.6 3 2.7-2.7-1.4-1.4-2.7 2.7-2.7-2.7-1.4 1.4 2.7 2.7-2.7 2.7 1.4 1.4 2.7-2.7 2.7 2.7 1.4-1.4z"/>' : '<path d="M4 9v6h4l5 5V4L8 9H4zm12.5 3A4.5 4.5 0 0 0 14 8v8a4.5 4.5 0 0 0 2.5-4z"/>';
|
| 147 |
+
};
|
| 148 |
+
$('#btnFull').onclick = () => { document.fullscreenElement ? document.exitFullscreen() : stageWrap.requestFullscreen?.(); };
|
| 149 |
+
|
| 150 |
+
function setOrient(o) {
|
| 151 |
+
state.orient = o;
|
| 152 |
+
stageWrap.classList.toggle('portrait', o === 'portrait');
|
| 153 |
+
document.querySelectorAll('#segOrient button').forEach(b => b.classList.toggle('on', b.dataset.v === o));
|
| 154 |
+
$('#safeWrap').style.opacity = o === 'portrait' ? 1 : .4;
|
| 155 |
+
resize();
|
| 156 |
+
}
|
| 157 |
+
$('#segOrient').addEventListener('click', e => { const b = e.target.closest('button'); if (b) setOrient(b.dataset.v); });
|
| 158 |
+
$('#chkSubs').onchange = e => { state.subs = e.target.checked; state.dirty = true; };
|
| 159 |
+
$('#chkSafe').checked = state.safe;
|
| 160 |
+
$('#chkSafe').onchange = e => { state.safe = e.target.checked; if (state.safe && state.orient !== 'portrait') setOrient('portrait'); state.dirty = true; };
|
| 161 |
+
|
| 162 |
+
window.addEventListener('keydown', e => {
|
| 163 |
+
if (e.target.closest('input,textarea')) return;
|
| 164 |
+
if (e.code === 'Space') { e.preventDefault(); toggle(); }
|
| 165 |
+
else if (e.code === 'ArrowRight') seek(state.T + 5);
|
| 166 |
+
else if (e.code === 'ArrowLeft') seek(state.T - 5);
|
| 167 |
+
else if (e.key === ']') { const s = TL.scenes[Math.min(TL.scenes.length - 1, sceneAt(state.T).si + 1)]; seek(s.start + .01); }
|
| 168 |
+
else if (e.key === '[') { const c = sceneAt(state.T); const s = state.T - c.start > 2 ? c : TL.scenes[Math.max(0, c.si - 1)]; seek(s.start + .01); }
|
| 169 |
+
else if (e.key === 'm' || e.key === 'M') $('#btnMute').click();
|
| 170 |
+
else if (e.key === 'f' || e.key === 'F') $('#btnFull').click();
|
| 171 |
+
});
|
| 172 |
+
|
| 173 |
+
// ---------- tabs + lazy panels ----------
|
| 174 |
+
const loaded = {};
|
| 175 |
+
async function openTab(name) {
|
| 176 |
+
document.querySelectorAll('#tabs button').forEach(b => b.classList.toggle('on', b.dataset.tab === name));
|
| 177 |
+
document.querySelectorAll('.panel').forEach(p => p.classList.toggle('on', p.dataset.panel === name));
|
| 178 |
+
if (loaded[name]) return; loaded[name] = true;
|
| 179 |
+
try {
|
| 180 |
+
if (name === 'export') (await import('./exporter.js')).initExportPanel({ state, getMix: ensureMix, pausePlayer: pause, toast });
|
| 181 |
+
if (name === 'publish') (await import('./publish.js')).initPublishPanel({ toast });
|
| 182 |
+
if (name === 'cover') (await import('./cover.js')).initCoverPanel({ toast });
|
| 183 |
+
} catch (e) { console.error(e); loaded[name] = false; }
|
| 184 |
+
}
|
| 185 |
+
$('#tabs').addEventListener('click', e => { const b = e.target.closest('button'); if (b) openTab(b.dataset.tab); });
|
| 186 |
+
if (qs.get('tab')) openTab(qs.get('tab'));
|
| 187 |
+
|
| 188 |
+
// ---------- loading ----------
|
| 189 |
+
function setLoading(msg) { const l = $('#loading'); if (!msg) { l.classList.add('hide'); return; } l.classList.remove('hide'); $('#loadingText').textContent = msg; }
|
| 190 |
+
|
| 191 |
+
async function boot() {
|
| 192 |
+
setOrient(state.orient);
|
| 193 |
+
try {
|
| 194 |
+
await Promise.all(['900 40px SolSC', '700 40px SolSC', '400 40px SolSC', '700 40px SolGrotesk', '500 40px SolMono'].map(f => document.fonts.load(f, 'SOL时钟')));
|
| 195 |
+
} catch { }
|
| 196 |
+
await document.fonts.ready;
|
| 197 |
+
setLoading(null);
|
| 198 |
+
state.dirty = true;
|
| 199 |
+
window.__ready = true;
|
| 200 |
+
// warm the audio mix in the background so playback starts with sound
|
| 201 |
+
if (!qs.has('nomix')) setTimeout(() => ensureMix(), 400);
|
| 202 |
+
}
|
| 203 |
+
boot();
|
| 204 |
+
requestAnimationFrame(loop);
|
| 205 |
+
|
| 206 |
+
// test hook: render a specific time synchronously
|
| 207 |
+
window.__render = (T, o) => { if (o) setOrient(o); state.T = T; draw(); return true; };
|
solana-clock/js/props.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Reusable illustrated props (stateless)
|
| 2 |
+
import { clamp, lerp, inv, eOut, eBack, rng, glow, text, gradText, hexA, rr, glass, F_SC, F_GR, F_MONO, PURPLE, GREEN, WHITE, RED, GOLD, measure } from './draw.js';
|
| 3 |
+
|
| 4 |
+
export function monitor(ctx, x, y, w, o = {}) {
|
| 5 |
+
const { color = GREEN, alpha = 1, on = 1, label } = o;
|
| 6 |
+
if (alpha <= 0) return;
|
| 7 |
+
const h = w * .66;
|
| 8 |
+
ctx.save(); ctx.globalAlpha *= alpha; ctx.translate(x, y);
|
| 9 |
+
rr(ctx, -w / 2, -h / 2, w, h, w * .08); ctx.fillStyle = '#0d0b1c'; ctx.fill();
|
| 10 |
+
ctx.lineWidth = Math.max(2, w * .04); ctx.strokeStyle = hexA(color, .5 + .5 * on); ctx.stroke();
|
| 11 |
+
rr(ctx, -w / 2 + w * .08, -h / 2 + w * .08, w - w * .16, h - w * .16, w * .04);
|
| 12 |
+
ctx.fillStyle = hexA(color, .12 + .28 * on); ctx.fill();
|
| 13 |
+
ctx.fillStyle = hexA(color, .6); ctx.fillRect(-w * .12, h / 2, w * .24, w * .1); ctx.fillRect(-w * .25, h / 2 + w * .1, w * .5, w * .04);
|
| 14 |
+
if (label) text(ctx, label, 0, h / 2 + w * .32, { size: w * .2, fam: F_MONO, weight: 600, color: hexA(WHITE, .7), align: 'center' });
|
| 15 |
+
ctx.restore();
|
| 16 |
+
if (on > .5) glow(ctx, x, y, w * .9, color, .25 * on * alpha);
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export function coin(ctx, x, y, r, o = {}) {
|
| 20 |
+
const { label = '◎', c1 = PURPLE, c2 = GREEN, alpha = 1, spin = 0, fs } = o;
|
| 21 |
+
if (alpha <= 0) return;
|
| 22 |
+
const sx = Math.max(.08, Math.abs(Math.cos(spin)));
|
| 23 |
+
ctx.save(); ctx.globalAlpha *= alpha; ctx.translate(x, y); ctx.scale(sx, 1);
|
| 24 |
+
const g = ctx.createLinearGradient(-r, -r, r, r); g.addColorStop(0, c1); g.addColorStop(1, c2);
|
| 25 |
+
ctx.fillStyle = hexA('#000000', .35); ctx.beginPath(); ctx.arc(r * .08, r * .1, r, 0, 7); ctx.fill();
|
| 26 |
+
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(0, 0, r, 0, 7); ctx.fill();
|
| 27 |
+
ctx.strokeStyle = 'rgba(255,255,255,.55)'; ctx.lineWidth = r * .08; ctx.beginPath(); ctx.arc(0, 0, r * .8, 0, 7); ctx.stroke();
|
| 28 |
+
if (sx > .3) text(ctx, label, 0, r * .04, { size: fs || r * (label.length > 2 ? .5 : .9), weight: 900, fam: F_GR, color: '#fff', align: 'center' });
|
| 29 |
+
ctx.restore();
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
export function stamp(ctx, str, x, y, t, t0, o = {}) {
|
| 33 |
+
const { size = 120, color = RED, rot = -.12, fam = F_SC, glowC } = o;
|
| 34 |
+
const dt = t - t0;
|
| 35 |
+
if (dt < 0) return;
|
| 36 |
+
const p = eOut(inv(0, .28, dt));
|
| 37 |
+
const s = lerp(2.4, 1, p), a = clamp(dt / .12);
|
| 38 |
+
ctx.save(); ctx.translate(x, y); ctx.rotate(rot); ctx.scale(s, s); ctx.globalAlpha *= a;
|
| 39 |
+
const w = measure(ctx, str, size, 900, fam) + size * .5;
|
| 40 |
+
ctx.lineWidth = size * .07; ctx.strokeStyle = color; rr(ctx, -w / 2, -size * .7, w, size * 1.4, size * .18); ctx.stroke();
|
| 41 |
+
text(ctx, str, 0, size * .04, { size, weight: 900, fam, color, align: 'center' });
|
| 42 |
+
ctx.restore();
|
| 43 |
+
if (dt < .5) glow(ctx, x, y, size * 2.5, glowC || color, (1 - dt / .5) * .6);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
export function bubble(ctx, str, x, y, o = {}) {
|
| 47 |
+
const { size = 34, alpha = 1, color = 'rgba(255,255,255,.92)', fg = '#1a1030', tail = 'left' } = o;
|
| 48 |
+
if (alpha <= 0) return;
|
| 49 |
+
const w = measure(ctx, str, size, 800) + size * 1.2, h = size * 1.8;
|
| 50 |
+
ctx.save(); ctx.globalAlpha *= alpha;
|
| 51 |
+
rr(ctx, x - w / 2, y - h / 2, w, h, h * .4); ctx.fillStyle = color; ctx.fill();
|
| 52 |
+
ctx.beginPath(); const tx = tail === 'left' ? x - w / 2 + h * .5 : x + w / 2 - h * .5;
|
| 53 |
+
ctx.moveTo(tx - 12, y + h / 2 - 2); ctx.lineTo(tx + (tail === 'left' ? -18 : 18), y + h / 2 + 20); ctx.lineTo(tx + 14, y + h / 2 - 2); ctx.fill();
|
| 54 |
+
ctx.restore();
|
| 55 |
+
text(ctx, str, x, y + 2, { size, weight: 800, color: fg, align: 'center', alpha });
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
export function nodesIn(seed, n, rect, minD = 80) {
|
| 59 |
+
const r = rng(seed), out = [];
|
| 60 |
+
let guard = 0;
|
| 61 |
+
while (out.length < n && guard++ < 4000) {
|
| 62 |
+
const p = { x: rect.x + r() * rect.w, y: rect.y + r() * rect.h, s: r(), ph: r() * 6.28 };
|
| 63 |
+
if (out.every(q => Math.hypot(q.x - p.x, q.y - p.y) > minD)) out.push(p);
|
| 64 |
+
}
|
| 65 |
+
return out;
|
| 66 |
+
}
|
| 67 |
+
export function linksFor(nodes, k = 3) {
|
| 68 |
+
const L = [];
|
| 69 |
+
nodes.forEach((a, i) => {
|
| 70 |
+
const near = nodes.map((b, j) => ({ j, d: Math.hypot(a.x - b.x, a.y - b.y) })).filter(o => o.j !== i).sort((p, q) => p.d - q.d).slice(0, k);
|
| 71 |
+
for (const o of near) if (i < o.j || !L.some(l => l[0] === o.j && l[1] === i)) L.push([i, o.j]);
|
| 72 |
+
});
|
| 73 |
+
return L;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
export function rocket(ctx, x, y, s, o = {}) {
|
| 77 |
+
const { flame = 0, t = 0, alpha = 1 } = o;
|
| 78 |
+
ctx.save(); ctx.globalAlpha *= alpha; ctx.translate(x, y); ctx.scale(s, s);
|
| 79 |
+
if (flame > 0) {
|
| 80 |
+
for (let i = 0; i < 3; i++) {
|
| 81 |
+
const fl = (60 + 40 * Math.sin(t * 40 + i * 2)) * flame;
|
| 82 |
+
const g = ctx.createLinearGradient(0, 70, 0, 70 + fl * (1.2 - i * .25));
|
| 83 |
+
g.addColorStop(0, i === 2 ? '#fff' : i === 1 ? GOLD : '#ff6a3d'); g.addColorStop(1, 'rgba(255,80,40,0)');
|
| 84 |
+
ctx.fillStyle = g; ctx.beginPath(); ctx.moveTo(-22 + i * 7, 68); ctx.quadraticCurveTo(0, 70 + fl * (1.4 - i * .3), 22 - i * 7, 68); ctx.fill();
|
| 85 |
+
}
|
| 86 |
+
glow(ctx, 0, 90, 120 * flame, '#ff8a3d', .8);
|
| 87 |
+
}
|
| 88 |
+
const g = ctx.createLinearGradient(-30, 0, 30, 0); g.addColorStop(0, '#d8d4ee'); g.addColorStop(.5, '#ffffff'); g.addColorStop(1, '#9d97c4');
|
| 89 |
+
ctx.fillStyle = g;
|
| 90 |
+
ctx.beginPath(); ctx.moveTo(0, -110); ctx.bezierCurveTo(38, -70, 34, 20, 26, 70); ctx.lineTo(-26, 70); ctx.bezierCurveTo(-34, 20, -38, -70, 0, -110); ctx.fill();
|
| 91 |
+
ctx.fillStyle = PURPLE; ctx.beginPath(); ctx.moveTo(-26, 30); ctx.lineTo(-52, 80); ctx.lineTo(-24, 66); ctx.fill(); ctx.beginPath(); ctx.moveTo(26, 30); ctx.lineTo(52, 80); ctx.lineTo(24, 66); ctx.fill();
|
| 92 |
+
ctx.fillStyle = GREEN; ctx.beginPath(); ctx.arc(0, -30, 13, 0, 7); ctx.fill();
|
| 93 |
+
ctx.strokeStyle = '#2a2050'; ctx.lineWidth = 4; ctx.stroke();
|
| 94 |
+
ctx.restore();
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
export function skyline(ctx, x0, base, w, h, seed, o = {}) {
|
| 98 |
+
const { alpha = 1, lit = .5, color = '#0a0b1e', t = 0, win = GOLD } = o;
|
| 99 |
+
const r = rng(seed);
|
| 100 |
+
ctx.save(); ctx.globalAlpha *= alpha;
|
| 101 |
+
let x = x0;
|
| 102 |
+
while (x < x0 + w) {
|
| 103 |
+
const bw = 40 + r() * 90, bh = h * (.3 + r() * .7);
|
| 104 |
+
ctx.fillStyle = color; ctx.fillRect(x, base - bh, bw - 4, bh);
|
| 105 |
+
for (let wy = base - bh + 12; wy < base - 10; wy += 22) for (let wx = x + 8; wx < x + bw - 14; wx += 16) {
|
| 106 |
+
const v = r(); if (v < lit * (.7 + .3 * Math.sin(t * .3 + v * 20))) { ctx.fillStyle = hexA(win, .35 + .5 * v); ctx.fillRect(wx, wy, 7, 10); }
|
| 107 |
+
}
|
| 108 |
+
x += bw;
|
| 109 |
+
}
|
| 110 |
+
ctx.restore();
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
/** text that resolves from random glyphs */
|
| 114 |
+
export function scramble(target, p, seed = 1) {
|
| 115 |
+
const g = '!<>-_/[]{}=+*^?#ABCDEFGHJKLMNOPQRSTUVWXYZ0123456789';
|
| 116 |
+
const arr = Array.from(target);
|
| 117 |
+
return arr.map((ch, i) => {
|
| 118 |
+
const k = clamp(p * (arr.length + 3) - i);
|
| 119 |
+
if (k >= 1) return ch;
|
| 120 |
+
if (k <= 0) return '';
|
| 121 |
+
return g[Math.floor((Math.sin(seed * 99 + i * 13 + p * 400) * .5 + .5) * g.length) % g.length];
|
| 122 |
+
}).join('');
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
export function avatar(ctx, x, y, r, initials, c1, c2, alpha = 1) {
|
| 126 |
+
if (alpha <= 0) return;
|
| 127 |
+
ctx.save(); ctx.globalAlpha *= alpha;
|
| 128 |
+
const g = ctx.createLinearGradient(x - r, y - r, x + r, y + r); g.addColorStop(0, c1); g.addColorStop(1, c2);
|
| 129 |
+
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(x, y, r, 0, 7); ctx.fill();
|
| 130 |
+
// silhouette
|
| 131 |
+
ctx.save(); ctx.beginPath(); ctx.arc(x, y, r, 0, 7); ctx.clip();
|
| 132 |
+
ctx.fillStyle = 'rgba(10,6,30,.45)'; ctx.beginPath(); ctx.arc(x, y - r * .18, r * .36, 0, 7); ctx.fill();
|
| 133 |
+
ctx.beginPath(); ctx.ellipse(x, y + r * .78, r * .7, r * .55, 0, 0, 7); ctx.fill();
|
| 134 |
+
ctx.restore();
|
| 135 |
+
ctx.restore();
|
| 136 |
+
text(ctx, initials, x, y + r * .02, { size: r * .55, weight: 900, fam: F_GR, color: '#fff', align: 'center', alpha, stroke: 4, strokeColor: 'rgba(0,0,0,.35)' });
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
export function personCard(ctx, x, y, w, o) {
|
| 140 |
+
const { name, role, sub, ini, c1 = PURPLE, c2 = GREEN, a = 1 } = o;
|
| 141 |
+
if (a <= 0) return;
|
| 142 |
+
const h = w * 1.18;
|
| 143 |
+
ctx.save(); ctx.globalAlpha *= a; ctx.translate(x, y + (1 - a) * 60);
|
| 144 |
+
glass(ctx, -w / 2, -h / 2, w, h, { r: 26 });
|
| 145 |
+
avatar(ctx, 0, -h * .16, w * .26, ini, c1, c2);
|
| 146 |
+
text(ctx, name, 0, h * .2, { size: w * .1, weight: 800, fam: F_GR, color: WHITE, align: 'center', maxW: w * .9 });
|
| 147 |
+
text(ctx, role, 0, h * .31, { size: w * .085, weight: 700, color: GREEN, align: 'center', maxW: w * .9 });
|
| 148 |
+
if (sub) text(ctx, sub, 0, h * .41, { size: w * .07, weight: 500, color: 'rgba(255,255,255,.6)', align: 'center', maxW: w * .9 });
|
| 149 |
+
ctx.restore();
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
export function tower(ctx, x, base, w, h, label, o = {}) {
|
| 153 |
+
const { color = '#1b2440', edge = '#6f8cc0', alpha = 1, lit = .6, seed = 3 } = o;
|
| 154 |
+
if (alpha <= 0) return;
|
| 155 |
+
ctx.save(); ctx.globalAlpha *= alpha;
|
| 156 |
+
const g = ctx.createLinearGradient(x - w / 2, 0, x + w / 2, 0); g.addColorStop(0, color); g.addColorStop(1, '#0a0e1c');
|
| 157 |
+
ctx.fillStyle = g; ctx.fillRect(x - w / 2, base - h, w, h);
|
| 158 |
+
ctx.strokeStyle = hexA(edge, .7); ctx.lineWidth = 3; ctx.strokeRect(x - w / 2, base - h, w, h);
|
| 159 |
+
const r = rng(seed);
|
| 160 |
+
for (let yy = base - h + 70; yy < base - 14; yy += 26) for (let xx = x - w / 2 + 14; xx < x + w / 2 - 18; xx += 22) if (r() < lit) { ctx.fillStyle = hexA('#bfe3ff', .25 + r() * .5); ctx.fillRect(xx, yy, 10, 12); }
|
| 161 |
+
ctx.fillStyle = '#0c1020'; ctx.fillRect(x - w / 2 + 10, base - h + 10, w - 20, 50);
|
| 162 |
+
text(ctx, label, x, base - h + 36, { size: Math.min(44, w * .3), weight: 900, fam: F_GR, color: '#e9f1ff', align: 'center', maxW: w - 30 });
|
| 163 |
+
ctx.restore();
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
/** stateless falling debris: pieces of rect fall after t0 */
|
| 167 |
+
export function crumble(ctx, x, base, w, h, label, dt, o = {}) {
|
| 168 |
+
const { seed = 9, cols = 4, rows = 8 } = o;
|
| 169 |
+
const r = rng(seed);
|
| 170 |
+
const pw = w / cols, ph = h / rows;
|
| 171 |
+
ctx.save();
|
| 172 |
+
for (let j = 0; j < rows; j++) for (let i = 0; i < cols; i++) {
|
| 173 |
+
const delay = (rows - j) * .05 + r() * .25, vx = (r() - .5) * 260, spin = (r() - .5) * 6;
|
| 174 |
+
const tt = Math.max(0, dt - delay);
|
| 175 |
+
const px = x - w / 2 + i * pw + vx * tt, py = base - h + j * ph + 900 * tt * tt;
|
| 176 |
+
if (py > base + 400) continue;
|
| 177 |
+
ctx.save(); ctx.translate(px + pw / 2, py + ph / 2); ctx.rotate(spin * tt);
|
| 178 |
+
ctx.globalAlpha = clamp(1 - tt * .8);
|
| 179 |
+
ctx.fillStyle = (i + j) % 2 ? '#1b2440' : '#141b31'; ctx.fillRect(-pw / 2, -ph / 2, pw - 2, ph - 2);
|
| 180 |
+
ctx.strokeStyle = 'rgba(111,140,192,.6)'; ctx.lineWidth = 2; ctx.strokeRect(-pw / 2, -ph / 2, pw - 2, ph - 2);
|
| 181 |
+
ctx.restore();
|
| 182 |
+
}
|
| 183 |
+
ctx.restore();
|
| 184 |
+
void label;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
export function snow(ctx, L, T, n = 120, a = .7) {
|
| 188 |
+
const r = rng(77);
|
| 189 |
+
ctx.save(); ctx.fillStyle = '#e8f2ff';
|
| 190 |
+
for (let i = 0; i < n; i++) {
|
| 191 |
+
const sx = r(), sp = 40 + r() * 80, sz = 1.5 + r() * 3.5, ph = r() * 100;
|
| 192 |
+
const y = ((ph * 37 + T * sp) % (L.H + 40)) - 20, x = (sx * L.W + Math.sin(T * .8 + ph) * 30 + L.W) % L.W;
|
| 193 |
+
ctx.globalAlpha = a * (.4 + .6 * r()); ctx.beginPath(); ctx.arc(x, y, sz, 0, 7); ctx.fill();
|
| 194 |
+
}
|
| 195 |
+
ctx.restore();
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
export function rain(ctx, L, T, n = 160, a = .4, color = '#9fb4ff') {
|
| 199 |
+
const r = rng(51);
|
| 200 |
+
ctx.save(); ctx.strokeStyle = color; ctx.lineWidth = 1.6;
|
| 201 |
+
for (let i = 0; i < n; i++) {
|
| 202 |
+
const sx = r(), sp = 900 + r() * 700, ph = r() * 10, len = 20 + r() * 30;
|
| 203 |
+
const y = ((ph * 300 + T * sp) % (L.H + 100)) - 50, x = (sx * (L.W + 300) - y * .25);
|
| 204 |
+
ctx.globalAlpha = a * (.3 + .7 * r()); ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x - len * .25, y + len); ctx.stroke();
|
| 205 |
+
}
|
| 206 |
+
ctx.restore();
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
export function bigNum(ctx, str, x, y, size, o = {}) {
|
| 210 |
+
return gradText(ctx, str, x, y, { size, fam: F_GR, weight: 700, align: 'center', ...o });
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
export { GOLD, RED, GREEN, PURPLE, WHITE, F_SC, F_GR, F_MONO };
|
solana-clock/js/scenes-a.js
ADDED
|
@@ -0,0 +1,746 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Scenes 1–7: hook, night, poh, name, build, mainnet, rise
|
| 2 |
+
import { clamp, lerp, inv, eOut, eIn, eIO, eBack, eElastic, win, life, rng, hash1, hexStr, glow, text, gradText, glowText, glass, pill, rr, solLogo, dial, burst, ring, typed, arrow, shake, measure, hexA, fmtUSD, F_SC, F_GR, F_MONO, PURPLE, GREEN, TEAL, PINK, RED, GOLD, WHITE } from './draw.js';
|
| 3 |
+
import { monitor, coin, stamp, bubble, nodesIn, linksFor, rocket, skyline, scramble, personCard, rain, bigNum } from './props.js';
|
| 4 |
+
import { drawChart, idx, LAST } from './chart.js';
|
| 5 |
+
|
| 6 |
+
const BTC_2020_03 = [['03-05', 9083], ['03-06', 9152], ['03-07', 9098], ['03-08', 8756], ['03-09', 7908], ['03-10', 8031], ['03-11', 7832], ['03-12', 6537], ['03-13', 5696], ['03-14', 5409], ['03-15', 5424], ['03-16', 4665]]; // Surf market-price BTC daily
|
| 7 |
+
|
| 8 |
+
function chip(ctx, x, y, w, h, big, small, a, color = GREEN) {
|
| 9 |
+
if (a <= 0) return;
|
| 10 |
+
ctx.save(); ctx.globalAlpha *= a; ctx.translate(x, y); const s = lerp(.6, 1, eBack(a)); ctx.scale(s, s);
|
| 11 |
+
glass(ctx, -w / 2, -h / 2, w, h, { r: 24 });
|
| 12 |
+
gradText(ctx, big, 0, -h * .14, { size: h * .38, fam: F_GR, weight: 700, align: 'center', c2: color });
|
| 13 |
+
text(ctx, small, 0, h * .27, { size: h * .17, weight: 700, color: 'rgba(255,255,255,.75)', align: 'center' });
|
| 14 |
+
ctx.restore();
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
function speedLines(ctx, L, t, a, n = 46, color = WHITE) {
|
| 18 |
+
if (a <= 0) return;
|
| 19 |
+
const r = rng(5);
|
| 20 |
+
ctx.save(); ctx.globalCompositeOperation = 'lighter';
|
| 21 |
+
for (let i = 0; i < n; i++) {
|
| 22 |
+
const y = r() * L.H, sp = 1800 + r() * 2600, len = 120 + r() * 380, ph = r();
|
| 23 |
+
const x = ((ph * (L.W + 800) + t * sp) % (L.W + 800)) - 400;
|
| 24 |
+
const g = ctx.createLinearGradient(x - len, 0, x, 0); g.addColorStop(0, hexA(color, 0)); g.addColorStop(1, hexA(i % 3 ? GREEN : PURPLE, .7 * a));
|
| 25 |
+
ctx.strokeStyle = g; ctx.lineWidth = 1 + r() * 3; ctx.beginPath(); ctx.moveTo(x - len, y); ctx.lineTo(x, y); ctx.stroke();
|
| 26 |
+
}
|
| 27 |
+
ctx.restore();
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
// ---------------------------------------------------------------- 1. HOOK
|
| 31 |
+
const hook = {
|
| 32 |
+
draw(ctx, L, s) {
|
| 33 |
+
const { t } = s, st = L.stage, P = L.portrait;
|
| 34 |
+
const c1 = s.cue(1), c2 = s.cue(2), c3 = s.cue(3);
|
| 35 |
+
const m = Math.min(st.w, st.h);
|
| 36 |
+
const toC = eIO(inv(c1 - .2, c1 + .6, t));
|
| 37 |
+
const dx = P ? st.cx : lerp(st.cx - 430, st.cx, toC);
|
| 38 |
+
const dy = P ? lerp(st.cy - 170, st.cy - 40, toC) : st.cy;
|
| 39 |
+
const R = m * (P ? .33 : .38) * (1 + .1 * toC);
|
| 40 |
+
const aDial = win(t, .2, 1.2) * (1 - .7 * win(t, c2 - .2, .6)) * (1 - win(t, c3 - .2, .5));
|
| 41 |
+
const steps = Math.floor(Math.min(t, c1) / .4);
|
| 42 |
+
const hand = steps / 60 + (t > c1 ? Math.pow(t - c1, 2) * .55 : 0);
|
| 43 |
+
const ph = (t % .4) / .4;
|
| 44 |
+
glow(ctx, dx, dy, R * 1.5, PURPLE, (.25 + .3 * (1 - ph) * (t < c1 ? 1 : .3)) * aDial);
|
| 45 |
+
dial(ctx, dx, dy, R, { alpha: aDial, hand, minute: .1 + hand / 60, lw: 4 });
|
| 46 |
+
if (t < c1 && aDial > 0) ring(ctx, dx, dy, t - steps * .4, { r: R * 1.35, color: GREEN, dur: .4, lw: 3 });
|
| 47 |
+
// centre readout: 400ms
|
| 48 |
+
if (t < c1 + .4) {
|
| 49 |
+
const a = win(t, .6, .8) * (1 - win(t, c1, .4));
|
| 50 |
+
text(ctx, '400', dx, dy - 8, { size: R * .38, fam: F_GR, weight: 700, color: WHITE, align: 'center', alpha: a });
|
| 51 |
+
text(ctx, 'ms / slot', dx, dy + R * .26, { size: R * .1, fam: F_MONO, weight: 600, color: GREEN, align: 'center', alpha: a, ls: 3 });
|
| 52 |
+
}
|
| 53 |
+
// stat chips
|
| 54 |
+
const chips = [['数千笔', '每秒处理交易', s.at(0, '每秒')], ['400ms', '出一个区块', s.at(0, '四百')], ['<$0.01', '单笔手续费', s.at(0, '手续费')]];
|
| 55 |
+
chips.forEach(([b, sm, at], i) => {
|
| 56 |
+
const a = win(t, at, .45, eOut) * (1 - win(t, c1 - .15, .35));
|
| 57 |
+
const fly = win(t, c1 - .15, .5, eIn) * (i === 1 ? 0 : 1);
|
| 58 |
+
let x, y, w, h;
|
| 59 |
+
if (P) { w = 270; h = 150; x = st.cx + (i - 1) * 290; y = st.cy + 300; }
|
| 60 |
+
else { w = 470; h = 160; x = st.cx + 380; y = st.cy + (i - 1) * 200; }
|
| 61 |
+
chip(ctx, x + fly * 900, y, w, h, b, sm, a);
|
| 62 |
+
});
|
| 63 |
+
// c1: why so fast?
|
| 64 |
+
speedLines(ctx, L, t, win(t, c1 - .1, .3) * (1 - win(t, c2 - .2, .5)));
|
| 65 |
+
const aQ1 = win(t, c1, .35, eBack) * (1 - win(t, c2 - .1, .3));
|
| 66 |
+
if (aQ1 > 0) {
|
| 67 |
+
ctx.save(); ctx.translate(dx, dy); const sc = lerp(1.6, 1, eOut(inv(c1, c1 + .35, t))); ctx.scale(sc, sc);
|
| 68 |
+
glowText(ctx, '凭什么这么快?', 0, 0, { size: P ? 96 : 120, weight: 900, align: 'center', alpha: clamp(aQ1), glowColor: GREEN, blur: 40 });
|
| 69 |
+
ctx.restore();
|
| 70 |
+
}
|
| 71 |
+
// c2: the crash & the comeback, drawn with the real price history
|
| 72 |
+
const aC = win(t, c2 - .1, .4) * (1 - win(t, c3 - .3, .4));
|
| 73 |
+
if (aC > 0) {
|
| 74 |
+
const rect = P ? { x: st.x + 40, y: st.y + 260, w: st.w - 80, h: 460 } : { x: st.x + 80, y: st.y + 170, w: st.w - 160, h: 470 };
|
| 75 |
+
const prog = eIO(inv(c2, s.end(2) + .2, t));
|
| 76 |
+
drawChart(ctx, rect, { from: 0, to: LAST, prog, log: true, alpha: aC, grid: false, dates: false, headLabel: false, lw: 4, color: GREEN, color2: PINK, yMin: .4, yMax: 400 });
|
| 77 |
+
const a1 = win(t, s.at(2, '摔'), .3) * aC, a2 = win(t, s.at(2, '还能'), .3) * aC;
|
| 78 |
+
const ty = P ? st.y + 120 : st.y + 70;
|
| 79 |
+
if (P) {
|
| 80 |
+
glowText(ctx, '摔得那么惨?', st.cx, ty, { size: 74, weight: 900, color: RED, align: 'center', alpha: a1, glowColor: RED });
|
| 81 |
+
glowText(ctx, '还能爬回来?', st.cx, rect.y + rect.h + 110, { size: 74, weight: 900, color: GREEN, align: 'center', alpha: a2, glowColor: GREEN });
|
| 82 |
+
} else {
|
| 83 |
+
glowText(ctx, '摔得那么惨?', st.cx - 330, ty, { size: 78, weight: 900, color: RED, align: 'center', alpha: a1, glowColor: RED });
|
| 84 |
+
glowText(ctx, '还能爬回来?', st.cx + 330, ty, { size: 78, weight: 900, color: GREEN, align: 'center', alpha: a2, glowColor: GREEN });
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
// c3: logo + title
|
| 88 |
+
if (t > c3 - .2) {
|
| 89 |
+
const p = inv(c3, c3 + 1.1, t);
|
| 90 |
+
const ly = st.cy - (P ? 120 : 90), ls = P ? 300 : 260;
|
| 91 |
+
glow(ctx, st.cx, ly, ls * 1.6, PURPLE, .5 * p);
|
| 92 |
+
solLogo(ctx, st.cx, ly, ls, p);
|
| 93 |
+
burst(ctx, st.cx, ly, t - (c3 + .75), { r: 520, n: 60, seed: 3 });
|
| 94 |
+
ring(ctx, st.cx, ly, t - (c3 + .75), { r: 700, dur: 1.1 });
|
| 95 |
+
const a = win(t, c3 + .8, .6);
|
| 96 |
+
const word = scramble('SOLANA', inv(c3 + .8, c3 + 1.6, t), 4);
|
| 97 |
+
gradText(ctx, word, st.cx, ly + ls * .72 + 20, { size: P ? 120 : 132, fam: F_GR, weight: 700, align: 'center', ls: P ? 16 : 26, alpha: a });
|
| 98 |
+
text(ctx, '给 区 块 链 装 上 时 钟 的 人', st.cx, ly + ls * .72 + (P ? 130 : 135), { size: P ? 40 : 42, weight: 700, color: 'rgba(255,255,255,.85)', align: 'center', alpha: win(t, c3 + 1.5, .6), ls: 4 });
|
| 99 |
+
}
|
| 100 |
+
},
|
| 101 |
+
sfx(s) {
|
| 102 |
+
const ev = [], c1 = s.cue(1), c2 = s.cue(2), c3 = s.cue(3);
|
| 103 |
+
for (let k = 1; k * .4 < c1; k++) ev.push({ t: k * .4, k: 'tick', v: k % 5 === 0 ? 1 : .55 });
|
| 104 |
+
ev.push({ t: s.at(0, '每秒'), k: 'pop' }, { t: s.at(0, '四百'), k: 'pop' }, { t: s.at(0, '手续费'), k: 'pop' });
|
| 105 |
+
ev.push({ t: c1 - .3, k: 'whoosh', v: 1 }, { t: c1, k: 'hit', v: .8 });
|
| 106 |
+
ev.push({ t: s.at(2, '摔') , k: 'drop' }, { t: s.at(2, '还能'), k: 'rise' });
|
| 107 |
+
ev.push({ t: c3 - 1.2, k: 'riser', v: 1 }, { t: c3 + .75, k: 'impact', v: 1 }, { t: c3 + .8, k: 'shimmer' });
|
| 108 |
+
return ev;
|
| 109 |
+
},
|
| 110 |
+
};
|
| 111 |
+
|
| 112 |
+
// ---------------------------------------------------------------- 2. NIGHT 2017
|
| 113 |
+
function person(ctx, x, deskY, s, rim = PURPLE) {
|
| 114 |
+
ctx.save(); ctx.translate(x, deskY); ctx.scale(s, s);
|
| 115 |
+
ctx.fillStyle = '#05040c';
|
| 116 |
+
ctx.beginPath(); ctx.moveTo(-150, 0); ctx.bezierCurveTo(-150, -120, -110, -175, -60, -185); ctx.lineTo(60, -185); ctx.bezierCurveTo(110, -175, 150, -120, 150, 0); ctx.fill();
|
| 117 |
+
ctx.fillRect(-28, -215, 56, 40);
|
| 118 |
+
ctx.beginPath(); ctx.ellipse(0, -265, 62, 72, 0, 0, 7); ctx.fill();
|
| 119 |
+
// rim light
|
| 120 |
+
ctx.strokeStyle = hexA(rim, .75); ctx.lineWidth = 4;
|
| 121 |
+
ctx.beginPath(); ctx.ellipse(0, -265, 62, 72, 0, -1.2, .6); ctx.stroke();
|
| 122 |
+
ctx.beginPath(); ctx.moveTo(60, -185); ctx.bezierCurveTo(110, -175, 150, -120, 150, 0); ctx.stroke();
|
| 123 |
+
ctx.restore();
|
| 124 |
+
}
|
| 125 |
+
function cup(ctx, x, y, kind, a) {
|
| 126 |
+
if (a <= 0) return;
|
| 127 |
+
ctx.save(); ctx.translate(x, y); const s = eBack(a); ctx.scale(s, s); ctx.globalAlpha *= clamp(a * 2);
|
| 128 |
+
if (kind === 'beer') {
|
| 129 |
+
const g = ctx.createLinearGradient(0, -110, 0, 0); g.addColorStop(0, '#fff6d8'); g.addColorStop(.18, GOLD); g.addColorStop(1, '#d88a12');
|
| 130 |
+
ctx.fillStyle = g; ctx.beginPath(); ctx.moveTo(-30, -110); ctx.lineTo(30, -110); ctx.lineTo(26, 0); ctx.lineTo(-26, 0); ctx.fill();
|
| 131 |
+
ctx.fillStyle = '#fffdf2'; ctx.beginPath(); ctx.ellipse(0, -112, 33, 12, 0, 0, 7); ctx.fill();
|
| 132 |
+
ctx.strokeStyle = 'rgba(255,255,255,.7)'; ctx.lineWidth = 3; ctx.strokeRect(-30, -110, 60, 110);
|
| 133 |
+
glow(ctx, 0, -55, 80, GOLD, .35);
|
| 134 |
+
} else {
|
| 135 |
+
ctx.fillStyle = '#efe9ff'; rr(ctx, -30, -64, 60, 64, 10); ctx.fill();
|
| 136 |
+
ctx.strokeStyle = '#efe9ff'; ctx.lineWidth = 8; ctx.beginPath(); ctx.arc(34, -34, 15, -1.3, 1.3); ctx.stroke();
|
| 137 |
+
ctx.fillStyle = '#6b3b1d'; ctx.beginPath(); ctx.ellipse(0, -62, 26, 7, 0, 0, 7); ctx.fill();
|
| 138 |
+
ctx.restore(); ctx.save(); ctx.translate(x, y); ctx.globalAlpha *= a * .5; ctx.strokeStyle = '#fff'; ctx.lineWidth = 3;
|
| 139 |
+
for (let i = 0; i < 2; i++) { ctx.beginPath(); ctx.moveTo(-8 + i * 16, -78); ctx.bezierCurveTo(-20 + i * 16, -100, 4 + i * 16, -110, -8 + i * 16, -135); ctx.stroke(); }
|
| 140 |
+
}
|
| 141 |
+
ctx.restore();
|
| 142 |
+
}
|
| 143 |
+
const night = {
|
| 144 |
+
draw(ctx, L, s) {
|
| 145 |
+
const { t, T } = s, st = L.stage, P = L.portrait;
|
| 146 |
+
const a0 = win(t, 0, 1);
|
| 147 |
+
const c1 = s.cue(1), c2 = s.cue(2), c3 = s.cue(3), c4 = s.cue(4);
|
| 148 |
+
// window
|
| 149 |
+
const wx = P ? st.x + 20 : st.cx - 480, wy = P ? st.y - 10 : st.y - 10, ww = P ? st.w - 40 : 960, wh = P ? 400 : 420;
|
| 150 |
+
ctx.save(); ctx.globalAlpha = a0;
|
| 151 |
+
rr(ctx, wx, wy, ww, wh, 18); ctx.save(); ctx.clip();
|
| 152 |
+
const sg = ctx.createLinearGradient(0, wy, 0, wy + wh); sg.addColorStop(0, '#0a1440'); sg.addColorStop(1, '#2a1a5e'); ctx.fillStyle = sg; ctx.fillRect(wx, wy, ww, wh);
|
| 153 |
+
glow(ctx, wx + ww * .78, wy + 90, 120, '#cfd8ff', .6); ctx.fillStyle = '#eef0ff'; ctx.beginPath(); ctx.arc(wx + ww * .78, wy + 90, 34, 0, 7); ctx.fill();
|
| 154 |
+
skyline(ctx, wx - 10, wy + wh, ww + 20, wh * .62, 21, { lit: .35, t: T, color: '#090a1d' });
|
| 155 |
+
ctx.restore();
|
| 156 |
+
ctx.strokeStyle = '#1d1838'; ctx.lineWidth = 16; rr(ctx, wx, wy, ww, wh, 18); ctx.stroke();
|
| 157 |
+
ctx.fillStyle = '#1d1838'; ctx.fillRect(wx + ww / 2 - 6, wy, 12, wh); ctx.fillRect(wx, wy + wh * .45, ww, 10);
|
| 158 |
+
ctx.restore();
|
| 159 |
+
// desk
|
| 160 |
+
const deskY = P ? st.y + st.h - 150 : st.y + st.h - 90;
|
| 161 |
+
ctx.save(); ctx.globalAlpha = a0;
|
| 162 |
+
const dg = ctx.createLinearGradient(0, deskY, 0, deskY + 200); dg.addColorStop(0, '#2b1e3e'); dg.addColorStop(1, '#0a0612');
|
| 163 |
+
ctx.fillStyle = dg; ctx.fillRect(0, deskY, L.W, 260);
|
| 164 |
+
ctx.fillStyle = 'rgba(153,69,255,.35)'; ctx.fillRect(0, deskY, L.W, 3);
|
| 165 |
+
ctx.restore();
|
| 166 |
+
// lamp
|
| 167 |
+
const lx = P ? st.x + st.w - 60 : st.cx + 640;
|
| 168 |
+
ctx.save(); ctx.globalAlpha = a0;
|
| 169 |
+
ctx.strokeStyle = '#3a2f5e'; ctx.lineWidth = 10; ctx.beginPath(); ctx.moveTo(lx, deskY); ctx.lineTo(lx + 20, deskY - 200); ctx.lineTo(lx - 70, deskY - 300); ctx.stroke();
|
| 170 |
+
ctx.fillStyle = '#4a3c78'; ctx.beginPath(); ctx.moveTo(lx - 120, deskY - 270); ctx.lineTo(lx - 30, deskY - 330); ctx.lineTo(lx - 10, deskY - 290); ctx.closePath(); ctx.fill();
|
| 171 |
+
const cg = ctx.createLinearGradient(lx - 70, deskY - 290, lx - 180, deskY); cg.addColorStop(0, 'rgba(255,214,140,.35)'); cg.addColorStop(1, 'rgba(255,214,140,0)');
|
| 172 |
+
ctx.fillStyle = cg; ctx.beginPath(); ctx.moveTo(lx - 110, deskY - 280); ctx.lineTo(lx - 30, deskY - 320); ctx.lineTo(lx + 60, deskY); ctx.lineTo(lx - 330, deskY); ctx.fill();
|
| 173 |
+
ctx.restore();
|
| 174 |
+
glow(ctx, lx - 90, deskY - 280, 120, GOLD, .5 * a0);
|
| 175 |
+
// laptop (screen faces the person and us)
|
| 176 |
+
const px = P ? st.cx - 140 : st.cx - 170, lpx = P ? st.cx + 110 : st.cx + 140;
|
| 177 |
+
const sw = P ? 250 : 300, sh = sw * .64;
|
| 178 |
+
ctx.save(); ctx.globalAlpha = a0;
|
| 179 |
+
rr(ctx, lpx - sw / 2, deskY - sh - 18, sw, sh, 12); ctx.fillStyle = '#121022'; ctx.fill(); ctx.strokeStyle = '#3a2f6e'; ctx.lineWidth = 4; ctx.stroke();
|
| 180 |
+
ctx.save(); rr(ctx, lpx - sw / 2 + 10, deskY - sh - 8, sw - 20, sh - 20, 6); ctx.clip();
|
| 181 |
+
ctx.fillStyle = '#0b1424'; ctx.fillRect(lpx - sw / 2, deskY - sh - 18, sw, sh);
|
| 182 |
+
const r = rng(8);
|
| 183 |
+
for (let i = 0; i < 14; i++) {
|
| 184 |
+
const yy = deskY - sh - 2 + i * 14 - ((t * 22) % 14);
|
| 185 |
+
const len = (.2 + r() * .7) * (sw - 50);
|
| 186 |
+
ctx.fillStyle = [GREEN, PURPLE, TEAL, '#ffffff'][i % 4]; ctx.globalAlpha = .65 * a0;
|
| 187 |
+
ctx.fillRect(lpx - sw / 2 + 22 + (i % 3) * 12, yy, len, 5);
|
| 188 |
+
}
|
| 189 |
+
ctx.restore();
|
| 190 |
+
ctx.fillStyle = '#2a2448'; ctx.fillRect(lpx - sw / 2 - 20, deskY - 18, sw + 40, 18);
|
| 191 |
+
ctx.restore();
|
| 192 |
+
glow(ctx, lpx, deskY - sh / 2, sw, TEAL, .35 * a0);
|
| 193 |
+
person(ctx, px, deskY + 20, P ? 1.05 : 1.15);
|
| 194 |
+
// wall clock
|
| 195 |
+
const ckx = P ? st.x + 80 : st.cx - 650, cky = P ? st.y + 470 : st.y + 120;
|
| 196 |
+
dial(ctx, ckx, cky, 62, { alpha: a0 * .9, hand: Math.floor(T) / 60, minute: .62, ticks: 12, lw: 3, accent: RED });
|
| 197 |
+
text(ctx, '2017.11 深夜', ckx, cky + 96, { size: 24, fam: F_SC, weight: 700, color: 'rgba(255,255,255,.65)', align: 'center', alpha: a0 });
|
| 198 |
+
// c1: name card
|
| 199 |
+
const ac = win(t, c1, .5) * (1 - win(t, c4 - .3, .4));
|
| 200 |
+
if (ac > 0) {
|
| 201 |
+
const cw = P ? 520 : 560, ch = 200, cx = P ? st.cx : st.x + 300, cy = P ? st.y + 180 : st.cy + 40;
|
| 202 |
+
ctx.save(); ctx.globalAlpha = ac; ctx.translate((1 - ac) * -60, 0);
|
| 203 |
+
glass(ctx, cx - cw / 2, cy - ch / 2, cw, ch, {});
|
| 204 |
+
text(ctx, 'Anatoly Yakovenko', cx, cy - 50, { size: 46, fam: F_GR, weight: 700, color: WHITE, align: 'center', maxW: cw - 40 });
|
| 205 |
+
text(ctx, '阿纳托利 · 亚科文科', cx, cy + 8, { size: 32, weight: 700, color: GREEN, align: 'center' });
|
| 206 |
+
text(ctx, 'Solana 创始人 · 前高通工程师', cx, cy + 58, { size: 26, weight: 500, color: 'rgba(255,255,255,.7)', align: 'center' });
|
| 207 |
+
ctx.restore();
|
| 208 |
+
}
|
| 209 |
+
// c2: skill tags rising from the laptop
|
| 210 |
+
const tags = [['压缩算法', -1], ['分布式系统', 0], ['高通 · 十几年', 1]];
|
| 211 |
+
tags.forEach(([tg, k], i) => {
|
| 212 |
+
const a = win(t, c2 + i * .45, .5) * (1 - win(t, c3, .5));
|
| 213 |
+
if (a <= 0) return;
|
| 214 |
+
const x = lpx + k * (P ? 150 : 200), y = deskY - sh - 80 - i * 70 - eOut(a) * 40;
|
| 215 |
+
pill(ctx, tg, x, y, { size: P ? 26 : 28, color: [GREEN, TEAL, PURPLE][i], fg: i === 2 ? '#fff' : '#0b0820', alpha: a });
|
| 216 |
+
});
|
| 217 |
+
// c3: two coffees + one beer
|
| 218 |
+
const cupsX = P ? [lpx + 150, lpx + 210, lpx + 280] : [lpx + 230, lpx + 310, lpx + 400];
|
| 219 |
+
const tc = [s.at(3, '两杯'), s.at(3, '两杯') + .35, s.at(3, '啤酒')];
|
| 220 |
+
['coffee', 'coffee', 'beer'].forEach((k, i) => cup(ctx, cupsX[i], deskY + 2, k, win(t, tc[i], .45, x => x)));
|
| 221 |
+
// c4: the idea — a clock for the blockchain
|
| 222 |
+
const ai = win(t, c4 - .1, .5);
|
| 223 |
+
if (ai > 0) {
|
| 224 |
+
ctx.save(); ctx.fillStyle = `rgba(4,2,12,${.55 * ai})`; ctx.fillRect(0, 0, L.W, L.H); ctx.restore();
|
| 225 |
+
const ix = st.cx, iy = P ? st.cy - 120 : st.cy - 60, ir = P ? 190 : 170;
|
| 226 |
+
glow(ctx, ix, iy, ir * 2.4, GREEN, .5 * ai);
|
| 227 |
+
dial(ctx, ix, iy, ir * eBack(ai), { alpha: ai, hand: t * .8, minute: t * .05, lw: 6, accent: GREEN });
|
| 228 |
+
burst(ctx, ix, iy, t - c4, { r: 480, n: 50, seed: 9, color: GOLD });
|
| 229 |
+
ring(ctx, ix, iy, t - c4 - .1, { r: 600, color: GOLD });
|
| 230 |
+
gradText(ctx, '给区块链装上时钟?', ix, iy + ir + (P ? 110 : 90), { size: P ? 78 : 88, align: 'center', alpha: win(t, c4 + .3, .5), c1: GOLD, c2: GREEN });
|
| 231 |
+
}
|
| 232 |
+
},
|
| 233 |
+
sfx(s) {
|
| 234 |
+
return [
|
| 235 |
+
{ t: s.cue(1), k: 'pop', v: .7 },
|
| 236 |
+
{ t: s.cue(2), k: 'pop', v: .5 }, { t: s.cue(2) + .45, k: 'pop', v: .5 }, { t: s.cue(2) + .9, k: 'pop', v: .5 },
|
| 237 |
+
{ t: s.at(3, '两杯'), k: 'clink' }, { t: s.at(3, '两杯') + .35, k: 'clink' }, { t: s.at(3, '啤酒'), k: 'clink', v: 1.1 },
|
| 238 |
+
{ t: s.cue(4) - .1, k: 'idea', v: 1 }, { t: s.cue(4), k: 'shimmer' },
|
| 239 |
+
];
|
| 240 |
+
},
|
| 241 |
+
};
|
| 242 |
+
|
| 243 |
+
// ---------------------------------------------------------------- 3. PROOF OF HISTORY
|
| 244 |
+
const pohCache = {};
|
| 245 |
+
const poh = {
|
| 246 |
+
draw(ctx, L, s) {
|
| 247 |
+
const { t } = s, st = L.stage, P = L.portrait;
|
| 248 |
+
const c1 = s.cue(1), c2 = s.cue(2), c3 = s.cue(3), c4 = s.cue(4), c5 = s.cue(5), c6 = s.cue(6);
|
| 249 |
+
const N = 8, rx = P ? st.w * .4 : st.w * .4, ry = P ? st.h * .4 : st.h * .4;
|
| 250 |
+
const nodes = Array.from({ length: N }, (_, i) => { const a = i / N * Math.PI * 2 - Math.PI / 2; return { x: st.cx + Math.cos(a) * rx, y: st.cy + Math.sin(a) * ry, sp: .3 + hash1(i) * 1.6, off: hash1(i + 9) }; });
|
| 251 |
+
const dim = 1 - .75 * win(t, c3 - .2, .6) * (1 - win(t, c6, .6));
|
| 252 |
+
const sync = win(t, c6, 1.2, eIO);
|
| 253 |
+
// c2: chaotic messaging between every pair
|
| 254 |
+
const aMsg = win(t, c2, .5) * (1 - win(t, c3 - .2, .5));
|
| 255 |
+
if (aMsg > 0) {
|
| 256 |
+
ctx.save();
|
| 257 |
+
for (let i = 0; i < N; i++) for (let j = i + 1; j < N; j++) {
|
| 258 |
+
const A = nodes[i], B = nodes[j];
|
| 259 |
+
ctx.strokeStyle = `rgba(255,90,120,${.18 * aMsg})`; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(A.x, A.y); ctx.lineTo(B.x, B.y); ctx.stroke();
|
| 260 |
+
for (let k = 0; k < 2; k++) {
|
| 261 |
+
const p = ((t - c2) * (.5 + hash1(i * 9 + j + k * 50)) + hash1(i + j * 3 + k)) % 1;
|
| 262 |
+
const fw = (i + j + k) % 2 ? p : 1 - p;
|
| 263 |
+
const x = lerp(A.x, B.x, fw), y = lerp(A.y, B.y, fw);
|
| 264 |
+
ctx.fillStyle = `rgba(255,120,150,${aMsg})`; ctx.beginPath(); ctx.arc(x, y, 5, 0, 7); ctx.fill();
|
| 265 |
+
}
|
| 266 |
+
}
|
| 267 |
+
ctx.restore();
|
| 268 |
+
const n = Math.floor(Math.pow(Math.max(0, t - c2), 2) * 180);
|
| 269 |
+
text(ctx, '确认中…', st.cx, st.cy - 30, { size: P ? 60 : 64, weight: 900, color: '#ff9fb2', align: 'center', alpha: aMsg });
|
| 270 |
+
text(ctx, `消息往返 × ${n.toLocaleString()}`, st.cx, st.cy + 40, { size: 30, fam: F_MONO, weight: 600, color: 'rgba(255,255,255,.75)', align: 'center', alpha: aMsg });
|
| 271 |
+
}
|
| 272 |
+
// c6: synced links from the PoH core to every node
|
| 273 |
+
const core = { x: st.cx, y: P ? st.cy - 150 : st.cy - 110 };
|
| 274 |
+
if (sync > 0) {
|
| 275 |
+
ctx.save();
|
| 276 |
+
for (const n of nodes) {
|
| 277 |
+
ctx.strokeStyle = hexA(GREEN, .35 * sync); ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(core.x, core.y); ctx.lineTo(n.x, n.y); ctx.stroke();
|
| 278 |
+
const p = ((t - c6) * 1.4) % 1; ctx.fillStyle = hexA(GREEN, sync); ctx.beginPath(); ctx.arc(lerp(core.x, n.x, p), lerp(core.y, n.y, p), 6, 0, 7); ctx.fill();
|
| 279 |
+
}
|
| 280 |
+
ctx.restore();
|
| 281 |
+
}
|
| 282 |
+
// nodes with their own clocks
|
| 283 |
+
nodes.forEach((n, i) => {
|
| 284 |
+
const a = win(t, .2 + i * .12, .5) * dim;
|
| 285 |
+
if (a <= 0) return;
|
| 286 |
+
const own = n.off + t * n.sp * .3, shared = t * .25;
|
| 287 |
+
const hand = lerp(own, shared, sync);
|
| 288 |
+
monitor(ctx, n.x, n.y + 30, P ? 92 : 100, { color: sync > .5 ? GREEN : (aMsg > .3 ? RED : TEAL), alpha: a, on: .7 });
|
| 289 |
+
dial(ctx, n.x, n.y - 46, 30, { alpha: a, hand, ticks: 12, lw: 2, accent: sync > .5 ? GREEN : GOLD });
|
| 290 |
+
const aq = win(t, c1 + i * .08, .3, eBack) * (1 - win(t, c3 - .2, .4));
|
| 291 |
+
if (aq > 0) text(ctx, '?', n.x + 42, n.y - 72, { size: 56, weight: 900, fam: F_GR, color: GOLD, align: 'center', alpha: clamp(aq) });
|
| 292 |
+
});
|
| 293 |
+
// c3+: SHA-256 core and the chain of hashes
|
| 294 |
+
const aCore = win(t, c3, .7) ;
|
| 295 |
+
if (aCore > 0) {
|
| 296 |
+
const R = P ? 120 : 110;
|
| 297 |
+
glow(ctx, core.x, core.y, R * 3, GREEN, .45 * aCore);
|
| 298 |
+
ctx.save(); ctx.translate(core.x, core.y); ctx.rotate(t * .6); ctx.globalAlpha = aCore;
|
| 299 |
+
for (let k = 0; k < 2; k++) {
|
| 300 |
+
ctx.beginPath(); for (let i = 0; i < 6; i++) { const a = i / 6 * Math.PI * 2; ctx[i ? 'lineTo' : 'moveTo'](Math.cos(a) * R * (1 - k * .22), Math.sin(a) * R * (1 - k * .22)); } ctx.closePath();
|
| 301 |
+
ctx.strokeStyle = k ? PURPLE : GREEN; ctx.lineWidth = k ? 3 : 6; ctx.stroke();
|
| 302 |
+
}
|
| 303 |
+
ctx.restore();
|
| 304 |
+
ctx.save(); ctx.globalAlpha = aCore; ctx.fillStyle = 'rgba(6,20,16,.85)'; ctx.beginPath(); ctx.arc(core.x, core.y, R * .66, 0, 7); ctx.fill(); ctx.restore();
|
| 305 |
+
text(ctx, 'SHA-256', core.x, core.y, { size: P ? 36 : 34, fam: F_MONO, weight: 800, color: GREEN, align: 'center', alpha: aCore });
|
| 306 |
+
text(ctx, '历史证明 · Proof of History', core.x, core.y - R - 46, { size: P ? 34 : 34, weight: 800, color: WHITE, align: 'center', alpha: win(t, c3 + .4, .5) * (1 - win(t, c6, .5)) });
|
| 307 |
+
}
|
| 308 |
+
// chain
|
| 309 |
+
const aCh = win(t, c4 - .3, .6);
|
| 310 |
+
if (aCh > 0) {
|
| 311 |
+
const per = .55, gap = P ? 196 : 214, bw = P ? 170 : 186, bh = 70;
|
| 312 |
+
const cyR = P ? st.cy + 140 : st.cy + 150;
|
| 313 |
+
const born = Math.floor((t - (c4 - .3)) / per);
|
| 314 |
+
const speed = gap / per;
|
| 315 |
+
ctx.save(); ctx.globalAlpha = aCh;
|
| 316 |
+
// feeding line from core
|
| 317 |
+
ctx.strokeStyle = hexA(GREEN, .5); ctx.lineWidth = 3; ctx.setLineDash([8, 8]); ctx.beginPath(); ctx.moveTo(core.x, core.y + 110); ctx.lineTo(core.x, cyR - bh / 2); ctx.stroke(); ctx.setLineDash([]);
|
| 318 |
+
for (let b = Math.max(0, born - 12); b <= born; b++) {
|
| 319 |
+
const tb = (c4 - .3) + b * per;
|
| 320 |
+
const x = core.x - (t - tb) * speed;
|
| 321 |
+
if (x < -bw) continue;
|
| 322 |
+
const pop = eBack(inv(tb, tb + .25, t));
|
| 323 |
+
const isTx = t > c5 && txAt(b, c5, c4 - .3, per);
|
| 324 |
+
ctx.save(); ctx.translate(x, cyR); ctx.scale(pop, pop);
|
| 325 |
+
rr(ctx, -bw / 2, -bh / 2, bw, bh, 14); ctx.fillStyle = isTx ? 'rgba(60,44,8,.9)' : 'rgba(8,30,24,.9)'; ctx.fill();
|
| 326 |
+
ctx.strokeStyle = isTx ? GOLD : GREEN; ctx.lineWidth = 3; ctx.stroke();
|
| 327 |
+
text(ctx, hexStr(b + 17, 8), 0, -6, { size: 25, fam: F_MONO, weight: 700, color: isTx ? GOLD : GREEN, align: 'center' });
|
| 328 |
+
text(ctx, '#' + (1048576 + b).toLocaleString(), 0, 22, { size: 16, fam: F_MONO, weight: 500, color: 'rgba(255,255,255,.55)', align: 'center' });
|
| 329 |
+
if (isTx) { pill(ctx, 'TX ⏱', 0, -bh / 2 - 22, { size: 18, color: GOLD, fam: F_MONO }); }
|
| 330 |
+
ctx.restore();
|
| 331 |
+
if (b > 0) arrow(ctx, x - bw / 2 - 2, cyR, x - gap + bw / 2 + 4, cyR, hexA(GREEN, .7), 3, 12, clamp(pop));
|
| 332 |
+
}
|
| 333 |
+
ctx.restore();
|
| 334 |
+
const af = win(t, c4, .5) * (1 - win(t, c6 - .2, .4));
|
| 335 |
+
text(ctx, 'hash(n) = SHA256( hash(n−1) )', st.cx, cyR + (P ? 110 : 100), { size: P ? 30 : 34, fam: F_MONO, weight: 700, color: 'rgba(255,255,255,.85)', align: 'center', alpha: af });
|
| 336 |
+
// c5: falling transactions
|
| 337 |
+
if (t > c5 - .8) {
|
| 338 |
+
for (let k = 0; k < 10; k++) {
|
| 339 |
+
const ts = c5 + k * .9; if (t < ts - .7 || t > ts + .1) continue;
|
| 340 |
+
const p = eIn(inv(ts - .7, ts, t));
|
| 341 |
+
const x = core.x, y = lerp(st.y - 40, cyR - bh / 2 - 30, p);
|
| 342 |
+
ctx.save(); ctx.globalAlpha = 1 - inv(ts, ts + .1, t);
|
| 343 |
+
rr(ctx, x - 40, y - 26, 80, 52, 6); ctx.fillStyle = '#fff3d0'; ctx.fill();
|
| 344 |
+
ctx.strokeStyle = '#b57f12'; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(x - 40, y - 26); ctx.lineTo(x, y + 4); ctx.lineTo(x + 40, y - 26); ctx.stroke();
|
| 345 |
+
ctx.restore();
|
| 346 |
+
}
|
| 347 |
+
}
|
| 348 |
+
}
|
| 349 |
+
const ast = win(t, s.at(5, '时间戳'), .5) * (1 - win(t, c6 - .2, .4));
|
| 350 |
+
if (ast > 0) gradText(ctx, '自带时间戳', st.cx, P ? st.y + st.h - 10 : st.y + st.h + 20, { size: 56, align: 'center', alpha: ast, c1: GOLD, c2: GREEN });
|
| 351 |
+
// c6: verdict
|
| 352 |
+
const av = win(t, c6 + .2, .5);
|
| 353 |
+
if (av > 0) {
|
| 354 |
+
speedLines(ctx, L, t, av * .7, 30);
|
| 355 |
+
gradText(ctx, '不用对表 · 各干各的', st.cx, P ? st.y + st.h - 10 : st.y + st.h + 20, { size: P ? 64 : 70, align: 'center', alpha: av });
|
| 356 |
+
}
|
| 357 |
+
void pohCache;
|
| 358 |
+
},
|
| 359 |
+
sfx(s) {
|
| 360 |
+
const ev = [];
|
| 361 |
+
for (let i = 0; i < 8; i++) ev.push({ t: .2 + i * .12, k: 'blip', v: .35, p: i });
|
| 362 |
+
ev.push({ t: s.cue(1), k: 'question' });
|
| 363 |
+
ev.push({ t: s.cue(2), k: 'chatter', d: s.cue(3) - s.cue(2) });
|
| 364 |
+
ev.push({ t: s.cue(3), k: 'power', v: 1 });
|
| 365 |
+
for (let b = 0; b * .55 < s.cue(6) + 2 - s.cue(4); b++) ev.push({ t: s.cue(4) - .3 + b * .55, k: 'hashtick', v: .45, p: b });
|
| 366 |
+
for (let k = 0; k < 6; k++) ev.push({ t: s.cue(5) + k * .9, k: 'stamp', v: .5 });
|
| 367 |
+
ev.push({ t: s.cue(6), k: 'whoosh' }, { t: s.cue(6) + .2, k: 'shimmer' });
|
| 368 |
+
return ev;
|
| 369 |
+
},
|
| 370 |
+
};
|
| 371 |
+
function txAt(b, c5, start, per) {
|
| 372 |
+
// bead b is "born" at start + b*per; a TX lands on it if a drop time coincides
|
| 373 |
+
const tb = start + b * per;
|
| 374 |
+
for (let k = 0; k < 10; k++) { const ts = c5 + k * .9; if (ts >= tb && ts < tb + per) return true; }
|
| 375 |
+
return false;
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
// ---------------------------------------------------------------- 4. NAME 2018
|
| 379 |
+
const nameScene = {
|
| 380 |
+
draw(ctx, L, s) {
|
| 381 |
+
const { t, T } = s, st = L.stage, P = L.portrait, { W, H } = L;
|
| 382 |
+
const c1 = s.cue(1), c2 = s.cue(2), c3 = s.cue(3);
|
| 383 |
+
const a0 = win(t, 0, 1.2);
|
| 384 |
+
const hy = H * (P ? .6 : .64);
|
| 385 |
+
// retro sunset
|
| 386 |
+
ctx.save(); ctx.globalAlpha = a0;
|
| 387 |
+
const sky = ctx.createLinearGradient(0, 0, 0, hy); sky.addColorStop(0, '#120a35'); sky.addColorStop(.55, '#6b1f6e'); sky.addColorStop(1, '#ff7a59');
|
| 388 |
+
ctx.fillStyle = sky; ctx.fillRect(0, 0, W, hy);
|
| 389 |
+
const sr = P ? 260 : 250, sx = W / 2 - (P ? 46 : 0), sy = hy - sr * .25 + Math.sin(T * .2) * 4;
|
| 390 |
+
glow(ctx, sx, sy, sr * 2.6, '#ff8a5c', .55);
|
| 391 |
+
ctx.save(); ctx.beginPath(); ctx.arc(sx, sy, sr, 0, 7); ctx.clip();
|
| 392 |
+
const sg = ctx.createLinearGradient(0, sy - sr, 0, sy + sr); sg.addColorStop(0, '#ffe36b'); sg.addColorStop(.6, '#ff5e8a'); sg.addColorStop(1, '#b02cff');
|
| 393 |
+
ctx.fillStyle = sg; ctx.fillRect(sx - sr, sy - sr, sr * 2, sr * 2);
|
| 394 |
+
ctx.fillStyle = '#6b1f6e';
|
| 395 |
+
for (let i = 0; i < 7; i++) { const yy = sy + i * sr * .14 - ((T * 12) % (sr * .14)); const hh = 3 + i * 2.2; ctx.fillRect(sx - sr, yy, sr * 2, hh); }
|
| 396 |
+
ctx.restore();
|
| 397 |
+
const sea = ctx.createLinearGradient(0, hy, 0, H); sea.addColorStop(0, '#3a1050'); sea.addColorStop(1, '#07030f');
|
| 398 |
+
ctx.fillStyle = sea; ctx.fillRect(0, hy, W, H - hy);
|
| 399 |
+
for (let i = 0; i < 22; i++) {
|
| 400 |
+
const yy = hy + Math.pow(i / 22, 1.6) * (H - hy), w2 = (40 + i * 26) * (.6 + .4 * Math.sin(T * 1.5 + i));
|
| 401 |
+
ctx.fillStyle = `rgba(255,170,120,${.5 - i * .02})`; ctx.fillRect(sx - w2 / 2 + Math.sin(T + i) * 12, yy, w2, 3 + i * .3);
|
| 402 |
+
}
|
| 403 |
+
// waves
|
| 404 |
+
ctx.strokeStyle = 'rgba(255,255,255,.12)'; ctx.lineWidth = 2;
|
| 405 |
+
for (let k = 0; k < 5; k++) { ctx.beginPath(); for (let x = 0; x <= W; x += 20) { const y = hy + 40 + k * 70 + Math.sin(x * .01 + T * (1 + k * .2) + k) * (6 + k * 3); x ? ctx.lineTo(x, y) : ctx.moveTo(x, y); } ctx.stroke(); }
|
| 406 |
+
ctx.restore();
|
| 407 |
+
// palms
|
| 408 |
+
palm(ctx, P ? 40 : 140, hy + 40, P ? .9 : 1.1, T, a0);
|
| 409 |
+
palm(ctx, P ? W - 210 : W - 160, hy + 60, P ? .75 : 1, T + 2, a0, true);
|
| 410 |
+
// c0: LOOM → name clash
|
| 411 |
+
const ty = P ? st.y + 120 : st.y + 110;
|
| 412 |
+
const aL = win(t, .3, .5) * (1 - win(t, c1 + .1, .4));
|
| 413 |
+
if (aL > 0) {
|
| 414 |
+
const sh = t > s.at(0, '撞') && t < s.at(0, '撞') + .4 ? shake(t, 10) : [0, 0];
|
| 415 |
+
text(ctx, 'LOOM', st.cx + sh[0], ty + sh[1], { size: P ? 150 : 170, fam: F_GR, weight: 700, color: WHITE, align: 'center', alpha: aL, ls: 20, shadow: 30, shadowColor: PURPLE });
|
| 416 |
+
const ag = win(t, s.at(0, '撞'), .4) * aL;
|
| 417 |
+
text(ctx, 'LOOM NETWORK', st.cx + (1 - ag) * 400 + 30, ty + 18, { size: P ? 64 : 80, fam: F_GR, weight: 700, color: 'rgba(255,90,120,.55)', align: 'center', alpha: ag, ls: 6 });
|
| 418 |
+
stamp(ctx, '撞名!', st.cx + (P ? 170 : 330), ty - 70, t, s.at(0, '撞') + .2, { size: P ? 64 : 70, color: RED });
|
| 419 |
+
}
|
| 420 |
+
// c1: becomes SOLANA
|
| 421 |
+
const aS = win(t, c1 + .1, .3);
|
| 422 |
+
if (aS > 0) {
|
| 423 |
+
const word = scramble('SOLANA', inv(c1 + .1, c1 + 1.1, t), 7);
|
| 424 |
+
glow(ctx, st.cx, ty, 400, PURPLE, .4 * aS);
|
| 425 |
+
gradText(ctx, word, st.cx, ty, { size: P ? 140 : 170, fam: F_GR, weight: 700, align: 'center', ls: P ? 14 : 22, alpha: aS, c1: '#ffe36b', c2: GREEN });
|
| 426 |
+
pill(ctx, '2018 · 正式更名', st.cx, ty + (P ? 110 : 120), { size: 28, alpha: win(t, c1 + 1, .4) * (1 - win(t, c2, .4)), color: GOLD });
|
| 427 |
+
}
|
| 428 |
+
// c2: map card
|
| 429 |
+
const am = win(t, c2, .5) * (1 - win(t, c3 + 1.5, .5));
|
| 430 |
+
if (am > 0) {
|
| 431 |
+
const cw = P ? 560 : 520, ch = P ? 420 : 380, cx = P ? st.cx : st.x + 330, cy = P ? st.cy + 210 : st.cy + 110;
|
| 432 |
+
ctx.save(); ctx.globalAlpha = am; ctx.translate(0, (1 - am) * 60);
|
| 433 |
+
glass(ctx, cx - cw / 2, cy - ch / 2, cw, ch, { fill: 'rgba(12,10,30,.82)' });
|
| 434 |
+
ctx.save(); rr(ctx, cx - cw / 2 + 14, cy - ch / 2 + 14, cw - 28, ch - 90, 14); ctx.clip();
|
| 435 |
+
ctx.fillStyle = '#123b66'; ctx.fillRect(cx - cw / 2, cy - ch / 2, cw, ch);
|
| 436 |
+
ctx.fillStyle = '#2c2440'; ctx.beginPath(); ctx.moveTo(cx + cw / 2, cy - ch / 2);
|
| 437 |
+
for (let i = 0; i <= 20; i++) { const yy = cy - ch / 2 + i / 20 * (ch - 76); ctx.lineTo(cx - 40 + Math.sin(i * .7) * 26 + i * 6, yy); }
|
| 438 |
+
ctx.lineTo(cx + cw / 2, cy + ch / 2); ctx.fill();
|
| 439 |
+
ctx.restore();
|
| 440 |
+
const pinT = s.at(2, 'Solana Beach');
|
| 441 |
+
const pins = [['Solana Beach', -.28, pinT, GREEN], ['San Diego 圣地亚哥', .22, c2 + .3, '#ffffff']];
|
| 442 |
+
pins.forEach(([nm, fy, pt, col], i) => {
|
| 443 |
+
const pa = win(t, pt, .5, eBack); if (pa <= 0) return;
|
| 444 |
+
const x = cx - 10 + (i ? 60 : 0) + Math.sin(fy * 8) * 10, y = cy + fy * (ch - 90) - 40 - (1 - pa) * 80;
|
| 445 |
+
ctx.fillStyle = col; ctx.beginPath(); ctx.arc(x, y - 30, 16, Math.PI, 0); ctx.lineTo(x, y); ctx.closePath(); ctx.fill();
|
| 446 |
+
if (i === 0) glow(ctx, x, y - 30, 70, GREEN, .7 * pa);
|
| 447 |
+
text(ctx, nm, x + 28, y - 30, { size: i ? 24 : 32, fam: F_GR, weight: 700, color: col, alpha: pa, stroke: 5, strokeColor: 'rgba(0,0,0,.6)' });
|
| 448 |
+
});
|
| 449 |
+
text(ctx, '加州 · 圣地亚哥以北的海滨小镇', cx, cy + ch / 2 - 38, { size: 28, weight: 700, color: 'rgba(255,255,255,.85)', align: 'center' });
|
| 450 |
+
ctx.restore();
|
| 451 |
+
}
|
| 452 |
+
// c3: surfer + code
|
| 453 |
+
const aSf = win(t, c3, .5);
|
| 454 |
+
if (aSf > 0) {
|
| 455 |
+
const p = inv(c3, s.d, t);
|
| 456 |
+
const x = lerp(P ? -100 : st.x + 500, P ? W * .75 : W - 200, p), y = hy + (P ? 120 : 90) + Math.sin(T * 2.4) * 10;
|
| 457 |
+
ctx.save(); ctx.globalAlpha = aSf;
|
| 458 |
+
ctx.fillStyle = 'rgba(255,255,255,.75)'; ctx.beginPath(); ctx.moveTo(x - 160, y + 30); ctx.quadraticCurveTo(x - 30, y - 70, x + 70, y + 30); ctx.fill();
|
| 459 |
+
surfer(ctx, x, y + 6);
|
| 460 |
+
ctx.restore();
|
| 461 |
+
const codes = ['</>', '{ }', 'fn()', '0x'];
|
| 462 |
+
codes.forEach((c, i) => {
|
| 463 |
+
const a = win(t, c3 + .4 + i * .3, .5) * aSf; const fy = ((t - c3) * 30 + i * 40) % 120;
|
| 464 |
+
text(ctx, c, x - 80 + i * 70, y - 140 - fy, { size: 34, fam: F_MONO, weight: 700, color: [GREEN, TEAL, GOLD, PURPLE][i], align: 'center', alpha: a * (1 - fy / 120) });
|
| 465 |
+
});
|
| 466 |
+
}
|
| 467 |
+
},
|
| 468 |
+
sfx(s) {
|
| 469 |
+
return [
|
| 470 |
+
{ t: .3, k: 'waves', d: s.d - .3 }, { t: .35, k: 'hit', v: .6 },
|
| 471 |
+
{ t: s.at(0, '撞') + .2, k: 'stamp', v: 1 },
|
| 472 |
+
{ t: s.cue(1) + .1, k: 'scramble', d: 1 }, { t: s.cue(1) + 1.1, k: 'shimmer' },
|
| 473 |
+
{ t: s.at(2, 'Solana Beach'), k: 'pop' }, { t: s.cue(2) + .3, k: 'pop', v: .5 },
|
| 474 |
+
{ t: s.cue(3), k: 'whoosh', v: .6 },
|
| 475 |
+
];
|
| 476 |
+
},
|
| 477 |
+
};
|
| 478 |
+
function palm(ctx, x, y, s, T, a, flip) {
|
| 479 |
+
ctx.save(); ctx.globalAlpha = a; ctx.translate(x, y); ctx.scale(flip ? -s : s, s); ctx.fillStyle = '#12051c'; ctx.strokeStyle = '#12051c';
|
| 480 |
+
ctx.lineWidth = 16; ctx.beginPath(); ctx.moveTo(0, 0); ctx.quadraticCurveTo(30, -180, 90, -330); ctx.stroke();
|
| 481 |
+
for (let i = 0; i < 6; i++) {
|
| 482 |
+
const a0 = -Math.PI / 2 + (i - 2.5) * .55 + Math.sin(T * 1.2 + i) * .04;
|
| 483 |
+
ctx.save(); ctx.translate(90, -330); ctx.rotate(a0 + Math.PI / 2);
|
| 484 |
+
ctx.beginPath(); ctx.moveTo(0, 0); ctx.quadraticCurveTo(60, -30, 150, 30); ctx.quadraticCurveTo(70, 0, 0, 10); ctx.fill();
|
| 485 |
+
ctx.restore();
|
| 486 |
+
}
|
| 487 |
+
ctx.restore();
|
| 488 |
+
}
|
| 489 |
+
function surfer(ctx, x, y) {
|
| 490 |
+
ctx.save(); ctx.translate(x, y); ctx.fillStyle = '#1a0a24';
|
| 491 |
+
ctx.beginPath(); ctx.ellipse(0, 0, 70, 9, -.08, 0, 7); ctx.fill();
|
| 492 |
+
ctx.lineWidth = 9; ctx.strokeStyle = '#1a0a24'; ctx.lineCap = 'round';
|
| 493 |
+
ctx.beginPath(); ctx.moveTo(-22, -4); ctx.lineTo(-8, -40); ctx.lineTo(16, -4); ctx.stroke();
|
| 494 |
+
ctx.beginPath(); ctx.moveTo(-8, -40); ctx.lineTo(0, -80); ctx.stroke();
|
| 495 |
+
ctx.beginPath(); ctx.moveTo(-34, -70); ctx.lineTo(0, -70); ctx.lineTo(36, -62); ctx.stroke();
|
| 496 |
+
ctx.beginPath(); ctx.arc(2, -96, 13, 0, 7); ctx.fill();
|
| 497 |
+
ctx.restore();
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
// ---------------------------------------------------------------- 5. BUILD 2018-19
|
| 501 |
+
const TEAM = [
|
| 502 |
+
{ ini: 'AY', name: 'Anatoly Yakovenko', role: 'CEO · 创始人', sub: '前高通', c1: PURPLE, c2: GREEN, k: [0, null] },
|
| 503 |
+
{ ini: 'GF', name: 'Greg Fitzgerald', role: '联合创始人 · CTO', sub: '前高通', c1: '#00D1FF', c2: PURPLE, k: [0, 'Greg'] },
|
| 504 |
+
{ ini: 'SA', name: 'Stephen Akridge', role: '联合创始人', sub: '前高通', c1: GOLD, c2: PINK, k: [0, 'Stephen'] },
|
| 505 |
+
{ ini: 'RG', name: 'Raj Gokal', role: '联合创始人 · COO', sub: '产品与增长', c1: GREEN, c2: '#00D1FF', k: [1, 'Raj'] },
|
| 506 |
+
];
|
| 507 |
+
const build = {
|
| 508 |
+
draw(ctx, L, s) {
|
| 509 |
+
const { t } = s, st = L.stage, P = L.portrait;
|
| 510 |
+
const c2 = s.cue(2), c3 = s.cue(3), c4 = s.cue(4);
|
| 511 |
+
const out = win(t, c2 - .2, .6, eIn);
|
| 512 |
+
TEAM.forEach((m, i) => {
|
| 513 |
+
const at = m.k[1] ? s.at(m.k[0], m.k[1]) : .3;
|
| 514 |
+
const a = win(t, at, .55) * (1 - out);
|
| 515 |
+
let x, y, w;
|
| 516 |
+
if (P) { w = 360; x = st.cx + (i % 2 ? 195 : -195); y = st.cy + (i < 2 ? -225 : 225); }
|
| 517 |
+
else { w = 360; x = st.cx + (i - 1.5) * 400; y = st.cy - 10; }
|
| 518 |
+
personCard(ctx, x, y - out * 200, w, { ...m, a });
|
| 519 |
+
});
|
| 520 |
+
// testnet network
|
| 521 |
+
const rect = P ? { x: st.x + 30, y: st.y + 160, w: st.w - 60, h: st.h - 260 } : { x: st.x + 40, y: st.y + 60, w: st.w - 80, h: st.h - 120 };
|
| 522 |
+
const key = L.orient;
|
| 523 |
+
build._n = build._n || {};
|
| 524 |
+
const nodes = build._n[key] || (build._n[key] = nodesIn(11, P ? 34 : 46, rect, P ? 95 : 120));
|
| 525 |
+
build._l = build._l || {};
|
| 526 |
+
const links = build._l[key] || (build._l[key] = linksFor(nodes, 3));
|
| 527 |
+
const aN = win(t, c2 - .1, .5) * (1 - .6 * win(t, c3, .5)) * (1 - win(t, c4, .5));
|
| 528 |
+
if (aN > 0) {
|
| 529 |
+
ctx.save();
|
| 530 |
+
links.forEach(([i, j], k) => {
|
| 531 |
+
const ti = Math.max(onT(i, c2), onT(j, c2)); const a = win(t, ti, .3) * aN; if (a <= 0) return;
|
| 532 |
+
const A = nodes[i], B = nodes[j];
|
| 533 |
+
ctx.strokeStyle = hexA(TEAL, .35 * a); ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(A.x, A.y); ctx.lineTo(B.x, B.y); ctx.stroke();
|
| 534 |
+
const p = ((t * .8) + hash1(k)) % 1; ctx.fillStyle = hexA(GREEN, a); ctx.beginPath(); ctx.arc(lerp(A.x, B.x, p), lerp(A.y, B.y, p), 3.5, 0, 7); ctx.fill();
|
| 535 |
+
});
|
| 536 |
+
nodes.forEach((n, i) => {
|
| 537 |
+
const a = win(t, onT(i, c2), .3, eBack) * aN; if (a <= 0) return;
|
| 538 |
+
glow(ctx, n.x, n.y, 40, GREEN, .5 * clamp(a));
|
| 539 |
+
ctx.fillStyle = WHITE; ctx.globalAlpha = clamp(a); ctx.beginPath(); ctx.arc(n.x, n.y, 6 + n.s * 4, 0, 7); ctx.fill(); ctx.globalAlpha = 1;
|
| 540 |
+
});
|
| 541 |
+
ctx.restore();
|
| 542 |
+
const lit = nodes.filter((_, i) => t > onT(i, c2)).length;
|
| 543 |
+
pill(ctx, `TESTNET · 2018 · 节点 ${lit}`, st.cx, P ? st.y + 60 : st.y + 10, { size: 30, alpha: aN, color: TEAL, fam: F_GR });
|
| 544 |
+
}
|
| 545 |
+
// Series A
|
| 546 |
+
const aF = win(t, s.at(3, '两千万') - .3, .5) * (1 - win(t, c4, .5));
|
| 547 |
+
if (aF > 0) {
|
| 548 |
+
// coin rain
|
| 549 |
+
for (let k = 0; k < 26; k++) {
|
| 550 |
+
const t0 = c3 + k * .14, dt = t - t0; if (dt < 0) continue;
|
| 551 |
+
const x = st.x + hash1(k * 3) * st.w, y = st.y - 80 + dt * dt * 900 + dt * 200;
|
| 552 |
+
if (y > L.H + 80) continue;
|
| 553 |
+
coin(ctx, x, y, 34, { label: '$', c1: GOLD, c2: '#ff9f2e', spin: dt * 6 + k, alpha: aF });
|
| 554 |
+
}
|
| 555 |
+
const p = eOut(inv(s.at(3, '两千万'), s.at(3, '两千万') + 1.3, t));
|
| 556 |
+
const v = Math.round(20000000 * p / 10000) * 10000;
|
| 557 |
+
ctx.save(); ctx.globalAlpha = aF; glow(ctx, st.cx, st.cy, 500, GOLD, .35);
|
| 558 |
+
bigNum(ctx, '$' + v.toLocaleString('en-US'), st.cx, st.cy - 30, P ? 108 : 150, { c1: GOLD, c2: GREEN });
|
| 559 |
+
text(ctx, '2019 · A 轮融资', st.cx, st.cy + (P ? 60 : 80), { size: P ? 40 : 44, weight: 800, color: WHITE, align: 'center' });
|
| 560 |
+
text(ctx, 'Multicoin Capital 领投', st.cx, st.cy + (P ? 120 : 140), { size: P ? 34 : 36, weight: 700, fam: F_GR, color: GOLD, align: 'center' });
|
| 561 |
+
ctx.restore();
|
| 562 |
+
}
|
| 563 |
+
// waiting rocket
|
| 564 |
+
const aR = win(t, c4 - .1, .6);
|
| 565 |
+
if (aR > 0) {
|
| 566 |
+
const ry = st.y + st.h - (P ? 150 : 110);
|
| 567 |
+
ctx.save(); ctx.globalAlpha = aR; ctx.fillStyle = '#231a40'; ctx.fillRect(st.cx - 160, ry + 70, 320, 18); ctx.fillRect(st.cx + 70, ry - 160, 16, 240); ctx.restore();
|
| 568 |
+
rocket(ctx, st.cx, ry - 10, P ? 1.6 : 1.5, { alpha: aR, flame: .15 + .1 * Math.sin(t * 9), t });
|
| 569 |
+
const blink = Math.sin(t * 6) > 0 ? 1 : .35;
|
| 570 |
+
gradText(ctx, 'MAINNET ?', st.cx, P ? st.y + 120 : st.y + 90, { size: P ? 96 : 110, fam: F_GR, weight: 700, align: 'center', alpha: aR * blink, ls: 8 });
|
| 571 |
+
}
|
| 572 |
+
},
|
| 573 |
+
sfx(s) {
|
| 574 |
+
const ev = TEAM.map(m => ({ t: m.k[1] ? s.at(m.k[0], m.k[1]) : .3, k: 'card' }));
|
| 575 |
+
for (let i = 0; i < 12; i++) ev.push({ t: s.cue(2) + i * .12, k: 'blip', v: .3, p: i });
|
| 576 |
+
ev.push({ t: s.at(3, '两千万') - .2, k: 'coins', d: 1.6 }, { t: s.at(3, '两千万') + 1.3, k: 'cash' });
|
| 577 |
+
ev.push({ t: s.cue(4), k: 'rumble', d: s.d - s.cue(4) });
|
| 578 |
+
return ev;
|
| 579 |
+
},
|
| 580 |
+
};
|
| 581 |
+
const onT = (i, c2) => c2 + hash1(i * 7 + 1) * 1.8;
|
| 582 |
+
|
| 583 |
+
// ---------------------------------------------------------------- 6. MAINNET 2020
|
| 584 |
+
const mainnet = {
|
| 585 |
+
draw(ctx, L, s) {
|
| 586 |
+
const { t, T } = s, st = L.stage, P = L.portrait, { W, H } = L;
|
| 587 |
+
const c1 = s.cue(1), c2 = s.cue(2), c3 = s.cue(3);
|
| 588 |
+
const launch = s.at(2, 'Solana');
|
| 589 |
+
const storm = 1 - win(t, launch + .5, 1.5);
|
| 590 |
+
// storm + lightning
|
| 591 |
+
rain(ctx, L, T, 180, .45 * storm, '#ff9fb0');
|
| 592 |
+
const flashes = [c1 + .4, s.at(1, '暴跌') + .1, s.at(1, '腰斩'), c2 - .4];
|
| 593 |
+
for (const f of flashes) { const d = t - f; if (d > 0 && d < .35) { ctx.fillStyle = `rgba(255,220,230,${(1 - d / .35) * .35 * storm})`; ctx.fillRect(0, 0, W, H); } }
|
| 594 |
+
// rocket on pad until launch, then lift-off
|
| 595 |
+
const lp = Math.max(0, t - launch);
|
| 596 |
+
const ry = st.y + st.h - (P ? 150 : 110) - lp * lp * 160;
|
| 597 |
+
const shk = t > launch - .8 && t < launch + 1.2 ? shake(t, 6) : [0, 0];
|
| 598 |
+
if (ry > -300) {
|
| 599 |
+
const pad = 1 - win(t, launch + 1, .5);
|
| 600 |
+
ctx.save(); ctx.globalAlpha = pad; ctx.fillStyle = '#231a40'; ctx.fillRect(st.cx - 160, st.y + st.h - (P ? 150 : 110) + 60, 320, 18); ctx.restore();
|
| 601 |
+
// smoke
|
| 602 |
+
if (t > launch - .8) {
|
| 603 |
+
for (let k = 0; k < 40; k++) {
|
| 604 |
+
const t0 = launch - .8 + k * .06, d = t - t0; if (d < 0 || d > 3) continue;
|
| 605 |
+
const dir = hash1(k) > .5 ? 1 : -1;
|
| 606 |
+
const x = st.cx + dir * d * (120 + hash1(k + 5) * 260), y = st.y + st.h - (P ? 90 : 50) - d * 30;
|
| 607 |
+
ctx.save(); ctx.globalAlpha = (1 - d / 3) * .5; ctx.fillStyle = '#d9cfe8'; ctx.beginPath(); ctx.arc(x, y, 30 + d * 50, 0, 7); ctx.fill(); ctx.restore();
|
| 608 |
+
}
|
| 609 |
+
}
|
| 610 |
+
rocket(ctx, st.cx + shk[0], ry + shk[1], P ? 1.6 : 1.5, { flame: t > launch - .8 ? 1 : .1, t });
|
| 611 |
+
}
|
| 612 |
+
// c1: 312 crash chart (BTC, Surf daily data)
|
| 613 |
+
const aC = win(t, c1, .5) * (1 - win(t, c2 + .2, .5));
|
| 614 |
+
if (aC > 0) {
|
| 615 |
+
const rect = P ? { x: st.x + 60, y: st.y + 140, w: st.w - 100, h: 360 } : { x: st.x + 120, y: st.y + 40, w: 900, h: 420 };
|
| 616 |
+
const p = inv(c1 + .2, s.at(1, '腰斩') + .6, t);
|
| 617 |
+
ctx.save(); ctx.globalAlpha = aC;
|
| 618 |
+
glass(ctx, rect.x - 40, rect.y - 70, rect.w + 80, rect.h + 130, { border: RED, fill: 'rgba(30,6,12,.75)' });
|
| 619 |
+
text(ctx, 'BTC · 2020 年 3 月(日线)', rect.x, rect.y - 30, { size: 26, weight: 700, color: 'rgba(255,255,255,.8)' });
|
| 620 |
+
const hi = 9300, lo = 4400, n = BTC_2020_03.length - 1;
|
| 621 |
+
const X = i => rect.x + i / n * rect.w, Y = v => rect.y + (hi - v) / (hi - lo) * rect.h;
|
| 622 |
+
ctx.beginPath(); const upto = p * n;
|
| 623 |
+
for (let i = 0; i <= Math.floor(upto); i++) { const v = BTC_2020_03[i][1]; i ? ctx.lineTo(X(i), Y(v)) : ctx.moveTo(X(i), Y(v)); }
|
| 624 |
+
const fi = Math.floor(upto); let hx = X(0), hy2 = Y(BTC_2020_03[0][1]);
|
| 625 |
+
if (fi < n) { const v = lerp(BTC_2020_03[fi][1], BTC_2020_03[fi + 1][1], upto - fi); hx = X(upto); hy2 = Y(v); ctx.lineTo(hx, hy2); } else { hx = X(n); hy2 = Y(BTC_2020_03[n][1]); }
|
| 626 |
+
ctx.strokeStyle = RED; ctx.lineWidth = 6; ctx.lineJoin = 'round'; ctx.stroke();
|
| 627 |
+
glow(ctx, hx, hy2, 50, RED, .9);
|
| 628 |
+
BTC_2020_03.forEach(([d], i) => { if (i % 2 === 0 || i === n) text(ctx, d.replace('03-', '3/'), X(i), rect.y + rect.h + 30, { size: 18, fam: F_MONO, color: 'rgba(255,255,255,.5)', align: 'center' }); });
|
| 629 |
+
ctx.restore();
|
| 630 |
+
stamp(ctx, '3.12', P ? st.cx + 250 : rect.x + rect.w + 230, P ? st.y + 600 : st.y + 170, t, s.at(1, '312') - .2, { size: P ? 90 : 120, color: RED, rot: -.1 });
|
| 631 |
+
const ak = win(t, s.at(1, '腰斩'), .4) * aC;
|
| 632 |
+
text(ctx, '≈ −50%', P ? st.cx : rect.x + rect.w + 230, P ? st.y + 740 : st.y + 360, { size: P ? 80 : 96, fam: F_GR, weight: 700, color: RED, align: 'center', alpha: ak, shadow: 30, shadowColor: RED });
|
| 633 |
+
}
|
| 634 |
+
// c2: calendar flip 12 → 16
|
| 635 |
+
const aK = win(t, c2 - .1, .4) * (1 - win(t, launch + .8, .5));
|
| 636 |
+
if (aK > 0) {
|
| 637 |
+
const day = 12 + Math.min(4, Math.floor(inv(c2, s.at(2, '3 月 16') + .2, t) * 4.999));
|
| 638 |
+
const x = P ? st.cx : st.cx - 480, y = P ? st.y + 230 : st.cy - 60;
|
| 639 |
+
ctx.save(); ctx.globalAlpha = aK;
|
| 640 |
+
rr(ctx, x - 130, y - 150, 260, 300, 26); ctx.fillStyle = '#f4f0ff'; ctx.fill();
|
| 641 |
+
rr(ctx, x - 130, y - 150, 260, 80, 26); ctx.fillStyle = day === 16 ? GREEN : RED; ctx.fill(); ctx.fillRect(x - 130, y - 100, 260, 30);
|
| 642 |
+
text(ctx, '2020 · 3 月', x, y - 112, { size: 32, weight: 800, color: '#fff', align: 'center' });
|
| 643 |
+
text(ctx, String(day), x, y + 50, { size: 150, fam: F_GR, weight: 700, color: '#1a1030', align: 'center' });
|
| 644 |
+
ctx.restore();
|
| 645 |
+
}
|
| 646 |
+
// c3: genesis block
|
| 647 |
+
const aG = win(t, c3 - .3, .7);
|
| 648 |
+
if (aG > 0) {
|
| 649 |
+
const gx = st.cx, gy = P ? st.cy - 40 : st.cy - 20, R = P ? 150 : 140;
|
| 650 |
+
glow(ctx, gx, gy, R * 3, GREEN, .5 * aG);
|
| 651 |
+
cube(ctx, gx, gy, R * eBack(aG), t);
|
| 652 |
+
ring(ctx, gx, gy, t - c3, { r: 520, color: GREEN, dur: 1.2 });
|
| 653 |
+
gradText(ctx, 'MAINNET BETA', gx, gy - R - (P ? 130 : 110), { size: P ? 80 : 92, fam: F_GR, weight: 700, align: 'center', alpha: aG, ls: 6 });
|
| 654 |
+
text(ctx, '2020.03.16 · 创世区块 Block 0', gx, gy - R - (P ? 60 : 44), { size: P ? 34 : 36, weight: 800, color: WHITE, align: 'center', alpha: aG });
|
| 655 |
+
// chain begins
|
| 656 |
+
for (let b = 0; b < 8; b++) {
|
| 657 |
+
const tb = c3 + .6 + b * .45, a = win(t, tb, .3, eBack); if (a <= 0) continue;
|
| 658 |
+
const dir = b % 2 ? 1 : -1, k = Math.floor(b / 2) + 1;
|
| 659 |
+
const bx = gx + dir * (R + 30 + k * (P ? 90 : 120)), by = gy + R + (P ? 90 : 60);
|
| 660 |
+
ctx.save(); ctx.globalAlpha = clamp(a); rr(ctx, bx - 36, by - 24, 72, 48, 10); ctx.fillStyle = 'rgba(8,30,24,.9)'; ctx.fill(); ctx.strokeStyle = GREEN; ctx.lineWidth = 3; ctx.stroke(); ctx.restore();
|
| 661 |
+
text(ctx, String(b + 1), bx, by + 2, { size: 22, fam: F_MONO, weight: 700, color: GREEN, align: 'center', alpha: clamp(a) });
|
| 662 |
+
}
|
| 663 |
+
}
|
| 664 |
+
},
|
| 665 |
+
sfx(s) {
|
| 666 |
+
const launch = s.at(2, 'Solana');
|
| 667 |
+
return [
|
| 668 |
+
{ t: 0, k: 'rainloop', d: launch + 1.5 },
|
| 669 |
+
{ t: s.cue(1) + .4, k: 'thunder' }, { t: s.at(1, '暴跌') + .1, k: 'thunder', v: .8 }, { t: s.at(1, '腰斩'), k: 'thunder' },
|
| 670 |
+
{ t: s.cue(1) + .2, k: 'drop', v: .8 }, { t: s.at(1, '312') - .2, k: 'stamp', v: 1 },
|
| 671 |
+
{ t: s.cue(2), k: 'flip' }, { t: s.cue(2) + .35, k: 'flip' }, { t: s.cue(2) + .7, k: 'flip' }, { t: s.at(2, '3 月 16') - .1, k: 'flip' },
|
| 672 |
+
{ t: launch - .8, k: 'rocket', d: 3.5 }, { t: s.cue(3), k: 'impact', v: .8 }, { t: s.cue(3) + .1, k: 'shimmer' },
|
| 673 |
+
];
|
| 674 |
+
},
|
| 675 |
+
};
|
| 676 |
+
function cube(ctx, x, y, R, t) {
|
| 677 |
+
const a = t * .5;
|
| 678 |
+
const pts = []; for (let i = 0; i < 8; i++) { const px = i & 1 ? 1 : -1, py = i & 2 ? 1 : -1, pz = i & 4 ? 1 : -1; const rx = px * Math.cos(a) - pz * Math.sin(a), rz = px * Math.sin(a) + pz * Math.cos(a); const ry = py * Math.cos(.5) - rz * Math.sin(.5), rz2 = py * Math.sin(.5) + rz * Math.cos(.5); pts.push([x + rx * R * .6, y + ry * R * .6, rz2]); }
|
| 679 |
+
const faces = [[0, 1, 3, 2], [4, 5, 7, 6], [0, 1, 5, 4], [2, 3, 7, 6], [0, 2, 6, 4], [1, 3, 7, 5]];
|
| 680 |
+
const sorted = faces.map(f => ({ f, z: f.reduce((s, i) => s + pts[i][2], 0) })).sort((p, q) => p.z - q.z);
|
| 681 |
+
ctx.save();
|
| 682 |
+
sorted.forEach(({ f }, k) => {
|
| 683 |
+
ctx.beginPath(); f.forEach((i, j) => ctx[j ? 'lineTo' : 'moveTo'](pts[i][0], pts[i][1])); ctx.closePath();
|
| 684 |
+
ctx.fillStyle = `rgba(${20 + k * 10},${120 + k * 18},${110 + k * 10},.35)`; ctx.fill(); ctx.strokeStyle = k > 2 ? GREEN : hexA(GREEN, .5); ctx.lineWidth = 3; ctx.stroke();
|
| 685 |
+
});
|
| 686 |
+
ctx.restore();
|
| 687 |
+
text(ctx, '0', x, y, { size: R * .5, fam: F_GR, weight: 700, color: WHITE, align: 'center', shadow: 20, shadowColor: GREEN });
|
| 688 |
+
}
|
| 689 |
+
|
| 690 |
+
// ---------------------------------------------------------------- 7. RISE 2021
|
| 691 |
+
const ECO = [['Serum', '订单簿交易所', '#00D1FF'], ['Raydium', '自动做市商 AMM', PURPLE], ['Phantom', '钱包', '#ab9ff2'], ['Magic Eden', 'NFT 市场', PINK]];
|
| 692 |
+
const rise = {
|
| 693 |
+
draw(ctx, L, s) {
|
| 694 |
+
const { t } = s, st = L.stage, P = L.portrait;
|
| 695 |
+
const c1 = s.cue(1), c2 = s.cue(2), c3 = s.cue(3), c4 = s.cue(4);
|
| 696 |
+
const rect = P ? { x: st.x + 90, y: st.y + 400, w: st.w - 120, h: st.h - 430 } : { x: st.x + 110, y: st.y + 30, w: st.w - 140, h: st.h - 70 };
|
| 697 |
+
const prog = .1 * eOut(inv(.4, c3, t)) + .9 * eIO(inv(c3, s.end(3) + .4, t));
|
| 698 |
+
const head = drawChart(ctx, rect, { from: idx('2021-01-01'), to: idx('2021-11-07'), prog, alpha: win(t, .2, .6), yMin: 0, yMax: 280, gridVals: [50, 100, 150, 200, 250] });
|
| 699 |
+
// eco bubbles
|
| 700 |
+
ECO.forEach(([nm, cat, col], i) => {
|
| 701 |
+
const a = win(t, s.at(1, nm.split(' ')[0]), .5, eBack) * (1 - .55 * win(t, c3, .6));
|
| 702 |
+
if (a <= 0) return;
|
| 703 |
+
const fl = Math.sin(t * 1.3 + i * 1.7) * 10;
|
| 704 |
+
let x, y, r;
|
| 705 |
+
if (P) { r = 96; x = st.cx + (i % 2 ? 210 : -210); y = st.y + 70 + (i < 2 ? 0 : 200) + fl; }
|
| 706 |
+
else { r = 105; x = rect.x + 170 + (i % 2) * 290 + (i > 1 ? 120 : 0); y = rect.y + 120 + (i > 1 ? 250 : 0) + fl; }
|
| 707 |
+
ctx.save(); ctx.globalAlpha = clamp(a); ctx.translate(x, y); ctx.scale(a, a);
|
| 708 |
+
glow(ctx, 0, 0, r * 1.8, col, .4);
|
| 709 |
+
ctx.fillStyle = 'rgba(14,10,32,.85)'; ctx.beginPath(); ctx.arc(0, 0, r, 0, 7); ctx.fill();
|
| 710 |
+
ctx.strokeStyle = col; ctx.lineWidth = 5; ctx.stroke();
|
| 711 |
+
text(ctx, nm, 0, -12, { size: r * .3, fam: F_GR, weight: 700, color: WHITE, align: 'center', maxW: r * 1.7 });
|
| 712 |
+
text(ctx, cat, 0, r * .3, { size: r * .18, weight: 700, color: col, align: 'center', maxW: r * 1.7 });
|
| 713 |
+
ctx.restore();
|
| 714 |
+
});
|
| 715 |
+
// users/devs flowing in
|
| 716 |
+
const aU = win(t, c2 - .2, .5) * (1 - win(t, c3 + .5, .6));
|
| 717 |
+
if (aU > 0) {
|
| 718 |
+
ctx.save(); ctx.globalCompositeOperation = 'lighter';
|
| 719 |
+
for (let k = 0; k < 120; k++) {
|
| 720 |
+
const ang = hash1(k) * Math.PI * 2, sp = .4 + hash1(k + 3) * .6, ph = (hash1(k + 7) + (t - c2) * sp) % 1;
|
| 721 |
+
const R0 = Math.max(L.W, L.H) * .7;
|
| 722 |
+
const x = st.cx + Math.cos(ang) * R0 * (1 - ph), y = st.cy + Math.sin(ang) * R0 * (1 - ph);
|
| 723 |
+
ctx.fillStyle = k % 2 ? GREEN : PURPLE; ctx.globalAlpha = aU * ph; ctx.beginPath(); ctx.arc(x, y, 4, 0, 7); ctx.fill();
|
| 724 |
+
}
|
| 725 |
+
ctx.restore();
|
| 726 |
+
gradText(ctx, '开发者 + 用户 涌入', st.cx, P ? st.y + 340 : st.y + st.h - 120, { size: P ? 50 : 56, align: 'center', alpha: aU });
|
| 727 |
+
}
|
| 728 |
+
// ×170
|
| 729 |
+
if (t > c4 - .2) {
|
| 730 |
+
const x = P ? st.cx - 40 : rect.x + rect.w * .38, y = P ? st.y + 230 : rect.y + rect.h * .35;
|
| 731 |
+
stamp(ctx, '×170', x, y, t, c4, { size: P ? 120 : 150, color: GREEN, fam: F_GR, rot: -.08 });
|
| 732 |
+
text(ctx, '$1.51 → $258.93(日收盘)', x, y + (P ? 120 : 140), { size: P ? 32 : 36, fam: F_SC, weight: 800, color: WHITE, align: 'center', alpha: win(t, c4 + .3, .4) });
|
| 733 |
+
burst(ctx, head.x, head.y, t - c4, { r: 300, n: 40, seed: 8 });
|
| 734 |
+
}
|
| 735 |
+
},
|
| 736 |
+
sfx(s) {
|
| 737 |
+
const ev = [{ t: .2, k: 'riser', v: .5 }];
|
| 738 |
+
ECO.forEach(([nm]) => ev.push({ t: s.at(1, nm.split(' ')[0]), k: 'bubble' }));
|
| 739 |
+
ev.push({ t: s.cue(2) - .2, k: 'swarm', d: 2.5 });
|
| 740 |
+
ev.push({ t: s.cue(3), k: 'climb', d: s.end(3) + .4 - s.cue(3) });
|
| 741 |
+
ev.push({ t: s.cue(4), k: 'stamp', v: 1 }, { t: s.cue(4), k: 'impact', v: .7 });
|
| 742 |
+
return ev;
|
| 743 |
+
},
|
| 744 |
+
};
|
| 745 |
+
|
| 746 |
+
export const SCENES_A = { hook, night, poh, name: nameScene, build, mainnet, rise };
|
solana-clock/js/scenes-b.js
ADDED
|
@@ -0,0 +1,788 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Scenes 8–15: outage, ftx, rebuild, meme, ath, wall, now, outro
|
| 2 |
+
import { clamp, lerp, inv, eOut, eIn, eIO, eBack, win, rng, hash1, hexStr, glow, text, gradText, glowText, glass, pill, rr, solLogo, dial, burst, ring, shake, measure, hexA, fmtUSD, F_SC, F_GR, F_MONO, PURPLE, GREEN, TEAL, PINK, RED, GOLD, WHITE } from './draw.js';
|
| 3 |
+
import { monitor, coin, stamp, bubble, nodesIn, linksFor, tower, crumble, snow, bigNum } from './props.js';
|
| 4 |
+
import { drawChart, idx, LAST, price } from './chart.js';
|
| 5 |
+
|
| 6 |
+
// ---------------------------------------------------------------- 8. OUTAGE
|
| 7 |
+
const outage = {
|
| 8 |
+
draw(ctx, L, s) {
|
| 9 |
+
const { t, T } = s, st = L.stage, P = L.portrait;
|
| 10 |
+
const c1 = s.cue(1), c2 = s.cue(2), c3 = s.cue(3), c4 = s.cue(4);
|
| 11 |
+
const rect = P ? { x: st.x + 40, y: st.y + 40, w: st.w - 80, h: st.h - 160 } : { x: st.x + 60, y: st.y + 20, w: st.w - 120, h: st.h - 80 };
|
| 12 |
+
outage._n = outage._n || {}; outage._l = outage._l || {};
|
| 13 |
+
const nodes = outage._n[L.orient] || (outage._n[L.orient] = nodesIn(23, P ? 26 : 36, rect, P ? 110 : 140));
|
| 14 |
+
const links = outage._l[L.orient] || (outage._l[L.orient] = linksFor(nodes, 3));
|
| 15 |
+
const flood = win(t, c1 + .3, 2.2, eIn);
|
| 16 |
+
const dead = win(t, c2 - .2, .4);
|
| 17 |
+
const aNet = win(t, 0, .6) * (1 - .8 * win(t, c3 - .2, .5));
|
| 18 |
+
if (aNet > 0) {
|
| 19 |
+
ctx.save();
|
| 20 |
+
links.forEach(([i, j], k) => {
|
| 21 |
+
const A = nodes[i], B = nodes[j];
|
| 22 |
+
ctx.strokeStyle = dead > .5 ? `rgba(90,90,110,${.25 * aNet})` : hexA(flood > .3 ? RED : TEAL, .35 * aNet); ctx.lineWidth = 2;
|
| 23 |
+
ctx.beginPath(); ctx.moveTo(A.x, A.y); ctx.lineTo(B.x, B.y); ctx.stroke();
|
| 24 |
+
if (dead < .5) {
|
| 25 |
+
const np = 1 + Math.floor(flood * 6);
|
| 26 |
+
for (let q = 0; q < np; q++) {
|
| 27 |
+
const p = ((t * (.7 + flood * 2.5)) + hash1(k * 7 + q)) % 1;
|
| 28 |
+
ctx.fillStyle = q > 0 ? hexA(RED, aNet) : hexA(GREEN, aNet); ctx.beginPath(); ctx.arc(lerp(A.x, B.x, p), lerp(A.y, B.y, p), q > 0 ? 5 : 4, 0, 7); ctx.fill();
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
});
|
| 32 |
+
nodes.forEach((n, i) => {
|
| 33 |
+
const off = dead > .5 && hash1(i * 3) < dead * 1.1;
|
| 34 |
+
const col = off ? '#3a3a4a' : flood > .4 ? RED : GREEN;
|
| 35 |
+
if (!off) glow(ctx, n.x, n.y, 40, col, .45 * aNet);
|
| 36 |
+
ctx.fillStyle = col; ctx.globalAlpha = aNet; ctx.beginPath(); ctx.arc(n.x, n.y, 8, 0, 7); ctx.fill(); ctx.globalAlpha = 1;
|
| 37 |
+
});
|
| 38 |
+
ctx.restore();
|
| 39 |
+
if (flood > .1 && dead < .5) {
|
| 40 |
+
for (let k = 0; k < 14; k++) {
|
| 41 |
+
const a = win(t, c1 + .4 + k * .18, .2) * (1 - dead) * aNet; if (a <= 0) continue;
|
| 42 |
+
const x = rect.x + hash1(k * 11) * rect.w, y = rect.y + hash1(k * 13) * rect.h;
|
| 43 |
+
pill(ctx, 'BOT', x, y, { size: 22, color: RED, fg: '#fff', alpha: a, fam: F_MONO });
|
| 44 |
+
}
|
| 45 |
+
text(ctx, `垃圾交易 ${(Math.floor(flood * 400000)).toLocaleString()} / 秒`, st.cx, P ? st.y + st.h - 100 : st.y + st.h - 40, { size: P ? 36 : 40, fam: F_SC, weight: 900, color: RED, align: 'center', alpha: flood, stroke: 6, strokeColor: 'rgba(0,0,0,.7)' });
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
// c2: breaking news + 17h timer
|
| 49 |
+
const aB = win(t, c2 - .1, .4) * (1 - win(t, c3 - .1, .4));
|
| 50 |
+
if (aB > 0) {
|
| 51 |
+
ctx.save(); ctx.fillStyle = `rgba(0,0,0,${.45 * aB})`; ctx.fillRect(0, 0, L.W, L.H); ctx.restore();
|
| 52 |
+
const hrs = 17 * eOut(inv(c2, s.end(2), t));
|
| 53 |
+
const hh = Math.floor(hrs), mm = Math.floor((hrs - hh) * 60), ss = Math.floor(((hrs - hh) * 60 - mm) * 60);
|
| 54 |
+
const tstr = `${String(hh).padStart(2, '0')}:${String(mm).padStart(2, '0')}:${String(ss).padStart(2, '0')}`;
|
| 55 |
+
glowText(ctx, 'OFFLINE', st.cx, st.cy - (P ? 160 : 120), { size: P ? 120 : 150, fam: F_GR, weight: 700, color: RED, align: 'center', alpha: aB * (Math.sin(t * 10) > -.3 ? 1 : .5), glowColor: RED, ls: 10 });
|
| 56 |
+
text(ctx, tstr, st.cx, st.cy + (P ? 10 : 40), { size: P ? 110 : 130, fam: F_MONO, weight: 800, color: WHITE, align: 'center', alpha: aB });
|
| 57 |
+
newsBar(ctx, L, '突发', 'Solana 主网中断约 17 小时 · 验证者协调重启', aB, t);
|
| 58 |
+
}
|
| 59 |
+
// c3: outage markers on a timeline
|
| 60 |
+
const aT = win(t, c3 - .1, .5) * (1 - win(t, c4 + .2, .4));
|
| 61 |
+
if (aT > 0) {
|
| 62 |
+
const marks = [['2021.09', 0], ['2022.01', .3], ['2022.05', .45], ['2022.06', .6], ['2022.10', .78], ['2023.02', .95]];
|
| 63 |
+
const x0 = P ? st.x + 40 : st.x + 80, x1 = P ? st.x + st.w - 40 : st.x + st.w - 80, y = st.cy;
|
| 64 |
+
ctx.save(); ctx.globalAlpha = aT;
|
| 65 |
+
ctx.strokeStyle = 'rgba(255,255,255,.4)'; ctx.lineWidth = 4; ctx.beginPath(); ctx.moveTo(x0, y); ctx.lineTo(x1, y); ctx.stroke();
|
| 66 |
+
ctx.restore();
|
| 67 |
+
marks.forEach(([d, f], i) => {
|
| 68 |
+
const a = win(t, c3 + .2 + i * .35, .3, eBack) * aT; if (a <= 0) return;
|
| 69 |
+
const x = lerp(x0, x1, f), up = i % 2 ? -1 : 1;
|
| 70 |
+
glow(ctx, x, y, 70, RED, .6 * clamp(a));
|
| 71 |
+
ctx.save(); ctx.translate(x, y); ctx.scale(a, a); ctx.strokeStyle = RED; ctx.lineWidth = 9; ctx.lineCap = 'round';
|
| 72 |
+
ctx.beginPath(); ctx.moveTo(-18, -18); ctx.lineTo(18, 18); ctx.moveTo(18, -18); ctx.lineTo(-18, 18); ctx.stroke(); ctx.restore();
|
| 73 |
+
text(ctx, d, x, y + up * 64, { size: P ? 26 : 30, fam: F_MONO, weight: 700, color: WHITE, align: 'center', alpha: clamp(a) });
|
| 74 |
+
});
|
| 75 |
+
text(ctx, '停摆 / 严重降级 · 示意', st.cx, y + (P ? 170 : 150), { size: 26, weight: 700, color: 'rgba(255,255,255,.55)', align: 'center', alpha: aT });
|
| 76 |
+
}
|
| 77 |
+
// c4: the running joke
|
| 78 |
+
const aJ = win(t, c4 - .1, .5);
|
| 79 |
+
if (aJ > 0) {
|
| 80 |
+
const sh = shake(t, 4 * aJ);
|
| 81 |
+
glowText(ctx, '又宕机了?', st.cx + sh[0], st.cy - (P ? 60 : 40) + sh[1], { size: P ? 120 : 150, weight: 900, color: WHITE, align: 'center', alpha: aJ, glowColor: RED });
|
| 82 |
+
const bs = [['哈哈哈哈', -1, -1], ['重启大师', 1, -1], ['又双叒叕', -1, 1], ['熟悉的味道', 1, 1], ['关机下班', 0, 1.6]];
|
| 83 |
+
bs.forEach(([str, dx, dy], i) => {
|
| 84 |
+
const a = win(t, c4 + .2 + i * .25, .3, eBack); if (a <= 0) return;
|
| 85 |
+
const x = st.cx + dx * (P ? 230 : 480) + Math.sin(t * 2 + i) * 8, y = st.cy - 40 + dy * (P ? 210 : 190);
|
| 86 |
+
bubble(ctx, str, x, y, { size: P ? 32 : 36, alpha: clamp(a), tail: dx > 0 ? 'right' : 'left' });
|
| 87 |
+
});
|
| 88 |
+
}
|
| 89 |
+
},
|
| 90 |
+
post(ctx, L, s) {
|
| 91 |
+
// RGB-split glitch slices while the network floods
|
| 92 |
+
const { t } = s;
|
| 93 |
+
const g = win(t, s.cue(1) + .6, 1.5) * (1 - win(t, s.cue(2) + .3, .4)) + (s.t > s.cue(4) && s.t < s.cue(4) + .25 ? 1 : 0);
|
| 94 |
+
if (g <= 0) return;
|
| 95 |
+
const cv = ctx.canvas, sc = cv.width / L.W;
|
| 96 |
+
ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0);
|
| 97 |
+
const n = 7, f = Math.floor(t * 18);
|
| 98 |
+
for (let i = 0; i < n; i++) {
|
| 99 |
+
if (hash1(f * 31 + i) > .55 * g + .1) continue;
|
| 100 |
+
const y = hash1(f * 17 + i * 3) * cv.height, h = (10 + hash1(f + i * 5) * 60) * sc, dx = (hash1(f * 7 + i) - .5) * 120 * sc * g;
|
| 101 |
+
ctx.drawImage(cv, 0, y, cv.width, h, dx, y, cv.width, h);
|
| 102 |
+
}
|
| 103 |
+
ctx.globalCompositeOperation = 'lighter'; ctx.globalAlpha = .12 * g; ctx.fillStyle = RED; ctx.fillRect(0, 0, cv.width, cv.height);
|
| 104 |
+
ctx.restore();
|
| 105 |
+
},
|
| 106 |
+
sfx(s) {
|
| 107 |
+
return [
|
| 108 |
+
{ t: 0, k: 'hum', d: s.cue(1) + .5 },
|
| 109 |
+
{ t: s.cue(1) + .3, k: 'glitchloop', d: s.cue(2) - s.cue(1) },
|
| 110 |
+
{ t: s.cue(2) - .2, k: 'powerdown', v: 1 }, { t: s.cue(2) + .1, k: 'alarm', d: 2.4 },
|
| 111 |
+
...[0, 1, 2, 3, 4, 5].map(i => ({ t: s.cue(3) + .2 + i * .35, k: 'error', v: .6 })),
|
| 112 |
+
{ t: s.cue(4), k: 'glitch', v: .8 }, ...[0, 1, 2, 3, 4].map(i => ({ t: s.cue(4) + .2 + i * .25, k: 'pop', v: .5 })),
|
| 113 |
+
];
|
| 114 |
+
},
|
| 115 |
+
};
|
| 116 |
+
function newsBar(ctx, L, tag, msg, a, t) {
|
| 117 |
+
const P = L.portrait;
|
| 118 |
+
const y = P ? L.safe.b - 230 : L.H - 230, x = P ? L.safe.l : 0, w = P ? L.safe.r - L.safe.l : L.W;
|
| 119 |
+
ctx.save(); ctx.globalAlpha = a;
|
| 120 |
+
ctx.fillStyle = 'rgba(10,10,20,.92)'; ctx.fillRect(x, y, w, 70);
|
| 121 |
+
ctx.fillStyle = RED; ctx.fillRect(x, y, P ? 120 : 190, 70);
|
| 122 |
+
text(ctx, tag, x + (P ? 60 : 95), y + 36, { size: 36, weight: 900, color: '#fff', align: 'center' });
|
| 123 |
+
ctx.save(); ctx.beginPath(); ctx.rect(x + (P ? 130 : 210), y, w - (P ? 140 : 220), 70); ctx.clip();
|
| 124 |
+
const mw = measure(ctx, msg, 34, 800);
|
| 125 |
+
const avail = w - (P ? 140 : 220);
|
| 126 |
+
const off = mw > avail ? ((t * 140) % (mw + 200)) : 0;
|
| 127 |
+
text(ctx, msg, x + (P ? 140 : 230) - off, y + 36, { size: 34, weight: 800, color: '#fff' });
|
| 128 |
+
if (mw > avail) text(ctx, msg, x + (P ? 140 : 230) - off + mw + 200, y + 36, { size: 34, weight: 800, color: '#fff' });
|
| 129 |
+
ctx.restore();
|
| 130 |
+
ctx.fillStyle = GOLD; ctx.fillRect(x, y + 70, w, 5);
|
| 131 |
+
ctx.restore();
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
// ---------------------------------------------------------------- 9. FTX
|
| 135 |
+
const ftx = {
|
| 136 |
+
draw(ctx, L, s) {
|
| 137 |
+
const { t, T } = s, st = L.stage, P = L.portrait;
|
| 138 |
+
const c1 = s.cue(1), c2 = s.cue(2), c3 = s.cue(3), c4 = s.cue(4), c5 = s.cue(5);
|
| 139 |
+
snow(ctx, L, T, 140, .6);
|
| 140 |
+
const base = st.y + st.h - (P ? 40 : 10);
|
| 141 |
+
const fall = s.at(3, '大厦');
|
| 142 |
+
const aTw = win(t, c1 - .4, .7) * (1 - win(t, c4, .5));
|
| 143 |
+
const tw = P ? [{ x: st.cx - 170, w: 220, h: 560, l: 'FTX', sd: 3 }, { x: st.cx + 170, w: 200, h: 440, l: 'Alameda', sd: 5 }] : [{ x: st.cx - 520, w: 240, h: 580, l: 'FTX', sd: 3 }, { x: st.cx - 200, w: 220, h: 460, l: 'Alameda', sd: 5 }];
|
| 144 |
+
if (aTw > 0) {
|
| 145 |
+
tw.forEach((b, i) => {
|
| 146 |
+
if (i === 1 && t < c2 - .2) return;
|
| 147 |
+
const a = (i ? win(t, s.at(2, 'Alameda') - .2, .5) : 1) * aTw;
|
| 148 |
+
const rise = 1 - win(t, i ? s.at(2, 'Alameda') - .2 : c1 - .4, .9);
|
| 149 |
+
if (t < fall) {
|
| 150 |
+
const sh = t > c1 && t < fall ? shake(t, i === 0 ? 3 : 1, i) : [0, 0];
|
| 151 |
+
tower(ctx, b.x + sh[0], base + rise * 300, b.w, b.h, b.l, { alpha: a, seed: b.sd, lit: .55 });
|
| 152 |
+
// cracks
|
| 153 |
+
const cr = win(t, c1 + .5, 1.5);
|
| 154 |
+
if (i === 0 && cr > 0) { ctx.save(); ctx.strokeStyle = `rgba(255,255,255,${.8 * cr})`; ctx.lineWidth = 3; ctx.beginPath(); let x = b.x - 10, y = base - b.h + 60; ctx.moveTo(x, y); const r = rng(4); for (let k = 0; k < 12 * cr; k++) { x += (r() - .5) * 50; y += 40; ctx.lineTo(x, y); } ctx.stroke(); ctx.restore(); }
|
| 155 |
+
} else {
|
| 156 |
+
crumble(ctx, b.x, base, b.w, b.h, b.l, t - fall - i * .25, { seed: 9 + i });
|
| 157 |
+
}
|
| 158 |
+
});
|
| 159 |
+
if (t > fall) for (let i = 0; i < 2; i++) { const d = t - fall - i * .25; if (d > 0 && d < 2.5) { ctx.save(); ctx.globalAlpha = (1 - d / 2.5) * .55; ctx.fillStyle = '#b8c4d8'; ctx.beginPath(); ctx.ellipse(tw[i].x, base, 140 + d * 200, 50 + d * 60, 0, 0, 7); ctx.fill(); ctx.restore(); } }
|
| 160 |
+
}
|
| 161 |
+
// news
|
| 162 |
+
const aN = win(t, c1 - .1, .4) * (1 - win(t, c2 + .4, .4));
|
| 163 |
+
if (aN > 0) newsBar(ctx, L, '突发', '曾经的头部加密交易所 FTX 申请破产保护(2022.11)', aN, t);
|
| 164 |
+
// SOL linked with chains to the towers
|
| 165 |
+
const aS = win(t, c2 + .2, .6) * (1 - win(t, c4, .5));
|
| 166 |
+
const sx = P ? st.cx : st.cx + 320, sy = P ? st.y + 150 : st.cy - 60;
|
| 167 |
+
if (aS > 0) {
|
| 168 |
+
const tension = t < fall ? 0 : 1;
|
| 169 |
+
tw.forEach((b, i) => {
|
| 170 |
+
ctx.save(); ctx.globalAlpha = aS * (1 - tension * win(t, fall, .4)); ctx.strokeStyle = '#8aa0c0'; ctx.lineWidth = 5; ctx.setLineDash([16, 10]); ctx.lineDashOffset = -t * 40;
|
| 171 |
+
ctx.beginPath(); ctx.moveTo(b.x, base - b.h + 30); ctx.quadraticCurveTo((b.x + sx) / 2, sy + 120, sx, sy); ctx.stroke(); ctx.restore();
|
| 172 |
+
void i;
|
| 173 |
+
});
|
| 174 |
+
const drop = t > fall ? Math.min(1, (t - fall) * .5) : 0;
|
| 175 |
+
coin(ctx, sx, sy + drop * 60, P ? 90 : 110, { label: 'SOL', c1: PURPLE, c2: GREEN, alpha: aS, spin: t * 1.4, fs: 40 });
|
| 176 |
+
text(ctx, '大量 SOL 持仓', sx, sy + (P ? 140 : 160), { size: 32, weight: 800, color: WHITE, align: 'center', alpha: aS });
|
| 177 |
+
}
|
| 178 |
+
// crash chart
|
| 179 |
+
const aC = win(t, fall + .2, .5);
|
| 180 |
+
if (aC > 0) {
|
| 181 |
+
const rect = P ? { x: st.x + 90, y: st.y + 300, w: st.w - 120, h: st.h - 380 } : { x: st.x + 120, y: st.y + 50, w: st.w - 160, h: st.h - 120 };
|
| 182 |
+
const prog = eIO(inv(fall + .2, c4 - .1, t));
|
| 183 |
+
ctx.save(); ctx.fillStyle = `rgba(4,6,14,${.55 * aC})`; ctx.fillRect(0, 0, L.W, L.H); ctx.restore();
|
| 184 |
+
drawChart(ctx, rect, { from: idx('2022-11-01'), to: idx('2022-12-30'), prog, color: RED, color2: '#ff9a3d', alpha: aC * (1 - .7 * win(t, c5, .5)), yMin: 0, yMax: 40, gridVals: [10, 20, 30, 40] });
|
| 185 |
+
// −96%
|
| 186 |
+
if (t > c4 - .1) {
|
| 187 |
+
const sh = shake(t, 10 * (1 - inv(c4, c4 + .8, t)));
|
| 188 |
+
stamp(ctx, '−96%', st.cx + sh[0], (P ? st.y + 180 : st.cy - 40) + sh[1], t, c4, { size: P ? 140 : 190, color: RED, fam: F_GR, rot: -.06 });
|
| 189 |
+
text(ctx, '$258.93 → $9.69(日收盘)', st.cx, (P ? st.y + 330 : st.cy + 140), { size: P ? 34 : 40, weight: 800, color: WHITE, align: 'center', alpha: win(t, c4 + .3, .4) * (1 - win(t, c5, .4)) });
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
// c5: "Solana is dead"
|
| 193 |
+
const aD = win(t, c5 - .1, .6);
|
| 194 |
+
if (aD > 0) {
|
| 195 |
+
ctx.save(); ctx.fillStyle = `rgba(2,3,8,${.6 * aD})`; ctx.fillRect(0, 0, L.W, L.H); ctx.restore();
|
| 196 |
+
const gx = st.cx, gy = P ? st.cy + 140 : st.cy + 120, gw = P ? 360 : 380, gh = P ? 440 : 420;
|
| 197 |
+
ctx.save(); ctx.globalAlpha = aD; ctx.translate(0, (1 - eOut(aD)) * 200);
|
| 198 |
+
ctx.fillStyle = '#4a4f62'; ctx.beginPath(); ctx.moveTo(gx - gw / 2, gy + gh / 2); ctx.lineTo(gx - gw / 2, gy - gh / 2 + gw / 2); ctx.arc(gx, gy - gh / 2 + gw / 2, gw / 2, Math.PI, 0); ctx.lineTo(gx + gw / 2, gy + gh / 2); ctx.closePath(); ctx.fill();
|
| 199 |
+
ctx.strokeStyle = '#6c7390'; ctx.lineWidth = 6; ctx.stroke();
|
| 200 |
+
text(ctx, 'R.I.P.', gx, gy - gh / 2 + 120, { size: 64, fam: F_GR, weight: 700, color: '#cfd5e6', align: 'center' });
|
| 201 |
+
text(ctx, 'SOLANA', gx, gy + 10, { size: 54, fam: F_GR, weight: 700, color: '#cfd5e6', align: 'center', ls: 6 });
|
| 202 |
+
text(ctx, '2020 – 2022 ?', gx, gy + 80, { size: 34, fam: F_MONO, weight: 600, color: '#aab1c6', align: 'center' });
|
| 203 |
+
ctx.fillStyle = '#1a1d28'; ctx.fillRect(gx - gw, gy + gh / 2 - 4, gw * 2, 30);
|
| 204 |
+
ctx.restore();
|
| 205 |
+
const hl = ['“Solana 已死”', '“归零倒计时”', '“生态大逃亡”'];
|
| 206 |
+
hl.forEach((h, i) => {
|
| 207 |
+
const a = win(t, c5 + .3 + i * .3, .4) * aD; if (a <= 0) return;
|
| 208 |
+
const x = P ? st.cx + (i - 1) * 40 : st.cx + (i - 1) * 540, y = P ? st.y + 120 + i * 120 : st.y + 60 + (i % 2) * 80;
|
| 209 |
+
text(ctx, h, x, y, { size: P ? 42 : 46, weight: 900, color: '#9aa3bb', align: 'center', alpha: a * .9 });
|
| 210 |
+
});
|
| 211 |
+
}
|
| 212 |
+
},
|
| 213 |
+
sfx(s) {
|
| 214 |
+
const fall = s.at(3, '大厦');
|
| 215 |
+
return [
|
| 216 |
+
{ t: 0, k: 'wind', d: s.d }, { t: s.cue(1) - .4, k: 'rumble', d: 1.5 }, { t: s.cue(1), k: 'news', v: 1 },
|
| 217 |
+
{ t: s.cue(1) + .5, k: 'crack', v: .8 }, { t: s.at(2, 'Alameda') - .2, k: 'rumble', d: 1 },
|
| 218 |
+
{ t: fall, k: 'collapse', v: 1 }, { t: fall + .25, k: 'crash', v: .8 }, { t: fall + .3, k: 'drop', v: 1 },
|
| 219 |
+
{ t: s.cue(4), k: 'impact', v: 1 }, { t: s.cue(5) - .1, k: 'bell', v: .7 },
|
| 220 |
+
];
|
| 221 |
+
},
|
| 222 |
+
};
|
| 223 |
+
|
| 224 |
+
// ---------------------------------------------------------------- 10. REBUILD 2023
|
| 225 |
+
const rebuild = {
|
| 226 |
+
draw(ctx, L, s) {
|
| 227 |
+
const { t, T } = s, st = L.stage, P = L.portrait;
|
| 228 |
+
const c1 = s.cue(1), c2 = s.cue(2), c3 = s.cue(3), c4 = s.cue(4), c5 = s.cue(5);
|
| 229 |
+
// c0: embers converge into the logo
|
| 230 |
+
const a0 = 1 - win(t, c1 - .1, .5);
|
| 231 |
+
if (a0 > 0) {
|
| 232 |
+
const lx = st.cx, ly = st.cy - 30, size = P ? 380 : 360;
|
| 233 |
+
const conv = eIO(inv(.2, c1 - .6, t));
|
| 234 |
+
ctx.save(); ctx.globalCompositeOperation = 'lighter';
|
| 235 |
+
for (let k = 0; k < 260; k++) {
|
| 236 |
+
const r1 = hash1(k), r2 = hash1(k + 500), r3 = hash1(k + 900);
|
| 237 |
+
const bar = Math.floor(r3 * 3), yy = [-size * .32, 0, size * .32][bar], xx = (r1 - .5) * size * .9 + (bar === 1 ? -1 : 1) * (r2 - .5) * 20;
|
| 238 |
+
const sx = st.cx + (hash1(k + 77) - .5) * L.W * 1.1, sy = L.H + 50 - hash1(k + 33) * 300 - ((t * 120 * (.5 + r2)) % 200);
|
| 239 |
+
const x = lerp(sx, lx + xx, conv), y = lerp(sy, ly + yy, conv);
|
| 240 |
+
ctx.globalAlpha = a0 * (.4 + .6 * r2); ctx.fillStyle = r1 > .5 ? '#ffb347' : '#ff6a3d'; ctx.beginPath(); ctx.arc(x, y, 3 + r2 * 3, 0, 7); ctx.fill();
|
| 241 |
+
}
|
| 242 |
+
ctx.restore();
|
| 243 |
+
glow(ctx, lx, ly, size * 1.4, '#ff8a3d', .5 * conv * a0);
|
| 244 |
+
solLogo(ctx, lx, ly, size, conv > .9 ? 1 : 0, a0 * win(t, c1 - 1, .6));
|
| 245 |
+
// wings
|
| 246 |
+
const wa = win(t, c1 - .9, .6) * a0;
|
| 247 |
+
if (wa > 0) for (const d of [-1, 1]) {
|
| 248 |
+
ctx.save(); ctx.globalAlpha = wa * .8; ctx.translate(lx + d * size * .55, ly); ctx.scale(d, 1);
|
| 249 |
+
for (let f = 0; f < 6; f++) { const a = -.3 - f * .22 + Math.sin(T * 3) * .05; ctx.rotate(0); ctx.strokeStyle = f % 2 ? GOLD : '#ff6a3d'; ctx.lineWidth = 8 - f; ctx.beginPath(); ctx.moveTo(0, 0); ctx.quadraticCurveTo(size * .3, -size * .2 + f * 10, Math.cos(a) * size * (.7 - f * .06), Math.sin(a) * size * (.7 - f * .06)); ctx.stroke(); }
|
| 250 |
+
ctx.restore();
|
| 251 |
+
}
|
| 252 |
+
}
|
| 253 |
+
// c1: Serum → OpenBook fork
|
| 254 |
+
const aF = win(t, c1 - .1, .5) * (1 - win(t, c2 - .1, .4));
|
| 255 |
+
if (aF > 0) {
|
| 256 |
+
const y = st.cy, x0 = P ? st.x + 90 : st.cx - 600, xm = P ? st.cx - 60 : st.cx - 150, x1 = P ? st.x + st.w - 120 : st.cx + 520;
|
| 257 |
+
const p = eIO(inv(c1, c1 + 1.6, t));
|
| 258 |
+
ctx.save(); ctx.globalAlpha = aF; ctx.lineCap = 'round';
|
| 259 |
+
ctx.strokeStyle = '#7a7a90'; ctx.lineWidth = 12; ctx.beginPath(); ctx.moveTo(x0, y); ctx.lineTo(xm, y); ctx.stroke();
|
| 260 |
+
ctx.strokeStyle = 'rgba(255,90,90,.6)'; ctx.setLineDash([10, 14]); ctx.beginPath(); ctx.moveTo(xm, y); ctx.lineTo(lerp(xm, x1, p), y + (P ? 220 : 160) * p); ctx.stroke(); ctx.setLineDash([]);
|
| 261 |
+
const g = ctx.createLinearGradient(xm, 0, x1, 0); g.addColorStop(0, PURPLE); g.addColorStop(1, GREEN);
|
| 262 |
+
ctx.strokeStyle = g; ctx.beginPath(); ctx.moveTo(xm, y); ctx.bezierCurveTo(xm + 120, y, lerp(xm, x1, p) - 100, y - (P ? 220 : 160) * p, lerp(xm, x1, p), y - (P ? 220 : 160) * p); ctx.stroke();
|
| 263 |
+
ctx.restore();
|
| 264 |
+
node(ctx, x0, y, 'Serum', '#9a9ab0', aF);
|
| 265 |
+
if (p > .95) { node(ctx, x1, y - (P ? 220 : 160), 'OpenBook', GREEN, aF); text(ctx, '已停用', x1, y + (P ? 220 : 160) + 70, { size: 28, weight: 700, color: 'rgba(255,120,120,.8)', align: 'center', alpha: aF }); }
|
| 266 |
+
pill(ctx, '社区分叉 Fork', xm, y + 80, { size: 30, color: GOLD, alpha: aF });
|
| 267 |
+
}
|
| 268 |
+
// c2: BONK christmas
|
| 269 |
+
const aK = win(t, c2 - .1, .5) * (1 - win(t, c3 - .1, .4));
|
| 270 |
+
if (aK > 0) {
|
| 271 |
+
// string lights
|
| 272 |
+
ctx.save(); ctx.globalAlpha = aK;
|
| 273 |
+
for (let row = 0; row < 2; row++) {
|
| 274 |
+
const y0 = (P ? st.y + 30 : st.y - 10) + row * 60;
|
| 275 |
+
ctx.strokeStyle = '#2b4a2b'; ctx.lineWidth = 3; ctx.beginPath();
|
| 276 |
+
for (let x = 0; x <= L.W; x += 20) { const y = y0 + Math.sin(x / L.W * Math.PI * 3 + row) * 22; x ? ctx.lineTo(x, y) : ctx.moveTo(x, y); } ctx.stroke();
|
| 277 |
+
for (let k = 0; k < 18; k++) { const x = (k + .5) / 18 * L.W, y = y0 + Math.sin(x / L.W * Math.PI * 3 + row) * 22 + 10; const on = Math.sin(T * 4 + k * 1.7 + row) > 0; const col = [RED, GOLD, GREEN, TEAL][(k + row) % 4]; if (on) glow(ctx, x, y, 26, col, .9); ctx.fillStyle = col; ctx.beginPath(); ctx.arc(x, y, 7, 0, 7); ctx.fill(); }
|
| 278 |
+
}
|
| 279 |
+
ctx.restore();
|
| 280 |
+
for (let k = 0; k < 30; k++) {
|
| 281 |
+
const t0 = c2 + k * .13, d = t - t0; if (d < 0) continue;
|
| 282 |
+
const x = st.x + hash1(k * 5) * st.w, y = st.y - 60 + d * d * 500 + d * 150;
|
| 283 |
+
if (y > L.H + 60) continue;
|
| 284 |
+
coin(ctx, x, y, 38, { label: 'BONK', c1: '#ffb13b', c2: '#ff7a00', spin: d * 5 + k, alpha: aK, fs: 16 });
|
| 285 |
+
}
|
| 286 |
+
const bx = st.cx, by = st.cy;
|
| 287 |
+
glow(ctx, bx, by, 300, '#ff9f2e', .5 * aK);
|
| 288 |
+
dog(ctx, bx, by, P ? 1.1 : 1.15, aK, T);
|
| 289 |
+
text(ctx, '2022.12.25 · BONK 空投', bx, by + (P ? 230 : 220), { size: P ? 42 : 46, weight: 900, color: '#ffcf6b', align: 'center', alpha: aK, stroke: 6, strokeColor: 'rgba(0,0,0,.6)' });
|
| 290 |
+
}
|
| 291 |
+
// c3: Firedancer
|
| 292 |
+
const aD = win(t, c3 - .1, .5) * (1 - win(t, c4 - .1, .4));
|
| 293 |
+
if (aD > 0) {
|
| 294 |
+
const fx = st.cx, fy = st.cy + (P ? 140 : 160);
|
| 295 |
+
flames(ctx, fx, fy, P ? 1.25 : 1.3, T, aD);
|
| 296 |
+
const v = Math.round(1000000 * eOut(inv(s.at(3, '百万') - .6, s.at(3, '百万') + .6, t)));
|
| 297 |
+
bigNum(ctx, v.toLocaleString('en-US'), fx, fy - (P ? 380 : 360), P ? 110 : 130, { c1: GOLD, c2: '#ff5a3d', alpha: aD });
|
| 298 |
+
text(ctx, 'TPS · 测试演示', fx, fy - (P ? 290 : 270), { size: 34, weight: 800, color: WHITE, align: 'center', alpha: aD });
|
| 299 |
+
text(ctx, 'Firedancer', fx, fy + (P ? 100 : 80), { size: P ? 64 : 70, fam: F_GR, weight: 700, color: '#ffd28a', align: 'center', alpha: aD, shadow: 30, shadowColor: '#ff6a3d' });
|
| 300 |
+
text(ctx, 'Jump 团队打造的第二个验证者客户端', fx, fy + (P ? 170 : 145), { size: 30, weight: 700, color: 'rgba(255,255,255,.8)', align: 'center', alpha: aD });
|
| 301 |
+
}
|
| 302 |
+
// c4: three cards
|
| 303 |
+
const aC = win(t, c4 - .1, .4) * (1 - win(t, c5 - .1, .4));
|
| 304 |
+
if (aC > 0) {
|
| 305 |
+
const cards = [['状态压缩', '100 万个 NFT', '≈ $110', s.at(4, '状态压缩')], ['Helium', '整个网络迁入', 'Solana', s.at(4, 'Helium')], ['Saga', 'Solana 手机', '2023', s.at(4, 'Saga')]];
|
| 306 |
+
cards.forEach(([a1, a2, a3, at], i) => {
|
| 307 |
+
const a = win(t, at, .45) * aC; if (a <= 0) return;
|
| 308 |
+
let x, y, w = P ? 520 : 470, h = P ? 220 : 380;
|
| 309 |
+
if (P) { x = st.cx; y = st.y + 120 + i * 260; } else { x = st.cx + (i - 1) * 520; y = st.cy; }
|
| 310 |
+
ctx.save(); ctx.globalAlpha = a; ctx.translate(x, y + (1 - a) * 80);
|
| 311 |
+
glass(ctx, -w / 2, -h / 2, w, h, { border: '#ffa63d' });
|
| 312 |
+
if (i === 2) phone(ctx, P ? -170 : 0, P ? 0 : -60, P ? .6 : .9, t);
|
| 313 |
+
else if (i === 0) { for (let k = 0; k < 16; k++) { const gx = (k % 4 - 1.5) * (P ? 34 : 44) + (P ? -170 : 0), gy = (Math.floor(k / 4) - 1.5) * (P ? 34 : 44) + (P ? 0 : -60); ctx.fillStyle = [PURPLE, GREEN, TEAL, PINK][k % 4]; ctx.globalAlpha = a * (.5 + .5 * Math.sin(t * 3 + k)); rr(ctx, gx - 14, gy - 14, 28, 28, 6); ctx.fill(); } ctx.globalAlpha = a; }
|
| 314 |
+
else { ctx.strokeStyle = TEAL; ctx.lineWidth = 4; for (let k = 1; k <= 3; k++) { ctx.globalAlpha = a * (1 - ((t * .8 + k / 3) % 1)); ctx.beginPath(); ctx.arc(P ? -170 : 0, P ? 0 : -60, ((t * .8 + k / 3) % 1) * 80, 0, 7); ctx.stroke(); } ctx.globalAlpha = a; ctx.fillStyle = TEAL; ctx.beginPath(); ctx.arc(P ? -170 : 0, P ? 0 : -60, 14, 0, 7); ctx.fill(); }
|
| 315 |
+
const tx = P ? 60 : 0, ty0 = P ? -60 : 80;
|
| 316 |
+
text(ctx, a1, tx, ty0, { size: P ? 44 : 46, weight: 900, fam: F_SC, color: WHITE, align: 'center' });
|
| 317 |
+
text(ctx, a2, tx, ty0 + 52, { size: 30, weight: 700, color: 'rgba(255,255,255,.75)', align: 'center' });
|
| 318 |
+
gradText(ctx, a3, tx, ty0 + 108, { size: 42, fam: F_GR, weight: 700, align: 'center', c1: GOLD, c2: GREEN });
|
| 319 |
+
ctx.restore();
|
| 320 |
+
});
|
| 321 |
+
}
|
| 322 |
+
// c5: 2023 chart ×10
|
| 323 |
+
const aR = win(t, c5 - .1, .5);
|
| 324 |
+
if (aR > 0) {
|
| 325 |
+
const rect = P ? { x: st.x + 90, y: st.y + 330, w: st.w - 120, h: st.h - 400 } : { x: st.x + 120, y: st.y + 40, w: st.w - 160, h: st.h - 110 };
|
| 326 |
+
const prog = eIO(inv(c5, s.end(5) + .2, t));
|
| 327 |
+
const head = drawChart(ctx, rect, { from: idx('2023-01-01'), to: idx('2023-12-31'), prog, alpha: aR, color: '#ffb347', color2: GREEN, yMin: 0, yMax: 130, gridVals: [25, 50, 75, 100, 125] });
|
| 328 |
+
if (t > s.end(5) - .4) {
|
| 329 |
+
stamp(ctx, '×10', P ? st.cx : rect.x + rect.w * .3, P ? st.y + 170 : rect.y + rect.h * .3, t, s.end(5) - .4, { size: P ? 130 : 150, color: GREEN, fam: F_GR });
|
| 330 |
+
burst(ctx, head.x, head.y, t - s.end(5) + .4, { r: 300, seed: 12 });
|
| 331 |
+
}
|
| 332 |
+
}
|
| 333 |
+
},
|
| 334 |
+
sfx(s) {
|
| 335 |
+
return [
|
| 336 |
+
{ t: .2, k: 'riser', v: .8 }, { t: s.cue(1) - .9, k: 'phoenix', v: 1 }, { t: s.cue(1) - .6, k: 'shimmer' },
|
| 337 |
+
{ t: s.cue(1) + .2, k: 'whoosh', v: .6 }, { t: s.cue(1) + 1.6, k: 'pop' },
|
| 338 |
+
{ t: s.cue(2), k: 'jingle', d: s.cue(3) - s.cue(2) }, { t: s.cue(2) + .2, k: 'coins', d: 2.5 }, { t: s.cue(2) + .4, k: 'bark' },
|
| 339 |
+
{ t: s.cue(3) - .1, k: 'fire', d: s.cue(4) - s.cue(3) + .2 }, { t: s.at(3, '百万') + .6, k: 'impact', v: .6 },
|
| 340 |
+
{ t: s.at(4, '状态压缩'), k: 'card' }, { t: s.at(4, 'Helium'), k: 'card' }, { t: s.at(4, 'Saga'), k: 'card' },
|
| 341 |
+
{ t: s.cue(5), k: 'climb', d: s.end(5) - s.cue(5) }, { t: s.end(5) - .4, k: 'stamp', v: 1 },
|
| 342 |
+
];
|
| 343 |
+
},
|
| 344 |
+
};
|
| 345 |
+
function node(ctx, x, y, label, col, a) {
|
| 346 |
+
glow(ctx, x, y, 90, col, .5 * a);
|
| 347 |
+
ctx.save(); ctx.globalAlpha = a; ctx.fillStyle = '#0d0b1c'; ctx.beginPath(); ctx.arc(x, y, 34, 0, 7); ctx.fill(); ctx.strokeStyle = col; ctx.lineWidth = 6; ctx.stroke(); ctx.restore();
|
| 348 |
+
text(ctx, label, x, y - 66, { size: 38, fam: F_GR, weight: 700, color: col, align: 'center', alpha: a });
|
| 349 |
+
}
|
| 350 |
+
function dog(ctx, x, y, s, a, T) {
|
| 351 |
+
ctx.save(); ctx.globalAlpha = a; ctx.translate(x, y); ctx.scale(s, s); ctx.rotate(Math.sin(T * 3) * .05);
|
| 352 |
+
ctx.fillStyle = '#f2a33a';
|
| 353 |
+
ctx.beginPath(); ctx.moveTo(-110, -60); ctx.lineTo(-80, -170); ctx.lineTo(-30, -100); ctx.fill();
|
| 354 |
+
ctx.beginPath(); ctx.moveTo(110, -60); ctx.lineTo(80, -170); ctx.lineTo(30, -100); ctx.fill();
|
| 355 |
+
ctx.beginPath(); ctx.ellipse(0, 0, 130, 120, 0, 0, 7); ctx.fill();
|
| 356 |
+
ctx.fillStyle = '#fff3dd'; ctx.beginPath(); ctx.ellipse(0, 50, 80, 62, 0, 0, 7); ctx.fill();
|
| 357 |
+
ctx.fillStyle = '#1a1020'; ctx.beginPath(); ctx.ellipse(-45, -20, 13, 17, 0, 0, 7); ctx.ellipse(45, -20, 13, 17, 0, 0, 7); ctx.fill();
|
| 358 |
+
ctx.beginPath(); ctx.ellipse(0, 22, 22, 15, 0, 0, 7); ctx.fill();
|
| 359 |
+
ctx.strokeStyle = '#1a1020'; ctx.lineWidth = 6; ctx.beginPath(); ctx.moveTo(0, 36); ctx.lineTo(0, 56); ctx.moveTo(-28, 64); ctx.quadraticCurveTo(0, 82, 28, 64); ctx.stroke();
|
| 360 |
+
// santa hat
|
| 361 |
+
ctx.fillStyle = RED; ctx.beginPath(); ctx.moveTo(-90, -95); ctx.quadraticCurveTo(10, -230, 120, -150); ctx.lineTo(70, -95); ctx.fill();
|
| 362 |
+
ctx.fillStyle = '#fff'; rr(ctx, -100, -112, 180, 34, 17); ctx.fill(); ctx.beginPath(); ctx.arc(124, -150, 20, 0, 7); ctx.fill();
|
| 363 |
+
ctx.restore();
|
| 364 |
+
}
|
| 365 |
+
function flames(ctx, x, y, s, T, a) {
|
| 366 |
+
ctx.save(); ctx.globalAlpha = a; ctx.translate(x, y); ctx.scale(s, s); ctx.globalCompositeOperation = 'lighter';
|
| 367 |
+
for (let k = 0; k < 26; k++) {
|
| 368 |
+
const ph = (T * (1.2 + hash1(k) * .8) + hash1(k + 3)) % 1, xo = (hash1(k + 9) - .5) * 260 * (1 - ph * .6);
|
| 369 |
+
const h = 180 + hash1(k + 4) * 160, w = 50 + hash1(k + 1) * 50;
|
| 370 |
+
const yy = -ph * h;
|
| 371 |
+
const g = ctx.createRadialGradient(xo, yy, 0, xo, yy, w);
|
| 372 |
+
g.addColorStop(0, `rgba(255,240,180,${(1 - ph) * .9})`); g.addColorStop(.4, `rgba(255,130,40,${(1 - ph) * .6})`); g.addColorStop(1, 'rgba(255,60,20,0)');
|
| 373 |
+
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(xo, yy, w, 0, 7); ctx.fill();
|
| 374 |
+
}
|
| 375 |
+
ctx.restore();
|
| 376 |
+
glow(ctx, x, y - 120, 360, '#ff7a2e', .55 * a);
|
| 377 |
+
}
|
| 378 |
+
function phone(ctx, x, y, s, t) {
|
| 379 |
+
ctx.save(); ctx.translate(x, y); ctx.scale(s, s);
|
| 380 |
+
rr(ctx, -55, -100, 110, 200, 18); ctx.fillStyle = '#111'; ctx.fill(); ctx.strokeStyle = '#ffa63d'; ctx.lineWidth = 4; ctx.stroke();
|
| 381 |
+
rr(ctx, -46, -88, 92, 176, 10); const g = ctx.createLinearGradient(0, -88, 0, 88); g.addColorStop(0, PURPLE); g.addColorStop(1, GREEN); ctx.fillStyle = g; ctx.fill();
|
| 382 |
+
solLogo(ctx, 0, 0, 56, 1, .9 + .1 * Math.sin(t * 3));
|
| 383 |
+
ctx.restore();
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
// ---------------------------------------------------------------- 11. MEME 2024
|
| 387 |
+
const MEMES = [['WIF', '#e8b98a', 'hat'], ['BONK', '#f2a33a', 'dog'], ['POPCAT', '#d9d0c4', 'cat']];
|
| 388 |
+
const meme = {
|
| 389 |
+
draw(ctx, L, s) {
|
| 390 |
+
const { t, T } = s, st = L.stage, P = L.portrait;
|
| 391 |
+
const c1 = s.cue(1), c2 = s.cue(2), c3 = s.cue(3), c4 = s.cue(4);
|
| 392 |
+
// confetti
|
| 393 |
+
ctx.save();
|
| 394 |
+
for (let k = 0; k < 90; k++) {
|
| 395 |
+
const x = (hash1(k) * L.W + Math.sin(T + k) * 30), y = ((hash1(k + 4) * L.H + T * (80 + hash1(k + 2) * 120)) % (L.H + 40)) - 20;
|
| 396 |
+
ctx.save(); ctx.translate(x, y); ctx.rotate(T * 3 + k); ctx.fillStyle = [PURPLE, GREEN, GOLD, PINK, TEAL][k % 5]; ctx.globalAlpha = .55; ctx.fillRect(-7, -3, 14, 6); ctx.restore();
|
| 397 |
+
}
|
| 398 |
+
ctx.restore();
|
| 399 |
+
// c0: title
|
| 400 |
+
const a0 = win(t, .2, .5, eBack) * (1 - win(t, c1 - .1, .4));
|
| 401 |
+
if (a0 > 0) {
|
| 402 |
+
ctx.save(); ctx.translate(st.cx, st.cy); ctx.rotate(Math.sin(T * 4) * .03); ctx.scale(a0, a0);
|
| 403 |
+
gradText(ctx, 'MEME', 0, -40, { size: P ? 200 : 240, fam: F_GR, weight: 700, align: 'center', c1: PINK, c2: GOLD, ls: 10 });
|
| 404 |
+
text(ctx, '主 场', 0, P ? 110 : 130, { size: P ? 80 : 90, weight: 900, color: WHITE, align: 'center', ls: 20 });
|
| 405 |
+
ctx.restore();
|
| 406 |
+
}
|
| 407 |
+
// c1: Jupiter airdrop parachutes
|
| 408 |
+
const aJ = win(t, c1 - .1, .4) * (1 - win(t, c2 - .1, .4));
|
| 409 |
+
if (aJ > 0) {
|
| 410 |
+
for (let k = 0; k < 9; k++) {
|
| 411 |
+
const t0 = c1 + k * .25, d = t - t0; if (d < 0) continue;
|
| 412 |
+
const x = st.x + (k + .5) / 9 * st.w + Math.sin(T * 1.5 + k) * 20, y = st.y - 80 + d * 140;
|
| 413 |
+
ctx.save(); ctx.globalAlpha = aJ;
|
| 414 |
+
ctx.fillStyle = k % 2 ? GREEN : TEAL; ctx.beginPath(); ctx.arc(x, y - 90, 60, Math.PI, 0); ctx.fill();
|
| 415 |
+
ctx.strokeStyle = 'rgba(255,255,255,.6)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(x - 60, y - 90); ctx.lineTo(x, y - 20); ctx.lineTo(x + 60, y - 90); ctx.moveTo(x, y - 150); ctx.lineTo(x, y - 20); ctx.stroke();
|
| 416 |
+
ctx.restore();
|
| 417 |
+
coin(ctx, x, y, 30, { label: 'JUP', c1: '#2fd1a5', c2: '#c7f284', alpha: aJ, spin: T * 2 + k, fs: 15 });
|
| 418 |
+
}
|
| 419 |
+
const n = Math.round(955000 * eOut(inv(c1, s.end(1), t)) / 1000) * 1000;
|
| 420 |
+
ctx.save(); ctx.globalAlpha = aJ;
|
| 421 |
+
glass(ctx, st.cx - (P ? 400 : 420), st.cy + (P ? 120 : 80), P ? 800 : 840, 200, {});
|
| 422 |
+
ctx.restore();
|
| 423 |
+
text(ctx, 'Jupiter 空投 · 2024.01', st.cx, st.cy + (P ? 165 : 125), { size: 36, weight: 800, color: GREEN, align: 'center', alpha: aJ });
|
| 424 |
+
bigNum(ctx, '≈ ' + n.toLocaleString('en-US') + ' 个地址', st.cx, st.cy + (P ? 245 : 205), P ? 58 : 64, { alpha: aJ, fam: F_SC, weight: 900 });
|
| 425 |
+
}
|
| 426 |
+
// c2: pump.fun coin machine
|
| 427 |
+
const aP = win(t, c2 - .1, .4) * (1 - win(t, c3 - .1, .4));
|
| 428 |
+
if (aP > 0) {
|
| 429 |
+
const mx = P ? st.cx - 200 : st.cx - 420, my = st.cy;
|
| 430 |
+
ctx.save(); ctx.globalAlpha = aP;
|
| 431 |
+
rr(ctx, mx - 130, my - 170, 260, 340, 30); const g = ctx.createLinearGradient(mx - 130, 0, mx + 130, 0); g.addColorStop(0, '#2a1f5a'); g.addColorStop(1, '#4a2f8a'); ctx.fillStyle = g; ctx.fill(); ctx.strokeStyle = GREEN; ctx.lineWidth = 5; ctx.stroke();
|
| 432 |
+
rr(ctx, mx - 90, my - 130, 180, 110, 16); ctx.fillStyle = '#0a1a14'; ctx.fill();
|
| 433 |
+
text(ctx, 'pump.fun', mx, my - 76, { size: 34, fam: F_GR, weight: 700, color: GREEN, align: 'center' });
|
| 434 |
+
ctx.fillStyle = RED; ctx.beginPath(); ctx.arc(mx, my + 50, 40 + 4 * Math.sin(t * 10), 0, 7); ctx.fill();
|
| 435 |
+
text(ctx, '发币', mx, my + 52, { size: 26, weight: 900, color: '#fff', align: 'center' });
|
| 436 |
+
ctx.restore();
|
| 437 |
+
const per = .22, n0 = Math.floor((t - c2) / per);
|
| 438 |
+
for (let k = Math.max(0, n0 - 14); k <= n0; k++) {
|
| 439 |
+
const d = t - (c2 + k * per); if (d < 0) continue;
|
| 440 |
+
const x = mx + 150 + d * (P ? 260 : 520), y = my + 120 + Math.sin(d * 3 + k) * 20 - d * 60 * (hash1(k) - .3);
|
| 441 |
+
if (x > L.W + 50) continue;
|
| 442 |
+
coin(ctx, x, y, 32, { label: hexStr(k, 3).toUpperCase(), c1: [PINK, GOLD, TEAL, GREEN][k % 4], c2: PURPLE, alpha: aP, spin: d * 4, fs: 16 });
|
| 443 |
+
}
|
| 444 |
+
const cnt = Math.max(0, n0) * 137 + 1;
|
| 445 |
+
text(ctx, `新币 +${cnt.toLocaleString()}`, P ? st.cx : st.cx + 280, P ? st.y + 120 : st.cy - 200, { size: P ? 56 : 64, fam: F_SC, weight: 900, color: GOLD, align: 'center', alpha: aP, stroke: 6, strokeColor: 'rgba(0,0,0,.6)' });
|
| 446 |
+
text(ctx, '几秒钟,发一个币(示意)', P ? st.cx : st.cx + 280, P ? st.y + 190 : st.cy - 130, { size: 30, weight: 700, color: 'rgba(255,255,255,.75)', align: 'center', alpha: aP });
|
| 447 |
+
}
|
| 448 |
+
// c3: meme faces
|
| 449 |
+
const aM = win(t, c3 - .1, .4) * (1 - win(t, c4 - .1, .4));
|
| 450 |
+
if (aM > 0) {
|
| 451 |
+
MEMES.forEach(([nm, col, kind], i) => {
|
| 452 |
+
const a = win(t, s.at(3, nm), .4, eBack) * aM; if (a <= 0) return;
|
| 453 |
+
const x = P ? st.cx + (i === 1 ? 160 : -100) : st.cx + (i - 1) * 520, y = P ? st.y + 160 + i * 270 : st.cy - 40;
|
| 454 |
+
const r = P ? 120 : 150, bob = Math.sin(T * 3 + i) * 12;
|
| 455 |
+
ctx.save(); ctx.translate(x, y + bob); ctx.scale(a, a); ctx.globalAlpha = clamp(a);
|
| 456 |
+
glow(ctx, 0, 0, r * 1.8, col, .45);
|
| 457 |
+
face(ctx, 0, 0, r, col, kind, T);
|
| 458 |
+
ctx.restore();
|
| 459 |
+
text(ctx, '$' + nm, x + (P ? (i === 1 ? -190 : 190) : 0), y + bob + (P ? 0 : r + 50), { size: P ? 50 : 54, fam: F_GR, weight: 700, color: WHITE, align: 'center', alpha: clamp(a), stroke: 6, strokeColor: 'rgba(0,0,0,.6)' });
|
| 460 |
+
});
|
| 461 |
+
const aa = win(t, s.at(3, '数十亿'), .4) * aM;
|
| 462 |
+
gradText(ctx, '市值 数十亿美元', st.cx, P ? st.y + st.h - 20 : st.y + st.h - 50, { size: P ? 60 : 66, align: 'center', alpha: aa, c1: GOLD, c2: GREEN });
|
| 463 |
+
}
|
| 464 |
+
// c4: DEX volume race (illustrative)
|
| 465 |
+
const aV = win(t, c4 - .1, .4);
|
| 466 |
+
if (aV > 0) {
|
| 467 |
+
const p = inv(c4, s.end(4) + .3, t);
|
| 468 |
+
const sol = .45 + .5 * eIO(p), eth = .7 + .05 * Math.sin(p * 6);
|
| 469 |
+
const bars = [['Solana DEX', sol, GREEN], ['Ethereum DEX', eth, '#8c9cff']];
|
| 470 |
+
const bx = P ? st.x + 30 : st.cx - 640, bwMax = P ? st.w - 60 : 1280, by = st.cy - 80;
|
| 471 |
+
bars.forEach(([nm, v, col], i) => {
|
| 472 |
+
const y = by + i * 150;
|
| 473 |
+
ctx.save(); ctx.globalAlpha = aV;
|
| 474 |
+
text(ctx, nm, bx, y - 50, { size: 36, fam: F_GR, weight: 700, color: col });
|
| 475 |
+
rr(ctx, bx, y - 22, bwMax, 56, 28); ctx.fillStyle = 'rgba(255,255,255,.06)'; ctx.fill();
|
| 476 |
+
rr(ctx, bx, y - 22, bwMax * v, 56, 28); ctx.fillStyle = col; ctx.fill();
|
| 477 |
+
ctx.restore();
|
| 478 |
+
if (i === 0) glow(ctx, bx + bwMax * v, y + 6, 90, GREEN, .6 * aV);
|
| 479 |
+
});
|
| 480 |
+
if (sol > eth) stamp(ctx, '反超!', P ? st.cx : st.cx + 420, by - 170, t, c4 + (s.end(4) + .3 - c4) * .62, { size: P ? 70 : 80, color: GREEN, rot: -.1 });
|
| 481 |
+
text(ctx, '交易量对比 · 示意', st.cx, by + 260, { size: 26, weight: 700, color: 'rgba(255,255,255,.5)', align: 'center', alpha: aV });
|
| 482 |
+
}
|
| 483 |
+
},
|
| 484 |
+
sfx(s) {
|
| 485 |
+
const ev = [{ t: .2, k: 'party', v: 1 }, { t: s.cue(1), k: 'whoosh', v: .5 }];
|
| 486 |
+
for (let k = 0; k < 9; k++) ev.push({ t: s.cue(1) + k * .25, k: 'coin', v: .45, p: k });
|
| 487 |
+
ev.push({ t: s.cue(2), k: 'machine', d: s.cue(3) - s.cue(2) });
|
| 488 |
+
MEMES.forEach(([nm]) => ev.push({ t: s.at(3, nm), k: 'boing' }));
|
| 489 |
+
ev.push({ t: s.cue(4), k: 'climb', d: s.end(4) - s.cue(4) }, { t: s.cue(4) + (s.end(4) + .3 - s.cue(4)) * .62, k: 'stamp', v: 1 });
|
| 490 |
+
return ev;
|
| 491 |
+
},
|
| 492 |
+
};
|
| 493 |
+
function face(ctx, x, y, r, col, kind, T) {
|
| 494 |
+
ctx.fillStyle = col; ctx.beginPath(); ctx.arc(x, y, r, 0, 7); ctx.fill();
|
| 495 |
+
if (kind === 'cat') { ctx.beginPath(); ctx.moveTo(x - r * .9, y - r * .3); ctx.lineTo(x - r * .7, y - r * 1.1); ctx.lineTo(x - r * .25, y - r * .85); ctx.moveTo(x + r * .9, y - r * .3); ctx.lineTo(x + r * .7, y - r * 1.1); ctx.lineTo(x + r * .25, y - r * .85); ctx.fill(); }
|
| 496 |
+
else { ctx.beginPath(); ctx.ellipse(x - r * .85, y - r * .2, r * .25, r * .5, .3, 0, 7); ctx.ellipse(x + r * .85, y - r * .2, r * .25, r * .5, -.3, 0, 7); ctx.fill(); }
|
| 497 |
+
ctx.fillStyle = '#1a1020'; ctx.beginPath(); ctx.arc(x - r * .35, y - r * .15, r * .1, 0, 7); ctx.arc(x + r * .35, y - r * .15, r * .1, 0, 7); ctx.fill();
|
| 498 |
+
if (kind === 'cat') { const o = (Math.sin(T * 8) > 0) ? 1 : .2; ctx.beginPath(); ctx.ellipse(x, y + r * .35, r * .22, r * .3 * o + r * .04, 0, 0, 7); ctx.fill(); }
|
| 499 |
+
else { ctx.beginPath(); ctx.ellipse(x, y + r * .12, r * .15, r * .1, 0, 0, 7); ctx.fill(); ctx.strokeStyle = '#1a1020'; ctx.lineWidth = r * .05; ctx.beginPath(); ctx.arc(x, y + r * .3, r * .25, .2, Math.PI - .2); ctx.stroke(); }
|
| 500 |
+
if (kind === 'hat') { ctx.fillStyle = '#ff9fc8'; ctx.beginPath(); ctx.ellipse(x, y - r * .78, r * .62, r * .28, 0, 0, 7); ctx.fill(); ctx.beginPath(); ctx.ellipse(x, y - r * .7, r * .9, r * .14, 0, 0, 7); ctx.fill(); }
|
| 501 |
+
if (kind === 'dog') { ctx.fillStyle = '#fff3dd'; ctx.beginPath(); ctx.ellipse(x, y + r * .4, r * .5, r * .35, 0, 0, 7); ctx.globalAlpha *= .6; ctx.fill(); ctx.globalAlpha /= .6; }
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
// ---------------------------------------------------------------- 12. ATH 2025
|
| 505 |
+
const ath = {
|
| 506 |
+
draw(ctx, L, s) {
|
| 507 |
+
const { t, T } = s, st = L.stage, P = L.portrait;
|
| 508 |
+
const c1 = s.cue(1), c2 = s.cue(2);
|
| 509 |
+
const rect = P ? { x: st.x + 90, y: st.y + 360, w: st.w - 120, h: st.h - 420 } : { x: st.x + 120, y: st.y + 40, w: st.w - 160, h: st.h - 100 };
|
| 510 |
+
const prog = eIO(inv(.3, c1 + .4, t));
|
| 511 |
+
const dim = 1 - .55 * win(t, c1 + .4, .5);
|
| 512 |
+
const head = drawChart(ctx, rect, { from: idx('2024-01-01'), to: idx('2025-01-19'), prog, alpha: win(t, 0, .5) * dim, yMin: 50, yMax: 290, gridVals: [100, 150, 200, 250] });
|
| 513 |
+
// fireworks
|
| 514 |
+
if (t > c1 + .3) {
|
| 515 |
+
for (let k = 0; k < 14; k++) {
|
| 516 |
+
const t0 = c1 + .3 + k * .42, d = t - t0; if (d < 0) continue;
|
| 517 |
+
const x = st.x + hash1(k * 7) * st.w, y = st.y + 60 + hash1(k * 9) * st.h * .55;
|
| 518 |
+
const up = inv(0, .35, d); if (up < 1) { ctx.save(); ctx.fillStyle = GOLD; ctx.globalAlpha = .8; ctx.beginPath(); ctx.arc(x, L.H - (L.H - y) * eOut(up), 4, 0, 7); ctx.fill(); ctx.restore(); continue; }
|
| 519 |
+
burst(ctx, x, y, d - .35, { r: 200 + hash1(k) * 140, n: 44, seed: k + 20, color: [GREEN, PINK, GOLD, TEAL][k % 4], dur: 1.5, size: 5 });
|
| 520 |
+
}
|
| 521 |
+
}
|
| 522 |
+
const aN = win(t, c1 + .4, .5, eBack);
|
| 523 |
+
if (aN > 0) {
|
| 524 |
+
const v = 262.09 * eOut(inv(c1 + .4, c1 + 1.6, t));
|
| 525 |
+
const y = P ? st.y + 160 : st.cy - 60;
|
| 526 |
+
glow(ctx, st.cx, y, 520, GREEN, .45 * clamp(aN));
|
| 527 |
+
ctx.save(); ctx.translate(st.cx, y); ctx.scale(clamp(aN, 0, 1.2), clamp(aN, 0, 1.2));
|
| 528 |
+
bigNum(ctx, '$' + v.toFixed(2), 0, 0, P ? 170 : 220, { c1: '#b3ffdf', c2: GREEN });
|
| 529 |
+
ctx.restore();
|
| 530 |
+
pill(ctx, 'ATH · 2025.01.19 日收盘', st.cx, y + (P ? 120 : 150), { size: P ? 32 : 36, alpha: clamp(aN), color: GOLD });
|
| 531 |
+
}
|
| 532 |
+
const a2 = win(t, c2, .5);
|
| 533 |
+
if (a2 > 0) {
|
| 534 |
+
const y = P ? st.y + st.h - 20 : st.cy + 250;
|
| 535 |
+
text(ctx, '$0.74', st.cx - (P ? 230 : 300), y, { size: P ? 60 : 70, fam: F_GR, weight: 700, color: 'rgba(255,255,255,.75)', align: 'center', alpha: a2 });
|
| 536 |
+
text(ctx, '→', st.cx - (P ? 90 : 120), y, { size: 60, fam: F_GR, weight: 700, color: WHITE, align: 'center', alpha: a2 });
|
| 537 |
+
text(ctx, '$262', st.cx + (P ? 30 : 40), y, { size: P ? 60 : 70, fam: F_GR, weight: 700, color: GREEN, align: 'center', alpha: a2 });
|
| 538 |
+
stamp(ctx, '×350', st.cx + (P ? 230 : 330), y - 10, t, c2 + .4, { size: P ? 70 : 84, color: GOLD, fam: F_GR, rot: -.1 });
|
| 539 |
+
text(ctx, '2020.04.10 首个日线收盘 → 2025.01.19', st.cx, y + (P ? 70 : 80), { size: 26, weight: 600, color: 'rgba(255,255,255,.55)', align: 'center', alpha: a2 });
|
| 540 |
+
}
|
| 541 |
+
void head;
|
| 542 |
+
},
|
| 543 |
+
sfx(s) {
|
| 544 |
+
const ev = [{ t: .3, k: 'climb', d: s.cue(1) + .1 }, { t: s.cue(1) + .4, k: 'impact', v: 1 }, { t: s.cue(1) + .45, k: 'cheer', d: 4 }];
|
| 545 |
+
for (let k = 0; k < 14; k++) ev.push({ t: s.cue(1) + .3 + k * .42 + .35, k: 'firework', v: .55, p: k });
|
| 546 |
+
ev.push({ t: s.cue(2) + .4, k: 'stamp', v: .9 });
|
| 547 |
+
return ev;
|
| 548 |
+
},
|
| 549 |
+
};
|
| 550 |
+
|
| 551 |
+
// ---------------------------------------------------------------- 13. WALL ST 2025
|
| 552 |
+
const wall = {
|
| 553 |
+
draw(ctx, L, s) {
|
| 554 |
+
const { t, T } = s, st = L.stage, P = L.portrait;
|
| 555 |
+
const c1 = s.cue(1), c2 = s.cue(2), c3 = s.cue(3);
|
| 556 |
+
const out = win(t, c3 - .2, .6);
|
| 557 |
+
// exchange facade
|
| 558 |
+
const aF = win(t, 0, .8) * (1 - out);
|
| 559 |
+
if (aF > 0) {
|
| 560 |
+
const fx = st.cx, fb = st.y + st.h - (P ? 30 : 10), fw = P ? 760 : 980, fh = P ? 520 : 560;
|
| 561 |
+
ctx.save(); ctx.globalAlpha = aF;
|
| 562 |
+
ctx.fillStyle = '#1c2236'; ctx.beginPath(); ctx.moveTo(fx - fw / 2 - 30, fb - fh + 110); ctx.lineTo(fx, fb - fh); ctx.lineTo(fx + fw / 2 + 30, fb - fh + 110); ctx.closePath(); ctx.fill();
|
| 563 |
+
ctx.strokeStyle = hexA(GOLD, .7); ctx.lineWidth = 4; ctx.stroke();
|
| 564 |
+
ctx.fillStyle = '#232a42'; ctx.fillRect(fx - fw / 2 - 20, fb - fh + 110, fw + 40, 40);
|
| 565 |
+
text(ctx, 'WALL STREET', fx, fb - fh + 131, { size: 28, fam: F_GR, weight: 700, color: GOLD, align: 'center', ls: 8 });
|
| 566 |
+
const n = P ? 6 : 8;
|
| 567 |
+
for (let i = 0; i < n; i++) { const x = fx - fw / 2 + (i + .5) * fw / n; const g = ctx.createLinearGradient(x - 22, 0, x + 22, 0); g.addColorStop(0, '#2e3654'); g.addColorStop(.5, '#59628a'); g.addColorStop(1, '#2e3654'); ctx.fillStyle = g; ctx.fillRect(x - 22, fb - fh + 150, 44, fh - 190); }
|
| 568 |
+
ctx.fillStyle = '#1c2236'; ctx.fillRect(fx - fw / 2 - 40, fb - 40, fw + 80, 40);
|
| 569 |
+
ctx.restore();
|
| 570 |
+
// door light (c2)
|
| 571 |
+
const dl = win(t, s.at(2, '大门') - .3, .8);
|
| 572 |
+
if (dl > 0) { ctx.save(); ctx.globalCompositeOperation = 'lighter'; const g = ctx.createLinearGradient(0, fb - 260, 0, L.H); g.addColorStop(0, hexA(GREEN, .55 * dl)); g.addColorStop(1, hexA(PURPLE, 0)); ctx.fillStyle = g; ctx.beginPath(); ctx.moveTo(fx - 50, fb - 220); ctx.lineTo(fx + 50, fb - 220); ctx.lineTo(fx + 300 * dl, L.H); ctx.lineTo(fx - 300 * dl, L.H); ctx.fill(); ctx.restore(); glow(ctx, fx, fb - 160, 200, GREEN, dl * .7); }
|
| 573 |
+
}
|
| 574 |
+
// c1: balance sheet
|
| 575 |
+
const aB = win(t, c1 - .1, .5) * (1 - win(t, c2 - .1, .4));
|
| 576 |
+
if (aB > 0) {
|
| 577 |
+
const w = P ? 640 : 620, h = 420, x = st.cx - w / 2, y = st.cy - h / 2 - (P ? 40 : 30);
|
| 578 |
+
ctx.save(); ctx.globalAlpha = aB; ctx.translate(0, (1 - aB) * 60);
|
| 579 |
+
rr(ctx, x, y, w, h, 18); ctx.fillStyle = '#f5f2ea'; ctx.fill();
|
| 580 |
+
text(ctx, '资产负债表 · 资产', x + 40, y + 50, { size: 36, weight: 900, color: '#1a1030' });
|
| 581 |
+
ctx.fillStyle = '#c9c2b0'; ctx.fillRect(x + 40, y + 84, w - 80, 3);
|
| 582 |
+
const rows = [['现金及等价物', '$ ███'], ['短期投资', '$ ███'], ['数字资产 · SOL', '◎ ████']];
|
| 583 |
+
rows.forEach(([a, b], i) => {
|
| 584 |
+
const ra = i < 2 ? 1 : win(t, s.at(1, 'SOL'), .4);
|
| 585 |
+
const yy = y + 140 + i * 80;
|
| 586 |
+
if (i === 2 && ra > 0) { ctx.globalAlpha = aB * ra; rr(ctx, x + 24, yy - 34, w - 48, 68, 12); ctx.fillStyle = hexA(GREEN, .25); ctx.fill(); }
|
| 587 |
+
text(ctx, a, x + 40, yy, { size: 32, weight: 700, color: '#2a2040', alpha: ra });
|
| 588 |
+
text(ctx, b, x + w - 40, yy, { size: 32, fam: F_MONO, weight: 700, color: i === 2 ? '#0a7a50' : '#2a2040', align: 'right', alpha: ra });
|
| 589 |
+
ctx.globalAlpha = aB;
|
| 590 |
+
});
|
| 591 |
+
ctx.restore();
|
| 592 |
+
stamp(ctx, '上市公司', st.cx + (P ? 180 : 260), st.cy - h / 2 - (P ? 40 : 20), t, c1 + .5, { size: 54, color: PURPLE, rot: .12 });
|
| 593 |
+
}
|
| 594 |
+
// c2: ETF board
|
| 595 |
+
const aE = win(t, c2 - .1, .5) * (1 - out);
|
| 596 |
+
if (aE > 0) {
|
| 597 |
+
const y = P ? st.y + 120 : st.y + 70, w = P ? 780 : 1000;
|
| 598 |
+
ctx.save(); ctx.globalAlpha = aE;
|
| 599 |
+
rr(ctx, st.cx - w / 2, y - 70, w, 140, 16); ctx.fillStyle = '#05070c'; ctx.fill(); ctx.strokeStyle = GOLD; ctx.lineWidth = 4; ctx.stroke();
|
| 600 |
+
ctx.restore();
|
| 601 |
+
const msg = '现货 SOLANA ETF';
|
| 602 |
+
const flip = inv(c2, c2 + 1.2, t);
|
| 603 |
+
const shown = Array.from(msg).map((ch, i) => (flip * (msg.length + 4) - i > 1 ? ch : '▒')).join('');
|
| 604 |
+
text(ctx, shown, st.cx, y + 2, { size: P ? 64 : 76, weight: 900, color: GOLD, align: 'center', alpha: aE, ls: 4, shadow: 20, shadowColor: GOLD });
|
| 605 |
+
pill(ctx, '2025 年 10 月底 · 美国首批上市交易', st.cx, y + 115, { size: 28, alpha: win(t, c2 + 1, .4) * aE, color: GREEN });
|
| 606 |
+
}
|
| 607 |
+
// c3: Alpenglow
|
| 608 |
+
const aA = win(t, c3 - .1, .7);
|
| 609 |
+
if (aA > 0) {
|
| 610 |
+
const base = st.y + st.h + (P ? -60 : 40);
|
| 611 |
+
ctx.save(); ctx.globalAlpha = aA;
|
| 612 |
+
const sky = ctx.createLinearGradient(0, 0, 0, L.H); sky.addColorStop(0, 'rgba(30,10,50,.85)'); sky.addColorStop(1, 'rgba(255,110,140,.45)'); ctx.fillStyle = sky; ctx.fillRect(0, 0, L.W, L.H);
|
| 613 |
+
for (let layer = 0; layer < 3; layer++) {
|
| 614 |
+
ctx.beginPath(); ctx.moveTo(0, L.H);
|
| 615 |
+
const r = rng(30 + layer), peaks = 7 + layer * 2;
|
| 616 |
+
let x = -60; ctx.lineTo(x, base - 100);
|
| 617 |
+
for (let i = 0; i <= peaks; i++) { x = (i + .5) / peaks * (L.W + 120) - 60; const h = (360 - layer * 90) * (.5 + r() * .7); ctx.lineTo(x - 50, base - h * .55); ctx.lineTo(x, base - h); }
|
| 618 |
+
ctx.lineTo(L.W + 60, base - 80); ctx.lineTo(L.W, L.H); ctx.closePath();
|
| 619 |
+
const g = ctx.createLinearGradient(0, base - 400, 0, base); g.addColorStop(0, ['#ff7aa8', '#c04a8a', '#3a1a4a'][layer]); g.addColorStop(1, ['#4a1a5a', '#2a0f3a', '#120618'][layer]);
|
| 620 |
+
ctx.fillStyle = g; ctx.fill();
|
| 621 |
+
}
|
| 622 |
+
ctx.restore();
|
| 623 |
+
glow(ctx, L.W * .7, base - 400, 500, '#ff7aa8', .4 * aA);
|
| 624 |
+
const ty = P ? st.y + 110 : st.y + 60;
|
| 625 |
+
gradText(ctx, 'Alpenglow', st.cx, ty, { size: P ? 100 : 120, fam: F_GR, weight: 700, align: 'center', alpha: aA, c1: '#ffd1e0', c2: GREEN });
|
| 626 |
+
text(ctx, '新共识 · 2025 年获验证者投票通过', st.cx, ty + (P ? 80 : 90), { size: P ? 34 : 38, weight: 800, color: WHITE, align: 'center', alpha: aA });
|
| 627 |
+
// finality bars
|
| 628 |
+
const p = eIO(inv(s.at(3, '目标'), s.end(3), t));
|
| 629 |
+
const bx = P ? st.x + 30 : st.cx - 600, bw = P ? st.w - 60 : 1200, by = ty + (P ? 250 : 230);
|
| 630 |
+
const rows = [['当前最终确认 ≈ 12.8 秒', 1, '#c0c6dc'], ['Alpenglow 目标 ≈ 150 毫秒', lerp(1, .012, p), GREEN]];
|
| 631 |
+
rows.forEach(([lab, v, col], i) => {
|
| 632 |
+
const y = by + i * 130;
|
| 633 |
+
ctx.save(); ctx.globalAlpha = aA * win(t, c3 + .5 + i * .4, .4);
|
| 634 |
+
text(ctx, lab, bx, y - 40, { size: P ? 32 : 34, weight: 800, color: col });
|
| 635 |
+
rr(ctx, bx, y - 12, Math.max(14, bw * v), 40, 20); ctx.fillStyle = col; ctx.fill();
|
| 636 |
+
ctx.restore();
|
| 637 |
+
});
|
| 638 |
+
if (p > .9) { glow(ctx, bx + 14, by + 138, 80, GREEN, .8 * aA); text(ctx, '≈ 85× 更快', bx + 60, by + 138, { size: 34, weight: 900, color: GREEN, alpha: win(t, s.end(3) - .3, .4) }); }
|
| 639 |
+
}
|
| 640 |
+
void T;
|
| 641 |
+
},
|
| 642 |
+
sfx(s) {
|
| 643 |
+
return [
|
| 644 |
+
{ t: 0, k: 'bell', v: .5 }, { t: s.cue(1), k: 'paper' }, { t: s.at(1, 'SOL'), k: 'pop' }, { t: s.cue(1) + .5, k: 'stamp', v: .8 },
|
| 645 |
+
{ t: s.cue(2), k: 'flipboard', d: 1.2 }, { t: s.at(2, '大门') - .3, k: 'bell', v: 1 }, { t: s.at(2, '大门'), k: 'shimmer' },
|
| 646 |
+
{ t: s.cue(3) - .1, k: 'riser', v: .5 }, { t: s.at(3, '目标'), k: 'zip', d: s.end(3) - s.at(3, '目标') }, { t: s.end(3), k: 'chime' },
|
| 647 |
+
];
|
| 648 |
+
},
|
| 649 |
+
};
|
| 650 |
+
|
| 651 |
+
// ---------------------------------------------------------------- 14. NOW — the whole ride
|
| 652 |
+
const MARKS = [['2020-04-10', '起点'], ['2021-11-07', '2021 高点'], ['2022-12-30', 'FTX 后低点'], ['2023-12-31', '重回 $100'], ['2025-01-19', '历史新高'], [null, '今天']];
|
| 653 |
+
const now = {
|
| 654 |
+
draw(ctx, L, s) {
|
| 655 |
+
const { t } = s, st = L.stage, P = L.portrait;
|
| 656 |
+
const c1 = s.cue(1), c2 = s.cue(2), c3 = s.cue(3);
|
| 657 |
+
const rect = P ? { x: st.x + 90, y: st.y + 160, w: st.w - 130, h: st.h - 260 } : { x: st.x + 120, y: st.y + 40, w: st.w - 200, h: st.h - 110 };
|
| 658 |
+
const prog = eIO(inv(.3, s.end(1) + .2, t));
|
| 659 |
+
const head = drawChart(ctx, rect, { from: 0, to: LAST, prog, log: true, yMin: .4, yMax: 420, gridVals: [1, 10, 100], headLabel: t > s.end(1), lw: 5 });
|
| 660 |
+
// milestone markers
|
| 661 |
+
MARKS.forEach(([d, lab], i) => {
|
| 662 |
+
const di = d ? idx(d) : LAST;
|
| 663 |
+
if (head.i < di) return;
|
| 664 |
+
const x = head.X(di), y = head.Y(price(di));
|
| 665 |
+
const a = win(t, .3 + (s.end(1) - .1) * (di / LAST), .35, eBack) * (1 - .5 * win(t, c2, .5));
|
| 666 |
+
const up = i % 2 === 0 ? -1 : 1, off = (P ? 70 : 80) * (i === 2 ? 1 : up === -1 ? -1 : -1);
|
| 667 |
+
glow(ctx, x, y, 40, GOLD, .7 * clamp(a));
|
| 668 |
+
ctx.save(); ctx.globalAlpha = clamp(a); ctx.fillStyle = GOLD; ctx.beginPath(); ctx.arc(x, y, 8, 0, 7); ctx.fill(); ctx.restore();
|
| 669 |
+
if (i === 5) return;
|
| 670 |
+
const yy = y + off - (i === 4 ? 30 : 0);
|
| 671 |
+
text(ctx, fmtUSD(price(di)), x, yy - 14, { size: P ? 24 : 28, fam: F_MONO, weight: 800, color: WHITE, align: 'center', alpha: clamp(a), stroke: 5, strokeColor: 'rgba(0,0,0,.7)' });
|
| 672 |
+
text(ctx, lab, x, yy + 16, { size: P ? 20 : 22, weight: 700, color: GOLD, align: 'center', alpha: clamp(a), stroke: 4, strokeColor: 'rgba(0,0,0,.7)' });
|
| 673 |
+
});
|
| 674 |
+
// rollercoaster cart riding the head
|
| 675 |
+
if (prog > 0 && prog < 1) {
|
| 676 |
+
const ang = Math.atan(head.slope);
|
| 677 |
+
ctx.save(); ctx.translate(head.x, head.y); ctx.rotate(ang); ctx.translate(0, -26);
|
| 678 |
+
rr(ctx, -34, -18, 68, 30, 8); ctx.fillStyle = PINK; ctx.fill(); ctx.strokeStyle = '#fff'; ctx.lineWidth = 3; ctx.stroke();
|
| 679 |
+
ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(-18, 14, 7, 0, 7); ctx.arc(18, 14, 7, 0, 7); ctx.fill();
|
| 680 |
+
ctx.fillStyle = '#1a1030'; ctx.beginPath(); ctx.arc(-8, -26, 9, 0, 7); ctx.arc(12, -26, 9, 0, 7); ctx.fill();
|
| 681 |
+
ctx.restore();
|
| 682 |
+
}
|
| 683 |
+
// c2: 2026 range band
|
| 684 |
+
const aR = win(t, c2, .5);
|
| 685 |
+
if (aR > 0) {
|
| 686 |
+
const x0 = head.X(idx('2026-01-01')), x1 = head.X(LAST), y0 = head.Y(146.51), y1 = head.Y(62.18);
|
| 687 |
+
ctx.save(); ctx.globalAlpha = aR;
|
| 688 |
+
ctx.fillStyle = hexA(TEAL, .16); ctx.fillRect(x0, y0, x1 - x0 + 10, y1 - y0);
|
| 689 |
+
ctx.strokeStyle = TEAL; ctx.setLineDash([8, 6]); ctx.lineWidth = 2; ctx.strokeRect(x0, y0, x1 - x0 + 10, y1 - y0); ctx.setLineDash([]);
|
| 690 |
+
ctx.restore();
|
| 691 |
+
text(ctx, '2026 震荡区间 $62 – $147', x0 - 16, y1 + 34, { size: P ? 24 : 28, weight: 800, color: TEAL, align: 'right', alpha: aR, stroke: 5, strokeColor: 'rgba(0,0,0,.7)' });
|
| 692 |
+
}
|
| 693 |
+
const aN = win(t, c3, .5);
|
| 694 |
+
if (aN > 0) {
|
| 695 |
+
const y = P ? st.y + 40 : st.y + 10;
|
| 696 |
+
ctx.save(); ctx.globalAlpha = aN; glass(ctx, st.cx - (P ? 360 : 380), y - 50, P ? 720 : 760, 100, {}); ctx.restore();
|
| 697 |
+
text(ctx, 'SOL ≈ $117 · 2026.10.07', st.cx, y + 2, { size: P ? 46 : 52, fam: F_GR, weight: 700, color: WHITE, align: 'center', alpha: aN });
|
| 698 |
+
}
|
| 699 |
+
text(ctx, '数据:Surf · SOL/USD 日收盘价(对数坐标)', rect.x + rect.w, rect.y + rect.h + (P ? 60 : 56), { size: 20, weight: 500, color: 'rgba(255,255,255,.45)', align: 'right', alpha: win(t, .3, .5) });
|
| 700 |
+
},
|
| 701 |
+
sfx(s) {
|
| 702 |
+
const ev = [{ t: .3, k: 'coaster', d: s.end(1) - .1 }];
|
| 703 |
+
MARKS.forEach(([d]) => { const di = d ? idx(d) : LAST; ev.push({ t: .3 + (s.end(1) - .1) * (di / LAST), k: 'blip', v: .6, p: 3 }); });
|
| 704 |
+
ev.push({ t: s.cue(2), k: 'whoosh', v: .4 }, { t: s.cue(3), k: 'chime' });
|
| 705 |
+
return ev;
|
| 706 |
+
},
|
| 707 |
+
};
|
| 708 |
+
|
| 709 |
+
// ---------------------------------------------------------------- 15. OUTRO
|
| 710 |
+
const outro = {
|
| 711 |
+
draw(ctx, L, s) {
|
| 712 |
+
const { t, T } = s, st = L.stage, P = L.portrait;
|
| 713 |
+
const c1 = s.cue(1), c2 = s.cue(2), c3 = s.cue(3), c4 = s.cue(4);
|
| 714 |
+
const cx = st.cx, cy = P ? st.cy - 120 : st.cy - 20;
|
| 715 |
+
const R = P ? 330 : 300;
|
| 716 |
+
const aC = win(t, 0, 1) * (1 - .65 * win(t, c2 - .2, .5) * (1 - win(t, c3, .6)));
|
| 717 |
+
// chain ring of hashes around the clock
|
| 718 |
+
const beads = 36;
|
| 719 |
+
ctx.save(); ctx.globalAlpha = aC;
|
| 720 |
+
for (let i = 0; i < beads; i++) {
|
| 721 |
+
const lit = (t / .4) % beads;
|
| 722 |
+
const a = (i / beads) * Math.PI * 2 - Math.PI / 2 + t * .05;
|
| 723 |
+
const x = cx + Math.cos(a) * R * 1.18, y = cy + Math.sin(a) * R * 1.18;
|
| 724 |
+
const on = ((lit - i + beads) % beads) < 8;
|
| 725 |
+
ctx.fillStyle = on ? GREEN : hexA(PURPLE, .5); ctx.beginPath(); ctx.arc(x, y, on ? 9 : 6, 0, 7); ctx.fill();
|
| 726 |
+
if (on) glow(ctx, x, y, 26, GREEN, .5);
|
| 727 |
+
}
|
| 728 |
+
ctx.restore();
|
| 729 |
+
dial(ctx, cx, cy, R, { alpha: aC, hand: Math.floor(t / .4) / 60, minute: t / 600, lw: 4 });
|
| 730 |
+
glow(ctx, cx, cy, R * 1.3, PURPLE, .35 * aC);
|
| 731 |
+
solLogo(ctx, cx, cy, R * .8, eOut(inv(.2, 1.6, t)), aC);
|
| 732 |
+
// c0/c1 quote lines
|
| 733 |
+
const q = [['第一个发明', '证明时间', 0, .1], ['被嘲笑 · 宕机 · 被判死刑', '又一次次站起来', 1, .1]];
|
| 734 |
+
q.forEach(([a1, a2, li]) => {
|
| 735 |
+
const a = win(t, s.cue(li) + .1, .5) * (1 - win(t, s.end(li) + .1, .3));
|
| 736 |
+
if (a <= 0) return;
|
| 737 |
+
const y = P ? cy + R + 200 : cy + R + 50;
|
| 738 |
+
text(ctx, a1, cx, y - 30, { size: P ? 40 : 38, weight: 700, color: 'rgba(255,255,255,.8)', align: 'center', alpha: a });
|
| 739 |
+
gradText(ctx, a2, cx, y + 36, { size: P ? 64 : 58, align: 'center', alpha: a });
|
| 740 |
+
});
|
| 741 |
+
// c2: the line
|
| 742 |
+
const aT = win(t, c2 - .1, .6) * (1 - win(t, c3 + .2, .5));
|
| 743 |
+
if (aT > 0) {
|
| 744 |
+
ctx.save(); ctx.fillStyle = `rgba(3,2,10,${.5 * aT})`; ctx.fillRect(0, 0, L.W, L.H); ctx.restore();
|
| 745 |
+
const y1 = P ? st.cy - 160 : st.cy - 90, y2 = P ? st.cy + 60 : st.cy + 90;
|
| 746 |
+
glowText(ctx, '速度,可以靠工程;', cx, y1, { size: P ? 80 : 104, weight: 900, color: WHITE, align: 'center', alpha: win(t, c2, .5), glowColor: PURPLE });
|
| 747 |
+
const a2 = win(t, s.at(2, '信任'), .5);
|
| 748 |
+
glow(ctx, cx, y2, 500, GREEN, .35 * a2 * aT);
|
| 749 |
+
gradText(ctx, '信任,只能靠时间。', cx, y2, { size: P ? 92 : 120, align: 'center', alpha: a2 * aT, c1: GOLD, c2: GREEN });
|
| 750 |
+
}
|
| 751 |
+
// c3: chain keeps writing
|
| 752 |
+
const a3 = win(t, c3, .5);
|
| 753 |
+
if (a3 > 0) {
|
| 754 |
+
const y = P ? cy + R + 170 : cy + R + 50;
|
| 755 |
+
const slot = Math.floor((T) / .4) + 420000000;
|
| 756 |
+
text(ctx, 'SLOT ' + slot.toLocaleString('en-US'), cx, y, { size: P ? 50 : 54, fam: F_MONO, weight: 800, color: GREEN, align: 'center', alpha: a3 * (1 - win(t, c4 - .2, .4)), shadow: 20, shadowColor: GREEN });
|
| 757 |
+
text(ctx, '每 400 毫秒 · 新的一页', cx, y + 64, { size: 34, weight: 700, color: 'rgba(255,255,255,.75)', align: 'center', alpha: a3 * (1 - win(t, c4 - .2, .4)) });
|
| 758 |
+
}
|
| 759 |
+
// c4: CTA
|
| 760 |
+
const a4 = win(t, c4 - .1, .5);
|
| 761 |
+
if (a4 > 0) {
|
| 762 |
+
const y = P ? cy + R + 170 : cy + R + 60;
|
| 763 |
+
const icons = [['赞', PINK], ['币', GOLD], ['藏', TEAL], ['关注', GREEN]];
|
| 764 |
+
icons.forEach(([lab, col], i) => {
|
| 765 |
+
const a = win(t, c4 + .2 + i * .25, .35, eBack); if (a <= 0) return;
|
| 766 |
+
const x = cx + (i - 1.5) * (P ? 170 : 190);
|
| 767 |
+
ctx.save(); ctx.translate(x, y); ctx.scale(a, a);
|
| 768 |
+
glow(ctx, 0, 0, 90, col, .5);
|
| 769 |
+
ctx.fillStyle = 'rgba(14,10,32,.9)'; ctx.beginPath(); ctx.arc(0, 0, 58, 0, 7); ctx.fill(); ctx.strokeStyle = col; ctx.lineWidth = 5; ctx.stroke();
|
| 770 |
+
text(ctx, lab, 0, 2, { size: lab.length > 1 ? 32 : 44, weight: 900, color: col, align: 'center' });
|
| 771 |
+
ctx.restore();
|
| 772 |
+
});
|
| 773 |
+
text(ctx, '仅为科普回顾 · 不构成任何投资建议', cx, y + 110, { size: 28, weight: 700, color: 'rgba(255,255,255,.6)', align: 'center', alpha: a4 });
|
| 774 |
+
text(ctx, 'SOLANA · 2017 → ∞', cx, P ? st.y + 20 : st.y - 30, { size: P ? 44 : 44, fam: F_GR, weight: 700, color: 'rgba(255,255,255,.85)', align: 'center', alpha: a4, ls: 8 });
|
| 775 |
+
}
|
| 776 |
+
},
|
| 777 |
+
sfx(s) {
|
| 778 |
+
const ev = [{ t: .1, k: 'shimmer' }];
|
| 779 |
+
for (let k = 1; k * .4 < 2.4; k++) ev.push({ t: k * .4, k: 'tick', v: .35 });
|
| 780 |
+
ev.push({ t: s.cue(2) - .1, k: 'impact', v: .6 }, { t: s.at(2, '信任'), k: 'shimmer' });
|
| 781 |
+
ev.push({ t: s.cue(3), k: 'hashtick', v: .35, p: 1 });
|
| 782 |
+
for (let i = 0; i < 4; i++) ev.push({ t: s.cue(4) + .2 + i * .25, k: 'pop', v: .6 });
|
| 783 |
+
ev.push({ t: s.d - 2.6, k: 'tick', v: .8 }, { t: s.d - 2.2, k: 'tick', v: .8 }, { t: s.d - 1.8, k: 'tick', v: 1 });
|
| 784 |
+
return ev;
|
| 785 |
+
},
|
| 786 |
+
};
|
| 787 |
+
|
| 788 |
+
export const SCENES_B = { outage, ftx, rebuild, meme, ath, wall, now, outro };
|
solana-clock/js/scenes.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { SCENES_A } from './scenes-a.js';
|
| 2 |
+
import { SCENES_B } from './scenes-b.js';
|
| 3 |
+
export const SCENES = { ...SCENES_A, ...SCENES_B };
|
solana-clock/js/timeline.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Narration-driven timeline + layouts
|
| 2 |
+
import { SCRIPT } from './script-data.js';
|
| 3 |
+
|
| 4 |
+
const LEAD = { hook: 2.6, outro: 1.0 }; // silence before first line (s)
|
| 5 |
+
const TAIL = { hook: 1.6, outro: 4.5, ftx: 1.8, ath: 1.8 };
|
| 6 |
+
const GAP = { default: .42 };
|
| 7 |
+
|
| 8 |
+
export function buildTimeline() {
|
| 9 |
+
let T = 0;
|
| 10 |
+
const scenes = SCRIPT.map((sc, si) => {
|
| 11 |
+
const start = T;
|
| 12 |
+
let t = LEAD[sc.id] ?? 1.0;
|
| 13 |
+
const lines = sc.lines.map((ln, i) => {
|
| 14 |
+
const l = { ...ln, i, scene: sc.id, local: t, start: start + t, end: start + t + ln.dur };
|
| 15 |
+
t += ln.dur + (GAP[sc.id] ?? GAP.default);
|
| 16 |
+
return l;
|
| 17 |
+
});
|
| 18 |
+
t += (TAIL[sc.id] ?? 1.2) - (GAP[sc.id] ?? GAP.default);
|
| 19 |
+
T += t;
|
| 20 |
+
return { ...sc, si, start, end: start + t, dur: t, lines };
|
| 21 |
+
});
|
| 22 |
+
const lines = scenes.flatMap(s => s.lines);
|
| 23 |
+
return { scenes, lines, total: T };
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
export const SIZES = { landscape: [1920, 1080], portrait: [1080, 1920] };
|
| 27 |
+
|
| 28 |
+
/** Layout regions. Portrait keeps clear of notch/status bar (top), Douyin caption + nav (bottom) and action buttons (right). */
|
| 29 |
+
export function layout(orient) {
|
| 30 |
+
const [W, H] = SIZES[orient];
|
| 31 |
+
if (orient === 'landscape') {
|
| 32 |
+
return {
|
| 33 |
+
orient, W, H, portrait: false,
|
| 34 |
+
safe: { l: 90, t: 56, r: W - 90, b: H - 56 },
|
| 35 |
+
head: { x: 96, y: 92 },
|
| 36 |
+
stage: { x: 110, y: 170, w: W - 220, h: 700, cx: W / 2, cy: 170 + 350 },
|
| 37 |
+
sub: { cx: W / 2, y: H - 92, maxW: 1560, size: 50 },
|
| 38 |
+
unit: 1,
|
| 39 |
+
};
|
| 40 |
+
}
|
| 41 |
+
const safe = { l: 64, t: 236, r: W - 156, b: H - 450 };
|
| 42 |
+
return {
|
| 43 |
+
orient, W, H, portrait: true,
|
| 44 |
+
safe,
|
| 45 |
+
head: { x: 72, y: safe.t + 52 },
|
| 46 |
+
stage: { x: safe.l, y: safe.t + 170, w: safe.r - safe.l, h: 880, cx: (safe.l + safe.r) / 2, cy: safe.t + 170 + 440 },
|
| 47 |
+
sub: { cx: (safe.l + safe.r) / 2, y: safe.b - 76, maxW: safe.r - safe.l - 10, size: 50 },
|
| 48 |
+
unit: 1,
|
| 49 |
+
};
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
/** split a narration line into subtitle chunks with timings proportional to character count */
|
| 53 |
+
export function subChunks(line, maxChars) {
|
| 54 |
+
const parts = line.t.match(/[^,。;:?!、—]+[,。;:?!、—]*/g) || [line.t];
|
| 55 |
+
const chunks = [];
|
| 56 |
+
let cur = '';
|
| 57 |
+
const len = s => Array.from(s.replace(/[A-Za-z0-9.\-$%]/g, 'a')).reduce((n, ch) => n + (/[a-z]/.test(ch) ? .55 : 1), 0);
|
| 58 |
+
for (const p of parts) {
|
| 59 |
+
if (cur && len(cur + p) > maxChars) { chunks.push(cur); cur = p; } else cur += p;
|
| 60 |
+
}
|
| 61 |
+
if (cur) chunks.push(cur);
|
| 62 |
+
// split chunks still too long
|
| 63 |
+
const out = [];
|
| 64 |
+
for (const c of chunks) {
|
| 65 |
+
if (len(c) <= maxChars * 1.15) { out.push(c); continue; }
|
| 66 |
+
const arr = Array.from(c), n = Math.ceil(len(c) / maxChars), step = Math.ceil(arr.length / n);
|
| 67 |
+
for (let i = 0; i < arr.length; i += step) out.push(arr.slice(i, i + step).join(''));
|
| 68 |
+
}
|
| 69 |
+
const total = out.reduce((n, c) => n + len(c), 0);
|
| 70 |
+
let acc = 0;
|
| 71 |
+
return out.map(c => {
|
| 72 |
+
const a = line.start + line.dur * acc / total; acc += len(c);
|
| 73 |
+
return { text: c.replace(/[,。;、—]+$/, ''), start: a, end: line.start + line.dur * acc / total };
|
| 74 |
+
});
|
| 75 |
+
}
|
solana-clock/tools/fonts.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Subset fonts to only the characters used by the animation + UI -> fonts/*.woff2
|
| 3 |
+
Usage: python3 tools/fonts.py (expects source fonts in /tmp/fonts)
|
| 4 |
+
"""
|
| 5 |
+
import pathlib, re
|
| 6 |
+
from fontTools import subset
|
| 7 |
+
from fontTools.ttLib import TTFont
|
| 8 |
+
|
| 9 |
+
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
| 10 |
+
SRC = pathlib.Path('/tmp/fonts')
|
| 11 |
+
OUT = ROOT / 'fonts'
|
| 12 |
+
OUT.mkdir(exist_ok=True)
|
| 13 |
+
|
| 14 |
+
chars = set()
|
| 15 |
+
for p in list((ROOT / 'js').glob('*.js')) + [ROOT / 'tools' / 'script.json', ROOT / 'index.html']:
|
| 16 |
+
if p.exists() and p.name != 'data-sol.js':
|
| 17 |
+
chars |= set(p.read_text(encoding='utf-8'))
|
| 18 |
+
chars |= set(chr(c) for c in range(0x20, 0x7f))
|
| 19 |
+
chars |= set('“”‘’…—–·•×÷±≈≥≤→←↑↓★☆●○◆▲▼✓✕%$,。!?:;、()《》【】')
|
| 20 |
+
text = ''.join(sorted(c for c in chars if ord(c) >= 0x20))
|
| 21 |
+
print('chars:', len(text))
|
| 22 |
+
|
| 23 |
+
JOBS = [
|
| 24 |
+
('NotoSansSC-Black.otf', 'sol-sc-900.woff2', None),
|
| 25 |
+
('NotoSansSC-Bold.otf', 'sol-sc-700.woff2', None),
|
| 26 |
+
('NotoSansSC-Regular.otf', 'sol-sc-400.woff2', None),
|
| 27 |
+
('SpaceGrotesk[wght].ttf', 'sol-grotesk.woff2', 'latin'),
|
| 28 |
+
('JetBrainsMono[wght].ttf', 'sol-mono.woff2', 'latin'),
|
| 29 |
+
]
|
| 30 |
+
for src, dst, kind in JOBS:
|
| 31 |
+
opts = subset.Options()
|
| 32 |
+
opts.flavor = 'woff2'
|
| 33 |
+
opts.layout_features = ['*']
|
| 34 |
+
opts.name_IDs = ['*']
|
| 35 |
+
opts.hinting = False
|
| 36 |
+
f = TTFont(SRC / src)
|
| 37 |
+
s = subset.Subsetter(opts)
|
| 38 |
+
t = text if kind is None else ''.join(c for c in text if ord(c) < 0x2500)
|
| 39 |
+
s.populate(text=t)
|
| 40 |
+
s.subset(f)
|
| 41 |
+
f.flavor = 'woff2'
|
| 42 |
+
f.save(OUT / dst)
|
| 43 |
+
print(dst, (OUT / dst).stat().st_size // 1024, 'KB')
|
solana-clock/tools/lint_layout.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Layout lint: instrument fillText/strokeText and path calls; report text that leaves the frame
|
| 3 |
+
(or the Douyin safe area in portrait) and NaN coordinates. Samples every STEP seconds."""
|
| 4 |
+
import sys, json, asyncio, pathlib
|
| 5 |
+
from playwright.async_api import async_playwright
|
| 6 |
+
base = 'http://localhost:8765/solana-clock/index.html'
|
| 7 |
+
STEP = float(sys.argv[1]) if len(sys.argv) > 1 else 0.5
|
| 8 |
+
|
| 9 |
+
JS = r"""
|
| 10 |
+
(async (STEP) => {
|
| 11 |
+
const P = CanvasRenderingContext2D.prototype;
|
| 12 |
+
const issues = new Map();
|
| 13 |
+
let cur = null, L = null;
|
| 14 |
+
const note = (k, msg) => { if (!issues.has(k)) issues.set(k, msg); };
|
| 15 |
+
for (const fn of ['fillText', 'strokeText']) {
|
| 16 |
+
const orig = P[fn];
|
| 17 |
+
P[fn] = function (str, x, y, maxW) {
|
| 18 |
+
if (cur !== null && this.canvas.id === 'view' && this.globalAlpha > 0.12 && String(str).trim()) {
|
| 19 |
+
const m = this.measureText(str);
|
| 20 |
+
let x0 = x - m.actualBoundingBoxLeft, x1 = x + m.actualBoundingBoxRight;
|
| 21 |
+
if (maxW && x1 - x0 > maxW) { const s = maxW / (x1 - x0); x0 = x + (x0 - x) * s; x1 = x + (x1 - x) * s; }
|
| 22 |
+
const y0 = y - m.actualBoundingBoxAscent, y1 = y + m.actualBoundingBoxDescent;
|
| 23 |
+
const t = this.getTransform(), sc = this.canvas.width / L.W;
|
| 24 |
+
const pts = [[x0, y0], [x1, y0], [x0, y1], [x1, y1]].map(([a, b]) => [(t.a * a + t.c * b + t.e) / sc, (t.b * a + t.d * b + t.f) / sc]);
|
| 25 |
+
const bx0 = Math.min(...pts.map(p => p[0])), bx1 = Math.max(...pts.map(p => p[0]));
|
| 26 |
+
const by0 = Math.min(...pts.map(p => p[1])), by1 = Math.max(...pts.map(p => p[1]));
|
| 27 |
+
const R = L.portrait ? L.safe : { l: 0, t: 0, r: L.W, b: L.H };
|
| 28 |
+
const tol = 6;
|
| 29 |
+
if (!window.__guides && (bx0 < R.l - tol || bx1 > R.r + tol || by0 < R.t - tol || by1 > R.b + tol)) {
|
| 30 |
+
const k = L.orient + '|' + cur.id + '|' + String(str).slice(0, 18);
|
| 31 |
+
note(k, `${L.orient} ${cur.id} @${cur.T.toFixed(1)} "${String(str).slice(0, 24)}" box=[${bx0|0},${by0|0},${bx1|0},${by1|0}] a=${this.globalAlpha.toFixed(2)}`);
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
return orig.apply(this, arguments);
|
| 35 |
+
};
|
| 36 |
+
}
|
| 37 |
+
for (const fn of ['moveTo', 'lineTo', 'arc', 'fillRect', 'quadraticCurveTo', 'bezierCurveTo', 'rect', 'drawImage', 'ellipse']) {
|
| 38 |
+
const orig = P[fn];
|
| 39 |
+
P[fn] = function (...a) {
|
| 40 |
+
if (cur && a.some(v => typeof v === 'number' && !Number.isFinite(v))) note('nan|' + cur.id + fn, `NaN in ${fn} ${L.orient} ${cur.id} @${cur.T.toFixed(1)}`);
|
| 41 |
+
return orig.apply(this, a);
|
| 42 |
+
};
|
| 43 |
+
}
|
| 44 |
+
const eng = await import('./js/engine.js'), tl = await import('./js/timeline.js');
|
| 45 |
+
const origGuides = eng.safeGuides;
|
| 46 |
+
for (const o of ['landscape', 'portrait']) {
|
| 47 |
+
L = tl.layout(o);
|
| 48 |
+
for (let T = 0; T < eng.TL.total; T += STEP) {
|
| 49 |
+
cur = { id: eng.sceneAt(T).id, T };
|
| 50 |
+
window.__render(T, o);
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
cur = null;
|
| 54 |
+
return [...issues.values()];
|
| 55 |
+
})
|
| 56 |
+
"""
|
| 57 |
+
|
| 58 |
+
async def main():
|
| 59 |
+
async with async_playwright() as p:
|
| 60 |
+
b = await p.chromium.launch()
|
| 61 |
+
pg = await b.new_page(viewport={'width': 1400, 'height': 900})
|
| 62 |
+
errs = []
|
| 63 |
+
pg.on('pageerror', lambda e: errs.append(str(e)))
|
| 64 |
+
await pg.goto(base + '?nomix=1', wait_until='networkidle')
|
| 65 |
+
await pg.wait_for_function('window.__ready === true')
|
| 66 |
+
res = await pg.evaluate(JS + f'({STEP})')
|
| 67 |
+
print(len(res), 'issues'); print('\n'.join(res))
|
| 68 |
+
print('pageerrors:', errs)
|
| 69 |
+
await b.close()
|
| 70 |
+
asyncio.run(main())
|
solana-clock/tools/shoot.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Headless smoke test: load the player, render frames at many times in both orientations,
|
| 3 |
+
collect console errors, save screenshots to tools/out/.
|
| 4 |
+
Usage: python3 tools/shoot.py [base_url] [--every N] [--times 1,2,3]"""
|
| 5 |
+
import sys, json, pathlib, asyncio
|
| 6 |
+
from playwright.async_api import async_playwright
|
| 7 |
+
|
| 8 |
+
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
| 9 |
+
OUT = ROOT / 'tools' / 'out'; OUT.mkdir(parents=True, exist_ok=True)
|
| 10 |
+
base = next((a for a in sys.argv[1:] if a.startswith('http')), 'http://localhost:5173/solana-clock/index.html')
|
| 11 |
+
times = None
|
| 12 |
+
if '--times' in sys.argv: times = [float(x) for x in sys.argv[sys.argv.index('--times') + 1].split(',')]
|
| 13 |
+
orients = ['landscape', 'portrait']
|
| 14 |
+
if '--only' in sys.argv: orients = [sys.argv[sys.argv.index('--only') + 1]]
|
| 15 |
+
|
| 16 |
+
async def main():
|
| 17 |
+
async with async_playwright() as p:
|
| 18 |
+
b = await p.chromium.launch(args=['--autoplay-policy=no-user-gesture-required'])
|
| 19 |
+
errors = []
|
| 20 |
+
for o in orients:
|
| 21 |
+
pg = await b.new_page(viewport={'width': 1920, 'height': 1080} if o == 'landscape' else {'width': 1080, 'height': 1920})
|
| 22 |
+
pg.on('console', lambda m: errors.append(f'[{m.type}] {m.text}') if m.type in ('error', 'warning') else None)
|
| 23 |
+
pg.on('pageerror', lambda e: errors.append(f'[pageerror] {e}'))
|
| 24 |
+
await pg.goto(base + f'?nomix=1&o={o}', wait_until='networkidle')
|
| 25 |
+
await pg.wait_for_function('window.__ready === true', timeout=30000)
|
| 26 |
+
# make canvas exact design size for screenshots
|
| 27 |
+
await pg.add_style_tag(content='.top,.controls,.toggles,.side,.foot,.big-play,.loading{display:none!important}.app{padding:0;max-width:none}.layout{display:block}.stage-wrap{border:0;border-radius:0}.stage,.stage-wrap.portrait .stage{width:%dpx!important;height:%dpx!important}' % ((1920, 1080) if o == 'landscape' else (1080, 1920)))
|
| 28 |
+
await pg.wait_for_timeout(300)
|
| 29 |
+
scenes = await pg.evaluate('import("./js/engine.js").then(m => m.TL.scenes.map(s => ({id: s.id, start: s.start, dur: s.dur, cues: s.lines.map(l => l.local)})))')
|
| 30 |
+
ts = []
|
| 31 |
+
if times: ts = [(f't{t:07.2f}', t) for t in times]
|
| 32 |
+
else:
|
| 33 |
+
for s in scenes:
|
| 34 |
+
for k, f in enumerate([.12, .45, .8]):
|
| 35 |
+
ts.append((f"{s['id']}-{k}", s['start'] + s['dur'] * f))
|
| 36 |
+
for name, t in ts:
|
| 37 |
+
await pg.evaluate(f'window.__render({t})')
|
| 38 |
+
await pg.locator('#view').screenshot(path=str(OUT / f'{o[0]}-{name}.png'))
|
| 39 |
+
await pg.close()
|
| 40 |
+
await b.close()
|
| 41 |
+
print('frames ok')
|
| 42 |
+
print('\n'.join(dict.fromkeys(errors)) or 'no console errors')
|
| 43 |
+
|
| 44 |
+
asyncio.run(main())
|