w2-junior-designer-en.html 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983
  1. <!doctype html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8" />
  5. <title>w2 · Rough draft now beats perfect draft later</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. --bad: #6E3A2E; /* 失败暗红调,不刺眼 */
  21. --bad-strong: #C85A42; /* 失败叉号强调,对比度提升 */
  22. --cool: rgba(255,255,255,0.42); /* 冷色参考线(左路径) */
  23. --cd-bg: #F5F4F0;
  24. --cd-panel: #FFFFFF;
  25. --cd-ink: #1A1918;
  26. --serif-zh: "Noto Serif SC", "Songti SC", serif;
  27. --serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
  28. --sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
  29. --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
  30. }
  31. html, body {
  32. margin: 0; padding: 0;
  33. background: #000;
  34. overflow: hidden;
  35. font-family: var(--sans);
  36. color: var(--ink);
  37. -webkit-font-smoothing: antialiased;
  38. }
  39. * { box-sizing: border-box; }
  40. .stage {
  41. position: fixed;
  42. top: 50%; left: 50%;
  43. width: 1920px; height: 1080px;
  44. transform-origin: center center;
  45. background: var(--bg);
  46. overflow: hidden;
  47. }
  48. /* Film grain */
  49. .stage::before {
  50. content: '';
  51. position: absolute;
  52. inset: 0;
  53. 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>");
  54. opacity: 0.02;
  55. pointer-events: none;
  56. z-index: 100;
  57. }
  58. /* Chrome · watermark */
  59. .mark {
  60. position: absolute;
  61. top: 48px; left: 64px;
  62. font-family: var(--mono);
  63. font-size: 13px;
  64. letter-spacing: 0.2em;
  65. color: rgba(255,255,255,1);
  66. opacity: 0.16;
  67. pointer-events: none;
  68. z-index: 50;
  69. }
  70. .mark-right {
  71. position: absolute;
  72. top: 48px; right: 64px;
  73. font-family: var(--mono);
  74. font-size: 13px;
  75. letter-spacing: 0.2em;
  76. color: rgba(255,255,255,1);
  77. opacity: 0.16;
  78. pointer-events: none;
  79. z-index: 50;
  80. }
  81. /* Title */
  82. .title-line {
  83. position: absolute;
  84. top: 112px;
  85. left: 50%;
  86. transform: translateX(-50%);
  87. font-family: var(--mono);
  88. font-size: 14px;
  89. letter-spacing: 0.28em;
  90. color: var(--muted);
  91. text-transform: uppercase;
  92. opacity: 0;
  93. will-change: opacity;
  94. white-space: nowrap;
  95. }
  96. /* Splitter — horizontal line dividing the two halves */
  97. .splitter {
  98. position: absolute;
  99. left: 160px;
  100. right: 160px;
  101. top: 50%;
  102. height: 1px;
  103. background: var(--hairline);
  104. transform: scaleX(0);
  105. transform-origin: left center;
  106. will-change: transform;
  107. z-index: 5;
  108. }
  109. .splitter-label {
  110. position: absolute;
  111. top: 50%;
  112. left: 50%;
  113. transform: translate(-50%, -50%);
  114. background: var(--bg);
  115. padding: 0 28px;
  116. font-family: var(--mono);
  117. font-size: 11px;
  118. letter-spacing: 0.32em;
  119. color: var(--muted);
  120. z-index: 6;
  121. opacity: 0;
  122. will-change: opacity;
  123. white-space: nowrap;
  124. }
  125. /* ======================================================
  126. * TOP HALF · 闷头一把梭(3 hours, all at once)
  127. * ====================================================== */
  128. .half-top {
  129. position: absolute;
  130. top: 200px;
  131. left: 160px;
  132. right: 160px;
  133. height: 300px;
  134. opacity: 0;
  135. will-change: opacity;
  136. }
  137. .half-label {
  138. font-family: var(--mono);
  139. font-size: 13px;
  140. letter-spacing: 0.24em;
  141. color: var(--muted);
  142. text-transform: uppercase;
  143. margin-bottom: 24px;
  144. display: flex;
  145. align-items: center;
  146. gap: 12px;
  147. }
  148. .half-label .tag {
  149. padding: 3px 10px;
  150. border: 1px solid var(--hairline);
  151. border-radius: 2px;
  152. color: var(--ink-60);
  153. }
  154. .half-top .half-label .tag { border-color: rgba(160,74,56,0.4); color: rgba(200,120,100,0.85); }
  155. .half-label .zh {
  156. font-family: var(--serif-zh);
  157. font-size: 22px;
  158. font-weight: 400;
  159. letter-spacing: 0.02em;
  160. color: var(--ink-80);
  161. margin-left: 4px;
  162. }
  163. /* Single huge terminal panel */
  164. .terminal-big {
  165. width: 100%;
  166. height: 200px;
  167. background: rgba(20, 20, 20, 1);
  168. border: 1px solid var(--hairline);
  169. border-radius: 10px;
  170. overflow: hidden;
  171. box-shadow:
  172. 0 0 0 1px rgba(255,255,255,0.02),
  173. 0 40px 80px -30px rgba(0,0,0,0.7);
  174. position: relative;
  175. }
  176. .tty-head {
  177. display: flex;
  178. align-items: center;
  179. gap: 8px;
  180. padding: 14px 18px;
  181. border-bottom: 1px solid var(--hairline);
  182. background: rgba(255,255,255,0.02);
  183. }
  184. .tty-head .d {
  185. width: 10px; height: 10px; border-radius: 50%;
  186. background: var(--hairline);
  187. }
  188. .tty-title {
  189. margin-left: 14px;
  190. color: var(--muted);
  191. font-size: 12px;
  192. font-family: var(--mono);
  193. letter-spacing: 0.04em;
  194. }
  195. .tty-body {
  196. padding: 28px 30px;
  197. font-family: var(--mono);
  198. font-size: 17px;
  199. line-height: 1.6;
  200. color: rgba(255,255,255,0.86);
  201. }
  202. .tty-body .line {
  203. opacity: 0;
  204. will-change: opacity;
  205. }
  206. .tty-body .prompt { color: var(--accent); margin-right: 10px; }
  207. .tty-body .dim { color: var(--muted); }
  208. /* The long running progress bar (simulated "3-hour render") */
  209. .progress-row {
  210. margin-top: 14px;
  211. display: flex;
  212. align-items: center;
  213. gap: 14px;
  214. font-family: var(--mono);
  215. font-size: 14px;
  216. color: var(--ink-60);
  217. opacity: 0;
  218. will-change: opacity;
  219. }
  220. .progress-bar {
  221. flex: 1;
  222. height: 4px;
  223. background: var(--hairline);
  224. border-radius: 2px;
  225. position: relative;
  226. overflow: hidden;
  227. }
  228. .progress-bar-fill {
  229. position: absolute;
  230. top: 0; left: 0;
  231. height: 100%;
  232. background: var(--accent);
  233. width: 0%;
  234. will-change: width, background;
  235. }
  236. .progress-bar.failed .progress-bar-fill {
  237. background: var(--bad-strong);
  238. }
  239. .progress-pct {
  240. font-variant-numeric: tabular-nums;
  241. letter-spacing: 0.04em;
  242. min-width: 54px;
  243. text-align: right;
  244. }
  245. .progress-hours {
  246. color: var(--muted);
  247. font-size: 12px;
  248. letter-spacing: 0.12em;
  249. }
  250. .progress-row.failed {
  251. color: var(--bad-strong);
  252. }
  253. /* Big X overlay for failure stamp */
  254. .fail-stamp {
  255. position: absolute;
  256. right: 32px;
  257. top: 50%;
  258. transform: translateY(-50%) rotate(-8deg);
  259. width: 120px; height: 120px;
  260. pointer-events: none;
  261. opacity: 0;
  262. will-change: opacity, transform;
  263. z-index: 10;
  264. }
  265. .fail-stamp svg { width: 100%; height: 100%; }
  266. .fail-stamp .stamp-text {
  267. position: absolute;
  268. bottom: -22px;
  269. left: 50%;
  270. transform: translateX(-50%);
  271. font-family: var(--mono);
  272. font-size: 10px;
  273. letter-spacing: 0.32em;
  274. color: var(--bad-strong);
  275. white-space: nowrap;
  276. }
  277. /* ======================================================
  278. * BOTTOM HALF · 尽早 show(small iterations)
  279. * ====================================================== */
  280. .half-bot {
  281. position: absolute;
  282. top: 580px;
  283. left: 160px;
  284. right: 160px;
  285. height: 340px;
  286. opacity: 0;
  287. will-change: opacity;
  288. }
  289. .half-bot .half-label .tag {
  290. border-color: rgba(217,119,87,0.35);
  291. color: var(--accent);
  292. }
  293. .iter-row {
  294. display: flex;
  295. gap: 32px;
  296. align-items: flex-end;
  297. height: 240px;
  298. margin-top: 12px;
  299. }
  300. .iter-panel {
  301. flex: 1;
  302. background: rgba(20, 20, 20, 1);
  303. border: 1px solid var(--hairline);
  304. border-radius: 8px;
  305. overflow: hidden;
  306. height: 100%;
  307. position: relative;
  308. opacity: 0;
  309. transform: translateY(20px);
  310. will-change: opacity, transform;
  311. display: flex;
  312. flex-direction: column;
  313. }
  314. .iter-panel .ip-head {
  315. padding: 10px 14px;
  316. border-bottom: 1px solid var(--hairline);
  317. font-family: var(--mono);
  318. font-size: 11px;
  319. letter-spacing: 0.16em;
  320. color: var(--muted);
  321. display: flex;
  322. align-items: center;
  323. justify-content: space-between;
  324. }
  325. .iter-panel .ip-version {
  326. color: var(--accent);
  327. font-weight: 500;
  328. }
  329. .iter-panel .ip-body {
  330. flex: 1;
  331. padding: 16px 18px;
  332. display: flex;
  333. flex-direction: column;
  334. justify-content: center;
  335. gap: 10px;
  336. }
  337. /* Rough mockup blocks that grow more detailed each iteration */
  338. .iter-panel .m-block {
  339. height: 8px;
  340. background: var(--dim);
  341. border-radius: 2px;
  342. opacity: 0.8;
  343. }
  344. .iter-panel .m-block.accent { background: var(--accent); opacity: 0.8; }
  345. .iter-panel .m-block.short { width: 40%; }
  346. .iter-panel .m-block.med { width: 70%; }
  347. .iter-panel .m-block.full { width: 100%; }
  348. .iter-panel .m-block.tall { height: 24px; }
  349. .iter-panel .m-block.big { height: 40px; }
  350. .iter-panel .nod {
  351. position: absolute;
  352. top: 10px;
  353. right: 14px;
  354. width: 16px; height: 16px;
  355. opacity: 0;
  356. will-change: opacity, transform;
  357. }
  358. .iter-panel .nod svg {
  359. width: 100%; height: 100%;
  360. stroke: var(--accent);
  361. fill: none;
  362. stroke-width: 2;
  363. }
  364. .iter-panel .ip-minutes {
  365. position: absolute;
  366. bottom: 10px;
  367. left: 14px;
  368. font-family: var(--mono);
  369. font-size: 10px;
  370. letter-spacing: 0.12em;
  371. color: var(--muted);
  372. }
  373. /* Rising curve visualization for bottom half */
  374. .curve-wrap {
  375. position: absolute;
  376. right: 0;
  377. bottom: 0;
  378. width: 340px;
  379. height: 180px;
  380. opacity: 0;
  381. will-change: opacity;
  382. }
  383. .curve-wrap svg {
  384. width: 100%;
  385. height: 100%;
  386. overflow: visible;
  387. }
  388. .curve-wrap .axis {
  389. stroke: var(--hairline);
  390. stroke-width: 1;
  391. fill: none;
  392. }
  393. .curve-wrap .curve-path {
  394. stroke: var(--accent);
  395. stroke-width: 2;
  396. fill: none;
  397. stroke-linecap: round;
  398. stroke-linejoin: round;
  399. }
  400. .curve-wrap .curve-dot {
  401. fill: var(--accent);
  402. r: 3;
  403. }
  404. .curve-wrap .curve-label {
  405. font-family: var(--mono);
  406. font-size: 9px;
  407. fill: var(--muted);
  408. letter-spacing: 0.12em;
  409. }
  410. /* ======================================================
  411. * BEAT 3 · Full comparison chart crossfade
  412. * ====================================================== */
  413. .final-chart {
  414. position: absolute;
  415. left: 50%;
  416. top: 50%;
  417. transform: translate(-50%, -50%);
  418. width: 1280px;
  419. height: 620px;
  420. opacity: 0;
  421. will-change: opacity;
  422. z-index: 60;
  423. }
  424. .final-chart svg {
  425. width: 100%; height: 100%;
  426. overflow: visible;
  427. }
  428. .final-chart .axis {
  429. stroke: var(--hairline);
  430. stroke-width: 1;
  431. fill: none;
  432. }
  433. .final-chart .axis-label {
  434. font-family: var(--mono);
  435. font-size: 13px;
  436. fill: var(--muted);
  437. letter-spacing: 0.16em;
  438. }
  439. .final-chart .tick-label {
  440. font-family: var(--mono);
  441. font-size: 11px;
  442. fill: var(--dim);
  443. letter-spacing: 0.06em;
  444. }
  445. .final-chart .curve-a {
  446. stroke: var(--cool);
  447. stroke-width: 2;
  448. fill: none;
  449. stroke-linecap: round;
  450. stroke-linejoin: round;
  451. }
  452. .final-chart .curve-a-dash {
  453. stroke: var(--bad-strong);
  454. stroke-width: 2.5;
  455. fill: none;
  456. stroke-dasharray: 5 7;
  457. stroke-linecap: round;
  458. }
  459. .final-chart .curve-b {
  460. stroke: var(--accent);
  461. stroke-width: 3;
  462. fill: none;
  463. stroke-linecap: round;
  464. stroke-linejoin: round;
  465. }
  466. .final-chart .curve-b-glow {
  467. stroke: var(--accent);
  468. stroke-width: 6;
  469. fill: none;
  470. opacity: 0.18;
  471. stroke-linecap: round;
  472. stroke-linejoin: round;
  473. }
  474. .final-chart .curve-dot {
  475. fill: var(--accent);
  476. }
  477. .final-chart .fail-dot {
  478. fill: none;
  479. stroke: var(--bad-strong);
  480. stroke-width: 2.5;
  481. }
  482. .final-chart .cool-dot {
  483. fill: var(--cool);
  484. }
  485. .final-chart .anchor-label {
  486. font-family: var(--serif-zh);
  487. font-size: 20px;
  488. font-weight: 400;
  489. letter-spacing: 0.02em;
  490. }
  491. .final-chart .anchor-en {
  492. font-family: var(--mono);
  493. font-size: 11px;
  494. letter-spacing: 0.18em;
  495. text-transform: uppercase;
  496. }
  497. /* ======================================================
  498. * BRAND REVEAL — 统一动作
  499. * ====================================================== */
  500. .brand-sheet {
  501. position: absolute;
  502. inset: 0;
  503. background: var(--cd-bg);
  504. transform: translateY(100%);
  505. will-change: transform;
  506. z-index: 80;
  507. }
  508. .brand-reveal {
  509. position: absolute;
  510. inset: 0;
  511. z-index: 81;
  512. display: flex;
  513. flex-direction: column;
  514. align-items: center;
  515. justify-content: center;
  516. opacity: 0;
  517. will-change: opacity;
  518. }
  519. .brand-reveal .wordmark {
  520. font-family: var(--sans);
  521. font-weight: 100;
  522. font-size: 128px;
  523. letter-spacing: -0.045em;
  524. color: var(--cd-ink);
  525. line-height: 1;
  526. }
  527. .brand-reveal .wordmark .accent { color: var(--accent-deep); }
  528. .brand-reveal .underline {
  529. width: 0;
  530. height: 2px;
  531. background: var(--accent);
  532. margin-top: 36px;
  533. will-change: width;
  534. }
  535. </style>
  536. </head>
  537. <body>
  538. <div class="stage" id="stage">
  539. <div class="mark">HUASHU · DESIGN</div>
  540. <div class="mark-right">V2 · 2026</div>
  541. <div class="title-line" id="titleLine">w2 · rough draft now beats perfect draft later</div>
  542. <!-- Splitter -->
  543. <div class="splitter" id="splitter"></div>
  544. <div class="splitter-label" id="splitterLabel">VS</div>
  545. <!-- ============ TOP HALF: All-at-once ============ -->
  546. <div class="half-top" id="halfTop">
  547. <div class="half-label">
  548. <span class="tag">A</span>
  549. <span class="zh" style="font-family: var(--serif-en); font-style: italic; letter-spacing: 0.01em;">All-at-once</span>
  550. <span style="font-family: var(--mono); color: var(--muted); letter-spacing: 0.18em; font-size: 11px; margin-left: auto;">3&nbsp;HOUR&nbsp;SESSION</span>
  551. </div>
  552. <div class="terminal-big">
  553. <div class="tty-head">
  554. <div class="d"></div><div class="d"></div><div class="d"></div>
  555. <div class="tty-title">designer@studio · 3h session</div>
  556. </div>
  557. <div class="tty-body">
  558. <div class="line" id="ttyL1"><span class="prompt">$</span>build final_design.html <span class="dim">// v1.0 · ship it all at once</span></div>
  559. <div class="progress-row" id="progRow">
  560. <div class="progress-bar" id="progBar">
  561. <div class="progress-bar-fill" id="progFill"></div>
  562. </div>
  563. <span class="progress-pct" id="progPct">0%</span>
  564. <span class="progress-hours" id="progHours">03:00:00</span>
  565. </div>
  566. </div>
  567. <div class="fail-stamp" id="failStamp">
  568. <svg viewBox="0 0 120 120">
  569. <circle cx="60" cy="60" r="52" fill="none" stroke="#A04A38" stroke-width="3"/>
  570. <path d="M 38 38 L 82 82 M 82 38 L 38 82" stroke="#A04A38" stroke-width="4" stroke-linecap="round"/>
  571. </svg>
  572. <div class="stamp-text">REJECTED</div>
  573. </div>
  574. </div>
  575. </div>
  576. <!-- ============ BOTTOM HALF: Show early ============ -->
  577. <div class="half-bot" id="halfBot">
  578. <div class="half-label">
  579. <span class="tag">B</span>
  580. <span class="zh" style="font-family: var(--serif-en); font-style: italic; letter-spacing: 0.01em;">Show early</span>
  581. <span style="font-family: var(--mono); color: var(--muted); letter-spacing: 0.18em; font-size: 11px; margin-left: auto;">SMALL&nbsp;ITERATIONS</span>
  582. </div>
  583. <div class="iter-row">
  584. <div class="iter-panel" id="iter1">
  585. <div class="ip-head">
  586. <span>draft · v1</span>
  587. <span class="ip-version">15 min</span>
  588. </div>
  589. <div class="ip-body">
  590. <div class="m-block short"></div>
  591. <div class="m-block med"></div>
  592. <div class="m-block short"></div>
  593. </div>
  594. <div class="nod" id="nod1">
  595. <svg viewBox="0 0 16 16"><path d="M3 8 L7 12 L13 4"/></svg>
  596. </div>
  597. </div>
  598. <div class="iter-panel" id="iter2">
  599. <div class="ip-head">
  600. <span>draft · v2</span>
  601. <span class="ip-version">25 min</span>
  602. </div>
  603. <div class="ip-body">
  604. <div class="m-block full tall"></div>
  605. <div class="m-block med"></div>
  606. <div class="m-block short"></div>
  607. <div class="m-block med accent"></div>
  608. </div>
  609. <div class="nod" id="nod2">
  610. <svg viewBox="0 0 16 16"><path d="M3 8 L7 12 L13 4"/></svg>
  611. </div>
  612. </div>
  613. <div class="iter-panel" id="iter3">
  614. <div class="ip-head">
  615. <span>draft · v3</span>
  616. <span class="ip-version">35 min</span>
  617. </div>
  618. <div class="ip-body">
  619. <div class="m-block full big"></div>
  620. <div class="m-block full tall accent"></div>
  621. <div class="m-block med"></div>
  622. <div class="m-block full"></div>
  623. <div class="m-block short"></div>
  624. </div>
  625. <div class="nod" id="nod3">
  626. <svg viewBox="0 0 16 16"><path d="M3 8 L7 12 L13 4"/></svg>
  627. </div>
  628. </div>
  629. </div>
  630. </div>
  631. <!-- ============ Beat 3 · Final comparison chart ============ -->
  632. <div class="final-chart" id="finalChart">
  633. <svg viewBox="0 0 1280 620" preserveAspectRatio="xMidYMid meet">
  634. <!-- Axes -->
  635. <line class="axis" x1="110" y1="60" x2="110" y2="520"/>
  636. <line class="axis" x1="110" y1="520" x2="1200" y2="520"/>
  637. <!-- Y-axis label -->
  638. <text class="axis-label" x="58" y="290" transform="rotate(-90 58 290)" text-anchor="middle">QUALITY</text>
  639. <!-- X-axis label -->
  640. <text class="axis-label" x="655" y="570" text-anchor="middle">TIME</text>
  641. <!-- Tick marks -->
  642. <text class="tick-label" x="110" y="545" text-anchor="middle">0</text>
  643. <text class="tick-label" x="290" y="545" text-anchor="middle">15m</text>
  644. <text class="tick-label" x="480" y="545" text-anchor="middle">25m</text>
  645. <text class="tick-label" x="680" y="545" text-anchor="middle">35m</text>
  646. <text class="tick-label" x="1200" y="545" text-anchor="middle">3h</text>
  647. <!-- Curve A (All-at-once): flat crawl near zero, late spike, then crash -->
  648. <path class="curve-a" id="curveA"
  649. d="M 110 500 L 400 495 L 700 490 L 1000 485 L 1140 180" />
  650. <path class="curve-a-dash" id="curveACrash"
  651. d="M 1140 180 L 1200 510" />
  652. <circle class="fail-dot" id="failDot" cx="1140" cy="180" r="9"/>
  653. <g id="failX" opacity="0">
  654. <line x1="1130" y1="170" x2="1150" y2="190" stroke="#C85A42" stroke-width="2.5" stroke-linecap="round"/>
  655. <line x1="1150" y1="170" x2="1130" y2="190" stroke="#C85A42" stroke-width="2.5" stroke-linecap="round"/>
  656. </g>
  657. <text class="anchor-label" x="1200" y="150" fill="#C85A42" text-anchor="end" style="font-family: var(--serif-en); font-style: italic;">All-at-once</text>
  658. <text class="anchor-en" x="1200" y="170" fill="#C85A42" text-anchor="end">REJECTED</text>
  659. <!-- Curve B (Show early): steady step rise across first 35 min -->
  660. <path class="curve-b-glow" id="curveBGlow"
  661. d="M 110 500 L 290 380 L 480 270 L 680 140" />
  662. <path class="curve-b" id="curveB"
  663. d="M 110 500 L 290 380 L 480 270 L 680 140" />
  664. <circle class="curve-dot" cx="290" cy="380" r="6"/>
  665. <circle class="curve-dot" cx="480" cy="270" r="6"/>
  666. <circle class="curve-dot" cx="680" cy="140" r="8"/>
  667. <text class="anchor-label" x="680" y="115" fill="#D97757" text-anchor="middle" style="font-family: var(--serif-en); font-style: italic;">Show early</text>
  668. <text class="anchor-en" x="680" y="96" fill="#D97757" text-anchor="middle">SHIPPED</text>
  669. <text class="tick-label" x="555" y="477" text-anchor="middle" fill="rgba(255,255,255,0.3)" style="letter-spacing: 0.12em;">— 3 hours silence —</text>
  670. </svg>
  671. </div>
  672. <!-- Brand reveal -->
  673. <div class="brand-sheet" id="brandSheet"></div>
  674. <div class="brand-reveal" id="brandReveal">
  675. <div class="wordmark">huashu<span class="accent"> · </span>design</div>
  676. <div class="underline" id="brandUnderline"></div>
  677. </div>
  678. </div>
  679. <script>
  680. // Auto-scale stage
  681. function fitStage() {
  682. const stage = document.getElementById('stage');
  683. const sx = window.innerWidth / 1920;
  684. const sy = window.innerHeight / 1080;
  685. const s = Math.min(sx, sy);
  686. stage.style.transform = `translate(-50%, -50%) scale(${s})`;
  687. }
  688. fitStage();
  689. window.addEventListener('resize', fitStage);
  690. // Easings
  691. const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
  692. const expoIn = t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
  693. const cubicInOut = t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
  694. const cubicOut = t => 1 - Math.pow(1 - t, 3);
  695. const cubicIn = t => t * t * t;
  696. function lerp(t, a, b, easing) {
  697. if (t <= 0) return a;
  698. if (t >= 1) return b;
  699. const e = easing ? easing(t) : t;
  700. return a + (b - a) * e;
  701. }
  702. function seg(time, start, end) {
  703. if (time <= start) return 0;
  704. if (time >= end) return 1;
  705. return (time - start) / (end - start);
  706. }
  707. // ────────────────────────────────────
  708. // Timeline — total 12s (Beat 1: 0-2 · Beat 2: 2-10 · Beat 3: 10-12)
  709. //
  710. // 0.0-0.6 title + splitter grow
  711. // 0.6-1.4 two half-labels fade in (top first, then bot)
  712. // 1.4-2.0 top terminal line 1 types; bot panel 1 enters
  713. //
  714. // Top track (闷头):
  715. // 2.0-7.8 progress bar crawls from 0 to 99% (slow, painful)
  716. // 7.8-8.4 stuck at 99%
  717. // 8.4-8.9 fail stamp lands + bar turns red + bar drops to 0
  718. //
  719. // Bottom track (尽早):
  720. // 2.0-2.6 iter1 enters, nod1 appears @ 2.8
  721. // 3.6-4.2 iter2 enters, nod2 appears @ 4.4
  722. // 5.6-6.2 iter3 enters, nod3 appears @ 6.4 (final tick — biggest)
  723. //
  724. // 8.8-9.8 both halves dim; final chart crossfades in
  725. // (curves draw via stroke-dasharray)
  726. // 9.8-10.4 chart settles, anchor labels bloom
  727. // 10.0-12.0 brand reveal (sheet + wordmark + underline)
  728. // ────────────────────────────────────
  729. const el = {
  730. title: document.getElementById('titleLine'),
  731. splitter: document.getElementById('splitter'),
  732. splitterLb: document.getElementById('splitterLabel'),
  733. halfTop: document.getElementById('halfTop'),
  734. halfBot: document.getElementById('halfBot'),
  735. ttyL1: document.getElementById('ttyL1'),
  736. progRow: document.getElementById('progRow'),
  737. progBar: document.getElementById('progBar'),
  738. progFill: document.getElementById('progFill'),
  739. progPct: document.getElementById('progPct'),
  740. progHours: document.getElementById('progHours'),
  741. failStamp: document.getElementById('failStamp'),
  742. iter1: document.getElementById('iter1'),
  743. iter2: document.getElementById('iter2'),
  744. iter3: document.getElementById('iter3'),
  745. nod1: document.getElementById('nod1'),
  746. nod2: document.getElementById('nod2'),
  747. nod3: document.getElementById('nod3'),
  748. finalChart: document.getElementById('finalChart'),
  749. brandSheet: document.getElementById('brandSheet'),
  750. brandReveal:document.getElementById('brandReveal'),
  751. brandUnder: document.getElementById('brandUnderline'),
  752. curveA: document.getElementById('curveA'),
  753. curveACrash:document.getElementById('curveACrash'),
  754. curveB: document.getElementById('curveB'),
  755. curveBGlow: document.getElementById('curveBGlow'),
  756. };
  757. // Precompute path lengths for draw-on animation
  758. const lenA = el.curveA.getTotalLength();
  759. const lenACrash = el.curveACrash.getTotalLength();
  760. const lenB = el.curveB.getTotalLength();
  761. el.curveA.style.strokeDasharray = `${lenA} ${lenA}`;
  762. el.curveA.style.strokeDashoffset = lenA;
  763. el.curveACrash.style.strokeDasharray = `${lenACrash} ${lenACrash}`;
  764. el.curveACrash.style.strokeDashoffset = lenACrash;
  765. el.curveB.style.strokeDasharray = `${lenB} ${lenB}`;
  766. el.curveB.style.strokeDashoffset = lenB;
  767. el.curveBGlow.style.strokeDasharray = `${lenB} ${lenB}`;
  768. el.curveBGlow.style.strokeDashoffset = lenB;
  769. // Also precompute chart dot selections (hide initially)
  770. const chartDots = el.finalChart.querySelectorAll('circle');
  771. const chartAnchors = el.finalChart.querySelectorAll('.anchor-label, .anchor-en');
  772. const chartTicks = el.finalChart.querySelectorAll('.tick-label, .axis-label');
  773. const DURATION = 12.0;
  774. let startTime = null;
  775. let loop = true;
  776. if (window.__recording === true) loop = false;
  777. function tick(now) {
  778. if (startTime === null) startTime = now;
  779. let t = (now - startTime) / 1000;
  780. if (t >= DURATION) {
  781. if (loop) { startTime = now; t = 0; }
  782. else { t = DURATION; }
  783. }
  784. // ────── Title
  785. const titleIn = seg(t, 0.1, 1.0);
  786. const titleOut = seg(t, 9.2, 9.8);
  787. el.title.style.opacity = Math.max(0, Math.min(cubicOut(titleIn), 1 - titleOut));
  788. // ────── Splitter (fade out earlier so Beat 3 is clean)
  789. const splitT = seg(t, 0.0, 0.8);
  790. const splitOut = seg(t, 8.4, 8.9);
  791. el.splitter.style.transform = `scaleX(${expoOut(splitT) * (1 - splitOut)})`;
  792. const splitLabelT = seg(t, 0.4, 1.0);
  793. const splitLabelOut = seg(t, 8.2, 8.7);
  794. el.splitterLb.style.opacity = Math.max(0, Math.min(cubicOut(splitLabelT), 1 - splitLabelOut));
  795. // ────── Halves fade in / out (fade out earlier to clear for Beat 3 chart)
  796. const topIn = seg(t, 0.6, 1.4);
  797. const topOut = seg(t, 8.4, 9.0);
  798. el.halfTop.style.opacity = Math.max(0, Math.min(cubicOut(topIn), 1 - topOut));
  799. const botIn = seg(t, 1.0, 1.8);
  800. const botOut = seg(t, 8.4, 9.0);
  801. el.halfBot.style.opacity = Math.max(0, Math.min(cubicOut(botIn), 1 - botOut));
  802. // ────── TOP track: terminal line + progress bar
  803. const ttyL1In = seg(t, 1.4, 1.8);
  804. el.ttyL1.style.opacity = cubicOut(ttyL1In);
  805. // Progress bar appears @ 1.8, starts crawling 2.0-7.8, stuck 7.8-8.4, fails @ 8.4
  806. const progRowIn = seg(t, 1.8, 2.2);
  807. el.progRow.style.opacity = cubicOut(progRowIn);
  808. let pct = 0;
  809. let hoursTxt = '03:00:00';
  810. if (t >= 2.0 && t < 7.8) {
  811. const p = seg(t, 2.0, 7.8);
  812. // Easing: starts fast, slows down to 99% (mimics the "last 10% takes forever" trope)
  813. pct = 99 * (1 - Math.pow(1 - p, 2.2));
  814. const remaining = Math.max(0, (1 - p) * 3 * 60 * 60);
  815. const hh = String(Math.floor(remaining / 3600)).padStart(2, '0');
  816. const mm = String(Math.floor((remaining % 3600) / 60)).padStart(2, '0');
  817. const ss = String(Math.floor(remaining % 60)).padStart(2, '0');
  818. hoursTxt = `${hh}:${mm}:${ss}`;
  819. } else if (t >= 7.8 && t < 8.4) {
  820. pct = 99;
  821. // Micro-jitter to show "stuck"
  822. const jitter = Math.sin(t * 30) * 0.1;
  823. pct = 99 + jitter;
  824. hoursTxt = '00:00:12';
  825. } else if (t >= 8.4 && t < 8.7) {
  826. // Fail animation — pct stays at 99 briefly then snaps to 0
  827. pct = 99;
  828. hoursTxt = '— REJECTED —';
  829. } else if (t >= 8.7) {
  830. pct = 0;
  831. hoursTxt = '— REJECTED —';
  832. }
  833. el.progFill.style.width = `${pct}%`;
  834. el.progPct.textContent = `${Math.floor(Math.max(0, pct))}%`;
  835. el.progHours.textContent = hoursTxt;
  836. // Fail state toggle
  837. if (t >= 8.4) {
  838. el.progBar.classList.add('failed');
  839. el.progRow.classList.add('failed');
  840. } else {
  841. el.progBar.classList.remove('failed');
  842. el.progRow.classList.remove('failed');
  843. }
  844. // Fail stamp lands at 8.4
  845. const stampIn = seg(t, 8.4, 8.7);
  846. if (stampIn > 0) {
  847. el.failStamp.style.opacity = cubicOut(stampIn);
  848. const scale = lerp(stampIn, 1.6, 1.0, expoOut);
  849. el.failStamp.style.transform = `translateY(-50%) rotate(-8deg) scale(${scale})`;
  850. } else {
  851. el.failStamp.style.opacity = 0;
  852. }
  853. // ────── BOTTOM track: 3 iter panels
  854. const iterTimings = [
  855. { enter: [2.0, 2.6], nod: [2.8, 3.2] },
  856. { enter: [3.6, 4.2], nod: [4.4, 4.8] },
  857. { enter: [5.6, 6.2], nod: [6.4, 6.9] },
  858. ];
  859. [el.iter1, el.iter2, el.iter3].forEach((panel, i) => {
  860. const { enter } = iterTimings[i];
  861. const p = seg(t, enter[0], enter[1]);
  862. const op = expoOut(p);
  863. const ty = lerp(p, 20, 0, expoOut);
  864. panel.style.opacity = op;
  865. panel.style.transform = `translateY(${ty}px)`;
  866. });
  867. [el.nod1, el.nod2, el.nod3].forEach((n, i) => {
  868. const { nod } = iterTimings[i];
  869. const p = seg(t, nod[0], nod[1]);
  870. const op = expoOut(p);
  871. const scale = lerp(p, 0.4, 1.0, expoOut);
  872. n.style.opacity = op;
  873. n.style.transform = `scale(${scale})`;
  874. });
  875. // ────── Beat 3 · final chart crossfade (chart appears as halves fade)
  876. const chartIn = seg(t, 8.5, 9.2);
  877. el.finalChart.style.opacity = cubicOut(chartIn);
  878. const curveBT = seg(t, 8.8, 9.8);
  879. el.curveB.style.strokeDashoffset = lenB * (1 - expoOut(curveBT));
  880. el.curveBGlow.style.strokeDashoffset = lenB * (1 - expoOut(curveBT));
  881. const curveAT = seg(t, 8.9, 9.7);
  882. el.curveA.style.strokeDashoffset = lenA * (1 - cubicOut(curveAT));
  883. const curveACrashT = seg(t, 9.7, 9.95);
  884. el.curveACrash.style.strokeDashoffset = lenACrash * (1 - expoOut(curveACrashT));
  885. const failXT = seg(t, 9.65, 9.85);
  886. const failXEl = document.getElementById('failX');
  887. if (failXEl) {
  888. failXEl.style.opacity = cubicOut(failXT);
  889. failXEl.style.transform = `scale(${lerp(failXT, 1.6, 1.0, expoOut)})`;
  890. failXEl.style.transformOrigin = '1140px 180px';
  891. }
  892. chartDots.forEach((dot, i) => {
  893. const dotT = seg(t, 9.0 + i * 0.12, 9.3 + i * 0.12);
  894. dot.style.opacity = cubicOut(dotT);
  895. });
  896. chartAnchors.forEach((a) => {
  897. const aT = seg(t, 9.5, 9.95);
  898. a.style.opacity = cubicOut(aT);
  899. });
  900. chartTicks.forEach((tk) => {
  901. const tkT = seg(t, 8.7, 9.3);
  902. tk.style.opacity = cubicOut(tkT) * 0.9;
  903. });
  904. // ────── Brand reveal 10.0-12.0
  905. const sheetT = seg(t, 10.0, 10.6);
  906. el.brandSheet.style.transform = `translateY(${lerp(sheetT, 100, 0, expoOut)}%)`;
  907. const wordT = seg(t, 10.6, 11.4);
  908. el.brandReveal.style.opacity = cubicOut(wordT);
  909. const underT = seg(t, 11.4, 11.9);
  910. el.brandUnder.style.width = `${lerp(underT, 0, 280, expoOut)}px`;
  911. // Mark ready for recorder
  912. if (!window.__ready) window.__ready = true;
  913. if (loop || t < DURATION) requestAnimationFrame(tick);
  914. }
  915. (document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve())
  916. .then(() => requestAnimationFrame(tick));
  917. </script>
  918. </body>
  919. </html>