1
0

w3-fallback-advisor-en.html 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647
  1. <!doctype html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8" />
  5. <title>w3 · Fallback Advisor (English)</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=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-en: "Source Serif 4", Georgia, serif;
  23. --sans: "Inter", -apple-system, system-ui, sans-serif;
  24. --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
  25. }
  26. html, body {
  27. margin: 0; padding: 0;
  28. background: #000;
  29. overflow: hidden;
  30. font-family: var(--sans);
  31. color: var(--ink);
  32. -webkit-font-smoothing: antialiased;
  33. }
  34. * { box-sizing: border-box; }
  35. .stage {
  36. position: fixed;
  37. top: 50%; left: 50%;
  38. width: 1920px; height: 1080px;
  39. transform-origin: center center;
  40. background: var(--bg);
  41. overflow: hidden;
  42. }
  43. /* Watermarks */
  44. .watermark-tl {
  45. position: absolute;
  46. top: 40px; left: 56px;
  47. font-family: var(--mono);
  48. font-size: 12px;
  49. letter-spacing: 0.2em;
  50. color: rgba(255,255,255,0.16);
  51. z-index: 200;
  52. pointer-events: none;
  53. text-transform: uppercase;
  54. }
  55. .watermark-br {
  56. position: absolute;
  57. bottom: 32px; right: 40px;
  58. font-family: var(--mono);
  59. font-size: 10px;
  60. letter-spacing: 0.24em;
  61. color: rgba(255,255,255,0.14);
  62. z-index: 200;
  63. pointer-events: none;
  64. text-transform: uppercase;
  65. }
  66. /* Top title — English uses Serif Display */
  67. .top-title {
  68. position: absolute;
  69. top: 82px; left: 50%;
  70. transform: translateX(-50%);
  71. font-family: var(--serif-en);
  72. font-weight: 300;
  73. font-size: 46px;
  74. font-style: italic;
  75. letter-spacing: -0.01em;
  76. color: var(--ink-80);
  77. text-align: center;
  78. opacity: 0;
  79. will-change: opacity, transform;
  80. z-index: 120;
  81. line-height: 1.12;
  82. }
  83. .top-title .accent { color: var(--accent); font-style: italic; }
  84. .sub-caption {
  85. position: absolute;
  86. top: 148px; left: 50%;
  87. transform: translateX(-50%);
  88. font-family: var(--sans);
  89. font-weight: 300;
  90. font-size: 13px;
  91. letter-spacing: 0.34em;
  92. color: var(--muted);
  93. text-transform: uppercase;
  94. opacity: 0;
  95. will-change: opacity;
  96. z-index: 120;
  97. }
  98. /* Philosophy wall */
  99. .wall-viewport {
  100. position: absolute;
  101. top: 50%; left: 50%;
  102. transform: translate(-50%, -50%);
  103. width: 1480px;
  104. height: 760px;
  105. perspective: 2400px;
  106. perspective-origin: 50% 50%;
  107. will-change: transform, opacity, filter;
  108. }
  109. .wall-grid {
  110. position: absolute;
  111. inset: 0;
  112. display: grid;
  113. grid-template-columns: repeat(5, 1fr);
  114. grid-template-rows: repeat(4, 1fr);
  115. gap: 18px;
  116. transform: rotateX(10deg) rotateY(-6deg);
  117. transform-style: preserve-3d;
  118. will-change: transform, opacity;
  119. }
  120. .cell {
  121. position: relative;
  122. background: #0f0f0f;
  123. border: 1px solid var(--hairline);
  124. border-radius: 8px;
  125. overflow: hidden;
  126. opacity: 0;
  127. will-change: opacity, transform, filter;
  128. display: flex;
  129. flex-direction: column;
  130. justify-content: space-between;
  131. padding: 14px 16px;
  132. }
  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. .cell.selected {
  159. border-color: var(--accent);
  160. background: #1a0f0a;
  161. }
  162. .cell.selected .name { color: var(--accent); }
  163. /* Scan light */
  164. .scan-light {
  165. position: absolute;
  166. left: -5%;
  167. right: -5%;
  168. top: -15%;
  169. height: 200px;
  170. background: linear-gradient(
  171. 180deg,
  172. rgba(217, 119, 87, 0) 0%,
  173. rgba(217, 119, 87, 0.18) 40%,
  174. rgba(255, 220, 200, 0.45) 50%,
  175. rgba(217, 119, 87, 0.18) 60%,
  176. rgba(217, 119, 87, 0) 100%
  177. );
  178. filter: blur(8px);
  179. z-index: 80;
  180. opacity: 0;
  181. will-change: opacity, transform;
  182. pointer-events: none;
  183. }
  184. /* Foreground 3 cards */
  185. .fg-row {
  186. position: absolute;
  187. top: 50%; left: 50%;
  188. transform: translate(-50%, -50%);
  189. display: flex;
  190. gap: 56px;
  191. opacity: 0;
  192. will-change: opacity;
  193. z-index: 100;
  194. }
  195. .fg-card {
  196. width: 440px;
  197. display: flex;
  198. flex-direction: column;
  199. opacity: 0;
  200. transform: translateZ(-800px) scale(0.4);
  201. will-change: opacity, transform;
  202. }
  203. .fg-card .card-body {
  204. background: #0f0f0f;
  205. border: 1px solid var(--accent);
  206. border-radius: 12px;
  207. padding: 32px 30px;
  208. box-shadow:
  209. 0 30px 80px -20px rgba(217,119,87,0.25),
  210. 0 10px 30px -10px rgba(0,0,0,0.6);
  211. }
  212. .fg-card .label {
  213. font-family: var(--mono);
  214. font-size: 11px;
  215. letter-spacing: 0.18em;
  216. color: var(--accent);
  217. text-transform: uppercase;
  218. margin-bottom: 14px;
  219. }
  220. .fg-card .title-main {
  221. font-family: var(--serif-en);
  222. font-style: italic;
  223. font-size: 40px;
  224. font-weight: 300;
  225. letter-spacing: -0.01em;
  226. line-height: 1.08;
  227. color: var(--ink);
  228. margin-bottom: 10px;
  229. }
  230. .fg-card .title-sub {
  231. font-family: var(--sans);
  232. font-weight: 300;
  233. font-size: 14px;
  234. letter-spacing: 0.14em;
  235. text-transform: uppercase;
  236. color: var(--ink-60);
  237. margin-bottom: 22px;
  238. }
  239. .fg-card .feature {
  240. font-family: var(--sans);
  241. font-size: 13px;
  242. font-weight: 300;
  243. letter-spacing: 0.03em;
  244. color: var(--muted);
  245. line-height: 1.6;
  246. padding-top: 18px;
  247. border-top: 1px solid var(--hairline);
  248. text-transform: uppercase;
  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 .dot { color: var(--accent); font-style: normal; padding: 0 6px; }
  291. .brand-mark .accent { color: var(--accent); font-style: italic; }
  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. <div class="watermark-tl">HUASHU · DESIGN</div>
  314. <div class="watermark-br">V2 · 2026 · w3</div>
  315. <!-- English version: parallel rewrite, fewer words, more breathing room -->
  316. <div class="top-title" id="topTitle">
  317. Not sure? <span class="accent">Here are 3 roads.</span>
  318. </div>
  319. <div class="sub-caption" id="subCaption">20 Philosophies · 3 Directions</div>
  320. <div class="scan-light" id="scanLight"></div>
  321. <div class="wall-viewport" id="wallViewport">
  322. <div class="wall-grid" id="wallGrid">
  323. <!-- 20 cells injected by JS -->
  324. </div>
  325. </div>
  326. <div class="fg-row" id="fgRow">
  327. <div class="fg-card" id="card1">
  328. <div class="card-body">
  329. <div class="label">Road 01 · Eastern Space</div>
  330. <div class="title-main">Kenya Hara</div>
  331. <div class="title-sub">Ma / Emptiness</div>
  332. <div class="feature">Terracotta · Vast whitespace · Paper grain</div>
  333. </div>
  334. <div class="thumb-wrap" id="thumb1">
  335. <img src="demo-takram.png" alt="demo takram" />
  336. </div>
  337. </div>
  338. <div class="fg-card" id="card2">
  339. <div class="card-body">
  340. <div class="label">Road 02 · Information Architecture</div>
  341. <div class="title-main">Pentagram</div>
  342. <div class="title-sub">Grid / Rigor</div>
  343. <div class="feature">Strict grid · High contrast · Editorial</div>
  344. </div>
  345. <div class="thumb-wrap" id="thumb2">
  346. <img src="demo-pentagram.png" alt="demo pentagram" />
  347. </div>
  348. </div>
  349. <div class="fg-card" id="card3">
  350. <div class="card-body">
  351. <div class="label">Road 03 · Experimental Edge</div>
  352. <div class="title-main">David Carson</div>
  353. <div class="title-sub">Raw / Punk</div>
  354. <div class="feature">Broken type · Brutal geometry · Visual shock</div>
  355. </div>
  356. <div class="thumb-wrap" id="thumb3">
  357. <img src="demo-build.png" alt="demo build" />
  358. </div>
  359. </div>
  360. </div>
  361. <div class="brand-panel" id="brandPanel">
  362. <div class="brand-mark" id="brandMark">huashu<span class="dot">·</span><span class="accent">design</span></div>
  363. <div class="brand-underline" id="brandUnderline"></div>
  364. <div class="brand-tag" id="brandTag">HTML as Designer's Medium</div>
  365. </div>
  366. </div>
  367. <script>
  368. (function(){
  369. function scaleStage(){
  370. const stage = document.getElementById('stage');
  371. const sx = window.innerWidth / 1920;
  372. const sy = window.innerHeight / 1080;
  373. const s = Math.min(sx, sy);
  374. stage.style.transform = `translate(-50%, -50%) scale(${s})`;
  375. }
  376. window.addEventListener('resize', scaleStage);
  377. scaleStage();
  378. // 20 philosophies — identical structure to zh.html (designer names are brand identifiers, kept as-is)
  379. const PHILOSOPHIES = [
  380. { name: 'Pentagram', glyph: 'grid' },
  381. { name: 'M. Vignelli', glyph: 'bars' },
  382. { name: 'Apple HIG', glyph: 'radius' },
  383. { name: 'Spin', glyph: 'slash' },
  384. { name: 'Build', glyph: 'type' },
  385. { name: 'Field.io', glyph: 'wave' },
  386. { name: 'Active Theory',glyph: 'orbit' },
  387. { name: 'Hi-Res!', glyph: 'dots' },
  388. { name: 'Locomotive', glyph: 'arrow' },
  389. { name: 'Takram', glyph: 'circle' },
  390. { name: 'Kenya Hara', glyph: 'ma' },
  391. { name: 'D. Rams', glyph: 'square' },
  392. { name: 'J. Ive', glyph: 'arc' },
  393. { name: 'J. Morrison', glyph: 'minimal' },
  394. { name: 'S. Ogata', glyph: 'line' },
  395. { name: 'D. Carson', glyph: 'collage' },
  396. { name: 'S. Sagmeister',glyph: 'stamp' },
  397. { name: 'P. Scher', glyph: 'poster' },
  398. { name: 'M. Glaser', glyph: 'heart' },
  399. { name: 'K. Sato', glyph: 'logo' },
  400. ];
  401. const SELECTED = [10, 0, 15];
  402. function makeGlyph(kind){
  403. const svgs = {
  404. grid: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1" fill="none">
  405. <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"/>
  406. <rect x="6" y="30" width="60" height="22"/></g></svg>`,
  407. bars: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g fill="rgba(255,255,255,0.22)">
  408. <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"/>
  409. <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"/>
  410. <rect x="82" y="22" width="8" height="34"/></g></svg>`,
  411. 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">
  412. <rect x="14" y="10" width="72" height="40" rx="20" ry="20"/></g></svg>`,
  413. 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">
  414. <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>`,
  415. 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>`,
  416. 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>`,
  417. 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>`,
  418. 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>`,
  419. 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>`,
  420. 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>`,
  421. 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>`,
  422. 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>`,
  423. 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>`,
  424. 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>`,
  425. 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>`,
  426. 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>`,
  427. 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>`,
  428. 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>`,
  429. 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>`,
  430. 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>`,
  431. };
  432. return svgs[kind] || svgs.minimal;
  433. }
  434. const wallGrid = document.getElementById('wallGrid');
  435. PHILOSOPHIES.forEach((p, idx) => {
  436. const cell = document.createElement('div');
  437. cell.className = 'cell';
  438. cell.dataset.idx = idx;
  439. const row = Math.floor(idx / 5);
  440. const col = idx % 5;
  441. const dr = row - 1.5;
  442. const dc = col - 2;
  443. const dist = Math.sqrt(dr * dr + dc * dc);
  444. cell.dataset.dist = dist.toFixed(3);
  445. cell.innerHTML = `
  446. <div class="glyph">${makeGlyph(p.glyph)}</div>
  447. <div class="num">${String(idx + 1).padStart(2, '0')}</div>
  448. <div class="name">${p.name}</div>
  449. `;
  450. wallGrid.appendChild(cell);
  451. });
  452. const cells = Array.from(wallGrid.querySelectorAll('.cell'));
  453. const maxDist = Math.max(...cells.map(c => parseFloat(c.dataset.dist)));
  454. const T_TOTAL = 12.0;
  455. const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
  456. const cubicInOut = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2;
  457. const cubicOut = t => 1 - Math.pow(1 - t, 3);
  458. const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
  459. const clamp01 = v => clamp(v, 0, 1);
  460. const lerp = (a, b, t) => a + (b - a) * t;
  461. const topTitle = document.getElementById('topTitle');
  462. const subCap = document.getElementById('subCaption');
  463. const wallViewport = document.getElementById('wallViewport');
  464. const scanLight = document.getElementById('scanLight');
  465. const fgRow = document.getElementById('fgRow');
  466. const card1 = document.getElementById('card1');
  467. const card2 = document.getElementById('card2');
  468. const card3 = document.getElementById('card3');
  469. const thumb1 = document.getElementById('thumb1');
  470. const thumb2 = document.getElementById('thumb2');
  471. const thumb3 = document.getElementById('thumb3');
  472. const brandPanel = document.getElementById('brandPanel');
  473. const brandMark = document.getElementById('brandMark');
  474. const brandUnderline = document.getElementById('brandUnderline');
  475. const brandTag = document.getElementById('brandTag');
  476. function tick(t){
  477. t = Math.max(0, Math.min(T_TOTAL, t));
  478. // Ripple in 20 cells
  479. const rippleStart = 0.15;
  480. cells.forEach(cell => {
  481. const d = parseFloat(cell.dataset.dist);
  482. const delay = (d / maxDist) * 0.85;
  483. const cellT = clamp01((t - rippleStart - delay * 0.55) / 0.7);
  484. const eased = expoOut(cellT);
  485. const idx = parseInt(cell.dataset.idx, 10);
  486. const isSel = SELECTED.includes(idx);
  487. cell.style.opacity = (eased * (isSel ? 1.0 : 0.85)).toFixed(3);
  488. const ty = lerp(30, 0, eased);
  489. const scale = lerp(0.88, 1, eased);
  490. cell.style.transform = `translateY(${ty}px) scale(${scale})`;
  491. });
  492. // Scan light
  493. const scanStart = 2.6;
  494. const scanEnd = 4.0;
  495. const scanT = clamp01((t - scanStart) / (scanEnd - scanStart));
  496. if (scanT > 0 && scanT < 1) {
  497. scanLight.style.opacity = Math.min(1, Math.sin(scanT * Math.PI) * 1.3).toFixed(3);
  498. const py = lerp(-180, 820, cubicInOut(scanT));
  499. scanLight.style.transform = `translateY(${py}px)`;
  500. } else {
  501. scanLight.style.opacity = 0;
  502. }
  503. // Light up selected, dim others
  504. const lightStart = 4.0;
  505. const lightEnd = 4.8;
  506. const lightT = clamp01((t - lightStart) / (lightEnd - lightStart));
  507. const lightE = expoOut(lightT);
  508. cells.forEach(cell => {
  509. const idx = parseInt(cell.dataset.idx, 10);
  510. const isSel = SELECTED.includes(idx);
  511. if (isSel) {
  512. cell.classList.toggle('selected', lightT > 0.05);
  513. } else {
  514. if (t >= lightStart) {
  515. const dimmedOpacity = lerp(0.85, 0.08, lightE);
  516. cell.style.opacity = dimmedOpacity.toFixed(3);
  517. }
  518. }
  519. });
  520. // Foreground cards break out
  521. const breakStart = 4.8;
  522. if (t >= breakStart - 0.1) fgRow.style.opacity = 1;
  523. else fgRow.style.opacity = 0;
  524. [card1, card2, card3].forEach((card, i) => {
  525. const stagger = i * 0.18;
  526. const cT = clamp01((t - breakStart - stagger) / 0.85);
  527. const cE = expoOut(cT);
  528. card.style.opacity = cE.toFixed(3);
  529. const tz = lerp(-800, 0, cE);
  530. const sc = lerp(0.45, 1, cE);
  531. const ty = lerp(40, 0, cE);
  532. card.style.transform = `translateZ(${tz}px) scale(${sc}) translateY(${ty}px)`;
  533. });
  534. // Dim wall background
  535. if (t >= breakStart) {
  536. const dimT = clamp01((t - breakStart) / 0.9);
  537. const dimE = expoOut(dimT);
  538. wallViewport.style.opacity = lerp(1, 0.25, dimE).toFixed(3);
  539. wallViewport.style.filter = `blur(${lerp(0, 6, dimE).toFixed(1)}px)`;
  540. } else {
  541. wallViewport.style.opacity = 1;
  542. wallViewport.style.filter = 'blur(0px)';
  543. }
  544. // Demo thumbnails grow
  545. const thumbStart = 6.6;
  546. [thumb1, thumb2, thumb3].forEach((thumb, i) => {
  547. const stagger = i * 0.32;
  548. const ttT = clamp01((t - thumbStart - stagger) / 1.0);
  549. const ttE = cubicOut(ttT);
  550. thumb.style.opacity = ttE.toFixed(3);
  551. const h = lerp(0, 250, ttE);
  552. thumb.style.height = `${h}px`;
  553. });
  554. // Top title fade
  555. const titleStart = 7.2;
  556. const titleT = clamp01((t - titleStart) / 0.9);
  557. const titleE = cubicOut(titleT);
  558. topTitle.style.opacity = titleE.toFixed(3);
  559. topTitle.style.transform = `translateX(-50%) translateY(${lerp(-14, 0, titleE)}px)`;
  560. subCap.style.opacity = (titleE * 0.95).toFixed(3);
  561. // Brand reveal
  562. const brandStart = 9.8;
  563. const panelT = clamp01((t - brandStart) / 0.7);
  564. const panelE = expoOut(panelT);
  565. brandPanel.style.opacity = panelE.toFixed(3);
  566. brandPanel.style.transform = `translateY(${lerp(100, 0, panelE)}%)`;
  567. const markStart = 10.3;
  568. const markT = clamp01((t - markStart) / 0.6);
  569. const markE = expoOut(markT);
  570. brandMark.style.opacity = markE.toFixed(3);
  571. brandMark.style.transform = `scale(${lerp(0.92, 1, markE)})`;
  572. const ulStart = 10.7;
  573. const ulT = clamp01((t - ulStart) / 0.55);
  574. brandUnderline.style.width = `${lerp(0, 280, expoOut(ulT))}px`;
  575. const tagStart = 11.1;
  576. const tagT = clamp01((t - tagStart) / 0.5);
  577. brandTag.style.opacity = cubicOut(tagT).toFixed(3);
  578. }
  579. window.__ready = false;
  580. window.__duration = T_TOTAL;
  581. let startTime = null;
  582. let paused = false;
  583. const recording = window.__recording === true;
  584. function loop(now){
  585. if (paused) return;
  586. if (startTime === null) startTime = now;
  587. const t = (now - startTime) / 1000;
  588. tick(t);
  589. if (t < T_TOTAL) requestAnimationFrame(loop);
  590. else if (!recording) { startTime = now; requestAnimationFrame(loop); }
  591. }
  592. tick(0);
  593. window.__ready = true;
  594. requestAnimationFrame(loop);
  595. window.__pause = function(){ paused = true; };
  596. window.__resume = function(){
  597. if (!paused) return;
  598. paused = false; startTime = null;
  599. requestAnimationFrame(loop);
  600. };
  601. window.__setTime = function(t){ paused = true; tick(t); };
  602. })();
  603. </script>
  604. </body>
  605. </html>