mirror of
https://github.com/aculix/huashu-design.git
synced 2026-10-27 07:15:54 +00:00
launch huashu-design v1.0: 9 demos + marketing-first README
- 新增 9 个能力 demo(workflow + capability),每个 MP4 + GIF + 60fps - README 重写:营销 hook 开头 · 功能优先结构 · 自然嵌入 GIF - 修复 render-video.js Google Fonts networkidle 超时 - 修复 c1-ios-prototype 刘海/Home Indicator 背景色 - 补充 references/editable-pptx.md 和 scripts/export_deck_stage_pdf.mjs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
f3692aac10
commit
b127f8baa3
24 files changed
+8206
-281
No files matched your search
@@ -0,0 +1,787 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Huashu-Design · 品牌资产协议 5 步硬流程</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.25.6/babel.min.js"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<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">
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body { width: 100%; height: 100%; overflow: hidden; }
|
||||
body {
|
||||
background: #0c0c0c;
|
||||
font-family: 'Newsreader', 'Noto Serif SC', Georgia, serif;
|
||||
color: #1a1a1a;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
<!-- animations.jsx inlined -->
|
||||
<script type="text/babel">
|
||||
(function() {
|
||||
const { createContext, useContext, useState, useEffect, useRef, useCallback } = React;
|
||||
const TimeContext = createContext({ time: 0, duration: 10, playing: false });
|
||||
const SpriteContext = createContext(null);
|
||||
|
||||
const Easing = {
|
||||
linear: t => t,
|
||||
easeIn: t => t * t,
|
||||
easeOut: t => 1 - (1 - t) * (1 - t),
|
||||
easeInOut: t => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
|
||||
spring: t => {
|
||||
const c = (2 * Math.PI) / 3;
|
||||
return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c) + 1;
|
||||
},
|
||||
};
|
||||
|
||||
function interpolate(t, input, output, easing) {
|
||||
const [inStart, inEnd] = input;
|
||||
const [outStart, outEnd] = output;
|
||||
if (t <= inStart) return outStart;
|
||||
if (t >= inEnd) return outEnd;
|
||||
let progress = (t - inStart) / (inEnd - inStart);
|
||||
if (easing) progress = easing(progress);
|
||||
return outStart + (outEnd - outStart) * progress;
|
||||
}
|
||||
|
||||
function useTime() { return useContext(TimeContext).time; }
|
||||
function useSprite() {
|
||||
const sprite = useContext(SpriteContext);
|
||||
return sprite || { t: 0, elapsed: 0, duration: 0 };
|
||||
}
|
||||
|
||||
function Stage({ duration = 10, width = 1920, height = 1080, loop = true, children, bgColor = '#fff' }) {
|
||||
const [time, setTime] = useState(0);
|
||||
const [playing, setPlaying] = useState(true);
|
||||
const [scale, setScale] = useState(1);
|
||||
const rafRef = useRef(null);
|
||||
const effectiveLoop = (typeof window !== 'undefined' && window.__recording) ? false : loop;
|
||||
|
||||
useEffect(() => {
|
||||
function updateScale() {
|
||||
const vw = window.innerWidth;
|
||||
const vh = window.innerHeight - 56;
|
||||
const s = Math.min(vw / width, vh / height);
|
||||
setScale(s);
|
||||
}
|
||||
updateScale();
|
||||
window.addEventListener('resize', updateScale);
|
||||
return () => window.removeEventListener('resize', updateScale);
|
||||
}, [width, height]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!playing) return;
|
||||
let cancelled = false;
|
||||
let last = null;
|
||||
function tick(now) {
|
||||
if (cancelled) return;
|
||||
if (last === null) {
|
||||
last = now;
|
||||
if (typeof window !== 'undefined') window.__ready = true;
|
||||
}
|
||||
const delta = (now - last) / 1000;
|
||||
last = now;
|
||||
setTime(prev => {
|
||||
const next = prev + delta;
|
||||
if (next >= duration) return effectiveLoop ? 0 : duration - 0.001;
|
||||
return next;
|
||||
});
|
||||
rafRef.current = requestAnimationFrame(tick);
|
||||
}
|
||||
const start = () => { if (!cancelled) rafRef.current = requestAnimationFrame(tick); };
|
||||
if (document.fonts && document.fonts.ready) document.fonts.ready.then(start); else start();
|
||||
return () => { cancelled = true; cancelAnimationFrame(rafRef.current); };
|
||||
}, [playing, duration, effectiveLoop]);
|
||||
|
||||
const progress = time / duration;
|
||||
const ctx = { time, duration, playing, setPlaying, setTime };
|
||||
|
||||
const canvasStyle = {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transformOrigin: 'center center',
|
||||
width,
|
||||
height,
|
||||
background: bgColor,
|
||||
overflow: 'hidden',
|
||||
transform: `translate(-50%, -50%) scale(${scale})`,
|
||||
};
|
||||
|
||||
return (
|
||||
<TimeContext.Provider value={ctx}>
|
||||
<div style={{position:'fixed', inset:0, background:'#0c0c0c', display:'flex', flexDirection:'column'}}>
|
||||
<div style={{flex:1, position:'relative', overflow:'hidden'}}>
|
||||
<div style={canvasStyle}>{children}</div>
|
||||
</div>
|
||||
<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}}>
|
||||
<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>
|
||||
<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>
|
||||
<div style={{fontFamily:'ui-monospace, monospace', fontVariantNumeric:'tabular-nums', minWidth:90}}>{time.toFixed(2)}s / {duration.toFixed(2)}s</div>
|
||||
<div style={{flex:1, height:4, background:'rgba(255,255,255,0.2)', borderRadius:2, position:'relative'}}>
|
||||
<div style={{position:'absolute', top:0, left:0, height:'100%', width:`${progress*100}%`, background:'#fff', borderRadius:2}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TimeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Sprite({ start = 0, end, children, style }) {
|
||||
const { time } = useContext(TimeContext);
|
||||
const actualEnd = end == null ? Infinity : end;
|
||||
if (time < start || time >= actualEnd) return null;
|
||||
const duration = actualEnd - start;
|
||||
const elapsed = time - start;
|
||||
const t = duration === 0 ? 1 : Math.max(0, Math.min(1, elapsed / duration));
|
||||
const spriteValue = { t, elapsed, duration, start, end: actualEnd };
|
||||
return (
|
||||
<SpriteContext.Provider value={spriteValue}>
|
||||
<div style={{position:'absolute', inset:0, ...style}}>{children}</div>
|
||||
</SpriteContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
window.Animations = { Stage, Sprite, useTime, useSprite, Easing, interpolate };
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Demo scene -->
|
||||
<script type="text/babel">
|
||||
const { Stage, Sprite, useTime, useSprite, Easing, interpolate } = window.Animations;
|
||||
|
||||
// ── Design tokens ─────────────────────────────────────────
|
||||
const CREAM = '#FAF6EF';
|
||||
const INK = '#1a1a1a';
|
||||
const TERRA = '#C04A1A';
|
||||
const ASH = '#6b6b6b';
|
||||
const LINE = '#d9d2c5';
|
||||
const OLIVE = '#6a6b4e';
|
||||
|
||||
const serif = "'Newsreader', 'Noto Serif SC', Georgia, serif";
|
||||
const sans = "'Inter', -apple-system, sans-serif";
|
||||
const mono = "'JetBrains Mono', ui-monospace, monospace";
|
||||
|
||||
// ── Scene 1: Trigger (0 – 3s) ─────────────────────────────
|
||||
function Scene1_Trigger() {
|
||||
const { elapsed } = useSprite();
|
||||
const labelOp = interpolate(elapsed, [0, 0.5], [0, 1]);
|
||||
const titleY = interpolate(elapsed, [0, 1], [30, 0], Easing.easeOut);
|
||||
const titleOp = interpolate(elapsed, [0.2, 1], [0, 1]);
|
||||
const brandsOp = interpolate(elapsed, [1, 1.6], [0, 1]);
|
||||
const switchOp = interpolate(elapsed, [1.9, 2.4], [0, 1]);
|
||||
const fadeOut = interpolate(elapsed, [2.6, 3], [1, 0]);
|
||||
|
||||
const brands = ['Kimi', 'Linear', 'Lovart', 'Stripe'];
|
||||
|
||||
return (
|
||||
<div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeOut,
|
||||
display:'flex', alignItems:'center', justifyContent:'center', flexDirection:'column'}}>
|
||||
<div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.4em',
|
||||
color: TERRA, marginBottom: 28, opacity: labelOp}}>
|
||||
品 牌 资 产 协 议 · BRAND PROTOCOL
|
||||
</div>
|
||||
<div style={{fontFamily: serif, fontSize: 120, fontWeight: 500, color: INK,
|
||||
lineHeight: 1, letterSpacing:'-0.01em',
|
||||
opacity: titleOp, transform: `translateY(${titleY}px)`}}>
|
||||
涉及具体品牌<span style={{color: TERRA, fontStyle:'italic'}}>?</span>
|
||||
</div>
|
||||
|
||||
<div style={{display:'flex', gap: 48, marginTop: 64, opacity: brandsOp}}>
|
||||
{brands.map((b, i) => (
|
||||
<span key={i} style={{fontFamily: serif, fontStyle:'italic',
|
||||
fontSize: 44, color: ASH, letterSpacing:'0.02em'}}>
|
||||
{b}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{marginTop: 72, opacity: switchOp, textAlign:'center'}}>
|
||||
<div style={{fontFamily: serif, fontStyle:'italic', fontSize: 34,
|
||||
color: TERRA, letterSpacing:'0.02em'}}>
|
||||
先停下——走 5 步硬流程
|
||||
</div>
|
||||
<div style={{height:1, background: TERRA, width: 180, margin:'18px auto 0'}} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Scene 2: Step 1 & 2 (3 – 7s) ──────────────────────────
|
||||
function Scene2_AskAndSearch() {
|
||||
const { elapsed } = useSprite();
|
||||
const headOp = interpolate(elapsed, [0, 0.4], [0, 1]);
|
||||
const leftOp = interpolate(elapsed, [0.3, 0.9], [0, 1]);
|
||||
const leftY = interpolate(elapsed, [0.3, 0.9], [20, 0], Easing.easeOut);
|
||||
const rightOp = interpolate(elapsed, [0.8, 1.4], [0, 1]);
|
||||
const rightY = interpolate(elapsed, [0.8, 1.4], [20, 0], Easing.easeOut);
|
||||
const fadeOut = interpolate(elapsed, [3.5, 4], [1, 0]);
|
||||
|
||||
// cursor sweeping through search paths 1.6 → 3.2
|
||||
const paths = [
|
||||
'<brand>.com/brand',
|
||||
'<brand>.com/press',
|
||||
'brand.<brand>.com',
|
||||
'<brand>.github.io/brand',
|
||||
];
|
||||
// current active index based on time
|
||||
const activeIdx = Math.min(3, Math.max(0, Math.floor((elapsed - 1.6) / 0.4)));
|
||||
|
||||
return (
|
||||
<div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeOut,
|
||||
padding:'72px 120px', display:'flex', flexDirection:'column'}}>
|
||||
<div style={{display:'flex', justifyContent:'space-between',
|
||||
alignItems:'baseline', opacity: headOp, marginBottom: 48}}>
|
||||
<div style={{fontFamily: serif, fontSize: 44, fontWeight: 500, color: INK}}>
|
||||
Step 1 & 2 · 问 · 搜
|
||||
</div>
|
||||
<div style={{fontFamily: mono, fontSize: 11, color: ASH, letterSpacing:'0.25em'}}>
|
||||
01 / 05 → 02 / 05
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap: 48, flex:1}}>
|
||||
{/* Left: Step 1 Ask */}
|
||||
<div style={{opacity: leftOp, transform:`translateY(${leftY}px)`,
|
||||
display:'flex', flexDirection:'column', gap: 24}}>
|
||||
<div style={{display:'flex', alignItems:'baseline', gap: 18}}>
|
||||
<div style={{fontFamily: mono, fontSize: 11, color: TERRA,
|
||||
letterSpacing:'0.3em'}}>STEP · 01</div>
|
||||
<div style={{fontFamily: serif, fontSize: 40, color: INK,
|
||||
fontWeight: 500}}>问</div>
|
||||
</div>
|
||||
|
||||
<div style={{background:'#fff', border:`1px solid ${LINE}`,
|
||||
padding:'32px 34px', position:'relative'}}>
|
||||
<div style={{fontFamily: serif, fontStyle:'italic', fontSize: 64,
|
||||
color: TERRA, lineHeight:1, position:'absolute',
|
||||
top: 8, left: 14}}>「</div>
|
||||
<div style={{fontFamily: serif, fontSize: 24, color: INK,
|
||||
lineHeight: 1.55, paddingLeft: 36}}>
|
||||
这个品牌有 <span style={{fontStyle:'italic', color: TERRA}}>brand guidelines</span> 吗?
|
||||
</div>
|
||||
<div style={{height:1, background: LINE, margin:'22px 0'}} />
|
||||
<div style={{fontFamily: serif, fontSize: 20, color: ASH,
|
||||
lineHeight: 1.6, paddingLeft: 36}}>
|
||||
有的话直接给我;没有我去搜。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{fontFamily: mono, fontSize: 11, color: ASH,
|
||||
letterSpacing:'0.15em', marginTop: 6}}>
|
||||
▸ 用户提供 > AI 猜测
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Step 2 Search */}
|
||||
<div style={{opacity: rightOp, transform:`translateY(${rightY}px)`,
|
||||
display:'flex', flexDirection:'column', gap: 24}}>
|
||||
<div style={{display:'flex', alignItems:'baseline', gap: 18}}>
|
||||
<div style={{fontFamily: mono, fontSize: 11, color: TERRA,
|
||||
letterSpacing:'0.3em'}}>STEP · 02</div>
|
||||
<div style={{fontFamily: serif, fontSize: 40, color: INK,
|
||||
fontWeight: 500}}>搜 官 方 品 牌 页</div>
|
||||
</div>
|
||||
|
||||
<div style={{background:'#1a1a1a', color:'#e8e3d6',
|
||||
padding:'24px 28px', fontFamily: mono, fontSize: 16,
|
||||
lineHeight: 1.9, flex: 1}}>
|
||||
<div style={{color:'#6b6b6b', fontSize: 11,
|
||||
letterSpacing:'0.2em', marginBottom: 14}}>
|
||||
$ HTTP GET · typical paths
|
||||
</div>
|
||||
{paths.map((p, i) => {
|
||||
const isActive = i === activeIdx && elapsed > 1.6 && elapsed < 3.4;
|
||||
const visited = i < activeIdx;
|
||||
return (
|
||||
<div key={i} style={{
|
||||
color: isActive ? TERRA : (visited ? '#8a8878' : '#e8e3d6'),
|
||||
background: isActive ? 'rgba(192,74,26,0.12)' : 'transparent',
|
||||
padding:'2px 8px', marginLeft:-8,
|
||||
display:'flex', alignItems:'baseline', gap: 14}}>
|
||||
<span style={{color: isActive ? TERRA : '#555', fontSize: 13}}>
|
||||
{isActive ? '▸' : (visited ? '✓' : ' ')}
|
||||
</span>
|
||||
<span style={{letterSpacing:'0.02em'}}>{p}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18,
|
||||
color: ASH, marginTop: 6}}>
|
||||
按顺序试,中断也要记 404 状态
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Scene 3: Step 3 · Three fallbacks (7 – 12s) ───────────
|
||||
function Scene3_Fallbacks() {
|
||||
const { elapsed } = useSprite();
|
||||
const headOp = interpolate(elapsed, [0, 0.4], [0, 1]);
|
||||
const cardsOp = interpolate(elapsed, [0.3, 1], [0, 1]);
|
||||
const fadeOut = interpolate(elapsed, [4.5, 5], [1, 0]);
|
||||
|
||||
// Card state: 0 idle, 1 active/lit, 2 failed/gray, 3 success
|
||||
// Card 1 active 1.2→2.0, then fails at 2.0
|
||||
// Card 2 active 2.2→3.0, then fails at 3.0
|
||||
// Card 3 active 3.2→4.2, succeeds at 4.2
|
||||
const card1State = elapsed < 1.2 ? 0 : elapsed < 2.0 ? 1 : 2;
|
||||
const card2State = elapsed < 2.2 ? 0 : elapsed < 3.0 ? 1 : (elapsed < 3.2 ? 2 : 2);
|
||||
const card3State = elapsed < 3.2 ? 0 : elapsed < 4.2 ? 1 : 3;
|
||||
|
||||
const captionOp = interpolate(elapsed, [4.2, 4.6], [0, 1]);
|
||||
|
||||
const cards = [
|
||||
{ n: '01', title: 'SVG 文件', cmd: 'curl -o logo.svg <url>', status: '最理想', state: card1State },
|
||||
{ n: '02', title: '官网 HTML', cmd: 'curl -A "Mozilla/5.0" \\\n -L <url>', status: '80% 场景必用', state: card2State },
|
||||
{ n: '03', title: '产品截图取色', cmd: 'macOS Preview · 吸管', status: 'App 必走', state: card3State },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeOut,
|
||||
padding:'70px 100px 60px', display:'flex', flexDirection:'column'}}>
|
||||
<div style={{opacity: headOp, marginBottom: 40}}>
|
||||
<div style={{display:'flex', alignItems:'baseline', gap: 18, marginBottom: 6}}>
|
||||
<div style={{fontFamily: mono, fontSize: 11, color: TERRA,
|
||||
letterSpacing:'0.3em'}}>STEP · 03</div>
|
||||
<div style={{fontFamily: serif, fontSize: 18, color: ASH,
|
||||
fontStyle:'italic'}}>下 · download</div>
|
||||
</div>
|
||||
<div style={{fontFamily: serif, fontSize: 60, fontWeight: 500,
|
||||
color: INK, letterSpacing:'-0.01em'}}>
|
||||
三条兜底路径
|
||||
</div>
|
||||
<div style={{height:1, background: LINE, width:'100%', marginTop: 20}} />
|
||||
</div>
|
||||
|
||||
<div style={{display:'grid', gridTemplateColumns:'1fr 1fr 1fr',
|
||||
gap: 28, flex:1, opacity: cardsOp}}>
|
||||
{cards.map((c, i) => {
|
||||
const isIdle = c.state === 0;
|
||||
const isActive = c.state === 1;
|
||||
const isFailed = c.state === 2;
|
||||
const isSuccess = c.state === 3;
|
||||
|
||||
const borderColor = isSuccess ? TERRA :
|
||||
isActive ? INK :
|
||||
isFailed ? '#c4bda7' : LINE;
|
||||
const borderWidth = (isActive || isSuccess) ? 2 : 1;
|
||||
const op = isFailed ? 0.42 : 1;
|
||||
const bg = isSuccess ? '#fffaf3' : '#fff';
|
||||
|
||||
return (
|
||||
<div key={i} style={{
|
||||
background: bg,
|
||||
border:`${borderWidth}px solid ${borderColor}`,
|
||||
opacity: op,
|
||||
padding:'28px 28px 24px',
|
||||
display:'flex', flexDirection:'column',
|
||||
position:'relative',
|
||||
transition: 'none',
|
||||
}}>
|
||||
<div style={{display:'flex', justifyContent:'space-between',
|
||||
alignItems:'baseline', marginBottom: 20}}>
|
||||
<div style={{fontFamily: serif, fontSize: 56, fontWeight: 300,
|
||||
color: isSuccess ? TERRA : INK, lineHeight:1}}>
|
||||
{c.n}
|
||||
</div>
|
||||
<div style={{fontFamily: mono, fontSize: 10, letterSpacing:'0.2em',
|
||||
color: isSuccess ? TERRA : isFailed ? ASH : INK}}>
|
||||
{isIdle && 'READY'}
|
||||
{isActive && '▸ TRYING…'}
|
||||
{isFailed && '× FAILED'}
|
||||
{isSuccess && '✓ GOT IT'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{fontFamily: serif, fontSize: 28, fontWeight: 500,
|
||||
color: INK, marginBottom: 14, letterSpacing:'-0.005em'}}>
|
||||
{c.title}
|
||||
</div>
|
||||
|
||||
<div style={{background:'#1a1a1a', color:'#e8e3d6',
|
||||
fontFamily: mono, fontSize: 12, padding:'14px 16px',
|
||||
whiteSpace:'pre-wrap', lineHeight: 1.6,
|
||||
marginBottom: 20, borderLeft: `2px solid ${isSuccess ? TERRA : '#333'}`}}>
|
||||
{c.cmd}
|
||||
</div>
|
||||
|
||||
<div style={{marginTop:'auto', borderTop:`1px solid ${LINE}`,
|
||||
paddingTop: 14, display:'flex', justifyContent:'space-between',
|
||||
alignItems:'baseline'}}>
|
||||
<div style={{fontFamily: serif, fontStyle:'italic',
|
||||
fontSize: 15, color: ASH}}>
|
||||
场景
|
||||
</div>
|
||||
<div style={{fontFamily: serif, fontSize: 16,
|
||||
color: isSuccess ? TERRA : INK, fontWeight: 500}}>
|
||||
{c.status}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isSuccess && (
|
||||
<div style={{position:'absolute', top:-10, right:-10,
|
||||
width: 28, height: 28, borderRadius:'50%', background: TERRA,
|
||||
color:'#fff', display:'flex', alignItems:'center',
|
||||
justifyContent:'center', fontFamily: serif, fontSize: 14,
|
||||
fontWeight: 600}}>
|
||||
✓
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{opacity: captionOp, marginTop: 22, textAlign:'center',
|
||||
fontFamily: serif, fontStyle:'italic', fontSize: 22, color: INK}}>
|
||||
前一条失败,<span style={{color: TERRA}}>立刻</span>走下一条——不要停
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Scene 4: Step 4 · Grep colors (12 – 17s) ──────────────
|
||||
function Scene4_GrepColors() {
|
||||
const { elapsed } = useSprite();
|
||||
const headOp = interpolate(elapsed, [0, 0.4], [0, 1]);
|
||||
const cmdOp = interpolate(elapsed, [0.4, 0.9], [0, 1]);
|
||||
const fadeOut = interpolate(elapsed, [4.5, 5], [1, 0]);
|
||||
|
||||
const results = [
|
||||
{ count: 47, hex: '#1783FF', label: 'Kimi primary' },
|
||||
{ count: 32, hex: '#FAFAFA', label: 'background' },
|
||||
{ count: 18, hex: '#1a1a1a', label: 'ink' },
|
||||
{ count: 12, hex: '#FF6B35', label: 'accent' },
|
||||
{ count: 8, hex: '#6A6B4E', label: 'muted' },
|
||||
];
|
||||
|
||||
// Stagger each result row, starting at t=1.5
|
||||
const rowStart = 1.5;
|
||||
const rowStep = 0.32;
|
||||
|
||||
const captionOp = interpolate(elapsed, [3.6, 4.1], [0, 1]);
|
||||
|
||||
return (
|
||||
<div style={{position:'absolute', inset:0, background:'#1a1a1a', opacity: fadeOut,
|
||||
padding:'64px 100px', display:'flex', flexDirection:'column',
|
||||
color:'#e8e3d6'}}>
|
||||
<div style={{display:'flex', justifyContent:'space-between',
|
||||
alignItems:'baseline', marginBottom: 36, opacity: headOp}}>
|
||||
<div>
|
||||
<div style={{fontFamily: mono, fontSize: 11, color: TERRA,
|
||||
letterSpacing:'0.3em', marginBottom: 4}}>STEP · 04</div>
|
||||
<div style={{fontFamily: serif, fontSize: 54, fontWeight: 500,
|
||||
color:'#faf6ef', letterSpacing:'-0.01em'}}>
|
||||
Grep 色 值
|
||||
</div>
|
||||
</div>
|
||||
<div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18,
|
||||
color:'#8a8878', textAlign:'right'}}>
|
||||
频次排序 = 品牌色权重<br/>
|
||||
<span style={{fontFamily: mono, fontSize: 10, letterSpacing:'0.2em'}}>
|
||||
/ DON'T GUESS · COUNT
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Command */}
|
||||
<div style={{opacity: cmdOp, background:'#0e0e0e',
|
||||
border:'1px solid #333', padding:'20px 26px',
|
||||
fontFamily: mono, fontSize: 15, lineHeight: 1.7,
|
||||
marginBottom: 28}}>
|
||||
<div style={{color: TERRA, fontSize: 10, letterSpacing:'0.2em',
|
||||
marginBottom: 10}}>$ COMMAND</div>
|
||||
<div style={{color:'#e8e3d6'}}>
|
||||
<span style={{color:'#888'}}>$</span>{' '}
|
||||
<span style={{color:'#7dd3fc'}}>grep</span>{' '}
|
||||
<span style={{color:'#fbbf24'}}>-hoE</span>{' '}
|
||||
<span style={{color:'#a5f3c5'}}>'#[0-9A-Fa-f]{'{6}'}'</span>{' '}
|
||||
<span style={{color:'#e8e3d6'}}>assets/*.{'{svg,html}'}</span> \<br/>
|
||||
{' '}<span style={{color:'#666'}}>|</span>{' '}
|
||||
<span style={{color:'#7dd3fc'}}>sort</span>{' '}
|
||||
<span style={{color:'#666'}}>|</span>{' '}
|
||||
<span style={{color:'#7dd3fc'}}>uniq -c</span>{' '}
|
||||
<span style={{color:'#666'}}>|</span>{' '}
|
||||
<span style={{color:'#7dd3fc'}}>sort -rn</span>{' '}
|
||||
<span style={{color:'#666'}}>|</span>{' '}
|
||||
<span style={{color:'#7dd3fc'}}>head -20</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results + swatches */}
|
||||
<div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap: 60,
|
||||
flex: 1}}>
|
||||
{/* Left: result list */}
|
||||
<div style={{fontFamily: mono, fontSize: 18, lineHeight: 2}}>
|
||||
<div style={{color:'#555', fontSize: 10, letterSpacing:'0.2em',
|
||||
marginBottom: 12}}>▸ RESULT · top 5</div>
|
||||
{results.map((r, i) => {
|
||||
const visible = elapsed > (rowStart + i * rowStep);
|
||||
if (!visible) return <div key={i} style={{height: 36}}/>;
|
||||
const fadeIn = interpolate(elapsed,
|
||||
[rowStart + i*rowStep, rowStart + i*rowStep + 0.3],
|
||||
[0, 1]);
|
||||
return (
|
||||
<div key={i} style={{opacity: fadeIn, display:'flex',
|
||||
alignItems:'center', gap: 20}}>
|
||||
<span style={{color:'#fbbf24', minWidth: 40, textAlign:'right'}}>
|
||||
{String(r.count).padStart(3, ' ').replace(/ /g, '\u00A0')}
|
||||
</span>
|
||||
<span style={{color:'#e8e3d6'}}>{r.hex}</span>
|
||||
<span style={{color:'#666', fontSize: 14}}>←</span>
|
||||
<span style={{color:'#8a8878', fontStyle:'italic',
|
||||
fontFamily: serif, fontSize: 16}}>{r.label}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Right: color swatches */}
|
||||
<div style={{display:'flex', flexDirection:'column', gap: 14}}>
|
||||
<div style={{color:'#555', fontFamily: mono, fontSize: 10,
|
||||
letterSpacing:'0.2em', marginBottom: 2}}>▸ PALETTE</div>
|
||||
{results.map((r, i) => {
|
||||
const visible = elapsed > (rowStart + i * rowStep);
|
||||
if (!visible) return <div key={i} style={{height: 54}}/>;
|
||||
const fadeIn = interpolate(elapsed,
|
||||
[rowStart + i*rowStep, rowStart + i*rowStep + 0.3],
|
||||
[0, 1]);
|
||||
const w = interpolate(elapsed,
|
||||
[rowStart + i*rowStep, rowStart + i*rowStep + 0.5],
|
||||
[0, r.count * 9]);
|
||||
return (
|
||||
<div key={i} style={{opacity: fadeIn, display:'flex',
|
||||
alignItems:'center', gap: 14, height: 50}}>
|
||||
<div style={{width: 50, height: 50, background: r.hex,
|
||||
border:'1px solid #333', flexShrink: 0}} />
|
||||
<div style={{height: 24, background: r.hex,
|
||||
width: `${w}px`, opacity: 0.7}} />
|
||||
<div style={{fontFamily: mono, fontSize: 12,
|
||||
color:'#8a8878'}}>×{r.count}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Key caption */}
|
||||
<div style={{opacity: captionOp, textAlign:'center', marginTop: 18,
|
||||
paddingTop: 18, borderTop:'1px solid #333'}}>
|
||||
<span style={{fontFamily: serif, fontStyle:'italic', fontSize: 26,
|
||||
color: TERRA}}>
|
||||
不要凭记忆猜品牌色
|
||||
</span>
|
||||
<span style={{fontFamily: serif, fontSize: 20, color:'#8a8878',
|
||||
marginLeft: 14}}>
|
||||
——频次说了算
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Scene 5: Step 5 · brand-spec.md (17 – 22s) ────────────
|
||||
function Scene5_SpecFile() {
|
||||
const { elapsed } = useSprite();
|
||||
const headOp = interpolate(elapsed, [0, 0.4], [0, 1]);
|
||||
const mdOp = interpolate(elapsed, [0.3, 0.9], [0, 1]);
|
||||
const mdX = interpolate(elapsed, [0.3, 0.9], [-40, 0], Easing.easeOut);
|
||||
const cssOp = interpolate(elapsed, [0.9, 1.5], [0, 1]);
|
||||
const cssX = interpolate(elapsed, [0.9, 1.5], [40, 0], Easing.easeOut);
|
||||
const arrowOp = interpolate(elapsed, [2.2, 2.7], [0, 1]);
|
||||
const captionOp = interpolate(elapsed, [3.4, 3.9], [0, 1]);
|
||||
const fadeOut = interpolate(elapsed, [4.5, 5], [1, 0]);
|
||||
|
||||
return (
|
||||
<div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeOut,
|
||||
padding:'64px 80px 48px', display:'flex', flexDirection:'column'}}>
|
||||
<div style={{opacity: headOp, marginBottom: 28, display:'flex',
|
||||
justifyContent:'space-between', alignItems:'baseline'}}>
|
||||
<div>
|
||||
<div style={{fontFamily: mono, fontSize: 11, color: TERRA,
|
||||
letterSpacing:'0.3em', marginBottom: 4}}>STEP · 05 · FINAL</div>
|
||||
<div style={{fontFamily: serif, fontSize: 54, fontWeight: 500,
|
||||
color: INK, letterSpacing:'-0.01em'}}>
|
||||
固化为 <span style={{fontStyle:'italic'}}>brand-spec.md</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18,
|
||||
color: ASH, textAlign:'right'}}>
|
||||
单一真相源 · single source of truth
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{display:'grid', gridTemplateColumns:'1.1fr 0.9fr',
|
||||
gap: 40, flex:1, position:'relative'}}>
|
||||
{/* Left: .md preview */}
|
||||
<div style={{opacity: mdOp, transform:`translateX(${mdX}px)`,
|
||||
background:'#fff', border:`1px solid ${LINE}`,
|
||||
display:'flex', flexDirection:'column'}}>
|
||||
<div style={{padding:'12px 20px', borderBottom:`1px solid ${LINE}`,
|
||||
display:'flex', justifyContent:'space-between', alignItems:'center',
|
||||
background:'#f7f2e8'}}>
|
||||
<div style={{fontFamily: mono, fontSize: 11, color: ASH,
|
||||
letterSpacing:'0.15em'}}>
|
||||
▸ brand-spec.md
|
||||
</div>
|
||||
<div style={{fontFamily: mono, fontSize: 10, color: TERRA,
|
||||
letterSpacing:'0.15em'}}>
|
||||
✓ COMMITTED
|
||||
</div>
|
||||
</div>
|
||||
<div style={{padding:'22px 28px', fontFamily: mono, fontSize: 12,
|
||||
lineHeight: 1.75, color: INK, flex:1, overflow:'hidden'}}>
|
||||
<div style={{color: ASH}}>---</div>
|
||||
<div><span style={{color: OLIVE}}>brand</span>: Kimi</div>
|
||||
<div><span style={{color: OLIVE}}>fetched</span>: 2026-04-20</div>
|
||||
<div style={{color: ASH}}>---</div>
|
||||
<div style={{height: 10}} />
|
||||
<div style={{fontFamily: serif, fontSize: 20, fontWeight: 500,
|
||||
marginBottom: 4}}>## 色板</div>
|
||||
<div>- primary: <span style={{color: TERRA}}>#1783FF</span> (47)</div>
|
||||
<div>- bg: <span style={{color: TERRA}}>#FAFAFA</span> (32)</div>
|
||||
<div>- ink: <span style={{color: TERRA}}>#1a1a1a</span> (18)</div>
|
||||
<div>- accent: <span style={{color: TERRA}}>#FF6B35</span> (12)</div>
|
||||
<div style={{height: 12}} />
|
||||
<div style={{fontFamily: serif, fontSize: 20, fontWeight: 500,
|
||||
marginBottom: 4}}>## 字型</div>
|
||||
<div>- display: <span style={{color: OLIVE}}>Newsreader</span></div>
|
||||
<div>- body: <span style={{color: OLIVE}}>Inter</span></div>
|
||||
<div>- mono: <span style={{color: OLIVE}}>JetBrains Mono</span></div>
|
||||
<div style={{height: 12}} />
|
||||
<div style={{fontFamily: serif, fontSize: 20, fontWeight: 500,
|
||||
marginBottom: 4}}>## 签名细节</div>
|
||||
<div>- 1px hairlines</div>
|
||||
<div>- 圆角半径统一 4px</div>
|
||||
<div style={{height: 12}} />
|
||||
<div style={{fontFamily: serif, fontSize: 20, fontWeight: 500,
|
||||
marginBottom: 4, color: TERRA}}>## 禁区</div>
|
||||
<div style={{color: ASH}}>- 不用紫渐变 / emoji / 4px 以上 shadow</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: CSS vars + consumers */}
|
||||
<div style={{opacity: cssOp, transform:`translateX(${cssX}px)`,
|
||||
display:'flex', flexDirection:'column', gap: 20}}>
|
||||
<div style={{background:'#0e0e0e', color:'#e8e3d6',
|
||||
fontFamily: mono, fontSize: 14, padding:'22px 26px',
|
||||
lineHeight: 1.7, flex: 1}}>
|
||||
<div style={{color:'#666', fontSize: 10, letterSpacing:'0.2em',
|
||||
marginBottom: 12}}>/* tokens.css */</div>
|
||||
<div><span style={{color:'#7dd3fc'}}>:root</span> {'{'}</div>
|
||||
<div> <span style={{color:'#a5f3c5'}}>--brand-primary</span>: <span style={{color:'#fbbf24'}}>#1783FF</span>;</div>
|
||||
<div> <span style={{color:'#a5f3c5'}}>--brand-bg</span>: <span style={{color:'#fbbf24'}}>#FAFAFA</span>;</div>
|
||||
<div> <span style={{color:'#a5f3c5'}}>--brand-ink</span>: <span style={{color:'#fbbf24'}}>#1a1a1a</span>;</div>
|
||||
<div> <span style={{color:'#a5f3c5'}}>--brand-accent</span>: <span style={{color:'#fbbf24'}}>#FF6B35</span>;</div>
|
||||
<div>{'}'}</div>
|
||||
</div>
|
||||
|
||||
{/* Arrow cascade */}
|
||||
<div style={{opacity: arrowOp, position:'relative',
|
||||
background:'#fff', border:`1px solid ${LINE}`,
|
||||
padding:'20px 24px'}}>
|
||||
<div style={{fontFamily: mono, fontSize: 10, color: TERRA,
|
||||
letterSpacing:'0.2em', marginBottom: 12}}>▸ CONSUMERS</div>
|
||||
<div style={{display:'flex', alignItems:'center', gap: 16,
|
||||
fontFamily: mono, fontSize: 13, color: INK, flexWrap:'wrap'}}>
|
||||
<span style={{padding:'4px 10px', border:`1px solid ${TERRA}`,
|
||||
color: TERRA}}>brand-spec.md</span>
|
||||
<span style={{color: TERRA}}>→</span>
|
||||
<span style={{padding:'4px 10px', border:`1px solid ${INK}`}}>:root</span>
|
||||
<span style={{color: TERRA}}>→</span>
|
||||
<span style={{padding:'4px 10px', background: INK, color:'#fff'}}>all .html</span>
|
||||
</div>
|
||||
<div style={{fontFamily: serif, fontStyle:'italic', fontSize: 15,
|
||||
color: ASH, marginTop: 14, lineHeight: 1.5}}>
|
||||
一个文件改动,所有 HTML 自动跟随。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{opacity: captionOp, marginTop: 22, textAlign:'center',
|
||||
fontFamily: serif, fontStyle:'italic', fontSize: 22, color: INK}}>
|
||||
全协议的<span style={{color: TERRA}}>制胜点</span>——让所有 CSS 引用这份文件
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Scene 6: Final (22 – 24s) ─────────────────────────────
|
||||
function Scene6_Final() {
|
||||
const { elapsed } = useSprite();
|
||||
const fadeIn = interpolate(elapsed, [0, 0.5], [0, 1], Easing.easeOut);
|
||||
const titleY = interpolate(elapsed, [0, 0.8], [24, 0], Easing.easeOut);
|
||||
const lineW = interpolate(elapsed, [0.5, 1.3], [0, 580]);
|
||||
const subOp = interpolate(elapsed, [0.8, 1.4], [0, 1]);
|
||||
|
||||
return (
|
||||
<div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeIn,
|
||||
display:'flex', alignItems:'center', justifyContent:'center',
|
||||
flexDirection:'column'}}>
|
||||
<div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.4em',
|
||||
color: TERRA, marginBottom: 24}}>
|
||||
01 → 02 → 03 → 04 → <span style={{color: INK}}>05 · DONE</span>
|
||||
</div>
|
||||
|
||||
<div style={{fontFamily: serif, fontSize: 92, fontWeight: 500,
|
||||
color: INK, lineHeight: 1.1, letterSpacing:'-0.01em',
|
||||
textAlign:'center', transform: `translateY(${titleY}px)`}}>
|
||||
<span style={{fontStyle:'italic', color: TERRA}}>15 分钟</span>的投资<br/>
|
||||
省下 <span style={{fontStyle:'italic'}}>1–2 小时</span> 返工
|
||||
</div>
|
||||
|
||||
<div style={{height:1, background: INK, width: lineW, marginTop: 40}} />
|
||||
|
||||
<div style={{fontFamily: serif, fontStyle:'italic', fontSize: 24,
|
||||
color: ASH, marginTop: 28, opacity: subOp,
|
||||
letterSpacing:'0.02em'}}>
|
||||
这是稳定性最便宜的投资。
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Watermark ─────────────────────────────────────────────
|
||||
function Watermark() {
|
||||
return (
|
||||
<div style={{position:'absolute', bottom: 24, right: 32,
|
||||
fontSize: 11, color: 'rgba(0,0,0,0.38)', letterSpacing:'0.15em',
|
||||
fontFamily: mono, pointerEvents:'none', zIndex: 100}}>
|
||||
Created by Huashu-Design
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main composition ──────────────────────────────────────
|
||||
function App() {
|
||||
return (
|
||||
<Stage duration={24} width={1920} height={1080} bgColor={CREAM}>
|
||||
<Sprite start={0} end={3}><Scene1_Trigger /></Sprite>
|
||||
<Sprite start={3} end={7}><Scene2_AskAndSearch /></Sprite>
|
||||
<Sprite start={7} end={12}><Scene3_Fallbacks /></Sprite>
|
||||
<Sprite start={12} end={17}><Scene4_GrepColors /></Sprite>
|
||||
<Sprite start={17} end={22}><Scene5_SpecFile /></Sprite>
|
||||
<Sprite start={22} end={24}><Scene6_Final /></Sprite>
|
||||
<Watermark />
|
||||
</Stage>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user