1
0

w3-fallback-advisor.html 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704
  1. <!doctype html>
  2. <html lang="zh-Hans">
  3. <head>
  4. <meta charset="utf-8" />
  5. <title>w3 · Fallback Advisor(中文版)</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&family=Noto+Serif+SC:wght@300;400;500;600&family=Inter:wght@200;300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
  9. <style>
  10. :root {
  11. --bg: #000000;
  12. --ink: #FFFFFF;
  13. --ink-80: rgba(255,255,255,0.82);
  14. --ink-60: rgba(255,255,255,0.58);
  15. --muted: rgba(255,255,255,0.40);
  16. --dim: rgba(255,255,255,0.18);
  17. --hairline: rgba(255,255,255,0.12);
  18. --accent: #D97757;
  19. --accent-deep: #B85D3D;
  20. --cd-bg: #F5F4F0;
  21. --cd-ink: #1A1918;
  22. --serif-cn: "Noto Serif SC", "Songti SC", serif;
  23. --serif-en: "Source Serif 4", Georgia, serif;
  24. --sans: "Inter", -apple-system, "PingFang 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. /* ============ Watermark ============ */
  45. .watermark-tl {
  46. position: absolute;
  47. top: 40px; left: 56px;
  48. font-family: var(--mono);
  49. font-size: 12px;
  50. letter-spacing: 0.2em;
  51. color: rgba(255,255,255,0.16);
  52. z-index: 200;
  53. pointer-events: none;
  54. text-transform: uppercase;
  55. }
  56. .watermark-br {
  57. position: absolute;
  58. bottom: 32px; right: 40px;
  59. font-family: var(--mono);
  60. font-size: 10px;
  61. letter-spacing: 0.24em;
  62. color: rgba(255,255,255,0.14);
  63. z-index: 200;
  64. pointer-events: none;
  65. text-transform: uppercase;
  66. }
  67. /* ============ Top Title ============ */
  68. .top-title {
  69. position: absolute;
  70. top: 88px; left: 50%;
  71. transform: translateX(-50%);
  72. font-family: var(--serif-cn);
  73. font-weight: 300;
  74. font-size: 42px;
  75. letter-spacing: 0.02em;
  76. color: var(--ink-80);
  77. text-align: center;
  78. opacity: 0;
  79. will-change: opacity, transform;
  80. z-index: 120;
  81. }
  82. .top-title .accent { color: var(--accent); font-weight: 400; }
  83. .sub-caption {
  84. position: absolute;
  85. top: 148px; left: 50%;
  86. transform: translateX(-50%);
  87. font-family: var(--sans);
  88. font-weight: 300;
  89. font-size: 15px;
  90. letter-spacing: 0.32em;
  91. color: var(--muted);
  92. text-transform: uppercase;
  93. opacity: 0;
  94. will-change: opacity;
  95. z-index: 120;
  96. }
  97. /* ============ Philosophy Wall (4 rows × 5 cols) ============ */
  98. .wall-viewport {
  99. position: absolute;
  100. top: 50%; left: 50%;
  101. transform: translate(-50%, -50%);
  102. width: 1480px;
  103. height: 760px;
  104. perspective: 2400px;
  105. perspective-origin: 50% 50%;
  106. will-change: transform, opacity, filter;
  107. }
  108. .wall-grid {
  109. position: absolute;
  110. inset: 0;
  111. display: grid;
  112. grid-template-columns: repeat(5, 1fr);
  113. grid-template-rows: repeat(4, 1fr);
  114. gap: 18px;
  115. transform: rotateX(10deg) rotateY(-6deg);
  116. transform-style: preserve-3d;
  117. will-change: transform, opacity;
  118. }
  119. .cell {
  120. position: relative;
  121. background: #0f0f0f;
  122. border: 1px solid var(--hairline);
  123. border-radius: 8px;
  124. overflow: hidden;
  125. opacity: 0;
  126. will-change: opacity, transform, filter;
  127. display: flex;
  128. flex-direction: column;
  129. justify-content: space-between;
  130. padding: 14px 16px;
  131. }
  132. /* abstract glyph per cell — geometric, no imagery */
  133. .cell .glyph {
  134. position: absolute;
  135. inset: 0;
  136. display: flex;
  137. align-items: center;
  138. justify-content: center;
  139. pointer-events: none;
  140. }
  141. .cell .name {
  142. position: relative;
  143. font-family: var(--mono);
  144. font-size: 11px;
  145. letter-spacing: 0.08em;
  146. color: var(--muted);
  147. z-index: 2;
  148. align-self: flex-end;
  149. }
  150. .cell .num {
  151. position: relative;
  152. font-family: var(--mono);
  153. font-size: 10px;
  154. color: var(--dim);
  155. letter-spacing: 0.1em;
  156. z-index: 2;
  157. }
  158. /* Selected cells — lit up */
  159. .cell.selected {
  160. border-color: var(--accent);
  161. background: #1a0f0a;
  162. }
  163. .cell.selected .name { color: var(--accent); }
  164. /* ============ Scan light ============ */
  165. .scan-light {
  166. position: absolute;
  167. left: -5%;
  168. right: -5%;
  169. top: -15%;
  170. height: 200px;
  171. background: linear-gradient(
  172. 180deg,
  173. rgba(217, 119, 87, 0) 0%,
  174. rgba(217, 119, 87, 0.18) 40%,
  175. rgba(255, 220, 200, 0.45) 50%,
  176. rgba(217, 119, 87, 0.18) 60%,
  177. rgba(217, 119, 87, 0) 100%
  178. );
  179. filter: blur(8px);
  180. z-index: 80;
  181. opacity: 0;
  182. will-change: opacity, transform;
  183. pointer-events: none;
  184. }
  185. /* ============ Foreground 3 cards ============ */
  186. .fg-row {
  187. position: absolute;
  188. top: 50%; left: 50%;
  189. transform: translate(-50%, -50%);
  190. display: flex;
  191. gap: 56px;
  192. opacity: 0;
  193. will-change: opacity;
  194. z-index: 100;
  195. }
  196. .fg-card {
  197. width: 440px;
  198. display: flex;
  199. flex-direction: column;
  200. align-items: stretch;
  201. opacity: 0;
  202. transform: translateZ(-800px) scale(0.4);
  203. will-change: opacity, transform;
  204. }
  205. .fg-card .card-body {
  206. background: #0f0f0f;
  207. border: 1px solid var(--accent);
  208. border-radius: 12px;
  209. padding: 32px 30px;
  210. box-shadow:
  211. 0 30px 80px -20px rgba(217,119,87,0.25),
  212. 0 10px 30px -10px rgba(0,0,0,0.6);
  213. }
  214. .fg-card .label {
  215. font-family: var(--mono);
  216. font-size: 11px;
  217. letter-spacing: 0.18em;
  218. color: var(--accent);
  219. text-transform: uppercase;
  220. margin-bottom: 14px;
  221. }
  222. .fg-card .title-cn {
  223. font-family: var(--serif-cn);
  224. font-size: 36px;
  225. font-weight: 400;
  226. letter-spacing: 0.01em;
  227. line-height: 1.15;
  228. color: var(--ink);
  229. margin-bottom: 10px;
  230. }
  231. .fg-card .title-en {
  232. font-family: var(--serif-en);
  233. font-style: italic;
  234. font-weight: 300;
  235. font-size: 17px;
  236. letter-spacing: 0.01em;
  237. color: var(--ink-60);
  238. margin-bottom: 22px;
  239. }
  240. .fg-card .feature {
  241. font-family: var(--sans);
  242. font-size: 14px;
  243. font-weight: 300;
  244. letter-spacing: 0.02em;
  245. color: var(--muted);
  246. line-height: 1.6;
  247. padding-top: 18px;
  248. border-top: 1px solid var(--hairline);
  249. }
  250. .fg-card .thumb-wrap {
  251. margin-top: 14px;
  252. height: 0;
  253. overflow: hidden;
  254. border-radius: 10px;
  255. background: #0a0a0a;
  256. border: 1px solid var(--hairline);
  257. opacity: 0;
  258. will-change: opacity, height;
  259. }
  260. .fg-card .thumb-wrap img {
  261. width: 100%;
  262. display: block;
  263. }
  264. /* ============ Brand Reveal (米色盖层) ============ */
  265. .brand-panel {
  266. position: absolute;
  267. inset: 0;
  268. background: var(--cd-bg);
  269. opacity: 0;
  270. transform: translateY(100%);
  271. will-change: opacity, transform;
  272. z-index: 300;
  273. display: flex;
  274. align-items: center;
  275. justify-content: center;
  276. flex-direction: column;
  277. }
  278. .brand-mark {
  279. font-family: var(--serif-en);
  280. font-style: italic;
  281. font-weight: 300;
  282. font-size: 112px;
  283. letter-spacing: -0.02em;
  284. color: var(--cd-ink);
  285. opacity: 0;
  286. transform: scale(0.92);
  287. will-change: opacity, transform;
  288. line-height: 1;
  289. }
  290. .brand-mark .accent { color: var(--accent); font-style: italic; }
  291. .brand-mark .dot { color: var(--accent); font-style: normal; padding: 0 6px; }
  292. .brand-underline {
  293. margin-top: 34px;
  294. height: 2px;
  295. width: 0;
  296. background: var(--accent);
  297. will-change: width;
  298. }
  299. .brand-tag {
  300. margin-top: 22px;
  301. font-family: var(--mono);
  302. font-size: 12px;
  303. letter-spacing: 0.32em;
  304. color: rgba(26,25,24,0.54);
  305. text-transform: uppercase;
  306. opacity: 0;
  307. will-change: opacity;
  308. }
  309. </style>
  310. </head>
  311. <body>
  312. <div class="stage" id="stage">
  313. <!-- 水印 -->
  314. <div class="watermark-tl">HUASHU · DESIGN</div>
  315. <div class="watermark-br">V2 · 2026 · w3</div>
  316. <!-- 顶部标题 -->
  317. <div class="top-title" id="topTitle">
  318. 不知道要什么?<span class="accent">先给你 3 个方向</span>
  319. </div>
  320. <div class="sub-caption" id="subCaption">20 Philosophies · 3 Directions</div>
  321. <!-- 扫描光 -->
  322. <div class="scan-light" id="scanLight"></div>
  323. <!-- 4×5 哲学墙 -->
  324. <div class="wall-viewport" id="wallViewport">
  325. <div class="wall-grid" id="wallGrid">
  326. <!-- 20 cells injected by JS -->
  327. </div>
  328. </div>
  329. <!-- 前景 3 张方向卡 -->
  330. <div class="fg-row" id="fgRow">
  331. <!-- card 1: Kenya Hara · 东方极简 -->
  332. <div class="fg-card" id="card1">
  333. <div class="card-body">
  334. <div class="label">方向 01 · 东方空间</div>
  335. <div class="title-cn">原研哉式留白</div>
  336. <div class="title-en">Kenya Hara</div>
  337. <div class="feature">赤土橙 · 大量留白 · 宣纸质感</div>
  338. </div>
  339. <div class="thumb-wrap" id="thumb1">
  340. <img src="demo-takram.png" alt="demo takram" />
  341. </div>
  342. </div>
  343. <!-- card 2: Pentagram · 信息建筑 -->
  344. <div class="fg-card" id="card2">
  345. <div class="card-body">
  346. <div class="label">方向 02 · 信息建筑</div>
  347. <div class="title-cn">Pentagram 秩序</div>
  348. <div class="title-en">Pentagram</div>
  349. <div class="feature">强网格 · 高对比 · 理性版式</div>
  350. </div>
  351. <div class="thumb-wrap" id="thumb2">
  352. <img src="demo-pentagram.png" alt="demo pentagram" />
  353. </div>
  354. </div>
  355. <!-- card 3: David Carson · 实验先锋 -->
  356. <div class="fg-card" id="card3">
  357. <div class="card-body">
  358. <div class="label">方向 03 · 实验先锋</div>
  359. <div class="title-cn">David Carson 式</div>
  360. <div class="title-en">Experimental Edge</div>
  361. <div class="feature">破格排印 · 粗野几何 · 视觉冲击</div>
  362. </div>
  363. <div class="thumb-wrap" id="thumb3">
  364. <img src="demo-build.png" alt="demo build" />
  365. </div>
  366. </div>
  367. </div>
  368. <!-- Brand Reveal -->
  369. <div class="brand-panel" id="brandPanel">
  370. <div class="brand-mark" id="brandMark">huashu<span class="dot">·</span><span class="accent">design</span></div>
  371. <div class="brand-underline" id="brandUnderline"></div>
  372. <div class="brand-tag" id="brandTag">HTML as Designer's Medium</div>
  373. </div>
  374. </div>
  375. <script>
  376. (function(){
  377. // ============ Stage auto-scale ============
  378. function scaleStage(){
  379. const stage = document.getElementById('stage');
  380. const sx = window.innerWidth / 1920;
  381. const sy = window.innerHeight / 1080;
  382. const s = Math.min(sx, sy);
  383. stage.style.transform = `translate(-50%, -50%) scale(${s})`;
  384. }
  385. window.addEventListener('resize', scaleStage);
  386. scaleStage();
  387. // ============ 20 Philosophies ============
  388. // 4 rows × 5 cols = 20. Selected: idx 0 (Pentagram), idx 9 (Kenya Hara), idx 12 (David Carson)
  389. const PHILOSOPHIES = [
  390. // row 1 — 信息建筑派
  391. { name: 'Pentagram', glyph: 'grid' },
  392. { name: 'M. Vignelli', glyph: 'bars' },
  393. { name: 'Apple HIG', glyph: 'radius' },
  394. { name: 'Spin', glyph: 'slash' },
  395. { name: 'Build', glyph: 'type' },
  396. // row 2 — 运动诗学派
  397. { name: 'Field.io', glyph: 'wave' },
  398. { name: 'Active Theory',glyph: 'orbit' },
  399. { name: 'Hi-Res!', glyph: 'dots' },
  400. { name: 'Locomotive', glyph: 'arrow' },
  401. { name: 'Takram', glyph: 'circle' },
  402. // row 3 — 极简/东方
  403. { name: 'Kenya Hara', glyph: 'ma' },
  404. { name: 'D. Rams', glyph: 'square' },
  405. { name: 'J. Ive', glyph: 'arc' },
  406. { name: 'J. Morrison', glyph: 'minimal' },
  407. { name: 'S. Ogata', glyph: 'line' },
  408. // row 4 — 实验 & 海报
  409. { name: 'D. Carson', glyph: 'collage' },
  410. { name: 'S. Sagmeister',glyph: 'stamp' },
  411. { name: 'P. Scher', glyph: 'poster' },
  412. { name: 'M. Glaser', glyph: 'heart' },
  413. { name: 'K. Sato', glyph: 'logo' },
  414. ];
  415. // selected indices — 3 differentiated directions
  416. const SELECTED = [10, 0, 15]; // Kenya Hara, Pentagram, David Carson
  417. function makeGlyph(kind){
  418. // Simple geometric SVG glyphs — one per cell, no real logos
  419. const svgs = {
  420. grid: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1" fill="none">
  421. <rect x="6" y="8" width="28" height="18"/><rect x="38" y="8" width="28" height="18"/><rect x="70" y="8" width="24" height="44"/>
  422. <rect x="6" y="30" width="60" height="22"/></g></svg>`,
  423. bars: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g fill="rgba(255,255,255,0.22)">
  424. <rect x="10" y="40" width="8" height="16"/><rect x="22" y="28" width="8" height="28"/><rect x="34" y="16" width="8" height="40"/>
  425. <rect x="46" y="24" width="8" height="32"/><rect x="58" y="10" width="8" height="46"/><rect x="70" y="34" width="8" height="22"/>
  426. <rect x="82" y="22" width="8" height="34"/></g></svg>`,
  427. radius: `<svg viewBox="0 0 100 60" width="72%" height="58%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1.2" fill="none">
  428. <rect x="14" y="10" width="72" height="40" rx="20" ry="20"/></g></svg>`,
  429. slash: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1.4" fill="none" stroke-linecap="square">
  430. <path d="M 14 50 L 52 10"/><path d="M 36 50 L 74 10"/><path d="M 58 50 L 86 22"/></g></svg>`,
  431. type: `<svg viewBox="0 0 100 60" width="78%" height="62%"><text x="50" y="42" text-anchor="middle" font-family="Source Serif 4, serif" font-size="40" font-style="italic" fill="rgba(255,255,255,0.22)">Aa</text></svg>`,
  432. wave: `<svg viewBox="0 0 100 60" width="82%" height="62%"><path d="M 6 30 Q 20 8, 34 30 T 62 30 T 90 30" stroke="rgba(255,255,255,0.22)" stroke-width="1.3" fill="none"/></svg>`,
  433. orbit: `<svg viewBox="0 0 100 60" width="74%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1.1" fill="none"><ellipse cx="50" cy="30" rx="36" ry="14"/><ellipse cx="50" cy="30" rx="14" ry="22"/><circle cx="50" cy="30" r="2" fill="rgba(255,255,255,0.32)"/></g></svg>`,
  434. dots: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g fill="rgba(255,255,255,0.22)"><circle cx="14" cy="18" r="2"/><circle cx="30" cy="18" r="2"/><circle cx="46" cy="18" r="2"/><circle cx="62" cy="18" r="2"/><circle cx="78" cy="18" r="2"/><circle cx="14" cy="30" r="2"/><circle cx="30" cy="30" r="2"/><circle cx="46" cy="30" r="3"/><circle cx="62" cy="30" r="2"/><circle cx="78" cy="30" r="2"/><circle cx="14" cy="42" r="2"/><circle cx="30" cy="42" r="2"/><circle cx="46" cy="42" r="2"/><circle cx="62" cy="42" r="2"/><circle cx="78" cy="42" r="2"/></g></svg>`,
  435. arrow: `<svg viewBox="0 0 100 60" width="78%" height="52%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1.2" fill="none" stroke-linecap="square"><path d="M 14 30 L 80 30"/><path d="M 68 18 L 82 30 L 68 42"/></g></svg>`,
  436. circle: `<svg viewBox="0 0 100 60" width="62%" height="62%"><circle cx="50" cy="30" r="22" stroke="rgba(255,255,255,0.22)" stroke-width="1.2" fill="none"/></svg>`,
  437. ma: `<svg viewBox="0 0 100 60" width="72%" height="62%"><g fill="none" stroke="rgba(255,255,255,0.22)" stroke-width="0.9"><rect x="18" y="14" width="64" height="32"/></g><circle cx="50" cy="30" r="1.4" fill="rgba(255,255,255,0.32)"/></svg>`,
  438. square: `<svg viewBox="0 0 100 60" width="62%" height="62%"><rect x="30" y="10" width="40" height="40" stroke="rgba(255,255,255,0.22)" stroke-width="1.2" fill="none"/></svg>`,
  439. arc: `<svg viewBox="0 0 100 60" width="78%" height="62%"><path d="M 14 46 Q 50 6, 86 46" stroke="rgba(255,255,255,0.22)" stroke-width="1.3" fill="none"/></svg>`,
  440. minimal: `<svg viewBox="0 0 100 60" width="78%" height="32%"><line x1="18" y1="30" x2="82" y2="30" stroke="rgba(255,255,255,0.22)" stroke-width="1.2"/></svg>`,
  441. line: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="0.9" fill="none"><line x1="14" y1="16" x2="86" y2="16"/><line x1="14" y1="30" x2="86" y2="30"/><line x1="14" y1="44" x2="60" y2="44"/></g></svg>`,
  442. collage: `<svg viewBox="0 0 100 60" width="82%" height="62%"><g fill="none" stroke="rgba(255,255,255,0.22)" stroke-width="1"><rect x="8" y="8" width="24" height="18" transform="rotate(-8 20 17)"/><rect x="36" y="18" width="28" height="20" transform="rotate(5 50 28)"/><rect x="60" y="6" width="32" height="24" transform="rotate(-4 76 18)"/></g><text x="50" y="56" text-anchor="middle" font-family="Source Serif 4, serif" font-size="14" font-style="italic" fill="rgba(255,255,255,0.3)">RAY</text></svg>`,
  443. stamp: `<svg viewBox="0 0 100 60" width="70%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1.2" fill="none"><circle cx="50" cy="30" r="22"/><text x="50" y="35" text-anchor="middle" font-family="Source Serif 4" font-size="16" font-weight="500" fill="rgba(255,255,255,0.3)">S</text></g></svg>`,
  444. poster: `<svg viewBox="0 0 100 60" width="82%" height="62%"><g fill="rgba(255,255,255,0.22)"><rect x="8" y="8" width="22" height="44"/><rect x="34" y="8" width="22" height="44"/><rect x="60" y="8" width="22" height="44"/></g></svg>`,
  445. heart: `<svg viewBox="0 0 100 60" width="58%" height="58%"><path d="M 50 48 C 30 32, 18 20, 30 14 C 40 10, 50 22, 50 22 C 50 22, 60 10, 70 14 C 82 20, 70 32, 50 48 Z" fill="rgba(217,119,87,0.28)"/></svg>`,
  446. logo: `<svg viewBox="0 0 100 60" width="60%" height="60%"><circle cx="50" cy="30" r="20" stroke="rgba(255,255,255,0.22)" stroke-width="1.3" fill="none"/><circle cx="50" cy="30" r="6" fill="rgba(255,255,255,0.22)"/></svg>`,
  447. };
  448. return svgs[kind] || svgs.minimal;
  449. }
  450. // Build the wall
  451. const wallGrid = document.getElementById('wallGrid');
  452. PHILOSOPHIES.forEach((p, idx) => {
  453. const cell = document.createElement('div');
  454. cell.className = 'cell';
  455. cell.dataset.idx = idx;
  456. const row = Math.floor(idx / 5);
  457. const col = idx % 5;
  458. // precompute distance from grid center (2, 1.5)
  459. const dr = row - 1.5;
  460. const dc = col - 2;
  461. const dist = Math.sqrt(dr * dr + dc * dc);
  462. cell.dataset.dist = dist.toFixed(3);
  463. cell.innerHTML = `
  464. <div class="glyph">${makeGlyph(p.glyph)}</div>
  465. <div class="num">${String(idx + 1).padStart(2, '0')}</div>
  466. <div class="name">${p.name}</div>
  467. `;
  468. wallGrid.appendChild(cell);
  469. });
  470. const cells = Array.from(wallGrid.querySelectorAll('.cell'));
  471. const maxDist = Math.max(...cells.map(c => parseFloat(c.dataset.dist)));
  472. // ============ Timeline ============
  473. const T_TOTAL = 12.0; // seconds (flow type w)
  474. const fps = 25;
  475. const frameDur = 1 / fps;
  476. // Easing
  477. const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
  478. const expoIn = t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
  479. const cubicInOut = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2;
  480. const cubicOut = t => 1 - Math.pow(1 - t, 3);
  481. const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
  482. const clamp01 = v => clamp(v, 0, 1);
  483. const lerp = (a, b, t) => a + (b - a) * t;
  484. // Element refs
  485. const topTitle = document.getElementById('topTitle');
  486. const subCap = document.getElementById('subCaption');
  487. const wallViewport = document.getElementById('wallViewport');
  488. const wallGridEl = wallGrid;
  489. const scanLight = document.getElementById('scanLight');
  490. const fgRow = document.getElementById('fgRow');
  491. const card1 = document.getElementById('card1');
  492. const card2 = document.getElementById('card2');
  493. const card3 = document.getElementById('card3');
  494. const thumb1 = document.getElementById('thumb1');
  495. const thumb2 = document.getElementById('thumb2');
  496. const thumb3 = document.getElementById('thumb3');
  497. const brandPanel = document.getElementById('brandPanel');
  498. const brandMark = document.getElementById('brandMark');
  499. const brandUnderline = document.getElementById('brandUnderline');
  500. const brandTag = document.getElementById('brandTag');
  501. function tick(t){
  502. // Clamp
  503. t = Math.max(0, Math.min(T_TOTAL, t));
  504. // ========== Phase 1: 0 - 2.5s — Ripple in 20 cells ==========
  505. const rippleStart = 0.15;
  506. const rippleSpan = 1.8;
  507. cells.forEach(cell => {
  508. const d = parseFloat(cell.dataset.dist);
  509. // delay scaled by distance-from-center (hero v10 formula)
  510. const delay = (d / maxDist) * 0.85;
  511. const cellT = clamp01((t - rippleStart - delay * 0.55) / 0.7);
  512. const eased = expoOut(cellT);
  513. const idx = parseInt(cell.dataset.idx, 10);
  514. const isSel = SELECTED.includes(idx);
  515. cell.style.opacity = (eased * (isSel ? 1.0 : 0.85)).toFixed(3);
  516. const ty = lerp(30, 0, eased);
  517. const scale = lerp(0.88, 1, eased);
  518. cell.style.transform = `translateY(${ty}px) scale(${scale})`;
  519. });
  520. // ========== Phase 2: 2.5 - 4.0s — scan light sweeps down ==========
  521. const scanStart = 2.6;
  522. const scanEnd = 4.0;
  523. const scanT = clamp01((t - scanStart) / (scanEnd - scanStart));
  524. if (scanT > 0 && scanT < 1) {
  525. scanLight.style.opacity = Math.min(1, Math.sin(scanT * Math.PI) * 1.3).toFixed(3);
  526. // travel from top to bottom across the wall (-150 to 860px within wallViewport-ish)
  527. const py = lerp(-180, 820, cubicInOut(scanT));
  528. scanLight.style.transform = `translateY(${py}px)`;
  529. } else {
  530. scanLight.style.opacity = 0;
  531. }
  532. // ========== Phase 3: 4.0 - 4.8s — 3 cells light up, others dim ==========
  533. const lightStart = 4.0;
  534. const lightEnd = 4.8;
  535. const lightT = clamp01((t - lightStart) / (lightEnd - lightStart));
  536. const lightE = expoOut(lightT);
  537. cells.forEach(cell => {
  538. const idx = parseInt(cell.dataset.idx, 10);
  539. const isSel = SELECTED.includes(idx);
  540. if (isSel) {
  541. cell.classList.toggle('selected', lightT > 0.05);
  542. } else {
  543. // dim non-selected from 0.85 → 0.08
  544. const base = 0.85;
  545. const dimmedOpacity = lerp(base, 0.08, lightE);
  546. // only override after ripple is done
  547. if (t >= lightStart) {
  548. cell.style.opacity = dimmedOpacity.toFixed(3);
  549. }
  550. }
  551. });
  552. // ========== Phase 4: 4.8 - 6.5s — 3 cells break out to foreground ==========
  553. // We don't literally move the wall cells; we fade in fg-cards "bursting from the wall"
  554. const breakStart = 4.8;
  555. const breakEnd = 6.5;
  556. const breakT = clamp01((t - breakStart) / (breakEnd - breakStart));
  557. const breakE = expoOut(breakT);
  558. if (t >= breakStart - 0.1) {
  559. fgRow.style.opacity = 1;
  560. } else {
  561. fgRow.style.opacity = 0;
  562. }
  563. [card1, card2, card3].forEach((card, i) => {
  564. const stagger = i * 0.18; // pop × 3 staggered
  565. const cT = clamp01((t - breakStart - stagger) / 0.85);
  566. const cE = expoOut(cT);
  567. card.style.opacity = cE.toFixed(3);
  568. // Z-rush: from translateZ(-800) to 0, scale 0.4 → 1
  569. const tz = lerp(-800, 0, cE);
  570. const sc = lerp(0.45, 1, cE);
  571. const ty = lerp(40, 0, cE);
  572. card.style.transform = `translateZ(${tz}px) scale(${sc}) translateY(${ty}px)`;
  573. });
  574. // Dim the wall (behind) when cards come forward
  575. if (t >= breakStart) {
  576. const dimT = clamp01((t - breakStart) / 0.9);
  577. const dimE = expoOut(dimT);
  578. wallViewport.style.opacity = lerp(1, 0.25, dimE).toFixed(3);
  579. wallViewport.style.filter = `blur(${lerp(0, 6, dimE).toFixed(1)}px)`;
  580. } else {
  581. wallViewport.style.opacity = 1;
  582. wallViewport.style.filter = 'blur(0px)';
  583. }
  584. // ========== Phase 5: 6.5 - 9.5s — thumbnails grow below each card ==========
  585. const thumbStart = 6.6;
  586. const thumbs = [thumb1, thumb2, thumb3];
  587. thumbs.forEach((thumb, i) => {
  588. const stagger = i * 0.32;
  589. const ttT = clamp01((t - thumbStart - stagger) / 1.0);
  590. const ttE = cubicOut(ttT);
  591. thumb.style.opacity = ttE.toFixed(3);
  592. // height from 0 to 250px
  593. const h = lerp(0, 250, ttE);
  594. thumb.style.height = `${h}px`;
  595. });
  596. // ========== Top title fade in 7.2 - 8.0 ==========
  597. const titleStart = 7.2;
  598. const titleT = clamp01((t - titleStart) / 0.9);
  599. const titleE = cubicOut(titleT);
  600. topTitle.style.opacity = titleE.toFixed(3);
  601. topTitle.style.transform = `translateX(-50%) translateY(${lerp(-14, 0, titleE)}px)`;
  602. subCap.style.opacity = (titleE * 0.95).toFixed(3);
  603. // ========== Phase 6: 9.8 - 12.0s — Brand Reveal ==========
  604. const brandStart = 9.8;
  605. const panelT = clamp01((t - brandStart) / 0.7);
  606. const panelE = expoOut(panelT);
  607. brandPanel.style.opacity = panelE.toFixed(3);
  608. brandPanel.style.transform = `translateY(${lerp(100, 0, panelE)}%)`;
  609. const markStart = 10.3;
  610. const markT = clamp01((t - markStart) / 0.6);
  611. const markE = expoOut(markT);
  612. brandMark.style.opacity = markE.toFixed(3);
  613. brandMark.style.transform = `scale(${lerp(0.92, 1, markE)})`;
  614. const ulStart = 10.7;
  615. const ulT = clamp01((t - ulStart) / 0.55);
  616. brandUnderline.style.width = `${lerp(0, 280, expoOut(ulT))}px`;
  617. const tagStart = 11.1;
  618. const tagT = clamp01((t - tagStart) / 0.5);
  619. brandTag.style.opacity = cubicOut(tagT).toFixed(3);
  620. }
  621. // ============ Animation loop ============
  622. window.__ready = false;
  623. window.__duration = T_TOTAL;
  624. let startTime = null;
  625. let paused = false;
  626. const recording = window.__recording === true;
  627. function loop(now){
  628. if (paused) return;
  629. if (startTime === null) startTime = now;
  630. const t = (now - startTime) / 1000;
  631. tick(t);
  632. if (t < T_TOTAL) {
  633. requestAnimationFrame(loop);
  634. } else if (!recording) {
  635. startTime = now;
  636. requestAnimationFrame(loop);
  637. }
  638. }
  639. // First-frame sync BEFORE requesting next frame
  640. tick(0);
  641. window.__ready = true;
  642. requestAnimationFrame(loop);
  643. // Pause raf loop — tests & recorder call this before seeking
  644. window.__pause = function(){ paused = true; };
  645. window.__resume = function(){
  646. if (!paused) return;
  647. paused = false;
  648. startTime = null;
  649. requestAnimationFrame(loop);
  650. };
  651. // Expose for video recorder (scripts/render-video.js uses __setTime)
  652. window.__setTime = function(t){ paused = true; tick(t); };
  653. })();
  654. </script>
  655. </body>
  656. </html>