1
0

c3-motion-design.html 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134
  1. <!doctype html>
  2. <html lang="zh-Hans">
  3. <head>
  4. <meta charset="utf-8" />
  5. <title>huashu-design · c3 motion design(中文版)</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;700&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&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. --hair-strong: rgba(255,255,255,0.22);
  19. --accent: #D97757;
  20. --accent-deep: #B85D3D;
  21. --accent-dim: rgba(217,119,87,0.25);
  22. --serif-cn: "Noto Serif SC", "Songti SC", "STSong", serif;
  23. --serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
  24. --sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
  25. --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
  26. }
  27. html, body {
  28. margin: 0; padding: 0;
  29. background: #000;
  30. overflow: hidden;
  31. font-family: var(--sans);
  32. color: var(--ink);
  33. -webkit-font-smoothing: antialiased;
  34. }
  35. * { box-sizing: border-box; }
  36. .stage {
  37. position: fixed;
  38. top: 50%; left: 50%;
  39. width: 1920px; height: 1080px;
  40. transform-origin: center center;
  41. background: var(--bg);
  42. overflow: hidden;
  43. }
  44. /* Subtle film grain overlay, 2% */
  45. .stage::after {
  46. content: '';
  47. position: absolute; inset: 0;
  48. pointer-events: none;
  49. opacity: 0.025;
  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 baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
  51. mix-blend-mode: overlay;
  52. z-index: 200;
  53. }
  54. /* Watermark */
  55. .watermark-tl {
  56. position: absolute;
  57. top: 40px; left: 56px;
  58. font-family: var(--mono);
  59. font-size: 14px;
  60. letter-spacing: 0.2em;
  61. color: rgba(255,255,255,0.16);
  62. z-index: 50;
  63. text-transform: none;
  64. font-weight: 500;
  65. }
  66. .watermark-br {
  67. position: absolute;
  68. bottom: 32px; right: 48px;
  69. font-family: var(--mono);
  70. font-size: 10px;
  71. letter-spacing: 0.24em;
  72. color: rgba(255,255,255,0.22);
  73. z-index: 100;
  74. text-transform: uppercase;
  75. opacity: 0;
  76. transition: opacity 0.6s;
  77. }
  78. .watermark-br.visible { opacity: 1; }
  79. /* Scene container */
  80. .scene {
  81. position: absolute; inset: 0;
  82. opacity: 0;
  83. visibility: hidden;
  84. will-change: opacity;
  85. }
  86. .scene.visible { visibility: visible; }
  87. /* ============ Split layout ============ */
  88. .split {
  89. position: absolute; inset: 0;
  90. }
  91. .split-top {
  92. position: absolute;
  93. top: 0; left: 0;
  94. width: 100%; height: 48%;
  95. display: flex;
  96. align-items: center;
  97. justify-content: center;
  98. }
  99. .split-bottom {
  100. position: absolute;
  101. bottom: 0; left: 0;
  102. width: 100%; height: 52%;
  103. }
  104. /* Horizontal divider hairline */
  105. .split-divider {
  106. position: absolute;
  107. left: 160px; right: 160px;
  108. top: 48%;
  109. height: 1px;
  110. background: var(--hairline);
  111. z-index: 5;
  112. }
  113. /* Section label (top-left of each half) */
  114. .panel-label {
  115. position: absolute;
  116. top: 32px;
  117. left: 160px;
  118. font-family: var(--mono);
  119. font-size: 12px;
  120. letter-spacing: 0.3em;
  121. color: var(--muted);
  122. text-transform: uppercase;
  123. }
  124. .split-bottom .panel-label { top: 32px; }
  125. .panel-label .accent { color: var(--accent); font-weight: 500; }
  126. /* ============ Top: Timeline ============ */
  127. .timeline-wrap {
  128. width: 1600px;
  129. position: relative;
  130. margin-top: 40px;
  131. }
  132. .timeline-track {
  133. position: relative;
  134. height: 2px;
  135. background: var(--hairline);
  136. width: 100%;
  137. }
  138. .timeline-track .fill {
  139. position: absolute;
  140. top: 0; left: 0;
  141. height: 100%;
  142. background: linear-gradient(90deg, var(--accent) 0%, rgba(217,119,87,0.4) 100%);
  143. width: 0%;
  144. will-change: width;
  145. }
  146. /* Tick marks */
  147. .tick {
  148. position: absolute;
  149. width: 1px;
  150. height: 10px;
  151. background: var(--muted);
  152. top: -4px;
  153. transform: translateX(-0.5px);
  154. }
  155. .tick.major { height: 14px; top: -6px; background: var(--ink-60); }
  156. .tick-label {
  157. position: absolute;
  158. top: 18px;
  159. font-family: var(--mono);
  160. font-size: 11px;
  161. color: var(--muted);
  162. letter-spacing: 0.1em;
  163. transform: translateX(-50%);
  164. }
  165. /* Playhead */
  166. .playhead {
  167. position: absolute;
  168. top: -28px;
  169. left: 0;
  170. width: 2px;
  171. height: 58px;
  172. background: var(--accent);
  173. transform: translateX(-1px);
  174. will-change: transform;
  175. z-index: 10;
  176. box-shadow: 0 0 20px rgba(217,119,87,0.5);
  177. }
  178. .playhead::before {
  179. content: '';
  180. position: absolute;
  181. top: -8px;
  182. left: 50%;
  183. transform: translateX(-50%);
  184. width: 14px; height: 14px;
  185. background: var(--accent);
  186. border-radius: 50%;
  187. box-shadow: 0 0 16px rgba(217,119,87,0.6);
  188. }
  189. .playhead::after {
  190. content: '';
  191. position: absolute;
  192. top: -6px;
  193. left: 50%;
  194. transform: translateX(-50%);
  195. width: 6px; height: 6px;
  196. background: var(--bg);
  197. border-radius: 50%;
  198. z-index: 2;
  199. }
  200. /* API capsules on timeline */
  201. .api-capsule {
  202. position: absolute;
  203. top: -92px;
  204. transform: translateX(-50%);
  205. padding: 10px 20px;
  206. border: 1px solid var(--hairline);
  207. border-radius: 999px;
  208. background: rgba(0,0,0,0.6);
  209. backdrop-filter: blur(8px);
  210. font-family: var(--mono);
  211. font-size: 18px;
  212. font-weight: 500;
  213. color: var(--ink-60);
  214. letter-spacing: 0.02em;
  215. transition: none;
  216. will-change: color, border-color, transform, box-shadow;
  217. white-space: nowrap;
  218. }
  219. .api-capsule.lit {
  220. color: var(--accent);
  221. border-color: var(--accent);
  222. box-shadow: 0 0 30px rgba(217,119,87,0.35);
  223. }
  224. .api-capsule .tiny {
  225. font-size: 10px;
  226. color: var(--muted);
  227. letter-spacing: 0.2em;
  228. margin-right: 10px;
  229. display: inline-block;
  230. vertical-align: middle;
  231. opacity: 0.7;
  232. }
  233. .api-capsule.lit .tiny { color: var(--accent); opacity: 0.9; }
  234. /* Tick connector (short vertical line from capsule to timeline) */
  235. .capsule-stem {
  236. position: absolute;
  237. top: -48px;
  238. width: 1px;
  239. height: 44px;
  240. background: var(--hairline);
  241. transform: translateX(-0.5px);
  242. z-index: 1;
  243. }
  244. .capsule-stem.lit { background: var(--accent); }
  245. /* ============ Bottom: Driven stage ============ */
  246. .driven-stage {
  247. position: absolute;
  248. top: 0; left: 0;
  249. width: 100%; height: 100%;
  250. }
  251. .viz {
  252. position: absolute;
  253. top: 46%; left: 50%;
  254. transform: translate(-50%, -50%);
  255. width: 1000px; height: 400px;
  256. opacity: 0;
  257. will-change: opacity;
  258. display: flex;
  259. align-items: center;
  260. justify-content: center;
  261. }
  262. /* viz 1: useTime — clock */
  263. .viz-clock {
  264. position: relative;
  265. width: 280px; height: 280px;
  266. border: 1.5px solid var(--hair-strong);
  267. border-radius: 50%;
  268. display: flex;
  269. align-items: center;
  270. justify-content: center;
  271. }
  272. .viz-clock .tickmark {
  273. position: absolute;
  274. width: 1px;
  275. height: 8px;
  276. background: var(--muted);
  277. top: 10px;
  278. left: 50%;
  279. transform-origin: 50% 130px;
  280. }
  281. .viz-clock .tickmark.q {
  282. width: 2px;
  283. height: 14px;
  284. background: var(--ink-60);
  285. }
  286. .viz-clock .hand-h {
  287. position: absolute;
  288. width: 3px; height: 80px;
  289. background: var(--ink);
  290. left: 50%;
  291. bottom: 50%;
  292. transform-origin: 50% 100%;
  293. transform: translateX(-50%) rotate(30deg);
  294. border-radius: 2px;
  295. will-change: transform;
  296. }
  297. .viz-clock .hand-m {
  298. position: absolute;
  299. width: 2px; height: 110px;
  300. background: var(--ink-80);
  301. left: 50%;
  302. bottom: 50%;
  303. transform-origin: 50% 100%;
  304. transform: translateX(-50%) rotate(120deg);
  305. border-radius: 2px;
  306. will-change: transform;
  307. }
  308. .viz-clock .hand-s {
  309. position: absolute;
  310. width: 1.5px; height: 120px;
  311. background: var(--accent);
  312. left: 50%;
  313. bottom: 50%;
  314. transform-origin: 50% 100%;
  315. transform: translateX(-50%) rotate(0deg);
  316. border-radius: 2px;
  317. will-change: transform;
  318. box-shadow: 0 0 10px rgba(217,119,87,0.4);
  319. }
  320. .viz-clock .center-dot {
  321. width: 12px; height: 12px;
  322. border-radius: 50%;
  323. background: var(--accent);
  324. z-index: 5;
  325. box-shadow: 0 0 10px rgba(217,119,87,0.6);
  326. }
  327. .viz-clock-label {
  328. position: absolute;
  329. bottom: -48px;
  330. left: 50%;
  331. transform: translateX(-50%);
  332. font-family: var(--mono);
  333. font-size: 13px;
  334. color: var(--muted);
  335. letter-spacing: 0.12em;
  336. white-space: nowrap;
  337. }
  338. .viz-clock-label .val {
  339. color: var(--accent);
  340. font-variant-numeric: tabular-nums;
  341. }
  342. /* viz 2: interpolate — morph box */
  343. .viz-morph {
  344. display: flex;
  345. gap: 80px;
  346. align-items: center;
  347. justify-content: center;
  348. width: 100%;
  349. }
  350. .morph-box {
  351. width: 260px; height: 260px;
  352. position: relative;
  353. display: flex;
  354. align-items: center;
  355. justify-content: center;
  356. }
  357. .morph-rect {
  358. background: var(--accent);
  359. border-radius: 4px;
  360. will-change: width, height, background, border-radius, transform;
  361. box-shadow: 0 0 40px rgba(217,119,87,0.25);
  362. }
  363. .morph-label {
  364. position: absolute;
  365. bottom: -48px;
  366. left: 50%;
  367. transform: translateX(-50%);
  368. font-family: var(--mono);
  369. font-size: 12px;
  370. color: var(--muted);
  371. letter-spacing: 0.12em;
  372. white-space: nowrap;
  373. }
  374. .morph-label .val { color: var(--accent); font-variant-numeric: tabular-nums; }
  375. .morph-arrow {
  376. font-family: var(--mono);
  377. font-size: 28px;
  378. color: var(--muted);
  379. letter-spacing: 0.2em;
  380. }
  381. /* viz 3: Easing — curves */
  382. .viz-curves {
  383. position: relative;
  384. width: 720px; height: 320px;
  385. display: flex;
  386. align-items: center;
  387. justify-content: center;
  388. }
  389. .curves-svg {
  390. width: 100%; height: 100%;
  391. }
  392. .curve-label {
  393. position: absolute;
  394. font-family: var(--mono);
  395. font-size: 12px;
  396. color: var(--muted);
  397. letter-spacing: 0.08em;
  398. white-space: nowrap;
  399. }
  400. /* viewBox 720x320 → right edge ≈ 680 of 720 → 94%. Vertical:
  401. y=40 is visual top (output value 1), y=260 is bottom (value 0).
  402. Labels go at right side, vertically aligned with where each curve
  403. approaches its asymptote at t≈0.7.
  404. expoOut at t=0.7 ~ 0.99 (≈ y=42)
  405. cubicOut at t=0.7 ~ 0.973 (≈ y=46)
  406. linear at t=0.7 ~ 0.7 (≈ y=106)
  407. So spatial order top→bottom: expoOut, cubicOut, linear
  408. */
  409. .curve-label.l-expo { top: 6%; right: 4%; color: var(--accent); }
  410. .curve-label.l-cubic { top: 16%; right: 4%; color: rgba(255,255,255,0.78); }
  411. .curve-label.l-linear { top: 36%; right: 4%; color: rgba(255,255,255,0.42); }
  412. .curve-dot {
  413. position: absolute;
  414. width: 10px; height: 10px;
  415. border-radius: 50%;
  416. background: var(--accent);
  417. transform: translate(-50%, -50%);
  418. box-shadow: 0 0 14px rgba(217,119,87,0.6);
  419. will-change: left, top;
  420. }
  421. /* viz 4: useSprite — choreographed grid */
  422. .viz-sprites {
  423. display: grid;
  424. grid-template-columns: repeat(6, 60px);
  425. grid-template-rows: repeat(4, 60px);
  426. gap: 18px;
  427. justify-content: center;
  428. align-content: center;
  429. padding: 40px 0;
  430. }
  431. .sprite {
  432. width: 60px; height: 60px;
  433. background: var(--hairline);
  434. border: 1px solid var(--dim);
  435. will-change: transform, opacity, background;
  436. opacity: 0;
  437. border-radius: 2px;
  438. }
  439. .sprite-label {
  440. position: absolute;
  441. bottom: -6px;
  442. left: 50%;
  443. transform: translateX(-50%);
  444. font-family: var(--mono);
  445. font-size: 12px;
  446. color: var(--muted);
  447. letter-spacing: 0.12em;
  448. white-space: nowrap;
  449. }
  450. .sprite-label .val { color: var(--accent); font-variant-numeric: tabular-nums; }
  451. /* ============ Scene 0: Opening title ============ */
  452. .scene-intro {
  453. display: flex;
  454. flex-direction: column;
  455. align-items: center;
  456. justify-content: center;
  457. }
  458. .scene-intro .title {
  459. font-family: var(--serif-cn);
  460. font-size: 108px;
  461. font-weight: 300;
  462. letter-spacing: -0.02em;
  463. color: var(--ink);
  464. line-height: 1.05;
  465. will-change: opacity, transform, font-weight;
  466. }
  467. .scene-intro .title .accent { color: var(--accent); }
  468. .scene-intro .sub {
  469. margin-top: 28px;
  470. font-family: var(--mono);
  471. font-size: 16px;
  472. color: var(--muted);
  473. letter-spacing: 0.3em;
  474. }
  475. /* ============ Scene 2: Brand reveal (米色面板标准动作) ============ */
  476. .scene-brand {
  477. background: transparent;
  478. pointer-events: none;
  479. z-index: 150;
  480. }
  481. .brand-panel {
  482. position: absolute;
  483. inset: 0;
  484. background: #F5F4F0;
  485. transform: translateY(100%);
  486. will-change: transform;
  487. }
  488. .brand-wordmark {
  489. position: absolute;
  490. top: 50%;
  491. left: 50%;
  492. transform: translate(-50%, calc(-50% + 20px));
  493. font-family: "Source Serif 4", Georgia, serif;
  494. font-size: 72px;
  495. font-weight: 100;
  496. font-variation-settings: "wght" 100;
  497. letter-spacing: -0.01em;
  498. color: #1A1918;
  499. text-align: center;
  500. line-height: 1;
  501. opacity: 0;
  502. white-space: nowrap;
  503. will-change: opacity, transform, font-weight, font-variation-settings;
  504. }
  505. .brand-wordmark .accent { color: #D97757; font-weight: inherit; }
  506. .brand-line {
  507. position: absolute;
  508. top: calc(50% + 60px);
  509. left: 50%;
  510. transform: translateX(-50%);
  511. height: 2px;
  512. width: 0px;
  513. background: #D97757;
  514. will-change: width;
  515. }
  516. /* ============ Replay button (hidden during record) ============ */
  517. .replay-btn {
  518. position: absolute;
  519. bottom: 40px;
  520. left: 50%;
  521. transform: translateX(-50%);
  522. padding: 12px 32px;
  523. border: 1px solid var(--hair-strong);
  524. border-radius: 999px;
  525. background: transparent;
  526. color: var(--ink-60);
  527. font-family: var(--mono);
  528. font-size: 13px;
  529. letter-spacing: 0.2em;
  530. cursor: pointer;
  531. opacity: 0;
  532. pointer-events: none;
  533. transition: opacity 0.4s;
  534. z-index: 300;
  535. }
  536. .replay-btn.visible {
  537. opacity: 1;
  538. pointer-events: auto;
  539. }
  540. </style>
  541. </head>
  542. <body>
  543. <div class="stage" id="stage">
  544. <!-- Top-left watermark (always on) -->
  545. <div class="watermark-tl">HUASHU · DESIGN</div>
  546. <!-- ============ Scene 0: Intro (0 → 1.6s) ============ -->
  547. <div class="scene scene-intro" id="scene-intro">
  548. <div class="title" id="introTitle">时间轴 <span class="accent">=</span> 代码</div>
  549. <div class="sub" id="introSub">TIMELINE · MOTION · ENGINE</div>
  550. </div>
  551. <!-- ============ Scene 1: Split view (1.6 → 8.2s) ============ -->
  552. <div class="scene" id="scene-main">
  553. <div class="split">
  554. <!-- TOP: Timeline -->
  555. <div class="split-top">
  556. <div class="panel-label">TIMELINE · <span class="accent">PLAYHEAD</span></div>
  557. <div class="timeline-wrap">
  558. <div class="timeline-track">
  559. <div class="fill" id="timelineFill"></div>
  560. <!-- Tick marks (10 ticks for 10s) -->
  561. <div class="tick" style="left: 0%;"></div>
  562. <div class="tick major" style="left: 0%;"></div>
  563. <div class="tick" style="left: 10%;"></div>
  564. <div class="tick major" style="left: 20%;"></div>
  565. <div class="tick" style="left: 30%;"></div>
  566. <div class="tick major" style="left: 40%;"></div>
  567. <div class="tick" style="left: 50%;"></div>
  568. <div class="tick major" style="left: 60%;"></div>
  569. <div class="tick" style="left: 70%;"></div>
  570. <div class="tick major" style="left: 80%;"></div>
  571. <div class="tick" style="left: 90%;"></div>
  572. <div class="tick major" style="left: 100%;"></div>
  573. <div class="tick-label" style="left: 0%;">0s</div>
  574. <div class="tick-label" style="left: 20%;">2s</div>
  575. <div class="tick-label" style="left: 40%;">4s</div>
  576. <div class="tick-label" style="left: 60%;">6s</div>
  577. <div class="tick-label" style="left: 80%;">8s</div>
  578. <div class="tick-label" style="left: 100%;">10s</div>
  579. <!-- API capsules anchored at their trigger points -->
  580. <!-- Scene-main spans 1.6→8.2; timeline maps 0→10s globally for clarity.
  581. cap positions here mirror when each API is "active" on the lower viz. -->
  582. <!-- useTime: global t = 1.8 → 3.3 → center ~2.5s → 25% -->
  583. <div class="capsule-stem" id="stem-time" style="left: 18%;"></div>
  584. <div class="api-capsule" id="cap-time" style="left: 18%;">
  585. <span class="tiny">01</span>useTime
  586. </div>
  587. <!-- interpolate: 3.5 → 5s → center 4.2s → 42% -->
  588. <div class="capsule-stem" id="stem-interp" style="left: 38%;"></div>
  589. <div class="api-capsule" id="cap-interp" style="left: 38%;">
  590. <span class="tiny">02</span>interpolate
  591. </div>
  592. <!-- Easing: 5 → 6.5s → center 5.7s → 57% -->
  593. <div class="capsule-stem" id="stem-easing" style="left: 58%;"></div>
  594. <div class="api-capsule" id="cap-easing" style="left: 58%;">
  595. <span class="tiny">03</span>Easing
  596. </div>
  597. <!-- useSprite: 6.5 → 8s → center 7.2s → 72% -->
  598. <div class="capsule-stem" id="stem-sprite" style="left: 80%;"></div>
  599. <div class="api-capsule" id="cap-sprite" style="left: 80%;">
  600. <span class="tiny">04</span>useSprite
  601. </div>
  602. <!-- Playhead -->
  603. <div class="playhead" id="playhead"></div>
  604. </div>
  605. </div>
  606. </div>
  607. <!-- Divider -->
  608. <div class="split-divider"></div>
  609. <!-- BOTTOM: Driven stage -->
  610. <div class="split-bottom">
  611. <div class="panel-label">DRIVEN · <span class="accent">STAGE</span></div>
  612. <div class="driven-stage">
  613. <!-- viz 1: useTime — clock -->
  614. <div class="viz" id="viz-time">
  615. <div class="viz-clock" id="clockRoot">
  616. <!-- 12 tick marks -->
  617. <div class="tickmark q" style="transform: translate(-50%, 0) rotate(0deg);"></div>
  618. <div class="tickmark" style="transform: translate(-50%, 0) rotate(30deg);"></div>
  619. <div class="tickmark" style="transform: translate(-50%, 0) rotate(60deg);"></div>
  620. <div class="tickmark q" style="transform: translate(-50%, 0) rotate(90deg);"></div>
  621. <div class="tickmark" style="transform: translate(-50%, 0) rotate(120deg);"></div>
  622. <div class="tickmark" style="transform: translate(-50%, 0) rotate(150deg);"></div>
  623. <div class="tickmark q" style="transform: translate(-50%, 0) rotate(180deg);"></div>
  624. <div class="tickmark" style="transform: translate(-50%, 0) rotate(210deg);"></div>
  625. <div class="tickmark" style="transform: translate(-50%, 0) rotate(240deg);"></div>
  626. <div class="tickmark q" style="transform: translate(-50%, 0) rotate(270deg);"></div>
  627. <div class="tickmark" style="transform: translate(-50%, 0) rotate(300deg);"></div>
  628. <div class="tickmark" style="transform: translate(-50%, 0) rotate(330deg);"></div>
  629. <div class="hand-h" id="handH"></div>
  630. <div class="hand-m" id="handM"></div>
  631. <div class="hand-s" id="handS"></div>
  632. <div class="center-dot"></div>
  633. <div class="viz-clock-label">
  634. t = <span class="val" id="timeVal">0.00s</span>
  635. </div>
  636. </div>
  637. </div>
  638. <!-- viz 2: interpolate — morph -->
  639. <div class="viz" id="viz-interp">
  640. <div class="viz-morph">
  641. <div class="morph-box">
  642. <div class="morph-rect" id="morphFrom" style="width: 80px; height: 80px; background: var(--hair-strong); border-radius: 2px;"></div>
  643. <div class="morph-label">FROM · <span class="val">0 → 100</span></div>
  644. </div>
  645. <div class="morph-arrow">──────→</div>
  646. <div class="morph-box">
  647. <div class="morph-rect" id="morphTo"></div>
  648. <div class="morph-label">INTERPOLATE · <span class="val" id="interpVal">0.00</span></div>
  649. </div>
  650. </div>
  651. </div>
  652. <!-- viz 3: Easing — 3 curves drawn in parallel -->
  653. <div class="viz" id="viz-easing">
  654. <div class="viz-curves">
  655. <svg class="curves-svg" viewBox="0 0 720 320" preserveAspectRatio="none">
  656. <!-- Grid -->
  657. <line x1="60" y1="260" x2="680" y2="260" stroke="rgba(255,255,255,0.18)" stroke-width="1"/>
  658. <line x1="60" y1="260" x2="60" y2="40" stroke="rgba(255,255,255,0.18)" stroke-width="1"/>
  659. <!-- Axis labels -->
  660. <text x="50" y="266" text-anchor="end" fill="rgba(255,255,255,0.4)" font-family="JetBrains Mono, monospace" font-size="11">0</text>
  661. <text x="50" y="48" text-anchor="end" fill="rgba(255,255,255,0.4)" font-family="JetBrains Mono, monospace" font-size="11">1</text>
  662. <text x="680" y="282" text-anchor="end" fill="rgba(255,255,255,0.4)" font-family="JetBrains Mono, monospace" font-size="11">t</text>
  663. <!-- Curves -->
  664. <path id="pathLinear" d="M 60 260 L 60 260" stroke="rgba(255,255,255,0.42)" stroke-width="1.5" fill="none" stroke-linecap="round"/>
  665. <path id="pathCubic" d="M 60 260 L 60 260" stroke="rgba(255,255,255,0.75)" stroke-width="1.8" fill="none" stroke-linecap="round"/>
  666. <path id="pathExpo" d="M 60 260 L 60 260" stroke="#D97757" stroke-width="2.2" fill="none" stroke-linecap="round"/>
  667. </svg>
  668. <div class="curve-label l-linear">linear</div>
  669. <div class="curve-label l-cubic">cubicOut</div>
  670. <div class="curve-label l-expo">expoOut</div>
  671. </div>
  672. </div>
  673. <!-- viz 4: useSprite — 24 sprites -->
  674. <div class="viz" id="viz-sprite">
  675. <div class="viz-sprites" id="spriteGrid">
  676. <!-- 24 sprites (6x4), filled by JS -->
  677. </div>
  678. </div>
  679. </div>
  680. </div>
  681. </div>
  682. </div>
  683. <!-- ============ Scene 2: Brand reveal (米色面板, 8.0 → 10s) ============ -->
  684. <div class="scene scene-brand" id="scene-brand">
  685. <div class="brand-panel" id="brandPanel"></div>
  686. <div class="brand-wordmark" id="wordmark">huashu<span class="accent">-</span>design</div>
  687. <div class="brand-line" id="brandLine"></div>
  688. </div>
  689. <!-- Bottom-right watermark -->
  690. <div class="watermark-br" id="watermarkBR">V2 · 2026</div>
  691. <!-- Replay button (hidden during recording) -->
  692. <button class="replay-btn no-record" id="replayBtn">REPLAY</button>
  693. </div>
  694. <script>
  695. (function() {
  696. // =============== Timing ===============
  697. const T = {
  698. DURATION: 10.0,
  699. // Scene 0: intro
  700. intro_in: [0.0, 0.5],
  701. intro_out: [1.3, 1.6],
  702. // Scene 1: main (timeline + driven stage)
  703. main_in: [1.5, 1.9], // fade in
  704. // Playhead sweeps from 0% (at t=1.6) to 100% (at t=8.2).
  705. // API activations use GLOBAL time. Their capsule position is placed so
  706. // that playhead passes under the capsule right when the API peaks.
  707. main_t0: 1.6,
  708. main_t_end: 8.2,
  709. main_out: [8.0, 8.4],
  710. // API activations (GLOBAL time)
  711. // Each API: [activate_start, peak, deactivate_end]
  712. // Capsule x% = (peak - 1.6) / (8.2 - 1.6) * 100
  713. useTime: [2.0, 2.8, 3.6], // capsule @ ~18%
  714. interpolate: [3.6, 4.1, 4.8], // capsule @ ~38%
  715. Easing: [4.8, 5.4, 6.2], // capsule @ ~58%
  716. useSprite: [6.2, 6.9, 7.9], // capsule @ ~80%
  717. // Scene 2: Brand reveal (米色面板 standard, last 2s of T=10)
  718. // [T-2.0 → T-1.7]: main fade 1→0 (already handled by main_out 8.0-8.4)
  719. // [T-1.7 → T-1.3]: beige panel translateY 100%→0, expoOut
  720. // [T-1.3 → T-0.7]: wordmark weight 100→500 + y:20→0 + opacity 0→1, expoOut
  721. // [T-0.7 → T-0.3]: orange line width 0→280px, cubicOut
  722. // [T-0.3 → T]: hold
  723. brand_panel: [8.3, 8.7],
  724. brand_word: [8.7, 9.3],
  725. brand_line: [9.3, 9.7],
  726. };
  727. // =============== Easings ===============
  728. const expoOut = t => (t >= 1 ? 1 : 1 - Math.pow(2, -10 * t));
  729. const expoIn = t => (t <= 0 ? 0 : Math.pow(2, 10 * (t - 1)));
  730. const cubicOut = t => 1 - Math.pow(1 - t, 3);
  731. const cubicIn = t => t * t * t;
  732. const cubicInOut = t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
  733. const easeInOut = cubicInOut;
  734. const linear = t => t;
  735. // =============== Utils ===============
  736. const clamp = (v, lo = 0, hi = 1) => Math.max(lo, Math.min(hi, v));
  737. const clampLerp = (t, t0, t1) => clamp((t - t0) / (t1 - t0));
  738. function lerp(t, t0, t1, v0, v1, easing = linear) {
  739. const p = clampLerp(t, t0, t1);
  740. return v0 + (v1 - v0) * easing(p);
  741. }
  742. // =============== DOM refs ===============
  743. const scenes = {
  744. intro: document.getElementById('scene-intro'),
  745. main: document.getElementById('scene-main'),
  746. brand: document.getElementById('scene-brand'),
  747. };
  748. const introTitle = document.getElementById('introTitle');
  749. const introSub = document.getElementById('introSub');
  750. const timelineFill = document.getElementById('timelineFill');
  751. const playhead = document.getElementById('playhead');
  752. const capTime = document.getElementById('cap-time');
  753. const capInterp = document.getElementById('cap-interp');
  754. const capEasing = document.getElementById('cap-easing');
  755. const capSprite = document.getElementById('cap-sprite');
  756. const stemTime = document.getElementById('stem-time');
  757. const stemInterp = document.getElementById('stem-interp');
  758. const stemEasing = document.getElementById('stem-easing');
  759. const stemSprite = document.getElementById('stem-sprite');
  760. const vizTime = document.getElementById('viz-time');
  761. const vizInterp = document.getElementById('viz-interp');
  762. const vizEasing = document.getElementById('viz-easing');
  763. const vizSprite = document.getElementById('viz-sprite');
  764. const handS = document.getElementById('handS');
  765. const handM = document.getElementById('handM');
  766. const handH = document.getElementById('handH');
  767. const timeVal = document.getElementById('timeVal');
  768. const morphTo = document.getElementById('morphTo');
  769. const interpVal = document.getElementById('interpVal');
  770. const pathLinear = document.getElementById('pathLinear');
  771. const pathCubic = document.getElementById('pathCubic');
  772. const pathExpo = document.getElementById('pathExpo');
  773. const spriteGrid = document.getElementById('spriteGrid');
  774. const wordmark = document.getElementById('wordmark');
  775. const brandLine = document.getElementById('brandLine');
  776. const brandPanel = document.getElementById('brandPanel');
  777. const watermarkBR = document.getElementById('watermarkBR');
  778. const replayBtn = document.getElementById('replayBtn');
  779. // Build 24 sprites (6x4 grid)
  780. const SPRITE_COLS = 6, SPRITE_ROWS = 4;
  781. const spriteEls = [];
  782. for (let r = 0; r < SPRITE_ROWS; r++) {
  783. for (let c = 0; c < SPRITE_COLS; c++) {
  784. const el = document.createElement('div');
  785. el.className = 'sprite';
  786. // center distance for ripple
  787. const dc = c - (SPRITE_COLS - 1) / 2;
  788. const dr = r - (SPRITE_ROWS - 1) / 2;
  789. const dist = Math.sqrt(dc * dc + dr * dr);
  790. const maxDist = Math.sqrt(((SPRITE_COLS - 1) / 2) ** 2 + ((SPRITE_ROWS - 1) / 2) ** 2);
  791. el.dataset.delay = (dist / maxDist).toFixed(3);
  792. spriteGrid.appendChild(el);
  793. spriteEls.push(el);
  794. }
  795. }
  796. // =============== Scene helpers ===============
  797. function showScene(el, opacity) {
  798. if (opacity > 0.001) el.classList.add('visible');
  799. else el.classList.remove('visible');
  800. el.style.opacity = opacity;
  801. }
  802. // =============== API activation logic ===============
  803. function apiState(t_local, api) {
  804. // Returns { on: bool, strength: 0-1 }
  805. const [a, peak, b] = T[api];
  806. if (t_local < a || t_local > b) return { on: false, strength: 0 };
  807. if (t_local < peak) {
  808. return { on: true, strength: expoOut(clampLerp(t_local, a, peak)) };
  809. } else {
  810. return { on: true, strength: 1 - cubicIn(clampLerp(t_local, peak, b)) };
  811. }
  812. }
  813. // =============== Draw easing curves progressively ===============
  814. function easingPath(easingFn, progress) {
  815. // progress 0-1 draws the curve from left to right
  816. // x range: 60 → 680, y range: 260 (0) → 40 (1)
  817. const X0 = 60, X1 = 680, Y0 = 260, Y1 = 40;
  818. const steps = Math.max(2, Math.floor(progress * 80));
  819. let d = `M ${X0} ${Y0}`;
  820. for (let i = 1; i <= steps; i++) {
  821. const t = (i / 80) * progress;
  822. const x = X0 + (X1 - X0) * t;
  823. const y = Y0 + (Y1 - Y0) * easingFn(t);
  824. d += ` L ${x.toFixed(2)} ${y.toFixed(2)}`;
  825. }
  826. return d;
  827. }
  828. // =============== Render ===============
  829. function render(t) {
  830. // ============ Scene 0: Intro ============
  831. if (t < T.main_in[1]) {
  832. let op = 0;
  833. if (t < T.intro_in[1]) op = clampLerp(t, T.intro_in[0], T.intro_in[1]);
  834. else if (t < T.intro_out[0]) op = 1;
  835. else op = 1 - clampLerp(t, T.intro_out[0], T.intro_out[1]);
  836. showScene(scenes.intro, op);
  837. // weight morph + rise
  838. const morphP = expoOut(clampLerp(t, T.intro_in[0], T.intro_in[1] + 0.3));
  839. const w = 150 + (400 - 150) * morphP;
  840. introTitle.style.fontWeight = Math.round(w);
  841. const rise = lerp(t, T.intro_in[0], T.intro_in[1], 16, 0, expoOut);
  842. introTitle.style.transform = `translate3d(0, ${rise}px, 0)`;
  843. introSub.style.opacity = clampLerp(t, T.intro_in[1], T.intro_in[1] + 0.4);
  844. } else {
  845. showScene(scenes.intro, 0);
  846. }
  847. // ============ Scene 1: Main (split view) ============
  848. if (t >= T.main_in[0] - 0.1 && t < T.main_out[1]) {
  849. let op;
  850. if (t < T.main_in[1]) op = clampLerp(t, T.main_in[0], T.main_in[1]);
  851. else if (t < T.main_out[0]) op = 1;
  852. else op = 1 - clampLerp(t, T.main_out[0], T.main_out[1]);
  853. showScene(scenes.main, op);
  854. // Playhead sweeps 0% → 100% across the window [main_t0, main_t_end]
  855. const phP = clampLerp(t, T.main_t0, T.main_t_end);
  856. const phPct = phP * 100;
  857. playhead.style.left = phPct + '%';
  858. // Keep: use t directly for API state
  859. const t_local_clamped = t;
  860. // Timeline fill
  861. timelineFill.style.width = phPct + '%';
  862. // API capsules: lit state driven by apiState
  863. const stTime = apiState(t_local_clamped, 'useTime');
  864. const stInterp = apiState(t_local_clamped, 'interpolate');
  865. const stEasing = apiState(t_local_clamped, 'Easing');
  866. const stSprite = apiState(t_local_clamped, 'useSprite');
  867. setLit(capTime, stemTime, stTime);
  868. setLit(capInterp, stemInterp, stInterp);
  869. setLit(capEasing, stemEasing, stEasing);
  870. setLit(capSprite, stemSprite, stSprite);
  871. // Viz opacities — each viz only visible during its API's window
  872. vizTime.style.opacity = stTime.on ? stTime.strength : 0;
  873. vizInterp.style.opacity = stInterp.on ? stInterp.strength : 0;
  874. vizEasing.style.opacity = stEasing.on ? stEasing.strength : 0;
  875. vizSprite.style.opacity = stSprite.on ? stSprite.strength : 0;
  876. // ========= viz 1: clock =========
  877. // Continuous rotation (not just when active) so transition looks natural
  878. // But only animate hands when api is near-active, to avoid wasted cpu
  879. {
  880. const [a, _peak, b] = T.useTime;
  881. // Second hand: one revolution over the active window
  882. const localP = clampLerp(t_local_clamped, a, b);
  883. // Multi-revolution: 1.5 turns over the window
  884. const sDeg = localP * 540;
  885. const mDeg = localP * 180 + 120;
  886. const hDeg = localP * 60 + 30;
  887. handS.style.transform = `translateX(-50%) rotate(${sDeg}deg)`;
  888. handM.style.transform = `translateX(-50%) rotate(${mDeg}deg)`;
  889. handH.style.transform = `translateX(-50%) rotate(${hDeg}deg)`;
  890. // Display value as t in seconds mapping 0→1.50
  891. const displayVal = (localP * 1.5).toFixed(2);
  892. timeVal.textContent = displayVal + 's';
  893. }
  894. // ========= viz 2: interpolate =========
  895. {
  896. const [a, _peak, b] = T.interpolate;
  897. const localP = clampLerp(t_local_clamped, a, b);
  898. const eased = easeInOut(localP);
  899. // morph from 80×80 black → 220×160 orange, rounded
  900. const W = 80 + (240 - 80) * eased;
  901. const H = 80 + (160 - 80) * eased;
  902. const bright = Math.round(30 + (217 - 30) * eased);
  903. const brightG = Math.round(30 + (119 - 30) * eased);
  904. const brightB = Math.round(30 + (87 - 30) * eased);
  905. const rad = 2 + (20 - 2) * eased;
  906. morphTo.style.width = W + 'px';
  907. morphTo.style.height = H + 'px';
  908. morphTo.style.background = `rgb(${bright}, ${brightG}, ${brightB})`;
  909. morphTo.style.borderRadius = rad + 'px';
  910. interpVal.textContent = eased.toFixed(2);
  911. }
  912. // ========= viz 3: easing curves =========
  913. {
  914. const [a, _peak, b] = T.Easing;
  915. const localP = clampLerp(t_local_clamped, a, b);
  916. pathLinear.setAttribute('d', easingPath(linear, localP));
  917. pathCubic.setAttribute('d', easingPath(cubicOut, localP));
  918. pathExpo.setAttribute('d', easingPath(expoOut, localP));
  919. }
  920. // ========= viz 4: sprites =========
  921. {
  922. const [a, _peak, b] = T.useSprite;
  923. const localP = clampLerp(t_local_clamped, a, b);
  924. for (const el of spriteEls) {
  925. const delay = parseFloat(el.dataset.delay);
  926. const spriteLocalT = clamp((localP - delay * 0.5) / 0.5, 0, 1);
  927. const op = expoOut(spriteLocalT);
  928. el.style.opacity = op;
  929. const scale = 0.5 + 0.5 * op;
  930. const y = (1 - op) * 14;
  931. el.style.transform = `translateY(${y}px) scale(${scale})`;
  932. el.style.background = op > 0.85 ? 'var(--accent)' : 'var(--hairline)';
  933. }
  934. }
  935. } else {
  936. showScene(scenes.main, 0);
  937. }
  938. // ============ Scene 2: Brand reveal (米色面板标准动作) ============
  939. if (t >= T.brand_panel[0] - 0.1) {
  940. showScene(scenes.brand, 1);
  941. // [T-1.7 → T-1.3]: beige panel slides up, expoOut
  942. const panelP = expoOut(clampLerp(t, T.brand_panel[0], T.brand_panel[1]));
  943. brandPanel.style.transform = `translateY(${(1 - panelP) * 100}%)`;
  944. // [T-1.3 → T-0.7]: wordmark weight 100→500 + y:20→0 + opacity:0→1, expoOut
  945. const wordP = expoOut(clampLerp(t, T.brand_word[0], T.brand_word[1]));
  946. const w = 100 + (500 - 100) * wordP;
  947. wordmark.style.fontVariationSettings = `"wght" ${w.toFixed(0)}`;
  948. wordmark.style.fontWeight = Math.round(w);
  949. wordmark.style.opacity = wordP;
  950. const wRise = (1 - wordP) * 20;
  951. wordmark.style.transform = `translate(-50%, calc(-50% + ${wRise}px))`;
  952. // [T-0.7 → T-0.3]: orange line expands 0→280px, cubicOut
  953. const lineP = cubicOut(clampLerp(t, T.brand_line[0], T.brand_line[1]));
  954. brandLine.style.width = (lineP * 280) + 'px';
  955. } else {
  956. showScene(scenes.brand, 0);
  957. brandPanel.style.transform = 'translateY(100%)';
  958. wordmark.style.opacity = 0;
  959. brandLine.style.width = '0px';
  960. }
  961. // Watermark visible from start of main until end
  962. if (t >= T.main_in[0] && t < T.DURATION - 0.15) {
  963. watermarkBR.classList.add('visible');
  964. } else {
  965. watermarkBR.classList.remove('visible');
  966. }
  967. }
  968. function setLit(capsule, stem, state) {
  969. if (state.on && state.strength > 0.15) {
  970. capsule.classList.add('lit');
  971. stem.classList.add('lit');
  972. // Subtle scale pulse centered on peak (simplistic)
  973. const scale = 1.0 + state.strength * 0.06;
  974. capsule.style.transform = `translateX(-50%) scale(${scale})`;
  975. } else {
  976. capsule.classList.remove('lit');
  977. stem.classList.remove('lit');
  978. capsule.style.transform = 'translateX(-50%)';
  979. }
  980. }
  981. // =============== Driver ===============
  982. let manualT = null;
  983. let startMs = null;
  984. let hasFinishedOnce = false;
  985. function tick(now) {
  986. if (manualT != null) {
  987. render(manualT);
  988. } else {
  989. if (startMs == null) startMs = now;
  990. const elapsed = (now - startMs) / 1000;
  991. const recording = window.__recording === true;
  992. let t;
  993. if (recording) {
  994. t = Math.min(elapsed, T.DURATION - 0.001);
  995. if (elapsed >= T.DURATION && !hasFinishedOnce) hasFinishedOnce = true;
  996. } else {
  997. t = elapsed % T.DURATION;
  998. // Show replay button when we've played at least once
  999. if (elapsed >= T.DURATION) {
  1000. replayBtn.classList.add('visible');
  1001. }
  1002. }
  1003. render(t);
  1004. }
  1005. requestAnimationFrame(tick);
  1006. }
  1007. // First paint signal for renderer
  1008. document.fonts.ready.then(() => {
  1009. render(0);
  1010. requestAnimationFrame(() => {
  1011. window.__ready = true;
  1012. requestAnimationFrame(tick);
  1013. });
  1014. });
  1015. // ========= Stage scaling (fit viewport) =========
  1016. function fitStage() {
  1017. const stage = document.getElementById('stage');
  1018. const scaleX = window.innerWidth / 1920;
  1019. const scaleY = window.innerHeight / 1080;
  1020. const scale = Math.min(scaleX, scaleY);
  1021. stage.style.transform = `translate(-50%, -50%) scale(${scale})`;
  1022. }
  1023. fitStage();
  1024. window.addEventListener('resize', fitStage);
  1025. // Replay
  1026. replayBtn.addEventListener('click', () => {
  1027. startMs = null;
  1028. replayBtn.classList.remove('visible');
  1029. });
  1030. // =============== Expose for frame-accurate rendering ===============
  1031. window.__setTime = (t) => { manualT = t; render(t); };
  1032. window.__resume = () => { manualT = null; startMs = null; };
  1033. window.__duration = T.DURATION;
  1034. window.__render = render;
  1035. })();
  1036. </script>
  1037. </body>
  1038. </html>