w1-brand-protocol.html 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Huashu-Design · 品牌资产协议 5 步硬流程</title>
  6. <script crossorigin src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
  7. <script crossorigin src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
  8. <script src="https://unpkg.com/@babel/standalone@7.25.6/babel.min.js"></script>
  9. <link rel="preconnect" href="https://fonts.googleapis.com">
  10. <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  11. <link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,300;1,6..72,400;1,6..72,500&family=Noto+Serif+SC:wght@300;400;500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
  12. <style>
  13. * { box-sizing: border-box; margin: 0; padding: 0; }
  14. html, body { width: 100%; height: 100%; overflow: hidden; }
  15. body {
  16. background: #0c0c0c;
  17. font-family: 'Newsreader', 'Noto Serif SC', Georgia, serif;
  18. color: #1a1a1a;
  19. -webkit-font-smoothing: antialiased;
  20. text-rendering: optimizeLegibility;
  21. }
  22. </style>
  23. </head>
  24. <body>
  25. <div id="root"></div>
  26. <!-- animations.jsx inlined -->
  27. <script type="text/babel">
  28. (function() {
  29. const { createContext, useContext, useState, useEffect, useRef, useCallback } = React;
  30. const TimeContext = createContext({ time: 0, duration: 10, playing: false });
  31. const SpriteContext = createContext(null);
  32. const Easing = {
  33. linear: t => t,
  34. easeIn: t => t * t,
  35. easeOut: t => 1 - (1 - t) * (1 - t),
  36. easeInOut: t => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
  37. spring: t => {
  38. const c = (2 * Math.PI) / 3;
  39. return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c) + 1;
  40. },
  41. };
  42. function interpolate(t, input, output, easing) {
  43. const [inStart, inEnd] = input;
  44. const [outStart, outEnd] = output;
  45. if (t <= inStart) return outStart;
  46. if (t >= inEnd) return outEnd;
  47. let progress = (t - inStart) / (inEnd - inStart);
  48. if (easing) progress = easing(progress);
  49. return outStart + (outEnd - outStart) * progress;
  50. }
  51. function useTime() { return useContext(TimeContext).time; }
  52. function useSprite() {
  53. const sprite = useContext(SpriteContext);
  54. return sprite || { t: 0, elapsed: 0, duration: 0 };
  55. }
  56. function Stage({ duration = 10, width = 1920, height = 1080, loop = true, children, bgColor = '#fff' }) {
  57. const [time, setTime] = useState(0);
  58. const [playing, setPlaying] = useState(true);
  59. const [scale, setScale] = useState(1);
  60. const rafRef = useRef(null);
  61. const effectiveLoop = (typeof window !== 'undefined' && window.__recording) ? false : loop;
  62. useEffect(() => {
  63. function updateScale() {
  64. const vw = window.innerWidth;
  65. const vh = window.innerHeight - 56;
  66. const s = Math.min(vw / width, vh / height);
  67. setScale(s);
  68. }
  69. updateScale();
  70. window.addEventListener('resize', updateScale);
  71. return () => window.removeEventListener('resize', updateScale);
  72. }, [width, height]);
  73. useEffect(() => {
  74. if (!playing) return;
  75. let cancelled = false;
  76. let last = null;
  77. function tick(now) {
  78. if (cancelled) return;
  79. if (last === null) {
  80. last = now;
  81. if (typeof window !== 'undefined') window.__ready = true;
  82. }
  83. const delta = (now - last) / 1000;
  84. last = now;
  85. setTime(prev => {
  86. const next = prev + delta;
  87. if (next >= duration) return effectiveLoop ? 0 : duration - 0.001;
  88. return next;
  89. });
  90. rafRef.current = requestAnimationFrame(tick);
  91. }
  92. const start = () => { if (!cancelled) rafRef.current = requestAnimationFrame(tick); };
  93. if (document.fonts && document.fonts.ready) document.fonts.ready.then(start); else start();
  94. return () => { cancelled = true; cancelAnimationFrame(rafRef.current); };
  95. }, [playing, duration, effectiveLoop]);
  96. const progress = time / duration;
  97. const ctx = { time, duration, playing, setPlaying, setTime };
  98. const canvasStyle = {
  99. position: 'absolute',
  100. top: '50%',
  101. left: '50%',
  102. transformOrigin: 'center center',
  103. width,
  104. height,
  105. background: bgColor,
  106. overflow: 'hidden',
  107. transform: `translate(-50%, -50%) scale(${scale})`,
  108. };
  109. return (
  110. <TimeContext.Provider value={ctx}>
  111. <div style={{position:'fixed', inset:0, background:'#0c0c0c', display:'flex', flexDirection:'column'}}>
  112. <div style={{flex:1, position:'relative', overflow:'hidden'}}>
  113. <div style={canvasStyle}>{children}</div>
  114. </div>
  115. <div className="no-record" style={{position:'fixed', bottom:0, left:0, right:0, background:'rgba(0,0,0,0.8)', backdropFilter:'blur(10px)', padding:'12px 20px', display:'flex', alignItems:'center', gap:16, color:'#fff', fontSize:12, zIndex:100}}>
  116. <button onClick={()=>setPlaying(p=>!p)} style={{background:'none', border:'1px solid rgba(255,255,255,0.3)', color:'#fff', padding:'6px 14px', borderRadius:4, cursor:'pointer', fontSize:12}}>{playing?'⏸ 暂停':'▶ 播放'}</button>
  117. <button onClick={()=>setTime(0)} style={{background:'none', border:'1px solid rgba(255,255,255,0.3)', color:'#fff', padding:'6px 14px', borderRadius:4, cursor:'pointer', fontSize:12}}>⏮ 开始</button>
  118. <div style={{fontFamily:'ui-monospace, monospace', fontVariantNumeric:'tabular-nums', minWidth:90}}>{time.toFixed(2)}s / {duration.toFixed(2)}s</div>
  119. <div style={{flex:1, height:4, background:'rgba(255,255,255,0.2)', borderRadius:2, position:'relative'}}>
  120. <div style={{position:'absolute', top:0, left:0, height:'100%', width:`${progress*100}%`, background:'#fff', borderRadius:2}} />
  121. </div>
  122. </div>
  123. </div>
  124. </TimeContext.Provider>
  125. );
  126. }
  127. function Sprite({ start = 0, end, children, style }) {
  128. const { time } = useContext(TimeContext);
  129. const actualEnd = end == null ? Infinity : end;
  130. if (time < start || time >= actualEnd) return null;
  131. const duration = actualEnd - start;
  132. const elapsed = time - start;
  133. const t = duration === 0 ? 1 : Math.max(0, Math.min(1, elapsed / duration));
  134. const spriteValue = { t, elapsed, duration, start, end: actualEnd };
  135. return (
  136. <SpriteContext.Provider value={spriteValue}>
  137. <div style={{position:'absolute', inset:0, ...style}}>{children}</div>
  138. </SpriteContext.Provider>
  139. );
  140. }
  141. window.Animations = { Stage, Sprite, useTime, useSprite, Easing, interpolate };
  142. })();
  143. </script>
  144. <!-- Demo scene -->
  145. <script type="text/babel">
  146. const { Stage, Sprite, useTime, useSprite, Easing, interpolate } = window.Animations;
  147. // ── Design tokens ─────────────────────────────────────────
  148. const CREAM = '#FAF6EF';
  149. const INK = '#1a1a1a';
  150. const TERRA = '#C04A1A';
  151. const ASH = '#6b6b6b';
  152. const LINE = '#d9d2c5';
  153. const OLIVE = '#6a6b4e';
  154. const serif = "'Newsreader', 'Noto Serif SC', Georgia, serif";
  155. const sans = "'Inter', -apple-system, sans-serif";
  156. const mono = "'JetBrains Mono', ui-monospace, monospace";
  157. // ── Scene 1: Trigger (0 – 3s) ─────────────────────────────
  158. function Scene1_Trigger() {
  159. const { elapsed } = useSprite();
  160. const labelOp = interpolate(elapsed, [0, 0.5], [0, 1]);
  161. const titleY = interpolate(elapsed, [0, 1], [30, 0], Easing.easeOut);
  162. const titleOp = interpolate(elapsed, [0.2, 1], [0, 1]);
  163. const brandsOp = interpolate(elapsed, [1, 1.6], [0, 1]);
  164. const switchOp = interpolate(elapsed, [1.9, 2.4], [0, 1]);
  165. const fadeOut = interpolate(elapsed, [2.6, 3], [1, 0]);
  166. const brands = ['Kimi', 'Linear', 'Lovart', 'Stripe'];
  167. return (
  168. <div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeOut,
  169. display:'flex', alignItems:'center', justifyContent:'center', flexDirection:'column'}}>
  170. <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.4em',
  171. color: TERRA, marginBottom: 28, opacity: labelOp}}>
  172. 品 牌 资 产 协 议 · BRAND PROTOCOL
  173. </div>
  174. <div style={{fontFamily: serif, fontSize: 120, fontWeight: 500, color: INK,
  175. lineHeight: 1, letterSpacing:'-0.01em',
  176. opacity: titleOp, transform: `translateY(${titleY}px)`}}>
  177. 涉及具体品牌<span style={{color: TERRA, fontStyle:'italic'}}>?</span>
  178. </div>
  179. <div style={{display:'flex', gap: 48, marginTop: 64, opacity: brandsOp}}>
  180. {brands.map((b, i) => (
  181. <span key={i} style={{fontFamily: serif, fontStyle:'italic',
  182. fontSize: 44, color: ASH, letterSpacing:'0.02em'}}>
  183. {b}
  184. </span>
  185. ))}
  186. </div>
  187. <div style={{marginTop: 72, opacity: switchOp, textAlign:'center'}}>
  188. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 34,
  189. color: TERRA, letterSpacing:'0.02em'}}>
  190. 先停下——走 5 步硬流程
  191. </div>
  192. <div style={{height:1, background: TERRA, width: 180, margin:'18px auto 0'}} />
  193. </div>
  194. </div>
  195. );
  196. }
  197. // ── Scene 2: Step 1 & 2 (3 – 7s) ──────────────────────────
  198. function Scene2_AskAndSearch() {
  199. const { elapsed } = useSprite();
  200. const headOp = interpolate(elapsed, [0, 0.4], [0, 1]);
  201. const leftOp = interpolate(elapsed, [0.3, 0.9], [0, 1]);
  202. const leftY = interpolate(elapsed, [0.3, 0.9], [20, 0], Easing.easeOut);
  203. const rightOp = interpolate(elapsed, [0.8, 1.4], [0, 1]);
  204. const rightY = interpolate(elapsed, [0.8, 1.4], [20, 0], Easing.easeOut);
  205. const fadeOut = interpolate(elapsed, [3.5, 4], [1, 0]);
  206. // cursor sweeping through search paths 1.6 → 3.2
  207. const paths = [
  208. '<brand>.com/brand',
  209. '<brand>.com/press',
  210. 'brand.<brand>.com',
  211. '<brand>.github.io/brand',
  212. ];
  213. // current active index based on time
  214. const activeIdx = Math.min(3, Math.max(0, Math.floor((elapsed - 1.6) / 0.4)));
  215. return (
  216. <div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeOut,
  217. padding:'72px 120px', display:'flex', flexDirection:'column'}}>
  218. <div style={{display:'flex', justifyContent:'space-between',
  219. alignItems:'baseline', opacity: headOp, marginBottom: 48}}>
  220. <div style={{fontFamily: serif, fontSize: 44, fontWeight: 500, color: INK}}>
  221. Step 1 & 2 · 问 · 搜
  222. </div>
  223. <div style={{fontFamily: mono, fontSize: 11, color: ASH, letterSpacing:'0.25em'}}>
  224. 01 / 05 → 02 / 05
  225. </div>
  226. </div>
  227. <div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap: 48, flex:1}}>
  228. {/* Left: Step 1 Ask */}
  229. <div style={{opacity: leftOp, transform:`translateY(${leftY}px)`,
  230. display:'flex', flexDirection:'column', gap: 24}}>
  231. <div style={{display:'flex', alignItems:'baseline', gap: 18}}>
  232. <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
  233. letterSpacing:'0.3em'}}>STEP · 01</div>
  234. <div style={{fontFamily: serif, fontSize: 40, color: INK,
  235. fontWeight: 500}}>问</div>
  236. </div>
  237. <div style={{background:'#fff', border:`1px solid ${LINE}`,
  238. padding:'32px 34px', position:'relative'}}>
  239. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 64,
  240. color: TERRA, lineHeight:1, position:'absolute',
  241. top: 8, left: 14}}>「</div>
  242. <div style={{fontFamily: serif, fontSize: 24, color: INK,
  243. lineHeight: 1.55, paddingLeft: 36}}>
  244. 这个品牌有 <span style={{fontStyle:'italic', color: TERRA}}>brand guidelines</span> 吗?
  245. </div>
  246. <div style={{height:1, background: LINE, margin:'22px 0'}} />
  247. <div style={{fontFamily: serif, fontSize: 20, color: ASH,
  248. lineHeight: 1.6, paddingLeft: 36}}>
  249. 有的话直接给我;没有我去搜。
  250. </div>
  251. </div>
  252. <div style={{fontFamily: mono, fontSize: 11, color: ASH,
  253. letterSpacing:'0.15em', marginTop: 6}}>
  254. ▸ 用户提供 &gt; AI 猜测
  255. </div>
  256. </div>
  257. {/* Right: Step 2 Search */}
  258. <div style={{opacity: rightOp, transform:`translateY(${rightY}px)`,
  259. display:'flex', flexDirection:'column', gap: 24}}>
  260. <div style={{display:'flex', alignItems:'baseline', gap: 18}}>
  261. <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
  262. letterSpacing:'0.3em'}}>STEP · 02</div>
  263. <div style={{fontFamily: serif, fontSize: 40, color: INK,
  264. fontWeight: 500}}>搜 官 方 品 牌 页</div>
  265. </div>
  266. <div style={{background:'#1a1a1a', color:'#e8e3d6',
  267. padding:'24px 28px', fontFamily: mono, fontSize: 16,
  268. lineHeight: 1.9, flex: 1}}>
  269. <div style={{color:'#6b6b6b', fontSize: 11,
  270. letterSpacing:'0.2em', marginBottom: 14}}>
  271. $ HTTP GET · typical paths
  272. </div>
  273. {paths.map((p, i) => {
  274. const isActive = i === activeIdx && elapsed > 1.6 && elapsed < 3.4;
  275. const visited = i < activeIdx;
  276. return (
  277. <div key={i} style={{
  278. color: isActive ? TERRA : (visited ? '#8a8878' : '#e8e3d6'),
  279. background: isActive ? 'rgba(192,74,26,0.12)' : 'transparent',
  280. padding:'2px 8px', marginLeft:-8,
  281. display:'flex', alignItems:'baseline', gap: 14}}>
  282. <span style={{color: isActive ? TERRA : '#555', fontSize: 13}}>
  283. {isActive ? '▸' : (visited ? '✓' : ' ')}
  284. </span>
  285. <span style={{letterSpacing:'0.02em'}}>{p}</span>
  286. </div>
  287. );
  288. })}
  289. </div>
  290. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18,
  291. color: ASH, marginTop: 6}}>
  292. 按顺序试,中断也要记 404 状态
  293. </div>
  294. </div>
  295. </div>
  296. </div>
  297. );
  298. }
  299. // ── Scene 3: Step 3 · Three fallbacks (7 – 12s) ───────────
  300. function Scene3_Fallbacks() {
  301. const { elapsed } = useSprite();
  302. const headOp = interpolate(elapsed, [0, 0.4], [0, 1]);
  303. const cardsOp = interpolate(elapsed, [0.3, 1], [0, 1]);
  304. const fadeOut = interpolate(elapsed, [4.5, 5], [1, 0]);
  305. // Card state: 0 idle, 1 active/lit, 2 failed/gray, 3 success
  306. // Card 1 active 1.2→2.0, then fails at 2.0
  307. // Card 2 active 2.2→3.0, then fails at 3.0
  308. // Card 3 active 3.2→4.2, succeeds at 4.2
  309. const card1State = elapsed < 1.2 ? 0 : elapsed < 2.0 ? 1 : 2;
  310. const card2State = elapsed < 2.2 ? 0 : elapsed < 3.0 ? 1 : (elapsed < 3.2 ? 2 : 2);
  311. const card3State = elapsed < 3.2 ? 0 : elapsed < 4.2 ? 1 : 3;
  312. const captionOp = interpolate(elapsed, [4.2, 4.6], [0, 1]);
  313. const cards = [
  314. { n: '01', title: 'SVG 文件', cmd: 'curl -o logo.svg <url>', status: '最理想', state: card1State },
  315. { n: '02', title: '官网 HTML', cmd: 'curl -A "Mozilla/5.0" \\\n -L <url>', status: '80% 场景必用', state: card2State },
  316. { n: '03', title: '产品截图取色', cmd: 'macOS Preview · 吸管', status: 'App 必走', state: card3State },
  317. ];
  318. return (
  319. <div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeOut,
  320. padding:'70px 100px 60px', display:'flex', flexDirection:'column'}}>
  321. <div style={{opacity: headOp, marginBottom: 40}}>
  322. <div style={{display:'flex', alignItems:'baseline', gap: 18, marginBottom: 6}}>
  323. <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
  324. letterSpacing:'0.3em'}}>STEP · 03</div>
  325. <div style={{fontFamily: serif, fontSize: 18, color: ASH,
  326. fontStyle:'italic'}}>下 · download</div>
  327. </div>
  328. <div style={{fontFamily: serif, fontSize: 60, fontWeight: 500,
  329. color: INK, letterSpacing:'-0.01em'}}>
  330. 三条兜底路径
  331. </div>
  332. <div style={{height:1, background: LINE, width:'100%', marginTop: 20}} />
  333. </div>
  334. <div style={{display:'grid', gridTemplateColumns:'1fr 1fr 1fr',
  335. gap: 28, flex:1, opacity: cardsOp}}>
  336. {cards.map((c, i) => {
  337. const isIdle = c.state === 0;
  338. const isActive = c.state === 1;
  339. const isFailed = c.state === 2;
  340. const isSuccess = c.state === 3;
  341. const borderColor = isSuccess ? TERRA :
  342. isActive ? INK :
  343. isFailed ? '#c4bda7' : LINE;
  344. const borderWidth = (isActive || isSuccess) ? 2 : 1;
  345. const op = isFailed ? 0.42 : 1;
  346. const bg = isSuccess ? '#fffaf3' : '#fff';
  347. return (
  348. <div key={i} style={{
  349. background: bg,
  350. border:`${borderWidth}px solid ${borderColor}`,
  351. opacity: op,
  352. padding:'28px 28px 24px',
  353. display:'flex', flexDirection:'column',
  354. position:'relative',
  355. transition: 'none',
  356. }}>
  357. <div style={{display:'flex', justifyContent:'space-between',
  358. alignItems:'baseline', marginBottom: 20}}>
  359. <div style={{fontFamily: serif, fontSize: 56, fontWeight: 300,
  360. color: isSuccess ? TERRA : INK, lineHeight:1}}>
  361. {c.n}
  362. </div>
  363. <div style={{fontFamily: mono, fontSize: 10, letterSpacing:'0.2em',
  364. color: isSuccess ? TERRA : isFailed ? ASH : INK}}>
  365. {isIdle && 'READY'}
  366. {isActive && '▸ TRYING…'}
  367. {isFailed && '× FAILED'}
  368. {isSuccess && '✓ GOT IT'}
  369. </div>
  370. </div>
  371. <div style={{fontFamily: serif, fontSize: 28, fontWeight: 500,
  372. color: INK, marginBottom: 14, letterSpacing:'-0.005em'}}>
  373. {c.title}
  374. </div>
  375. <div style={{background:'#1a1a1a', color:'#e8e3d6',
  376. fontFamily: mono, fontSize: 12, padding:'14px 16px',
  377. whiteSpace:'pre-wrap', lineHeight: 1.6,
  378. marginBottom: 20, borderLeft: `2px solid ${isSuccess ? TERRA : '#333'}`}}>
  379. {c.cmd}
  380. </div>
  381. <div style={{marginTop:'auto', borderTop:`1px solid ${LINE}`,
  382. paddingTop: 14, display:'flex', justifyContent:'space-between',
  383. alignItems:'baseline'}}>
  384. <div style={{fontFamily: serif, fontStyle:'italic',
  385. fontSize: 15, color: ASH}}>
  386. 场景
  387. </div>
  388. <div style={{fontFamily: serif, fontSize: 16,
  389. color: isSuccess ? TERRA : INK, fontWeight: 500}}>
  390. {c.status}
  391. </div>
  392. </div>
  393. {isSuccess && (
  394. <div style={{position:'absolute', top:-10, right:-10,
  395. width: 28, height: 28, borderRadius:'50%', background: TERRA,
  396. color:'#fff', display:'flex', alignItems:'center',
  397. justifyContent:'center', fontFamily: serif, fontSize: 14,
  398. fontWeight: 600}}>
  399. </div>
  400. )}
  401. </div>
  402. );
  403. })}
  404. </div>
  405. <div style={{opacity: captionOp, marginTop: 22, textAlign:'center',
  406. fontFamily: serif, fontStyle:'italic', fontSize: 22, color: INK}}>
  407. 前一条失败,<span style={{color: TERRA}}>立刻</span>走下一条——不要停
  408. </div>
  409. </div>
  410. );
  411. }
  412. // ── Scene 4: Step 4 · Grep colors (12 – 17s) ──────────────
  413. function Scene4_GrepColors() {
  414. const { elapsed } = useSprite();
  415. const headOp = interpolate(elapsed, [0, 0.4], [0, 1]);
  416. const cmdOp = interpolate(elapsed, [0.4, 0.9], [0, 1]);
  417. const fadeOut = interpolate(elapsed, [4.5, 5], [1, 0]);
  418. const results = [
  419. { count: 47, hex: '#1783FF', label: 'Kimi primary' },
  420. { count: 32, hex: '#FAFAFA', label: 'background' },
  421. { count: 18, hex: '#1a1a1a', label: 'ink' },
  422. { count: 12, hex: '#FF6B35', label: 'accent' },
  423. { count: 8, hex: '#6A6B4E', label: 'muted' },
  424. ];
  425. // Stagger each result row, starting at t=1.5
  426. const rowStart = 1.5;
  427. const rowStep = 0.32;
  428. const captionOp = interpolate(elapsed, [3.6, 4.1], [0, 1]);
  429. return (
  430. <div style={{position:'absolute', inset:0, background:'#1a1a1a', opacity: fadeOut,
  431. padding:'64px 100px', display:'flex', flexDirection:'column',
  432. color:'#e8e3d6'}}>
  433. <div style={{display:'flex', justifyContent:'space-between',
  434. alignItems:'baseline', marginBottom: 36, opacity: headOp}}>
  435. <div>
  436. <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
  437. letterSpacing:'0.3em', marginBottom: 4}}>STEP · 04</div>
  438. <div style={{fontFamily: serif, fontSize: 54, fontWeight: 500,
  439. color:'#faf6ef', letterSpacing:'-0.01em'}}>
  440. Grep 色 值
  441. </div>
  442. </div>
  443. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18,
  444. color:'#8a8878', textAlign:'right'}}>
  445. 频次排序 = 品牌色权重<br/>
  446. <span style={{fontFamily: mono, fontSize: 10, letterSpacing:'0.2em'}}>
  447. / DON'T GUESS · COUNT
  448. </span>
  449. </div>
  450. </div>
  451. {/* Command */}
  452. <div style={{opacity: cmdOp, background:'#0e0e0e',
  453. border:'1px solid #333', padding:'20px 26px',
  454. fontFamily: mono, fontSize: 15, lineHeight: 1.7,
  455. marginBottom: 28}}>
  456. <div style={{color: TERRA, fontSize: 10, letterSpacing:'0.2em',
  457. marginBottom: 10}}>$ COMMAND</div>
  458. <div style={{color:'#e8e3d6'}}>
  459. <span style={{color:'#888'}}>$</span>{' '}
  460. <span style={{color:'#7dd3fc'}}>grep</span>{' '}
  461. <span style={{color:'#fbbf24'}}>-hoE</span>{' '}
  462. <span style={{color:'#a5f3c5'}}>'#[0-9A-Fa-f]{'{6}'}'</span>{' '}
  463. <span style={{color:'#e8e3d6'}}>assets/*.{'{svg,html}'}</span> \<br/>
  464. {' '}<span style={{color:'#666'}}>|</span>{' '}
  465. <span style={{color:'#7dd3fc'}}>sort</span>{' '}
  466. <span style={{color:'#666'}}>|</span>{' '}
  467. <span style={{color:'#7dd3fc'}}>uniq -c</span>{' '}
  468. <span style={{color:'#666'}}>|</span>{' '}
  469. <span style={{color:'#7dd3fc'}}>sort -rn</span>{' '}
  470. <span style={{color:'#666'}}>|</span>{' '}
  471. <span style={{color:'#7dd3fc'}}>head -20</span>
  472. </div>
  473. </div>
  474. {/* Results + swatches */}
  475. <div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap: 60,
  476. flex: 1}}>
  477. {/* Left: result list */}
  478. <div style={{fontFamily: mono, fontSize: 18, lineHeight: 2}}>
  479. <div style={{color:'#555', fontSize: 10, letterSpacing:'0.2em',
  480. marginBottom: 12}}>▸ RESULT · top 5</div>
  481. {results.map((r, i) => {
  482. const visible = elapsed > (rowStart + i * rowStep);
  483. if (!visible) return <div key={i} style={{height: 36}}/>;
  484. const fadeIn = interpolate(elapsed,
  485. [rowStart + i*rowStep, rowStart + i*rowStep + 0.3],
  486. [0, 1]);
  487. return (
  488. <div key={i} style={{opacity: fadeIn, display:'flex',
  489. alignItems:'center', gap: 20}}>
  490. <span style={{color:'#fbbf24', minWidth: 40, textAlign:'right'}}>
  491. {String(r.count).padStart(3, ' ').replace(/ /g, '\u00A0')}
  492. </span>
  493. <span style={{color:'#e8e3d6'}}>{r.hex}</span>
  494. <span style={{color:'#666', fontSize: 14}}>←</span>
  495. <span style={{color:'#8a8878', fontStyle:'italic',
  496. fontFamily: serif, fontSize: 16}}>{r.label}</span>
  497. </div>
  498. );
  499. })}
  500. </div>
  501. {/* Right: color swatches */}
  502. <div style={{display:'flex', flexDirection:'column', gap: 14}}>
  503. <div style={{color:'#555', fontFamily: mono, fontSize: 10,
  504. letterSpacing:'0.2em', marginBottom: 2}}>▸ PALETTE</div>
  505. {results.map((r, i) => {
  506. const visible = elapsed > (rowStart + i * rowStep);
  507. if (!visible) return <div key={i} style={{height: 54}}/>;
  508. const fadeIn = interpolate(elapsed,
  509. [rowStart + i*rowStep, rowStart + i*rowStep + 0.3],
  510. [0, 1]);
  511. const w = interpolate(elapsed,
  512. [rowStart + i*rowStep, rowStart + i*rowStep + 0.5],
  513. [0, r.count * 9]);
  514. return (
  515. <div key={i} style={{opacity: fadeIn, display:'flex',
  516. alignItems:'center', gap: 14, height: 50}}>
  517. <div style={{width: 50, height: 50, background: r.hex,
  518. border:'1px solid #333', flexShrink: 0}} />
  519. <div style={{height: 24, background: r.hex,
  520. width: `${w}px`, opacity: 0.7}} />
  521. <div style={{fontFamily: mono, fontSize: 12,
  522. color:'#8a8878'}}>×{r.count}</div>
  523. </div>
  524. );
  525. })}
  526. </div>
  527. </div>
  528. {/* Key caption */}
  529. <div style={{opacity: captionOp, textAlign:'center', marginTop: 18,
  530. paddingTop: 18, borderTop:'1px solid #333'}}>
  531. <span style={{fontFamily: serif, fontStyle:'italic', fontSize: 26,
  532. color: TERRA}}>
  533. 不要凭记忆猜品牌色
  534. </span>
  535. <span style={{fontFamily: serif, fontSize: 20, color:'#8a8878',
  536. marginLeft: 14}}>
  537. ——频次说了算
  538. </span>
  539. </div>
  540. </div>
  541. );
  542. }
  543. // ── Scene 5: Step 5 · brand-spec.md (17 – 22s) ────────────
  544. function Scene5_SpecFile() {
  545. const { elapsed } = useSprite();
  546. const headOp = interpolate(elapsed, [0, 0.4], [0, 1]);
  547. const mdOp = interpolate(elapsed, [0.3, 0.9], [0, 1]);
  548. const mdX = interpolate(elapsed, [0.3, 0.9], [-40, 0], Easing.easeOut);
  549. const cssOp = interpolate(elapsed, [0.9, 1.5], [0, 1]);
  550. const cssX = interpolate(elapsed, [0.9, 1.5], [40, 0], Easing.easeOut);
  551. const arrowOp = interpolate(elapsed, [2.2, 2.7], [0, 1]);
  552. const captionOp = interpolate(elapsed, [3.4, 3.9], [0, 1]);
  553. const fadeOut = interpolate(elapsed, [4.5, 5], [1, 0]);
  554. return (
  555. <div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeOut,
  556. padding:'64px 80px 48px', display:'flex', flexDirection:'column'}}>
  557. <div style={{opacity: headOp, marginBottom: 28, display:'flex',
  558. justifyContent:'space-between', alignItems:'baseline'}}>
  559. <div>
  560. <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
  561. letterSpacing:'0.3em', marginBottom: 4}}>STEP · 05 · FINAL</div>
  562. <div style={{fontFamily: serif, fontSize: 54, fontWeight: 500,
  563. color: INK, letterSpacing:'-0.01em'}}>
  564. 固化为 <span style={{fontStyle:'italic'}}>brand-spec.md</span>
  565. </div>
  566. </div>
  567. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18,
  568. color: ASH, textAlign:'right'}}>
  569. 单一真相源 · single source of truth
  570. </div>
  571. </div>
  572. <div style={{display:'grid', gridTemplateColumns:'1.1fr 0.9fr',
  573. gap: 40, flex:1, position:'relative'}}>
  574. {/* Left: .md preview */}
  575. <div style={{opacity: mdOp, transform:`translateX(${mdX}px)`,
  576. background:'#fff', border:`1px solid ${LINE}`,
  577. display:'flex', flexDirection:'column'}}>
  578. <div style={{padding:'12px 20px', borderBottom:`1px solid ${LINE}`,
  579. display:'flex', justifyContent:'space-between', alignItems:'center',
  580. background:'#f7f2e8'}}>
  581. <div style={{fontFamily: mono, fontSize: 11, color: ASH,
  582. letterSpacing:'0.15em'}}>
  583. ▸ brand-spec.md
  584. </div>
  585. <div style={{fontFamily: mono, fontSize: 10, color: TERRA,
  586. letterSpacing:'0.15em'}}>
  587. ✓ COMMITTED
  588. </div>
  589. </div>
  590. <div style={{padding:'22px 28px', fontFamily: mono, fontSize: 12,
  591. lineHeight: 1.75, color: INK, flex:1, overflow:'hidden'}}>
  592. <div style={{color: ASH}}>---</div>
  593. <div><span style={{color: OLIVE}}>brand</span>: Kimi</div>
  594. <div><span style={{color: OLIVE}}>fetched</span>: 2026-04-20</div>
  595. <div style={{color: ASH}}>---</div>
  596. <div style={{height: 10}} />
  597. <div style={{fontFamily: serif, fontSize: 20, fontWeight: 500,
  598. marginBottom: 4}}>## 色板</div>
  599. <div>- primary: <span style={{color: TERRA}}>#1783FF</span> (47)</div>
  600. <div>- bg: <span style={{color: TERRA}}>#FAFAFA</span> (32)</div>
  601. <div>- ink: <span style={{color: TERRA}}>#1a1a1a</span> (18)</div>
  602. <div>- accent: <span style={{color: TERRA}}>#FF6B35</span> (12)</div>
  603. <div style={{height: 12}} />
  604. <div style={{fontFamily: serif, fontSize: 20, fontWeight: 500,
  605. marginBottom: 4}}>## 字型</div>
  606. <div>- display: <span style={{color: OLIVE}}>Newsreader</span></div>
  607. <div>- body: <span style={{color: OLIVE}}>Inter</span></div>
  608. <div>- mono: <span style={{color: OLIVE}}>JetBrains Mono</span></div>
  609. <div style={{height: 12}} />
  610. <div style={{fontFamily: serif, fontSize: 20, fontWeight: 500,
  611. marginBottom: 4}}>## 签名细节</div>
  612. <div>- 1px hairlines</div>
  613. <div>- 圆角半径统一 4px</div>
  614. <div style={{height: 12}} />
  615. <div style={{fontFamily: serif, fontSize: 20, fontWeight: 500,
  616. marginBottom: 4, color: TERRA}}>## 禁区</div>
  617. <div style={{color: ASH}}>- 不用紫渐变 / emoji / 4px 以上 shadow</div>
  618. </div>
  619. </div>
  620. {/* Right: CSS vars + consumers */}
  621. <div style={{opacity: cssOp, transform:`translateX(${cssX}px)`,
  622. display:'flex', flexDirection:'column', gap: 20}}>
  623. <div style={{background:'#0e0e0e', color:'#e8e3d6',
  624. fontFamily: mono, fontSize: 14, padding:'22px 26px',
  625. lineHeight: 1.7, flex: 1}}>
  626. <div style={{color:'#666', fontSize: 10, letterSpacing:'0.2em',
  627. marginBottom: 12}}>/* tokens.css */</div>
  628. <div><span style={{color:'#7dd3fc'}}>:root</span> {'{'}</div>
  629. <div> <span style={{color:'#a5f3c5'}}>--brand-primary</span>: <span style={{color:'#fbbf24'}}>#1783FF</span>;</div>
  630. <div> <span style={{color:'#a5f3c5'}}>--brand-bg</span>: <span style={{color:'#fbbf24'}}>#FAFAFA</span>;</div>
  631. <div> <span style={{color:'#a5f3c5'}}>--brand-ink</span>: <span style={{color:'#fbbf24'}}>#1a1a1a</span>;</div>
  632. <div> <span style={{color:'#a5f3c5'}}>--brand-accent</span>: <span style={{color:'#fbbf24'}}>#FF6B35</span>;</div>
  633. <div>{'}'}</div>
  634. </div>
  635. {/* Arrow cascade */}
  636. <div style={{opacity: arrowOp, position:'relative',
  637. background:'#fff', border:`1px solid ${LINE}`,
  638. padding:'20px 24px'}}>
  639. <div style={{fontFamily: mono, fontSize: 10, color: TERRA,
  640. letterSpacing:'0.2em', marginBottom: 12}}>▸ CONSUMERS</div>
  641. <div style={{display:'flex', alignItems:'center', gap: 16,
  642. fontFamily: mono, fontSize: 13, color: INK, flexWrap:'wrap'}}>
  643. <span style={{padding:'4px 10px', border:`1px solid ${TERRA}`,
  644. color: TERRA}}>brand-spec.md</span>
  645. <span style={{color: TERRA}}>→</span>
  646. <span style={{padding:'4px 10px', border:`1px solid ${INK}`}}>:root</span>
  647. <span style={{color: TERRA}}>→</span>
  648. <span style={{padding:'4px 10px', background: INK, color:'#fff'}}>all .html</span>
  649. </div>
  650. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 15,
  651. color: ASH, marginTop: 14, lineHeight: 1.5}}>
  652. 一个文件改动,所有 HTML 自动跟随。
  653. </div>
  654. </div>
  655. </div>
  656. </div>
  657. <div style={{opacity: captionOp, marginTop: 22, textAlign:'center',
  658. fontFamily: serif, fontStyle:'italic', fontSize: 22, color: INK}}>
  659. 全协议的<span style={{color: TERRA}}>制胜点</span>——让所有 CSS 引用这份文件
  660. </div>
  661. </div>
  662. );
  663. }
  664. // ── Scene 6: Final (22 – 24s) ─────────────────────────────
  665. function Scene6_Final() {
  666. const { elapsed } = useSprite();
  667. const fadeIn = interpolate(elapsed, [0, 0.5], [0, 1], Easing.easeOut);
  668. const titleY = interpolate(elapsed, [0, 0.8], [24, 0], Easing.easeOut);
  669. const lineW = interpolate(elapsed, [0.5, 1.3], [0, 580]);
  670. const subOp = interpolate(elapsed, [0.8, 1.4], [0, 1]);
  671. return (
  672. <div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeIn,
  673. display:'flex', alignItems:'center', justifyContent:'center',
  674. flexDirection:'column'}}>
  675. <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.4em',
  676. color: TERRA, marginBottom: 24}}>
  677. 01 → 02 → 03 → 04 → <span style={{color: INK}}>05 · DONE</span>
  678. </div>
  679. <div style={{fontFamily: serif, fontSize: 92, fontWeight: 500,
  680. color: INK, lineHeight: 1.1, letterSpacing:'-0.01em',
  681. textAlign:'center', transform: `translateY(${titleY}px)`}}>
  682. <span style={{fontStyle:'italic', color: TERRA}}>15 分钟</span>的投资<br/>
  683. 省下 <span style={{fontStyle:'italic'}}>1–2 小时</span> 返工
  684. </div>
  685. <div style={{height:1, background: INK, width: lineW, marginTop: 40}} />
  686. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 24,
  687. color: ASH, marginTop: 28, opacity: subOp,
  688. letterSpacing:'0.02em'}}>
  689. 这是稳定性最便宜的投资。
  690. </div>
  691. </div>
  692. );
  693. }
  694. // ── Watermark ─────────────────────────────────────────────
  695. function Watermark() {
  696. return (
  697. <div style={{position:'absolute', bottom: 24, right: 32,
  698. fontSize: 11, color: 'rgba(0,0,0,0.38)', letterSpacing:'0.15em',
  699. fontFamily: mono, pointerEvents:'none', zIndex: 100}}>
  700. Created by Huashu-Design
  701. </div>
  702. );
  703. }
  704. // ── Main composition ──────────────────────────────────────
  705. function App() {
  706. return (
  707. <Stage duration={24} width={1920} height={1080} bgColor={CREAM}>
  708. <Sprite start={0} end={3}><Scene1_Trigger /></Sprite>
  709. <Sprite start={3} end={7}><Scene2_AskAndSearch /></Sprite>
  710. <Sprite start={7} end={12}><Scene3_Fallbacks /></Sprite>
  711. <Sprite start={12} end={17}><Scene4_GrepColors /></Sprite>
  712. <Sprite start={17} end={22}><Scene5_SpecFile /></Sprite>
  713. <Sprite start={22} end={24}><Scene6_Final /></Sprite>
  714. <Watermark />
  715. </Stage>
  716. );
  717. }
  718. ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  719. </script>
  720. </body>
  721. </html>