c6-expert-review.html 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Huashu-Design · Expert Review</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,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;600&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 { background: #0c0c0c; font-family: 'Newsreader', 'Noto Serif SC', Georgia, serif; color: #1a1a1a; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
  16. </style>
  17. </head>
  18. <body>
  19. <div id="root"></div>
  20. <!-- animations.jsx inlined -->
  21. <script type="text/babel">
  22. (function() {
  23. const { createContext, useContext, useState, useEffect, useRef } = React;
  24. const TimeContext = createContext({ time: 0, duration: 10, playing: false });
  25. const SpriteContext = createContext(null);
  26. const Easing = {
  27. linear: t => t,
  28. easeIn: t => t * t,
  29. easeOut: t => 1 - (1 - t) * (1 - t),
  30. easeInOut: t => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
  31. spring: t => {
  32. const c = (2 * Math.PI) / 3;
  33. return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c) + 1;
  34. },
  35. };
  36. function interpolate(t, input, output, easing) {
  37. const [a, b] = input, [x, y] = output;
  38. if (t <= a) return x; if (t >= b) return y;
  39. let p = (t - a) / (b - a); if (easing) p = easing(p);
  40. return x + (y - x) * p;
  41. }
  42. function useTime() { return useContext(TimeContext).time; }
  43. function useSprite() { return useContext(SpriteContext) || { t: 0, elapsed: 0, duration: 0 }; }
  44. function Stage({ duration = 10, width = 1920, height = 1080, loop = true, children, bgColor = '#fff' }) {
  45. const [time, setTime] = useState(0);
  46. const [playing, setPlaying] = useState(true);
  47. const [scale, setScale] = useState(1);
  48. const rafRef = useRef(null);
  49. const effectiveLoop = (typeof window !== 'undefined' && window.__recording) ? false : loop;
  50. useEffect(() => {
  51. const update = () => {
  52. const s = Math.min(window.innerWidth / width, (window.innerHeight - 56) / height);
  53. setScale(s);
  54. };
  55. update(); window.addEventListener('resize', update);
  56. return () => window.removeEventListener('resize', update);
  57. }, [width, height]);
  58. useEffect(() => {
  59. if (!playing) return;
  60. let cancelled = false, last = null;
  61. function tick(now) {
  62. if (cancelled) return;
  63. if (last === null) { last = now; if (typeof window !== 'undefined') window.__ready = true; }
  64. const delta = (now - last) / 1000; last = now;
  65. setTime(prev => {
  66. const next = prev + delta;
  67. if (next >= duration) return effectiveLoop ? 0 : duration - 0.001;
  68. return next;
  69. });
  70. rafRef.current = requestAnimationFrame(tick);
  71. }
  72. const start = () => { if (!cancelled) rafRef.current = requestAnimationFrame(tick); };
  73. if (document.fonts && document.fonts.ready) document.fonts.ready.then(start); else start();
  74. return () => { cancelled = true; cancelAnimationFrame(rafRef.current); };
  75. }, [playing, duration, effectiveLoop]);
  76. const progress = time / duration;
  77. const ctx = { time, duration, playing, setPlaying, setTime };
  78. return (
  79. <TimeContext.Provider value={ctx}>
  80. <div style={{position:'fixed', inset:0, background:'#0c0c0c', display:'flex', flexDirection:'column'}}>
  81. <div style={{flex:1, position:'relative', overflow:'hidden'}}>
  82. <div style={{position:'absolute', top:'50%', left:'50%', transformOrigin:'center center', width, height, background: bgColor, overflow:'hidden', transform:`translate(-50%, -50%) scale(${scale})`}}>
  83. {children}
  84. </div>
  85. </div>
  86. <div className="no-record" style={{position:'fixed', bottom:0, left:0, right:0, background:'rgba(0,0,0,0.8)', padding:'12px 20px', display:'flex', alignItems:'center', gap:16, color:'#fff', fontSize:12, zIndex:100}}>
  87. <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>
  88. <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>
  89. <div style={{fontFamily:'ui-monospace, monospace', fontVariantNumeric:'tabular-nums', minWidth:90}}>{time.toFixed(2)}s / {duration.toFixed(2)}s</div>
  90. <div style={{flex:1, height:4, background:'rgba(255,255,255,0.2)', borderRadius:2, position:'relative'}}>
  91. <div style={{position:'absolute', top:0, left:0, height:'100%', width:`${progress*100}%`, background:'#fff', borderRadius:2}} />
  92. </div>
  93. </div>
  94. </div>
  95. </TimeContext.Provider>
  96. );
  97. }
  98. function Sprite({ start = 0, end, children, style }) {
  99. const { time } = useContext(TimeContext);
  100. const actualEnd = end == null ? Infinity : end;
  101. if (time < start || time >= actualEnd) return null;
  102. const duration = actualEnd - start;
  103. const elapsed = time - start;
  104. const t = duration === 0 ? 1 : Math.max(0, Math.min(1, elapsed / duration));
  105. return (
  106. <SpriteContext.Provider value={{ t, elapsed, duration, start, end: actualEnd }}>
  107. <div style={{position:'absolute', inset:0, ...style}}>{children}</div>
  108. </SpriteContext.Provider>
  109. );
  110. }
  111. window.Animations = { Stage, Sprite, useTime, useSprite, Easing, interpolate };
  112. })();
  113. </script>
  114. <!-- Demo scene -->
  115. <script type="text/babel">
  116. const { Stage, Sprite, useTime, useSprite, Easing, interpolate } = window.Animations;
  117. // ── Design tokens ─────────────────────────────────────────
  118. const CREAM = '#FAF6EF';
  119. const INK = '#1a1a1a';
  120. const TERRA = '#C04A1A';
  121. const ASH = '#6b6b6b';
  122. const LINE = '#d9d2c5';
  123. const OLIVE = '#6a6b4e';
  124. const DEEP_BLUE = '#2a3552';
  125. const serif = "'Newsreader', 'Noto Serif SC', Georgia, serif";
  126. const sans = "'Inter', -apple-system, sans-serif";
  127. const mono = "'JetBrains Mono', ui-monospace, monospace";
  128. // ── 5 dimensions ──────────────────────────────────────────
  129. const DIMENSIONS = [
  130. { no: '01', name: '哲学一致性', desc: '是否遵循既定的设计风格', score: 9 },
  131. { no: '02', name: '视觉层级', desc: '信息优先级是否一目了然', score: 8 },
  132. { no: '03', name: '细节执行', desc: '排版、间距、字重是否到位', score: 7 },
  133. { no: '04', name: '功能性', desc: '交互是否顺畅、可用', score: 6 },
  134. { no: '05', name: '创新性', desc: '是否超出了平均水准', score: 8 },
  135. ];
  136. const COMMENTS = [
  137. '赤陶橙贯穿,serif + 留白很 Kenya Hara',
  138. 'Hero 和 body 强度接近,主次需再拉开',
  139. '行距、字号梯度还差一点点克制',
  140. 'CTA 可达但色值和主色冲突',
  141. '版面节奏有想法,避开了模板感',
  142. ];
  143. // ── Scene 1: Title (0 – 3s) ────────────────────────────
  144. function Scene1_Title() {
  145. const { elapsed } = useSprite();
  146. const topOp = interpolate(elapsed, [0, 0.5], [0, 1]);
  147. const titleY = interpolate(elapsed, [0.2, 1.3], [50, 0], Easing.easeOut);
  148. const titleOp = interpolate(elapsed, [0.2, 1.1], [0, 1]);
  149. const lineW = interpolate(elapsed, [1.0, 1.8], [0, 540]);
  150. const subOp = interpolate(elapsed, [1.4, 2.1], [0, 1]);
  151. const fadeOut = interpolate(elapsed, [2.6, 3.0], [1, 0]);
  152. return (
  153. <div style={{position:'absolute', inset:0, background: CREAM, opacity: fadeOut,
  154. display:'flex', alignItems:'center', justifyContent:'center', flexDirection:'column'}}>
  155. <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.4em',
  156. color: TERRA, marginBottom: 28, opacity: topOp}}>
  157. 设计评审 · 5 维度评分
  158. </div>
  159. <div style={{fontFamily: serif, fontSize: 120, fontWeight: 500, color: INK,
  160. lineHeight: 1, letterSpacing:'-0.02em', opacity: titleOp,
  161. transform: `translateY(${titleY}px)`}}>
  162. <span style={{fontStyle:'italic', color: TERRA}}>评</span>设计 · 不评设计师
  163. </div>
  164. <div style={{height: 1, background: INK, width: lineW, marginTop: 36}} />
  165. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 22, color: ASH,
  166. marginTop: 28, opacity: subOp, letterSpacing:'0.02em'}}>
  167. 做完之后 · 用 5 个刻度看清楚
  168. </div>
  169. </div>
  170. );
  171. }
  172. // ── Scene 2: 5 dimensions intro (3 – 8s) ───────────────
  173. function Scene2_Dimensions() {
  174. const { elapsed } = useSprite();
  175. const titleOp = interpolate(elapsed, [0, 0.4], [0, 1]);
  176. const fadeOut = interpolate(elapsed, [4.5, 5.0], [1, 0]);
  177. return (
  178. <div style={{position:'absolute', inset:0, background: CREAM, opacity: fadeOut,
  179. padding: '100px 100px 80px', display:'flex', flexDirection:'column'}}>
  180. <div style={{opacity: titleOp, marginBottom: 60,
  181. display:'flex', justifyContent:'space-between', alignItems:'baseline'}}>
  182. <div>
  183. <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
  184. letterSpacing:'0.3em', marginBottom: 6}}>步骤 1 / 3 · 评审维度</div>
  185. <div style={{fontFamily: serif, fontSize: 60, fontWeight: 500, color: INK,
  186. letterSpacing:'-0.01em'}}>
  187. 五把<span style={{fontStyle:'italic', color: TERRA}}>尺子</span>
  188. </div>
  189. </div>
  190. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 19, color: ASH,
  191. textAlign:'right', lineHeight: 1.5}}>
  192. 主观审美变不可辩论,<br/>
  193. 客观维度变可打分
  194. </div>
  195. </div>
  196. <div style={{flex: 1, display:'grid', gridTemplateColumns:'repeat(5, 1fr)',
  197. gap: 22, alignItems:'stretch'}}>
  198. {DIMENSIONS.map((d, i) => {
  199. const appearStart = 0.6 + i * 0.4;
  200. const appearEnd = appearStart + 0.7;
  201. const op = interpolate(elapsed, [appearStart, appearEnd], [0, 1], Easing.easeOut);
  202. const ty = interpolate(elapsed, [appearStart, appearEnd], [30, 0], Easing.easeOut);
  203. return (
  204. <div key={i} style={{
  205. opacity: op,
  206. transform: `translateY(${ty}px)`,
  207. background: '#fff',
  208. border: `1px solid ${LINE}`,
  209. padding: '32px 26px 30px',
  210. display:'flex', flexDirection:'column',
  211. position:'relative',
  212. }}>
  213. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 72,
  214. fontWeight: 400, color: TERRA, lineHeight: 1,
  215. letterSpacing:'-0.02em', marginBottom: 20}}>
  216. {d.no}
  217. </div>
  218. <div style={{height: 1, background: INK, width: 40, marginBottom: 18}} />
  219. <div style={{fontFamily: serif, fontSize: 26, fontWeight: 500,
  220. color: INK, lineHeight: 1.15, marginBottom: 12}}>
  221. {d.name}
  222. </div>
  223. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 15,
  224. color: ASH, lineHeight: 1.55, flex: 1}}>
  225. {d.desc}
  226. </div>
  227. <div style={{fontFamily: mono, fontSize: 10, color: ASH,
  228. letterSpacing:'0.2em', marginTop: 22}}>
  229. 0 – 10 PT
  230. </div>
  231. </div>
  232. );
  233. })}
  234. </div>
  235. </div>
  236. );
  237. }
  238. // ── Scene 3: Radar + scoring (8 – 14s) ────────────────
  239. function Scene3_Radar() {
  240. const { elapsed } = useSprite();
  241. const headerOp = interpolate(elapsed, [0, 0.5], [0, 1]);
  242. const fadeOut = interpolate(elapsed, [5.5, 6.0], [1, 0]);
  243. // Radar reveal progress — polygon expands from center
  244. const reveal = interpolate(elapsed, [0.8, 2.4], [0, 1], Easing.easeOut);
  245. // Total score count-up
  246. const totalT = interpolate(elapsed, [1.6, 2.8], [0, 1], Easing.easeOut);
  247. const totalVal = Math.round(totalT * 38);
  248. // Radar geometry
  249. const cx = 340, cy = 440, R = 260;
  250. const N = 5;
  251. const maxScore = 10;
  252. const angle = i => -Math.PI/2 + i * 2 * Math.PI / N;
  253. // Axis endpoints
  254. const axisPts = DIMENSIONS.map((_, i) => ({
  255. x: cx + Math.cos(angle(i)) * R,
  256. y: cy + Math.sin(angle(i)) * R,
  257. }));
  258. // Score polygon points (animated)
  259. const scorePts = DIMENSIONS.map((d, i) => {
  260. const r = (d.score / maxScore) * R * reveal;
  261. return {
  262. x: cx + Math.cos(angle(i)) * r,
  263. y: cy + Math.sin(angle(i)) * r,
  264. };
  265. });
  266. const scorePath = scorePts.map(p => `${p.x},${p.y}`).join(' ');
  267. // Concentric rings
  268. const rings = [2, 4, 6, 8, 10];
  269. return (
  270. <div style={{position:'absolute', inset:0, background: CREAM, opacity: fadeOut,
  271. padding: '70px 90px 50px', display:'flex', flexDirection:'column'}}>
  272. <div style={{opacity: headerOp, marginBottom: 24,
  273. display:'flex', justifyContent:'space-between', alignItems:'baseline'}}>
  274. <div>
  275. <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
  276. letterSpacing:'0.3em', marginBottom: 6}}>步骤 2 / 3 · 打分</div>
  277. <div style={{fontFamily: serif, fontSize: 54, fontWeight: 500, color: INK,
  278. letterSpacing:'-0.01em'}}>
  279. 五边形 · <span style={{fontStyle:'italic'}}>照见</span>每一维
  280. </div>
  281. </div>
  282. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18, color: ASH,
  283. textAlign:'right', lineHeight: 1.5}}>
  284. 不是给个评价,<br/>
  285. 是把问题「可视化」出来
  286. </div>
  287. </div>
  288. <div style={{flex: 1, display:'grid', gridTemplateColumns: '720px 1fr', gap: 60}}>
  289. {/* Radar */}
  290. <div style={{position:'relative', background:'#fff', border:`1px solid ${LINE}`}}>
  291. <svg viewBox="0 0 720 880" width="100%" height="100%" style={{display:'block'}}>
  292. {/* Concentric rings */}
  293. {rings.map((r, i) => {
  294. const ringR = (r / maxScore) * R;
  295. const pts = DIMENSIONS.map((_, k) => {
  296. const x = cx + Math.cos(angle(k)) * ringR;
  297. const y = cy + Math.sin(angle(k)) * ringR;
  298. return `${x},${y}`;
  299. }).join(' ');
  300. return (
  301. <g key={i}>
  302. <polygon points={pts} fill="none" stroke={LINE} strokeWidth="1" />
  303. <text x={cx + 6} y={cy - ringR + 4}
  304. fontFamily={mono} fontSize="10" fill={ASH}
  305. letterSpacing="0.1em">{r}</text>
  306. </g>
  307. );
  308. })}
  309. {/* Axes */}
  310. {axisPts.map((p, i) => (
  311. <line key={i} x1={cx} y1={cy} x2={p.x} y2={p.y}
  312. stroke={LINE} strokeWidth="1" />
  313. ))}
  314. {/* Score polygon */}
  315. <polygon points={scorePath}
  316. fill={TERRA} fillOpacity="0.18"
  317. stroke={TERRA} strokeWidth="2" />
  318. {/* Score dots */}
  319. {scorePts.map((p, i) => reveal > 0.6 && (
  320. <circle key={i} cx={p.x} cy={p.y} r="5"
  321. fill={TERRA} opacity={Math.min(1, (reveal - 0.6) / 0.4)} />
  322. ))}
  323. {/* Axis labels + score */}
  324. {DIMENSIONS.map((d, i) => {
  325. const labelR = R + 48;
  326. const lx = cx + Math.cos(angle(i)) * labelR;
  327. const ly = cy + Math.sin(angle(i)) * labelR;
  328. const anchor = Math.abs(Math.cos(angle(i))) < 0.2 ? 'middle'
  329. : Math.cos(angle(i)) > 0 ? 'start' : 'end';
  330. const showScore = elapsed > 2.4 + i * 0.15;
  331. return (
  332. <g key={i}>
  333. <text x={lx} y={ly}
  334. fontFamily={mono} fontSize="13" fill={INK}
  335. fontWeight="500" textAnchor={anchor}
  336. letterSpacing="0.08em">
  337. {d.name}
  338. </text>
  339. {showScore && (
  340. <text x={lx} y={ly + 20}
  341. fontFamily={serif} fontSize="22" fill={TERRA}
  342. fontStyle="italic" fontWeight="500" textAnchor={anchor}>
  343. {d.score}
  344. <tspan fontSize="13" fill={ASH} fontStyle="normal"> / 10</tspan>
  345. </text>
  346. )}
  347. </g>
  348. );
  349. })}
  350. {/* Center total score */}
  351. <text x={cx} y={750}
  352. fontFamily={mono} fontSize="11" fill={ASH}
  353. letterSpacing="0.3em" textAnchor="middle">
  354. 总分
  355. </text>
  356. <text x={cx} y={820}
  357. fontFamily={serif} fontSize="72" fill={INK}
  358. fontWeight="500" textAnchor="middle"
  359. letterSpacing="-0.02em">
  360. <tspan fontStyle="italic" fill={TERRA}>{totalVal}</tspan>
  361. <tspan fontSize="34" fill={ASH} letterSpacing="0"> / 50</tspan>
  362. </text>
  363. </svg>
  364. </div>
  365. {/* Right: breakdown list */}
  366. <div style={{display:'flex', flexDirection:'column', gap: 18, paddingTop: 4}}>
  367. <div style={{fontFamily: mono, fontSize: 10, color: ASH,
  368. letterSpacing:'0.25em', marginBottom: 4}}>
  369. BREAKDOWN · 逐项
  370. </div>
  371. {DIMENSIONS.map((d, i) => {
  372. const rowAppear = 2.2 + i * 0.25;
  373. const op = interpolate(elapsed, [rowAppear, rowAppear + 0.6], [0, 1]);
  374. const barT = interpolate(elapsed, [rowAppear + 0.2, rowAppear + 0.9],
  375. [0, d.score / 10], Easing.easeOut);
  376. const tx = interpolate(elapsed, [rowAppear, rowAppear + 0.5], [20, 0], Easing.easeOut);
  377. return (
  378. <div key={i} style={{opacity: op, transform:`translateX(${tx}px)`,
  379. background:'#fff', border:`1px solid ${LINE}`, padding:'14px 20px'}}>
  380. <div style={{display:'flex', justifyContent:'space-between',
  381. alignItems:'baseline', marginBottom: 8}}>
  382. <div style={{fontFamily: serif, fontSize: 22, fontWeight: 500, color: INK}}>
  383. <span style={{fontFamily: mono, fontSize: 11, color: TERRA,
  384. marginRight: 12, letterSpacing:'0.15em'}}>{d.no}</span>
  385. {d.name}
  386. </div>
  387. <div style={{fontFamily: serif, fontSize: 22, fontStyle:'italic',
  388. fontWeight: 500, color: TERRA}}>
  389. {d.score}<span style={{fontSize: 13, color: ASH, fontStyle:'normal'}}> / 10</span>
  390. </div>
  391. </div>
  392. {/* Progress bar */}
  393. <div style={{height: 4, background: LINE, position:'relative', marginBottom: 8}}>
  394. <div style={{position:'absolute', top:0, left:0, height:'100%',
  395. width: `${barT * 100}%`, background: TERRA}} />
  396. </div>
  397. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 14,
  398. color: ASH, lineHeight: 1.5}}>
  399. {COMMENTS[i]}
  400. </div>
  401. </div>
  402. );
  403. })}
  404. </div>
  405. </div>
  406. </div>
  407. );
  408. }
  409. // ── Scene 4: Keep / Fix / Quick Wins (14 – 20s) ──────
  410. function Scene4_Actions() {
  411. const { elapsed } = useSprite();
  412. const headerOp = interpolate(elapsed, [0, 0.4], [0, 1]);
  413. const fadeOut = interpolate(elapsed, [5.5, 6.0], [1, 0]);
  414. const keeps = [
  415. '赤陶橙 accent 贯穿全文',
  416. 'serif display 给了文学气质',
  417. '留白足够 · 信息不挤',
  418. ];
  419. const fixes = [
  420. { tag: '致命', sev: TERRA, text: 'Hero 图和 body 抢焦点 · 降低 hero 字号' },
  421. { tag: '重要', sev: OLIVE, text: '侧边 CTA 色和品牌主色冲突' },
  422. { tag: '优化', sev: ASH, text: 'Footer 字号可以再小 2px' },
  423. ];
  424. const wins = [
  425. 'Hero 字号 96 → 72',
  426. 'CTA 改成 terra 主色',
  427. 'Footer 字号 14 → 12',
  428. ];
  429. const col1T = interpolate(elapsed, [0.4, 1.2], [0, 1], Easing.easeOut);
  430. const col2T = interpolate(elapsed, [0.8, 1.6], [0, 1], Easing.easeOut);
  431. const col3T = interpolate(elapsed, [1.2, 2.0], [0, 1], Easing.easeOut);
  432. const footerOp = interpolate(elapsed, [3.8, 4.6], [0, 1]);
  433. return (
  434. <div style={{position:'absolute', inset:0, background: CREAM, opacity: fadeOut,
  435. padding: '70px 90px 60px', display:'flex', flexDirection:'column'}}>
  436. <div style={{opacity: headerOp, marginBottom: 40,
  437. display:'flex', justifyContent:'space-between', alignItems:'baseline'}}>
  438. <div>
  439. <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
  440. letterSpacing:'0.3em', marginBottom: 6}}>步骤 3 / 3 · 行动清单</div>
  441. <div style={{fontFamily: serif, fontSize: 56, fontWeight: 500, color: INK,
  442. letterSpacing:'-0.01em'}}>
  443. Keep · Fix · <span style={{fontStyle:'italic', color: TERRA}}>Quick Wins</span>
  444. </div>
  445. </div>
  446. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18, color: ASH,
  447. textAlign:'right', lineHeight: 1.5}}>
  448. 打完分 · 不是扔下报告,<br/>
  449. 是给一张可执行的「修复清单」
  450. </div>
  451. </div>
  452. <div style={{flex: 1, display:'grid', gridTemplateColumns:'1fr 1.15fr 1fr',
  453. gap: 28}}>
  454. {/* KEEP */}
  455. <div style={{opacity: col1T, transform:`translateY(${(1-col1T)*20}px)`,
  456. background:'#fff', border:`1px solid ${LINE}`,
  457. borderTop: `4px solid ${OLIVE}`, padding: '30px 30px 28px',
  458. display:'flex', flexDirection:'column'}}>
  459. <div style={{display:'flex', justifyContent:'space-between',
  460. alignItems:'baseline', marginBottom: 24}}>
  461. <div>
  462. <div style={{fontFamily: mono, fontSize: 11, color: OLIVE,
  463. letterSpacing:'0.3em', marginBottom: 8}}>KEEP</div>
  464. <div style={{fontFamily: serif, fontSize: 32, fontWeight: 500, color: INK}}>
  465. 保持这些
  466. </div>
  467. </div>
  468. <div style={{fontFamily: serif, fontSize: 44, fontStyle:'italic',
  469. fontWeight: 400, color: OLIVE, lineHeight: 1}}>
  470. 3
  471. </div>
  472. </div>
  473. <div style={{display:'flex', flexDirection:'column', gap: 18, flex: 1}}>
  474. {keeps.map((k, i) => (
  475. <div key={i} style={{display:'flex', gap: 14, alignItems:'flex-start'}}>
  476. <div style={{fontFamily: mono, fontSize: 16, color: OLIVE,
  477. fontWeight: 600, marginTop: 2}}>✓</div>
  478. <div style={{fontFamily: serif, fontSize: 19, color: INK,
  479. lineHeight: 1.5, flex: 1}}>{k}</div>
  480. </div>
  481. ))}
  482. </div>
  483. </div>
  484. {/* FIX */}
  485. <div style={{opacity: col2T, transform:`translateY(${(1-col2T)*20}px)`,
  486. background:'#fff', border:`1px solid ${LINE}`,
  487. borderTop: `4px solid ${TERRA}`, padding: '30px 30px 28px',
  488. display:'flex', flexDirection:'column'}}>
  489. <div style={{display:'flex', justifyContent:'space-between',
  490. alignItems:'baseline', marginBottom: 24}}>
  491. <div>
  492. <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
  493. letterSpacing:'0.3em', marginBottom: 8}}>FIX</div>
  494. <div style={{fontFamily: serif, fontSize: 32, fontWeight: 500, color: INK}}>
  495. 需修复 · 按严重度
  496. </div>
  497. </div>
  498. <div style={{fontFamily: serif, fontSize: 44, fontStyle:'italic',
  499. fontWeight: 400, color: TERRA, lineHeight: 1}}>
  500. 3
  501. </div>
  502. </div>
  503. <div style={{display:'flex', flexDirection:'column', gap: 16, flex: 1}}>
  504. {fixes.map((f, i) => (
  505. <div key={i} style={{display:'flex', gap: 14, alignItems:'flex-start',
  506. paddingBottom: 14, borderBottom: i < fixes.length - 1 ? `1px solid ${LINE}` : 'none'}}>
  507. <div style={{
  508. background: f.sev, color: '#fff',
  509. fontFamily: mono, fontSize: 10, letterSpacing:'0.15em',
  510. padding: '4px 10px', marginTop: 4, minWidth: 58,
  511. textAlign: 'center', fontWeight: 600,
  512. }}>
  513. {f.tag}
  514. </div>
  515. <div style={{fontFamily: serif, fontSize: 17, color: INK,
  516. lineHeight: 1.5, flex: 1}}>{f.text}</div>
  517. </div>
  518. ))}
  519. </div>
  520. </div>
  521. {/* QUICK WINS */}
  522. <div style={{opacity: col3T, transform:`translateY(${(1-col3T)*20}px)`,
  523. background:'#fff', border:`1px solid ${LINE}`,
  524. borderTop: `4px solid ${DEEP_BLUE}`, padding: '30px 30px 28px',
  525. display:'flex', flexDirection:'column'}}>
  526. <div style={{display:'flex', justifyContent:'space-between',
  527. alignItems:'baseline', marginBottom: 24}}>
  528. <div>
  529. <div style={{fontFamily: mono, fontSize: 11, color: DEEP_BLUE,
  530. letterSpacing:'0.3em', marginBottom: 8}}>QUICK WINS</div>
  531. <div style={{fontFamily: serif, fontSize: 32, fontWeight: 500, color: INK}}>
  532. 5 分钟能做的
  533. </div>
  534. </div>
  535. <div style={{fontFamily: mono, fontSize: 10, color: ASH,
  536. letterSpacing:'0.2em', textAlign:'right', lineHeight: 1.6}}>
  537. TOP<br/>3
  538. </div>
  539. </div>
  540. <div style={{display:'flex', flexDirection:'column', gap: 18, flex: 1}}>
  541. {wins.map((w, i) => (
  542. <div key={i} style={{display:'flex', gap: 16, alignItems:'flex-start'}}>
  543. <div style={{fontFamily: serif, fontSize: 32, fontStyle:'italic',
  544. color: DEEP_BLUE, fontWeight: 400, lineHeight: 1, minWidth: 32,
  545. marginTop: -4}}>
  546. {i+1}
  547. </div>
  548. <div style={{fontFamily: serif, fontSize: 19, color: INK,
  549. lineHeight: 1.5, flex: 1}}>{w}</div>
  550. </div>
  551. ))}
  552. </div>
  553. </div>
  554. </div>
  555. {/* Footer slogan */}
  556. <div style={{marginTop: 36, textAlign:'center', opacity: footerOp}}>
  557. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 26,
  558. color: TERRA, letterSpacing:'0.02em'}}>
  559. 不是给个评价 · 是给个修复清单
  560. </div>
  561. </div>
  562. </div>
  563. );
  564. }
  565. // ── Scene 5: Outro (20 – 22s) ─────────────────────────
  566. function Scene5_Outro() {
  567. const { elapsed } = useSprite();
  568. const fadeIn = interpolate(elapsed, [0, 0.6], [0, 1], Easing.easeOut);
  569. const titleY = interpolate(elapsed, [0, 1.0], [40, 0], Easing.easeOut);
  570. const lineW = interpolate(elapsed, [0.5, 1.3], [0, 540]);
  571. const subOp = interpolate(elapsed, [0.9, 1.6], [0, 1]);
  572. return (
  573. <div style={{position:'absolute', inset:0, background: CREAM, opacity: fadeIn,
  574. display:'flex', alignItems:'center', justifyContent:'center',
  575. flexDirection:'column'}}>
  576. <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.4em',
  577. color: TERRA, marginBottom: 22}}>
  578. 5 维度 · 客观 · 可操作
  579. </div>
  580. <div style={{fontFamily: serif, fontSize: 108, fontWeight: 500,
  581. color: INK, lineHeight: 1, letterSpacing:'-0.015em',
  582. transform: `translateY(${titleY}px)`}}>
  583. 先打分 · 再<span style={{fontStyle:'italic', color: TERRA}}>修</span>
  584. </div>
  585. <div style={{height: 1, background: INK, width: lineW, marginTop: 36}} />
  586. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 22, color: ASH,
  587. marginTop: 26, opacity: subOp, letterSpacing:'0.02em'}}>
  588. Huashu-Design · Expert Review
  589. </div>
  590. </div>
  591. );
  592. }
  593. // ── Watermark ──────────────────────────────────────────
  594. function Watermark() {
  595. return (
  596. <div style={{position:'absolute', bottom: 24, right: 32,
  597. fontSize: 11, color: 'rgba(0,0,0,0.38)', letterSpacing:'0.15em',
  598. fontFamily: mono, pointerEvents:'none', zIndex: 100}}>
  599. Created by Huashu-Design
  600. </div>
  601. );
  602. }
  603. function App() {
  604. return (
  605. <Stage duration={22} width={1920} height={1080} bgColor={CREAM}>
  606. <Sprite start={0} end={3}><Scene1_Title /></Sprite>
  607. <Sprite start={3} end={8}><Scene2_Dimensions /></Sprite>
  608. <Sprite start={8} end={14}><Scene3_Radar /></Sprite>
  609. <Sprite start={14} end={20}><Scene4_Actions /></Sprite>
  610. <Sprite start={20} end={22}><Scene5_Outro /></Sprite>
  611. <Watermark />
  612. </Stage>
  613. );
  614. }
  615. ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  616. </script>
  617. </body>
  618. </html>