1
0

c5-infographic.html 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813
  1. <!doctype html>
  2. <html lang="zh-Hans">
  3. <head>
  4. <meta charset="utf-8" />
  5. <title>c5-infographic · 数据 → 印刷级排版(中文版)</title>
  6. <link rel="preconnect" href="https://fonts.googleapis.com">
  7. <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  8. <link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@300;400;500;600&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
  9. <style>
  10. :root {
  11. --bg: #000000;
  12. --ink: #FFFFFF;
  13. --ink-80: rgba(255,255,255,0.82);
  14. --ink-60: rgba(255,255,255,0.58);
  15. --muted: rgba(255,255,255,0.40);
  16. --dim: rgba(255,255,255,0.18);
  17. --hairline: rgba(255,255,255,0.12);
  18. --accent: #D97757;
  19. --accent-deep: #B85D3D;
  20. /* Brand Reveal */
  21. --cd-bg: #F5F4F0;
  22. --cd-panel: #FFFFFF;
  23. --cd-ink: #1A1918;
  24. --cd-dim: #8B867E;
  25. --serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
  26. --serif-cn: "Noto Serif SC", "Songti SC", "Source Han Serif SC", serif;
  27. --sans: "Inter", -apple-system, "PingFang SC", system-ui, sans-serif;
  28. --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
  29. }
  30. html, body {
  31. margin: 0; padding: 0;
  32. background: #000;
  33. overflow: hidden;
  34. font-family: var(--sans);
  35. color: var(--ink);
  36. -webkit-font-smoothing: antialiased;
  37. -moz-osx-font-smoothing: grayscale;
  38. font-feature-settings: "kern" 1, "liga" 1, "calt" 1;
  39. }
  40. * { box-sizing: border-box; }
  41. .stage {
  42. position: fixed;
  43. top: 50%; left: 50%;
  44. width: 1920px; height: 1080px;
  45. transform-origin: center center;
  46. background: var(--bg);
  47. overflow: hidden;
  48. /* Subtle film grain via SVG — 2% opacity */
  49. background-image:
  50. radial-gradient(ellipse at 20% 30%, rgba(217,119,87,0.025), transparent 50%),
  51. radial-gradient(ellipse at 80% 70%, rgba(217,119,87,0.018), transparent 55%);
  52. }
  53. .watermark {
  54. position: absolute;
  55. top: 40px; left: 48px;
  56. font-family: var(--mono);
  57. font-size: 12px;
  58. letter-spacing: 0.2em;
  59. color: var(--ink);
  60. opacity: 0.16;
  61. text-transform: uppercase;
  62. z-index: 400;
  63. transition: color 0.3s ease;
  64. }
  65. .watermark.on-light { color: var(--cd-ink); opacity: 0.35; }
  66. .v2-mark {
  67. position: absolute;
  68. bottom: 40px; right: 48px;
  69. font-family: var(--mono);
  70. font-size: 11px;
  71. letter-spacing: 0.2em;
  72. color: var(--ink);
  73. opacity: 0.16;
  74. z-index: 400;
  75. }
  76. /* ============ Split layout ============ */
  77. .split-left {
  78. position: absolute;
  79. left: 120px; top: 50%;
  80. transform: translateY(-50%);
  81. width: 440px;
  82. will-change: opacity, transform;
  83. }
  84. .json-block {
  85. font-family: var(--mono);
  86. font-size: 15px;
  87. line-height: 1.75;
  88. color: var(--ink-60);
  89. letter-spacing: 0.01em;
  90. white-space: pre;
  91. }
  92. .json-block .k { color: var(--ink-80); }
  93. .json-block .s { color: var(--accent); }
  94. .json-block .n { color: var(--ink); font-weight: 500; }
  95. .json-block .p { color: var(--muted); }
  96. .json-label {
  97. font-family: var(--mono);
  98. font-size: 10px;
  99. letter-spacing: 0.28em;
  100. color: var(--muted);
  101. text-transform: uppercase;
  102. margin-bottom: 22px;
  103. }
  104. /* Pipe arrow from JSON → infographic */
  105. .pipe {
  106. position: absolute;
  107. left: 580px; top: 50%;
  108. transform: translateY(-50%);
  109. width: 90px; height: 2px;
  110. background: linear-gradient(to right, var(--hairline), var(--accent), var(--hairline));
  111. opacity: 0;
  112. will-change: opacity;
  113. }
  114. .pipe::after {
  115. content: '';
  116. position: absolute;
  117. right: -4px; top: 50%;
  118. transform: translateY(-50%) rotate(45deg);
  119. width: 8px; height: 8px;
  120. border-right: 2px solid var(--accent);
  121. border-top: 2px solid var(--accent);
  122. }
  123. /* ============ Infographic (right side) ============ */
  124. .infographic {
  125. position: absolute;
  126. right: 100px; top: 72px;
  127. width: 1120px; height: 936px;
  128. background: #0A0A0A;
  129. border: 1px solid var(--hairline);
  130. padding: 56px 64px;
  131. opacity: 0;
  132. transform: translateY(18px);
  133. will-change: opacity, transform;
  134. overflow: hidden;
  135. }
  136. .ig-masthead {
  137. display: flex;
  138. justify-content: space-between;
  139. align-items: baseline;
  140. border-bottom: 1px solid var(--hairline);
  141. padding-bottom: 20px;
  142. margin-bottom: 36px;
  143. opacity: 0;
  144. will-change: opacity;
  145. }
  146. .ig-masthead .issue {
  147. font-family: var(--mono);
  148. font-size: 10px;
  149. letter-spacing: 0.3em;
  150. color: var(--muted);
  151. text-transform: uppercase;
  152. }
  153. .ig-masthead .issue .orange { color: var(--accent); }
  154. .ig-masthead .dept {
  155. font-family: var(--serif-cn);
  156. font-weight: 300;
  157. font-size: 14px;
  158. letter-spacing: 0.35em;
  159. color: var(--ink-60);
  160. }
  161. .ig-display {
  162. font-family: var(--serif-cn);
  163. font-weight: 400;
  164. font-size: 84px;
  165. line-height: 1.02;
  166. letter-spacing: -0.01em;
  167. color: var(--ink);
  168. margin-bottom: 6px;
  169. opacity: 0;
  170. will-change: opacity, transform;
  171. text-wrap: pretty;
  172. }
  173. .ig-display .en {
  174. font-family: var(--serif-en);
  175. font-style: italic;
  176. font-weight: 300;
  177. color: var(--accent);
  178. font-feature-settings: "liga" 1, "dlig" 1, "swsh" 1;
  179. }
  180. .ig-deck {
  181. font-family: var(--serif-en);
  182. font-style: italic;
  183. font-weight: 300;
  184. font-size: 22px;
  185. color: var(--ink-60);
  186. letter-spacing: 0.01em;
  187. margin-bottom: 44px;
  188. opacity: 0;
  189. will-change: opacity;
  190. font-feature-settings: "liga" 1, "dlig" 1;
  191. }
  192. /* Grid of 5 stats */
  193. .ig-grid {
  194. display: grid;
  195. grid-template-columns: 1.3fr 1fr 1fr 1fr;
  196. gap: 32px;
  197. margin-bottom: 44px;
  198. }
  199. .ig-cell {
  200. opacity: 0;
  201. will-change: opacity, transform;
  202. border-top: 2px solid var(--ink);
  203. padding-top: 14px;
  204. }
  205. .ig-cell.accent { border-top-color: var(--accent); }
  206. .ig-cell .label {
  207. font-family: var(--serif-cn);
  208. font-size: 12px;
  209. font-weight: 300;
  210. color: var(--muted);
  211. letter-spacing: 0.22em;
  212. margin-bottom: 14px;
  213. }
  214. .ig-cell .label .en {
  215. font-family: var(--mono);
  216. text-transform: uppercase;
  217. letter-spacing: 0.26em;
  218. }
  219. .ig-cell .big {
  220. font-family: var(--serif-en);
  221. font-weight: 300;
  222. font-size: 72px;
  223. line-height: 0.92;
  224. color: var(--ink);
  225. letter-spacing: -0.03em;
  226. font-variant-numeric: oldstyle-nums proportional-nums;
  227. font-feature-settings: "onum" 1, "pnum" 1, "kern" 1;
  228. }
  229. .ig-cell.accent .big { color: var(--accent); }
  230. .ig-cell .big .unit {
  231. font-size: 28px;
  232. color: var(--ink-60);
  233. letter-spacing: 0;
  234. }
  235. .ig-cell .sub {
  236. margin-top: 12px;
  237. font-family: var(--serif-en);
  238. font-style: italic;
  239. font-size: 14px;
  240. color: var(--ink-60);
  241. line-height: 1.4;
  242. font-feature-settings: "liga" 1, "dlig" 1;
  243. letter-spacing: 0.005em;
  244. }
  245. /* Comparison bars */
  246. .ig-bars {
  247. display: grid;
  248. grid-template-columns: 140px 1fr 80px;
  249. gap: 18px 24px;
  250. row-gap: 18px;
  251. border-top: 1px solid var(--hairline);
  252. padding-top: 28px;
  253. align-items: center;
  254. opacity: 0;
  255. will-change: opacity;
  256. }
  257. .ig-bars .row-label {
  258. font-family: var(--serif-cn);
  259. font-size: 15px;
  260. font-weight: 400;
  261. color: var(--ink-80);
  262. letter-spacing: 0.02em;
  263. }
  264. .ig-bars .row-label.highlight { color: var(--accent); font-weight: 500; }
  265. .ig-bars .row-bar {
  266. height: 6px;
  267. background: var(--hairline);
  268. position: relative;
  269. overflow: hidden;
  270. }
  271. .ig-bars .row-bar .fill {
  272. position: absolute;
  273. left: 0; top: 0; bottom: 0;
  274. background: var(--ink-80);
  275. width: 0%;
  276. will-change: width;
  277. }
  278. .ig-bars .row-bar .fill.accent { background: var(--accent); }
  279. .ig-bars .row-val {
  280. font-family: var(--serif-en);
  281. font-size: 16px;
  282. color: var(--ink);
  283. text-align: right;
  284. font-variant-numeric: oldstyle-nums tabular-nums;
  285. font-feature-settings: "onum" 1, "tnum" 1;
  286. letter-spacing: 0.01em;
  287. }
  288. .ig-footer {
  289. position: absolute;
  290. bottom: 40px; left: 64px; right: 64px;
  291. display: flex; justify-content: space-between; align-items: baseline;
  292. border-top: 1px solid var(--hairline);
  293. padding-top: 16px;
  294. font-family: var(--mono);
  295. font-size: 10px;
  296. letter-spacing: 0.24em;
  297. color: var(--muted);
  298. text-transform: uppercase;
  299. opacity: 0;
  300. will-change: opacity;
  301. }
  302. .ig-footer .folio { color: var(--ink-60); letter-spacing: 0.32em; }
  303. /* ============ Typography detail zoom ============ */
  304. .detail-zoom {
  305. position: absolute;
  306. inset: 0;
  307. display: flex;
  308. align-items: center;
  309. justify-content: center;
  310. opacity: 0;
  311. will-change: opacity;
  312. background: radial-gradient(ellipse at center, #0A0A0A, #000000);
  313. z-index: 250;
  314. }
  315. .detail-word {
  316. font-family: var(--serif-en);
  317. font-weight: 300;
  318. font-style: italic;
  319. font-size: 320px;
  320. line-height: 0.9;
  321. letter-spacing: -0.01em;
  322. color: var(--ink);
  323. /* Enable OpenType ligatures, discretionary ligatures, swashes */
  324. font-feature-settings: "liga" 1, "dlig" 1, "swsh" 1, "salt" 1, "calt" 1;
  325. text-rendering: optimizeLegibility;
  326. will-change: transform, opacity;
  327. }
  328. .detail-word .fi {
  329. /* fi ligature is default with "liga" */
  330. color: var(--accent);
  331. }
  332. .detail-annotation {
  333. position: absolute;
  334. top: calc(50% + 170px); left: 50%;
  335. transform: translateX(-50%);
  336. font-family: var(--mono);
  337. font-size: 12px;
  338. letter-spacing: 0.28em;
  339. color: var(--muted);
  340. text-transform: uppercase;
  341. opacity: 0;
  342. will-change: opacity;
  343. white-space: nowrap;
  344. }
  345. .detail-annotation .dot {
  346. color: var(--accent);
  347. padding: 0 8px;
  348. }
  349. /* Callout lines pointing to ligature */
  350. .callout {
  351. position: absolute;
  352. left: 50%; top: 50%;
  353. transform: translate(-50%, -50%);
  354. pointer-events: none;
  355. opacity: 0;
  356. will-change: opacity;
  357. }
  358. .callout svg { overflow: visible; display: block; }
  359. /* ============ Brand Reveal ============ */
  360. .brand-wall {
  361. position: absolute;
  362. inset: 0;
  363. background: var(--cd-bg);
  364. z-index: 300;
  365. opacity: 0;
  366. transform: translateY(100%);
  367. will-change: transform, opacity;
  368. display: flex;
  369. flex-direction: column;
  370. align-items: center;
  371. justify-content: center;
  372. }
  373. .brand-wordmark {
  374. font-family: var(--serif-en);
  375. font-size: 132px;
  376. font-weight: 200;
  377. color: var(--cd-ink);
  378. letter-spacing: -0.04em;
  379. line-height: 1;
  380. opacity: 0;
  381. transform: scale(0.92);
  382. will-change: opacity, transform;
  383. font-feature-settings: "liga" 1, "dlig" 1;
  384. }
  385. .brand-wordmark .dot { color: var(--accent); padding: 0 10px; font-weight: 300; }
  386. .brand-underline {
  387. margin-top: 28px;
  388. height: 2px;
  389. width: 0;
  390. background: var(--accent);
  391. will-change: width;
  392. }
  393. .brand-cn {
  394. margin-top: 30px;
  395. font-family: var(--serif-cn);
  396. font-size: 18px;
  397. font-weight: 300;
  398. color: var(--cd-dim);
  399. letter-spacing: 0.4em;
  400. opacity: 0;
  401. will-change: opacity;
  402. }
  403. </style>
  404. </head>
  405. <body>
  406. <div class="stage" id="stage">
  407. <div class="watermark" id="watermark">HUASHU · DESIGN</div>
  408. <div class="v2-mark">V2 · 2026</div>
  409. <!-- Left: JSON data -->
  410. <div class="split-left" id="splitLeft" style="opacity:0">
  411. <div class="json-label" id="jsonLabel">DATA · benchmarks.json</div>
  412. <pre class="json-block" id="jsonBlock"></pre>
  413. </div>
  414. <!-- Pipe arrow -->
  415. <div class="pipe" id="pipe"></div>
  416. <!-- Right: Infographic -->
  417. <div class="infographic" id="infographic">
  418. <div class="ig-masthead" id="igMasthead">
  419. <div class="issue">Issue № 05 · <span class="orange">AI Benchmarks</span> · Q2 2026</div>
  420. <div class="dept">性 能 报 告</div>
  421. </div>
  422. <h1 class="ig-display" id="igDisplay">
  423. 大模型<br>
  424. <span class="en">benchmarks</span> 之年
  425. </h1>
  426. <p class="ig-deck" id="igDeck">
  427. Five frontier models, five numbers, one uncomfortable truth.
  428. </p>
  429. <div class="ig-grid" id="igGrid">
  430. <div class="ig-cell accent" data-cell="0">
  431. <div class="label">领跑模型 <span class="en">· leader</span></div>
  432. <div class="big">Claude 4.7</div>
  433. <div class="sub">Sonnet, 1M ctx · Anthropic</div>
  434. </div>
  435. <div class="ig-cell" data-cell="1">
  436. <div class="label"><span class="en">SWE-bench</span></div>
  437. <div class="big">77<span class="unit">.2%</span></div>
  438. <div class="sub">coding, verified split</div>
  439. </div>
  440. <div class="ig-cell" data-cell="2">
  441. <div class="label"><span class="en">GPQA</span></div>
  442. <div class="big">84<span class="unit">.5</span></div>
  443. <div class="sub">diamond, graduate science</div>
  444. </div>
  445. <div class="ig-cell" data-cell="3">
  446. <div class="label">价差 <span class="en">· price</span></div>
  447. <div class="big">$3<span class="unit">/M</span></div>
  448. <div class="sub">input token, typical</div>
  449. </div>
  450. </div>
  451. <div class="ig-bars" id="igBars">
  452. <div class="row-label highlight">Claude 4.7 Sonnet</div>
  453. <div class="row-bar"><div class="fill accent" data-w="77.2"></div></div>
  454. <div class="row-val">77.2</div>
  455. <div class="row-label">GPT-5 Turbo</div>
  456. <div class="row-bar"><div class="fill" data-w="74.8"></div></div>
  457. <div class="row-val">74.8</div>
  458. <div class="row-label">Gemini 3 Pro</div>
  459. <div class="row-bar"><div class="fill" data-w="71.3"></div></div>
  460. <div class="row-val">71.3</div>
  461. <div class="row-label">GLM-5</div>
  462. <div class="row-bar"><div class="fill" data-w="68.9"></div></div>
  463. <div class="row-val">68.9</div>
  464. <div class="row-label">Kimi k3</div>
  465. <div class="row-bar"><div class="fill" data-w="66.4"></div></div>
  466. <div class="row-val">66.4</div>
  467. </div>
  468. <div class="ig-footer" id="igFooter">
  469. <span>Set in Noto Serif SC &amp; Source Serif 4</span>
  470. <span class="folio">P. 05</span>
  471. <span>Data · 2026 Q2, public benchmarks</span>
  472. </div>
  473. </div>
  474. <!-- Detail zoom: Typography ligature -->
  475. <div class="detail-zoom" id="detailZoom">
  476. <div class="detail-word" id="detailWord">bench<span class="fi">ma</span>rks</div>
  477. <div class="callout" id="callout" style="display:none"></div>
  478. <div class="detail-annotation" id="detailAnnotation">
  479. SOURCE SERIF 4 <span class="dot">·</span> ITALIC <span class="dot">·</span> OLDSTYLE FIGURES
  480. </div>
  481. </div>
  482. <!-- Brand Reveal -->
  483. <div class="brand-wall" id="brandWall">
  484. <div class="brand-wordmark" id="brandWord">huashu<span class="dot">·</span>design</div>
  485. <div class="brand-underline" id="brandLine"></div>
  486. <div class="brand-cn" id="brandCn">数 据 · 印 刷 级 排 版</div>
  487. </div>
  488. </div>
  489. <script>
  490. (() => {
  491. 'use strict';
  492. // ---------- Scale stage to viewport ----------
  493. const stage = document.getElementById('stage');
  494. function fitStage() {
  495. const s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
  496. stage.style.transform = `translate(-50%, -50%) scale(${s})`;
  497. }
  498. fitStage();
  499. window.addEventListener('resize', fitStage);
  500. // ---------- Easing ----------
  501. const expoOut = t => t >= 1 ? 1 : 1 - Math.pow(2, -10 * t);
  502. const expoIn = t => t <= 0 ? 0 : Math.pow(2, 10 * (t - 1));
  503. const cubicOut = t => 1 - Math.pow(1 - t, 3);
  504. const cubicInOut = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2;
  505. const lerp = (t, a, b, c, d, ease=x=>x) => {
  506. if (b === a) return c;
  507. const k = Math.max(0, Math.min(1, (t - a) / (b - a)));
  508. return c + (d - c) * ease(k);
  509. };
  510. const seg = (t, a, b) => Math.max(0, Math.min(1, (t - a) / (b - a)));
  511. // ---------- Refs ----------
  512. const splitLeft = document.getElementById('splitLeft');
  513. const jsonLabel = document.getElementById('jsonLabel');
  514. const jsonBlock = document.getElementById('jsonBlock');
  515. const pipe = document.getElementById('pipe');
  516. const infographic = document.getElementById('infographic');
  517. const igMasthead = document.getElementById('igMasthead');
  518. const igDisplay = document.getElementById('igDisplay');
  519. const igDeck = document.getElementById('igDeck');
  520. const igGrid = document.getElementById('igGrid');
  521. const igCells = igGrid.querySelectorAll('.ig-cell');
  522. const igBars = document.getElementById('igBars');
  523. const igBarFills = igBars.querySelectorAll('.fill');
  524. const igFooter = document.getElementById('igFooter');
  525. const detailZoom = document.getElementById('detailZoom');
  526. const detailWord = document.getElementById('detailWord');
  527. const detailAnnotation = document.getElementById('detailAnnotation');
  528. const callout = document.getElementById('callout');
  529. const brandWall = document.getElementById('brandWall');
  530. const brandWord = document.getElementById('brandWord');
  531. const brandLine = document.getElementById('brandLine');
  532. const brandCn = document.getElementById('brandCn');
  533. const watermark = document.getElementById('watermark');
  534. // ---------- JSON content (for progressive reveal) ----------
  535. const jsonRaw = [
  536. '{',
  537. ' "issue": "2026-Q2",',
  538. ' "leader": "Claude 4.7",',
  539. ' "models": [',
  540. ' { "name": "Claude 4.7", "swe": 77.2 },',
  541. ' { "name": "GPT-5 Turbo", "swe": 74.8 },',
  542. ' { "name": "Gemini 3 Pro", "swe": 71.3 },',
  543. ' { "name": "GLM-5", "swe": 68.9 },',
  544. ' { "name": "Kimi k3", "swe": 66.4 }',
  545. ' ],',
  546. ' "gpqa_top": 84.5,',
  547. ' "price_per_M": 3',
  548. '}'
  549. ];
  550. function formatJson(lines) {
  551. return lines.map(line => {
  552. return line
  553. .replace(/"([a-zA-Z_]+)":/g, '<span class="k">"$1"</span>:')
  554. .replace(/: "([^"]+)"/g, ': <span class="s">"$1"</span>')
  555. .replace(/: ([0-9.]+)/g, ': <span class="n">$1</span>')
  556. .replace(/([{}\[\],])/g, '<span class="p">$1</span>');
  557. }).join('\n');
  558. }
  559. // ---------- Timeline ----------
  560. const DURATION = 10.0;
  561. // SFX cue points (played back in ffmpeg post-processing, not browser):
  562. // t=0.35 → keyboard/type-fast.mp3 (data entering)
  563. // t=2.15 → container/card-snap.mp3 (infographic settles)
  564. // t=6.75 → transition/whoosh-fast.mp3 (zoom-in to typography)
  565. // t=8.70 → impact/logo-reveal.mp3 (brand reveal chime)
  566. const sfxFired = new Set();
  567. function fireOnce(key) {
  568. if (sfxFired.has(key)) return;
  569. sfxFired.add(key);
  570. // cue emitted for post-processing; no in-browser playback
  571. }
  572. let startTime = null;
  573. let raf;
  574. function tick(now) {
  575. if (startTime == null) startTime = now;
  576. const t = (now - startTime) / 1000;
  577. // ── Beat 1: 0-2s · JSON data appears, types in ─────────
  578. // JSON label fade in
  579. {
  580. const k = cubicOut(seg(t, 0.15, 0.55));
  581. jsonLabel.style.opacity = k;
  582. splitLeft.style.opacity = '1';
  583. }
  584. // Progressive type-reveal: reveal N lines of JSON by time
  585. {
  586. const totalLines = jsonRaw.length;
  587. const k = seg(t, 0.3, 1.9);
  588. const linesShown = Math.floor(k * totalLines);
  589. const shown = jsonRaw.slice(0, Math.max(0, linesShown));
  590. jsonBlock.innerHTML = formatJson(shown);
  591. if (linesShown >= 3 && t < 1.9) fireOnce('datain');
  592. }
  593. // ── Pipe arrow (1.8 → 2.2) ─────────────────────────────
  594. {
  595. const k = cubicOut(seg(t, 1.8, 2.2));
  596. pipe.style.opacity = k;
  597. }
  598. // ── Beat 2a: 2.0-3.2s · Infographic canvas arrives ─────
  599. {
  600. const k = expoOut(seg(t, 2.0, 2.8));
  601. infographic.style.opacity = k;
  602. infographic.style.transform = `translateY(${lerp(t, 2.0, 2.8, 18, 0, expoOut)}px)`;
  603. if (t > 2.1) fireOnce('settle');
  604. }
  605. // Masthead
  606. {
  607. const k = cubicOut(seg(t, 2.6, 3.1));
  608. igMasthead.style.opacity = k;
  609. }
  610. // ── Beat 2b: 3.0-4.2s · Display headline appears ──────
  611. {
  612. const k = expoOut(seg(t, 3.0, 3.8));
  613. igDisplay.style.opacity = k;
  614. igDisplay.style.transform = `translateY(${lerp(t, 3.0, 3.8, 16, 0, expoOut)}px)`;
  615. }
  616. // Deck line (italic)
  617. {
  618. const k = cubicOut(seg(t, 3.6, 4.2));
  619. igDeck.style.opacity = k;
  620. }
  621. // ── Beat 2c: 4.0-5.2s · Grid cells (ripple, 4 cells) ──
  622. igCells.forEach((cell, i) => {
  623. const start = 4.0 + i * 0.12;
  624. const end = start + 0.5;
  625. const k = expoOut(seg(t, start, end));
  626. cell.style.opacity = k;
  627. cell.style.transform = `translateY(${lerp(t, start, end, 14, 0, expoOut)}px)`;
  628. });
  629. // ── Beat 2d: 5.2-6.4s · Comparison bars grow ─────────
  630. {
  631. const k = cubicOut(seg(t, 5.1, 5.4));
  632. igBars.style.opacity = k;
  633. }
  634. igBarFills.forEach((fill, i) => {
  635. const start = 5.3 + i * 0.08;
  636. const end = start + 0.7;
  637. const w = parseFloat(fill.getAttribute('data-w'));
  638. const pct = lerp(t, start, end, 0, w, expoOut);
  639. fill.style.width = pct + '%';
  640. });
  641. // Footer
  642. {
  643. const k = cubicOut(seg(t, 6.0, 6.6));
  644. igFooter.style.opacity = k * 0.9;
  645. }
  646. // ── Beat 2e: 6.6-8.2s · Zoom to typography detail ────
  647. if (t >= 6.6 && t < 8.3) {
  648. const k = expoOut(seg(t, 6.6, 7.4));
  649. // Infographic scales up and fades — simulate push-in
  650. const scale = lerp(t, 6.6, 7.4, 1, 3.4, expoOut);
  651. const ty = lerp(t, 6.6, 7.4, 0, -140, expoOut);
  652. infographic.style.transform = `translateY(${ty}px) scale(${scale})`;
  653. infographic.style.opacity = String(1 - k * 0.85);
  654. splitLeft.style.opacity = String(1 - k);
  655. pipe.style.opacity = String(1 - k);
  656. // Detail zoom fades in
  657. const k2 = expoOut(seg(t, 7.0, 7.7));
  658. detailZoom.style.opacity = k2;
  659. // Word subtle scale-in (starts from 0.96)
  660. detailWord.style.transform = `scale(${lerp(t, 7.0, 7.9, 0.96, 1.0, expoOut)})`;
  661. // SFX at 6.7
  662. if (t > 6.7) fireOnce('zoom');
  663. // Callout + annotation (7.5 → 8.1)
  664. const k3 = cubicOut(seg(t, 7.6, 8.1));
  665. callout.style.opacity = k3;
  666. detailAnnotation.style.opacity = k3;
  667. }
  668. // ── Beat 3: 8.2-10s · Brand reveal ───────────────────
  669. // Detail zoom fades under brand wall
  670. if (t >= 8.1) {
  671. const k = cubicOut(seg(t, 8.1, 8.5));
  672. detailZoom.style.opacity = String(Math.max(0, 1 - k));
  673. }
  674. // Brand wall slides up from bottom
  675. {
  676. const k = expoOut(seg(t, 8.1, 8.7));
  677. brandWall.style.transform = `translateY(${lerp(t, 8.1, 8.7, 100, 0, expoOut)}%)`;
  678. brandWall.style.opacity = k > 0 ? '1' : '0';
  679. if (k > 0.55) watermark.classList.add('on-light');
  680. else watermark.classList.remove('on-light');
  681. }
  682. // Wordmark
  683. {
  684. const k = expoOut(seg(t, 8.6, 9.2));
  685. brandWord.style.opacity = k;
  686. brandWord.style.transform = `scale(${lerp(t, 8.6, 9.2, 0.92, 1.0, expoOut)})`;
  687. if (t > 8.65) fireOnce('chime');
  688. }
  689. // Underline
  690. {
  691. const k = expoOut(seg(t, 9.0, 9.6));
  692. brandLine.style.width = (280 * k) + 'px';
  693. }
  694. // CN tagline
  695. {
  696. const k = cubicOut(seg(t, 9.3, 9.9));
  697. brandCn.style.opacity = k * 0.9;
  698. }
  699. // Loop / hold
  700. if (t < DURATION) {
  701. raf = requestAnimationFrame(tick);
  702. } else {
  703. if (!window.__recording) {
  704. setTimeout(() => {
  705. // Reset
  706. startTime = null;
  707. sfxFired.clear();
  708. jsonBlock.innerHTML = '';
  709. splitLeft.style.opacity = '0';
  710. pipe.style.opacity = '0';
  711. infographic.style.opacity = '0';
  712. infographic.style.transform = 'translateY(18px) scale(1)';
  713. igMasthead.style.opacity = '0';
  714. igDisplay.style.opacity = '0';
  715. igDeck.style.opacity = '0';
  716. igBars.style.opacity = '0';
  717. igFooter.style.opacity = '0';
  718. igCells.forEach(c => { c.style.opacity = '0'; });
  719. igBarFills.forEach(f => { f.style.width = '0%'; });
  720. detailZoom.style.opacity = '0';
  721. callout.style.opacity = '0';
  722. detailAnnotation.style.opacity = '0';
  723. brandWall.style.transform = 'translateY(100%)';
  724. brandWall.style.opacity = '0';
  725. brandWord.style.opacity = '0';
  726. brandLine.style.width = '0';
  727. brandCn.style.opacity = '0';
  728. watermark.classList.remove('on-light');
  729. raf = requestAnimationFrame(tick);
  730. }, 800);
  731. }
  732. }
  733. }
  734. window.__seek = function(s) {
  735. startTime = performance.now() - s * 1000;
  736. };
  737. // Wait for fonts, then start
  738. (document.fonts ? document.fonts.ready : Promise.resolve()).then(() => {
  739. requestAnimationFrame((now) => {
  740. startTime = now;
  741. window.__ready = true;
  742. raf = requestAnimationFrame(tick);
  743. });
  744. });
  745. })();
  746. </script>
  747. </body>
  748. </html>