w2-junior-designer.html 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Huashu-Design · Junior Designer 模式</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: Two working modes contrast (0 – 3s) ──────────
  158. function Scene1_Contrast() {
  159. const { elapsed } = useSprite();
  160. const titleOp = interpolate(elapsed, [0, 0.6], [0, 1]);
  161. const leftOp = interpolate(elapsed, [0.3, 1], [0, 1]);
  162. const rightOp = interpolate(elapsed, [0.5, 1.2], [0, 1]);
  163. // Left: long bar filling slowly, then user frown at end
  164. const badBar = Math.min(1, Math.max(0, (elapsed - 1.0) / 1.6));
  165. const badReveal = elapsed > 2.5 ? 1 : 0;
  166. // Right: small milestones ticking one by one
  167. const ticks = [0.9, 1.3, 1.7, 2.1, 2.5];
  168. const fadeOut = interpolate(elapsed, [2.85, 3], [1, 0], Easing.easeIn);
  169. return (
  170. <div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeOut,
  171. padding:'80px 120px', display:'flex', flexDirection:'column'}}>
  172. {/* Title */}
  173. <div style={{textAlign:'center', marginBottom: 60, opacity: titleOp}}>
  174. <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.4em',
  175. color: TERRA, marginBottom: 16}}>
  176. JUNIOR DESIGNER MODE · 工作方式对比
  177. </div>
  178. <div style={{fontFamily: serif, fontSize: 68, fontWeight: 500,
  179. color: INK, lineHeight: 1.1, letterSpacing:'-0.01em'}}>
  180. 理解错了 <span style={{fontStyle:'italic', color: TERRA}}>早改</span> 比 <span style={{fontStyle:'italic', color: TERRA}}>晚改</span> 便宜 100 倍
  181. </div>
  182. </div>
  183. {/* Two columns */}
  184. <div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap: 60, flex: 1}}>
  185. {/* Left: bad mode */}
  186. <div style={{opacity: leftOp, background:'#f4eee3', padding:'40px 44px',
  187. display:'flex', flexDirection:'column', position:'relative'}}>
  188. <div style={{display:'flex', alignItems:'center', gap: 14, marginBottom: 28}}>
  189. <div style={{width: 32, height: 32, borderRadius:'50%',
  190. background:'#ccc', color:'#fff', fontFamily: serif, fontSize: 20,
  191. fontWeight: 600, display:'flex', alignItems:'center',
  192. justifyContent:'center', lineHeight: 1}}>✗</div>
  193. <div style={{fontFamily: serif, fontSize: 30, fontWeight: 500,
  194. color:'#555'}}>闷头做大招</div>
  195. </div>
  196. <div style={{fontFamily: mono, fontSize: 11, color: ASH,
  197. letterSpacing: '0.2em', marginBottom: 14}}>
  198. 6 HOURS · 0 FEEDBACK
  199. </div>
  200. <div style={{height: 18, background:'#e2dbcd', position:'relative',
  201. marginBottom: 36}}>
  202. <div style={{position:'absolute', top:0, left:0, height:'100%',
  203. width: `${badBar * 100}%`, background:'#999'}} />
  204. </div>
  205. <div style={{fontFamily: serif, fontSize: 16, color: ASH,
  206. lineHeight: 1.7, flex: 1}}>
  207. 埋头 6 小时<br/>
  208. 一次性端出成品<br/>
  209. 默祷「方向没理解错」
  210. </div>
  211. {badReveal > 0 && (
  212. <div style={{marginTop: 20, padding:'14px 18px', background:'#fff',
  213. border: `1px solid #d0c8b8`, opacity: badReveal,
  214. display:'flex', alignItems:'center', gap: 12}}>
  215. <div style={{fontFamily: serif, fontSize: 22, color:'#888'}}>😐</div>
  216. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 15,
  217. color:'#666', lineHeight: 1.4}}>
  218. 「这方向不对…<br/>得重来」
  219. </div>
  220. </div>
  221. )}
  222. </div>
  223. {/* Right: good mode */}
  224. <div style={{opacity: rightOp, background:'#fff', padding:'40px 44px',
  225. border: `1.5px solid ${TERRA}`, display:'flex', flexDirection:'column',
  226. position:'relative'}}>
  227. <div style={{display:'flex', alignItems:'center', gap: 14, marginBottom: 28}}>
  228. <div style={{width: 32, height: 32, borderRadius:'50%',
  229. background: TERRA, color:'#fff', fontFamily: serif, fontSize: 20,
  230. fontWeight: 600, display:'flex', alignItems:'center',
  231. justifyContent:'center', lineHeight: 1}}>✓</div>
  232. <div style={{fontFamily: serif, fontSize: 30, fontWeight: 500,
  233. color: INK}}>Junior Designer 模式</div>
  234. </div>
  235. <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
  236. letterSpacing: '0.2em', marginBottom: 14}}>
  237. 5 CHECKPOINTS · CONTINUOUS FEEDBACK
  238. </div>
  239. {/* Timeline with ticks */}
  240. <div style={{position:'relative', height: 18, marginBottom: 36}}>
  241. <div style={{position:'absolute', top: 8, left: 0, right: 0,
  242. height: 2, background:'#e8e1d3'}} />
  243. {ticks.map((tick, i) => {
  244. const show = elapsed > tick ? 1 : 0;
  245. const pct = (i + 1) / ticks.length * 100;
  246. return (
  247. <div key={i} style={{position:'absolute',
  248. left: `calc(${pct}% - 10px)`, top: 0, width: 20, height: 20,
  249. borderRadius:'50%', background: TERRA, color:'#fff',
  250. fontSize: 11, display:'flex', alignItems:'center',
  251. justifyContent:'center', opacity: show,
  252. transform: `scale(${show})`, transition:'none',
  253. fontFamily: sans, fontWeight: 600}}>✓</div>
  254. );
  255. })}
  256. </div>
  257. <div style={{fontFamily: serif, fontSize: 16, color: INK,
  258. lineHeight: 1.7, flex: 1}}>
  259. 假设 · 骨架 · 粗图<br/>
  260. 每步都 show 一下<br/>
  261. 错得早,改得小
  262. </div>
  263. {elapsed > 2.5 && (
  264. <div style={{marginTop: 20, padding:'14px 18px', background:'#faf6ef',
  265. border: `1px solid ${LINE}`, display:'flex', alignItems:'center', gap: 12,
  266. opacity: interpolate(elapsed, [2.5, 2.85], [0, 1])}}>
  267. <div style={{fontFamily: serif, fontSize: 22}}>🙂</div>
  268. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 15,
  269. color: INK, lineHeight: 1.4}}>
  270. 「方向对,<br/>继续推进」
  271. </div>
  272. </div>
  273. )}
  274. </div>
  275. </div>
  276. </div>
  277. );
  278. }
  279. // ── Scene 2: HTML skeleton with assumptions + placeholders (3 – 8s) ──
  280. function Scene2_Skeleton() {
  281. const { elapsed } = useSprite();
  282. const opacity = interpolate(elapsed, [0, 0.5], [0, 1]);
  283. const fadeOut = interpolate(elapsed, [4.6, 5], [1, 0]);
  284. // Typewriter content
  285. const fullText = `<!--
  286. ASSUMPTIONS:
  287. - 主色用暖调橙(未确认,等品牌 spec)
  288. - 3 屏切换,Today / Memory / Chat
  289. - 字体:Newsreader + Noto Serif SC
  290. PLACEHOLDERS:
  291. - Hero 图暂用灰块 + 文字标签(有图再替换)
  292. - AI 洞察文本用「等用户提供真实数据」
  293. REASONING:
  294. - 选橙调是因为客户说要「温暖感」
  295. - Serif 因为是阅读类 App
  296. -->`;
  297. // Type at ~50 chars/sec
  298. const charCount = Math.floor(interpolate(elapsed, [0.4, 4.0], [0, fullText.length]));
  299. const shownText = fullText.slice(0, charCount);
  300. const cursorBlink = Math.floor(elapsed * 2.4) % 2 === 0 && charCount < fullText.length;
  301. // Hint callout appears after most text typed
  302. const hintOp = interpolate(elapsed, [3.5, 4.2], [0, 1]);
  303. const hintBob = Math.sin(elapsed * 3) * 4;
  304. return (
  305. <div style={{position:'absolute', inset:0, background:CREAM, opacity: opacity * fadeOut,
  306. padding:'60px 100px', display:'flex', flexDirection:'column'}}>
  307. {/* Header */}
  308. <div style={{display:'flex', justifyContent:'space-between',
  309. alignItems:'baseline', marginBottom: 32}}>
  310. <div>
  311. <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
  312. letterSpacing: '0.3em', marginBottom: 6}}>STEP 1 / 3</div>
  313. <div style={{fontFamily: serif, fontSize: 52, fontWeight: 500, color: INK}}>
  314. 先写 <span style={{fontStyle:'italic', color: TERRA}}>骨架</span> · 不写渲染
  315. </div>
  316. </div>
  317. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18,
  318. color: ASH, textAlign: 'right', lineHeight: 1.5}}>
  319. Assumptions + Placeholders + Reasoning<br/>
  320. <span style={{fontSize: 14}}>— 把「猜」写在代码里</span>
  321. </div>
  322. </div>
  323. {/* Editor */}
  324. <div style={{flex: 1, background:'#1a1a1a', position:'relative',
  325. display:'flex', flexDirection:'column', overflow:'hidden'}}>
  326. {/* Editor top bar */}
  327. <div style={{padding:'12px 20px', background:'#222',
  328. borderBottom:'1px solid #2e2e2e', display:'flex',
  329. alignItems:'center', gap: 16}}>
  330. <div style={{display:'flex', gap: 8}}>
  331. <div style={{width: 12, height: 12, borderRadius:'50%', background:'#ff5f57'}} />
  332. <div style={{width: 12, height: 12, borderRadius:'50%', background:'#febc2e'}} />
  333. <div style={{width: 12, height: 12, borderRadius:'50%', background:'#28c840'}} />
  334. </div>
  335. <div style={{fontFamily: mono, fontSize: 11, color:'#888',
  336. letterSpacing: '0.1em'}}>
  337. index.html · junior-pass-v0
  338. </div>
  339. </div>
  340. {/* Code body */}
  341. <div style={{flex: 1, padding:'32px 40px', fontFamily: mono,
  342. fontSize: 19, lineHeight: 1.7, color:'#8a8a8a',
  343. whiteSpace:'pre-wrap', position:'relative'}}>
  344. <span style={{color:'#6a8a56'}}>{shownText}</span>
  345. {cursorBlink && <span style={{background:'#c04a1a', display:'inline-block',
  346. width: 10, height: 22, verticalAlign:'text-bottom'}} />}
  347. </div>
  348. {/* Bottom status bar */}
  349. <div style={{padding:'10px 20px', background:'#161616',
  350. borderTop:'1px solid #2a2a2a', display:'flex',
  351. justifyContent:'space-between', fontFamily: mono, fontSize: 10,
  352. color:'#666', letterSpacing:'0.12em'}}>
  353. <span>HTML · COMMENTS ONLY</span>
  354. <span>LN {Math.min(15, Math.floor(charCount / 30) + 1)} / 15</span>
  355. </div>
  356. </div>
  357. {/* Callout */}
  358. <div style={{position:'absolute', right: 120, bottom: 100,
  359. opacity: hintOp, transform: `translateY(${hintBob}px)`,
  360. display:'flex', alignItems:'center', gap: 14}}>
  361. <svg width="48" height="24" viewBox="0 0 48 24">
  362. <path d="M 46 12 L 6 12 M 14 6 L 6 12 L 14 18"
  363. fill="none" stroke={TERRA} strokeWidth="2" strokeLinecap="round"/>
  364. </svg>
  365. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 22,
  366. color: TERRA, letterSpacing:'0.02em'}}>
  367. 还没写一行渲染代码
  368. </div>
  369. </div>
  370. </div>
  371. );
  372. }
  373. // ── Scene 3: First show to user (8 – 13s) ─────────────────
  374. function Scene3_FirstShow() {
  375. const { elapsed } = useSprite();
  376. const opacity = interpolate(elapsed, [0, 0.5], [0, 1]);
  377. const fadeOut = interpolate(elapsed, [4.6, 5], [1, 0]);
  378. // Wireframe fade in
  379. const wireOp = interpolate(elapsed, [0.2, 0.9], [0, 1]);
  380. // Chat bubbles stagger in
  381. const b1Op = interpolate(elapsed, [1.0, 1.5], [0, 1]);
  382. const b2Op = interpolate(elapsed, [2.0, 2.5], [0, 1]);
  383. const b3Op = interpolate(elapsed, [2.9, 3.4], [0, 1]);
  384. // Bottom subtitle
  385. const subOp = interpolate(elapsed, [3.8, 4.3], [0, 1]);
  386. return (
  387. <div style={{position:'absolute', inset:0, background:CREAM, opacity: opacity * fadeOut,
  388. padding:'60px 100px', display:'flex', flexDirection:'column'}}>
  389. {/* Header */}
  390. <div style={{display:'flex', justifyContent:'space-between',
  391. alignItems:'baseline', marginBottom: 28}}>
  392. <div>
  393. <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
  394. letterSpacing: '0.3em', marginBottom: 6}}>STEP 2 / 3</div>
  395. <div style={{fontFamily: serif, fontSize: 52, fontWeight: 500, color: INK}}>
  396. 第一次 <span style={{fontStyle:'italic', color: TERRA}}>show</span> 给用户
  397. </div>
  398. </div>
  399. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18,
  400. color: ASH, textAlign:'right', lineHeight: 1.5}}>
  401. 「先给你看方向 —— 对吗?」<br/>
  402. <span style={{fontSize: 14}}>— 10 分钟对齐</span>
  403. </div>
  404. </div>
  405. {/* Two panels */}
  406. <div style={{display:'grid', gridTemplateColumns:'1fr 1px 1.1fr', gap: 40,
  407. flex: 1, alignItems:'stretch'}}>
  408. {/* Left: wireframe */}
  409. <div style={{background:'#fff', border: `1px solid ${LINE}`,
  410. padding: 28, opacity: wireOp, display:'flex', flexDirection:'column'}}>
  411. <div style={{fontFamily: mono, fontSize: 10, color: ASH,
  412. letterSpacing:'0.2em', marginBottom: 14}}>
  413. WIREFRAME · NO STYLING
  414. </div>
  415. {/* Hero placeholder */}
  416. <div style={{height: 180, background:'#ececec', border:'1.5px dashed #bbb',
  417. display:'flex', alignItems:'center', justifyContent:'center',
  418. fontFamily: mono, fontSize: 13, color:'#888',
  419. letterSpacing:'0.15em', marginBottom: 20}}>
  420. [ HERO IMAGE ]
  421. </div>
  422. {/* Title placeholder */}
  423. <div style={{height: 26, background:'#e5e0d3', width:'75%',
  424. marginBottom: 10}} />
  425. <div style={{height: 16, background:'#ede8db', width:'55%',
  426. marginBottom: 24}} />
  427. {/* AI insight block */}
  428. <div style={{background:'#f5f0e3', padding:'16px 18px', marginBottom: 20,
  429. border: `1px dashed ${LINE}`}}>
  430. <div style={{fontFamily: mono, fontSize: 9, color: ASH,
  431. letterSpacing:'0.2em', marginBottom: 8}}>[ AI INSIGHT · 3 行 ]</div>
  432. <div style={{height: 8, background:'#d9d2c5', width:'92%', marginBottom: 6}} />
  433. <div style={{height: 8, background:'#d9d2c5', width:'88%', marginBottom: 6}} />
  434. <div style={{height: 8, background:'#d9d2c5', width:'70%'}} />
  435. </div>
  436. <div style={{fontFamily: serif, fontSize: 14, color:'#888',
  437. fontStyle:'italic', marginTop:'auto', textAlign:'center',
  438. paddingTop: 14, borderTop: `1px solid ${LINE}`}}>
  439. Marcus Aurelius · 第四卷
  440. </div>
  441. </div>
  442. {/* Divider */}
  443. <div style={{background: LINE, width: 1}} />
  444. {/* Right: chat */}
  445. <div style={{display:'flex', flexDirection:'column', gap: 18,
  446. paddingTop: 10}}>
  447. {/* Designer bubble */}
  448. <div style={{opacity: b1Op, display:'flex', gap: 14,
  449. alignItems:'flex-start'}}>
  450. <div style={{width: 40, height: 40, background: TERRA, color:'#fff',
  451. fontFamily: serif, fontSize: 16, fontWeight: 600,
  452. display:'flex', alignItems:'center', justifyContent:'center',
  453. flexShrink: 0, borderRadius: 2, letterSpacing:'0.05em'}}>JD</div>
  454. <div style={{background:'#fff', border: `1px solid ${LINE}`,
  455. padding:'16px 20px', maxWidth: '85%'}}>
  456. <div style={{fontFamily: mono, fontSize: 9, color: ASH,
  457. letterSpacing:'0.2em', marginBottom: 8}}>
  458. JUNIOR DESIGNER
  459. </div>
  460. <div style={{fontFamily: serif, fontSize: 19, color: INK,
  461. lineHeight: 1.5}}>
  462. 我对方向有几个假设,先给你看线框——想法对吗?
  463. </div>
  464. </div>
  465. </div>
  466. {/* User bubble 1 */}
  467. <div style={{opacity: b2Op, display:'flex', gap: 14,
  468. alignItems:'flex-start', justifyContent:'flex-end'}}>
  469. <div style={{background: OLIVE, color:'#fff',
  470. padding:'14px 20px', maxWidth: '80%'}}>
  471. <div style={{fontFamily: mono, fontSize: 9,
  472. color:'rgba(255,255,255,0.6)', letterSpacing:'0.2em',
  473. marginBottom: 8}}>USER · FEEDBACK</div>
  474. <div style={{fontFamily: serif, fontSize: 18, lineHeight: 1.5}}>
  475. 橙调 OK。AI 洞察那块我想要 <span style={{fontStyle:'italic',
  476. textDecoration:'underline'}}>两行</span> 不要三行
  477. </div>
  478. </div>
  479. </div>
  480. {/* User bubble 2 */}
  481. <div style={{opacity: b3Op, display:'flex', gap: 14,
  482. alignItems:'flex-start', justifyContent:'flex-end'}}>
  483. <div style={{background: OLIVE, color:'#fff',
  484. padding:'14px 20px', maxWidth: '80%'}}>
  485. <div style={{fontFamily: serif, fontSize: 18, lineHeight: 1.5}}>
  486. Hero 图要 <span style={{fontStyle:'italic',
  487. textDecoration:'underline'}}>摄影</span> 不要插画
  488. </div>
  489. </div>
  490. </div>
  491. {/* Subtitle */}
  492. <div style={{marginTop:'auto', paddingTop: 24, opacity: subOp,
  493. borderTop: `1px solid ${LINE}`}}>
  494. <div style={{fontFamily: mono, fontSize: 11,
  495. color: TERRA, letterSpacing:'0.3em', marginBottom: 6}}>
  496. ALIGNMENT
  497. </div>
  498. <div style={{fontFamily: serif, fontSize: 30, fontWeight: 500,
  499. color: INK, lineHeight: 1.2}}>
  500. 对齐 = <span style={{color: TERRA}}>10 分钟</span>
  501. <span style={{fontStyle:'italic', color: ASH, fontSize: 22}}> · 不是 2 小时</span>
  502. </div>
  503. </div>
  504. </div>
  505. </div>
  506. </div>
  507. );
  508. }
  509. // ── ArtBlock: oil painting hero (from c1-ios-prototype) ───
  510. function ArtBlock({ mood = 'warm' }) {
  511. const palettes = {
  512. warm: ['#8b4a2b', '#c67b4a', '#e3a876', '#f2d4a7'],
  513. quiet: ['#3d4a3a', '#6a8066', '#a8b89c', '#e0d8b8'],
  514. };
  515. const p = palettes[mood];
  516. return (
  517. <div style={{
  518. width:'100%', height:'100%', position:'relative', overflow:'hidden',
  519. background: `linear-gradient(135deg, ${p[0]} 0%, ${p[1]} 35%, ${p[2]} 70%, ${p[3]} 100%)`,
  520. }}>
  521. <div style={{
  522. position:'absolute', inset: 0,
  523. background: `
  524. radial-gradient(ellipse 80px 30px at 30% 40%, ${p[3]}44, transparent 70%),
  525. radial-gradient(ellipse 60px 20px at 70% 60%, ${p[0]}33, transparent 70%),
  526. radial-gradient(ellipse 100px 40px at 50% 80%, ${p[2]}44, transparent 70%),
  527. radial-gradient(ellipse 50px 25px at 20% 70%, ${p[1]}55, transparent 70%)
  528. `,
  529. filter:'blur(1px)',
  530. }} />
  531. <svg width="100%" height="100%" style={{position:'absolute', inset:0, opacity: 0.18}}>
  532. <filter id="paint-noise-w2">
  533. <feTurbulence baseFrequency="0.9" numOctaves="2" />
  534. <feColorMatrix values="0 0 0 0 0.3 0 0 0 0 0.2 0 0 0 0 0.1 0 0 0 1 0" />
  535. </filter>
  536. <rect width="100%" height="100%" filter="url(#paint-noise-w2)" />
  537. </svg>
  538. </div>
  539. );
  540. }
  541. // ── Scene 4: Full pass fills in (13 – 18s) ────────────────
  542. function Scene4_FullPass() {
  543. const { elapsed } = useSprite();
  544. const opacity = interpolate(elapsed, [0, 0.5], [0, 1]);
  545. const fadeOut = interpolate(elapsed, [4.6, 5], [1, 0]);
  546. // Staggered reveal of real content
  547. const heroReveal = interpolate(elapsed, [0.6, 1.8], [0, 1], Easing.easeOut);
  548. const titleReveal = interpolate(elapsed, [1.5, 2.3], [0, 1]);
  549. const insightReveal = interpolate(elapsed, [2.2, 3.1], [0, 1]);
  550. const meta = interpolate(elapsed, [3.0, 3.8], [0, 1]);
  551. // Bottom subtitle
  552. const subOp = interpolate(elapsed, [3.9, 4.4], [0, 1]);
  553. return (
  554. <div style={{position:'absolute', inset:0, background:CREAM, opacity: opacity * fadeOut,
  555. padding:'60px 100px', display:'flex', flexDirection:'column'}}>
  556. {/* Header */}
  557. <div style={{display:'flex', justifyContent:'space-between',
  558. alignItems:'baseline', marginBottom: 28}}>
  559. <div>
  560. <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
  561. letterSpacing: '0.3em', marginBottom: 6}}>STEP 3 / 3</div>
  562. <div style={{fontFamily: serif, fontSize: 52, fontWeight: 500, color: INK}}>
  563. Full pass · <span style={{fontStyle:'italic', color: TERRA}}>灰块变真图</span>
  564. </div>
  565. </div>
  566. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18,
  567. color: ASH, textAlign:'right', lineHeight: 1.5}}>
  568. Placeholders → Content<br/>
  569. <span style={{fontSize: 14}}>— 方向对了再填色</span>
  570. </div>
  571. </div>
  572. {/* Two panels: before wireframe (thumbnail) vs after (full card) */}
  573. <div style={{display:'grid', gridTemplateColumns:'0.55fr 60px 1fr', gap: 0,
  574. flex: 1, alignItems:'center'}}>
  575. {/* Left: wireframe thumbnail, still, for contrast */}
  576. <div style={{background:'#fff', border: `1px solid ${LINE}`,
  577. padding: 20, opacity: 0.55, transform:'scale(0.92)',
  578. transformOrigin:'center right'}}>
  579. <div style={{fontFamily: mono, fontSize: 9, color: ASH,
  580. letterSpacing:'0.2em', marginBottom: 10}}>
  581. BEFORE
  582. </div>
  583. <div style={{height: 100, background:'#ececec',
  584. border:'1.5px dashed #bbb', display:'flex',
  585. alignItems:'center', justifyContent:'center',
  586. fontFamily: mono, fontSize: 10, color:'#888',
  587. letterSpacing:'0.15em', marginBottom: 12}}>
  588. [ HERO IMAGE ]
  589. </div>
  590. <div style={{height: 14, background:'#e5e0d3', width:'75%', marginBottom: 6}} />
  591. <div style={{height: 10, background:'#ede8db', width:'55%', marginBottom: 14}} />
  592. <div style={{background:'#f5f0e3', padding:'10px 12px',
  593. border: `1px dashed ${LINE}`}}>
  594. <div style={{height: 6, background:'#d9d2c5', width:'92%', marginBottom: 4}} />
  595. <div style={{height: 6, background:'#d9d2c5', width:'88%', marginBottom: 4}} />
  596. <div style={{height: 6, background:'#d9d2c5', width:'70%'}} />
  597. </div>
  598. </div>
  599. {/* Arrow */}
  600. <div style={{display:'flex', alignItems:'center', justifyContent:'center'}}>
  601. <svg width="40" height="24" viewBox="0 0 40 24">
  602. <path d="M 2 12 L 38 12 M 30 4 L 38 12 L 30 20"
  603. fill="none" stroke={TERRA} strokeWidth="2" strokeLinecap="round"/>
  604. </svg>
  605. </div>
  606. {/* Right: full card */}
  607. <div style={{background:'#fff', border: `1px solid ${LINE}`,
  608. boxShadow:'0 20px 60px rgba(0,0,0,0.08), 0 4px 16px rgba(0,0,0,0.04)',
  609. display:'flex', flexDirection:'column', overflow:'hidden'}}>
  610. <div style={{padding:'14px 24px', borderBottom:`1px solid ${LINE}`,
  611. display:'flex', justifyContent:'space-between', alignItems:'center'}}>
  612. <div style={{fontFamily: mono, fontSize: 10, color: TERRA,
  613. letterSpacing:'0.25em'}}>AFTER · FULL PASS</div>
  614. <div style={{fontFamily: mono, fontSize: 10, color: ASH,
  615. letterSpacing:'0.15em'}}>STOIC READER · v0.3</div>
  616. </div>
  617. {/* Hero image: oil painting */}
  618. <div style={{height: 220, position:'relative', overflow:'hidden'}}>
  619. <div style={{position:'absolute', inset: 0, opacity: heroReveal}}>
  620. <ArtBlock mood="warm" />
  621. </div>
  622. {heroReveal < 0.95 && (
  623. <div style={{position:'absolute', inset: 0, background:'#ececec',
  624. opacity: 1 - heroReveal, display:'flex', alignItems:'center',
  625. justifyContent:'center', fontFamily: mono, fontSize: 12,
  626. color:'#888', letterSpacing:'0.15em',
  627. border:'1.5px dashed #bbb'}}>
  628. [ HERO IMAGE ]
  629. </div>
  630. )}
  631. {/* Photo credit */}
  632. <div style={{position:'absolute', bottom: 10, right: 12,
  633. fontFamily: mono, fontSize: 9, color:'rgba(255,255,255,0.7)',
  634. letterSpacing:'0.2em', opacity: heroReveal}}>
  635. PHOTO · J. TURNER
  636. </div>
  637. </div>
  638. {/* Title */}
  639. <div style={{padding:'26px 32px 10px', opacity: titleReveal}}>
  640. <div style={{fontFamily: serif, fontSize: 36, fontWeight: 500,
  641. color: INK, lineHeight: 1.15, letterSpacing:'-0.01em',
  642. marginBottom: 6}}>
  643. The Obstacle<br/>
  644. <span style={{fontStyle:'italic', color: TERRA}}>is the Way</span>
  645. </div>
  646. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 15,
  647. color: ASH}}>
  648. Marcus Aurelius · 第四卷 · 今日阅读
  649. </div>
  650. </div>
  651. {/* AI insight — NOW 2 lines, not 3 */}
  652. <div style={{margin:'10px 32px 20px', padding:'16px 20px',
  653. background:'#faf6ef', borderLeft: `3px solid ${TERRA}`,
  654. opacity: insightReveal}}>
  655. <div style={{fontFamily: mono, fontSize: 9, color: TERRA,
  656. letterSpacing:'0.25em', marginBottom: 8}}>AI INSIGHT · 2 LINES</div>
  657. <div style={{fontFamily: serif, fontSize: 15, color: INK,
  658. lineHeight: 1.55}}>
  659. 你最近笔记里「困难」出现 7 次。<br/>
  660. Aurelius 今天正好在说:障碍本身就是路。
  661. </div>
  662. </div>
  663. {/* Meta row */}
  664. <div style={{padding:'14px 32px', borderTop:`1px solid ${LINE}`,
  665. display:'flex', justifyContent:'space-between',
  666. fontFamily: mono, fontSize: 10, color: ASH,
  667. letterSpacing:'0.15em', opacity: meta}}>
  668. <span>READ · 4 MIN</span>
  669. <span>MEMORY · 12 NOTES</span>
  670. <span>CHAT · ASK AURELIUS</span>
  671. </div>
  672. </div>
  673. </div>
  674. {/* Subtitle */}
  675. <div style={{textAlign:'center', marginTop: 30, opacity: subOp}}>
  676. <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 22,
  677. color: INK}}>
  678. 迭代到 80%,再做最后 20% 的抛光
  679. </div>
  680. </div>
  681. </div>
  682. );
  683. }
  684. // ── Scene 5: Closing + 4 checkpoints (18 – 22s) ───────────
  685. function Scene5_Closing() {
  686. const { elapsed } = useSprite();
  687. const opacity = interpolate(elapsed, [0, 0.6], [0, 1]);
  688. const titleY = interpolate(elapsed, [0, 1.0], [30, 0], Easing.easeOut);
  689. const lineW = interpolate(elapsed, [0.8, 1.6], [0, 620]);
  690. const items = [
  691. { text: '问 clarifying questions(一次一批)' },
  692. { text: '写 assumptions + placeholders' },
  693. { text: '尽早 show(哪怕只是灰块)' },
  694. { text: '迭代再抛光,不一次做完' },
  695. ];
  696. return (
  697. <div style={{position:'absolute', inset:0, background:CREAM, opacity,
  698. display:'flex', alignItems:'center', justifyContent:'center',
  699. flexDirection:'column', padding:'60px 120px'}}>
  700. <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.4em',
  701. color: TERRA, marginBottom: 24}}>
  702. JUNIOR DESIGNER · RECAP
  703. </div>
  704. <div style={{fontFamily: serif, fontSize: 100, fontWeight: 500,
  705. color: INK, lineHeight: 1.05, letterSpacing:'-0.015em',
  706. transform: `translateY(${titleY}px)`, textAlign:'center'}}>
  707. 早 <span style={{fontStyle:'italic', color: TERRA}}>show</span>
  708. <span style={{color: ASH, fontWeight: 400, margin:'0 18px'}}>·</span>
  709. 早 <span style={{fontStyle:'italic', color: TERRA}}>改</span>
  710. <span style={{color: ASH, fontWeight: 400, margin:'0 18px'}}>·</span>
  711. 早 <span style={{fontStyle:'italic', color: TERRA}}>对齐</span>
  712. </div>
  713. <div style={{height: 1, background: INK, width: lineW, marginTop: 34,
  714. marginBottom: 46}} />
  715. {/* 4 checkpoints */}
  716. <div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap:'18px 60px',
  717. maxWidth: 1100, width:'100%'}}>
  718. {items.map((it, i) => {
  719. const appear = interpolate(elapsed, [1.6 + i * 0.25, 2.2 + i * 0.25], [0, 1]);
  720. const checkAppear = interpolate(elapsed, [2.0 + i * 0.25, 2.5 + i * 0.25], [0, 1], Easing.spring);
  721. return (
  722. <div key={i} style={{display:'flex', alignItems:'center', gap: 20,
  723. opacity: appear, borderBottom:`1px solid ${LINE}`, paddingBottom: 14}}>
  724. <div style={{width: 36, height: 36, border:`2px solid ${TERRA}`,
  725. display:'flex', alignItems:'center', justifyContent:'center',
  726. flexShrink: 0, background: checkAppear > 0.5 ? TERRA : 'transparent',
  727. color:'#fff', fontFamily: serif, fontWeight: 600, fontSize: 20,
  728. lineHeight: 1, transform: `scale(${0.7 + checkAppear * 0.3})`}}>
  729. {checkAppear > 0.5 ? '✓' : ''}
  730. </div>
  731. <div style={{fontFamily: serif, fontSize: 26, color: INK,
  732. lineHeight: 1.35, letterSpacing:'0.005em'}}>
  733. {it.text}
  734. </div>
  735. </div>
  736. );
  737. })}
  738. </div>
  739. </div>
  740. );
  741. }
  742. // ── Watermark ─────────────────────────────────────────────
  743. function Watermark() {
  744. return (
  745. <div style={{position:'absolute', bottom: 24, right: 32,
  746. fontSize: 11, color: 'rgba(0,0,0,0.38)', letterSpacing:'0.15em',
  747. fontFamily: mono, pointerEvents:'none', zIndex: 100}}>
  748. Created by Huashu-Design
  749. </div>
  750. );
  751. }
  752. // ── Main composition ──────────────────────────────────────
  753. function App() {
  754. return (
  755. <Stage duration={22} width={1920} height={1080} bgColor={CREAM}>
  756. <Sprite start={0} end={3}><Scene1_Contrast /></Sprite>
  757. <Sprite start={3} end={8}><Scene2_Skeleton /></Sprite>
  758. <Sprite start={8} end={13}><Scene3_FirstShow /></Sprite>
  759. <Sprite start={13} end={18}><Scene4_FullPass /></Sprite>
  760. <Sprite start={18} end={22}><Scene5_Closing /></Sprite>
  761. <Watermark />
  762. </Stage>
  763. );
  764. }
  765. ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  766. </script>
  767. </body>
  768. </html>