1
0

c4-tweaks.html 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989
  1. <!doctype html>
  2. <html lang="zh-Hans">
  3. <head>
  4. <meta charset="utf-8" />
  5. <title>c4-tweaks · 拨动即所得(中文版)</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&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. --hairline: rgba(255,255,255,0.12);
  17. --accent: #D97757;
  18. --accent-deep: #B85D3D;
  19. /* Mock landing page · warm variant (initial state) */
  20. --warm-bg: #F6EFE6;
  21. --warm-panel: #FFFFFF;
  22. --warm-ink: #1A1918;
  23. --warm-dim: #8B867E;
  24. --warm-hair: rgba(0,0,0,0.08);
  25. --warm-accent: #D97757;
  26. /* Mock landing page · cool variant (after slider 1) */
  27. --cool-bg: #0E1620;
  28. --cool-panel: #17222E;
  29. --cool-ink: #E8EEF5;
  30. --cool-dim: #7A8A9B;
  31. --cool-hair: rgba(255,255,255,0.08);
  32. --cool-accent: #5A8CB8;
  33. --serif-en: "Source Serif 4", Georgia, serif;
  34. --serif-cn: "Noto Serif SC", "Source Serif 4", Georgia, serif;
  35. --sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
  36. --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
  37. }
  38. html, body {
  39. margin: 0; padding: 0;
  40. background: #000;
  41. overflow: hidden;
  42. font-family: var(--sans);
  43. color: var(--ink);
  44. -webkit-font-smoothing: antialiased;
  45. }
  46. * { box-sizing: border-box; }
  47. .stage {
  48. position: fixed;
  49. top: 50%; left: 50%;
  50. width: 1920px; height: 1080px;
  51. transform: translate(-50%, -50%);
  52. transform-origin: center center;
  53. background: var(--bg);
  54. overflow: hidden;
  55. }
  56. /* Film grain */
  57. .grain {
  58. position: absolute; inset: 0;
  59. background-image:
  60. radial-gradient(rgba(255,255,255,0.02) 1px, transparent 1px);
  61. background-size: 3px 3px;
  62. opacity: 0.4;
  63. pointer-events: none;
  64. z-index: 2;
  65. }
  66. /* Watermark */
  67. .watermark {
  68. position: absolute;
  69. top: 44px; left: 56px;
  70. font-family: var(--mono);
  71. font-size: 14px;
  72. font-weight: 500;
  73. letter-spacing: 0.2em;
  74. color: rgba(255,255,255,0.16);
  75. z-index: 10;
  76. }
  77. .version-mark {
  78. position: absolute;
  79. bottom: 44px; right: 56px;
  80. font-family: var(--mono);
  81. font-size: 12px;
  82. letter-spacing: 0.2em;
  83. color: rgba(255,255,255,0.12);
  84. z-index: 10;
  85. }
  86. /* ============ Main composition ============ */
  87. .composition {
  88. position: absolute;
  89. inset: 0;
  90. display: grid;
  91. grid-template-columns: 1080px 500px;
  92. gap: 80px;
  93. padding: 130px 120px 140px 140px;
  94. align-items: center;
  95. perspective: 2400px;
  96. }
  97. /* ---- Design preview (left) ---- */
  98. .preview-frame {
  99. position: relative;
  100. width: 1080px;
  101. height: 800px;
  102. border-radius: 18px;
  103. overflow: hidden;
  104. transform-style: preserve-3d;
  105. transform: rotateX(6deg) rotateY(-4deg);
  106. box-shadow:
  107. 0 50px 120px rgba(0,0,0,0.6),
  108. 0 0 0 1px rgba(255,255,255,0.06);
  109. opacity: 0;
  110. will-change: opacity, transform, background;
  111. transition: background 280ms cubic-bezier(.2,.8,.2,1);
  112. }
  113. .preview-frame.warm {
  114. background: var(--warm-bg);
  115. }
  116. .preview-frame.cool {
  117. background: var(--cool-bg);
  118. }
  119. /* Browser chrome top bar */
  120. .browser-chrome {
  121. display: flex;
  122. align-items: center;
  123. gap: 10px;
  124. padding: 16px 22px;
  125. border-bottom: 1px solid var(--warm-hair);
  126. background: var(--warm-panel);
  127. transition: all 280ms cubic-bezier(.2,.8,.2,1);
  128. }
  129. .cool .browser-chrome {
  130. background: var(--cool-panel);
  131. border-bottom-color: var(--cool-hair);
  132. }
  133. .dot {
  134. width: 11px; height: 11px; border-radius: 50%;
  135. background: rgba(0,0,0,0.14);
  136. }
  137. .cool .dot { background: rgba(255,255,255,0.14); }
  138. .url-bar {
  139. flex: 1;
  140. margin-left: 14px;
  141. padding: 6px 14px;
  142. border-radius: 6px;
  143. background: rgba(0,0,0,0.04);
  144. font-family: var(--mono);
  145. font-size: 12px;
  146. color: var(--warm-dim);
  147. letter-spacing: 0.05em;
  148. transition: all 280ms cubic-bezier(.2,.8,.2,1);
  149. }
  150. .cool .url-bar {
  151. background: rgba(255,255,255,0.04);
  152. color: var(--cool-dim);
  153. }
  154. /* Hero content */
  155. .preview-body {
  156. padding: 54px 72px 60px 72px;
  157. color: var(--warm-ink);
  158. transition: color 280ms cubic-bezier(.2,.8,.2,1);
  159. }
  160. .cool .preview-body { color: var(--cool-ink); }
  161. .preview-eyebrow {
  162. font-family: var(--mono);
  163. font-size: 11px;
  164. font-weight: 500;
  165. letter-spacing: 0.24em;
  166. text-transform: uppercase;
  167. color: var(--warm-accent);
  168. transition: color 280ms cubic-bezier(.2,.8,.2,1);
  169. }
  170. .cool .preview-eyebrow { color: var(--cool-accent); }
  171. .preview-title {
  172. margin-top: 16px;
  173. font-family: var(--serif-cn);
  174. font-weight: 400;
  175. font-size: 86px;
  176. line-height: 1.02;
  177. letter-spacing: -0.02em;
  178. transition: font-family 240ms cubic-bezier(.2,.8,.2,1),
  179. font-weight 240ms cubic-bezier(.2,.8,.2,1),
  180. letter-spacing 240ms cubic-bezier(.2,.8,.2,1);
  181. }
  182. .preview-title .em {
  183. color: var(--warm-accent);
  184. font-style: italic;
  185. transition: color 280ms cubic-bezier(.2,.8,.2,1);
  186. }
  187. .cool .preview-title .em { color: var(--cool-accent); }
  188. .preview-frame.sans .preview-title {
  189. font-family: var(--sans);
  190. font-weight: 200;
  191. letter-spacing: -0.045em;
  192. }
  193. .preview-frame.sans .preview-title .em {
  194. font-style: normal;
  195. }
  196. .preview-sub {
  197. margin-top: 24px;
  198. font-family: var(--serif-cn);
  199. font-size: 20px;
  200. font-weight: 300;
  201. line-height: 1.6;
  202. max-width: 720px;
  203. color: var(--warm-dim);
  204. transition: color 280ms cubic-bezier(.2,.8,.2,1),
  205. font-family 240ms cubic-bezier(.2,.8,.2,1);
  206. }
  207. .cool .preview-sub { color: var(--cool-dim); }
  208. .preview-frame.sans .preview-sub {
  209. font-family: var(--sans);
  210. }
  211. /* Density cards grid */
  212. .card-grid {
  213. margin-top: 54px;
  214. display: grid;
  215. grid-template-columns: repeat(3, 1fr);
  216. gap: 18px;
  217. transition: grid-template-columns 280ms cubic-bezier(.2,.8,.2,1),
  218. gap 280ms cubic-bezier(.2,.8,.2,1);
  219. }
  220. .preview-frame.dense .card-grid {
  221. grid-template-columns: repeat(3, 1fr);
  222. grid-auto-rows: minmax(72px, auto);
  223. gap: 10px;
  224. }
  225. .card {
  226. padding: 22px 22px 24px 22px;
  227. border-radius: 10px;
  228. background: rgba(0,0,0,0.035);
  229. border: 1px solid var(--warm-hair);
  230. transition: all 280ms cubic-bezier(.2,.8,.2,1);
  231. }
  232. .cool .card {
  233. background: rgba(255,255,255,0.03);
  234. border-color: var(--cool-hair);
  235. }
  236. .preview-frame.dense .card {
  237. padding: 12px 14px;
  238. }
  239. .card-icon {
  240. width: 28px; height: 28px;
  241. border-radius: 6px;
  242. background: var(--warm-accent);
  243. opacity: 0.16;
  244. margin-bottom: 14px;
  245. transition: all 280ms cubic-bezier(.2,.8,.2,1);
  246. }
  247. .cool .card-icon { background: var(--cool-accent); }
  248. .preview-frame.dense .card-icon {
  249. width: 18px; height: 18px;
  250. margin-bottom: 8px;
  251. }
  252. .card-title {
  253. font-family: var(--serif-cn);
  254. font-size: 18px;
  255. font-weight: 500;
  256. color: var(--warm-ink);
  257. letter-spacing: -0.005em;
  258. transition: color 280ms cubic-bezier(.2,.8,.2,1),
  259. font-family 240ms cubic-bezier(.2,.8,.2,1),
  260. font-size 280ms cubic-bezier(.2,.8,.2,1);
  261. }
  262. .cool .card-title { color: var(--cool-ink); }
  263. .preview-frame.sans .card-title {
  264. font-family: var(--sans);
  265. font-weight: 500;
  266. }
  267. .preview-frame.dense .card-title {
  268. font-size: 13px;
  269. }
  270. .card-text {
  271. margin-top: 6px;
  272. font-family: var(--serif-cn);
  273. font-size: 13px;
  274. line-height: 1.45;
  275. color: var(--warm-dim);
  276. transition: all 280ms cubic-bezier(.2,.8,.2,1);
  277. }
  278. .cool .card-text { color: var(--cool-dim); }
  279. .preview-frame.sans .card-text { font-family: var(--sans); }
  280. .preview-frame.dense .card-text {
  281. font-size: 11px;
  282. line-height: 1.3;
  283. opacity: 0.85;
  284. }
  285. /* Extra cards (hidden in sparse mode) */
  286. .card.extra {
  287. opacity: 0;
  288. transform: scale(0.92);
  289. transition: opacity 240ms cubic-bezier(.2,.8,.2,1),
  290. transform 240ms cubic-bezier(.2,.8,.2,1),
  291. background 280ms cubic-bezier(.2,.8,.2,1),
  292. border-color 280ms cubic-bezier(.2,.8,.2,1);
  293. pointer-events: none;
  294. max-height: 0;
  295. padding: 0;
  296. overflow: hidden;
  297. }
  298. .preview-frame.dense .card.extra {
  299. opacity: 1;
  300. transform: scale(1);
  301. max-height: 120px;
  302. padding: 12px 14px;
  303. }
  304. /* ---- Slider panel (right) ---- */
  305. .slider-panel {
  306. position: relative;
  307. width: 500px;
  308. opacity: 0;
  309. will-change: opacity, transform;
  310. display: flex;
  311. flex-direction: column;
  312. gap: 64px;
  313. }
  314. .anchor-line {
  315. position: absolute;
  316. top: -80px;
  317. left: 8px;
  318. font-family: var(--serif-cn);
  319. font-weight: 400;
  320. font-size: 26px;
  321. letter-spacing: 0.02em;
  322. color: var(--ink-80);
  323. opacity: 0;
  324. will-change: opacity, transform;
  325. }
  326. .anchor-line .em {
  327. color: var(--accent);
  328. font-weight: 500;
  329. }
  330. .slider-item {
  331. display: flex;
  332. flex-direction: column;
  333. gap: 18px;
  334. }
  335. .slider-label {
  336. display: flex;
  337. align-items: baseline;
  338. justify-content: space-between;
  339. }
  340. .slider-name {
  341. font-family: var(--mono);
  342. font-size: 14px;
  343. font-weight: 500;
  344. letter-spacing: 0.18em;
  345. color: var(--ink-80);
  346. text-transform: uppercase;
  347. }
  348. .slider-value {
  349. font-family: var(--mono);
  350. font-size: 12px;
  351. letter-spacing: 0.14em;
  352. color: var(--muted);
  353. }
  354. /* Track */
  355. .track {
  356. position: relative;
  357. width: 100%;
  358. height: 2px;
  359. background: var(--hairline);
  360. }
  361. .track-fill {
  362. position: absolute;
  363. top: 0; left: 0;
  364. height: 100%;
  365. width: 10%;
  366. background: var(--accent);
  367. will-change: width;
  368. }
  369. /* Tick marks */
  370. .ticks {
  371. position: absolute;
  372. inset: -4px 0 -4px 0;
  373. display: flex;
  374. justify-content: space-between;
  375. pointer-events: none;
  376. }
  377. .tick {
  378. width: 1px;
  379. height: 10px;
  380. background: rgba(255,255,255,0.14);
  381. }
  382. /* Knob */
  383. .knob {
  384. position: absolute;
  385. top: 50%;
  386. left: 10%;
  387. width: 26px; height: 26px;
  388. border-radius: 50%;
  389. background: var(--ink);
  390. transform: translate(-50%, -50%);
  391. box-shadow: 0 0 0 1px rgba(0,0,0,0.6),
  392. 0 8px 24px rgba(0,0,0,0.5);
  393. will-change: left, transform, box-shadow;
  394. }
  395. .knob.active {
  396. box-shadow: 0 0 0 2px var(--accent),
  397. 0 0 30px rgba(217,119,87,0.45),
  398. 0 8px 24px rgba(0,0,0,0.5);
  399. }
  400. /* Cursor */
  401. .cursor {
  402. position: absolute;
  403. width: 20px; height: 20px;
  404. pointer-events: none;
  405. will-change: left, top, opacity;
  406. opacity: 0;
  407. z-index: 20;
  408. }
  409. .cursor svg { width: 100%; height: 100%; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.8)); }
  410. /* ---- Brand reveal ---- */
  411. /* Stage dimmer: fades the composition out just before the panel slides in */
  412. .stage-dimmer {
  413. position: absolute;
  414. inset: 0;
  415. background: #000000;
  416. opacity: 0;
  417. z-index: 40;
  418. pointer-events: none;
  419. will-change: opacity;
  420. }
  421. .brand-panel {
  422. position: absolute;
  423. inset: 0;
  424. background: #F5F4F0;
  425. transform: translateY(100%);
  426. display: flex;
  427. flex-direction: column;
  428. align-items: center;
  429. justify-content: center;
  430. z-index: 50;
  431. will-change: transform;
  432. }
  433. .brand-wordmark {
  434. font-family: var(--serif-en);
  435. font-size: 72px;
  436. font-weight: 100;
  437. font-variation-settings: "wght" 100;
  438. letter-spacing: -0.02em;
  439. color: #1A1918;
  440. text-align: center;
  441. line-height: 1;
  442. opacity: 0;
  443. transform: translateY(20px);
  444. will-change: opacity, transform, font-variation-settings, font-weight;
  445. }
  446. .brand-wordmark .accent { color: #D97757; font-weight: inherit; }
  447. .brand-line {
  448. /* Flex-centered, 60px below wordmark (line-height 1 @ 72px → descender + 24 gap) */
  449. margin-top: 60px;
  450. height: 2px;
  451. width: 0;
  452. background: #D97757;
  453. align-self: center;
  454. will-change: width;
  455. }
  456. </style>
  457. </head>
  458. <body>
  459. <div class="stage" id="stage">
  460. <div class="grain"></div>
  461. <div class="watermark">HUASHU · DESIGN</div>
  462. <div class="version-mark">V2 · 2026</div>
  463. <div class="composition">
  464. <!-- LEFT: design preview -->
  465. <div class="preview-frame warm" id="preview">
  466. <div class="browser-chrome">
  467. <span class="dot"></span><span class="dot"></span><span class="dot"></span>
  468. <div class="url-bar">yourbrand.design</div>
  469. </div>
  470. <div class="preview-body">
  471. <div class="preview-eyebrow">Agent Studio</div>
  472. <div class="preview-title">为<span class="em">他们</span>造好<br/>工作的场所。</div>
  473. <div class="preview-sub">一个设计系统,不等你打开;它在你睡觉时,已经把草稿交出来了。</div>
  474. <div class="card-grid" id="cardGrid">
  475. <div class="card">
  476. <div class="card-icon"></div>
  477. <div class="card-title">品牌资产</div>
  478. <div class="card-text">Logo / 色板 / 字型的单一事实源。</div>
  479. </div>
  480. <div class="card">
  481. <div class="card-icon"></div>
  482. <div class="card-title">原型工场</div>
  483. <div class="card-text">写一句话,得到一个能点的 App。</div>
  484. </div>
  485. <div class="card">
  486. <div class="card-icon"></div>
  487. <div class="card-title">动效引擎</div>
  488. <div class="card-text">时间轴即代码,25 到 60 帧随意切。</div>
  489. </div>
  490. <div class="card extra">
  491. <div class="card-icon"></div>
  492. <div class="card-title">文档工坊</div>
  493. <div class="card-text">HTML 即 PPTX。</div>
  494. </div>
  495. <div class="card extra">
  496. <div class="card-icon"></div>
  497. <div class="card-title">信息图</div>
  498. <div class="card-text">数据进,杂志出。</div>
  499. </div>
  500. <div class="card extra">
  501. <div class="card-icon"></div>
  502. <div class="card-title">专家评审</div>
  503. <div class="card-text">五维打分,诚实的体检。</div>
  504. </div>
  505. <div class="card extra">
  506. <div class="card-icon"></div>
  507. <div class="card-title">方向顾问</div>
  508. <div class="card-text">给你三条路选。</div>
  509. </div>
  510. <div class="card extra">
  511. <div class="card-icon"></div>
  512. <div class="card-title">Junior 模式</div>
  513. <div class="card-text">先 show,再精修。</div>
  514. </div>
  515. <div class="card extra">
  516. <div class="card-icon"></div>
  517. <div class="card-title">品牌协议</div>
  518. <div class="card-text">五步,不能跳。</div>
  519. </div>
  520. </div>
  521. </div>
  522. </div>
  523. <!-- RIGHT: slider panel -->
  524. <div class="slider-panel" id="panel">
  525. <div class="anchor-line" id="anchor">
  526. 拨动<span class="em">即所得</span>
  527. </div>
  528. <!-- Slider 1 · 调色 -->
  529. <div class="slider-item">
  530. <div class="slider-label">
  531. <span class="slider-name">调色</span>
  532. <span class="slider-value" id="val1">warm</span>
  533. </div>
  534. <div class="track">
  535. <div class="ticks">
  536. <span class="tick"></span><span class="tick"></span><span class="tick"></span>
  537. <span class="tick"></span><span class="tick"></span>
  538. </div>
  539. <div class="track-fill" id="fill1"></div>
  540. <div class="knob" id="knob1"></div>
  541. </div>
  542. </div>
  543. <!-- Slider 2 · 字型 -->
  544. <div class="slider-item">
  545. <div class="slider-label">
  546. <span class="slider-name">字型</span>
  547. <span class="slider-value" id="val2">serif</span>
  548. </div>
  549. <div class="track">
  550. <div class="ticks">
  551. <span class="tick"></span><span class="tick"></span><span class="tick"></span>
  552. <span class="tick"></span><span class="tick"></span>
  553. </div>
  554. <div class="track-fill" id="fill2"></div>
  555. <div class="knob" id="knob2"></div>
  556. </div>
  557. </div>
  558. <!-- Slider 3 · 密度 -->
  559. <div class="slider-item">
  560. <div class="slider-label">
  561. <span class="slider-name">密度</span>
  562. <span class="slider-value" id="val3">sparse</span>
  563. </div>
  564. <div class="track">
  565. <div class="ticks">
  566. <span class="tick"></span><span class="tick"></span><span class="tick"></span>
  567. <span class="tick"></span><span class="tick"></span>
  568. </div>
  569. <div class="track-fill" id="fill3"></div>
  570. <div class="knob" id="knob3"></div>
  571. </div>
  572. </div>
  573. </div>
  574. <!-- Cursor -->
  575. <div class="cursor" id="cursor">
  576. <svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
  577. <path d="M2 2 L2 16 L6 12 L9 18 L11 17 L8 11 L14 11 Z"
  578. fill="white" stroke="#000" stroke-width="1.2" stroke-linejoin="round"/>
  579. </svg>
  580. </div>
  581. </div>
  582. <!-- Stage dimmer (fades scene to black before panel sweeps in) -->
  583. <div class="stage-dimmer" id="stageDimmer"></div>
  584. <!-- Brand reveal layer -->
  585. <div class="brand-panel" id="brandPanel">
  586. <div class="brand-wordmark" id="brandMark">huashu<span class="accent">-</span>design</div>
  587. <div class="brand-line" id="brandLine"></div>
  588. </div>
  589. </div>
  590. <script>
  591. (function() {
  592. // ---------- Fit stage ----------
  593. const stage = document.getElementById('stage');
  594. function rescale() {
  595. const s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
  596. stage.style.transform = `translate(-50%, -50%) scale(${s})`;
  597. }
  598. rescale();
  599. window.addEventListener('resize', rescale);
  600. // ---------- Animation ----------
  601. const DURATION = 10.0; // seconds
  602. const preview = document.getElementById('preview');
  603. const panel = document.getElementById('panel');
  604. const anchor = document.getElementById('anchor');
  605. const cursor = document.getElementById('cursor');
  606. const knob1 = document.getElementById('knob1');
  607. const knob2 = document.getElementById('knob2');
  608. const knob3 = document.getElementById('knob3');
  609. const fill1 = document.getElementById('fill1');
  610. const fill2 = document.getElementById('fill2');
  611. const fill3 = document.getElementById('fill3');
  612. const val1 = document.getElementById('val1');
  613. const val2 = document.getElementById('val2');
  614. const val3 = document.getElementById('val3');
  615. const stageDimmer = document.getElementById('stageDimmer');
  616. const brandPanel = document.getElementById('brandPanel');
  617. const brandMark = document.getElementById('brandMark');
  618. const brandLine = document.getElementById('brandLine');
  619. // Easings
  620. const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
  621. const expoIn = t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
  622. const cubicInOut = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2;
  623. const cubicOut = t => 1 - Math.pow(1 - t, 3);
  624. function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
  625. function lerp(t, t0, t1, v0, v1, ease) {
  626. if (t <= t0) return v0;
  627. if (t >= t1) return v1;
  628. const k = (t - t0) / (t1 - t0);
  629. return v0 + (v1 - v0) * (ease ? ease(k) : k);
  630. }
  631. function clampLerp(t, t0, t1) {
  632. if (t <= t0) return 0;
  633. if (t >= t1) return 1;
  634. return (t - t0) / (t1 - t0);
  635. }
  636. // Knob motion — drag feel: first 70% is a cubic ease (hand moving),
  637. // final 15% is overshoot + snap to target (magnetic arrival).
  638. function knobMotion(t, t0, t1, fromPct, toPct) {
  639. if (t <= t0) return fromPct;
  640. if (t >= t1) return toPct;
  641. const k = (t - t0) / (t1 - t0);
  642. const direction = toPct > fromPct ? 1 : -1;
  643. const range = Math.abs(toPct - fromPct);
  644. if (k < 0.72) {
  645. // Main drag: cubic easeInOut feels like a hand moving
  646. const e = cubicInOut(k / 0.72);
  647. return fromPct + (toPct - fromPct) * e;
  648. } else if (k < 0.85) {
  649. // Overshoot past target by ~2%
  650. const overK = (k - 0.72) / 0.13;
  651. const overshoot = 2.2;
  652. return toPct + direction * overshoot * Math.sin(overK * Math.PI);
  653. } else {
  654. // Settled at target
  655. return toPct;
  656. }
  657. }
  658. // Timeline (seconds, 10s total)
  659. const T = {
  660. stage_in: [0.0, 1.0], // frame + panel appear
  661. anchor_in: [0.8, 1.4],
  662. // Slider 1 · palette: warm → cool (1.2s → 3.2s) — arrive at 3.0s
  663. s1_cursor_to: [1.3, 1.9],
  664. s1_drag: [1.9, 2.9],
  665. s1_settle: [2.9, 3.1],
  666. // Slider 2 · type: serif → sans
  667. s2_cursor_to: [3.2, 3.7],
  668. s2_drag: [3.7, 4.7],
  669. s2_settle: [4.7, 4.9],
  670. // Slider 3 · density: sparse → dense
  671. s3_cursor_to: [5.0, 5.5],
  672. s3_drag: [5.5, 6.5],
  673. s3_settle: [6.5, 6.7],
  674. hold: [6.7, 8.0],
  675. // Brand reveal (米色 walloff · 2s total)
  676. scene_out: [8.0, 8.3], // main composition fade to black (0.3s)
  677. brand_panel: [8.3, 8.7], // cream panel sweeps up from bottom, expoOut (0.4s)
  678. brand_mark: [8.7, 9.3], // wordmark: wght 100→500 + y 20→0 + opacity 0→1 (0.6s)
  679. brand_line: [9.3, 9.7], // orange line expands 0→280 from center (0.4s)
  680. brand_hold: [9.7, 10.0], // hold final frame
  681. };
  682. // Slider-to-state logic. Value-changes happen at settle start.
  683. let state = { palette: 'warm', type: 'serif', density: 'sparse' };
  684. let lastStateHash = '';
  685. function updatePreview() {
  686. preview.classList.remove('warm', 'cool', 'sans', 'dense');
  687. if (state.palette === 'warm') preview.classList.add('warm');
  688. else preview.classList.add('cool');
  689. if (state.type === 'sans') preview.classList.add('sans');
  690. if (state.density === 'dense') preview.classList.add('dense');
  691. }
  692. updatePreview();
  693. function setKnobState(knob, active) {
  694. if (active) knob.classList.add('active');
  695. else knob.classList.remove('active');
  696. }
  697. function setValueLabel(el, text) {
  698. if (el.textContent !== text) el.textContent = text;
  699. }
  700. // ---------- Cursor path (in composition coords) ----------
  701. // Composition uses grid: left column 1220 + 60 gap, panel is at right.
  702. // We'll position cursor using .composition-relative absolute positioning.
  703. // Cursor is child of .composition, whose padding is 130/100/140/140.
  704. // So coords relative to .composition padding-box.
  705. // Simpler: cursor is absolute in .stage coords since parent composition
  706. // covers full stage. Use inline style left/top in px.
  707. // Anchor positions (rough — will fine-tune):
  708. const CURSOR_PARK = { x: 1900, y: 1080 }; // off-screen bottom-right
  709. // Slider tracks: panel starts around x≈1420, width 520. Each track spans that width.
  710. // We'll measure actual rect at first tick.
  711. let sliderRects = null;
  712. function measureRects() {
  713. const stageRect = stage.getBoundingClientRect();
  714. const scale = stageRect.width / 1920;
  715. const getTrackBox = (id) => {
  716. const el = document.getElementById(id).parentElement; // .track
  717. const r = el.getBoundingClientRect();
  718. return {
  719. left: (r.left - stageRect.left) / scale,
  720. top: (r.top - stageRect.top) / scale,
  721. width: r.width / scale,
  722. height: r.height / scale,
  723. };
  724. };
  725. sliderRects = {
  726. s1: getTrackBox('knob1'),
  727. s2: getTrackBox('knob2'),
  728. s3: getTrackBox('knob3'),
  729. };
  730. }
  731. function positionCursor(x, y, opacity) {
  732. cursor.style.left = x + 'px';
  733. cursor.style.top = y + 'px';
  734. cursor.style.opacity = opacity;
  735. }
  736. function knobLeft(id, pct) {
  737. const el = document.getElementById(id);
  738. el.style.left = pct + '%';
  739. }
  740. function fillWidth(id, pct) {
  741. const el = document.getElementById(id);
  742. el.style.width = pct + '%';
  743. }
  744. // Tick / render
  745. let startTs = null;
  746. let frameCount = 0;
  747. function tick(ts) {
  748. if (!startTs) startTs = ts;
  749. const t = (ts - startTs) / 1000;
  750. // Measure rects once
  751. if (!sliderRects && frameCount > 1) {
  752. measureRects();
  753. }
  754. // --- Stage in ---
  755. const stageK = clampLerp(t, T.stage_in[0], T.stage_in[1]);
  756. const stageOp = cubicOut(stageK);
  757. preview.style.opacity = stageOp;
  758. preview.style.transform = `rotateX(${lerp(t, T.stage_in[0], T.stage_in[1], 10, 6, cubicOut)}deg) rotateY(-4deg) translateY(${lerp(t, T.stage_in[0], T.stage_in[1], 20, 0, expoOut)}px)`;
  759. panel.style.opacity = stageOp;
  760. panel.style.transform = `translateX(${lerp(t, T.stage_in[0], T.stage_in[1], 30, 0, expoOut)}px)`;
  761. // Anchor
  762. const aK = clampLerp(t, T.anchor_in[0], T.anchor_in[1]);
  763. anchor.style.opacity = cubicOut(aK);
  764. anchor.style.transform = `translateY(${lerp(t, T.anchor_in[0], T.anchor_in[1], 10, 0, expoOut)}px)`;
  765. // Snap point: when knob reaches target (72% of drag duration)
  766. const s1SnapT = T.s1_drag[0] + (T.s1_drag[1] - T.s1_drag[0]) * 0.72;
  767. const s2SnapT = T.s2_drag[0] + (T.s2_drag[1] - T.s2_drag[0]) * 0.72;
  768. const s3SnapT = T.s3_drag[0] + (T.s3_drag[1] - T.s3_drag[0]) * 0.72;
  769. // --- Slider 1: palette ---
  770. // Knob 10% → 90%
  771. const k1pct = knobMotion(t, T.s1_drag[0], T.s1_drag[1], 10, 90);
  772. knobLeft('knob1', k1pct); fillWidth('fill1', k1pct);
  773. setKnobState(knob1, t >= T.s1_cursor_to[0] && t < T.s1_settle[1] + 0.2);
  774. if (t >= s1SnapT && state.palette !== 'cool') {
  775. state.palette = 'cool'; updatePreview(); setValueLabel(val1, 'cool');
  776. }
  777. // --- Slider 2: type ---
  778. const k2pct = knobMotion(t, T.s2_drag[0], T.s2_drag[1], 10, 90);
  779. knobLeft('knob2', k2pct); fillWidth('fill2', k2pct);
  780. setKnobState(knob2, t >= T.s2_cursor_to[0] && t < T.s2_settle[1] + 0.2);
  781. if (t >= s2SnapT && state.type !== 'sans') {
  782. state.type = 'sans'; updatePreview(); setValueLabel(val2, 'sans');
  783. }
  784. // --- Slider 3: density ---
  785. const k3pct = knobMotion(t, T.s3_drag[0], T.s3_drag[1], 10, 90);
  786. knobLeft('knob3', k3pct); fillWidth('fill3', k3pct);
  787. setKnobState(knob3, t >= T.s3_cursor_to[0] && t < T.s3_settle[1] + 0.2);
  788. if (t >= s3SnapT && state.density !== 'dense') {
  789. state.density = 'dense'; updatePreview(); setValueLabel(val3, 'dense');
  790. }
  791. // --- Cursor choreography ---
  792. if (sliderRects) {
  793. const r1 = sliderRects.s1, r2 = sliderRects.s2, r3 = sliderRects.s3;
  794. // Positions of knob at 10% and 90%
  795. const k1Start = { x: r1.left + r1.width * 0.10, y: r1.top + r1.height/2 };
  796. const k1End = { x: r1.left + r1.width * 0.90, y: r1.top + r1.height/2 };
  797. const k2Start = { x: r2.left + r2.width * 0.10, y: r2.top + r2.height/2 };
  798. const k2End = { x: r2.left + r2.width * 0.90, y: r2.top + r2.height/2 };
  799. const k3Start = { x: r3.left + r3.width * 0.10, y: r3.top + r3.height/2 };
  800. const k3End = { x: r3.left + r3.width * 0.90, y: r3.top + r3.height/2 };
  801. let cx = CURSOR_PARK.x, cy = CURSOR_PARK.y, co = 0;
  802. if (t < T.s1_cursor_to[0]) {
  803. // still off-screen (or just appeared)
  804. cx = CURSOR_PARK.x; cy = CURSOR_PARK.y; co = 0;
  805. } else if (t < T.s1_cursor_to[1]) {
  806. // cursor flies to s1 knob start
  807. const k = clampLerp(t, T.s1_cursor_to[0], T.s1_cursor_to[1]);
  808. const e = cubicOut(k);
  809. cx = lerp(t, T.s1_cursor_to[0], T.s1_cursor_to[1], CURSOR_PARK.x, k1Start.x, cubicOut);
  810. cy = lerp(t, T.s1_cursor_to[0], T.s1_cursor_to[1], CURSOR_PARK.y, k1Start.y, cubicOut);
  811. co = e;
  812. } else if (t < T.s1_drag[1]) {
  813. // dragging s1
  814. cx = r1.left + (r1.width * k1pct / 100);
  815. cy = r1.top + r1.height/2;
  816. co = 1;
  817. } else if (t < T.s2_cursor_to[0]) {
  818. cx = k1End.x; cy = k1End.y; co = 1;
  819. } else if (t < T.s2_cursor_to[1]) {
  820. cx = lerp(t, T.s2_cursor_to[0], T.s2_cursor_to[1], k1End.x, k2Start.x, cubicInOut);
  821. cy = lerp(t, T.s2_cursor_to[0], T.s2_cursor_to[1], k1End.y, k2Start.y, cubicInOut);
  822. co = 1;
  823. } else if (t < T.s2_drag[1]) {
  824. cx = r2.left + (r2.width * k2pct / 100);
  825. cy = r2.top + r2.height/2;
  826. co = 1;
  827. } else if (t < T.s3_cursor_to[0]) {
  828. cx = k2End.x; cy = k2End.y; co = 1;
  829. } else if (t < T.s3_cursor_to[1]) {
  830. cx = lerp(t, T.s3_cursor_to[0], T.s3_cursor_to[1], k2End.x, k3Start.x, cubicInOut);
  831. cy = lerp(t, T.s3_cursor_to[0], T.s3_cursor_to[1], k2End.y, k3Start.y, cubicInOut);
  832. co = 1;
  833. } else if (t < T.s3_drag[1]) {
  834. cx = r3.left + (r3.width * k3pct / 100);
  835. cy = r3.top + r3.height/2;
  836. co = 1;
  837. } else if (t < T.hold[1]) {
  838. // fade out cursor
  839. cx = k3End.x; cy = k3End.y;
  840. co = lerp(t, T.s3_drag[1], T.hold[1], 1, 0, cubicOut);
  841. }
  842. positionCursor(cx, cy, co);
  843. }
  844. // --- Brand reveal (米色 walloff · aligned with hero-v10 signature) ---
  845. // 1) Scene dimmer: composition fades to black (0.3s)
  846. const soK = clampLerp(t, T.scene_out[0], T.scene_out[1]);
  847. stageDimmer.style.opacity = cubicOut(soK);
  848. // 2) Cream panel sweeps up from bottom, expoOut (0.4s)
  849. const bpK = clampLerp(t, T.brand_panel[0], T.brand_panel[1]);
  850. const panelY = lerp(t, T.brand_panel[0], T.brand_panel[1], 100, 0, expoOut);
  851. brandPanel.style.transform = `translateY(${panelY}%)`;
  852. // 3) Wordmark: font-weight 100→500 + y 20→0 + opacity 0→1, expoOut (0.6s)
  853. const bmK = clampLerp(t, T.brand_mark[0], T.brand_mark[1]);
  854. const bmE = expoOut(bmK);
  855. const wght = 100 + (500 - 100) * bmE;
  856. brandMark.style.opacity = bmE;
  857. brandMark.style.transform = `translateY(${20 * (1 - bmE)}px)`;
  858. brandMark.style.fontWeight = Math.round(wght);
  859. brandMark.style.fontVariationSettings = `"wght" ${wght.toFixed(0)}`;
  860. // 4) Orange line: width 0→280 from center, cubicOut (0.4s)
  861. const blK = clampLerp(t, T.brand_line[0], T.brand_line[1]);
  862. brandLine.style.width = (280 * cubicOut(blK)) + 'px';
  863. frameCount++;
  864. // Loop or stop
  865. if (t < DURATION) {
  866. requestAnimationFrame(tick);
  867. } else {
  868. if (window.__recording === true) {
  869. // recording mode: hold last frame
  870. return;
  871. }
  872. // Restart after 1s pause (for manual viewing)
  873. setTimeout(() => {
  874. startTs = null;
  875. state = { palette: 'warm', type: 'serif', density: 'sparse' };
  876. updatePreview();
  877. setValueLabel(val1, 'warm'); setValueLabel(val2, 'serif'); setValueLabel(val3, 'sparse');
  878. requestAnimationFrame(tick);
  879. }, 900);
  880. }
  881. }
  882. // Start animation after fonts ready
  883. const startAnim = () => {
  884. requestAnimationFrame((ts) => {
  885. startTs = ts;
  886. window.__ready = true; // signal for render-video.js
  887. requestAnimationFrame(tick);
  888. });
  889. };
  890. if (document.fonts && document.fonts.ready) {
  891. document.fonts.ready.then(startAnim);
  892. } else {
  893. setTimeout(startAnim, 500);
  894. }
  895. })();
  896. </script>
  897. </body>
  898. </html>