1
0

c6-expert-review.html 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894
  1. <!doctype html>
  2. <html lang="zh-Hans">
  3. <head>
  4. <meta charset="utf-8" />
  5. <title>c6 · 五个维度,给你一份手术单</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@200;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. --cd-bg: #F5F4F0;
  21. --cd-panel: #FFFFFF;
  22. --cd-ink: #1A1918;
  23. --serif-zh: "Noto Serif SC", "Songti SC", serif;
  24. --serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
  25. --sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
  26. --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
  27. }
  28. html, body {
  29. margin: 0; padding: 0;
  30. background: #000;
  31. overflow: hidden;
  32. font-family: var(--sans);
  33. color: var(--ink);
  34. -webkit-font-smoothing: antialiased;
  35. }
  36. * { box-sizing: border-box; }
  37. .stage {
  38. position: fixed;
  39. top: 50%; left: 50%;
  40. width: 1920px; height: 1080px;
  41. transform-origin: center center;
  42. background: var(--bg);
  43. overflow: hidden;
  44. }
  45. /* Film grain */
  46. .stage::before {
  47. content: '';
  48. position: absolute;
  49. inset: 0;
  50. background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='300'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>");
  51. opacity: 0.02;
  52. pointer-events: none;
  53. z-index: 100;
  54. }
  55. /* Chrome */
  56. .mark {
  57. position: absolute;
  58. top: 48px; left: 64px;
  59. font-family: var(--mono);
  60. font-size: 13px;
  61. letter-spacing: 0.2em;
  62. color: rgba(255,255,255,1);
  63. opacity: 0.16;
  64. pointer-events: none;
  65. z-index: 50;
  66. }
  67. .mark-right {
  68. position: absolute;
  69. top: 48px; right: 64px;
  70. font-family: var(--mono);
  71. font-size: 13px;
  72. letter-spacing: 0.2em;
  73. color: rgba(255,255,255,1);
  74. opacity: 0.16;
  75. pointer-events: none;
  76. z-index: 50;
  77. }
  78. /* Title */
  79. .title-line {
  80. position: absolute;
  81. top: 108px;
  82. left: 50%;
  83. transform: translateX(-50%);
  84. font-family: var(--mono);
  85. font-size: 13px;
  86. letter-spacing: 0.28em;
  87. color: var(--muted);
  88. text-transform: uppercase;
  89. opacity: 0;
  90. will-change: opacity, transform;
  91. }
  92. /* Main composition: camera wrapper for push-in at Beat 3 */
  93. .camera {
  94. position: absolute;
  95. inset: 0;
  96. transform-origin: 1000px 940px; /* center of Fix first-row */
  97. will-change: transform;
  98. }
  99. /* ============ LEFT: under-review artwork ============ */
  100. .subject {
  101. position: absolute;
  102. left: 150px;
  103. top: 310px;
  104. width: 640px;
  105. height: 460px;
  106. background: #0B0B0B;
  107. border: 1px solid var(--hairline);
  108. border-radius: 8px;
  109. overflow: hidden;
  110. opacity: 0;
  111. will-change: opacity, transform, filter;
  112. transform: translateY(12px);
  113. }
  114. .subject::after {
  115. /* subtle inner vignette */
  116. content: '';
  117. position: absolute;
  118. inset: 0;
  119. box-shadow: inset 0 0 120px rgba(0,0,0,0.6);
  120. pointer-events: none;
  121. }
  122. .subject-label {
  123. position: absolute;
  124. left: 20px;
  125. top: 18px;
  126. font-family: var(--mono);
  127. font-size: 10px;
  128. letter-spacing: 0.25em;
  129. color: var(--muted);
  130. z-index: 3;
  131. }
  132. .subject-dot {
  133. position: absolute;
  134. right: 20px;
  135. top: 18px;
  136. width: 6px;
  137. height: 6px;
  138. background: var(--accent);
  139. border-radius: 50%;
  140. z-index: 3;
  141. box-shadow: 0 0 10px rgba(217,119,87,0.6);
  142. }
  143. /* Subject wireframe: abstract design mockup */
  144. .subject-canvas {
  145. position: absolute;
  146. inset: 50px 36px 36px;
  147. }
  148. .wf-h1 {
  149. width: 62%;
  150. height: 18px;
  151. background: rgba(255,255,255,0.28);
  152. border-radius: 2px;
  153. margin-bottom: 10px;
  154. }
  155. .wf-h2 {
  156. width: 38%;
  157. height: 10px;
  158. background: rgba(255,255,255,0.14);
  159. border-radius: 2px;
  160. margin-bottom: 28px;
  161. }
  162. .wf-row {
  163. display: flex;
  164. gap: 12px;
  165. margin-bottom: 12px;
  166. }
  167. .wf-row .bar {
  168. height: 8px;
  169. background: rgba(255,255,255,0.10);
  170. border-radius: 2px;
  171. }
  172. .wf-grid {
  173. display: grid;
  174. grid-template-columns: 1fr 1fr 1fr;
  175. gap: 14px;
  176. margin-top: 28px;
  177. }
  178. .wf-card {
  179. height: 82px;
  180. background: rgba(255,255,255,0.04);
  181. border: 1px solid rgba(255,255,255,0.06);
  182. border-radius: 6px;
  183. position: relative;
  184. }
  185. .wf-card::before {
  186. content: '';
  187. position: absolute;
  188. left: 12px; top: 14px;
  189. width: 40%;
  190. height: 6px;
  191. background: rgba(255,255,255,0.22);
  192. border-radius: 2px;
  193. }
  194. .wf-card::after {
  195. content: '';
  196. position: absolute;
  197. left: 12px; bottom: 16px;
  198. width: 64%;
  199. height: 4px;
  200. background: rgba(255,255,255,0.10);
  201. border-radius: 2px;
  202. }
  203. .wf-card.accent { border-color: rgba(217,119,87,0.55); background: rgba(217,119,87,0.06); }
  204. .wf-card.accent::before { background: var(--accent); }
  205. .wf-foot {
  206. position: absolute;
  207. left: 0; right: 0;
  208. bottom: 0;
  209. height: 44px;
  210. display: flex;
  211. align-items: center;
  212. gap: 10px;
  213. padding: 0 4px;
  214. }
  215. .wf-chip {
  216. height: 22px;
  217. padding: 0 10px;
  218. background: rgba(255,255,255,0.05);
  219. border: 1px solid rgba(255,255,255,0.08);
  220. border-radius: 11px;
  221. flex: 0 0 auto;
  222. width: 68px;
  223. }
  224. .wf-chip.wide { width: 120px; }
  225. /* ============ Light sweep ============ */
  226. .sweep {
  227. position: absolute;
  228. left: 130px;
  229. top: 250px;
  230. width: 680px;
  231. height: 140px;
  232. background: linear-gradient(180deg,
  233. rgba(217,119,87,0) 0%,
  234. rgba(217,119,87,0.12) 20%,
  235. rgba(255,220,200,0.62) 50%,
  236. rgba(217,119,87,0.18) 80%,
  237. rgba(217,119,87,0) 100%);
  238. filter: blur(14px);
  239. opacity: 0;
  240. pointer-events: none;
  241. z-index: 4;
  242. mix-blend-mode: screen;
  243. will-change: opacity, transform;
  244. }
  245. .sweep-line {
  246. position: absolute;
  247. left: 150px;
  248. top: 310px;
  249. width: 640px;
  250. height: 1px;
  251. background: linear-gradient(90deg,
  252. transparent 0%,
  253. rgba(255,220,200,0.2) 10%,
  254. rgba(255,220,200,0.9) 50%,
  255. rgba(255,220,200,0.2) 90%,
  256. transparent 100%);
  257. filter: blur(0.6px);
  258. box-shadow: 0 0 14px rgba(217,119,87,0.8), 0 0 30px rgba(217,119,87,0.3);
  259. opacity: 0;
  260. pointer-events: none;
  261. z-index: 6;
  262. will-change: opacity, transform;
  263. }
  264. /* ============ RIGHT: radar chart ============ */
  265. .radar-wrap {
  266. position: absolute;
  267. right: 280px;
  268. top: 200px;
  269. width: 520px;
  270. height: 520px;
  271. opacity: 0;
  272. will-change: opacity, transform;
  273. }
  274. .radar-wrap svg {
  275. width: 100%;
  276. height: 100%;
  277. overflow: visible;
  278. }
  279. .radar-grid path {
  280. fill: none;
  281. stroke: rgba(255,255,255,0.10);
  282. stroke-width: 1;
  283. }
  284. .radar-spoke {
  285. stroke: rgba(255,255,255,0.08);
  286. stroke-width: 1;
  287. }
  288. .radar-poly {
  289. fill: rgba(217,119,87,0.16);
  290. stroke: var(--accent);
  291. stroke-width: 2;
  292. stroke-linejoin: round;
  293. }
  294. .radar-point {
  295. fill: var(--accent);
  296. stroke: #1A1918;
  297. stroke-width: 2;
  298. }
  299. .radar-label {
  300. font-family: var(--mono);
  301. font-size: 12px;
  302. letter-spacing: 0.2em;
  303. fill: var(--ink-80);
  304. text-transform: uppercase;
  305. }
  306. .radar-label-zh {
  307. font-family: var(--serif-zh);
  308. font-size: 22px;
  309. font-weight: 300;
  310. fill: var(--ink);
  311. letter-spacing: 0.05em;
  312. }
  313. .radar-score {
  314. font-family: var(--mono);
  315. font-size: 13px;
  316. fill: var(--accent);
  317. letter-spacing: 0.08em;
  318. }
  319. .radar-title {
  320. position: absolute;
  321. right: 280px;
  322. top: 160px;
  323. width: 520px;
  324. text-align: center;
  325. font-family: var(--mono);
  326. font-size: 11px;
  327. letter-spacing: 0.28em;
  328. color: var(--muted);
  329. text-transform: uppercase;
  330. opacity: 0;
  331. will-change: opacity;
  332. }
  333. .radar-score-total {
  334. position: absolute;
  335. left: 150px;
  336. top: 170px;
  337. width: 640px;
  338. text-align: left;
  339. opacity: 0;
  340. will-change: opacity;
  341. }
  342. .radar-score-total .score-row {
  343. display: flex;
  344. align-items: baseline;
  345. gap: 24px;
  346. }
  347. .radar-score-total .score-label {
  348. font-family: var(--mono);
  349. font-size: 11px;
  350. letter-spacing: 0.28em;
  351. color: var(--muted);
  352. text-transform: uppercase;
  353. }
  354. .radar-score-total .score-num {
  355. font-family: var(--serif-en);
  356. font-size: 72px;
  357. font-weight: 300;
  358. color: var(--ink);
  359. letter-spacing: -0.02em;
  360. line-height: 1;
  361. }
  362. .radar-score-total .score-num .accent { color: var(--accent); }
  363. .radar-score-total .score-total {
  364. font-family: var(--mono);
  365. font-size: 11px;
  366. letter-spacing: 0.28em;
  367. color: var(--muted);
  368. margin-top: 8px;
  369. text-transform: uppercase;
  370. }
  371. /* ============ Single Fix row (Concept Card lean) ============ */
  372. .fix-lane {
  373. position: absolute;
  374. left: 150px;
  375. bottom: 120px;
  376. width: 1620px;
  377. opacity: 0;
  378. will-change: opacity, transform;
  379. }
  380. .fix-head {
  381. display: flex;
  382. align-items: baseline;
  383. gap: 14px;
  384. margin-bottom: 20px;
  385. padding-bottom: 12px;
  386. border-bottom: 1px solid var(--hairline);
  387. }
  388. .fix-mark {
  389. font-family: var(--mono);
  390. font-size: 13px;
  391. letter-spacing: 0.28em;
  392. color: var(--accent);
  393. text-transform: uppercase;
  394. }
  395. .fix-zh {
  396. font-family: var(--serif-zh);
  397. font-size: 28px;
  398. font-weight: 400;
  399. color: var(--ink);
  400. }
  401. .fix-count {
  402. margin-left: auto;
  403. font-family: var(--mono);
  404. font-size: 11px;
  405. color: var(--muted);
  406. letter-spacing: 0.2em;
  407. }
  408. .fix-row {
  409. position: relative;
  410. font-family: var(--sans);
  411. font-size: 28px;
  412. font-weight: 300;
  413. color: var(--ink);
  414. line-height: 1.45;
  415. padding: 12px 0;
  416. display: flex;
  417. gap: 20px;
  418. align-items: center;
  419. }
  420. .fix-row .idx {
  421. font-family: var(--mono);
  422. font-size: 12px;
  423. color: var(--muted);
  424. letter-spacing: 0.2em;
  425. flex: 0 0 40px;
  426. padding-top: 2px;
  427. }
  428. .fix-row .mono {
  429. font-family: var(--mono);
  430. font-size: 26px;
  431. letter-spacing: 0;
  432. color: var(--accent);
  433. font-weight: 400;
  434. }
  435. .fix-row .arrow {
  436. color: var(--muted);
  437. margin: 0 4px;
  438. }
  439. .fix-severity {
  440. display: inline-block;
  441. padding: 3px 10px;
  442. font-family: var(--mono);
  443. font-size: 11px;
  444. letter-spacing: 0.22em;
  445. color: var(--accent);
  446. border: 1px solid rgba(217,119,87,0.5);
  447. border-radius: 3px;
  448. margin-right: 10px;
  449. vertical-align: 3px;
  450. }
  451. .fix-pulse {
  452. position: absolute;
  453. inset: 4px -12px 4px -12px;
  454. border: 1px solid var(--accent);
  455. border-radius: 4px;
  456. opacity: 0;
  457. pointer-events: none;
  458. will-change: opacity;
  459. box-shadow: 0 0 24px rgba(217,119,87,0.35);
  460. }
  461. /* ============ Brand Reveal (hero-v10 signature) ============ */
  462. .stage-dimmer {
  463. position: absolute;
  464. inset: 0;
  465. background: #000000;
  466. opacity: 0;
  467. z-index: 40;
  468. pointer-events: none;
  469. will-change: opacity;
  470. }
  471. .brand-panel {
  472. position: absolute;
  473. inset: 0;
  474. background: #F5F4F0;
  475. transform: translateY(100%);
  476. display: flex;
  477. flex-direction: column;
  478. align-items: center;
  479. justify-content: center;
  480. z-index: 50;
  481. will-change: transform;
  482. }
  483. .brand-wordmark {
  484. font-family: var(--serif-en);
  485. font-size: 72px;
  486. font-weight: 100;
  487. font-variation-settings: "wght" 100;
  488. letter-spacing: -0.02em;
  489. color: #1A1918;
  490. text-align: center;
  491. line-height: 1;
  492. opacity: 0;
  493. transform: translateY(20px);
  494. will-change: opacity, transform, font-variation-settings, font-weight;
  495. }
  496. .brand-wordmark .accent { color: #D97757; font-weight: inherit; }
  497. .brand-line {
  498. margin-top: 60px;
  499. height: 2px;
  500. width: 0;
  501. background: #D97757;
  502. align-self: center;
  503. will-change: width;
  504. }
  505. </style>
  506. </head>
  507. <body>
  508. <div class="stage" id="stage">
  509. <div class="mark">HUASHU · DESIGN</div>
  510. <div class="mark-right">V2 · 2026</div>
  511. <div class="title-line" id="titleLine">c6 · 专家评审 · 五个维度</div>
  512. <div class="camera" id="camera">
  513. <!-- Subject: design under review -->
  514. <div class="subject" id="subject">
  515. <div class="subject-label">SUBJECT · DRAFT_V3</div>
  516. <div class="subject-dot"></div>
  517. <div class="subject-canvas">
  518. <div class="wf-h1"></div>
  519. <div class="wf-h2"></div>
  520. <div class="wf-row"><div class="bar" style="width:24%"></div><div class="bar" style="width:14%"></div><div class="bar" style="width:20%"></div></div>
  521. <div class="wf-row"><div class="bar" style="width:30%"></div><div class="bar" style="width:10%"></div></div>
  522. <div class="wf-grid">
  523. <div class="wf-card"></div>
  524. <div class="wf-card accent"></div>
  525. <div class="wf-card"></div>
  526. </div>
  527. <div class="wf-foot">
  528. <div class="wf-chip wide"></div>
  529. <div class="wf-chip"></div>
  530. <div class="wf-chip"></div>
  531. </div>
  532. </div>
  533. </div>
  534. <!-- Scanning light -->
  535. <div class="sweep" id="sweep"></div>
  536. <div class="sweep-line" id="sweepLine"></div>
  537. <!-- Radar chart (right) -->
  538. <div class="radar-title" id="radarTitle">五维诊断 · RADAR</div>
  539. <div class="radar-wrap" id="radarWrap">
  540. <svg viewBox="-270 -270 540 540" xmlns="http://www.w3.org/2000/svg">
  541. <!-- Grid rings (5 levels) -->
  542. <g class="radar-grid" id="radarGrid"></g>
  543. <!-- Spokes to 5 axes -->
  544. <g id="radarSpokes"></g>
  545. <!-- Filled polygon -->
  546. <polygon id="radarPoly" class="radar-poly" points="" />
  547. <!-- Points -->
  548. <g id="radarPoints"></g>
  549. <!-- Axis labels -->
  550. <g id="radarLabels"></g>
  551. </svg>
  552. </div>
  553. <div class="radar-score-total" id="radarTotal">
  554. <div class="score-row">
  555. <div class="score-num"><span id="scoreNum">0</span><span class="accent">/50</span></div>
  556. <div>
  557. <div class="score-label">总评 · PASSED</div>
  558. <div class="score-total">五维加权 · 7.4</div>
  559. </div>
  560. </div>
  561. </div>
  562. <!-- Single Fix row: Concept Card lean -->
  563. <div class="fix-lane" id="fixLane">
  564. <div class="fix-head">
  565. <span class="fix-mark">FIX</span>
  566. <span class="fix-zh">修复</span>
  567. <span class="fix-count">01 / 01</span>
  568. </div>
  569. <div class="fix-row">
  570. <span class="idx">01</span>
  571. <span><span class="fix-severity">⚡</span>字距 <span class="mono">0.02em</span><span class="arrow"> → </span><span class="mono">0.04em</span></span>
  572. <div class="fix-pulse" id="fixPulse"></div>
  573. </div>
  574. </div>
  575. </div>
  576. <!-- Brand Reveal (hero-v10 signature) -->
  577. <div class="stage-dimmer" id="stageDimmer"></div>
  578. <div class="brand-panel" id="brandPanel">
  579. <div class="brand-wordmark" id="brandMark">huashu<span class="accent">-</span>design</div>
  580. <div class="brand-line" id="brandLine"></div>
  581. </div>
  582. </div>
  583. <script>
  584. // Auto-scale
  585. function fitStage() {
  586. const stage = document.getElementById('stage');
  587. const sx = window.innerWidth / 1920;
  588. const sy = window.innerHeight / 1080;
  589. const s = Math.min(sx, sy);
  590. stage.style.transform = `translate(-50%, -50%) scale(${s})`;
  591. }
  592. fitStage();
  593. window.addEventListener('resize', fitStage);
  594. // Easings
  595. const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
  596. const expoIn = t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
  597. const cubicInOut = t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
  598. const cubicOut = t => 1 - Math.pow(1 - t, 3);
  599. function lerp(t, a, b, easing) {
  600. if (t <= 0) return a;
  601. if (t >= 1) return b;
  602. const e = easing ? easing(t) : t;
  603. return a + (b - a) * e;
  604. }
  605. function seg(time, start, end) {
  606. if (time <= start) return 0;
  607. if (time >= end) return 1;
  608. return (time - start) / (end - start);
  609. }
  610. // ============ Build radar SVG ============
  611. const RADIUS = 210;
  612. const AXES = [
  613. { zh: '哲学', en: 'PHILOSOPHY', score: 8 },
  614. { zh: '层级', en: 'HIERARCHY', score: 6 },
  615. { zh: '执行', en: 'EXECUTION', score: 8 },
  616. { zh: '功能', en: 'FUNCTION', score: 7 },
  617. { zh: '创新', en: 'INNOVATION', score: 8 },
  618. ];
  619. const N = AXES.length;
  620. function axisPoint(i, r) {
  621. // Start at top (-90deg), clockwise
  622. const angle = -Math.PI / 2 + (2 * Math.PI * i) / N;
  623. return [Math.cos(angle) * r, Math.sin(angle) * r];
  624. }
  625. // Grid rings (polygons at 5 levels)
  626. const gridG = document.getElementById('radarGrid');
  627. for (let level = 1; level <= 5; level++) {
  628. const r = (RADIUS * level) / 5;
  629. const pts = [];
  630. for (let i = 0; i < N; i++) {
  631. const [x, y] = axisPoint(i, r);
  632. pts.push(`${x.toFixed(2)},${y.toFixed(2)}`);
  633. }
  634. const poly = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
  635. poly.setAttribute('points', pts.join(' '));
  636. poly.setAttribute('fill', 'none');
  637. poly.setAttribute('stroke', level === 5 ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.07)');
  638. poly.setAttribute('stroke-width', '1');
  639. gridG.appendChild(poly);
  640. }
  641. // Spokes
  642. const spokesG = document.getElementById('radarSpokes');
  643. for (let i = 0; i < N; i++) {
  644. const [x, y] = axisPoint(i, RADIUS);
  645. const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
  646. line.setAttribute('x1', 0);
  647. line.setAttribute('y1', 0);
  648. line.setAttribute('x2', x.toFixed(2));
  649. line.setAttribute('y2', y.toFixed(2));
  650. line.setAttribute('class', 'radar-spoke');
  651. spokesG.appendChild(line);
  652. }
  653. // Labels (position outside). ZH sits at a base radial distance; EN stacks
  654. // below it with a fixed vertical offset to avoid overlap on the side axes.
  655. const labelsG = document.getElementById('radarLabels');
  656. AXES.forEach((axis, i) => {
  657. const angle = -Math.PI / 2 + (2 * Math.PI * i) / N;
  658. const dirX = Math.cos(angle);
  659. const dirY = Math.sin(angle);
  660. // text-anchor based on horizontal direction
  661. let anchor = 'middle';
  662. if (dirX > 0.3) anchor = 'start';
  663. else if (dirX < -0.3) anchor = 'end';
  664. const baseRadial = RADIUS + 36;
  665. const [bx, by] = axisPoint(i, baseRadial);
  666. // ZH label
  667. const zhText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
  668. zhText.setAttribute('x', bx.toFixed(2));
  669. zhText.setAttribute('y', by.toFixed(2));
  670. zhText.setAttribute('text-anchor', anchor);
  671. zhText.setAttribute('dominant-baseline', 'middle');
  672. zhText.setAttribute('class', 'radar-label-zh');
  673. zhText.textContent = axis.zh;
  674. labelsG.appendChild(zhText);
  675. // EN label stacks vertically below ZH (always +22px in y)
  676. const enText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
  677. enText.setAttribute('x', bx.toFixed(2));
  678. enText.setAttribute('y', (by + 22).toFixed(2));
  679. enText.setAttribute('text-anchor', anchor);
  680. enText.setAttribute('dominant-baseline', 'middle');
  681. enText.setAttribute('class', 'radar-label');
  682. enText.textContent = axis.en;
  683. enText.setAttribute('opacity', '0');
  684. enText.setAttribute('data-type', 'en-label');
  685. labelsG.appendChild(enText);
  686. });
  687. // Points (initial: center)
  688. const pointsG = document.getElementById('radarPoints');
  689. const pointEls = AXES.map((axis, i) => {
  690. const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
  691. circle.setAttribute('cx', 0);
  692. circle.setAttribute('cy', 0);
  693. circle.setAttribute('r', 5);
  694. circle.setAttribute('class', 'radar-point');
  695. circle.setAttribute('opacity', '0');
  696. pointsG.appendChild(circle);
  697. return circle;
  698. });
  699. const radarPoly = document.getElementById('radarPoly');
  700. // ============ Timeline (10s) ============
  701. // Beat 1 (0-2s): title + subject enters
  702. // Beat 2 (2-8s):
  703. // 2.0-3.8: light sweep top → bottom (1.8s)
  704. // 3.2-4.8: radar grid fades in + polygon + points grow from center
  705. // 4.8-5.2: score count up
  706. // 5.0-6.0: Keep col ripple in
  707. // 5.5-6.5: Fix col ripple in
  708. // 6.0-7.0: Quick Wins col ripple in
  709. // 7.0-8.0: hold
  710. // Beat 3 (8-10s): push-in camera to fix[0] + pulse (8-9), brand reveal (8.0-10.0)
  711. const titleLine = document.getElementById('titleLine');
  712. const subject = document.getElementById('subject');
  713. const sweep = document.getElementById('sweep');
  714. const sweepLine = document.getElementById('sweepLine');
  715. const radarTitle = document.getElementById('radarTitle');
  716. const radarWrap = document.getElementById('radarWrap');
  717. const radarTotal = document.getElementById('radarTotal');
  718. const scoreNum = document.getElementById('scoreNum');
  719. const fixLane = document.getElementById('fixLane');
  720. const fixPulse = document.getElementById('fixPulse');
  721. const camera = document.getElementById('camera');
  722. const stageDimmer = document.getElementById('stageDimmer');
  723. const brandPanel = document.getElementById('brandPanel');
  724. const brandMark = document.getElementById('brandMark');
  725. const brandLine = document.getElementById('brandLine');
  726. const DURATION = 10.0;
  727. let startTime = null;
  728. let loop = true;
  729. if (window.__recording === true) loop = false;
  730. function tick(now) {
  731. if (startTime === null) startTime = now;
  732. let t = (now - startTime) / 1000;
  733. if (t >= DURATION) {
  734. if (loop) { startTime = now; t = 0; }
  735. else { t = DURATION; }
  736. }
  737. // Title fade in/out
  738. const titleIn = seg(t, 0.2, 1.2);
  739. const titleOut = seg(t, 7.6, 8.0);
  740. titleLine.style.opacity = Math.min(cubicOut(titleIn), 1 - titleOut);
  741. titleLine.style.transform = `translateX(-50%) translateY(${lerp(titleIn, -6, 0, cubicOut)}px)`;
  742. // Subject appears Beat 1
  743. const subjectIn = seg(t, 0.4, 1.8);
  744. subject.style.opacity = expoOut(subjectIn);
  745. subject.style.transform = `translateY(${lerp(subjectIn, 14, 0, expoOut)}px)`;
  746. // Subject dims after sweep completes (during Beat 2 to keep focus right)
  747. const subjectDim = seg(t, 4.4, 5.6);
  748. const dimFactor = lerp(subjectDim, 1.0, 0.38, cubicInOut);
  749. subject.style.filter = `saturate(${lerp(subjectDim, 1.0, 0.5, cubicInOut)}) brightness(${dimFactor})`;
  750. // Light sweep: 2.0-3.8 top to bottom
  751. const sweepProgress = seg(t, 2.0, 3.8);
  752. const sweepOp = (t < 2.0 || t > 4.2) ? 0 :
  753. (t < 2.2 ? seg(t, 2.0, 2.2) :
  754. t < 3.7 ? 1 :
  755. 1 - seg(t, 3.7, 4.2));
  756. sweep.style.opacity = sweepOp * 0.95;
  757. sweepLine.style.opacity = sweepOp * 1.0;
  758. // Move from y=250 to y=700 (subject top 310 to bottom 770)
  759. const sweepY = lerp(sweepProgress, -70, 410, cubicInOut);
  760. sweep.style.transform = `translateY(${sweepY}px)`;
  761. sweepLine.style.transform = `translateY(${sweepY + 70}px)`;
  762. // Radar title + wrap appear 3.2
  763. const radarIn = seg(t, 3.2, 4.0);
  764. radarTitle.style.opacity = cubicOut(radarIn);
  765. radarWrap.style.opacity = cubicOut(radarIn);
  766. radarWrap.style.transform = `scale(${lerp(radarIn, 0.92, 1.0, expoOut)})`;
  767. // Radar grid strokes already visible once wrap fades; animate grid via stroke-dasharray trick would be overkill.
  768. // Instead, grow polygon + points from center (3.6-4.8)
  769. const polyGrow = seg(t, 3.6, 4.8);
  770. const polyT = expoOut(polyGrow);
  771. const polyPts = [];
  772. AXES.forEach((axis, i) => {
  773. const targetR = (axis.score / 10) * RADIUS;
  774. const r = targetR * polyT;
  775. const [x, y] = axisPoint(i, r);
  776. polyPts.push(`${x.toFixed(2)},${y.toFixed(2)}`);
  777. const pt = pointEls[i];
  778. pt.setAttribute('cx', x.toFixed(2));
  779. pt.setAttribute('cy', y.toFixed(2));
  780. pt.setAttribute('opacity', polyT.toFixed(2));
  781. });
  782. radarPoly.setAttribute('points', polyPts.join(' '));
  783. // EN labels fade in slightly later
  784. const enLabelIn = seg(t, 4.2, 4.8);
  785. document.querySelectorAll('[data-type="en-label"]').forEach(el => {
  786. el.setAttribute('opacity', cubicOut(enLabelIn).toFixed(2));
  787. });
  788. // Score count up 4.6-5.4, target total = 37
  789. const scoreT = seg(t, 4.6, 5.4);
  790. const total = AXES.reduce((s, a) => s + a.score, 0); // 37
  791. const shown = Math.round(lerp(scoreT, 0, total, cubicOut));
  792. scoreNum.textContent = shown;
  793. radarTotal.style.opacity = cubicOut(seg(t, 4.4, 5.0));
  794. // Fix lane ripple in (5.3-6.1)
  795. const fixRip = seg(t, 5.3, 6.1);
  796. fixLane.style.opacity = expoOut(fixRip);
  797. fixLane.style.transform = `translateY(${lerp(fixRip, 24, 0, expoOut)}px)`;
  798. // Beat 3: Push-in camera to Fix row + pulse (7.4-8.0)
  799. const pushT = seg(t, 7.4, 8.0);
  800. const scale = lerp(pushT, 1.0, 1.18, cubicInOut);
  801. camera.style.transform = `scale(${scale})`;
  802. // Fix pulse border: blink 2 times between 7.6-8.0
  803. const pulseOp = t < 7.6 ? 0 :
  804. t < 8.0 ? (0.4 + 0.6 * Math.abs(Math.sin((t - 7.6) * Math.PI * 2.4))) :
  805. 0;
  806. fixPulse.style.opacity = pulseOp;
  807. // ============ Brand Reveal (hero-v10 signature, aligned) ============
  808. // [T-2.0 → T-1.7s] i.e. 8.0-8.3: scene fade to black (0.3s)
  809. const soK = seg(t, 8.0, 8.3);
  810. stageDimmer.style.opacity = cubicOut(soK);
  811. const sceneFade = seg(t, 8.0, 8.3);
  812. camera.style.opacity = 1 - cubicOut(sceneFade);
  813. // [T-1.7 → T-1.3s] i.e. 8.3-8.7: cream panel slides from bottom (0.4s, expoOut)
  814. const panelT = seg(t, 8.3, 8.7);
  815. const panelY = lerp(panelT, 100, 0, expoOut);
  816. brandPanel.style.transform = `translateY(${panelY}%)`;
  817. // [T-1.3 → T-0.7s] i.e. 8.7-9.3: wordmark wght 100→500 + y 20→0 + opacity 0→1 (0.6s)
  818. const markT = seg(t, 8.7, 9.3);
  819. const markE = expoOut(markT);
  820. const wght = 100 + (500 - 100) * markE;
  821. brandMark.style.opacity = markE;
  822. brandMark.style.transform = `translateY(${20 * (1 - markE)}px)`;
  823. brandMark.style.fontWeight = Math.round(wght);
  824. brandMark.style.fontVariationSettings = `"wght" ${wght.toFixed(0)}`;
  825. // [T-0.7 → T-0.3s] i.e. 9.3-9.7: orange line width 0→280 (0.4s, cubicOut)
  826. const lineT = seg(t, 9.3, 9.7);
  827. brandLine.style.width = `${lerp(lineT, 0, 280, cubicOut)}px`;
  828. // [T-0.3 → T] hold
  829. if (!window.__ready) window.__ready = true;
  830. if (loop || t < DURATION) requestAnimationFrame(tick);
  831. }
  832. (document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve())
  833. .then(() => requestAnimationFrame(tick));
  834. </script>
  835. </body>
  836. </html>