c2-slides-pptx.html 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Huashu-Design · Slides → PPTX</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 DEEP_BLUE = '#2a3552';
  155. const serif = "'Newsreader', 'Noto Serif SC', Georgia, serif";
  156. const sans = "'Inter', -apple-system, sans-serif";
  157. const mono = "'JetBrains Mono', ui-monospace, monospace";
  158. // ══════════════════════════════════════════════════════════
  159. // Scene 1 (0 – 3s) · 开题
  160. // ══════════════════════════════════════════════════════════
  161. function Scene1_Title() {
  162. const { elapsed } = useSprite();
  163. const tagOp = interpolate(elapsed, [0, 0.6], [0, 1]);
  164. const mainOp = interpolate(elapsed, [0.4, 1.2], [0, 1]);
  165. const mainY = interpolate(elapsed, [0.4, 1.2], [40, 0], Easing.easeOut);
  166. const terraOp = interpolate(elapsed, [1.1, 1.8], [0, 1]);
  167. const lineW = interpolate(elapsed, [1.6, 2.2], [0, 640]);
  168. const subOp = interpolate(elapsed, [1.9, 2.5], [0, 1]);
  169. const fadeOut = interpolate(elapsed, [2.7, 3.0], [1, 0]);
  170. return (
  171. <div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeOut,
  172. display:'flex', alignItems:'center', justifyContent:'center', flexDirection:'column'}}>
  173. <div style={{position:'absolute', top: 72, left: 88,
  174. fontFamily: mono, fontSize: 12, letterSpacing:'0.3em',
  175. color: ASH, opacity: tagOp}}>
  176. <span style={{color: TERRA}}>●</span> 幻灯片能力 · HTML + PPTX
  177. </div>
  178. <div style={{fontFamily: serif, fontSize: 130, fontWeight: 500,
  179. color: INK, lineHeight: 1.0, letterSpacing:'-0.015em',
  180. opacity: mainOp, transform: `translateY(${mainY}px)`,
  181. textAlign: 'center'}}>
  182. <span style={{fontStyle:'italic'}}>播放</span>用 HTML,<br/>
  183. <span style={{fontStyle:'italic', color: TERRA, opacity: terraOp}}>编辑</span>用 PPTX
  184. </div>
  185. <div style={{height: 1, background: INK, width: lineW, marginTop: 40}} />
  186. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 24,
  187. color: ASH, marginTop: 24, opacity: subOp, letterSpacing:'0.02em'}}>
  188. 一个源文件,两种交付形态
  189. </div>
  190. </div>
  191. );
  192. }
  193. // ══════════════════════════════════════════════════════════
  194. // Scene 2 (3 – 9s) · HTML Deck 翻页
  195. // ══════════════════════════════════════════════════════════
  196. function Scene2_DeckFlip() {
  197. const { elapsed } = useSprite();
  198. const frameOp = interpolate(elapsed, [0, 0.6], [0, 1]);
  199. const frameScale = interpolate(elapsed, [0, 0.8], [0.94, 1], Easing.easeOut);
  200. // Three pages, each ~1.5s. Stagger timings inside deck.
  201. // Page 1: 0.6 – 2.2 | Page 2: 2.2 – 3.8 | Page 3: 3.8 – 5.6
  202. const pageIndex = elapsed < 2.2 ? 0 : elapsed < 3.8 ? 1 : 2;
  203. const pageNum = pageIndex + 1;
  204. const fadeOut = interpolate(elapsed, [5.6, 6.0], [1, 0]);
  205. return (
  206. <div style={{position:'absolute', inset:0, background: CREAM, opacity: fadeOut,
  207. display:'flex', alignItems:'center', justifyContent:'center', flexDirection:'column'}}>
  208. <div style={{position:'absolute', top: 48, left: 88,
  209. fontFamily: mono, fontSize: 11, letterSpacing:'0.3em', color: ASH}}>
  210. <span style={{color: TERRA}}>●</span> SCENE 02 · HTML DECK
  211. </div>
  212. <div style={{position:'absolute', top: 48, right: 88,
  213. fontFamily: serif, fontStyle:'italic', fontSize: 20, color: ASH}}>
  214. 浏览器里直接演讲
  215. </div>
  216. <div style={{opacity: frameOp, transform: `scale(${frameScale})`,
  217. transformOrigin:'center center'}}>
  218. <BrowserFrame url="file:///Users/huashu/decks/annual-2026/deck.html">
  219. <DeckSlide pageIndex={pageIndex} localElapsed={elapsed} />
  220. {/* Footer inside deck */}
  221. <div style={{position:'absolute', bottom: 18, left: 28, right: 28,
  222. display:'flex', justifyContent:'space-between', alignItems:'center',
  223. zIndex: 5}}>
  224. <div style={{fontFamily: mono, fontSize: 11, color: ASH,
  225. letterSpacing:'0.15em'}}>
  226. {String(pageNum).padStart(2,'0')} / 12
  227. </div>
  228. <div style={{fontFamily: mono, fontSize: 10, color: ASH,
  229. letterSpacing:'0.2em'}}>
  230. HUASHU · DESIGN
  231. </div>
  232. </div>
  233. {/* TERRA progress bar */}
  234. <div style={{position:'absolute', bottom: 0, left: 0, right: 0,
  235. height: 3, background: '#eee', zIndex: 5}}>
  236. <div style={{height:'100%', width: `${(pageNum/12)*100}%`,
  237. background: TERRA}} />
  238. </div>
  239. </BrowserFrame>
  240. </div>
  241. <div style={{marginTop: 28, fontFamily: mono, fontSize: 11, color: ASH,
  242. letterSpacing:'0.25em'}}>
  243. <span style={{color: pageIndex === 0 ? TERRA : LINE}}>●</span>
  244. <span style={{margin:'0 10px', color: pageIndex === 1 ? TERRA : LINE}}>●</span>
  245. <span style={{color: pageIndex === 2 ? TERRA : LINE}}>●</span>
  246. </div>
  247. </div>
  248. );
  249. }
  250. // Browser chrome container (chrome style, 1600×900 deck 16:9)
  251. function BrowserFrame({ url, children }) {
  252. const W = 1400, H = 788; // 16:9 ratio
  253. return (
  254. <div style={{
  255. display:'inline-block',
  256. background:'#e8e4dc',
  257. borderRadius: 12,
  258. boxShadow:'0 30px 70px rgba(0,0,0,0.18), 0 10px 24px rgba(0,0,0,0.12)',
  259. padding: 0,
  260. overflow:'hidden',
  261. border:`1px solid ${LINE}`,
  262. }}>
  263. {/* Title bar */}
  264. <div style={{height: 42, display:'flex', alignItems:'center',
  265. background:'#e8e4dc', padding:'0 16px', gap: 8,
  266. borderBottom:`1px solid ${LINE}`}}>
  267. <div style={{width:12, height:12, borderRadius:'50%', background:'#ff5f57'}} />
  268. <div style={{width:12, height:12, borderRadius:'50%', background:'#febc2e'}} />
  269. <div style={{width:12, height:12, borderRadius:'50%', background:'#28c840'}} />
  270. <div style={{flex: 1, height: 26, background:'#faf6ef', border:`1px solid ${LINE}`,
  271. borderRadius: 6, marginLeft: 16, padding:'0 14px',
  272. display:'flex', alignItems:'center', gap: 8,
  273. fontFamily: mono, fontSize: 11, color: ASH, letterSpacing:'0.02em',
  274. overflow:'hidden', whiteSpace:'nowrap'}}>
  275. <svg width="10" height="12" viewBox="0 0 10 12" style={{flexShrink: 0}}>
  276. <path d="M2 5 V3.5 a3 3 0 016 0 V5" stroke={OLIVE} strokeWidth="1.2" fill="none"/>
  277. <rect x="1" y="5" width="8" height="6" fill={OLIVE} opacity="0.85"/>
  278. </svg>
  279. <span style={{color: INK, opacity: 0.7}}>{url}</span>
  280. </div>
  281. <div style={{fontFamily: mono, fontSize: 10, color: ASH,
  282. letterSpacing:'0.15em'}}>DECK MODE</div>
  283. </div>
  284. {/* Deck area */}
  285. <div style={{width: W, height: H, background:'#fff', position:'relative',
  286. overflow:'hidden'}}>
  287. {children}
  288. </div>
  289. </div>
  290. );
  291. }
  292. // Three deck pages
  293. function DeckSlide({ pageIndex, localElapsed }) {
  294. // Slide-in entrance each time pageIndex changes
  295. const pageStart = pageIndex === 0 ? 0.6 : pageIndex === 1 ? 2.2 : 3.8;
  296. const sinceStart = localElapsed - pageStart;
  297. const slideX = interpolate(sinceStart, [0, 0.5], [140, 0], Easing.easeOut);
  298. const fadeIn = interpolate(sinceStart, [0, 0.4], [0, 1]);
  299. return (
  300. <div key={pageIndex} style={{position:'absolute', inset:0,
  301. opacity: fadeIn, transform: `translateX(${slideX}px)`}}>
  302. {pageIndex === 0 && <CoverPage />}
  303. {pageIndex === 1 && <DataPage />}
  304. {pageIndex === 2 && <QuotePage />}
  305. </div>
  306. );
  307. }
  308. function CoverPage() {
  309. return (
  310. <div style={{padding: '80px 80px 60px', height:'100%', background:'#fff',
  311. display:'flex', flexDirection:'column'}}>
  312. <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.3em',
  313. color: TERRA, marginBottom: 14}}>
  314. VOL.01 · ANNUAL REPORT
  315. </div>
  316. <div style={{flex: 1, display:'flex', flexDirection:'column',
  317. justifyContent:'center'}}>
  318. <div style={{fontFamily: serif, fontSize: 132, fontWeight: 500,
  319. color: INK, lineHeight: 1.02, letterSpacing:'-0.02em'}}>
  320. 2026<br/>
  321. <span style={{fontStyle:'italic'}}>设计年度</span>报告
  322. </div>
  323. <div style={{height: 1, background: INK, width: 380, marginTop: 36,
  324. marginBottom: 28}} />
  325. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 22,
  326. color: ASH, letterSpacing:'0.02em'}}>
  327. The shape of digital craft, from typography to motion.
  328. </div>
  329. </div>
  330. </div>
  331. );
  332. }
  333. function DataPage() {
  334. const numbers = [
  335. { big: '428', label: '项目交付', unit: 'projects' },
  336. { big: '92%', label: '客户续约', unit: 'retention' },
  337. { big: '3.1x', label: '交付提速', unit: 'vs 2025' },
  338. ];
  339. const bars = [
  340. { h: 0.45, label: 'Q1' },
  341. { h: 0.62, label: 'Q2' },
  342. { h: 0.78, label: 'Q3' },
  343. { h: 1.00, label: 'Q4', hi: true },
  344. ];
  345. return (
  346. <div style={{padding: '60px 80px 56px', height:'100%', background:'#fff',
  347. display:'flex', flexDirection:'column'}}>
  348. <div style={{fontFamily: mono, fontSize: 11, letterSpacing:'0.3em',
  349. color: TERRA, marginBottom: 10}}>SECTION 02 · NUMBERS</div>
  350. <div style={{fontFamily: serif, fontSize: 56, fontWeight: 500, color: INK,
  351. letterSpacing:'-0.015em', marginBottom: 36}}>
  352. 今年的三个关键数字
  353. </div>
  354. <div style={{display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap: 48,
  355. marginBottom: 40}}>
  356. {numbers.map((n, i) => (
  357. <div key={i}>
  358. <div style={{fontFamily: serif, fontSize: 112, fontWeight: 400,
  359. color: i === 2 ? TERRA : INK, lineHeight: 1, letterSpacing:'-0.02em'}}>
  360. {n.big}
  361. </div>
  362. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 22,
  363. color: INK, marginTop: 10}}>
  364. {n.label}
  365. </div>
  366. <div style={{fontFamily: mono, fontSize: 11, color: ASH,
  367. letterSpacing:'0.2em', marginTop: 4}}>
  368. {n.unit}
  369. </div>
  370. </div>
  371. ))}
  372. </div>
  373. <div style={{flex: 1, display:'flex', alignItems:'flex-end', gap: 20,
  374. paddingLeft: 4, borderTop:`1px solid ${LINE}`, paddingTop: 24}}>
  375. {bars.map((b, i) => (
  376. <div key={i} style={{flex: 1, display:'flex', flexDirection:'column',
  377. alignItems:'center'}}>
  378. <div style={{width:'78%', height: `${b.h * 180}px`,
  379. background: b.hi ? TERRA : INK, marginBottom: 10}} />
  380. <div style={{fontFamily: mono, fontSize: 11, color: ASH,
  381. letterSpacing:'0.2em'}}>{b.label}</div>
  382. </div>
  383. ))}
  384. </div>
  385. </div>
  386. );
  387. }
  388. function QuotePage() {
  389. return (
  390. <div style={{padding: '80px', height:'100%', background:'#faf6ef',
  391. display:'flex', flexDirection:'column', justifyContent:'center',
  392. alignItems:'center', position:'relative'}}>
  393. <div style={{position:'absolute', top: 64, left: 80,
  394. fontFamily: mono, fontSize: 11, letterSpacing:'0.3em', color: TERRA}}>
  395. EPIGRAPH · III
  396. </div>
  397. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 104,
  398. fontWeight: 400, color: INK, lineHeight: 1.15, letterSpacing:'-0.015em',
  399. textAlign:'center', maxWidth: 1100}}>
  400. "Less,<br/>but <span style={{color: TERRA}}>better</span>."
  401. </div>
  402. <div style={{height: 1, background: INK, width: 140, marginTop: 44,
  403. marginBottom: 20}} />
  404. <div style={{fontFamily: serif, fontSize: 22, color: ASH,
  405. letterSpacing:'0.08em'}}>
  406. — Dieter Rams
  407. </div>
  408. </div>
  409. );
  410. }
  411. // ══════════════════════════════════════════════════════════
  412. // Scene 3 (9 – 15s) · 导出流水线
  413. // ══════════════════════════════════════════════════════════
  414. function Scene3_Pipeline() {
  415. const { elapsed } = useSprite();
  416. const titleOp = interpolate(elapsed, [0, 0.6], [0, 1]);
  417. const fadeOut = interpolate(elapsed, [5.6, 6.0], [1, 0]);
  418. const nodes = [
  419. { title: 'HTML Deck', sub: 'source of truth', icon: 'code', delay: 0.4 },
  420. { title: 'html2pptx.js', sub: 'read computedStyle', icon: 'scan', delay: 1.1, hi: true },
  421. { title: 'pptxgenjs', sub: 'assemble objects', icon: 'compose', delay: 1.8 },
  422. { title: 'deck.pptx', sub: 'editable output', icon: 'doc', delay: 2.5 },
  423. ];
  424. const cmdOp = interpolate(elapsed, [3.8, 4.4], [0, 1]);
  425. return (
  426. <div style={{position:'absolute', inset:0, background: CREAM, opacity: fadeOut,
  427. padding: '72px 96px 56px', display:'flex', flexDirection:'column'}}>
  428. <div style={{display:'flex', justifyContent:'space-between',
  429. alignItems:'baseline', opacity: titleOp, marginBottom: 12}}>
  430. <div>
  431. <div style={{fontFamily: mono, fontSize: 11, letterSpacing:'0.3em',
  432. color: TERRA, marginBottom: 10}}>
  433. <span>●</span> SCENE 03 · EXPORT PIPELINE
  434. </div>
  435. <div style={{fontFamily: mono, fontSize: 64, fontWeight: 500,
  436. color: INK, letterSpacing:'0.04em'}}>
  437. 导出流水线
  438. </div>
  439. </div>
  440. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 20,
  441. color: ASH, textAlign:'right', maxWidth: 380, lineHeight: 1.5}}>
  442. 把 DOM 翻译成<br/>
  443. PowerPoint 对象图
  444. </div>
  445. </div>
  446. <div style={{height: 1, background: INK, width: '100%', opacity: titleOp,
  447. marginTop: 28, marginBottom: 48}} />
  448. {/* Pipeline nodes */}
  449. <div style={{display:'flex', alignItems:'stretch', gap: 0, flex: 1,
  450. position:'relative'}}>
  451. {nodes.map((n, i) => {
  452. const op = interpolate(elapsed, [n.delay, n.delay + 0.5], [0, 1]);
  453. const ty = interpolate(elapsed, [n.delay, n.delay + 0.5], [28, 0], Easing.easeOut);
  454. return (
  455. <React.Fragment key={i}>
  456. <div style={{flex: 1, opacity: op, transform: `translateY(${ty}px)`,
  457. background: n.hi ? TERRA : '#fff',
  458. border: `1px solid ${n.hi ? TERRA : LINE}`,
  459. padding:'28px 24px', display:'flex', flexDirection:'column',
  460. color: n.hi ? '#fff' : INK}}>
  461. <div style={{fontFamily: mono, fontSize: 10, letterSpacing:'0.25em',
  462. opacity: n.hi ? 0.85 : 0.5, marginBottom: 18}}>
  463. STEP {String(i+1).padStart(2, '0')}
  464. </div>
  465. <NodeIcon kind={n.icon} hi={n.hi} />
  466. <div style={{fontFamily: mono, fontSize: 20, fontWeight: 500,
  467. marginTop: 20, letterSpacing:'0.01em'}}>
  468. {n.title}
  469. </div>
  470. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 15,
  471. opacity: n.hi ? 0.85 : 0.6, marginTop: 6}}>
  472. {n.sub}
  473. </div>
  474. </div>
  475. {i < nodes.length - 1 && (
  476. <ArrowBetween elapsed={elapsed} startTime={n.delay + 0.4} />
  477. )}
  478. </React.Fragment>
  479. );
  480. })}
  481. </div>
  482. {/* Data flow caption */}
  483. <div style={{marginTop: 36, display:'flex', alignItems:'center', gap: 24,
  484. opacity: interpolate(elapsed, [3.2, 3.8], [0, 1])}}>
  485. <div style={{fontFamily: mono, fontSize: 13, color: ASH,
  486. letterSpacing:'0.05em', flex: 1}}>
  487. <span style={{color: OLIVE}}>DOM node</span> <span style={{color: TERRA}}>→</span>{' '}
  488. <span style={{color: INK}}>{'{ type, text, font, color, x, y }'}</span>
  489. <span style={{color: ASH, margin:'0 14px'}}>·</span>
  490. <span style={{color: TERRA}}>→</span> <span style={{color: INK}}>slide.addText(...) / slide.addShape(...)</span>
  491. </div>
  492. </div>
  493. {/* Command subtitle */}
  494. <div style={{marginTop: 22, opacity: cmdOp,
  495. background:'#1a1a1a', padding:'16px 24px',
  496. borderLeft: `3px solid ${TERRA}`,
  497. display:'flex', alignItems:'center', gap: 16}}>
  498. <div style={{fontFamily: mono, fontSize: 12, color: TERRA,
  499. letterSpacing:'0.2em'}}>$</div>
  500. <div style={{fontFamily: mono, fontSize: 15, color: '#f5f0e6',
  501. letterSpacing:'0.02em'}}>
  502. node export_deck_pptx.mjs deck.html <span style={{color: '#8ca577'}}>--mode editable</span>
  503. </div>
  504. </div>
  505. </div>
  506. );
  507. }
  508. function NodeIcon({ kind, hi }) {
  509. const fg = hi ? '#fff' : INK;
  510. const bg = hi ? 'rgba(255,255,255,0.18)' : 'rgba(0,0,0,0.04)';
  511. if (kind === 'code') {
  512. return (
  513. <div style={{width: 72, height: 72, background: bg,
  514. display:'flex', alignItems:'center', justifyContent:'center'}}>
  515. <svg width="34" height="34" viewBox="0 0 34 34" fill="none">
  516. <path d="M12 10 L5 17 L12 24" stroke={fg} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
  517. <path d="M22 10 L29 17 L22 24" stroke={fg} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
  518. <path d="M19 7 L15 27" stroke={fg} strokeWidth="2" strokeLinecap="round"/>
  519. </svg>
  520. </div>
  521. );
  522. }
  523. if (kind === 'scan') {
  524. return (
  525. <div style={{width: 72, height: 72, background: bg,
  526. display:'flex', alignItems:'center', justifyContent:'center'}}>
  527. <svg width="38" height="38" viewBox="0 0 38 38" fill="none">
  528. <rect x="6" y="6" width="26" height="26" stroke={fg} strokeWidth="2"/>
  529. <line x1="6" y1="15" x2="32" y2="15" stroke={fg} strokeWidth="1.5"/>
  530. <line x1="6" y1="23" x2="32" y2="23" stroke={fg} strokeWidth="1.5"/>
  531. <line x1="15" y1="6" x2="15" y2="32" stroke={fg} strokeWidth="1.5"/>
  532. <line x1="23" y1="6" x2="23" y2="32" stroke={fg} strokeWidth="1.5"/>
  533. <circle cx="19" cy="19" r="3" fill={fg}/>
  534. </svg>
  535. </div>
  536. );
  537. }
  538. if (kind === 'compose') {
  539. return (
  540. <div style={{width: 72, height: 72, background: bg,
  541. display:'flex', alignItems:'center', justifyContent:'center'}}>
  542. <svg width="38" height="38" viewBox="0 0 38 38" fill="none">
  543. <rect x="4" y="4" width="16" height="12" stroke={fg} strokeWidth="2"/>
  544. <rect x="22" y="4" width="12" height="12" stroke={fg} strokeWidth="2"/>
  545. <rect x="4" y="20" width="12" height="14" stroke={fg} strokeWidth="2"/>
  546. <rect x="18" y="20" width="16" height="14" stroke={fg} strokeWidth="2"/>
  547. </svg>
  548. </div>
  549. );
  550. }
  551. // doc
  552. return (
  553. <div style={{width: 72, height: 72, background: bg,
  554. display:'flex', alignItems:'center', justifyContent:'center'}}>
  555. <svg width="34" height="38" viewBox="0 0 34 38" fill="none">
  556. <path d="M6 4 H22 L28 10 V34 H6 Z" stroke={fg} strokeWidth="2" strokeLinejoin="round"/>
  557. <path d="M22 4 V10 H28" stroke={fg} strokeWidth="2" strokeLinejoin="round"/>
  558. <line x1="11" y1="17" x2="23" y2="17" stroke={fg} strokeWidth="1.5"/>
  559. <line x1="11" y1="22" x2="23" y2="22" stroke={fg} strokeWidth="1.5"/>
  560. <line x1="11" y1="27" x2="19" y2="27" stroke={fg} strokeWidth="1.5"/>
  561. </svg>
  562. </div>
  563. );
  564. }
  565. function ArrowBetween({ elapsed, startTime }) {
  566. const reveal = interpolate(elapsed, [startTime, startTime + 0.3], [0, 1]);
  567. return (
  568. <div style={{width: 48, display:'flex', alignItems:'center',
  569. justifyContent:'center', position:'relative'}}>
  570. <svg width="48" height="24" viewBox="0 0 48 24" style={{opacity: reveal}}>
  571. <line x1="0" y1="12" x2={34 * reveal + 8} y2="12" stroke={TERRA} strokeWidth="1.5"/>
  572. {reveal > 0.6 && (
  573. <path d="M38 6 L44 12 L38 18" stroke={TERRA} strokeWidth="1.5" fill="none"
  574. strokeLinecap="round" strokeLinejoin="round"/>
  575. )}
  576. </svg>
  577. </div>
  578. );
  579. }
  580. // ══════════════════════════════════════════════════════════
  581. // Scene 4 (15 – 20s) · 产物:可编辑文本框
  582. // ══════════════════════════════════════════════════════════
  583. function Scene4_PPTEdit() {
  584. const { elapsed } = useSprite();
  585. const fadeIn = interpolate(elapsed, [0, 0.5], [0, 1]);
  586. const pptScale = interpolate(elapsed, [0, 0.8], [0.94, 1], Easing.easeOut);
  587. // Selection bounding box appears at 0.8s, handles animate in staggered
  588. const selectOp = interpolate(elapsed, [0.9, 1.3], [0, 1]);
  589. // Format panel slides in from right at 1.5s
  590. const panelX = interpolate(elapsed, [1.6, 2.4], [80, 0], Easing.easeOut);
  591. const panelOp = interpolate(elapsed, [1.6, 2.4], [0, 1]);
  592. // Caption fades in 2.4s
  593. const captionOp = interpolate(elapsed, [2.4, 3.0], [0, 1]);
  594. // Checkboxes tick in sequentially
  595. const chk1 = elapsed > 3.2 ? 1 : 0;
  596. const chk2 = elapsed > 3.7 ? 1 : 0;
  597. const chk3 = elapsed > 4.2 ? 1 : 0;
  598. const fadeOut = interpolate(elapsed, [4.6, 5.0], [1, 0]);
  599. return (
  600. <div style={{position:'absolute', inset:0, background: CREAM,
  601. opacity: fadeIn * fadeOut,
  602. display:'flex', flexDirection:'column', alignItems:'center',
  603. padding:'60px 60px 40px'}}>
  604. <div style={{width:'100%', display:'flex', justifyContent:'space-between',
  605. alignItems:'baseline', marginBottom: 20}}>
  606. <div>
  607. <div style={{fontFamily: mono, fontSize: 11, letterSpacing:'0.3em',
  608. color: TERRA, marginBottom: 8}}>
  609. <span>●</span> SCENE 04 · THE ARTIFACT
  610. </div>
  611. <div style={{fontFamily: serif, fontSize: 52, fontWeight: 500, color: INK,
  612. letterSpacing:'-0.01em'}}>
  613. 产物:可编辑文本框
  614. </div>
  615. </div>
  616. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18, color: ASH,
  617. textAlign:'right', maxWidth: 340, lineHeight: 1.5}}>
  618. 在 PowerPoint 里<br/>
  619. 像素级复现,字还是字
  620. </div>
  621. </div>
  622. <div style={{position:'relative', transform: `scale(${pptScale})`,
  623. transformOrigin:'center center'}}>
  624. <PPTMockup selectOp={selectOp} />
  625. {/* Format panel */}
  626. <div style={{position:'absolute', top: 94, right: -296,
  627. width: 272, background:'#f5f2ed', border:`1px solid ${LINE}`,
  628. boxShadow:'0 12px 30px rgba(0,0,0,0.08)',
  629. transform: `translateX(${panelX}px)`, opacity: panelOp,
  630. padding: 0}}>
  631. <FormatPanel />
  632. </div>
  633. </div>
  634. <div style={{marginTop: 28, display:'flex', alignItems:'center', gap: 48,
  635. opacity: captionOp}}>
  636. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 24,
  637. color: TERRA, letterSpacing:'0.01em'}}>
  638. 原生 PowerPoint 文本框 · 不是图片
  639. </div>
  640. <div style={{display:'flex', gap: 28, fontFamily: mono, fontSize: 13}}>
  641. <CheckRow label="文字可编辑" on={chk1} />
  642. <CheckRow label="字体保留" on={chk2} />
  643. <CheckRow label="位置/颜色精确" on={chk3} />
  644. </div>
  645. </div>
  646. </div>
  647. );
  648. }
  649. function CheckRow({ label, on }) {
  650. return (
  651. <div style={{display:'flex', alignItems:'center', gap: 8}}>
  652. <div style={{width: 18, height: 18, border:`1.5px solid ${on ? TERRA : LINE}`,
  653. background: on ? TERRA : 'transparent',
  654. display:'flex', alignItems:'center', justifyContent:'center',
  655. transition:'none'}}>
  656. {on ? (
  657. <svg width="12" height="12" viewBox="0 0 12 12">
  658. <path d="M2 6 L5 9 L10 3" stroke="#fff" strokeWidth="2" fill="none"
  659. strokeLinecap="round" strokeLinejoin="round"/>
  660. </svg>
  661. ) : null}
  662. </div>
  663. <span style={{color: on ? INK : ASH}}>{label}</span>
  664. </div>
  665. );
  666. }
  667. function PPTMockup({ selectOp }) {
  668. const W = 1100, H = 620;
  669. return (
  670. <div style={{width: W, height: H, background:'#f4f1ec',
  671. border:`1px solid ${LINE}`, boxShadow:'0 22px 50px rgba(0,0,0,0.14)',
  672. display:'flex', flexDirection:'column'}}>
  673. {/* PPT ribbon (title bar + tabs) */}
  674. <div style={{height: 32, background:'#dcd7cd', display:'flex',
  675. alignItems:'center', padding:'0 14px', gap: 8,
  676. borderBottom:`1px solid ${LINE}`}}>
  677. <div style={{width:10, height:10, borderRadius:'50%', background:'#ff5f57'}} />
  678. <div style={{width:10, height:10, borderRadius:'50%', background:'#febc2e'}} />
  679. <div style={{width:10, height:10, borderRadius:'50%', background:'#28c840'}} />
  680. <div style={{flex: 1, textAlign:'center', fontFamily: sans, fontSize: 11,
  681. color: ASH, letterSpacing:'0.02em'}}>
  682. deck.pptx — PowerPoint
  683. </div>
  684. </div>
  685. <div style={{height: 34, background:'#ebe7de', display:'flex',
  686. alignItems:'center', padding:'0 18px', gap: 22,
  687. fontFamily: sans, fontSize: 11, color: INK,
  688. borderBottom:`1px solid ${LINE}`}}>
  689. <span style={{color: TERRA, fontWeight: 600,
  690. borderBottom: `2px solid ${TERRA}`, paddingBottom: 6,
  691. marginBottom: -7}}>Home</span>
  692. <span style={{opacity: 0.55}}>Insert</span>
  693. <span style={{opacity: 0.55}}>Design</span>
  694. <span style={{opacity: 0.55}}>Transitions</span>
  695. <span style={{opacity: 0.55}}>Animations</span>
  696. <span style={{opacity: 0.55}}>Slide Show</span>
  697. <span style={{opacity: 0.55}}>Review</span>
  698. <span style={{opacity: 0.55}}>View</span>
  699. </div>
  700. {/* Body: slide panel (left) + slide canvas (main) */}
  701. <div style={{flex: 1, display:'flex'}}>
  702. {/* Thumbnails */}
  703. <div style={{width: 160, background:'#eae5db',
  704. borderRight:`1px solid ${LINE}`, padding:'12px 12px',
  705. display:'flex', flexDirection:'column', gap: 8}}>
  706. {[0,1,2,3].map(i => (
  707. <div key={i} style={{
  708. background:'#fff',
  709. border: i === 2 ? `2px solid ${TERRA}` : `1px solid ${LINE}`,
  710. aspectRatio:'16/9', position:'relative',
  711. padding: 8, display:'flex', alignItems:'center',
  712. justifyContent:'center'}}>
  713. <div style={{position:'absolute', top: 4, left: 4,
  714. fontFamily: mono, fontSize: 8, color: ASH}}>{i+1}</div>
  715. {i === 2 && (
  716. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 12,
  717. color: INK}}>"Less..."</div>
  718. )}
  719. {i !== 2 && (
  720. <div style={{width:'70%', height: 3, background: LINE}} />
  721. )}
  722. </div>
  723. ))}
  724. </div>
  725. {/* Slide canvas */}
  726. <div style={{flex: 1, background:'#e8e4dc', display:'flex',
  727. alignItems:'center', justifyContent:'center', padding: 32,
  728. position:'relative'}}>
  729. <div style={{width: 720, height: 405, background:'#faf6ef',
  730. boxShadow:'0 8px 24px rgba(0,0,0,0.1)',
  731. border:`1px solid ${LINE}`, position:'relative'}}>
  732. {/* The editable text box */}
  733. <div style={{position:'absolute', top:'50%', left:'50%',
  734. transform:'translate(-50%, -50%)', textAlign:'center',
  735. padding:'18px 40px'}}>
  736. <div style={{fontFamily: serif, fontStyle:'italic',
  737. fontSize: 72, color: INK, lineHeight: 1.1,
  738. letterSpacing:'-0.01em'}}>
  739. "Less, but <span style={{color: TERRA}}>better</span>."
  740. </div>
  741. <div style={{fontFamily: serif, fontSize: 14, color: ASH,
  742. marginTop: 14, letterSpacing:'0.1em'}}>
  743. — Dieter Rams
  744. </div>
  745. </div>
  746. {/* Selection bounding box + 8 handles */}
  747. {selectOp > 0 && <SelectionBox opacity={selectOp} />}
  748. {/* slide number */}
  749. <div style={{position:'absolute', bottom: 10, right: 14,
  750. fontFamily: sans, fontSize: 9, color: ASH}}>3</div>
  751. </div>
  752. </div>
  753. </div>
  754. </div>
  755. );
  756. }
  757. function SelectionBox({ opacity }) {
  758. // Box centered around the textbox (~ 520×160)
  759. const BW = 560, BH = 170;
  760. const color = '#4a9eff';
  761. const handles = [
  762. { x: 0, y: 0 }, { x: 0.5, y: 0 }, { x: 1, y: 0 },
  763. { x: 0, y: 0.5 }, { x: 1, y: 0.5 },
  764. { x: 0, y: 1 }, { x: 0.5, y: 1 }, { x: 1, y: 1 },
  765. ];
  766. return (
  767. <div style={{position:'absolute', top:'50%', left:'50%',
  768. width: BW, height: BH, transform:'translate(-50%, -50%)',
  769. border: `1.5px solid ${color}`, opacity,
  770. boxShadow:`0 0 0 1px rgba(255,255,255,0.6)`, pointerEvents:'none'}}>
  771. {handles.map((h, i) => (
  772. <div key={i} style={{position:'absolute',
  773. left: `${h.x * 100}%`, top: `${h.y * 100}%`,
  774. transform:'translate(-50%, -50%)',
  775. width: 10, height: 10, background:'#fff',
  776. border: `1.5px solid ${color}`, borderRadius: 2}} />
  777. ))}
  778. {/* Rotate handle */}
  779. <div style={{position:'absolute', left:'50%', top: -26,
  780. transform:'translateX(-50%)',
  781. width: 10, height: 10, background:'#fff',
  782. border: `1.5px solid ${color}`, borderRadius: '50%'}} />
  783. <div style={{position:'absolute', left:'50%', top: -17,
  784. transform:'translateX(-50%)', width: 1, height: 9, background: color}} />
  785. {/* Label: Text Box */}
  786. <div style={{position:'absolute', top: -26, left: 0,
  787. fontFamily: mono, fontSize: 10, color: color,
  788. background:'rgba(255,255,255,0.9)', padding:'2px 6px',
  789. letterSpacing:'0.1em'}}>
  790. TEXT BOX · shape #1
  791. </div>
  792. </div>
  793. );
  794. }
  795. function FormatPanel() {
  796. return (
  797. <div>
  798. <div style={{padding:'12px 16px', borderBottom:`1px solid ${LINE}`,
  799. display:'flex', justifyContent:'space-between', alignItems:'center',
  800. background:'#ebe7de'}}>
  801. <div style={{fontFamily: sans, fontSize: 12, color: INK,
  802. fontWeight: 600}}>Format Text</div>
  803. <div style={{fontFamily: mono, fontSize: 10, color: ASH}}>✕</div>
  804. </div>
  805. <div style={{padding:'16px'}}>
  806. <div style={{fontFamily: sans, fontSize: 10, color: ASH,
  807. letterSpacing:'0.15em', marginBottom: 8}}>FONT</div>
  808. <div style={{background:'#fff', border:`1px solid ${LINE}`,
  809. padding:'8px 10px', marginBottom: 14,
  810. display:'flex', justifyContent:'space-between', alignItems:'center',
  811. fontFamily: sans, fontSize: 12, color: INK}}>
  812. <span style={{fontFamily: serif, fontStyle:'italic'}}>Newsreader</span>
  813. <span style={{color: ASH, fontSize: 10}}>▾</span>
  814. </div>
  815. <div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap: 10,
  816. marginBottom: 14}}>
  817. <div>
  818. <div style={{fontFamily: sans, fontSize: 10, color: ASH,
  819. letterSpacing:'0.15em', marginBottom: 6}}>SIZE</div>
  820. <div style={{background:'#fff', border:`1px solid ${LINE}`,
  821. padding:'6px 10px', fontFamily: sans, fontSize: 12}}>72 pt</div>
  822. </div>
  823. <div>
  824. <div style={{fontFamily: sans, fontSize: 10, color: ASH,
  825. letterSpacing:'0.15em', marginBottom: 6}}>WEIGHT</div>
  826. <div style={{background:'#fff', border:`1px solid ${LINE}`,
  827. padding:'6px 10px', fontFamily: sans, fontSize: 12}}>400 · italic</div>
  828. </div>
  829. </div>
  830. <div style={{fontFamily: sans, fontSize: 10, color: ASH,
  831. letterSpacing:'0.15em', marginBottom: 6}}>COLOR</div>
  832. <div style={{display:'flex', gap: 8, marginBottom: 14}}>
  833. <div style={{width: 28, height: 28, background: INK, border:`2px solid ${INK}`}} />
  834. <div style={{width: 28, height: 28, background: TERRA,
  835. outline:`2px solid ${TERRA}`, outlineOffset: 1}} />
  836. <div style={{width: 28, height: 28, background: OLIVE}} />
  837. <div style={{width: 28, height: 28, background: DEEP_BLUE}} />
  838. <div style={{width: 28, height: 28, background:'#fff', border:`1px solid ${LINE}`}} />
  839. </div>
  840. <div style={{fontFamily: mono, fontSize: 10, color: ASH,
  841. letterSpacing:'0.1em', lineHeight: 1.6,
  842. paddingTop: 10, borderTop:`1px solid ${LINE}`}}>
  843. x: 2.4in · y: 2.1in<br/>
  844. w: 5.8in · h: 1.7in
  845. </div>
  846. </div>
  847. </div>
  848. );
  849. }
  850. // ══════════════════════════════════════════════════════════
  851. // Scene 5 (20 – 24s) · 收尾
  852. // ══════════════════════════════════════════════════════════
  853. function Scene5_Final() {
  854. const { elapsed } = useSprite();
  855. const tagOp = interpolate(elapsed, [0, 0.5], [0, 1]);
  856. const mainY = interpolate(elapsed, [0.2, 1.2], [50, 0], Easing.easeOut);
  857. const mainOp = interpolate(elapsed, [0.2, 1.0], [0, 1]);
  858. const lineW = interpolate(elapsed, [1.1, 1.8], [0, 540]);
  859. const subOp = interpolate(elapsed, [1.5, 2.2], [0, 1]);
  860. const monoOp = interpolate(elapsed, [2.2, 2.8], [0, 1]);
  861. return (
  862. <div style={{position:'absolute', inset:0, background: CREAM,
  863. display:'flex', alignItems:'center', justifyContent:'center',
  864. flexDirection:'column'}}>
  865. <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.4em',
  866. color: TERRA, marginBottom: 32, opacity: tagOp}}>
  867. ONE SOURCE · TWO STATES
  868. </div>
  869. <div style={{fontFamily: serif, fontSize: 220, fontWeight: 500,
  870. color: INK, lineHeight: 0.98, letterSpacing:'-0.03em',
  871. opacity: mainOp, transform: `translateY(${mainY}px)`}}>
  872. 一<span style={{color: ASH, fontStyle:'italic'}}>源</span>
  873. <span style={{color: TERRA, margin:'0 28px'}}>·</span>
  874. 双<span style={{color: ASH, fontStyle:'italic'}}>态</span>
  875. </div>
  876. <div style={{height: 1, background: INK, width: lineW, marginTop: 46}} />
  877. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 28,
  878. color: ASH, marginTop: 28, opacity: subOp, letterSpacing:'0.02em'}}>
  879. 浏览器里演讲 · PowerPoint 里二次编辑
  880. </div>
  881. <div style={{fontFamily: mono, fontSize: 18, color: INK, marginTop: 34,
  882. opacity: monoOp, letterSpacing:'0.1em',
  883. padding:'12px 28px', background:'#fff', border:`1px solid ${LINE}`}}>
  884. <span style={{color: OLIVE}}>deck.html</span>
  885. <span style={{color: TERRA, margin:'0 14px'}}>⇌</span>
  886. <span style={{color: DEEP_BLUE}}>deck.pptx</span>
  887. </div>
  888. </div>
  889. );
  890. }
  891. // ── Watermark ─────────────────────────────────────────────
  892. function Watermark() {
  893. return (
  894. <div style={{position:'absolute', bottom: 24, right: 32,
  895. fontSize: 11, color: 'rgba(0,0,0,0.38)', letterSpacing:'0.15em',
  896. fontFamily: mono, pointerEvents:'none', zIndex: 100}}>
  897. Created by Huashu-Design
  898. </div>
  899. );
  900. }
  901. // ── Main composition ──────────────────────────────────────
  902. function App() {
  903. return (
  904. <Stage duration={24} width={1920} height={1080} bgColor={CREAM}>
  905. <Sprite start={0} end={3}><Scene1_Title /></Sprite>
  906. <Sprite start={3} end={9}><Scene2_DeckFlip /></Sprite>
  907. <Sprite start={9} end={15}><Scene3_Pipeline /></Sprite>
  908. <Sprite start={15} end={20}><Scene4_PPTEdit /></Sprite>
  909. <Sprite start={20} end={24}><Scene5_Final /></Sprite>
  910. <Watermark />
  911. </Stage>
  912. );
  913. }
  914. ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  915. </script>
  916. </body>
  917. </html>