c4-tweaks.html 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Huashu-Design · Tweaks 实时变体</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=Noto+Sans+SC:wght@400;500;600;700&family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&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. const OLIVE = '#6a6b4e';
  154. const DEEP_BLUE = '#2a3552';
  155. const serif = "'Newsreader', 'Noto Serif SC', Georgia, serif";
  156. const sansCN = "'Noto Sans SC', -apple-system, sans-serif";
  157. const playfair = "'Playfair Display', Georgia, serif";
  158. const sans = "'Inter', -apple-system, sans-serif";
  159. const mono = "'JetBrains Mono', ui-monospace, monospace";
  160. // Palettes — the actual Tweak presets
  161. const PALETTES = {
  162. warm: { bg: CREAM, accent: TERRA, text: INK, sub: ASH, line: LINE, name: '暖米 + 赤陶', enName: 'WARM · TERRA' },
  163. olive: { bg: '#f2efdf', accent: OLIVE, text: '#2a2a1e', sub: '#7a7a5e', line: '#d4d1b8', name: '墨绿 + 鹅黄', enName: 'OLIVE · CITRON' },
  164. deep: { bg: '#f4efe6', accent: DEEP_BLUE, text: INK, sub: '#5a6478', line: '#c9c3b3', name: '深蓝 + 沙金', enName: 'DEEP · SAND' },
  165. };
  166. const FONTS = {
  167. serif: { ui: serif, display: serif, name: 'Newsreader(衬线)' },
  168. sans: { ui: sansCN, display: sansCN, name: '思源黑体' },
  169. play: { ui: playfair, display: playfair, name: 'Playfair Display' },
  170. };
  171. // ── Scene 1: Title (0 – 3s) ────────────────────────────────
  172. function Scene1_Title() {
  173. const { elapsed } = useSprite();
  174. const labelOp = interpolate(elapsed, [0.2, 0.8], [0, 1]);
  175. const mainY = interpolate(elapsed, [0.3, 1.3], [40, 0], Easing.easeOut);
  176. const mainOp = interpolate(elapsed, [0.3, 1.1], [0, 1]);
  177. const lineW = interpolate(elapsed, [1.1, 1.9], [0, 460]);
  178. const subOp = interpolate(elapsed, [1.4, 2.1], [0, 1]);
  179. const fadeOut = interpolate(elapsed, [2.6, 3.0], [1, 0], Easing.easeIn);
  180. return (
  181. <div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeOut,
  182. display:'flex', alignItems:'center', justifyContent:'center', flexDirection:'column'}}>
  183. <div style={{fontFamily: mono, fontSize: 14, letterSpacing:'0.35em',
  184. color: TERRA, marginBottom: 30, opacity: labelOp}}>
  185. 实时变体 · TWEAKS
  186. </div>
  187. <div style={{fontFamily: serif, fontSize: 120, fontWeight: 500, color: INK,
  188. lineHeight: 1.05, letterSpacing: '-0.01em',
  189. opacity: mainOp, transform: `translateY(${mainY}px)`}}>
  190. 一个 HTML · <span style={{fontStyle:'italic', color: TERRA}}>多种</span>设计
  191. </div>
  192. <div style={{height: 1, background: INK, width: lineW, marginTop: 40}} />
  193. <div style={{fontFamily: serif, fontStyle: 'italic', fontSize: 22,
  194. color: ASH, marginTop: 26, opacity: subOp, letterSpacing:'0.02em'}}>
  195. 不需要重新生成代码 · 只切参数
  196. </div>
  197. </div>
  198. );
  199. }
  200. // ── Scene 2: Main stage — control panel + live card (3 – 12s) ──
  201. function Scene2_MainStage() {
  202. const { elapsed } = useSprite();
  203. // Decide current Tweaks state based on elapsed time inside the scene.
  204. // Scene2 elapsed: 0 – 9s
  205. // 0 – 4s : warm + serif + 40
  206. // 4 – 7s : olive + serif + 40 (palette change @ 4s)
  207. // 7 – 9s : olive + sans + 40 (font change @ 7s)
  208. let palette = 'warm';
  209. let font = 'serif';
  210. const density = 40;
  211. if (elapsed >= 4) palette = 'olive';
  212. if (elapsed >= 7) font = 'sans';
  213. // Ripple trigger times
  214. const rippleTimes = [4, 7];
  215. const ripples = rippleTimes.map(t => {
  216. const e = elapsed - t;
  217. if (e < 0 || e > 1.2) return null;
  218. return { t, progress: e / 1.2 };
  219. }).filter(Boolean);
  220. // Fade-in intro
  221. const introOp = interpolate(elapsed, [0, 0.4], [0, 1]);
  222. const fadeOut = interpolate(elapsed, [8.6, 9.0], [1, 0]);
  223. const pal = PALETTES[palette];
  224. const fnt = FONTS[font];
  225. return (
  226. <div style={{position:'absolute', inset:0, background:CREAM,
  227. display:'flex', opacity: introOp * fadeOut}}>
  228. {/* Left: Control Panel (30%) */}
  229. <ControlPanel palette={palette} font={font} density={density} elapsed={elapsed} />
  230. {/* Right: Live Card (70%) */}
  231. <div style={{flex: 1, position:'relative', padding: '60px 80px',
  232. display:'flex', alignItems:'center', justifyContent:'center',
  233. transition: 'background 600ms ease-in-out',
  234. background: pal.bg}}>
  235. <LiveCard palette={palette} font={font} density={density} />
  236. {/* Ripples */}
  237. {ripples.map((r, i) => (
  238. <Ripple key={r.t} progress={r.progress}
  239. x={r.t === 4 ? 180 : 180}
  240. y={r.t === 4 ? 340 : 490} />
  241. ))}
  242. </div>
  243. </div>
  244. );
  245. }
  246. function ControlPanel({ palette, font, density, elapsed }) {
  247. return (
  248. <div style={{width: '30%', background: '#f2ece0',
  249. borderRight: `1px solid ${LINE}`, padding: '60px 44px 40px',
  250. display:'flex', flexDirection:'column', gap: 38,
  251. fontFamily: sans}}>
  252. <div>
  253. <div style={{fontFamily: mono, fontSize: 11, letterSpacing:'0.35em',
  254. color: TERRA, marginBottom: 6}}>
  255. TWEAKS
  256. </div>
  257. <div style={{fontFamily: serif, fontSize: 28, fontWeight: 500, color: INK,
  258. letterSpacing:'-0.01em'}}>
  259. 设计调参面板
  260. </div>
  261. </div>
  262. {/* Group 1: palette */}
  263. <ControlGroup label="01 · 配色方案" en="PALETTE">
  264. <Radio checked={palette==='warm'} label="暖米 + 赤陶" swatches={[CREAM, TERRA]} />
  265. <Radio checked={palette==='olive'} label="墨绿 + 鹅黄" swatches={['#f2efdf', OLIVE]} />
  266. <Radio checked={palette==='deep'} label="深蓝 + 沙金" swatches={['#f4efe6', DEEP_BLUE]} />
  267. </ControlGroup>
  268. {/* Group 2: font */}
  269. <ControlGroup label="02 · 字型" en="TYPEFACE">
  270. <Radio checked={font==='serif'} label="Newsreader(衬线)" fontFamily={serif} />
  271. <Radio checked={font==='sans'} label="思源黑体" fontFamily={sansCN} />
  272. <Radio checked={font==='play'} label="Playfair Display" fontFamily={playfair} />
  273. </ControlGroup>
  274. {/* Group 3: density */}
  275. <ControlGroup label="03 · 信息密度" en="DENSITY">
  276. <div style={{position:'relative', height: 4, background:'#e0dbcc',
  277. marginTop: 16, marginBottom: 10}}>
  278. <div style={{position:'absolute', left: 0, top: 0, height:'100%',
  279. width: `${density}%`, background: TERRA}} />
  280. <div style={{position:'absolute', left: `${density}%`, top: -6,
  281. transform:'translateX(-50%)', width: 16, height: 16,
  282. borderRadius:'50%', background: TERRA,
  283. boxShadow:'0 2px 6px rgba(0,0,0,0.15)'}} />
  284. </div>
  285. <div style={{display:'flex', justifyContent:'space-between',
  286. fontFamily: mono, fontSize: 9, letterSpacing:'0.2em', color: ASH}}>
  287. <span>克制</span><span style={{color: TERRA}}>标准</span><span>密集</span>
  288. </div>
  289. </ControlGroup>
  290. <div style={{flex: 1}} />
  291. <div style={{fontFamily: mono, fontSize: 10, letterSpacing:'0.12em',
  292. color: ASH, lineHeight: 1.6, borderTop: `1px solid ${LINE}`,
  293. paddingTop: 16}}>
  294. localStorage 持久化<br/>
  295. <span style={{color: TERRA}}>→</span> 刷新不丢
  296. </div>
  297. </div>
  298. );
  299. }
  300. function ControlGroup({ label, en, children }) {
  301. return (
  302. <div>
  303. <div style={{display:'flex', justifyContent:'space-between',
  304. alignItems:'baseline', marginBottom: 14}}>
  305. <div style={{fontFamily: serif, fontSize: 15, fontWeight: 500, color: INK}}>
  306. {label}
  307. </div>
  308. <div style={{fontFamily: mono, fontSize: 9, letterSpacing:'0.25em',
  309. color: ASH}}>{en}</div>
  310. </div>
  311. <div style={{display:'flex', flexDirection:'column', gap: 8}}>{children}</div>
  312. </div>
  313. );
  314. }
  315. function Radio({ checked, label, swatches, fontFamily }) {
  316. return (
  317. <div style={{display:'flex', alignItems:'center', gap: 12,
  318. padding:'9px 12px', background: checked ? '#fff' : 'transparent',
  319. border: `1px solid ${checked ? TERRA : 'transparent'}`,
  320. transition:'all 240ms ease-out'}}>
  321. <div style={{width: 14, height: 14, borderRadius:'50%',
  322. border: `1.5px solid ${checked ? TERRA : '#b0a898'}`,
  323. display:'flex', alignItems:'center', justifyContent:'center',
  324. flexShrink: 0}}>
  325. {checked && <div style={{width: 7, height: 7, borderRadius:'50%',
  326. background: TERRA}} />}
  327. </div>
  328. <div style={{flex: 1, fontFamily: fontFamily || sans, fontSize: 13,
  329. color: checked ? INK : '#4a4a4a'}}>{label}</div>
  330. {swatches && (
  331. <div style={{display:'flex', gap: 3}}>
  332. {swatches.map((c, i) => (
  333. <div key={i} style={{width: 12, height: 12, background: c,
  334. border:'1px solid rgba(0,0,0,0.06)'}} />
  335. ))}
  336. </div>
  337. )}
  338. </div>
  339. );
  340. }
  341. function Ripple({ progress, x, y }) {
  342. const size = progress * 420;
  343. const op = 1 - progress;
  344. return (
  345. <div style={{position:'absolute', left: x, top: y,
  346. width: size, height: size, borderRadius:'50%',
  347. border: `2px solid ${TERRA}`, opacity: op,
  348. transform: 'translate(-50%, -50%)',
  349. pointerEvents:'none'}} />
  350. );
  351. }
  352. function LiveCard({ palette, font, density }) {
  353. const pal = PALETTES[palette];
  354. const fnt = FONTS[font];
  355. return (
  356. <div style={{width: '100%', maxWidth: 880, background: '#fff',
  357. border: `1px solid ${pal.line}`,
  358. transition:'border-color 600ms ease-in-out',
  359. position:'relative'}}>
  360. {/* Header bar */}
  361. <div style={{padding:'18px 32px', borderBottom:`1px solid ${pal.line}`,
  362. display:'flex', justifyContent:'space-between', alignItems:'center',
  363. transition:'border-color 600ms ease-in-out'}}>
  364. <div style={{fontFamily: mono, fontSize: 11, letterSpacing:'0.3em',
  365. color: pal.accent, transition:'color 600ms ease-in-out'}}>
  366. LUMINA · v3.2
  367. </div>
  368. <div style={{fontFamily: mono, fontSize: 10, color: pal.sub,
  369. letterSpacing:'0.15em', transition:'color 600ms ease-in-out'}}>
  370. PALETTE · {pal.enName}
  371. </div>
  372. </div>
  373. {/* Hero content */}
  374. <div style={{padding:'56px 60px 48px', display:'grid',
  375. gridTemplateColumns:'1.4fr 1fr', gap: 48}}>
  376. <div>
  377. <div style={{fontFamily: mono, fontSize: 11,
  378. color: pal.accent, letterSpacing:'0.25em', marginBottom: 16,
  379. transition:'color 600ms ease-in-out'}}>
  380. READING · MEMORY
  381. </div>
  382. <div style={{fontFamily: fnt.display, fontSize: 68,
  383. fontWeight: font === 'sans' ? 700 : 500, color: pal.text,
  384. lineHeight: 1.05, letterSpacing:'-0.02em',
  385. transition:'color 600ms ease-in-out',
  386. marginBottom: 14}}>
  387. Lumina
  388. </div>
  389. <div style={{fontFamily: fnt.ui,
  390. fontStyle: font === 'play' ? 'italic' : 'normal',
  391. fontSize: 22, color: pal.sub, lineHeight: 1.4,
  392. letterSpacing: font === 'sans' ? 0 : '0.01em',
  393. transition:'color 600ms ease-in-out',
  394. marginBottom: 28}}>
  395. 阅读记忆 · 让每一次阅读被看见
  396. </div>
  397. <div style={{fontFamily: fnt.ui, fontSize: 14, color: pal.text,
  398. lineHeight: 1.7, opacity: 0.78, marginBottom: 32,
  399. transition:'color 600ms ease-in-out'}}>
  400. 把你读过的每一行、标注过的每一段,<br/>
  401. 汇成一条属于你的阅读河流。
  402. </div>
  403. <div style={{display:'flex', gap: 12, alignItems:'center'}}>
  404. <div style={{padding:'12px 26px', background: pal.accent,
  405. color:'#fff', fontFamily: fnt.ui, fontSize: 13,
  406. letterSpacing: font === 'sans' ? '0.05em' : '0.12em',
  407. transition:'background 600ms ease-in-out'}}>
  408. {font === 'sans' ? '开始使用' : 'Start Reading'}
  409. </div>
  410. <div style={{fontFamily: mono, fontSize: 11, color: pal.sub,
  411. letterSpacing:'0.2em',
  412. transition:'color 600ms ease-in-out'}}>
  413. FREE · BETA
  414. </div>
  415. </div>
  416. </div>
  417. {/* Right image block */}
  418. <div style={{background: pal.bg,
  419. border: `1px solid ${pal.line}`,
  420. transition:'all 600ms ease-in-out',
  421. aspectRatio: '3 / 4', position:'relative', overflow:'hidden'}}>
  422. {/* Abstract book spine illustration */}
  423. <svg width="100%" height="100%" viewBox="0 0 300 400"
  424. preserveAspectRatio="xMidYMid slice">
  425. {[0,1,2,3,4,5].map(i => (
  426. <rect key={i} x={40 + i * 35} y={60 + (i % 2) * 20}
  427. width={26} height={280 - (i % 3) * 30}
  428. fill="none" stroke={pal.accent}
  429. strokeWidth={i === 2 ? 2 : 1}
  430. opacity={0.55 + (i === 2 ? 0.4 : 0)}
  431. style={{transition:'stroke 600ms ease-in-out'}} />
  432. ))}
  433. <circle cx={150} cy={200} r={58} fill="none"
  434. stroke={pal.accent} strokeWidth={1.5} opacity={0.5}
  435. style={{transition:'stroke 600ms ease-in-out'}} />
  436. <line x1={40} y1={350} x2={260} y2={350}
  437. stroke={pal.text} strokeWidth={0.8} opacity={0.5}
  438. style={{transition:'stroke 600ms ease-in-out'}} />
  439. </svg>
  440. <div style={{position:'absolute', bottom: 16, left: 18,
  441. fontFamily: mono, fontSize: 9, letterSpacing:'0.2em',
  442. color: pal.sub,
  443. transition:'color 600ms ease-in-out'}}>
  444. FIG. 01 — SHELF
  445. </div>
  446. </div>
  447. </div>
  448. {/* Footer meta */}
  449. <div style={{padding:'14px 32px', borderTop:`1px solid ${pal.line}`,
  450. display:'flex', justifyContent:'space-between',
  451. fontFamily: mono, fontSize: 10, letterSpacing:'0.2em',
  452. color: pal.sub,
  453. transition:'all 600ms ease-in-out'}}>
  454. <span>DENSITY · {density}</span>
  455. <span>FONT · {font.toUpperCase()}</span>
  456. <span>TWEAK ID · #{palette}-{font}-{density}</span>
  457. </div>
  458. </div>
  459. );
  460. }
  461. // ── Scene 3: Code view (12 – 17s) ─────────────────────────
  462. function Scene3_CodeView() {
  463. const { elapsed } = useSprite();
  464. const introOp = interpolate(elapsed, [0, 0.5], [0, 1]);
  465. const fadeOut = interpolate(elapsed, [4.6, 5.0], [1, 0]);
  466. // Code typing effect
  467. const fullCode = `// Tweaks via localStorage + CSS vars
  468. const tweaks = {
  469. palette: 'warm', // ← user 选
  470. font: 'serif',
  471. density: 40,
  472. };
  473. document.documentElement.style
  474. .setProperty(
  475. '--accent',
  476. PALETTES[tweaks.palette].accent
  477. );
  478. localStorage.setItem(
  479. 'tweaks', JSON.stringify(tweaks)
  480. );`;
  481. const typeProgress = Math.max(0, Math.min(1, (elapsed - 0.6) / 2.4));
  482. const visibleChars = Math.floor(fullCode.length * typeProgress);
  483. const visibleCode = fullCode.slice(0, visibleChars);
  484. const cursorBlink = Math.floor(elapsed * 2.5) % 2 === 0 && typeProgress < 1;
  485. return (
  486. <div style={{position:'absolute', inset:0, background:CREAM,
  487. display:'flex', flexDirection:'column', opacity: introOp * fadeOut,
  488. padding:'60px 80px'}}>
  489. <div style={{display:'flex', justifyContent:'space-between',
  490. alignItems:'baseline', marginBottom: 36}}>
  491. <div>
  492. <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.35em',
  493. color: TERRA, marginBottom: 6}}>
  494. UNDER THE HOOD
  495. </div>
  496. <div style={{fontFamily: serif, fontSize: 52, fontWeight: 500,
  497. color: INK, letterSpacing:'-0.01em'}}>
  498. 原理 · <span style={{fontStyle:'italic', color: TERRA}}>一行配置</span>,无限变体
  499. </div>
  500. </div>
  501. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18,
  502. color: ASH, textAlign:'right', lineHeight: 1.5}}>
  503. 纯前端 · 无后端依赖<br/>
  504. <span style={{fontSize: 14}}>刷新保留状态</span>
  505. </div>
  506. </div>
  507. <div style={{display:'grid', gridTemplateColumns:'1fr 1.4fr', gap: 40,
  508. flex: 1}}>
  509. {/* Left: simplified tweak visualization */}
  510. <div style={{background:'#fff', border:`1px solid ${LINE}`,
  511. padding: 36, display:'flex', flexDirection:'column', gap: 28}}>
  512. <div style={{fontFamily: mono, fontSize: 11, letterSpacing:'0.3em',
  513. color: TERRA}}>TWEAK · STATE</div>
  514. <MiniRow label="palette" value="warm" swatch={TERRA} />
  515. <MiniRow label="font" value="serif" />
  516. <MiniRow label="density" value="40" />
  517. <div style={{height: 1, background: LINE, margin:'8px 0'}} />
  518. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 16,
  519. color: ASH, lineHeight: 1.55}}>
  520. 三个参数的组合空间:<br/>
  521. <span style={{color: TERRA, fontFamily: mono, fontStyle:'normal',
  522. fontSize: 14}}>3 × 3 × ∞ = 无限</span>
  523. </div>
  524. <div style={{flex: 1}} />
  525. <div style={{fontFamily: mono, fontSize: 10, color: ASH,
  526. letterSpacing:'0.15em', lineHeight: 1.7}}>
  527. → 改代码:不必要<br/>
  528. → 重新生成:不必要<br/>
  529. → 只改变量:30ms 生效
  530. </div>
  531. </div>
  532. {/* Right: code block */}
  533. <div style={{background:'#0e1016', padding:'28px 32px',
  534. fontFamily: mono, fontSize: 15, color:'#d4c9b5',
  535. lineHeight: 1.7, position:'relative', overflow:'hidden'}}>
  536. <div style={{display:'flex', gap: 8, marginBottom: 20}}>
  537. <div style={{width: 10, height: 10, borderRadius:'50%',
  538. background:'#ff5f57'}} />
  539. <div style={{width: 10, height: 10, borderRadius:'50%',
  540. background:'#febc2e'}} />
  541. <div style={{width: 10, height: 10, borderRadius:'50%',
  542. background:'#28c840'}} />
  543. <div style={{marginLeft: 14, fontSize: 10, color:'#888',
  544. letterSpacing:'0.15em'}}>tweaks.js</div>
  545. </div>
  546. <pre style={{whiteSpace:'pre-wrap', margin: 0,
  547. fontFamily: mono, fontSize: 15, lineHeight: 1.65}}>
  548. <CodeColorize text={visibleCode} />
  549. {cursorBlink && <span style={{color:'#ff6a3d'}}>▌</span>}
  550. </pre>
  551. </div>
  552. </div>
  553. </div>
  554. );
  555. }
  556. function MiniRow({ label, value, swatch }) {
  557. return (
  558. <div style={{display:'flex', alignItems:'center', gap: 14}}>
  559. <div style={{fontFamily: mono, fontSize: 11, color: ASH,
  560. letterSpacing:'0.2em', width: 80}}>{label}</div>
  561. <div style={{flex: 1, fontFamily: mono, fontSize: 14, color: INK}}>
  562. {value}
  563. </div>
  564. {swatch && (
  565. <div style={{width: 14, height: 14, background: swatch}} />
  566. )}
  567. </div>
  568. );
  569. }
  570. // Very light syntax coloring
  571. function CodeColorize({ text }) {
  572. const lines = text.split('\n');
  573. return (
  574. <>
  575. {lines.map((line, i) => (
  576. <span key={i}>
  577. {colorizeLine(line)}
  578. {'\n'}
  579. </span>
  580. ))}
  581. </>
  582. );
  583. }
  584. function colorizeLine(line) {
  585. const parts = [];
  586. let rest = line;
  587. // comment
  588. const cIdx = rest.indexOf('//');
  589. if (cIdx >= 0) {
  590. const before = rest.slice(0, cIdx);
  591. const comment = rest.slice(cIdx);
  592. return (
  593. <>
  594. {tokenize(before)}
  595. <span style={{color:'#6a7d6a'}}>{comment}</span>
  596. </>
  597. );
  598. }
  599. return tokenize(line);
  600. }
  601. function tokenize(s) {
  602. // keywords + strings
  603. const kw = ['const', 'let', 'var', 'function', 'return'];
  604. const words = s.split(/(\s+|[{}();,=.:'])/);
  605. return words.map((w, i) => {
  606. if (kw.includes(w)) return <span key={i} style={{color:'#c79cff'}}>{w}</span>;
  607. if (/^'[^']*'$/.test(w)) return <span key={i} style={{color:'#ffb86c'}}>{w}</span>;
  608. if (/^[0-9]+$/.test(w)) return <span key={i} style={{color:'#ff6a3d'}}>{w}</span>;
  609. if (['palette', 'font', 'density', 'tweaks', 'PALETTES', 'accent'].includes(w)) {
  610. return <span key={i} style={{color:'#8be9fd'}}>{w}</span>;
  611. }
  612. if (['document', 'localStorage'].includes(w)) {
  613. return <span key={i} style={{color:'#ff79c6'}}>{w}</span>;
  614. }
  615. return <span key={i}>{w}</span>;
  616. });
  617. }
  618. // ── Scene 4: Finale (17 – 20s) ────────────────────────────
  619. function Scene4_Final() {
  620. const { elapsed } = useSprite();
  621. const labelOp = interpolate(elapsed, [0.1, 0.7], [0, 1]);
  622. const mainY = interpolate(elapsed, [0.2, 1.2], [30, 0], Easing.easeOut);
  623. const mainOp = interpolate(elapsed, [0.2, 1.0], [0, 1]);
  624. const lineW = interpolate(elapsed, [1.0, 1.8], [0, 540]);
  625. const dimsOp = interpolate(elapsed, [1.3, 2.1], [0, 1]);
  626. const dimensions = ['配色', '字型', '密度', '布局', '动画速度'];
  627. return (
  628. <div style={{position:'absolute', inset:0, background:CREAM,
  629. display:'flex', alignItems:'center', justifyContent:'center',
  630. flexDirection:'column'}}>
  631. <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.4em',
  632. color: TERRA, marginBottom: 26, opacity: labelOp}}>
  633. TWEAKS · EVERYTHING IS A VARIABLE
  634. </div>
  635. <div style={{fontFamily: serif, fontSize: 108, fontWeight: 500,
  636. color: INK, lineHeight: 1.05, letterSpacing:'-0.01em',
  637. opacity: mainOp, transform: `translateY(${mainY}px)`,
  638. textAlign:'center'}}>
  639. 一个源文件 · <span style={{fontStyle:'italic', color: TERRA}}>无限</span>变体
  640. </div>
  641. <div style={{height: 1, background: INK, width: lineW, marginTop: 38}} />
  642. <div style={{marginTop: 36, display:'flex', gap: 10,
  643. opacity: dimsOp, alignItems:'center'}}>
  644. {dimensions.map((d, i) => (
  645. <React.Fragment key={i}>
  646. {i > 0 && (
  647. <span style={{fontFamily: mono, fontSize: 14,
  648. color: LINE, margin:'0 2px'}}>·</span>
  649. )}
  650. <span style={{fontFamily: mono, fontSize: 14,
  651. letterSpacing:'0.2em',
  652. color: i === 0 ? TERRA : ASH,
  653. padding:'6px 14px',
  654. border: `1px solid ${i === 0 ? TERRA : LINE}`}}>
  655. {d}
  656. </span>
  657. </React.Fragment>
  658. ))}
  659. </div>
  660. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 20,
  661. color: ASH, marginTop: 36, opacity: dimsOp,
  662. maxWidth: 720, textAlign:'center', lineHeight: 1.5}}>
  663. 设计不是一次性的结果 ——<br/>
  664. 而是一组可以随时拨动的旋钮
  665. </div>
  666. </div>
  667. );
  668. }
  669. // ── Watermark ─────────────────────────────────────────────
  670. function Watermark() {
  671. return (
  672. <div style={{position:'absolute', bottom: 24, right: 32,
  673. fontSize: 11, color: 'rgba(0,0,0,0.38)', letterSpacing:'0.15em',
  674. fontFamily: mono, pointerEvents:'none', zIndex: 100}}>
  675. Created by Huashu-Design
  676. </div>
  677. );
  678. }
  679. // ── Main composition ──────────────────────────────────────
  680. function App() {
  681. return (
  682. <Stage duration={20} width={1920} height={1080} bgColor={CREAM}>
  683. <Sprite start={0} end={3}><Scene1_Title /></Sprite>
  684. <Sprite start={3} end={12}><Scene2_MainStage /></Sprite>
  685. <Sprite start={12} end={17}><Scene3_CodeView /></Sprite>
  686. <Sprite start={17} end={20}><Scene4_Final /></Sprite>
  687. <Watermark />
  688. </Stage>
  689. );
  690. }
  691. ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  692. </script>
  693. </body>
  694. </html>