c5-infographic.html 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Huashu-Design · Infographic Demo</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 } = 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 [a, b] = input, [x, y] = output;
  44. if (t <= a) return x; if (t >= b) return y;
  45. let p = (t - a) / (b - a); if (easing) p = easing(p);
  46. return x + (y - x) * p;
  47. }
  48. function useTime() { return useContext(TimeContext).time; }
  49. function useSprite() { return useContext(SpriteContext) || { t: 0, elapsed: 0, duration: 0 }; }
  50. function Stage({ duration = 10, width = 1920, height = 1080, loop = true, children, bgColor = '#fff' }) {
  51. const [time, setTime] = useState(0);
  52. const [playing, setPlaying] = useState(true);
  53. const [scale, setScale] = useState(1);
  54. const rafRef = useRef(null);
  55. const effectiveLoop = (typeof window !== 'undefined' && window.__recording) ? false : loop;
  56. useEffect(() => {
  57. const update = () => {
  58. const s = Math.min(window.innerWidth / width, (window.innerHeight - 56) / height);
  59. setScale(s);
  60. };
  61. update(); window.addEventListener('resize', update);
  62. return () => window.removeEventListener('resize', update);
  63. }, [width, height]);
  64. useEffect(() => {
  65. if (!playing) return;
  66. let cancelled = false, last = null;
  67. function tick(now) {
  68. if (cancelled) return;
  69. if (last === null) { last = now; if (typeof window !== 'undefined') window.__ready = true; }
  70. const delta = (now - last) / 1000; last = now;
  71. setTime(prev => {
  72. const next = prev + delta;
  73. if (next >= duration) return effectiveLoop ? 0 : duration - 0.001;
  74. return next;
  75. });
  76. rafRef.current = requestAnimationFrame(tick);
  77. }
  78. const start = () => { if (!cancelled) rafRef.current = requestAnimationFrame(tick); };
  79. if (document.fonts && document.fonts.ready) document.fonts.ready.then(start); else start();
  80. return () => { cancelled = true; cancelAnimationFrame(rafRef.current); };
  81. }, [playing, duration, effectiveLoop]);
  82. const progress = time / duration;
  83. const ctx = { time, duration, playing, setPlaying, setTime };
  84. return (
  85. <TimeContext.Provider value={ctx}>
  86. <div style={{position:'fixed', inset:0, background:'#0c0c0c', display:'flex', flexDirection:'column'}}>
  87. <div style={{flex:1, position:'relative', overflow:'hidden'}}>
  88. <div style={{position:'absolute', top:'50%', left:'50%', transformOrigin:'center center', width, height, background: bgColor, overflow:'hidden', transform:`translate(-50%, -50%) scale(${scale})`}}>
  89. {children}
  90. </div>
  91. </div>
  92. <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}}>
  93. <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>
  94. <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>
  95. <div style={{fontFamily:'ui-monospace, monospace', fontVariantNumeric:'tabular-nums', minWidth:90}}>{time.toFixed(2)}s / {duration.toFixed(2)}s</div>
  96. <div style={{flex:1, height:4, background:'rgba(255,255,255,0.2)', borderRadius:2, position:'relative'}}>
  97. <div style={{position:'absolute', top:0, left:0, height:'100%', width:`${progress*100}%`, background:'#fff', borderRadius:2}} />
  98. </div>
  99. </div>
  100. </div>
  101. </TimeContext.Provider>
  102. );
  103. }
  104. function Sprite({ start = 0, end, children, style }) {
  105. const { time } = useContext(TimeContext);
  106. const actualEnd = end == null ? Infinity : end;
  107. if (time < start || time >= actualEnd) return null;
  108. const duration = actualEnd - start;
  109. const elapsed = time - start;
  110. const t = duration === 0 ? 1 : Math.max(0, Math.min(1, elapsed / duration));
  111. return (
  112. <SpriteContext.Provider value={{ t, elapsed, duration, start, end: actualEnd }}>
  113. <div style={{position:'absolute', inset:0, ...style}}>{children}</div>
  114. </SpriteContext.Provider>
  115. );
  116. }
  117. window.Animations = { Stage, Sprite, useTime, useSprite, Easing, interpolate };
  118. })();
  119. </script>
  120. <!-- Demo scene -->
  121. <script type="text/babel">
  122. const { Stage, Sprite, useTime, useSprite, Easing, interpolate } = window.Animations;
  123. // ── Design tokens ─────────────────────────────────────────
  124. const CREAM = '#FAF6EF';
  125. const INK = '#1a1a1a';
  126. const TERRA = '#C04A1A';
  127. const ASH = '#6b6b6b';
  128. const LINE = '#d9d2c5';
  129. const OLIVE = '#6a6b4e';
  130. const DEEP_BLUE = '#2a3552';
  131. const serif = "'Newsreader', 'Noto Serif SC', Georgia, serif";
  132. const sans = "'Inter', -apple-system, sans-serif";
  133. const mono = "'JetBrains Mono', ui-monospace, monospace";
  134. // ── Scene 1: Title (0 – 3s) ───────────────────────────────
  135. function Scene1_Title() {
  136. const { elapsed } = useSprite();
  137. const topOp = interpolate(elapsed, [0.0, 0.6], [0, 1]);
  138. const topLineW = interpolate(elapsed, [0.3, 1.0], [0, 220]);
  139. const mainOp = interpolate(elapsed, [0.5, 1.2], [0, 1]);
  140. const mainY = interpolate(elapsed, [0.5, 1.3], [32, 0], Easing.easeOut);
  141. const italicOp = interpolate(elapsed, [1.0, 1.6], [0, 1]);
  142. const subOp = interpolate(elapsed, [1.5, 2.1], [0, 1]);
  143. const fadeOut = interpolate(elapsed, [2.6, 3.0], [1, 0], Easing.easeIn);
  144. return (
  145. <div style={{position:'absolute', inset:0, background: CREAM, opacity: fadeOut,
  146. display:'flex', alignItems:'center', justifyContent:'center', flexDirection:'column'}}>
  147. <div style={{display:'flex', alignItems:'center', gap: 18, opacity: topOp, marginBottom: 48}}>
  148. <div style={{height: 1, background: TERRA, width: topLineW}}/>
  149. <div style={{fontFamily: mono, fontSize: 12, color: TERRA,
  150. letterSpacing:'0.35em'}}>
  151. 信息图 · 数据驱动 · 印刷级
  152. </div>
  153. <div style={{height: 1, background: TERRA, width: topLineW}}/>
  154. </div>
  155. <div style={{fontFamily: serif, fontSize: 160, fontWeight: 500,
  156. color: INK, lineHeight: 1, letterSpacing: '-0.02em',
  157. opacity: mainOp, transform: `translateY(${mainY}px)`}}>
  158. 让数据<span style={{fontStyle:'italic', color: TERRA, opacity: italicOp}}>说话</span>
  159. </div>
  160. <div style={{fontFamily: serif, fontStyle: 'italic', fontSize: 24,
  161. color: ASH, marginTop: 44, opacity: subOp, letterSpacing: '0.02em'}}>
  162. 精确排版 · 一眼看懂 · 可印刷
  163. </div>
  164. </div>
  165. );
  166. }
  167. // ── Scene 2: Full infographic layout (3 – 10s) ────────────
  168. // Uses a magazine spread style: headline, three columns (big numbers / bars / pie), trend line footer
  169. function Scene2_Spread() {
  170. const { elapsed } = useSprite();
  171. const headerOp = interpolate(elapsed, [0, 0.5], [0, 1]);
  172. const ruleW = interpolate(elapsed, [0.3, 1.0], [0, 1800]);
  173. const colDelay = [0.6, 1.0, 1.4];
  174. return (
  175. <div style={{position:'absolute', inset:0, background: CREAM,
  176. padding: '60px 80px 50px', display:'flex', flexDirection:'column'}}>
  177. {/* Masthead */}
  178. <div style={{opacity: headerOp, display:'flex', justifyContent:'space-between',
  179. alignItems:'baseline', marginBottom: 14, fontFamily: mono, fontSize: 11,
  180. letterSpacing: '0.3em', color: ASH}}>
  181. <span>HUASHU · INFOGRAPHIC REPORT</span>
  182. <span>VOL. 01 · 2026.04</span>
  183. </div>
  184. {/* Headline */}
  185. <div style={{opacity: headerOp, fontFamily: serif, fontSize: 72,
  186. fontWeight: 500, color: INK, lineHeight: 1.05, letterSpacing: '-0.01em',
  187. marginBottom: 8}}>
  188. 2026 AI 写作工具
  189. <span style={{fontStyle:'italic', color: TERRA, marginLeft: 20}}>年度观察</span>
  190. </div>
  191. <div style={{opacity: headerOp, fontFamily: serif, fontStyle:'italic',
  192. fontSize: 20, color: ASH, marginBottom: 22}}>
  193. 156 位创作者匿名问卷 · 3 月 15 日 – 4 月 10 日
  194. </div>
  195. {/* Top rule */}
  196. <div style={{width: ruleW, height: 1, background: INK, marginBottom: 28}}/>
  197. {/* Three-column grid */}
  198. <div style={{display:'grid', gridTemplateColumns:'1fr 1px 1.15fr 1px 0.95fr',
  199. gap: 36, flex: 1}}>
  200. <ColumnLeft elapsed={elapsed - colDelay[0]} />
  201. <div style={{background: LINE}}/>
  202. <ColumnMid elapsed={elapsed - colDelay[1]} />
  203. <div style={{background: LINE}}/>
  204. <ColumnRight elapsed={elapsed - colDelay[2]} />
  205. </div>
  206. {/* Footer trend line */}
  207. <FooterTrend elapsed={elapsed - 3.5} />
  208. </div>
  209. );
  210. }
  211. function ColumnLeft({ elapsed }) {
  212. const e = Math.max(0, elapsed);
  213. const labelOp = interpolate(e, [0, 0.4], [0, 1]);
  214. // 87%
  215. const n1 = Math.round(interpolate(e, [0.3, 1.8], [0, 87], Easing.easeOut));
  216. const bar1 = interpolate(e, [0.3, 1.8], [0, 87], Easing.easeOut);
  217. // 3.2x
  218. const n2 = interpolate(e, [1.3, 2.8], [1.0, 3.2], Easing.easeOut);
  219. const bar2 = interpolate(e, [1.3, 2.8], [0, 3.2/5*100], Easing.easeOut);
  220. // 156
  221. const n3 = Math.round(interpolate(e, [2.3, 3.6], [0, 156], Easing.easeOut));
  222. const bar3 = interpolate(e, [2.3, 3.6], [0, 100], Easing.easeOut);
  223. return (
  224. <div style={{display:'flex', flexDirection:'column', gap: 30}}>
  225. <div style={{fontFamily: mono, fontSize: 10, letterSpacing:'0.3em',
  226. color: TERRA, opacity: labelOp}}>
  227. COLUMN / 01 · 核心指标
  228. </div>
  229. <MetricRow
  230. value={`${n1}%`}
  231. width={`${bar1}%`}
  232. label="用户每周使用 AI 辅助写作"
  233. note="¹ 每周 ≥ 3 次"
  234. color={TERRA}
  235. />
  236. <MetricRow
  237. value={`${n2.toFixed(1)}×`}
  238. width={`${bar2}%`}
  239. label="平均产出效率提升"
  240. note="² 自述周稿字数"
  241. color={OLIVE}
  242. />
  243. <MetricRow
  244. value={String(n3)}
  245. width={`${bar3}%`}
  246. label="有效样本数"
  247. note="³ 剔除 AI 默认答卷"
  248. color={DEEP_BLUE}
  249. />
  250. </div>
  251. );
  252. }
  253. function MetricRow({ value, width, label, note, color }) {
  254. return (
  255. <div>
  256. <div style={{display:'flex', alignItems:'baseline', gap: 10, marginBottom: 8}}>
  257. <div style={{fontFamily: serif, fontSize: 72, fontWeight: 500,
  258. color: INK, lineHeight: 0.95, letterSpacing:'-0.02em',
  259. fontVariantNumeric:'tabular-nums'}}>
  260. {value}
  261. </div>
  262. </div>
  263. <div style={{height: 6, background: '#eee7d7', width:'100%',
  264. marginBottom: 10, position:'relative'}}>
  265. <div style={{position:'absolute', top:0, left:0, height:'100%',
  266. width, background: color}}/>
  267. </div>
  268. <div style={{fontFamily: serif, fontSize: 15, color: INK, lineHeight: 1.4}}>
  269. {label}
  270. <span style={{fontFamily: mono, fontSize: 9, color: ASH,
  271. verticalAlign:'super', marginLeft: 4}}>{note}</span>
  272. </div>
  273. </div>
  274. );
  275. }
  276. function ColumnMid({ elapsed }) {
  277. const e = Math.max(0, elapsed);
  278. const labelOp = interpolate(e, [0, 0.4], [0, 1]);
  279. // 5 bars, staggered 0.15s
  280. const bars = [
  281. { name:'长文创作', pct: 78, color: TERRA },
  282. { name:'短内容', pct: 64, color: OLIVE },
  283. { name:'标题/文案', pct: 52, color: DEEP_BLUE },
  284. { name:'润色校对', pct: 41, color: ASH },
  285. { name:'翻译', pct: 29, color: ASH },
  286. ];
  287. const chartH = 320;
  288. const maxPct = 100;
  289. return (
  290. <div style={{display:'flex', flexDirection:'column', gap: 18}}>
  291. <div style={{fontFamily: mono, fontSize: 10, letterSpacing:'0.3em',
  292. color: TERRA, opacity: labelOp}}>
  293. COLUMN / 02 · 用途分布
  294. </div>
  295. <div style={{fontFamily: serif, fontSize: 28, fontWeight: 500,
  296. color: INK, lineHeight: 1.2, opacity: labelOp,
  297. letterSpacing: '-0.01em'}}>
  298. 你最常用 AI 做什么?
  299. </div>
  300. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 13,
  301. color: ASH, opacity: labelOp}}>
  302. 多选题 · 百分比占全样本
  303. </div>
  304. {/* chart */}
  305. <div style={{position:'relative', height: chartH, display:'flex',
  306. alignItems:'flex-end', gap: 18, padding: '0 8px', marginTop: 4,
  307. borderBottom:`1px solid ${INK}`}}>
  308. {/* y-axis gridlines */}
  309. {[25, 50, 75, 100].map(v => (
  310. <div key={v} style={{position:'absolute', left: 0, right: 0,
  311. bottom: (v/maxPct)*chartH, height: 1,
  312. borderTop:`1px dashed ${LINE}`, pointerEvents:'none'}}>
  313. <div style={{position:'absolute', left: -36, top: -8,
  314. fontFamily: mono, fontSize: 9, color: ASH,
  315. letterSpacing:'0.05em'}}>
  316. {v}%
  317. </div>
  318. </div>
  319. ))}
  320. {bars.map((b, i) => {
  321. const delay = 0.8 + i * 0.15;
  322. const growT = Math.max(0, Math.min(1, (e - delay) / 0.55));
  323. const h = growT * (b.pct / maxPct) * chartH;
  324. const labelOpB = Math.max(0, Math.min(1, (e - delay - 0.35) / 0.3));
  325. return (
  326. <div key={i} style={{flex: 1, position:'relative',
  327. display:'flex', flexDirection:'column', alignItems:'center',
  328. justifyContent:'flex-end', height:'100%'}}>
  329. <div style={{position:'absolute', top: -22,
  330. fontFamily: mono, fontSize: 11, color: INK,
  331. letterSpacing:'0.02em', fontVariantNumeric:'tabular-nums',
  332. opacity: labelOpB}}>
  333. {b.pct}%
  334. </div>
  335. <div style={{width: '100%', height: h, background: b.color,
  336. transition:'none'}}/>
  337. </div>
  338. );
  339. })}
  340. </div>
  341. {/* x-axis labels */}
  342. <div style={{display:'flex', gap: 18, padding: '0 8px', marginTop: -8}}>
  343. {bars.map((b, i) => (
  344. <div key={i} style={{flex: 1, textAlign:'center',
  345. fontFamily: serif, fontSize: 12, color: INK,
  346. letterSpacing:'0.02em', opacity: labelOp}}>
  347. {b.name}
  348. </div>
  349. ))}
  350. </div>
  351. </div>
  352. );
  353. }
  354. function ColumnRight({ elapsed }) {
  355. const e = Math.max(0, elapsed);
  356. const labelOp = interpolate(e, [0, 0.4], [0, 1]);
  357. // Three pie slices sweep in
  358. const slices = [
  359. { label:'Claude', pct: 46, color: TERRA },
  360. { label:'GPT', pct: 31, color: DEEP_BLUE },
  361. { label:'GLM/国产', pct: 23, color: OLIVE },
  362. ];
  363. const cx = 130, cy = 130, r = 104;
  364. const C = 2 * Math.PI * r;
  365. // cumulative pct as fractions
  366. let acc = 0;
  367. const slicesCalc = slices.map((s, i) => {
  368. const delay = 0.6 + i * 0.45;
  369. const sweepT = Math.max(0, Math.min(1, (e - delay) / 0.7));
  370. const start = acc;
  371. const end = acc + s.pct / 100;
  372. acc = end;
  373. return { ...s, start, end, sweepT, delay };
  374. });
  375. return (
  376. <div style={{display:'flex', flexDirection:'column', gap: 14}}>
  377. <div style={{fontFamily: mono, fontSize: 10, letterSpacing:'0.3em',
  378. color: TERRA, opacity: labelOp}}>
  379. COLUMN / 03 · 模型占有率
  380. </div>
  381. <div style={{fontFamily: serif, fontSize: 24, fontWeight: 500,
  382. color: INK, lineHeight: 1.2, opacity: labelOp,
  383. letterSpacing:'-0.01em'}}>
  384. 主力模型分布
  385. </div>
  386. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 12,
  387. color: ASH, opacity: labelOp, marginBottom: 6}}>
  388. 单选题 · 日常首选
  389. </div>
  390. <div style={{display:'flex', alignItems:'center', gap: 18}}>
  391. <svg width="260" height="260" viewBox="0 0 260 260">
  392. {/* Background ring */}
  393. <circle cx={cx} cy={cy} r={r} fill="none"
  394. stroke={LINE} strokeWidth={1}/>
  395. {slicesCalc.map((s, i) => {
  396. // Draw partial arc with stroke-dasharray
  397. const sweepLen = (s.end - s.start) * s.sweepT;
  398. const dash = sweepLen * C;
  399. const gap = C - dash;
  400. const rot = s.start * 360 - 90;
  401. return (
  402. <circle key={i} cx={cx} cy={cy} r={r}
  403. fill="none" stroke={s.color} strokeWidth={28}
  404. strokeDasharray={`${dash} ${gap}`}
  405. strokeDashoffset={0}
  406. transform={`rotate(${rot} ${cx} ${cy})`}
  407. opacity={0.95}/>
  408. );
  409. })}
  410. {/* Inner text */}
  411. <text x={cx} y={cy - 4} textAnchor="middle"
  412. fontFamily={serif} fontSize={34} fill={INK}
  413. fontWeight={500} letterSpacing="-0.5">
  414. n=156
  415. </text>
  416. <text x={cx} y={cy + 22} textAnchor="middle"
  417. fontFamily={mono} fontSize={10} fill={ASH}
  418. letterSpacing="0.2em">
  419. TOTAL
  420. </text>
  421. </svg>
  422. <div style={{display:'flex', flexDirection:'column', gap: 14, flex: 1}}>
  423. {slicesCalc.map((s, i) => {
  424. const txtOp = Math.max(0, Math.min(1, (e - s.delay - 0.3) / 0.4));
  425. return (
  426. <div key={i} style={{display:'flex', alignItems:'baseline',
  427. gap: 10, opacity: txtOp}}>
  428. <div style={{width: 10, height: 10, background: s.color,
  429. marginTop: 4, flexShrink: 0}}/>
  430. <div style={{flex: 1}}>
  431. <div style={{fontFamily: serif, fontSize: 18,
  432. fontWeight: 500, color: INK, letterSpacing:'0.01em'}}>
  433. {s.label}
  434. </div>
  435. </div>
  436. <div style={{fontFamily: serif, fontSize: 22,
  437. fontWeight: 500, color: INK,
  438. fontVariantNumeric:'tabular-nums'}}>
  439. {s.pct}%
  440. </div>
  441. </div>
  442. );
  443. })}
  444. </div>
  445. </div>
  446. </div>
  447. );
  448. }
  449. function FooterTrend({ elapsed }) {
  450. const e = Math.max(0, elapsed);
  451. const op = interpolate(e, [0, 0.5], [0, 1]);
  452. const data = [12, 18, 24, 31, 38, 48, 57, 64, 71, 78, 84, 87];
  453. const months = ['05','06','07','08','09','10','11','12','01','02','03','04'];
  454. const W = 1760, H = 86, PAD = 8;
  455. const maxV = 100;
  456. // progressive reveal of line
  457. const revealT = Math.max(0, Math.min(1, (e - 0.3) / 1.4));
  458. const nPoints = Math.max(1, Math.floor(revealT * data.length));
  459. const pts = [];
  460. for (let i = 0; i < data.length; i++) {
  461. const x = (i / (data.length - 1)) * W;
  462. const y = H - (data[i] / maxV) * (H - PAD * 2) - PAD;
  463. pts.push([x, y]);
  464. }
  465. const visiblePts = pts.slice(0, nPoints);
  466. const d = visiblePts.map((p, i) => (i === 0 ? 'M' : 'L') + p[0].toFixed(1) + ' ' + p[1].toFixed(1)).join(' ');
  467. const area = visiblePts.length > 1
  468. ? d + ` L ${visiblePts[visiblePts.length-1][0].toFixed(1)} ${H} L 0 ${H} Z`
  469. : '';
  470. return (
  471. <div style={{marginTop: 26, opacity: op}}>
  472. <div style={{display:'flex', justifyContent:'space-between',
  473. alignItems:'baseline', marginBottom: 8}}>
  474. <div style={{fontFamily: mono, fontSize: 10, color: TERRA,
  475. letterSpacing:'0.3em'}}>TREND · 过去 12 个月 AI 周使用率(%)</div>
  476. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 13,
  477. color: ASH}}>
  478. 从 12% 到 87% · 增长 7.25×
  479. </div>
  480. </div>
  481. <svg width={W} height={H} viewBox={`0 0 ${W} ${H}`}
  482. style={{display:'block', width:'100%', height: H}}>
  483. {area && <path d={area} fill={TERRA} opacity={0.08}/>}
  484. {d && <path d={d} fill="none" stroke={TERRA} strokeWidth={1.6}/>}
  485. {visiblePts.map((p, i) => (
  486. <circle key={i} cx={p[0]} cy={p[1]} r={2.4} fill={TERRA}/>
  487. ))}
  488. {/* Axis labels */}
  489. {pts.map((p, i) => (
  490. <text key={i} x={p[0]} y={H - 0}
  491. textAnchor="middle" fontFamily={mono} fontSize={9} fill={ASH}
  492. opacity={0.6}>
  493. {months[i]}
  494. </text>
  495. ))}
  496. </svg>
  497. </div>
  498. );
  499. }
  500. // ── Scene 3: Typography close-up (10 – 17s) ───────────────
  501. function Scene3_Typography() {
  502. const { elapsed } = useSprite();
  503. const fadeIn = interpolate(elapsed, [0, 0.6], [0, 1]);
  504. const fadeOut = interpolate(elapsed, [6.5, 7.0], [1, 0], Easing.easeIn);
  505. const opacity = Math.min(fadeIn, fadeOut);
  506. const labelOp = interpolate(elapsed, [0.2, 0.8], [0, 1]);
  507. const leftOp = interpolate(elapsed, [0.4, 1.2], [0, 1]);
  508. const compareOp = interpolate(elapsed, [1.8, 2.6], [0, 1]);
  509. const captionOp = interpolate(elapsed, [3.6, 4.4], [0, 1]);
  510. // Pulsing scale on "87"
  511. const pulse = 1 + Math.sin(elapsed * 1.6) * 0.008;
  512. return (
  513. <div style={{position:'absolute', inset:0, background: CREAM, opacity,
  514. padding: '60px 80px', display:'flex', flexDirection:'column'}}>
  515. {/* Top label */}
  516. <div style={{display:'flex', alignItems:'baseline',
  517. justifyContent:'space-between', marginBottom: 32, opacity: labelOp}}>
  518. <div>
  519. <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
  520. letterSpacing:'0.3em', marginBottom: 6}}>DETAIL · ZOOM 1.5×</div>
  521. <div style={{fontFamily: serif, fontSize: 46, fontWeight: 500,
  522. color: INK, letterSpacing:'-0.01em'}}>
  523. 排版细节:<span style={{fontStyle:'italic', color: TERRA}}>品味税</span>
  524. </div>
  525. </div>
  526. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18,
  527. color: ASH, textAlign:'right', maxWidth: 400, lineHeight: 1.5}}>
  528. "AI 能写中文,但分不清什么是好的中文排版"
  529. </div>
  530. </div>
  531. <div style={{display:'grid', gridTemplateColumns:'1.25fr 1fr',
  532. gap: 56, flex: 1}}>
  533. {/* Left: Number gradient showcase */}
  534. <div style={{opacity: leftOp, display:'flex', flexDirection:'column',
  535. gap: 28}}>
  536. <div style={{fontFamily: mono, fontSize: 10, color: ASH,
  537. letterSpacing:'0.3em'}}>01 · 字号梯度 · HIERARCHY</div>
  538. <div style={{display:'flex', alignItems:'baseline', gap: 36,
  539. borderBottom:`1px solid ${LINE}`, paddingBottom: 28}}>
  540. <div style={{fontFamily: serif, fontSize: 220, fontWeight: 500,
  541. color: INK, lineHeight: 0.88, letterSpacing:'-0.04em',
  542. fontVariantNumeric:'tabular-nums', display:'inline-block',
  543. transform:`scale(${pulse})`, transformOrigin:'left bottom'}}>
  544. 87<span style={{fontSize: 80, color: TERRA,
  545. verticalAlign:'super', marginLeft: 4, fontStyle:'italic'}}>%</span>
  546. </div>
  547. <div style={{fontFamily: serif, fontSize: 110, fontWeight: 400,
  548. color: OLIVE, lineHeight: 0.88, letterSpacing:'-0.02em',
  549. fontVariantNumeric:'tabular-nums'}}>
  550. 3.2<span style={{fontSize: 44, fontStyle:'italic',
  551. color: ASH, marginLeft: 2}}>×</span>
  552. </div>
  553. <div style={{fontFamily: serif, fontSize: 56, fontWeight: 400,
  554. color: DEEP_BLUE, lineHeight: 0.88,
  555. fontVariantNumeric:'tabular-nums'}}>
  556. 156
  557. </div>
  558. </div>
  559. <div style={{fontFamily: serif, fontSize: 15, color: ASH,
  560. lineHeight: 1.55, maxWidth: 580}}>
  561. 主数据 <span style={{color: INK, fontWeight: 500}}>220pt</span>、
  562. 次级 <span style={{color: INK, fontWeight: 500}}>110pt</span>、
  563. 辅助 <span style={{color: INK, fontWeight: 500}}>56pt</span>——
  564. 梯度 2× 不是工程师拍脑袋,是几百年印刷品的视觉惯性。
  565. </div>
  566. <div style={{fontFamily: mono, fontSize: 10, color: ASH,
  567. letterSpacing:'0.3em', marginTop: 10}}>02 · 换行 · TEXT-WRAP: PRETTY</div>
  568. <div style={{fontFamily: serif, fontSize: 34, fontWeight: 500,
  569. color: INK, lineHeight: 1.25, letterSpacing:'-0.01em',
  570. maxWidth: 620, textWrap:'pretty'}}>
  571. 标题在该断的地方断开<br/>
  572. 避免孤字和单字成行
  573. </div>
  574. <div style={{fontFamily: mono, fontSize: 10, color: ASH,
  575. letterSpacing:'0.3em', marginTop: 4}}>03 · 上标辅注 · MONO FOOTNOTE</div>
  576. <div style={{fontFamily: serif, fontSize: 20, color: INK,
  577. lineHeight: 1.6, maxWidth: 620}}>
  578. 87%
  579. <span style={{fontFamily: mono, fontSize: 11, color: TERRA,
  580. verticalAlign:'super', marginLeft: 4}}>¹</span>
  581. 用户每周用 AI 辅助写作
  582. <div style={{fontFamily: mono, fontSize: 11, color: ASH,
  583. marginTop: 10, letterSpacing:'0.05em'}}>
  584. ¹ 基于 156 位创作者调研,每周 ≥ 3 次
  585. </div>
  586. </div>
  587. </div>
  588. {/* Right: AI slop vs 精致 */}
  589. <div style={{opacity: compareOp, display:'flex', flexDirection:'column',
  590. gap: 20}}>
  591. <div style={{fontFamily: mono, fontSize: 10, color: ASH,
  592. letterSpacing:'0.3em'}}>04 · AI SLOP vs 精致版</div>
  593. {/* Slop version */}
  594. <div style={{position:'relative', border: `1.5px dashed #c06060`,
  595. padding: '22px 22px', borderRadius: 16,
  596. background: 'linear-gradient(135deg, #6a47d4 0%, #3a1a7a 100%)'}}>
  597. <div style={{position:'absolute', top: -10, left: 14,
  598. background: '#c06060', color:'#fff', fontFamily: mono,
  599. fontSize: 9, padding:'2px 10px', letterSpacing:'0.2em'}}>
  600. ✕ 反例 · 不要这样做
  601. </div>
  602. <div style={{fontFamily: sans, fontSize: 28, fontWeight: 700,
  603. color:'#fff', marginBottom: 6, letterSpacing:'-0.01em'}}>
  604. 🚀 AI 写作工具爆发增长!
  605. </div>
  606. <div style={{fontFamily: sans, fontSize: 13, color:'rgba(255,255,255,0.85)',
  607. lineHeight: 1.5}}>
  608. ✨ 87% 用户都在用!💡 效率提升 3.2 倍!🎯 赶紧加入!
  609. </div>
  610. <div style={{marginTop: 14, display:'flex', gap: 8}}>
  611. <div style={{background:'rgba(255,255,255,0.2)',
  612. padding:'6px 12px', borderRadius: 999,
  613. fontFamily: sans, fontSize: 11, color:'#fff'}}>
  614. #AI写作
  615. </div>
  616. <div style={{background:'rgba(255,255,255,0.2)',
  617. padding:'6px 12px', borderRadius: 999,
  618. fontFamily: sans, fontSize: 11, color:'#fff'}}>
  619. #爆款
  620. </div>
  621. </div>
  622. </div>
  623. {/* Good version */}
  624. <div style={{position:'relative', background:'#fff',
  625. border: `1px solid ${LINE}`, padding: '22px 22px'}}>
  626. <div style={{position:'absolute', top: -10, left: 14,
  627. background: TERRA, color:'#fff', fontFamily: mono,
  628. fontSize: 9, padding:'2px 10px', letterSpacing:'0.2em'}}>
  629. ✓ 精致版 · DO THIS
  630. </div>
  631. <div style={{fontFamily: mono, fontSize: 9, color: TERRA,
  632. letterSpacing:'0.3em', marginBottom: 4}}>ESSAY · 2026.04</div>
  633. <div style={{fontFamily: serif, fontSize: 26, fontWeight: 500,
  634. color: INK, lineHeight: 1.2, letterSpacing:'-0.01em',
  635. marginBottom: 8}}>
  636. AI 写作<br/>
  637. <span style={{fontStyle:'italic'}}>悄然</span>改变创作者
  638. </div>
  639. <div style={{height: 1, background: INK, width: 70, marginBottom: 10}}/>
  640. <div style={{fontFamily: serif, fontSize: 13, color:'#444',
  641. lineHeight: 1.6}}>
  642. 87% 的创作者已经把 AI 纳入日常工作流;
  643. 效率提升 3.2×,但人味不减反增——
  644. 工具不定义内容,品味才定义。
  645. </div>
  646. </div>
  647. </div>
  648. </div>
  649. {/* Caption */}
  650. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 20,
  651. color: ASH, textAlign:'center', marginTop: 22, opacity: captionOp,
  652. letterSpacing:'0.02em'}}>
  653. 排版细节是 AI 分不清的 <span style={{color: TERRA, fontWeight: 500,
  654. fontStyle:'normal'}}>品味税</span>
  655. </div>
  656. </div>
  657. );
  658. }
  659. // ── Scene 4: Outro (17 – 22s) ─────────────────────────────
  660. function Scene4_Outro() {
  661. const { elapsed } = useSprite();
  662. const fadeIn = interpolate(elapsed, [0, 0.6], [0, 1]);
  663. const mainY = interpolate(elapsed, [0, 1.2], [28, 0], Easing.easeOut);
  664. const italicOp = interpolate(elapsed, [0.8, 1.4], [0, 1]);
  665. const lineW = interpolate(elapsed, [1.0, 1.8], [0, 680]);
  666. const subOp = interpolate(elapsed, [1.6, 2.2], [0, 1]);
  667. const monoOp = interpolate(elapsed, [2.4, 3.2], [0, 1]);
  668. const monoLineW = interpolate(elapsed, [2.8, 3.8], [0, 520]);
  669. return (
  670. <div style={{position:'absolute', inset:0, background: CREAM, opacity: fadeIn,
  671. display:'flex', alignItems:'center', justifyContent:'center',
  672. flexDirection:'column'}}>
  673. <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.4em',
  674. color: TERRA, marginBottom: 32, opacity: fadeIn}}>
  675. HUASHU-DESIGN · INFOGRAPHIC CAPABILITY
  676. </div>
  677. <div style={{fontFamily: serif, fontSize: 148, fontWeight: 500,
  678. color: INK, lineHeight: 1, letterSpacing:'-0.02em',
  679. transform:`translateY(${mainY}px)`}}>
  680. <span style={{fontStyle:'italic', opacity: italicOp}}>数据</span>
  681. <span style={{opacity: fadeIn}}> 配得上 </span>
  682. <span style={{color: TERRA, opacity: italicOp}}>好看</span>
  683. </div>
  684. <div style={{height: 1, background: INK, width: lineW, marginTop: 44}}/>
  685. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 24,
  686. color: ASH, marginTop: 26, opacity: subOp, letterSpacing:'0.02em'}}>
  687. 印刷级 · 不因为缩放而失真
  688. </div>
  689. <div style={{marginTop: 60, opacity: monoOp,
  690. display:'flex', alignItems:'center', flexDirection:'column', gap: 14}}>
  691. <div style={{height: 1, background: LINE, width: monoLineW}}/>
  692. <div style={{fontFamily: mono, fontSize: 14, color: INK,
  693. letterSpacing:'0.18em'}}>
  694. export → <span style={{color: TERRA}}>PDF 矢量</span> /
  695. <span style={{color: OLIVE}}> PNG 300dpi</span> /
  696. <span style={{color: DEEP_BLUE}}> SVG 原生</span>
  697. </div>
  698. <div style={{height: 1, background: LINE, width: monoLineW}}/>
  699. </div>
  700. </div>
  701. );
  702. }
  703. // ── Watermark ─────────────────────────────────────────────
  704. function Watermark() {
  705. return (
  706. <div style={{position:'absolute', bottom: 24, right: 32,
  707. fontSize: 11, color: 'rgba(0,0,0,0.38)', letterSpacing:'0.15em',
  708. fontFamily: mono, pointerEvents:'none', zIndex: 100}}>
  709. Created by Huashu-Design
  710. </div>
  711. );
  712. }
  713. // ── Composition ───────────────────────────────────────────
  714. function App() {
  715. return (
  716. <Stage duration={22} width={1920} height={1080} bgColor={CREAM}>
  717. <Sprite start={0} end={3}><Scene1_Title /></Sprite>
  718. <Sprite start={3} end={10}><Scene2_Spread /></Sprite>
  719. <Sprite start={10} end={17}><Scene3_Typography /></Sprite>
  720. <Sprite start={17} end={22}><Scene4_Outro /></Sprite>
  721. <Watermark />
  722. </Stage>
  723. );
  724. }
  725. ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  726. </script>
  727. </body>
  728. </html>