w3-fallback-advisor.html 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Huashu-Design · Fallback 设计顾问</title>
  6. <script crossorigin src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
  7. <script crossorigin src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
  8. <script src="https://unpkg.com/@babel/standalone@7.25.6/babel.min.js"></script>
  9. <link rel="preconnect" href="https://fonts.googleapis.com">
  10. <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  11. <link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,300;1,6..72,400;1,6..72,500&family=Noto+Serif+SC:wght@300;400;500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
  12. <style>
  13. * { box-sizing: border-box; margin: 0; padding: 0; }
  14. html, body { width: 100%; height: 100%; overflow: hidden; }
  15. body {
  16. background: #0c0c0c;
  17. font-family: 'Newsreader', 'Noto Serif SC', Georgia, serif;
  18. color: #1a1a1a;
  19. -webkit-font-smoothing: antialiased;
  20. text-rendering: optimizeLegibility;
  21. }
  22. </style>
  23. </head>
  24. <body>
  25. <div id="root"></div>
  26. <!-- animations.jsx inlined -->
  27. <script type="text/babel">
  28. (function() {
  29. const { createContext, useContext, useState, useEffect, useRef, useCallback } = React;
  30. const TimeContext = createContext({ time: 0, duration: 10, playing: false });
  31. const SpriteContext = createContext(null);
  32. const Easing = {
  33. linear: t => t,
  34. easeIn: t => t * t,
  35. easeOut: t => 1 - (1 - t) * (1 - t),
  36. easeInOut: t => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
  37. spring: t => {
  38. const c = (2 * Math.PI) / 3;
  39. return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c) + 1;
  40. },
  41. };
  42. function interpolate(t, input, output, easing) {
  43. const [inStart, inEnd] = input;
  44. const [outStart, outEnd] = output;
  45. if (t <= inStart) return outStart;
  46. if (t >= inEnd) return outEnd;
  47. let progress = (t - inStart) / (inEnd - inStart);
  48. if (easing) progress = easing(progress);
  49. return outStart + (outEnd - outStart) * progress;
  50. }
  51. function useTime() { return useContext(TimeContext).time; }
  52. function useSprite() {
  53. const sprite = useContext(SpriteContext);
  54. return sprite || { t: 0, elapsed: 0, duration: 0 };
  55. }
  56. function Stage({ duration = 10, width = 1920, height = 1080, loop = true, children, bgColor = '#fff' }) {
  57. const [time, setTime] = useState(0);
  58. const [playing, setPlaying] = useState(true);
  59. const [scale, setScale] = useState(1);
  60. const rafRef = useRef(null);
  61. const effectiveLoop = (typeof window !== 'undefined' && window.__recording) ? false : loop;
  62. useEffect(() => {
  63. function updateScale() {
  64. const vw = window.innerWidth;
  65. const vh = window.innerHeight - 56;
  66. const s = Math.min(vw / width, vh / height);
  67. setScale(s);
  68. }
  69. updateScale();
  70. window.addEventListener('resize', updateScale);
  71. return () => window.removeEventListener('resize', updateScale);
  72. }, [width, height]);
  73. useEffect(() => {
  74. if (!playing) return;
  75. let cancelled = false;
  76. let last = null;
  77. function tick(now) {
  78. if (cancelled) return;
  79. if (last === null) {
  80. last = now;
  81. if (typeof window !== 'undefined') window.__ready = true;
  82. }
  83. const delta = (now - last) / 1000;
  84. last = now;
  85. setTime(prev => {
  86. const next = prev + delta;
  87. if (next >= duration) return effectiveLoop ? 0 : duration - 0.001;
  88. return next;
  89. });
  90. rafRef.current = requestAnimationFrame(tick);
  91. }
  92. const start = () => { if (!cancelled) rafRef.current = requestAnimationFrame(tick); };
  93. if (document.fonts && document.fonts.ready) document.fonts.ready.then(start); else start();
  94. return () => { cancelled = true; cancelAnimationFrame(rafRef.current); };
  95. }, [playing, duration, effectiveLoop]);
  96. const progress = time / duration;
  97. const ctx = { time, duration, playing, setPlaying, setTime };
  98. const canvasStyle = {
  99. position: 'absolute',
  100. top: '50%',
  101. left: '50%',
  102. transformOrigin: 'center center',
  103. width,
  104. height,
  105. background: bgColor,
  106. overflow: 'hidden',
  107. transform: `translate(-50%, -50%) scale(${scale})`,
  108. };
  109. return (
  110. <TimeContext.Provider value={ctx}>
  111. <div style={{position:'fixed', inset:0, background:'#0c0c0c', display:'flex', flexDirection:'column'}}>
  112. <div style={{flex:1, position:'relative', overflow:'hidden'}}>
  113. <div style={canvasStyle}>{children}</div>
  114. </div>
  115. <div className="no-record" style={{position:'fixed', bottom:0, left:0, right:0, background:'rgba(0,0,0,0.8)', backdropFilter:'blur(10px)', padding:'12px 20px', display:'flex', alignItems:'center', gap:16, color:'#fff', fontSize:12, zIndex:100}}>
  116. <button onClick={()=>setPlaying(p=>!p)} style={{background:'none', border:'1px solid rgba(255,255,255,0.3)', color:'#fff', padding:'6px 14px', borderRadius:4, cursor:'pointer', fontSize:12}}>{playing?'⏸ 暂停':'▶ 播放'}</button>
  117. <button onClick={()=>setTime(0)} style={{background:'none', border:'1px solid rgba(255,255,255,0.3)', color:'#fff', padding:'6px 14px', borderRadius:4, cursor:'pointer', fontSize:12}}>⏮ 开始</button>
  118. <div style={{fontFamily:'ui-monospace, monospace', fontVariantNumeric:'tabular-nums', minWidth:90}}>{time.toFixed(2)}s / {duration.toFixed(2)}s</div>
  119. <div style={{flex:1, height:4, background:'rgba(255,255,255,0.2)', borderRadius:2, position:'relative'}}>
  120. <div style={{position:'absolute', top:0, left:0, height:'100%', width:`${progress*100}%`, background:'#fff', borderRadius:2}} />
  121. </div>
  122. </div>
  123. </div>
  124. </TimeContext.Provider>
  125. );
  126. }
  127. function Sprite({ start = 0, end, children, style }) {
  128. const { time } = useContext(TimeContext);
  129. const actualEnd = end == null ? Infinity : end;
  130. if (time < start || time >= actualEnd) return null;
  131. const duration = actualEnd - start;
  132. const elapsed = time - start;
  133. const t = duration === 0 ? 1 : Math.max(0, Math.min(1, elapsed / duration));
  134. const spriteValue = { t, elapsed, duration, start, end: actualEnd };
  135. return (
  136. <SpriteContext.Provider value={spriteValue}>
  137. <div style={{position:'absolute', inset:0, ...style}}>{children}</div>
  138. </SpriteContext.Provider>
  139. );
  140. }
  141. window.Animations = { Stage, Sprite, useTime, useSprite, Easing, interpolate };
  142. })();
  143. </script>
  144. <!-- Demo scene -->
  145. <script type="text/babel">
  146. const { Stage, Sprite, useTime, useSprite, Easing, interpolate } = window.Animations;
  147. // ── Design tokens ─────────────────────────────────────────
  148. const CREAM = '#FAF6EF';
  149. const INK = '#1a1a1a';
  150. const TERRA = '#C04A1A';
  151. const ASH = '#6b6b6b';
  152. const LINE = '#d9d2c5';
  153. // ── 20 design philosophies ────────────────────────────────
  154. const PHILOSOPHIES = [
  155. { n: 'Pentagram', school: '信息建筑', en: 'INFO-ARCH' },
  156. { n: 'Massimo Vignelli', school: '信息建筑', en: 'INFO-ARCH' },
  157. { n: 'Dieter Rams', school: '信息建筑', en: 'INFO-ARCH' },
  158. { n: 'Otl Aicher', school: '信息建筑', en: 'INFO-ARCH' },
  159. { n: 'Field.io', school: '运动诗学', en: 'KINETIC' },
  160. { n: 'Active Theory', school: '运动诗学', en: 'KINETIC' },
  161. { n: 'Locomotive', school: '运动诗学', en: 'KINETIC' },
  162. { n: 'Joshua Davis', school: '运动诗学', en: 'KINETIC' },
  163. { n: 'Kenya Hara', school: '东方哲学', en: 'EASTERN' },
  164. { n: 'Naoto Fukasawa', school: '东方哲学', en: 'EASTERN' },
  165. { n: 'Kashiwa Sato', school: '东方哲学', en: 'EASTERN' },
  166. { n: 'John Maeda', school: '东方哲学', en: 'EASTERN' },
  167. { n: 'Sagmeister', school: '实验先锋', en: 'AVANT' },
  168. { n: 'David Carson', school: '实验先锋', en: 'AVANT' },
  169. { n: 'Paula Scher', school: '实验先锋', en: 'AVANT' },
  170. { n: 'Tomato', school: '实验先锋', en: 'AVANT' },
  171. { n: 'Dan Flavin', school: '极简主义', en: 'MINIMAL' },
  172. { n: 'Ryuichi Sakamoto', school: '极简主义', en: 'MINIMAL' },
  173. { n: 'Agnes Martin', school: '极简主义', en: 'MINIMAL' },
  174. { n: 'Donald Judd', school: '极简主义', en: 'MINIMAL' },
  175. ];
  176. const SELECTED_INDICES = [0, 4, 8]; // Pentagram, Field.io, Kenya Hara
  177. // ── Shared typography helpers ─────────────────────────────
  178. const serif = "'Newsreader', 'Noto Serif SC', Georgia, serif";
  179. const sans = "'Inter', -apple-system, sans-serif";
  180. const mono = "'JetBrains Mono', ui-monospace, monospace";
  181. // ── Scene 1: Vague brief (0 – 3.5s) ───────────────────────
  182. function Scene1_VagueBrief() {
  183. const { t, elapsed } = useSprite();
  184. const charCount = Math.floor(interpolate(elapsed, [0.3, 1.8], [0, 9]));
  185. const text = '做个好看的页面'.slice(0, charCount);
  186. const cursorBlink = Math.floor(elapsed * 2.4) % 2 === 0;
  187. const questionOpacity = interpolate(elapsed, [1.8, 2.4], [0, 1]);
  188. const questionBob = Math.sin(elapsed * 4) * 6;
  189. const fadeOut = interpolate(elapsed, [2.8, 3.5], [1, 0], Easing.easeIn);
  190. return (
  191. <div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeOut,
  192. display:'flex', alignItems:'center', justifyContent:'center', flexDirection:'column'}}>
  193. <div style={{fontFamily: sans, fontSize:14, letterSpacing:'0.3em',
  194. color: ASH, marginBottom: 40}}>
  195. 用户需求
  196. </div>
  197. <div style={{display:'flex', alignItems:'flex-start', gap: 36}}>
  198. <div style={{fontFamily: serif, fontStyle: 'italic', fontSize: 120,
  199. color: TERRA, lineHeight: 1, marginTop: -20}}>「</div>
  200. <div style={{fontFamily: serif, fontSize: 96, fontWeight: 400,
  201. color: INK, letterSpacing: '0.02em', position: 'relative'}}>
  202. {text}
  203. <span style={{opacity: cursorBlink ? 1 : 0, color: TERRA,
  204. marginLeft: 4, fontWeight: 300}}>|</span>
  205. </div>
  206. <div style={{fontFamily: serif, fontStyle: 'italic', fontSize: 120,
  207. color: TERRA, lineHeight: 1, marginTop: -20}}>」</div>
  208. </div>
  209. <div style={{fontFamily: sans, fontSize: 20, color: ASH, marginTop: 60,
  210. opacity: questionOpacity, transform: `translateY(${questionBob}px)`,
  211. letterSpacing: '0.05em'}}>
  212. <span style={{color: TERRA, fontSize: 28, marginRight: 12}}>?</span>
  213. 风格、受众、情感基调—— 都没说
  214. </div>
  215. </div>
  216. );
  217. }
  218. // ── Scene 2: Advisor activates (3.5 – 6.5s) ───────────────
  219. function Scene2_AdvisorIntro() {
  220. const { elapsed } = useSprite();
  221. const mainY = interpolate(elapsed, [0, 1.2], [40, 0], Easing.easeOut);
  222. const mainOpacity = interpolate(elapsed, [0, 0.8], [0, 1]);
  223. const lineWidth = interpolate(elapsed, [0.8, 1.8], [0, 320]);
  224. const subOpacity = interpolate(elapsed, [1.2, 2], [0, 1]);
  225. const fadeOut = interpolate(elapsed, [2.5, 3], [1, 0]);
  226. return (
  227. <div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeOut,
  228. display:'flex', alignItems:'center', justifyContent:'center', flexDirection:'column'}}>
  229. <div style={{fontFamily: sans, fontSize: 12, letterSpacing: '0.4em',
  230. color: TERRA, marginBottom: 24, opacity: mainOpacity}}>
  231. 设计方向顾问 · Fallback
  232. </div>
  233. <div style={{fontFamily: serif, fontSize: 132, fontWeight: 500,
  234. color: INK, lineHeight: 1, letterSpacing: '-0.01em',
  235. opacity: mainOpacity, transform: `translateY(${mainY}px)`}}>
  236. 推荐 <span style={{fontStyle:'italic', color: TERRA}}>3</span> 个方向
  237. </div>
  238. <div style={{height: 1, background: INK, width: lineWidth, marginTop: 36}} />
  239. <div style={{fontFamily: serif, fontStyle: 'italic', fontSize: 26,
  240. color: ASH, marginTop: 28, opacity: subOpacity}}>
  241. 从 20 种设计哲学里,按 5 个不同流派差异化推荐
  242. </div>
  243. </div>
  244. );
  245. }
  246. // ── Scene 3: 20 philosophies grid scan (6.5 – 10.5s) ──────
  247. function Scene3_GridScan() {
  248. const { elapsed } = useSprite();
  249. const titleOp = interpolate(elapsed, [0, 0.4], [0, 1]);
  250. return (
  251. <div style={{position:'absolute', inset:0, background:CREAM,
  252. padding: '80px 120px', display:'flex', flexDirection:'column'}}>
  253. <div style={{display:'flex', justifyContent:'space-between',
  254. alignItems:'baseline', opacity: titleOp, marginBottom: 50}}>
  255. <div style={{fontFamily: serif, fontSize: 52, fontWeight: 500, color: INK}}>
  256. 设计哲学库
  257. </div>
  258. <div style={{fontFamily: mono, fontSize: 14, color: ASH, letterSpacing:'0.1em'}}>
  259. 20 位设计师 · 5 个流派
  260. </div>
  261. </div>
  262. <div style={{display:'grid', gridTemplateColumns:'repeat(5, 1fr)', gap: 20, flex: 1}}>
  263. {PHILOSOPHIES.map((p, i) => {
  264. const stagger = i * 0.06;
  265. const appearT = Math.max(0, Math.min(1, (elapsed - 0.5 - stagger) / 0.4));
  266. const op = appearT;
  267. const ty = (1 - appearT) * 24;
  268. // Scanner highlight: sweeps through 20 cards from t=2.2 to t=3.2
  269. const scannerStart = 2.2 + i * 0.04;
  270. const scannerEnd = scannerStart + 0.25;
  271. const scanHighlight = elapsed > scannerStart && elapsed < scannerEnd ? 1 : 0;
  272. // Selected cards get circled at t=3.3+
  273. const isSelected = SELECTED_INDICES.includes(i);
  274. const selectT = Math.max(0, Math.min(1, (elapsed - 3.3) / 0.5));
  275. const selectOp = isSelected ? selectT : 0;
  276. const selectDim = !isSelected && elapsed > 3.4 ? interpolate(elapsed, [3.4, 3.8], [1, 0.28]) : 1;
  277. return (
  278. <div key={i} style={{
  279. opacity: op * selectDim,
  280. transform: `translateY(${ty}px)`,
  281. background: scanHighlight ? '#fff' : 'transparent',
  282. border: `1px solid ${isSelected && selectT > 0.3 ? TERRA : LINE}`,
  283. borderWidth: isSelected && selectT > 0.3 ? 2 : 1,
  284. padding: '20px 18px',
  285. position: 'relative',
  286. transition: 'none',
  287. }}>
  288. <div style={{fontFamily: mono, fontSize: 10, color: ASH,
  289. letterSpacing: '0.15em', marginBottom: 10}}>
  290. {String(i+1).padStart(2,'0')} · {p.en}
  291. </div>
  292. <div style={{fontFamily: serif, fontSize: 22, fontWeight: 500,
  293. color: INK, lineHeight: 1.15, marginBottom: 6}}>
  294. {p.n}
  295. </div>
  296. <div style={{fontFamily: serif, fontStyle: 'italic', fontSize: 14,
  297. color: ASH}}>
  298. {p.school}
  299. </div>
  300. {isSelected && selectT > 0.4 && (
  301. <div style={{position:'absolute', top: -10, right: -10,
  302. width: 26, height: 26, borderRadius: '50%', background: TERRA,
  303. color: '#fff', display:'flex', alignItems:'center',
  304. justifyContent:'center', fontFamily: serif, fontSize: 14,
  305. fontWeight: 600, opacity: selectOp}}>
  306. {SELECTED_INDICES.indexOf(i) + 1}
  307. </div>
  308. )}
  309. </div>
  310. );
  311. })}
  312. </div>
  313. </div>
  314. );
  315. }
  316. // ── Scene 4: Three-panel parallel demo generation (10.5 – 19s) ──
  317. function Scene4_ParallelDemos() {
  318. const { elapsed } = useSprite();
  319. const slideIn = interpolate(elapsed, [0, 1], [200, 0], Easing.easeOut);
  320. const opacity = interpolate(elapsed, [0, 0.6], [0, 1]);
  321. const panels = [
  322. { name: 'Pentagram', school: '信息建筑派', en: 'Information Architecture',
  323. delay: 0, render: 'pentagram' },
  324. { name: 'Field.io', school: '运动诗学派', en: 'Kinetic Poetry',
  325. delay: 0.3, render: 'field' },
  326. { name: 'Kenya Hara', school: '东方哲学派', en: 'Eastern Minimalism',
  327. delay: 0.6, render: 'hara' },
  328. ];
  329. return (
  330. <div style={{position:'absolute', inset:0, background:CREAM,
  331. padding: '60px 60px 40px', display:'flex', flexDirection:'column',
  332. opacity}}>
  333. <div style={{display:'flex', justifyContent:'space-between',
  334. alignItems:'baseline', marginBottom: 28}}>
  335. <div>
  336. <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
  337. letterSpacing: '0.3em', marginBottom: 4}}>步骤 3 / 4</div>
  338. <div style={{fontFamily: serif, fontSize: 44, fontWeight: 500, color: INK}}>
  339. 并行生成视觉 Demo
  340. </div>
  341. </div>
  342. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18,
  343. color: ASH, textAlign: 'right'}}>
  344. "看到比说到更有效"<br/>
  345. <span style={{fontSize: 14}}>— 设计顾问模式 · Phase 5</span>
  346. </div>
  347. </div>
  348. <div style={{display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap: 24,
  349. flex: 1, transform: `translateY(${slideIn}px)`}}>
  350. {panels.map((p, i) => (
  351. <DemoPanel key={i} panel={p} localElapsed={elapsed - p.delay} />
  352. ))}
  353. </div>
  354. </div>
  355. );
  356. }
  357. function DemoPanel({ panel, localElapsed }) {
  358. const progressT = Math.max(0, Math.min(1, localElapsed / 3.0));
  359. const progressPct = progressT * 100;
  360. const done = progressT >= 0.92;
  361. // Content fades in during the last 0.7s of generation — overlaps with
  362. // skeleton fade-out so there's no empty-canvas gap when "READY" appears.
  363. const contentReveal = interpolate(localElapsed, [2.4, 3.2], [0, 1], Easing.easeOut);
  364. const skeletonOp = interpolate(localElapsed, [2.4, 3.2], [1, 0], Easing.easeOut);
  365. return (
  366. <div style={{
  367. background:'#fff',
  368. border: `1px solid ${LINE}`,
  369. display:'flex', flexDirection:'column',
  370. position:'relative',
  371. }}>
  372. {/* Header */}
  373. <div style={{padding: '18px 22px', borderBottom: `1px solid ${LINE}`,
  374. display:'flex', justifyContent:'space-between', alignItems:'baseline'}}>
  375. <div>
  376. <div style={{fontFamily: serif, fontSize: 28, fontWeight: 500, color: INK}}>
  377. {panel.name}
  378. </div>
  379. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 13,
  380. color: ASH, marginTop: 2}}>
  381. {panel.en}
  382. </div>
  383. </div>
  384. <div style={{fontFamily: mono, fontSize: 10, color: done ? TERRA : ASH,
  385. letterSpacing: '0.15em'}}>
  386. {done ? '✓ READY' : 'GENERATING'}
  387. </div>
  388. </div>
  389. {/* Canvas */}
  390. <div style={{flex: 1, position: 'relative', overflow: 'hidden'}}>
  391. {skeletonOp > 0.02 && (
  392. <div style={{position:'absolute', inset:0, opacity: skeletonOp}}>
  393. <GenerationSkeleton progress={progressT} />
  394. </div>
  395. )}
  396. <div style={{position:'absolute', inset:0, opacity: contentReveal}}>
  397. {panel.render === 'pentagram' && <PentagramDemo />}
  398. {panel.render === 'field' && <FieldDemo elapsed={localElapsed - 3.2} />}
  399. {panel.render === 'hara' && <HaraDemo />}
  400. </div>
  401. </div>
  402. {/* Progress bar */}
  403. <div style={{height: 2, background: '#eee', position: 'relative'}}>
  404. <div style={{position:'absolute', top:0, left:0, height:'100%',
  405. width: `${progressPct}%`, background: TERRA,
  406. transition:'none'}} />
  407. </div>
  408. </div>
  409. );
  410. }
  411. function GenerationSkeleton({ progress }) {
  412. const bars = [60, 85, 40, 72, 90, 55, 68];
  413. return (
  414. <div style={{padding: 24, display:'flex', flexDirection:'column', gap: 14}}>
  415. {bars.map((w, i) => (
  416. <div key={i} style={{height: 10, width: `${w}%`,
  417. background: `linear-gradient(90deg, ${LINE} 0%, ${LINE} ${100-progress*80}%, #fff ${100-progress*80}%)`,
  418. opacity: 0.6 + progress * 0.4}} />
  419. ))}
  420. <div style={{fontFamily: mono, fontSize: 10, color: ASH,
  421. marginTop: 20, letterSpacing:'0.1em'}}>
  422. {progress < 0.3 && '▸ loading style tokens...'}
  423. {progress >= 0.3 && progress < 0.6 && '▸ composing layout...'}
  424. {progress >= 0.6 && progress < 0.9 && '▸ applying typography...'}
  425. {progress >= 0.9 && '▸ finalizing...'}
  426. </div>
  427. </div>
  428. );
  429. }
  430. // ── Pentagram demo: serif editorial, strict grid, monochrome ──
  431. function PentagramDemo() {
  432. return (
  433. <div style={{padding: '28px 28px 24px', background:'#fafafa', height:'100%',
  434. display:'flex', flexDirection:'column', fontFamily: serif, color:'#111'}}>
  435. <div style={{display:'flex', justifyContent:'space-between',
  436. borderBottom:'1px solid #111', paddingBottom: 10, marginBottom: 16,
  437. fontFamily: mono, fontSize: 9, letterSpacing:'0.2em'}}>
  438. <span>VOL. 01 · MMXXVI</span>
  439. <span>NO. 043</span>
  440. </div>
  441. <div style={{fontFamily: mono, fontSize: 10, letterSpacing:'0.3em',
  442. color:'#888', marginBottom: 10}}>ESSAY</div>
  443. <div style={{fontSize: 40, lineHeight: 1.05, fontWeight: 500,
  444. letterSpacing: '-0.02em', marginBottom: 18}}>
  445. A Pure<br/>
  446. <span style={{fontStyle:'italic'}}>Information</span><br/>
  447. Architecture
  448. </div>
  449. <div style={{height: 1, background:'#111', margin:'8px 0 14px'}} />
  450. <div style={{fontSize: 13, lineHeight: 1.55, color:'#333', flex: 1}}>
  451. Designed not to impress, but to inform. The grid carries meaning; typography does the work.
  452. </div>
  453. <div style={{borderTop:'1px solid #111', paddingTop: 10, marginTop: 14,
  454. display:'flex', justifyContent:'space-between', fontFamily: mono,
  455. fontSize: 9, letterSpacing:'0.2em', color:'#888'}}>
  456. <span>NEW YORK</span>
  457. <span>PENTAGRAM</span>
  458. </div>
  459. </div>
  460. );
  461. }
  462. // ── Field.io demo: dark, kinetic geometric shapes ──
  463. function FieldDemo({ elapsed }) {
  464. const e = Math.max(0, elapsed || 0);
  465. return (
  466. <div style={{padding: 0, background:'#0e1016', height:'100%',
  467. position:'relative', overflow:'hidden'}}>
  468. <svg viewBox="0 0 400 500" width="100%" height="100%"
  469. style={{position:'absolute', inset:0}} preserveAspectRatio="xMidYMid slice">
  470. <defs>
  471. <linearGradient id="fg1" x1="0" y1="0" x2="1" y2="1">
  472. <stop offset="0%" stopColor="#ff6a3d" />
  473. <stop offset="100%" stopColor="#c04a1a" />
  474. </linearGradient>
  475. <linearGradient id="fg2" x1="0" y1="0" x2="1" y2="1">
  476. <stop offset="0%" stopColor="#4a9eff" />
  477. <stop offset="100%" stopColor="#1a4fc0" />
  478. </linearGradient>
  479. </defs>
  480. {/* Concentric circles breathing */}
  481. {[0, 1, 2, 3].map(i => (
  482. <circle key={i} cx="200" cy="280"
  483. r={40 + i * 50 + Math.sin(e * 1.2 + i) * 10}
  484. fill="none" stroke="url(#fg1)" strokeWidth={1.5}
  485. opacity={0.4 - i * 0.08} />
  486. ))}
  487. {/* Rotating triangle */}
  488. <g transform={`translate(200 280) rotate(${e * 20})`}>
  489. <polygon points="0,-70 60,35 -60,35" fill="url(#fg2)" opacity="0.7" />
  490. </g>
  491. {/* Orbiting dots */}
  492. {[0, 1, 2, 3, 4, 5].map(i => {
  493. const angle = (e * 0.8 + i * Math.PI / 3);
  494. return <circle key={i} cx={200 + Math.cos(angle) * 150}
  495. cy={280 + Math.sin(angle) * 150} r={4} fill="#ff6a3d" opacity={0.9}/>;
  496. })}
  497. </svg>
  498. <div style={{position:'absolute', top: 24, left: 24, right: 24,
  499. display:'flex', justifyContent:'space-between',
  500. fontFamily: mono, fontSize: 10, letterSpacing:'0.3em', color:'#fff', opacity: 0.7}}>
  501. <span>FIELD.IO</span>
  502. <span>LIVE · RECORDING</span>
  503. </div>
  504. <div style={{position:'absolute', bottom: 24, left: 24, right: 24,
  505. fontFamily: serif, fontStyle:'italic', fontSize: 20, color:'#fff',
  506. letterSpacing:'0.02em'}}>
  507. kinetic identity<br/>
  508. <span style={{fontFamily: mono, fontSize: 10, fontStyle:'normal',
  509. letterSpacing:'0.2em', color:'#ff6a3d', opacity: 0.8}}>
  510. / motion is the brand
  511. </span>
  512. </div>
  513. </div>
  514. );
  515. }
  516. // ── Kenya Hara demo: vast white space, tiny dot, haiku ──
  517. function HaraDemo() {
  518. return (
  519. <div style={{padding: 0, background:'#fdfbf6', height:'100%',
  520. position:'relative'}}>
  521. <div style={{position:'absolute', top: 28, left: 32,
  522. fontFamily: mono, fontSize: 10, letterSpacing:'0.3em', color:'#aaa'}}>
  523. HARA · MMXXVI
  524. </div>
  525. <div style={{position:'absolute', top: '42%', left:'50%',
  526. transform:'translate(-50%, -50%)', width: 14, height: 14,
  527. borderRadius:'50%', background:'#1a1a1a'}} />
  528. <div style={{position:'absolute', top:'58%', left:'50%',
  529. transform:'translateX(-50%)', fontFamily: serif, fontStyle:'italic',
  530. fontSize: 14, color:'#1a1a1a', letterSpacing:'0.1em'}}>
  531. white.
  532. </div>
  533. <div style={{position:'absolute', bottom: 32, right: 32,
  534. writingMode:'vertical-rl', fontFamily: "'Noto Serif SC', serif",
  535. fontSize: 16, color:'#888', letterSpacing:'0.2em'}}>
  536. 原 研 哉
  537. </div>
  538. <div style={{position:'absolute', bottom: 28, left: 32,
  539. fontFamily: serif, fontStyle:'italic', fontSize: 11, color:'#999',
  540. maxWidth: 200, lineHeight: 1.6}}>
  541. "Emptiness is not nothing—<br/>it is everything that could be."
  542. </div>
  543. </div>
  544. );
  545. }
  546. // ── Scene 5: User selects Kenya Hara (19 – 22s) ───────────
  547. function Scene5_Select() {
  548. const { elapsed } = useSprite();
  549. // Cursor travels from right edge toward middle panel
  550. const cursorX = interpolate(elapsed, [0, 1.2], [1750, 960], Easing.easeInOut);
  551. const cursorY = interpolate(elapsed, [0, 1.2], [240, 540], Easing.easeInOut);
  552. const cursorOp = interpolate(elapsed, [0, 0.2], [0, 1]);
  553. // Middle panel selection lock-in
  554. const selectLock = Math.max(0, Math.min(1, (elapsed - 1.2) / 0.4));
  555. // Left + right panels dim + shrink
  556. const sideDim = interpolate(elapsed, [1.2, 1.8], [1, 0.2]);
  557. const sideScale = interpolate(elapsed, [1.2, 1.8], [1, 0.92], Easing.easeOut);
  558. // Middle scales up
  559. const midScale = interpolate(elapsed, [1.2, 1.8], [1, 1.06], Easing.easeOut);
  560. return (
  561. <div style={{position:'absolute', inset:0, background:CREAM,
  562. padding: '60px 60px 40px', display:'flex', flexDirection:'column'}}>
  563. <div style={{display:'flex', justifyContent:'space-between',
  564. alignItems:'baseline', marginBottom: 28}}>
  565. <div>
  566. <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
  567. letterSpacing: '0.3em', marginBottom: 4}}>步骤 4 / 4</div>
  568. <div style={{fontFamily: serif, fontSize: 44, fontWeight: 500, color: INK}}>
  569. 用户选定方向
  570. </div>
  571. </div>
  572. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18, color: ASH}}>
  573. ——或混合:"A 的配色 + C 的布局"
  574. </div>
  575. </div>
  576. <div style={{display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap: 24,
  577. flex: 1}}>
  578. <StaticPanel which="pentagram" opacity={sideDim} scale={sideScale} />
  579. <StaticPanel which="hara" opacity={1} scale={midScale} selected={selectLock > 0.5}/>
  580. <StaticPanel which="field" opacity={sideDim} scale={sideScale} />
  581. </div>
  582. {/* Cursor */}
  583. <div style={{position:'absolute', left: cursorX, top: cursorY,
  584. opacity: cursorOp, pointerEvents:'none', zIndex: 50,
  585. filter:'drop-shadow(0 4px 8px rgba(0,0,0,0.2))'}}>
  586. <svg width="36" height="44" viewBox="0 0 36 44">
  587. <path d="M 2 2 L 2 38 L 11 30 L 16 42 L 22 40 L 17 28 L 28 28 Z"
  588. fill="#fff" stroke="#1a1a1a" strokeWidth="2" strokeLinejoin="round"/>
  589. </svg>
  590. </div>
  591. {/* "Selected" callout */}
  592. {selectLock > 0.5 && (
  593. <div style={{position:'absolute', left:'50%', top: 140,
  594. transform:'translateX(-50%)', background: TERRA, color:'#fff',
  595. padding:'10px 24px', fontFamily: mono, fontSize: 12,
  596. letterSpacing:'0.25em', opacity: selectLock, zIndex: 40}}>
  597. ✓ SELECTED
  598. </div>
  599. )}
  600. </div>
  601. );
  602. }
  603. function StaticPanel({ which, opacity, scale, selected }) {
  604. const titles = {
  605. pentagram: { n: 'Pentagram', en: 'Information Architecture' },
  606. hara: { n: 'Kenya Hara', en: 'Eastern Minimalism' },
  607. field: { n: 'Field.io', en: 'Kinetic Poetry' },
  608. };
  609. const t = titles[which];
  610. return (
  611. <div style={{
  612. background:'#fff',
  613. border: selected ? `3px solid ${TERRA}` : `1px solid ${LINE}`,
  614. opacity, transform: `scale(${scale})`, transformOrigin:'center center',
  615. display:'flex', flexDirection:'column',
  616. }}>
  617. <div style={{padding: '18px 22px', borderBottom: `1px solid ${LINE}`,
  618. display:'flex', justifyContent:'space-between', alignItems:'baseline'}}>
  619. <div>
  620. <div style={{fontFamily: serif, fontSize: 28, fontWeight: 500, color: INK}}>
  621. {t.n}
  622. </div>
  623. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 13,
  624. color: ASH, marginTop: 2}}>
  625. {t.en}
  626. </div>
  627. </div>
  628. <div style={{fontFamily: mono, fontSize: 10,
  629. color: selected ? TERRA : '#999',
  630. letterSpacing: '0.15em'}}>
  631. {selected ? '✓ SELECTED' : 'READY'}
  632. </div>
  633. </div>
  634. <div style={{flex: 1, position:'relative', overflow:'hidden'}}>
  635. {which === 'pentagram' && <PentagramDemo />}
  636. {which === 'field' && <FieldDemo elapsed={10} />}
  637. {which === 'hara' && <HaraDemo />}
  638. </div>
  639. </div>
  640. );
  641. }
  642. // ── Scene 6: Ready to execute (22 – 24s) ──────────────────
  643. function Scene6_Final() {
  644. const { elapsed } = useSprite();
  645. const fadeIn = interpolate(elapsed, [0, 0.6], [0, 1], Easing.easeOut);
  646. const lineW = interpolate(elapsed, [0.6, 1.4], [0, 600]);
  647. return (
  648. <div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeIn,
  649. display:'flex', alignItems:'center', justifyContent:'center',
  650. flexDirection:'column'}}>
  651. <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.4em',
  652. color: TERRA, marginBottom: 20}}>
  653. NEXT · JUNIOR DESIGNER PASS
  654. </div>
  655. <div style={{fontFamily: serif, fontSize: 104, fontWeight: 500,
  656. color: INK, lineHeight: 1, letterSpacing:'-0.01em'}}>
  657. 开始 <span style={{fontStyle:'italic', color: TERRA}}>Kenya Hara</span> 风格
  658. </div>
  659. <div style={{height: 1, background: INK, width: lineW, marginTop: 36}} />
  660. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 22,
  661. color: ASH, marginTop: 28, maxWidth: 700, textAlign:'center', lineHeight: 1.5}}>
  662. "方向确认 → 回到 Junior Designer 主干流程<br/>
  663. 这时已有明确的 design context,不再是凭空做"
  664. </div>
  665. </div>
  666. );
  667. }
  668. // ── Watermark (always visible) ────────────────────────────
  669. function Watermark() {
  670. return (
  671. <div style={{position:'absolute', bottom: 24, right: 32,
  672. fontSize: 11, color: 'rgba(0,0,0,0.38)', letterSpacing:'0.15em',
  673. fontFamily: mono, pointerEvents:'none', zIndex: 100}}>
  674. Created by Huashu-Design
  675. </div>
  676. );
  677. }
  678. // ── Main composition ──────────────────────────────────────
  679. function App() {
  680. return (
  681. <Stage duration={24} width={1920} height={1080} bgColor={CREAM}>
  682. <Sprite start={0} end={3.5}><Scene1_VagueBrief /></Sprite>
  683. <Sprite start={3.5} end={6.5}><Scene2_AdvisorIntro /></Sprite>
  684. <Sprite start={6.5} end={10.5}><Scene3_GridScan /></Sprite>
  685. <Sprite start={10.5} end={19}><Scene4_ParallelDemos /></Sprite>
  686. <Sprite start={19} end={22}><Scene5_Select /></Sprite>
  687. <Sprite start={22} end={24}><Scene6_Final /></Sprite>
  688. <Watermark />
  689. </Stage>
  690. );
  691. }
  692. ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  693. </script>
  694. </body>
  695. </html>