chore: ruler files update
Signed-off-by: Dmytro Stanchiev <git@dmytros.dev>
This commit is contained in:
1145
.claude/skills/huashu-design/demos/c1-ios-prototype-en.html
Normal file
1145
.claude/skills/huashu-design/demos/c1-ios-prototype-en.html
Normal file
File diff suppressed because it is too large
Load Diff
1142
.claude/skills/huashu-design/demos/c1-ios-prototype.html
Normal file
1142
.claude/skills/huashu-design/demos/c1-ios-prototype.html
Normal file
File diff suppressed because it is too large
Load Diff
1055
.claude/skills/huashu-design/demos/c2-slides-pptx-en.html
Normal file
1055
.claude/skills/huashu-design/demos/c2-slides-pptx-en.html
Normal file
File diff suppressed because it is too large
Load Diff
1055
.claude/skills/huashu-design/demos/c2-slides-pptx.html
Normal file
1055
.claude/skills/huashu-design/demos/c2-slides-pptx.html
Normal file
File diff suppressed because it is too large
Load Diff
1134
.claude/skills/huashu-design/demos/c3-motion-design-en.html
Normal file
1134
.claude/skills/huashu-design/demos/c3-motion-design-en.html
Normal file
File diff suppressed because it is too large
Load Diff
1134
.claude/skills/huashu-design/demos/c3-motion-design.html
Normal file
1134
.claude/skills/huashu-design/demos/c3-motion-design.html
Normal file
File diff suppressed because it is too large
Load Diff
989
.claude/skills/huashu-design/demos/c4-tweaks-en.html
Normal file
989
.claude/skills/huashu-design/demos/c4-tweaks-en.html
Normal file
@@ -0,0 +1,989 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>c4-tweaks · Slide. See it morph. (English)</title>
|
||||
<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=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@300;400;500;600;700&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #000000;
|
||||
--ink: #FFFFFF;
|
||||
--ink-80: rgba(255,255,255,0.82);
|
||||
--ink-60: rgba(255,255,255,0.58);
|
||||
--muted: rgba(255,255,255,0.40);
|
||||
--hairline: rgba(255,255,255,0.12);
|
||||
--accent: #D97757;
|
||||
--accent-deep: #B85D3D;
|
||||
|
||||
/* Mock landing page · warm variant (initial state) */
|
||||
--warm-bg: #F6EFE6;
|
||||
--warm-panel: #FFFFFF;
|
||||
--warm-ink: #1A1918;
|
||||
--warm-dim: #8B867E;
|
||||
--warm-hair: rgba(0,0,0,0.08);
|
||||
--warm-accent: #D97757;
|
||||
|
||||
/* Mock landing page · cool variant (after slider 1) */
|
||||
--cool-bg: #0E1620;
|
||||
--cool-panel: #17222E;
|
||||
--cool-ink: #E8EEF5;
|
||||
--cool-dim: #7A8A9B;
|
||||
--cool-hair: rgba(255,255,255,0.08);
|
||||
--cool-accent: #5A8CB8;
|
||||
|
||||
--serif-en: "Source Serif 4", Georgia, serif;
|
||||
--serif-cn: "Noto Serif SC", "Source Serif 4", Georgia, serif;
|
||||
--sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
|
||||
--mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
|
||||
}
|
||||
html, body {
|
||||
margin: 0; padding: 0;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
font-family: var(--sans);
|
||||
color: var(--ink);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
.stage {
|
||||
position: fixed;
|
||||
top: 50%; left: 50%;
|
||||
width: 1920px; height: 1080px;
|
||||
transform: translate(-50%, -50%);
|
||||
transform-origin: center center;
|
||||
background: var(--bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Film grain */
|
||||
.grain {
|
||||
position: absolute; inset: 0;
|
||||
background-image:
|
||||
radial-gradient(rgba(255,255,255,0.02) 1px, transparent 1px);
|
||||
background-size: 3px 3px;
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Watermark */
|
||||
.watermark {
|
||||
position: absolute;
|
||||
top: 44px; left: 56px;
|
||||
font-family: var(--mono);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.2em;
|
||||
color: rgba(255,255,255,0.16);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.version-mark {
|
||||
position: absolute;
|
||||
bottom: 44px; right: 56px;
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.2em;
|
||||
color: rgba(255,255,255,0.12);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* ============ Main composition ============ */
|
||||
.composition {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1080px 500px;
|
||||
gap: 80px;
|
||||
padding: 130px 120px 140px 140px;
|
||||
align-items: center;
|
||||
perspective: 2400px;
|
||||
}
|
||||
|
||||
/* ---- Design preview (left) ---- */
|
||||
.preview-frame {
|
||||
position: relative;
|
||||
width: 1080px;
|
||||
height: 800px;
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
transform-style: preserve-3d;
|
||||
transform: rotateX(6deg) rotateY(-4deg);
|
||||
box-shadow:
|
||||
0 50px 120px rgba(0,0,0,0.6),
|
||||
0 0 0 1px rgba(255,255,255,0.06);
|
||||
opacity: 0;
|
||||
will-change: opacity, transform, background;
|
||||
transition: background 280ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.preview-frame.warm {
|
||||
background: var(--warm-bg);
|
||||
}
|
||||
.preview-frame.cool {
|
||||
background: var(--cool-bg);
|
||||
}
|
||||
|
||||
/* Browser chrome top bar */
|
||||
.browser-chrome {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 16px 22px;
|
||||
border-bottom: 1px solid var(--warm-hair);
|
||||
background: var(--warm-panel);
|
||||
transition: all 280ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.cool .browser-chrome {
|
||||
background: var(--cool-panel);
|
||||
border-bottom-color: var(--cool-hair);
|
||||
}
|
||||
.dot {
|
||||
width: 11px; height: 11px; border-radius: 50%;
|
||||
background: rgba(0,0,0,0.14);
|
||||
}
|
||||
.cool .dot { background: rgba(255,255,255,0.14); }
|
||||
.url-bar {
|
||||
flex: 1;
|
||||
margin-left: 14px;
|
||||
padding: 6px 14px;
|
||||
border-radius: 6px;
|
||||
background: rgba(0,0,0,0.04);
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
color: var(--warm-dim);
|
||||
letter-spacing: 0.05em;
|
||||
transition: all 280ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.cool .url-bar {
|
||||
background: rgba(255,255,255,0.04);
|
||||
color: var(--cool-dim);
|
||||
}
|
||||
|
||||
/* Hero content */
|
||||
.preview-body {
|
||||
padding: 54px 72px 60px 72px;
|
||||
color: var(--warm-ink);
|
||||
transition: color 280ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.cool .preview-body { color: var(--cool-ink); }
|
||||
|
||||
.preview-eyebrow {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.24em;
|
||||
text-transform: uppercase;
|
||||
color: var(--warm-accent);
|
||||
transition: color 280ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.cool .preview-eyebrow { color: var(--cool-accent); }
|
||||
|
||||
.preview-title {
|
||||
margin-top: 16px;
|
||||
font-family: var(--serif-en);
|
||||
font-weight: 400;
|
||||
font-size: 86px;
|
||||
line-height: 1.02;
|
||||
letter-spacing: -0.02em;
|
||||
transition: font-family 240ms cubic-bezier(.2,.8,.2,1),
|
||||
font-weight 240ms cubic-bezier(.2,.8,.2,1),
|
||||
letter-spacing 240ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.preview-title .em {
|
||||
color: var(--warm-accent);
|
||||
font-style: italic;
|
||||
transition: color 280ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.cool .preview-title .em { color: var(--cool-accent); }
|
||||
|
||||
.preview-frame.sans .preview-title {
|
||||
font-family: var(--sans);
|
||||
font-weight: 200;
|
||||
letter-spacing: -0.045em;
|
||||
}
|
||||
.preview-frame.sans .preview-title .em {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.preview-sub {
|
||||
margin-top: 24px;
|
||||
font-family: var(--serif-en);
|
||||
font-size: 20px;
|
||||
font-weight: 300;
|
||||
line-height: 1.6;
|
||||
max-width: 720px;
|
||||
color: var(--warm-dim);
|
||||
transition: color 280ms cubic-bezier(.2,.8,.2,1),
|
||||
font-family 240ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.cool .preview-sub { color: var(--cool-dim); }
|
||||
.preview-frame.sans .preview-sub {
|
||||
font-family: var(--sans);
|
||||
}
|
||||
|
||||
/* Density cards grid */
|
||||
.card-grid {
|
||||
margin-top: 54px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 18px;
|
||||
transition: grid-template-columns 280ms cubic-bezier(.2,.8,.2,1),
|
||||
gap 280ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.preview-frame.dense .card-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-auto-rows: minmax(72px, auto);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 22px 22px 24px 22px;
|
||||
border-radius: 10px;
|
||||
background: rgba(0,0,0,0.035);
|
||||
border: 1px solid var(--warm-hair);
|
||||
transition: all 280ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.cool .card {
|
||||
background: rgba(255,255,255,0.03);
|
||||
border-color: var(--cool-hair);
|
||||
}
|
||||
.preview-frame.dense .card {
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 28px; height: 28px;
|
||||
border-radius: 6px;
|
||||
background: var(--warm-accent);
|
||||
opacity: 0.16;
|
||||
margin-bottom: 14px;
|
||||
transition: all 280ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.cool .card-icon { background: var(--cool-accent); }
|
||||
.preview-frame.dense .card-icon {
|
||||
width: 18px; height: 18px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-family: var(--serif-en);
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: var(--warm-ink);
|
||||
letter-spacing: -0.005em;
|
||||
transition: color 280ms cubic-bezier(.2,.8,.2,1),
|
||||
font-family 240ms cubic-bezier(.2,.8,.2,1),
|
||||
font-size 280ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.cool .card-title { color: var(--cool-ink); }
|
||||
.preview-frame.sans .card-title {
|
||||
font-family: var(--sans);
|
||||
font-weight: 500;
|
||||
}
|
||||
.preview-frame.dense .card-title {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
margin-top: 6px;
|
||||
font-family: var(--serif-en);
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
color: var(--warm-dim);
|
||||
transition: all 280ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.cool .card-text { color: var(--cool-dim); }
|
||||
.preview-frame.sans .card-text { font-family: var(--sans); }
|
||||
.preview-frame.dense .card-text {
|
||||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* Extra cards (hidden in sparse mode) */
|
||||
.card.extra {
|
||||
opacity: 0;
|
||||
transform: scale(0.92);
|
||||
transition: opacity 240ms cubic-bezier(.2,.8,.2,1),
|
||||
transform 240ms cubic-bezier(.2,.8,.2,1),
|
||||
background 280ms cubic-bezier(.2,.8,.2,1),
|
||||
border-color 280ms cubic-bezier(.2,.8,.2,1);
|
||||
pointer-events: none;
|
||||
max-height: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.preview-frame.dense .card.extra {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
max-height: 120px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
/* ---- Slider panel (right) ---- */
|
||||
.slider-panel {
|
||||
position: relative;
|
||||
width: 500px;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 64px;
|
||||
}
|
||||
|
||||
.anchor-line {
|
||||
position: absolute;
|
||||
top: -80px;
|
||||
left: 8px;
|
||||
font-family: var(--serif-en);
|
||||
font-weight: 400;
|
||||
font-size: 26px;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--ink-80);
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
.anchor-line .em {
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.slider-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.slider-label {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.slider-name {
|
||||
font-family: var(--mono);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
color: var(--ink-80);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.slider-value {
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.14em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* Track */
|
||||
.track {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--hairline);
|
||||
}
|
||||
.track-fill {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
height: 100%;
|
||||
width: 10%;
|
||||
background: var(--accent);
|
||||
will-change: width;
|
||||
}
|
||||
|
||||
/* Tick marks */
|
||||
.ticks {
|
||||
position: absolute;
|
||||
inset: -4px 0 -4px 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
pointer-events: none;
|
||||
}
|
||||
.tick {
|
||||
width: 1px;
|
||||
height: 10px;
|
||||
background: rgba(255,255,255,0.14);
|
||||
}
|
||||
|
||||
/* Knob */
|
||||
.knob {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 10%;
|
||||
width: 26px; height: 26px;
|
||||
border-radius: 50%;
|
||||
background: var(--ink);
|
||||
transform: translate(-50%, -50%);
|
||||
box-shadow: 0 0 0 1px rgba(0,0,0,0.6),
|
||||
0 8px 24px rgba(0,0,0,0.5);
|
||||
will-change: left, transform, box-shadow;
|
||||
}
|
||||
.knob.active {
|
||||
box-shadow: 0 0 0 2px var(--accent),
|
||||
0 0 30px rgba(217,119,87,0.45),
|
||||
0 8px 24px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
/* Cursor */
|
||||
.cursor {
|
||||
position: absolute;
|
||||
width: 20px; height: 20px;
|
||||
pointer-events: none;
|
||||
will-change: left, top, opacity;
|
||||
opacity: 0;
|
||||
z-index: 20;
|
||||
}
|
||||
.cursor svg { width: 100%; height: 100%; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.8)); }
|
||||
|
||||
/* ---- Brand reveal ---- */
|
||||
/* Stage dimmer: fades the composition out just before the panel slides in */
|
||||
.stage-dimmer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #000000;
|
||||
opacity: 0;
|
||||
z-index: 40;
|
||||
pointer-events: none;
|
||||
will-change: opacity;
|
||||
}
|
||||
.brand-panel {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #F5F4F0;
|
||||
transform: translateY(100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 50;
|
||||
will-change: transform;
|
||||
}
|
||||
.brand-wordmark {
|
||||
font-family: var(--serif-en);
|
||||
font-size: 72px;
|
||||
font-weight: 100;
|
||||
font-variation-settings: "wght" 100;
|
||||
letter-spacing: -0.02em;
|
||||
color: #1A1918;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
will-change: opacity, transform, font-variation-settings, font-weight;
|
||||
}
|
||||
.brand-wordmark .accent { color: #D97757; font-weight: inherit; }
|
||||
|
||||
.brand-line {
|
||||
/* Flex-centered, 60px below wordmark (line-height 1 @ 72px → descender + 24 gap) */
|
||||
margin-top: 60px;
|
||||
height: 2px;
|
||||
width: 0;
|
||||
background: #D97757;
|
||||
align-self: center;
|
||||
will-change: width;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="stage" id="stage">
|
||||
|
||||
<div class="grain"></div>
|
||||
<div class="watermark">HUASHU · DESIGN</div>
|
||||
<div class="version-mark">V2 · 2026</div>
|
||||
|
||||
<div class="composition">
|
||||
|
||||
<!-- LEFT: design preview -->
|
||||
<div class="preview-frame warm" id="preview">
|
||||
<div class="browser-chrome">
|
||||
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
|
||||
<div class="url-bar">yourbrand.design</div>
|
||||
</div>
|
||||
<div class="preview-body">
|
||||
<div class="preview-eyebrow">Agent Studio</div>
|
||||
<div class="preview-title">Built for <span class="em">them</span>.<br/>Who never sleep.</div>
|
||||
<div class="preview-sub">A design system that ships while you rest — ready before you open the file.</div>
|
||||
|
||||
<div class="card-grid" id="cardGrid">
|
||||
<div class="card">
|
||||
<div class="card-icon"></div>
|
||||
<div class="card-title">Brand Assets</div>
|
||||
<div class="card-text">Logos, palettes, type — one source of truth.</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon"></div>
|
||||
<div class="card-title">Prototype</div>
|
||||
<div class="card-text">One sentence in, a clickable app out.</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon"></div>
|
||||
<div class="card-title">Motion</div>
|
||||
<div class="card-text">Timeline is code. Swap 25 for 60 fps.</div>
|
||||
</div>
|
||||
<div class="card extra">
|
||||
<div class="card-icon"></div>
|
||||
<div class="card-title">Slides</div>
|
||||
<div class="card-text">HTML is PPTX.</div>
|
||||
</div>
|
||||
<div class="card extra">
|
||||
<div class="card-icon"></div>
|
||||
<div class="card-title">Infographic</div>
|
||||
<div class="card-text">Data in, magazine out.</div>
|
||||
</div>
|
||||
<div class="card extra">
|
||||
<div class="card-icon"></div>
|
||||
<div class="card-title">Review</div>
|
||||
<div class="card-text">Five axes. Honest punch list.</div>
|
||||
</div>
|
||||
<div class="card extra">
|
||||
<div class="card-icon"></div>
|
||||
<div class="card-title">Advisor</div>
|
||||
<div class="card-text">Three roads. You pick.</div>
|
||||
</div>
|
||||
<div class="card extra">
|
||||
<div class="card-icon"></div>
|
||||
<div class="card-title">Junior</div>
|
||||
<div class="card-text">Show first. Polish later.</div>
|
||||
</div>
|
||||
<div class="card extra">
|
||||
<div class="card-icon"></div>
|
||||
<div class="card-title">Protocol</div>
|
||||
<div class="card-text">Five steps. No skip.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT: slider panel -->
|
||||
<div class="slider-panel" id="panel">
|
||||
|
||||
<div class="anchor-line" id="anchor">
|
||||
Slide. <span class="em">See it morph.</span>
|
||||
</div>
|
||||
|
||||
<!-- Slider 1 · palette -->
|
||||
<div class="slider-item">
|
||||
<div class="slider-label">
|
||||
<span class="slider-name">Palette</span>
|
||||
<span class="slider-value" id="val1">warm</span>
|
||||
</div>
|
||||
<div class="track">
|
||||
<div class="ticks">
|
||||
<span class="tick"></span><span class="tick"></span><span class="tick"></span>
|
||||
<span class="tick"></span><span class="tick"></span>
|
||||
</div>
|
||||
<div class="track-fill" id="fill1"></div>
|
||||
<div class="knob" id="knob1"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slider 2 · type -->
|
||||
<div class="slider-item">
|
||||
<div class="slider-label">
|
||||
<span class="slider-name">Type</span>
|
||||
<span class="slider-value" id="val2">serif</span>
|
||||
</div>
|
||||
<div class="track">
|
||||
<div class="ticks">
|
||||
<span class="tick"></span><span class="tick"></span><span class="tick"></span>
|
||||
<span class="tick"></span><span class="tick"></span>
|
||||
</div>
|
||||
<div class="track-fill" id="fill2"></div>
|
||||
<div class="knob" id="knob2"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slider 3 · density -->
|
||||
<div class="slider-item">
|
||||
<div class="slider-label">
|
||||
<span class="slider-name">Density</span>
|
||||
<span class="slider-value" id="val3">sparse</span>
|
||||
</div>
|
||||
<div class="track">
|
||||
<div class="ticks">
|
||||
<span class="tick"></span><span class="tick"></span><span class="tick"></span>
|
||||
<span class="tick"></span><span class="tick"></span>
|
||||
</div>
|
||||
<div class="track-fill" id="fill3"></div>
|
||||
<div class="knob" id="knob3"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Cursor -->
|
||||
<div class="cursor" id="cursor">
|
||||
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 2 L2 16 L6 12 L9 18 L11 17 L8 11 L14 11 Z"
|
||||
fill="white" stroke="#000" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stage dimmer (fades scene to black before panel sweeps in) -->
|
||||
<div class="stage-dimmer" id="stageDimmer"></div>
|
||||
|
||||
<!-- Brand reveal layer -->
|
||||
<div class="brand-panel" id="brandPanel">
|
||||
<div class="brand-wordmark" id="brandMark">huashu<span class="accent">-</span>design</div>
|
||||
<div class="brand-line" id="brandLine"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
// ---------- Fit stage ----------
|
||||
const stage = document.getElementById('stage');
|
||||
function rescale() {
|
||||
const s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
|
||||
stage.style.transform = `translate(-50%, -50%) scale(${s})`;
|
||||
}
|
||||
rescale();
|
||||
window.addEventListener('resize', rescale);
|
||||
|
||||
// ---------- Animation ----------
|
||||
const DURATION = 10.0; // seconds
|
||||
|
||||
const preview = document.getElementById('preview');
|
||||
const panel = document.getElementById('panel');
|
||||
const anchor = document.getElementById('anchor');
|
||||
const cursor = document.getElementById('cursor');
|
||||
|
||||
const knob1 = document.getElementById('knob1');
|
||||
const knob2 = document.getElementById('knob2');
|
||||
const knob3 = document.getElementById('knob3');
|
||||
const fill1 = document.getElementById('fill1');
|
||||
const fill2 = document.getElementById('fill2');
|
||||
const fill3 = document.getElementById('fill3');
|
||||
const val1 = document.getElementById('val1');
|
||||
const val2 = document.getElementById('val2');
|
||||
const val3 = document.getElementById('val3');
|
||||
|
||||
const stageDimmer = document.getElementById('stageDimmer');
|
||||
const brandPanel = document.getElementById('brandPanel');
|
||||
const brandMark = document.getElementById('brandMark');
|
||||
const brandLine = document.getElementById('brandLine');
|
||||
|
||||
// Easings
|
||||
const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
|
||||
const expoIn = t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
|
||||
const cubicInOut = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2;
|
||||
const cubicOut = t => 1 - Math.pow(1 - t, 3);
|
||||
|
||||
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
|
||||
function lerp(t, t0, t1, v0, v1, ease) {
|
||||
if (t <= t0) return v0;
|
||||
if (t >= t1) return v1;
|
||||
const k = (t - t0) / (t1 - t0);
|
||||
return v0 + (v1 - v0) * (ease ? ease(k) : k);
|
||||
}
|
||||
function clampLerp(t, t0, t1) {
|
||||
if (t <= t0) return 0;
|
||||
if (t >= t1) return 1;
|
||||
return (t - t0) / (t1 - t0);
|
||||
}
|
||||
|
||||
// Knob motion — drag feel: first 70% is a cubic ease (hand moving),
|
||||
// final 15% is overshoot + snap to target (magnetic arrival).
|
||||
function knobMotion(t, t0, t1, fromPct, toPct) {
|
||||
if (t <= t0) return fromPct;
|
||||
if (t >= t1) return toPct;
|
||||
const k = (t - t0) / (t1 - t0);
|
||||
const direction = toPct > fromPct ? 1 : -1;
|
||||
const range = Math.abs(toPct - fromPct);
|
||||
|
||||
if (k < 0.72) {
|
||||
// Main drag: cubic easeInOut feels like a hand moving
|
||||
const e = cubicInOut(k / 0.72);
|
||||
return fromPct + (toPct - fromPct) * e;
|
||||
} else if (k < 0.85) {
|
||||
// Overshoot past target by ~2%
|
||||
const overK = (k - 0.72) / 0.13;
|
||||
const overshoot = 2.2;
|
||||
return toPct + direction * overshoot * Math.sin(overK * Math.PI);
|
||||
} else {
|
||||
// Settled at target
|
||||
return toPct;
|
||||
}
|
||||
}
|
||||
|
||||
// Timeline (seconds, 10s total)
|
||||
const T = {
|
||||
stage_in: [0.0, 1.0], // frame + panel appear
|
||||
anchor_in: [0.8, 1.4],
|
||||
|
||||
// Slider 1 · palette: warm → cool (1.2s → 3.2s) — arrive at 3.0s
|
||||
s1_cursor_to: [1.3, 1.9],
|
||||
s1_drag: [1.9, 2.9],
|
||||
s1_settle: [2.9, 3.1],
|
||||
|
||||
// Slider 2 · type: serif → sans
|
||||
s2_cursor_to: [3.2, 3.7],
|
||||
s2_drag: [3.7, 4.7],
|
||||
s2_settle: [4.7, 4.9],
|
||||
|
||||
// Slider 3 · density: sparse → dense
|
||||
s3_cursor_to: [5.0, 5.5],
|
||||
s3_drag: [5.5, 6.5],
|
||||
s3_settle: [6.5, 6.7],
|
||||
|
||||
hold: [6.7, 8.0],
|
||||
|
||||
// Brand reveal (cream walloff · aligned with hero-v10 signature)
|
||||
scene_out: [8.0, 8.3], // main composition fade to black (0.3s)
|
||||
brand_panel: [8.3, 8.7], // cream panel sweeps up from bottom, expoOut (0.4s)
|
||||
brand_mark: [8.7, 9.3], // wordmark: wght 100→500 + y 20→0 + opacity 0→1 (0.6s)
|
||||
brand_line: [9.3, 9.7], // orange line expands 0→280 from center (0.4s)
|
||||
brand_hold: [9.7, 10.0], // hold final frame
|
||||
};
|
||||
|
||||
// Slider-to-state logic. Value-changes happen at settle start.
|
||||
let state = { palette: 'warm', type: 'serif', density: 'sparse' };
|
||||
let lastStateHash = '';
|
||||
function updatePreview() {
|
||||
preview.classList.remove('warm', 'cool', 'sans', 'dense');
|
||||
if (state.palette === 'warm') preview.classList.add('warm');
|
||||
else preview.classList.add('cool');
|
||||
if (state.type === 'sans') preview.classList.add('sans');
|
||||
if (state.density === 'dense') preview.classList.add('dense');
|
||||
}
|
||||
updatePreview();
|
||||
|
||||
function setKnobState(knob, active) {
|
||||
if (active) knob.classList.add('active');
|
||||
else knob.classList.remove('active');
|
||||
}
|
||||
|
||||
function setValueLabel(el, text) {
|
||||
if (el.textContent !== text) el.textContent = text;
|
||||
}
|
||||
|
||||
// ---------- Cursor path (in composition coords) ----------
|
||||
// Composition uses grid: left column 1220 + 60 gap, panel is at right.
|
||||
// We'll position cursor using .composition-relative absolute positioning.
|
||||
// Cursor is child of .composition, whose padding is 130/100/140/140.
|
||||
// So coords relative to .composition padding-box.
|
||||
// Simpler: cursor is absolute in .stage coords since parent composition
|
||||
// covers full stage. Use inline style left/top in px.
|
||||
|
||||
// Anchor positions (rough — will fine-tune):
|
||||
const CURSOR_PARK = { x: 1900, y: 1080 }; // off-screen bottom-right
|
||||
// Slider tracks: panel starts around x≈1420, width 520. Each track spans that width.
|
||||
// We'll measure actual rect at first tick.
|
||||
let sliderRects = null;
|
||||
function measureRects() {
|
||||
const stageRect = stage.getBoundingClientRect();
|
||||
const scale = stageRect.width / 1920;
|
||||
const getTrackBox = (id) => {
|
||||
const el = document.getElementById(id).parentElement; // .track
|
||||
const r = el.getBoundingClientRect();
|
||||
return {
|
||||
left: (r.left - stageRect.left) / scale,
|
||||
top: (r.top - stageRect.top) / scale,
|
||||
width: r.width / scale,
|
||||
height: r.height / scale,
|
||||
};
|
||||
};
|
||||
sliderRects = {
|
||||
s1: getTrackBox('knob1'),
|
||||
s2: getTrackBox('knob2'),
|
||||
s3: getTrackBox('knob3'),
|
||||
};
|
||||
}
|
||||
|
||||
function positionCursor(x, y, opacity) {
|
||||
cursor.style.left = x + 'px';
|
||||
cursor.style.top = y + 'px';
|
||||
cursor.style.opacity = opacity;
|
||||
}
|
||||
|
||||
function knobLeft(id, pct) {
|
||||
const el = document.getElementById(id);
|
||||
el.style.left = pct + '%';
|
||||
}
|
||||
function fillWidth(id, pct) {
|
||||
const el = document.getElementById(id);
|
||||
el.style.width = pct + '%';
|
||||
}
|
||||
|
||||
// Tick / render
|
||||
let startTs = null;
|
||||
let frameCount = 0;
|
||||
|
||||
function tick(ts) {
|
||||
if (!startTs) startTs = ts;
|
||||
const t = (ts - startTs) / 1000;
|
||||
|
||||
// Measure rects once
|
||||
if (!sliderRects && frameCount > 1) {
|
||||
measureRects();
|
||||
}
|
||||
|
||||
// --- Stage in ---
|
||||
const stageK = clampLerp(t, T.stage_in[0], T.stage_in[1]);
|
||||
const stageOp = cubicOut(stageK);
|
||||
preview.style.opacity = stageOp;
|
||||
preview.style.transform = `rotateX(${lerp(t, T.stage_in[0], T.stage_in[1], 10, 6, cubicOut)}deg) rotateY(-4deg) translateY(${lerp(t, T.stage_in[0], T.stage_in[1], 20, 0, expoOut)}px)`;
|
||||
panel.style.opacity = stageOp;
|
||||
panel.style.transform = `translateX(${lerp(t, T.stage_in[0], T.stage_in[1], 30, 0, expoOut)}px)`;
|
||||
|
||||
// Anchor
|
||||
const aK = clampLerp(t, T.anchor_in[0], T.anchor_in[1]);
|
||||
anchor.style.opacity = cubicOut(aK);
|
||||
anchor.style.transform = `translateY(${lerp(t, T.anchor_in[0], T.anchor_in[1], 10, 0, expoOut)}px)`;
|
||||
|
||||
// Snap point: when knob reaches target (72% of drag duration)
|
||||
const s1SnapT = T.s1_drag[0] + (T.s1_drag[1] - T.s1_drag[0]) * 0.72;
|
||||
const s2SnapT = T.s2_drag[0] + (T.s2_drag[1] - T.s2_drag[0]) * 0.72;
|
||||
const s3SnapT = T.s3_drag[0] + (T.s3_drag[1] - T.s3_drag[0]) * 0.72;
|
||||
|
||||
// --- Slider 1: palette ---
|
||||
// Knob 10% → 90%
|
||||
const k1pct = knobMotion(t, T.s1_drag[0], T.s1_drag[1], 10, 90);
|
||||
knobLeft('knob1', k1pct); fillWidth('fill1', k1pct);
|
||||
setKnobState(knob1, t >= T.s1_cursor_to[0] && t < T.s1_settle[1] + 0.2);
|
||||
if (t >= s1SnapT && state.palette !== 'cool') {
|
||||
state.palette = 'cool'; updatePreview(); setValueLabel(val1, 'cool');
|
||||
}
|
||||
|
||||
// --- Slider 2: type ---
|
||||
const k2pct = knobMotion(t, T.s2_drag[0], T.s2_drag[1], 10, 90);
|
||||
knobLeft('knob2', k2pct); fillWidth('fill2', k2pct);
|
||||
setKnobState(knob2, t >= T.s2_cursor_to[0] && t < T.s2_settle[1] + 0.2);
|
||||
if (t >= s2SnapT && state.type !== 'sans') {
|
||||
state.type = 'sans'; updatePreview(); setValueLabel(val2, 'sans');
|
||||
}
|
||||
|
||||
// --- Slider 3: density ---
|
||||
const k3pct = knobMotion(t, T.s3_drag[0], T.s3_drag[1], 10, 90);
|
||||
knobLeft('knob3', k3pct); fillWidth('fill3', k3pct);
|
||||
setKnobState(knob3, t >= T.s3_cursor_to[0] && t < T.s3_settle[1] + 0.2);
|
||||
if (t >= s3SnapT && state.density !== 'dense') {
|
||||
state.density = 'dense'; updatePreview(); setValueLabel(val3, 'dense');
|
||||
}
|
||||
|
||||
// --- Cursor choreography ---
|
||||
if (sliderRects) {
|
||||
const r1 = sliderRects.s1, r2 = sliderRects.s2, r3 = sliderRects.s3;
|
||||
// Positions of knob at 10% and 90%
|
||||
const k1Start = { x: r1.left + r1.width * 0.10, y: r1.top + r1.height/2 };
|
||||
const k1End = { x: r1.left + r1.width * 0.90, y: r1.top + r1.height/2 };
|
||||
const k2Start = { x: r2.left + r2.width * 0.10, y: r2.top + r2.height/2 };
|
||||
const k2End = { x: r2.left + r2.width * 0.90, y: r2.top + r2.height/2 };
|
||||
const k3Start = { x: r3.left + r3.width * 0.10, y: r3.top + r3.height/2 };
|
||||
const k3End = { x: r3.left + r3.width * 0.90, y: r3.top + r3.height/2 };
|
||||
|
||||
let cx = CURSOR_PARK.x, cy = CURSOR_PARK.y, co = 0;
|
||||
|
||||
if (t < T.s1_cursor_to[0]) {
|
||||
// still off-screen (or just appeared)
|
||||
cx = CURSOR_PARK.x; cy = CURSOR_PARK.y; co = 0;
|
||||
} else if (t < T.s1_cursor_to[1]) {
|
||||
// cursor flies to s1 knob start
|
||||
const k = clampLerp(t, T.s1_cursor_to[0], T.s1_cursor_to[1]);
|
||||
const e = cubicOut(k);
|
||||
cx = lerp(t, T.s1_cursor_to[0], T.s1_cursor_to[1], CURSOR_PARK.x, k1Start.x, cubicOut);
|
||||
cy = lerp(t, T.s1_cursor_to[0], T.s1_cursor_to[1], CURSOR_PARK.y, k1Start.y, cubicOut);
|
||||
co = e;
|
||||
} else if (t < T.s1_drag[1]) {
|
||||
// dragging s1
|
||||
cx = r1.left + (r1.width * k1pct / 100);
|
||||
cy = r1.top + r1.height/2;
|
||||
co = 1;
|
||||
} else if (t < T.s2_cursor_to[0]) {
|
||||
cx = k1End.x; cy = k1End.y; co = 1;
|
||||
} else if (t < T.s2_cursor_to[1]) {
|
||||
cx = lerp(t, T.s2_cursor_to[0], T.s2_cursor_to[1], k1End.x, k2Start.x, cubicInOut);
|
||||
cy = lerp(t, T.s2_cursor_to[0], T.s2_cursor_to[1], k1End.y, k2Start.y, cubicInOut);
|
||||
co = 1;
|
||||
} else if (t < T.s2_drag[1]) {
|
||||
cx = r2.left + (r2.width * k2pct / 100);
|
||||
cy = r2.top + r2.height/2;
|
||||
co = 1;
|
||||
} else if (t < T.s3_cursor_to[0]) {
|
||||
cx = k2End.x; cy = k2End.y; co = 1;
|
||||
} else if (t < T.s3_cursor_to[1]) {
|
||||
cx = lerp(t, T.s3_cursor_to[0], T.s3_cursor_to[1], k2End.x, k3Start.x, cubicInOut);
|
||||
cy = lerp(t, T.s3_cursor_to[0], T.s3_cursor_to[1], k2End.y, k3Start.y, cubicInOut);
|
||||
co = 1;
|
||||
} else if (t < T.s3_drag[1]) {
|
||||
cx = r3.left + (r3.width * k3pct / 100);
|
||||
cy = r3.top + r3.height/2;
|
||||
co = 1;
|
||||
} else if (t < T.hold[1]) {
|
||||
// fade out cursor
|
||||
cx = k3End.x; cy = k3End.y;
|
||||
co = lerp(t, T.s3_drag[1], T.hold[1], 1, 0, cubicOut);
|
||||
}
|
||||
|
||||
positionCursor(cx, cy, co);
|
||||
}
|
||||
|
||||
// --- Brand reveal (cream walloff · aligned with hero-v10 signature) ---
|
||||
// 1) Scene dimmer: composition fades to black (0.3s)
|
||||
const soK = clampLerp(t, T.scene_out[0], T.scene_out[1]);
|
||||
stageDimmer.style.opacity = cubicOut(soK);
|
||||
|
||||
// 2) Cream panel sweeps up from bottom, expoOut (0.4s)
|
||||
const bpK = clampLerp(t, T.brand_panel[0], T.brand_panel[1]);
|
||||
const panelY = lerp(t, T.brand_panel[0], T.brand_panel[1], 100, 0, expoOut);
|
||||
brandPanel.style.transform = `translateY(${panelY}%)`;
|
||||
|
||||
// 3) Wordmark: font-weight 100→500 + y 20→0 + opacity 0→1, expoOut (0.6s)
|
||||
const bmK = clampLerp(t, T.brand_mark[0], T.brand_mark[1]);
|
||||
const bmE = expoOut(bmK);
|
||||
const wght = 100 + (500 - 100) * bmE;
|
||||
brandMark.style.opacity = bmE;
|
||||
brandMark.style.transform = `translateY(${20 * (1 - bmE)}px)`;
|
||||
brandMark.style.fontWeight = Math.round(wght);
|
||||
brandMark.style.fontVariationSettings = `"wght" ${wght.toFixed(0)}`;
|
||||
|
||||
// 4) Orange line: width 0→280 from center, cubicOut (0.4s)
|
||||
const blK = clampLerp(t, T.brand_line[0], T.brand_line[1]);
|
||||
brandLine.style.width = (280 * cubicOut(blK)) + 'px';
|
||||
|
||||
frameCount++;
|
||||
|
||||
// Loop or stop
|
||||
if (t < DURATION) {
|
||||
requestAnimationFrame(tick);
|
||||
} else {
|
||||
if (window.__recording === true) {
|
||||
// recording mode: hold last frame
|
||||
return;
|
||||
}
|
||||
// Restart after 1s pause (for manual viewing)
|
||||
setTimeout(() => {
|
||||
startTs = null;
|
||||
state = { palette: 'warm', type: 'serif', density: 'sparse' };
|
||||
updatePreview();
|
||||
setValueLabel(val1, 'warm'); setValueLabel(val2, 'serif'); setValueLabel(val3, 'sparse');
|
||||
requestAnimationFrame(tick);
|
||||
}, 900);
|
||||
}
|
||||
}
|
||||
|
||||
// Start animation after fonts ready
|
||||
const startAnim = () => {
|
||||
requestAnimationFrame((ts) => {
|
||||
startTs = ts;
|
||||
window.__ready = true; // signal for render-video.js
|
||||
requestAnimationFrame(tick);
|
||||
});
|
||||
};
|
||||
|
||||
if (document.fonts && document.fonts.ready) {
|
||||
document.fonts.ready.then(startAnim);
|
||||
} else {
|
||||
setTimeout(startAnim, 500);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
989
.claude/skills/huashu-design/demos/c4-tweaks.html
Normal file
989
.claude/skills/huashu-design/demos/c4-tweaks.html
Normal file
@@ -0,0 +1,989 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-Hans">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>c4-tweaks · 拨动即所得(中文版)</title>
|
||||
<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=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@300;400;500;600;700&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #000000;
|
||||
--ink: #FFFFFF;
|
||||
--ink-80: rgba(255,255,255,0.82);
|
||||
--ink-60: rgba(255,255,255,0.58);
|
||||
--muted: rgba(255,255,255,0.40);
|
||||
--hairline: rgba(255,255,255,0.12);
|
||||
--accent: #D97757;
|
||||
--accent-deep: #B85D3D;
|
||||
|
||||
/* Mock landing page · warm variant (initial state) */
|
||||
--warm-bg: #F6EFE6;
|
||||
--warm-panel: #FFFFFF;
|
||||
--warm-ink: #1A1918;
|
||||
--warm-dim: #8B867E;
|
||||
--warm-hair: rgba(0,0,0,0.08);
|
||||
--warm-accent: #D97757;
|
||||
|
||||
/* Mock landing page · cool variant (after slider 1) */
|
||||
--cool-bg: #0E1620;
|
||||
--cool-panel: #17222E;
|
||||
--cool-ink: #E8EEF5;
|
||||
--cool-dim: #7A8A9B;
|
||||
--cool-hair: rgba(255,255,255,0.08);
|
||||
--cool-accent: #5A8CB8;
|
||||
|
||||
--serif-en: "Source Serif 4", Georgia, serif;
|
||||
--serif-cn: "Noto Serif SC", "Source Serif 4", Georgia, serif;
|
||||
--sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
|
||||
--mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
|
||||
}
|
||||
html, body {
|
||||
margin: 0; padding: 0;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
font-family: var(--sans);
|
||||
color: var(--ink);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
.stage {
|
||||
position: fixed;
|
||||
top: 50%; left: 50%;
|
||||
width: 1920px; height: 1080px;
|
||||
transform: translate(-50%, -50%);
|
||||
transform-origin: center center;
|
||||
background: var(--bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Film grain */
|
||||
.grain {
|
||||
position: absolute; inset: 0;
|
||||
background-image:
|
||||
radial-gradient(rgba(255,255,255,0.02) 1px, transparent 1px);
|
||||
background-size: 3px 3px;
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Watermark */
|
||||
.watermark {
|
||||
position: absolute;
|
||||
top: 44px; left: 56px;
|
||||
font-family: var(--mono);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.2em;
|
||||
color: rgba(255,255,255,0.16);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.version-mark {
|
||||
position: absolute;
|
||||
bottom: 44px; right: 56px;
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.2em;
|
||||
color: rgba(255,255,255,0.12);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* ============ Main composition ============ */
|
||||
.composition {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1080px 500px;
|
||||
gap: 80px;
|
||||
padding: 130px 120px 140px 140px;
|
||||
align-items: center;
|
||||
perspective: 2400px;
|
||||
}
|
||||
|
||||
/* ---- Design preview (left) ---- */
|
||||
.preview-frame {
|
||||
position: relative;
|
||||
width: 1080px;
|
||||
height: 800px;
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
transform-style: preserve-3d;
|
||||
transform: rotateX(6deg) rotateY(-4deg);
|
||||
box-shadow:
|
||||
0 50px 120px rgba(0,0,0,0.6),
|
||||
0 0 0 1px rgba(255,255,255,0.06);
|
||||
opacity: 0;
|
||||
will-change: opacity, transform, background;
|
||||
transition: background 280ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.preview-frame.warm {
|
||||
background: var(--warm-bg);
|
||||
}
|
||||
.preview-frame.cool {
|
||||
background: var(--cool-bg);
|
||||
}
|
||||
|
||||
/* Browser chrome top bar */
|
||||
.browser-chrome {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 16px 22px;
|
||||
border-bottom: 1px solid var(--warm-hair);
|
||||
background: var(--warm-panel);
|
||||
transition: all 280ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.cool .browser-chrome {
|
||||
background: var(--cool-panel);
|
||||
border-bottom-color: var(--cool-hair);
|
||||
}
|
||||
.dot {
|
||||
width: 11px; height: 11px; border-radius: 50%;
|
||||
background: rgba(0,0,0,0.14);
|
||||
}
|
||||
.cool .dot { background: rgba(255,255,255,0.14); }
|
||||
.url-bar {
|
||||
flex: 1;
|
||||
margin-left: 14px;
|
||||
padding: 6px 14px;
|
||||
border-radius: 6px;
|
||||
background: rgba(0,0,0,0.04);
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
color: var(--warm-dim);
|
||||
letter-spacing: 0.05em;
|
||||
transition: all 280ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.cool .url-bar {
|
||||
background: rgba(255,255,255,0.04);
|
||||
color: var(--cool-dim);
|
||||
}
|
||||
|
||||
/* Hero content */
|
||||
.preview-body {
|
||||
padding: 54px 72px 60px 72px;
|
||||
color: var(--warm-ink);
|
||||
transition: color 280ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.cool .preview-body { color: var(--cool-ink); }
|
||||
|
||||
.preview-eyebrow {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.24em;
|
||||
text-transform: uppercase;
|
||||
color: var(--warm-accent);
|
||||
transition: color 280ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.cool .preview-eyebrow { color: var(--cool-accent); }
|
||||
|
||||
.preview-title {
|
||||
margin-top: 16px;
|
||||
font-family: var(--serif-cn);
|
||||
font-weight: 400;
|
||||
font-size: 86px;
|
||||
line-height: 1.02;
|
||||
letter-spacing: -0.02em;
|
||||
transition: font-family 240ms cubic-bezier(.2,.8,.2,1),
|
||||
font-weight 240ms cubic-bezier(.2,.8,.2,1),
|
||||
letter-spacing 240ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.preview-title .em {
|
||||
color: var(--warm-accent);
|
||||
font-style: italic;
|
||||
transition: color 280ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.cool .preview-title .em { color: var(--cool-accent); }
|
||||
|
||||
.preview-frame.sans .preview-title {
|
||||
font-family: var(--sans);
|
||||
font-weight: 200;
|
||||
letter-spacing: -0.045em;
|
||||
}
|
||||
.preview-frame.sans .preview-title .em {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.preview-sub {
|
||||
margin-top: 24px;
|
||||
font-family: var(--serif-cn);
|
||||
font-size: 20px;
|
||||
font-weight: 300;
|
||||
line-height: 1.6;
|
||||
max-width: 720px;
|
||||
color: var(--warm-dim);
|
||||
transition: color 280ms cubic-bezier(.2,.8,.2,1),
|
||||
font-family 240ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.cool .preview-sub { color: var(--cool-dim); }
|
||||
.preview-frame.sans .preview-sub {
|
||||
font-family: var(--sans);
|
||||
}
|
||||
|
||||
/* Density cards grid */
|
||||
.card-grid {
|
||||
margin-top: 54px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 18px;
|
||||
transition: grid-template-columns 280ms cubic-bezier(.2,.8,.2,1),
|
||||
gap 280ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.preview-frame.dense .card-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-auto-rows: minmax(72px, auto);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 22px 22px 24px 22px;
|
||||
border-radius: 10px;
|
||||
background: rgba(0,0,0,0.035);
|
||||
border: 1px solid var(--warm-hair);
|
||||
transition: all 280ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.cool .card {
|
||||
background: rgba(255,255,255,0.03);
|
||||
border-color: var(--cool-hair);
|
||||
}
|
||||
.preview-frame.dense .card {
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 28px; height: 28px;
|
||||
border-radius: 6px;
|
||||
background: var(--warm-accent);
|
||||
opacity: 0.16;
|
||||
margin-bottom: 14px;
|
||||
transition: all 280ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.cool .card-icon { background: var(--cool-accent); }
|
||||
.preview-frame.dense .card-icon {
|
||||
width: 18px; height: 18px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-family: var(--serif-cn);
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: var(--warm-ink);
|
||||
letter-spacing: -0.005em;
|
||||
transition: color 280ms cubic-bezier(.2,.8,.2,1),
|
||||
font-family 240ms cubic-bezier(.2,.8,.2,1),
|
||||
font-size 280ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.cool .card-title { color: var(--cool-ink); }
|
||||
.preview-frame.sans .card-title {
|
||||
font-family: var(--sans);
|
||||
font-weight: 500;
|
||||
}
|
||||
.preview-frame.dense .card-title {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
margin-top: 6px;
|
||||
font-family: var(--serif-cn);
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
color: var(--warm-dim);
|
||||
transition: all 280ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.cool .card-text { color: var(--cool-dim); }
|
||||
.preview-frame.sans .card-text { font-family: var(--sans); }
|
||||
.preview-frame.dense .card-text {
|
||||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* Extra cards (hidden in sparse mode) */
|
||||
.card.extra {
|
||||
opacity: 0;
|
||||
transform: scale(0.92);
|
||||
transition: opacity 240ms cubic-bezier(.2,.8,.2,1),
|
||||
transform 240ms cubic-bezier(.2,.8,.2,1),
|
||||
background 280ms cubic-bezier(.2,.8,.2,1),
|
||||
border-color 280ms cubic-bezier(.2,.8,.2,1);
|
||||
pointer-events: none;
|
||||
max-height: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.preview-frame.dense .card.extra {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
max-height: 120px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
/* ---- Slider panel (right) ---- */
|
||||
.slider-panel {
|
||||
position: relative;
|
||||
width: 500px;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 64px;
|
||||
}
|
||||
|
||||
.anchor-line {
|
||||
position: absolute;
|
||||
top: -80px;
|
||||
left: 8px;
|
||||
font-family: var(--serif-cn);
|
||||
font-weight: 400;
|
||||
font-size: 26px;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--ink-80);
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
.anchor-line .em {
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.slider-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.slider-label {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.slider-name {
|
||||
font-family: var(--mono);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
color: var(--ink-80);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.slider-value {
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.14em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* Track */
|
||||
.track {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--hairline);
|
||||
}
|
||||
.track-fill {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
height: 100%;
|
||||
width: 10%;
|
||||
background: var(--accent);
|
||||
will-change: width;
|
||||
}
|
||||
|
||||
/* Tick marks */
|
||||
.ticks {
|
||||
position: absolute;
|
||||
inset: -4px 0 -4px 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
pointer-events: none;
|
||||
}
|
||||
.tick {
|
||||
width: 1px;
|
||||
height: 10px;
|
||||
background: rgba(255,255,255,0.14);
|
||||
}
|
||||
|
||||
/* Knob */
|
||||
.knob {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 10%;
|
||||
width: 26px; height: 26px;
|
||||
border-radius: 50%;
|
||||
background: var(--ink);
|
||||
transform: translate(-50%, -50%);
|
||||
box-shadow: 0 0 0 1px rgba(0,0,0,0.6),
|
||||
0 8px 24px rgba(0,0,0,0.5);
|
||||
will-change: left, transform, box-shadow;
|
||||
}
|
||||
.knob.active {
|
||||
box-shadow: 0 0 0 2px var(--accent),
|
||||
0 0 30px rgba(217,119,87,0.45),
|
||||
0 8px 24px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
/* Cursor */
|
||||
.cursor {
|
||||
position: absolute;
|
||||
width: 20px; height: 20px;
|
||||
pointer-events: none;
|
||||
will-change: left, top, opacity;
|
||||
opacity: 0;
|
||||
z-index: 20;
|
||||
}
|
||||
.cursor svg { width: 100%; height: 100%; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.8)); }
|
||||
|
||||
/* ---- Brand reveal ---- */
|
||||
/* Stage dimmer: fades the composition out just before the panel slides in */
|
||||
.stage-dimmer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #000000;
|
||||
opacity: 0;
|
||||
z-index: 40;
|
||||
pointer-events: none;
|
||||
will-change: opacity;
|
||||
}
|
||||
.brand-panel {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #F5F4F0;
|
||||
transform: translateY(100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 50;
|
||||
will-change: transform;
|
||||
}
|
||||
.brand-wordmark {
|
||||
font-family: var(--serif-en);
|
||||
font-size: 72px;
|
||||
font-weight: 100;
|
||||
font-variation-settings: "wght" 100;
|
||||
letter-spacing: -0.02em;
|
||||
color: #1A1918;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
will-change: opacity, transform, font-variation-settings, font-weight;
|
||||
}
|
||||
.brand-wordmark .accent { color: #D97757; font-weight: inherit; }
|
||||
|
||||
.brand-line {
|
||||
/* Flex-centered, 60px below wordmark (line-height 1 @ 72px → descender + 24 gap) */
|
||||
margin-top: 60px;
|
||||
height: 2px;
|
||||
width: 0;
|
||||
background: #D97757;
|
||||
align-self: center;
|
||||
will-change: width;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="stage" id="stage">
|
||||
|
||||
<div class="grain"></div>
|
||||
<div class="watermark">HUASHU · DESIGN</div>
|
||||
<div class="version-mark">V2 · 2026</div>
|
||||
|
||||
<div class="composition">
|
||||
|
||||
<!-- LEFT: design preview -->
|
||||
<div class="preview-frame warm" id="preview">
|
||||
<div class="browser-chrome">
|
||||
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
|
||||
<div class="url-bar">yourbrand.design</div>
|
||||
</div>
|
||||
<div class="preview-body">
|
||||
<div class="preview-eyebrow">Agent Studio</div>
|
||||
<div class="preview-title">为<span class="em">他们</span>造好<br/>工作的场所。</div>
|
||||
<div class="preview-sub">一个设计系统,不等你打开;它在你睡觉时,已经把草稿交出来了。</div>
|
||||
|
||||
<div class="card-grid" id="cardGrid">
|
||||
<div class="card">
|
||||
<div class="card-icon"></div>
|
||||
<div class="card-title">品牌资产</div>
|
||||
<div class="card-text">Logo / 色板 / 字型的单一事实源。</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon"></div>
|
||||
<div class="card-title">原型工场</div>
|
||||
<div class="card-text">写一句话,得到一个能点的 App。</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon"></div>
|
||||
<div class="card-title">动效引擎</div>
|
||||
<div class="card-text">时间轴即代码,25 到 60 帧随意切。</div>
|
||||
</div>
|
||||
<div class="card extra">
|
||||
<div class="card-icon"></div>
|
||||
<div class="card-title">文档工坊</div>
|
||||
<div class="card-text">HTML 即 PPTX。</div>
|
||||
</div>
|
||||
<div class="card extra">
|
||||
<div class="card-icon"></div>
|
||||
<div class="card-title">信息图</div>
|
||||
<div class="card-text">数据进,杂志出。</div>
|
||||
</div>
|
||||
<div class="card extra">
|
||||
<div class="card-icon"></div>
|
||||
<div class="card-title">专家评审</div>
|
||||
<div class="card-text">五维打分,诚实的体检。</div>
|
||||
</div>
|
||||
<div class="card extra">
|
||||
<div class="card-icon"></div>
|
||||
<div class="card-title">方向顾问</div>
|
||||
<div class="card-text">给你三条路选。</div>
|
||||
</div>
|
||||
<div class="card extra">
|
||||
<div class="card-icon"></div>
|
||||
<div class="card-title">Junior 模式</div>
|
||||
<div class="card-text">先 show,再精修。</div>
|
||||
</div>
|
||||
<div class="card extra">
|
||||
<div class="card-icon"></div>
|
||||
<div class="card-title">品牌协议</div>
|
||||
<div class="card-text">五步,不能跳。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT: slider panel -->
|
||||
<div class="slider-panel" id="panel">
|
||||
|
||||
<div class="anchor-line" id="anchor">
|
||||
拨动<span class="em">即所得</span>
|
||||
</div>
|
||||
|
||||
<!-- Slider 1 · 调色 -->
|
||||
<div class="slider-item">
|
||||
<div class="slider-label">
|
||||
<span class="slider-name">调色</span>
|
||||
<span class="slider-value" id="val1">warm</span>
|
||||
</div>
|
||||
<div class="track">
|
||||
<div class="ticks">
|
||||
<span class="tick"></span><span class="tick"></span><span class="tick"></span>
|
||||
<span class="tick"></span><span class="tick"></span>
|
||||
</div>
|
||||
<div class="track-fill" id="fill1"></div>
|
||||
<div class="knob" id="knob1"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slider 2 · 字型 -->
|
||||
<div class="slider-item">
|
||||
<div class="slider-label">
|
||||
<span class="slider-name">字型</span>
|
||||
<span class="slider-value" id="val2">serif</span>
|
||||
</div>
|
||||
<div class="track">
|
||||
<div class="ticks">
|
||||
<span class="tick"></span><span class="tick"></span><span class="tick"></span>
|
||||
<span class="tick"></span><span class="tick"></span>
|
||||
</div>
|
||||
<div class="track-fill" id="fill2"></div>
|
||||
<div class="knob" id="knob2"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slider 3 · 密度 -->
|
||||
<div class="slider-item">
|
||||
<div class="slider-label">
|
||||
<span class="slider-name">密度</span>
|
||||
<span class="slider-value" id="val3">sparse</span>
|
||||
</div>
|
||||
<div class="track">
|
||||
<div class="ticks">
|
||||
<span class="tick"></span><span class="tick"></span><span class="tick"></span>
|
||||
<span class="tick"></span><span class="tick"></span>
|
||||
</div>
|
||||
<div class="track-fill" id="fill3"></div>
|
||||
<div class="knob" id="knob3"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Cursor -->
|
||||
<div class="cursor" id="cursor">
|
||||
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 2 L2 16 L6 12 L9 18 L11 17 L8 11 L14 11 Z"
|
||||
fill="white" stroke="#000" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stage dimmer (fades scene to black before panel sweeps in) -->
|
||||
<div class="stage-dimmer" id="stageDimmer"></div>
|
||||
|
||||
<!-- Brand reveal layer -->
|
||||
<div class="brand-panel" id="brandPanel">
|
||||
<div class="brand-wordmark" id="brandMark">huashu<span class="accent">-</span>design</div>
|
||||
<div class="brand-line" id="brandLine"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
// ---------- Fit stage ----------
|
||||
const stage = document.getElementById('stage');
|
||||
function rescale() {
|
||||
const s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
|
||||
stage.style.transform = `translate(-50%, -50%) scale(${s})`;
|
||||
}
|
||||
rescale();
|
||||
window.addEventListener('resize', rescale);
|
||||
|
||||
// ---------- Animation ----------
|
||||
const DURATION = 10.0; // seconds
|
||||
|
||||
const preview = document.getElementById('preview');
|
||||
const panel = document.getElementById('panel');
|
||||
const anchor = document.getElementById('anchor');
|
||||
const cursor = document.getElementById('cursor');
|
||||
|
||||
const knob1 = document.getElementById('knob1');
|
||||
const knob2 = document.getElementById('knob2');
|
||||
const knob3 = document.getElementById('knob3');
|
||||
const fill1 = document.getElementById('fill1');
|
||||
const fill2 = document.getElementById('fill2');
|
||||
const fill3 = document.getElementById('fill3');
|
||||
const val1 = document.getElementById('val1');
|
||||
const val2 = document.getElementById('val2');
|
||||
const val3 = document.getElementById('val3');
|
||||
|
||||
const stageDimmer = document.getElementById('stageDimmer');
|
||||
const brandPanel = document.getElementById('brandPanel');
|
||||
const brandMark = document.getElementById('brandMark');
|
||||
const brandLine = document.getElementById('brandLine');
|
||||
|
||||
// Easings
|
||||
const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
|
||||
const expoIn = t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
|
||||
const cubicInOut = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2;
|
||||
const cubicOut = t => 1 - Math.pow(1 - t, 3);
|
||||
|
||||
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
|
||||
function lerp(t, t0, t1, v0, v1, ease) {
|
||||
if (t <= t0) return v0;
|
||||
if (t >= t1) return v1;
|
||||
const k = (t - t0) / (t1 - t0);
|
||||
return v0 + (v1 - v0) * (ease ? ease(k) : k);
|
||||
}
|
||||
function clampLerp(t, t0, t1) {
|
||||
if (t <= t0) return 0;
|
||||
if (t >= t1) return 1;
|
||||
return (t - t0) / (t1 - t0);
|
||||
}
|
||||
|
||||
// Knob motion — drag feel: first 70% is a cubic ease (hand moving),
|
||||
// final 15% is overshoot + snap to target (magnetic arrival).
|
||||
function knobMotion(t, t0, t1, fromPct, toPct) {
|
||||
if (t <= t0) return fromPct;
|
||||
if (t >= t1) return toPct;
|
||||
const k = (t - t0) / (t1 - t0);
|
||||
const direction = toPct > fromPct ? 1 : -1;
|
||||
const range = Math.abs(toPct - fromPct);
|
||||
|
||||
if (k < 0.72) {
|
||||
// Main drag: cubic easeInOut feels like a hand moving
|
||||
const e = cubicInOut(k / 0.72);
|
||||
return fromPct + (toPct - fromPct) * e;
|
||||
} else if (k < 0.85) {
|
||||
// Overshoot past target by ~2%
|
||||
const overK = (k - 0.72) / 0.13;
|
||||
const overshoot = 2.2;
|
||||
return toPct + direction * overshoot * Math.sin(overK * Math.PI);
|
||||
} else {
|
||||
// Settled at target
|
||||
return toPct;
|
||||
}
|
||||
}
|
||||
|
||||
// Timeline (seconds, 10s total)
|
||||
const T = {
|
||||
stage_in: [0.0, 1.0], // frame + panel appear
|
||||
anchor_in: [0.8, 1.4],
|
||||
|
||||
// Slider 1 · palette: warm → cool (1.2s → 3.2s) — arrive at 3.0s
|
||||
s1_cursor_to: [1.3, 1.9],
|
||||
s1_drag: [1.9, 2.9],
|
||||
s1_settle: [2.9, 3.1],
|
||||
|
||||
// Slider 2 · type: serif → sans
|
||||
s2_cursor_to: [3.2, 3.7],
|
||||
s2_drag: [3.7, 4.7],
|
||||
s2_settle: [4.7, 4.9],
|
||||
|
||||
// Slider 3 · density: sparse → dense
|
||||
s3_cursor_to: [5.0, 5.5],
|
||||
s3_drag: [5.5, 6.5],
|
||||
s3_settle: [6.5, 6.7],
|
||||
|
||||
hold: [6.7, 8.0],
|
||||
|
||||
// Brand reveal (米色 walloff · 2s total)
|
||||
scene_out: [8.0, 8.3], // main composition fade to black (0.3s)
|
||||
brand_panel: [8.3, 8.7], // cream panel sweeps up from bottom, expoOut (0.4s)
|
||||
brand_mark: [8.7, 9.3], // wordmark: wght 100→500 + y 20→0 + opacity 0→1 (0.6s)
|
||||
brand_line: [9.3, 9.7], // orange line expands 0→280 from center (0.4s)
|
||||
brand_hold: [9.7, 10.0], // hold final frame
|
||||
};
|
||||
|
||||
// Slider-to-state logic. Value-changes happen at settle start.
|
||||
let state = { palette: 'warm', type: 'serif', density: 'sparse' };
|
||||
let lastStateHash = '';
|
||||
function updatePreview() {
|
||||
preview.classList.remove('warm', 'cool', 'sans', 'dense');
|
||||
if (state.palette === 'warm') preview.classList.add('warm');
|
||||
else preview.classList.add('cool');
|
||||
if (state.type === 'sans') preview.classList.add('sans');
|
||||
if (state.density === 'dense') preview.classList.add('dense');
|
||||
}
|
||||
updatePreview();
|
||||
|
||||
function setKnobState(knob, active) {
|
||||
if (active) knob.classList.add('active');
|
||||
else knob.classList.remove('active');
|
||||
}
|
||||
|
||||
function setValueLabel(el, text) {
|
||||
if (el.textContent !== text) el.textContent = text;
|
||||
}
|
||||
|
||||
// ---------- Cursor path (in composition coords) ----------
|
||||
// Composition uses grid: left column 1220 + 60 gap, panel is at right.
|
||||
// We'll position cursor using .composition-relative absolute positioning.
|
||||
// Cursor is child of .composition, whose padding is 130/100/140/140.
|
||||
// So coords relative to .composition padding-box.
|
||||
// Simpler: cursor is absolute in .stage coords since parent composition
|
||||
// covers full stage. Use inline style left/top in px.
|
||||
|
||||
// Anchor positions (rough — will fine-tune):
|
||||
const CURSOR_PARK = { x: 1900, y: 1080 }; // off-screen bottom-right
|
||||
// Slider tracks: panel starts around x≈1420, width 520. Each track spans that width.
|
||||
// We'll measure actual rect at first tick.
|
||||
let sliderRects = null;
|
||||
function measureRects() {
|
||||
const stageRect = stage.getBoundingClientRect();
|
||||
const scale = stageRect.width / 1920;
|
||||
const getTrackBox = (id) => {
|
||||
const el = document.getElementById(id).parentElement; // .track
|
||||
const r = el.getBoundingClientRect();
|
||||
return {
|
||||
left: (r.left - stageRect.left) / scale,
|
||||
top: (r.top - stageRect.top) / scale,
|
||||
width: r.width / scale,
|
||||
height: r.height / scale,
|
||||
};
|
||||
};
|
||||
sliderRects = {
|
||||
s1: getTrackBox('knob1'),
|
||||
s2: getTrackBox('knob2'),
|
||||
s3: getTrackBox('knob3'),
|
||||
};
|
||||
}
|
||||
|
||||
function positionCursor(x, y, opacity) {
|
||||
cursor.style.left = x + 'px';
|
||||
cursor.style.top = y + 'px';
|
||||
cursor.style.opacity = opacity;
|
||||
}
|
||||
|
||||
function knobLeft(id, pct) {
|
||||
const el = document.getElementById(id);
|
||||
el.style.left = pct + '%';
|
||||
}
|
||||
function fillWidth(id, pct) {
|
||||
const el = document.getElementById(id);
|
||||
el.style.width = pct + '%';
|
||||
}
|
||||
|
||||
// Tick / render
|
||||
let startTs = null;
|
||||
let frameCount = 0;
|
||||
|
||||
function tick(ts) {
|
||||
if (!startTs) startTs = ts;
|
||||
const t = (ts - startTs) / 1000;
|
||||
|
||||
// Measure rects once
|
||||
if (!sliderRects && frameCount > 1) {
|
||||
measureRects();
|
||||
}
|
||||
|
||||
// --- Stage in ---
|
||||
const stageK = clampLerp(t, T.stage_in[0], T.stage_in[1]);
|
||||
const stageOp = cubicOut(stageK);
|
||||
preview.style.opacity = stageOp;
|
||||
preview.style.transform = `rotateX(${lerp(t, T.stage_in[0], T.stage_in[1], 10, 6, cubicOut)}deg) rotateY(-4deg) translateY(${lerp(t, T.stage_in[0], T.stage_in[1], 20, 0, expoOut)}px)`;
|
||||
panel.style.opacity = stageOp;
|
||||
panel.style.transform = `translateX(${lerp(t, T.stage_in[0], T.stage_in[1], 30, 0, expoOut)}px)`;
|
||||
|
||||
// Anchor
|
||||
const aK = clampLerp(t, T.anchor_in[0], T.anchor_in[1]);
|
||||
anchor.style.opacity = cubicOut(aK);
|
||||
anchor.style.transform = `translateY(${lerp(t, T.anchor_in[0], T.anchor_in[1], 10, 0, expoOut)}px)`;
|
||||
|
||||
// Snap point: when knob reaches target (72% of drag duration)
|
||||
const s1SnapT = T.s1_drag[0] + (T.s1_drag[1] - T.s1_drag[0]) * 0.72;
|
||||
const s2SnapT = T.s2_drag[0] + (T.s2_drag[1] - T.s2_drag[0]) * 0.72;
|
||||
const s3SnapT = T.s3_drag[0] + (T.s3_drag[1] - T.s3_drag[0]) * 0.72;
|
||||
|
||||
// --- Slider 1: palette ---
|
||||
// Knob 10% → 90%
|
||||
const k1pct = knobMotion(t, T.s1_drag[0], T.s1_drag[1], 10, 90);
|
||||
knobLeft('knob1', k1pct); fillWidth('fill1', k1pct);
|
||||
setKnobState(knob1, t >= T.s1_cursor_to[0] && t < T.s1_settle[1] + 0.2);
|
||||
if (t >= s1SnapT && state.palette !== 'cool') {
|
||||
state.palette = 'cool'; updatePreview(); setValueLabel(val1, 'cool');
|
||||
}
|
||||
|
||||
// --- Slider 2: type ---
|
||||
const k2pct = knobMotion(t, T.s2_drag[0], T.s2_drag[1], 10, 90);
|
||||
knobLeft('knob2', k2pct); fillWidth('fill2', k2pct);
|
||||
setKnobState(knob2, t >= T.s2_cursor_to[0] && t < T.s2_settle[1] + 0.2);
|
||||
if (t >= s2SnapT && state.type !== 'sans') {
|
||||
state.type = 'sans'; updatePreview(); setValueLabel(val2, 'sans');
|
||||
}
|
||||
|
||||
// --- Slider 3: density ---
|
||||
const k3pct = knobMotion(t, T.s3_drag[0], T.s3_drag[1], 10, 90);
|
||||
knobLeft('knob3', k3pct); fillWidth('fill3', k3pct);
|
||||
setKnobState(knob3, t >= T.s3_cursor_to[0] && t < T.s3_settle[1] + 0.2);
|
||||
if (t >= s3SnapT && state.density !== 'dense') {
|
||||
state.density = 'dense'; updatePreview(); setValueLabel(val3, 'dense');
|
||||
}
|
||||
|
||||
// --- Cursor choreography ---
|
||||
if (sliderRects) {
|
||||
const r1 = sliderRects.s1, r2 = sliderRects.s2, r3 = sliderRects.s3;
|
||||
// Positions of knob at 10% and 90%
|
||||
const k1Start = { x: r1.left + r1.width * 0.10, y: r1.top + r1.height/2 };
|
||||
const k1End = { x: r1.left + r1.width * 0.90, y: r1.top + r1.height/2 };
|
||||
const k2Start = { x: r2.left + r2.width * 0.10, y: r2.top + r2.height/2 };
|
||||
const k2End = { x: r2.left + r2.width * 0.90, y: r2.top + r2.height/2 };
|
||||
const k3Start = { x: r3.left + r3.width * 0.10, y: r3.top + r3.height/2 };
|
||||
const k3End = { x: r3.left + r3.width * 0.90, y: r3.top + r3.height/2 };
|
||||
|
||||
let cx = CURSOR_PARK.x, cy = CURSOR_PARK.y, co = 0;
|
||||
|
||||
if (t < T.s1_cursor_to[0]) {
|
||||
// still off-screen (or just appeared)
|
||||
cx = CURSOR_PARK.x; cy = CURSOR_PARK.y; co = 0;
|
||||
} else if (t < T.s1_cursor_to[1]) {
|
||||
// cursor flies to s1 knob start
|
||||
const k = clampLerp(t, T.s1_cursor_to[0], T.s1_cursor_to[1]);
|
||||
const e = cubicOut(k);
|
||||
cx = lerp(t, T.s1_cursor_to[0], T.s1_cursor_to[1], CURSOR_PARK.x, k1Start.x, cubicOut);
|
||||
cy = lerp(t, T.s1_cursor_to[0], T.s1_cursor_to[1], CURSOR_PARK.y, k1Start.y, cubicOut);
|
||||
co = e;
|
||||
} else if (t < T.s1_drag[1]) {
|
||||
// dragging s1
|
||||
cx = r1.left + (r1.width * k1pct / 100);
|
||||
cy = r1.top + r1.height/2;
|
||||
co = 1;
|
||||
} else if (t < T.s2_cursor_to[0]) {
|
||||
cx = k1End.x; cy = k1End.y; co = 1;
|
||||
} else if (t < T.s2_cursor_to[1]) {
|
||||
cx = lerp(t, T.s2_cursor_to[0], T.s2_cursor_to[1], k1End.x, k2Start.x, cubicInOut);
|
||||
cy = lerp(t, T.s2_cursor_to[0], T.s2_cursor_to[1], k1End.y, k2Start.y, cubicInOut);
|
||||
co = 1;
|
||||
} else if (t < T.s2_drag[1]) {
|
||||
cx = r2.left + (r2.width * k2pct / 100);
|
||||
cy = r2.top + r2.height/2;
|
||||
co = 1;
|
||||
} else if (t < T.s3_cursor_to[0]) {
|
||||
cx = k2End.x; cy = k2End.y; co = 1;
|
||||
} else if (t < T.s3_cursor_to[1]) {
|
||||
cx = lerp(t, T.s3_cursor_to[0], T.s3_cursor_to[1], k2End.x, k3Start.x, cubicInOut);
|
||||
cy = lerp(t, T.s3_cursor_to[0], T.s3_cursor_to[1], k2End.y, k3Start.y, cubicInOut);
|
||||
co = 1;
|
||||
} else if (t < T.s3_drag[1]) {
|
||||
cx = r3.left + (r3.width * k3pct / 100);
|
||||
cy = r3.top + r3.height/2;
|
||||
co = 1;
|
||||
} else if (t < T.hold[1]) {
|
||||
// fade out cursor
|
||||
cx = k3End.x; cy = k3End.y;
|
||||
co = lerp(t, T.s3_drag[1], T.hold[1], 1, 0, cubicOut);
|
||||
}
|
||||
|
||||
positionCursor(cx, cy, co);
|
||||
}
|
||||
|
||||
// --- Brand reveal (米色 walloff · aligned with hero-v10 signature) ---
|
||||
// 1) Scene dimmer: composition fades to black (0.3s)
|
||||
const soK = clampLerp(t, T.scene_out[0], T.scene_out[1]);
|
||||
stageDimmer.style.opacity = cubicOut(soK);
|
||||
|
||||
// 2) Cream panel sweeps up from bottom, expoOut (0.4s)
|
||||
const bpK = clampLerp(t, T.brand_panel[0], T.brand_panel[1]);
|
||||
const panelY = lerp(t, T.brand_panel[0], T.brand_panel[1], 100, 0, expoOut);
|
||||
brandPanel.style.transform = `translateY(${panelY}%)`;
|
||||
|
||||
// 3) Wordmark: font-weight 100→500 + y 20→0 + opacity 0→1, expoOut (0.6s)
|
||||
const bmK = clampLerp(t, T.brand_mark[0], T.brand_mark[1]);
|
||||
const bmE = expoOut(bmK);
|
||||
const wght = 100 + (500 - 100) * bmE;
|
||||
brandMark.style.opacity = bmE;
|
||||
brandMark.style.transform = `translateY(${20 * (1 - bmE)}px)`;
|
||||
brandMark.style.fontWeight = Math.round(wght);
|
||||
brandMark.style.fontVariationSettings = `"wght" ${wght.toFixed(0)}`;
|
||||
|
||||
// 4) Orange line: width 0→280 from center, cubicOut (0.4s)
|
||||
const blK = clampLerp(t, T.brand_line[0], T.brand_line[1]);
|
||||
brandLine.style.width = (280 * cubicOut(blK)) + 'px';
|
||||
|
||||
frameCount++;
|
||||
|
||||
// Loop or stop
|
||||
if (t < DURATION) {
|
||||
requestAnimationFrame(tick);
|
||||
} else {
|
||||
if (window.__recording === true) {
|
||||
// recording mode: hold last frame
|
||||
return;
|
||||
}
|
||||
// Restart after 1s pause (for manual viewing)
|
||||
setTimeout(() => {
|
||||
startTs = null;
|
||||
state = { palette: 'warm', type: 'serif', density: 'sparse' };
|
||||
updatePreview();
|
||||
setValueLabel(val1, 'warm'); setValueLabel(val2, 'serif'); setValueLabel(val3, 'sparse');
|
||||
requestAnimationFrame(tick);
|
||||
}, 900);
|
||||
}
|
||||
}
|
||||
|
||||
// Start animation after fonts ready
|
||||
const startAnim = () => {
|
||||
requestAnimationFrame((ts) => {
|
||||
startTs = ts;
|
||||
window.__ready = true; // signal for render-video.js
|
||||
requestAnimationFrame(tick);
|
||||
});
|
||||
};
|
||||
|
||||
if (document.fonts && document.fonts.ready) {
|
||||
document.fonts.ready.then(startAnim);
|
||||
} else {
|
||||
setTimeout(startAnim, 500);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
816
.claude/skills/huashu-design/demos/c5-infographic-en.html
Normal file
816
.claude/skills/huashu-design/demos/c5-infographic-en.html
Normal file
@@ -0,0 +1,816 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>c5-infographic · Data → Typography (EN)</title>
|
||||
<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=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@300;400;500;600&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #000000;
|
||||
--ink: #FFFFFF;
|
||||
--ink-80: rgba(255,255,255,0.82);
|
||||
--ink-60: rgba(255,255,255,0.58);
|
||||
--muted: rgba(255,255,255,0.40);
|
||||
--dim: rgba(255,255,255,0.18);
|
||||
--hairline: rgba(255,255,255,0.12);
|
||||
--accent: #D97757;
|
||||
--accent-deep: #B85D3D;
|
||||
|
||||
/* Brand Reveal */
|
||||
--cd-bg: #F5F4F0;
|
||||
--cd-panel: #FFFFFF;
|
||||
--cd-ink: #1A1918;
|
||||
--cd-dim: #8B867E;
|
||||
|
||||
--serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
|
||||
--serif-cn: "Noto Serif SC", "Songti SC", "Source Han Serif SC", serif;
|
||||
--sans: "Inter", -apple-system, "PingFang SC", system-ui, sans-serif;
|
||||
--mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0; padding: 0;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
font-family: var(--sans);
|
||||
color: var(--ink);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-feature-settings: "kern" 1, "liga" 1, "calt" 1;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
.stage {
|
||||
position: fixed;
|
||||
top: 50%; left: 50%;
|
||||
width: 1920px; height: 1080px;
|
||||
transform-origin: center center;
|
||||
background: var(--bg);
|
||||
overflow: hidden;
|
||||
/* Subtle film grain via SVG — 2% opacity */
|
||||
background-image:
|
||||
radial-gradient(ellipse at 20% 30%, rgba(217,119,87,0.025), transparent 50%),
|
||||
radial-gradient(ellipse at 80% 70%, rgba(217,119,87,0.018), transparent 55%);
|
||||
}
|
||||
|
||||
.watermark {
|
||||
position: absolute;
|
||||
top: 40px; left: 48px;
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.2em;
|
||||
color: var(--ink);
|
||||
opacity: 0.16;
|
||||
text-transform: uppercase;
|
||||
z-index: 400;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
.watermark.on-light { color: var(--cd-ink); opacity: 0.35; }
|
||||
|
||||
.v2-mark {
|
||||
position: absolute;
|
||||
bottom: 40px; right: 48px;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.2em;
|
||||
color: var(--ink);
|
||||
opacity: 0.16;
|
||||
z-index: 400;
|
||||
}
|
||||
|
||||
/* ============ Split layout ============ */
|
||||
.split-left {
|
||||
position: absolute;
|
||||
left: 120px; top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 440px;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
.json-block {
|
||||
font-family: var(--mono);
|
||||
font-size: 15px;
|
||||
line-height: 1.75;
|
||||
color: var(--ink-60);
|
||||
letter-spacing: 0.01em;
|
||||
white-space: pre;
|
||||
}
|
||||
.json-block .k { color: var(--ink-80); }
|
||||
.json-block .s { color: var(--accent); }
|
||||
.json-block .n { color: var(--ink); font-weight: 500; }
|
||||
.json-block .p { color: var(--muted); }
|
||||
|
||||
.json-label {
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.28em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
/* Pipe arrow from JSON → infographic */
|
||||
.pipe {
|
||||
position: absolute;
|
||||
left: 580px; top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 90px; height: 2px;
|
||||
background: linear-gradient(to right, var(--hairline), var(--accent), var(--hairline));
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.pipe::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -4px; top: 50%;
|
||||
transform: translateY(-50%) rotate(45deg);
|
||||
width: 8px; height: 8px;
|
||||
border-right: 2px solid var(--accent);
|
||||
border-top: 2px solid var(--accent);
|
||||
}
|
||||
|
||||
/* ============ Infographic (right side) ============ */
|
||||
.infographic {
|
||||
position: absolute;
|
||||
right: 100px; top: 72px;
|
||||
width: 1120px; height: 936px;
|
||||
background: #0A0A0A;
|
||||
border: 1px solid var(--hairline);
|
||||
padding: 56px 64px;
|
||||
opacity: 0;
|
||||
transform: translateY(18px);
|
||||
will-change: opacity, transform;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ig-masthead {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 36px;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.ig-masthead .issue {
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.3em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.ig-masthead .issue .orange { color: var(--accent); }
|
||||
.ig-masthead .dept {
|
||||
font-family: var(--mono);
|
||||
font-weight: 400;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.3em;
|
||||
color: var(--ink-60);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.ig-display {
|
||||
font-family: var(--serif-en);
|
||||
font-weight: 300;
|
||||
font-size: 96px;
|
||||
line-height: 1.0;
|
||||
letter-spacing: -0.025em;
|
||||
color: var(--ink);
|
||||
margin-bottom: 6px;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
text-wrap: pretty;
|
||||
font-feature-settings: "liga" 1, "dlig" 1, "kern" 1;
|
||||
}
|
||||
.ig-display .en {
|
||||
font-family: var(--serif-en);
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: var(--accent);
|
||||
font-feature-settings: "liga" 1, "dlig" 1, "swsh" 1;
|
||||
}
|
||||
|
||||
.ig-deck {
|
||||
font-family: var(--serif-en);
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
font-size: 22px;
|
||||
color: var(--ink-60);
|
||||
letter-spacing: 0.01em;
|
||||
margin-bottom: 44px;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
font-feature-settings: "liga" 1, "dlig" 1;
|
||||
}
|
||||
|
||||
/* Grid of 5 stats */
|
||||
.ig-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.3fr 1fr 1fr 1fr;
|
||||
gap: 32px;
|
||||
margin-bottom: 44px;
|
||||
}
|
||||
.ig-cell {
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
border-top: 2px solid var(--ink);
|
||||
padding-top: 14px;
|
||||
}
|
||||
.ig-cell.accent { border-top-color: var(--accent); }
|
||||
.ig-cell .label {
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.26em;
|
||||
margin-bottom: 14px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.ig-cell .label .en {
|
||||
font-family: var(--mono);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.26em;
|
||||
}
|
||||
.ig-cell .big {
|
||||
font-family: var(--serif-en);
|
||||
font-weight: 300;
|
||||
font-size: 72px;
|
||||
line-height: 0.92;
|
||||
color: var(--ink);
|
||||
letter-spacing: -0.03em;
|
||||
font-variant-numeric: oldstyle-nums proportional-nums;
|
||||
font-feature-settings: "onum" 1, "pnum" 1, "kern" 1;
|
||||
}
|
||||
.ig-cell.accent .big { color: var(--accent); }
|
||||
.ig-cell .big .unit {
|
||||
font-size: 28px;
|
||||
color: var(--ink-60);
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.ig-cell .sub {
|
||||
margin-top: 12px;
|
||||
font-family: var(--serif-en);
|
||||
font-style: italic;
|
||||
font-size: 14px;
|
||||
color: var(--ink-60);
|
||||
line-height: 1.4;
|
||||
font-feature-settings: "liga" 1, "dlig" 1;
|
||||
letter-spacing: 0.005em;
|
||||
}
|
||||
|
||||
/* Comparison bars */
|
||||
.ig-bars {
|
||||
display: grid;
|
||||
grid-template-columns: 140px 1fr 80px;
|
||||
gap: 18px 24px;
|
||||
row-gap: 18px;
|
||||
border-top: 1px solid var(--hairline);
|
||||
padding-top: 28px;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.ig-bars .row-label {
|
||||
font-family: var(--serif-en);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
color: var(--ink-80);
|
||||
letter-spacing: 0.005em;
|
||||
}
|
||||
.ig-bars .row-label.highlight { color: var(--accent); font-weight: 500; }
|
||||
.ig-bars .row-bar {
|
||||
height: 6px;
|
||||
background: var(--hairline);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ig-bars .row-bar .fill {
|
||||
position: absolute;
|
||||
left: 0; top: 0; bottom: 0;
|
||||
background: var(--ink-80);
|
||||
width: 0%;
|
||||
will-change: width;
|
||||
}
|
||||
.ig-bars .row-bar .fill.accent { background: var(--accent); }
|
||||
.ig-bars .row-val {
|
||||
font-family: var(--serif-en);
|
||||
font-size: 16px;
|
||||
color: var(--ink);
|
||||
text-align: right;
|
||||
font-variant-numeric: oldstyle-nums tabular-nums;
|
||||
font-feature-settings: "onum" 1, "tnum" 1;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.ig-footer {
|
||||
position: absolute;
|
||||
bottom: 40px; left: 64px; right: 64px;
|
||||
display: flex; justify-content: space-between; align-items: baseline;
|
||||
border-top: 1px solid var(--hairline);
|
||||
padding-top: 16px;
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.24em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.ig-footer .folio { color: var(--ink-60); letter-spacing: 0.32em; }
|
||||
|
||||
/* ============ Typography detail zoom ============ */
|
||||
.detail-zoom {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
background: radial-gradient(ellipse at center, #0A0A0A, #000000);
|
||||
z-index: 250;
|
||||
}
|
||||
.detail-word {
|
||||
font-family: var(--serif-en);
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
font-size: 320px;
|
||||
line-height: 0.9;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--ink);
|
||||
/* Enable OpenType ligatures, discretionary ligatures, swashes */
|
||||
font-feature-settings: "liga" 1, "dlig" 1, "swsh" 1, "salt" 1, "calt" 1;
|
||||
text-rendering: optimizeLegibility;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
.detail-word .fi {
|
||||
/* fi ligature is default with "liga" */
|
||||
color: var(--accent);
|
||||
}
|
||||
.detail-annotation {
|
||||
position: absolute;
|
||||
top: calc(50% + 170px); left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.28em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.detail-annotation .dot {
|
||||
color: var(--accent);
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
/* Callout lines pointing to ligature */
|
||||
.callout {
|
||||
position: absolute;
|
||||
left: 50%; top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.callout svg { overflow: visible; display: block; }
|
||||
|
||||
/* ============ Brand Reveal ============ */
|
||||
.brand-wall {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--cd-bg);
|
||||
z-index: 300;
|
||||
opacity: 0;
|
||||
transform: translateY(100%);
|
||||
will-change: transform, opacity;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.brand-wordmark {
|
||||
font-family: var(--serif-en);
|
||||
font-size: 132px;
|
||||
font-weight: 200;
|
||||
color: var(--cd-ink);
|
||||
letter-spacing: -0.04em;
|
||||
line-height: 1;
|
||||
opacity: 0;
|
||||
transform: scale(0.92);
|
||||
will-change: opacity, transform;
|
||||
font-feature-settings: "liga" 1, "dlig" 1;
|
||||
}
|
||||
.brand-wordmark .dot { color: var(--accent); padding: 0 10px; font-weight: 300; }
|
||||
.brand-underline {
|
||||
margin-top: 28px;
|
||||
height: 2px;
|
||||
width: 0;
|
||||
background: var(--accent);
|
||||
will-change: width;
|
||||
}
|
||||
.brand-cn {
|
||||
margin-top: 30px;
|
||||
font-family: var(--serif-cn);
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
color: var(--cd-dim);
|
||||
letter-spacing: 0.4em;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="stage" id="stage">
|
||||
|
||||
<div class="watermark" id="watermark">HUASHU · DESIGN</div>
|
||||
<div class="v2-mark">V2 · 2026</div>
|
||||
|
||||
<!-- Left: JSON data -->
|
||||
<div class="split-left" id="splitLeft" style="opacity:0">
|
||||
<div class="json-label" id="jsonLabel">DATA → benchmarks.json</div>
|
||||
<pre class="json-block" id="jsonBlock"></pre>
|
||||
</div>
|
||||
|
||||
<!-- Pipe arrow -->
|
||||
<div class="pipe" id="pipe"></div>
|
||||
|
||||
<!-- Right: Infographic -->
|
||||
<div class="infographic" id="infographic">
|
||||
|
||||
<div class="ig-masthead" id="igMasthead">
|
||||
<div class="issue">Issue № 05 <span class="orange">· AI Benchmarks</span> · Q2 2026</div>
|
||||
<div class="dept">FRONTIER REPORT</div>
|
||||
</div>
|
||||
|
||||
<h1 class="ig-display" id="igDisplay">
|
||||
The Age of<br>
|
||||
<span class="en">benchmarks</span>.
|
||||
</h1>
|
||||
|
||||
<p class="ig-deck" id="igDeck">
|
||||
Five frontier models, five numbers, one uncomfortable truth.
|
||||
</p>
|
||||
|
||||
<div class="ig-grid" id="igGrid">
|
||||
<div class="ig-cell accent" data-cell="0">
|
||||
<div class="label">Leader <span class="en">· Q2</span></div>
|
||||
<div class="big">Claude 4.7</div>
|
||||
<div class="sub">Sonnet, 1M ctx · Anthropic</div>
|
||||
</div>
|
||||
<div class="ig-cell" data-cell="1">
|
||||
<div class="label"><span class="en">SWE-bench</span></div>
|
||||
<div class="big">77<span class="unit">.2%</span></div>
|
||||
<div class="sub">coding, verified split</div>
|
||||
</div>
|
||||
<div class="ig-cell" data-cell="2">
|
||||
<div class="label"><span class="en">GPQA</span></div>
|
||||
<div class="big">84<span class="unit">.5</span></div>
|
||||
<div class="sub">diamond, graduate science</div>
|
||||
</div>
|
||||
<div class="ig-cell" data-cell="3">
|
||||
<div class="label">Price <span class="en">· input</span></div>
|
||||
<div class="big">$3<span class="unit">/M</span></div>
|
||||
<div class="sub">per million tokens, typical</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ig-bars" id="igBars">
|
||||
<div class="row-label highlight">Claude 4.7 Sonnet</div>
|
||||
<div class="row-bar"><div class="fill accent" data-w="77.2"></div></div>
|
||||
<div class="row-val">77.2</div>
|
||||
|
||||
<div class="row-label">GPT-5 Turbo</div>
|
||||
<div class="row-bar"><div class="fill" data-w="74.8"></div></div>
|
||||
<div class="row-val">74.8</div>
|
||||
|
||||
<div class="row-label">Gemini 3 Pro</div>
|
||||
<div class="row-bar"><div class="fill" data-w="71.3"></div></div>
|
||||
<div class="row-val">71.3</div>
|
||||
|
||||
<div class="row-label">GLM-5</div>
|
||||
<div class="row-bar"><div class="fill" data-w="68.9"></div></div>
|
||||
<div class="row-val">68.9</div>
|
||||
|
||||
<div class="row-label">Kimi k3</div>
|
||||
<div class="row-bar"><div class="fill" data-w="66.4"></div></div>
|
||||
<div class="row-val">66.4</div>
|
||||
</div>
|
||||
|
||||
<div class="ig-footer" id="igFooter">
|
||||
<span>Set in Source Serif 4 & JetBrains Mono</span>
|
||||
<span class="folio">P. 05</span>
|
||||
<span>Data · 2026 Q2, public benchmarks</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Detail zoom: Typography ligature -->
|
||||
<div class="detail-zoom" id="detailZoom">
|
||||
<div class="detail-word" id="detailWord">bench<span class="fi">ma</span>rks</div>
|
||||
<div class="callout" id="callout" style="display:none"></div>
|
||||
<div class="detail-annotation" id="detailAnnotation">
|
||||
SOURCE SERIF 4 <span class="dot">·</span> ITALIC <span class="dot">·</span> OLDSTYLE FIGURES
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Brand Reveal -->
|
||||
<div class="brand-wall" id="brandWall">
|
||||
<div class="brand-wordmark" id="brandWord">huashu<span class="dot">·</span>design</div>
|
||||
<div class="brand-underline" id="brandLine"></div>
|
||||
<div class="brand-cn" id="brandCn">D A T A · T Y P O G R A P H Y</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
// ---------- Scale stage to viewport ----------
|
||||
const stage = document.getElementById('stage');
|
||||
function fitStage() {
|
||||
const s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
|
||||
stage.style.transform = `translate(-50%, -50%) scale(${s})`;
|
||||
}
|
||||
fitStage();
|
||||
window.addEventListener('resize', fitStage);
|
||||
|
||||
// ---------- Easing ----------
|
||||
const expoOut = t => t >= 1 ? 1 : 1 - Math.pow(2, -10 * t);
|
||||
const expoIn = t => t <= 0 ? 0 : Math.pow(2, 10 * (t - 1));
|
||||
const cubicOut = t => 1 - Math.pow(1 - t, 3);
|
||||
const cubicInOut = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2;
|
||||
const lerp = (t, a, b, c, d, ease=x=>x) => {
|
||||
if (b === a) return c;
|
||||
const k = Math.max(0, Math.min(1, (t - a) / (b - a)));
|
||||
return c + (d - c) * ease(k);
|
||||
};
|
||||
const seg = (t, a, b) => Math.max(0, Math.min(1, (t - a) / (b - a)));
|
||||
|
||||
// ---------- Refs ----------
|
||||
const splitLeft = document.getElementById('splitLeft');
|
||||
const jsonLabel = document.getElementById('jsonLabel');
|
||||
const jsonBlock = document.getElementById('jsonBlock');
|
||||
const pipe = document.getElementById('pipe');
|
||||
const infographic = document.getElementById('infographic');
|
||||
const igMasthead = document.getElementById('igMasthead');
|
||||
const igDisplay = document.getElementById('igDisplay');
|
||||
const igDeck = document.getElementById('igDeck');
|
||||
const igGrid = document.getElementById('igGrid');
|
||||
const igCells = igGrid.querySelectorAll('.ig-cell');
|
||||
const igBars = document.getElementById('igBars');
|
||||
const igBarFills = igBars.querySelectorAll('.fill');
|
||||
const igFooter = document.getElementById('igFooter');
|
||||
const detailZoom = document.getElementById('detailZoom');
|
||||
const detailWord = document.getElementById('detailWord');
|
||||
const detailAnnotation = document.getElementById('detailAnnotation');
|
||||
const callout = document.getElementById('callout');
|
||||
const brandWall = document.getElementById('brandWall');
|
||||
const brandWord = document.getElementById('brandWord');
|
||||
const brandLine = document.getElementById('brandLine');
|
||||
const brandCn = document.getElementById('brandCn');
|
||||
const watermark = document.getElementById('watermark');
|
||||
|
||||
// ---------- JSON content (for progressive reveal) ----------
|
||||
const jsonRaw = [
|
||||
'{',
|
||||
' "issue": "2026-Q2",',
|
||||
' "leader": "Claude 4.7",',
|
||||
' "models": [',
|
||||
' { "name": "Claude 4.7", "swe": 77.2 },',
|
||||
' { "name": "GPT-5 Turbo", "swe": 74.8 },',
|
||||
' { "name": "Gemini 3 Pro", "swe": 71.3 },',
|
||||
' { "name": "GLM-5", "swe": 68.9 },',
|
||||
' { "name": "Kimi k3", "swe": 66.4 }',
|
||||
' ],',
|
||||
' "gpqa_top": 84.5,',
|
||||
' "price_per_M": 3',
|
||||
'}'
|
||||
];
|
||||
|
||||
function formatJson(lines) {
|
||||
return lines.map(line => {
|
||||
return line
|
||||
.replace(/"([a-zA-Z_]+)":/g, '<span class="k">"$1"</span>:')
|
||||
.replace(/: "([^"]+)"/g, ': <span class="s">"$1"</span>')
|
||||
.replace(/: ([0-9.]+)/g, ': <span class="n">$1</span>')
|
||||
.replace(/([{}\[\],])/g, '<span class="p">$1</span>');
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
// ---------- Timeline ----------
|
||||
const DURATION = 10.0;
|
||||
|
||||
// SFX cue points (played back in ffmpeg post-processing, not browser):
|
||||
// t=0.35 → keyboard/type-fast.mp3 (data entering)
|
||||
// t=2.15 → container/card-snap.mp3 (infographic settles)
|
||||
// t=6.75 → transition/whoosh-fast.mp3 (zoom-in to typography)
|
||||
// t=8.70 → impact/logo-reveal.mp3 (brand reveal chime)
|
||||
const sfxFired = new Set();
|
||||
function fireOnce(key) {
|
||||
if (sfxFired.has(key)) return;
|
||||
sfxFired.add(key);
|
||||
// cue emitted for post-processing; no in-browser playback
|
||||
}
|
||||
|
||||
let startTime = null;
|
||||
let raf;
|
||||
|
||||
function tick(now) {
|
||||
if (startTime == null) startTime = now;
|
||||
const t = (now - startTime) / 1000;
|
||||
|
||||
// ── Beat 1: 0-2s · JSON data appears, types in ─────────
|
||||
// JSON label fade in
|
||||
{
|
||||
const k = cubicOut(seg(t, 0.15, 0.55));
|
||||
jsonLabel.style.opacity = k;
|
||||
splitLeft.style.opacity = '1';
|
||||
}
|
||||
// Progressive type-reveal: reveal N lines of JSON by time
|
||||
{
|
||||
const totalLines = jsonRaw.length;
|
||||
const k = seg(t, 0.3, 1.9);
|
||||
const linesShown = Math.floor(k * totalLines);
|
||||
const shown = jsonRaw.slice(0, Math.max(0, linesShown));
|
||||
jsonBlock.innerHTML = formatJson(shown);
|
||||
if (linesShown >= 3 && t < 1.9) fireOnce('datain');
|
||||
}
|
||||
|
||||
// ── Pipe arrow (1.8 → 2.2) ─────────────────────────────
|
||||
{
|
||||
const k = cubicOut(seg(t, 1.8, 2.2));
|
||||
pipe.style.opacity = k;
|
||||
}
|
||||
|
||||
// ── Beat 2a: 2.0-3.2s · Infographic canvas arrives ─────
|
||||
{
|
||||
const k = expoOut(seg(t, 2.0, 2.8));
|
||||
infographic.style.opacity = k;
|
||||
infographic.style.transform = `translateY(${lerp(t, 2.0, 2.8, 18, 0, expoOut)}px)`;
|
||||
if (t > 2.1) fireOnce('settle');
|
||||
}
|
||||
// Masthead
|
||||
{
|
||||
const k = cubicOut(seg(t, 2.6, 3.1));
|
||||
igMasthead.style.opacity = k;
|
||||
}
|
||||
|
||||
// ── Beat 2b: 3.0-4.2s · Display headline appears ──────
|
||||
{
|
||||
const k = expoOut(seg(t, 3.0, 3.8));
|
||||
igDisplay.style.opacity = k;
|
||||
igDisplay.style.transform = `translateY(${lerp(t, 3.0, 3.8, 16, 0, expoOut)}px)`;
|
||||
}
|
||||
// Deck line (italic)
|
||||
{
|
||||
const k = cubicOut(seg(t, 3.6, 4.2));
|
||||
igDeck.style.opacity = k;
|
||||
}
|
||||
|
||||
// ── Beat 2c: 4.0-5.2s · Grid cells (ripple, 4 cells) ──
|
||||
igCells.forEach((cell, i) => {
|
||||
const start = 4.0 + i * 0.12;
|
||||
const end = start + 0.5;
|
||||
const k = expoOut(seg(t, start, end));
|
||||
cell.style.opacity = k;
|
||||
cell.style.transform = `translateY(${lerp(t, start, end, 14, 0, expoOut)}px)`;
|
||||
});
|
||||
|
||||
// ── Beat 2d: 5.2-6.4s · Comparison bars grow ─────────
|
||||
{
|
||||
const k = cubicOut(seg(t, 5.1, 5.4));
|
||||
igBars.style.opacity = k;
|
||||
}
|
||||
igBarFills.forEach((fill, i) => {
|
||||
const start = 5.3 + i * 0.08;
|
||||
const end = start + 0.7;
|
||||
const w = parseFloat(fill.getAttribute('data-w'));
|
||||
const pct = lerp(t, start, end, 0, w, expoOut);
|
||||
fill.style.width = pct + '%';
|
||||
});
|
||||
// Footer
|
||||
{
|
||||
const k = cubicOut(seg(t, 6.0, 6.6));
|
||||
igFooter.style.opacity = k * 0.9;
|
||||
}
|
||||
|
||||
// ── Beat 2e: 6.6-8.2s · Zoom to typography detail ────
|
||||
if (t >= 6.6 && t < 8.3) {
|
||||
const k = expoOut(seg(t, 6.6, 7.4));
|
||||
// Infographic scales up and fades — simulate push-in
|
||||
const scale = lerp(t, 6.6, 7.4, 1, 3.4, expoOut);
|
||||
const ty = lerp(t, 6.6, 7.4, 0, -140, expoOut);
|
||||
infographic.style.transform = `translateY(${ty}px) scale(${scale})`;
|
||||
infographic.style.opacity = String(1 - k * 0.85);
|
||||
splitLeft.style.opacity = String(1 - k);
|
||||
pipe.style.opacity = String(1 - k);
|
||||
|
||||
// Detail zoom fades in
|
||||
const k2 = expoOut(seg(t, 7.0, 7.7));
|
||||
detailZoom.style.opacity = k2;
|
||||
// Word subtle scale-in (starts from 0.96)
|
||||
detailWord.style.transform = `scale(${lerp(t, 7.0, 7.9, 0.96, 1.0, expoOut)})`;
|
||||
|
||||
// SFX at 6.7
|
||||
if (t > 6.7) fireOnce('zoom');
|
||||
|
||||
// Callout + annotation (7.5 → 8.1)
|
||||
const k3 = cubicOut(seg(t, 7.6, 8.1));
|
||||
callout.style.opacity = k3;
|
||||
detailAnnotation.style.opacity = k3;
|
||||
}
|
||||
|
||||
// ── Beat 3: 8.2-10s · Brand reveal ───────────────────
|
||||
// Detail zoom fades under brand wall
|
||||
if (t >= 8.1) {
|
||||
const k = cubicOut(seg(t, 8.1, 8.5));
|
||||
detailZoom.style.opacity = String(Math.max(0, 1 - k));
|
||||
}
|
||||
// Brand wall slides up from bottom
|
||||
{
|
||||
const k = expoOut(seg(t, 8.1, 8.7));
|
||||
brandWall.style.transform = `translateY(${lerp(t, 8.1, 8.7, 100, 0, expoOut)}%)`;
|
||||
brandWall.style.opacity = k > 0 ? '1' : '0';
|
||||
if (k > 0.55) watermark.classList.add('on-light');
|
||||
else watermark.classList.remove('on-light');
|
||||
}
|
||||
// Wordmark
|
||||
{
|
||||
const k = expoOut(seg(t, 8.6, 9.2));
|
||||
brandWord.style.opacity = k;
|
||||
brandWord.style.transform = `scale(${lerp(t, 8.6, 9.2, 0.92, 1.0, expoOut)})`;
|
||||
if (t > 8.65) fireOnce('chime');
|
||||
}
|
||||
// Underline
|
||||
{
|
||||
const k = expoOut(seg(t, 9.0, 9.6));
|
||||
brandLine.style.width = (280 * k) + 'px';
|
||||
}
|
||||
// CN tagline
|
||||
{
|
||||
const k = cubicOut(seg(t, 9.3, 9.9));
|
||||
brandCn.style.opacity = k * 0.9;
|
||||
}
|
||||
|
||||
// Loop / hold
|
||||
if (t < DURATION) {
|
||||
raf = requestAnimationFrame(tick);
|
||||
} else {
|
||||
if (!window.__recording) {
|
||||
setTimeout(() => {
|
||||
// Reset
|
||||
startTime = null;
|
||||
sfxFired.clear();
|
||||
jsonBlock.innerHTML = '';
|
||||
splitLeft.style.opacity = '0';
|
||||
pipe.style.opacity = '0';
|
||||
infographic.style.opacity = '0';
|
||||
infographic.style.transform = 'translateY(18px) scale(1)';
|
||||
igMasthead.style.opacity = '0';
|
||||
igDisplay.style.opacity = '0';
|
||||
igDeck.style.opacity = '0';
|
||||
igBars.style.opacity = '0';
|
||||
igFooter.style.opacity = '0';
|
||||
igCells.forEach(c => { c.style.opacity = '0'; });
|
||||
igBarFills.forEach(f => { f.style.width = '0%'; });
|
||||
detailZoom.style.opacity = '0';
|
||||
callout.style.opacity = '0';
|
||||
detailAnnotation.style.opacity = '0';
|
||||
brandWall.style.transform = 'translateY(100%)';
|
||||
brandWall.style.opacity = '0';
|
||||
brandWord.style.opacity = '0';
|
||||
brandLine.style.width = '0';
|
||||
brandCn.style.opacity = '0';
|
||||
watermark.classList.remove('on-light');
|
||||
raf = requestAnimationFrame(tick);
|
||||
}, 800);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.__seek = function(s) {
|
||||
startTime = performance.now() - s * 1000;
|
||||
};
|
||||
|
||||
// Wait for fonts, then start
|
||||
(document.fonts ? document.fonts.ready : Promise.resolve()).then(() => {
|
||||
requestAnimationFrame((now) => {
|
||||
startTime = now;
|
||||
window.__ready = true;
|
||||
raf = requestAnimationFrame(tick);
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
813
.claude/skills/huashu-design/demos/c5-infographic.html
Normal file
813
.claude/skills/huashu-design/demos/c5-infographic.html
Normal file
@@ -0,0 +1,813 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-Hans">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>c5-infographic · 数据 → 印刷级排版(中文版)</title>
|
||||
<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=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@300;400;500;600&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #000000;
|
||||
--ink: #FFFFFF;
|
||||
--ink-80: rgba(255,255,255,0.82);
|
||||
--ink-60: rgba(255,255,255,0.58);
|
||||
--muted: rgba(255,255,255,0.40);
|
||||
--dim: rgba(255,255,255,0.18);
|
||||
--hairline: rgba(255,255,255,0.12);
|
||||
--accent: #D97757;
|
||||
--accent-deep: #B85D3D;
|
||||
|
||||
/* Brand Reveal */
|
||||
--cd-bg: #F5F4F0;
|
||||
--cd-panel: #FFFFFF;
|
||||
--cd-ink: #1A1918;
|
||||
--cd-dim: #8B867E;
|
||||
|
||||
--serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
|
||||
--serif-cn: "Noto Serif SC", "Songti SC", "Source Han Serif SC", serif;
|
||||
--sans: "Inter", -apple-system, "PingFang SC", system-ui, sans-serif;
|
||||
--mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0; padding: 0;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
font-family: var(--sans);
|
||||
color: var(--ink);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-feature-settings: "kern" 1, "liga" 1, "calt" 1;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
.stage {
|
||||
position: fixed;
|
||||
top: 50%; left: 50%;
|
||||
width: 1920px; height: 1080px;
|
||||
transform-origin: center center;
|
||||
background: var(--bg);
|
||||
overflow: hidden;
|
||||
/* Subtle film grain via SVG — 2% opacity */
|
||||
background-image:
|
||||
radial-gradient(ellipse at 20% 30%, rgba(217,119,87,0.025), transparent 50%),
|
||||
radial-gradient(ellipse at 80% 70%, rgba(217,119,87,0.018), transparent 55%);
|
||||
}
|
||||
|
||||
.watermark {
|
||||
position: absolute;
|
||||
top: 40px; left: 48px;
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.2em;
|
||||
color: var(--ink);
|
||||
opacity: 0.16;
|
||||
text-transform: uppercase;
|
||||
z-index: 400;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
.watermark.on-light { color: var(--cd-ink); opacity: 0.35; }
|
||||
|
||||
.v2-mark {
|
||||
position: absolute;
|
||||
bottom: 40px; right: 48px;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.2em;
|
||||
color: var(--ink);
|
||||
opacity: 0.16;
|
||||
z-index: 400;
|
||||
}
|
||||
|
||||
/* ============ Split layout ============ */
|
||||
.split-left {
|
||||
position: absolute;
|
||||
left: 120px; top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 440px;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
.json-block {
|
||||
font-family: var(--mono);
|
||||
font-size: 15px;
|
||||
line-height: 1.75;
|
||||
color: var(--ink-60);
|
||||
letter-spacing: 0.01em;
|
||||
white-space: pre;
|
||||
}
|
||||
.json-block .k { color: var(--ink-80); }
|
||||
.json-block .s { color: var(--accent); }
|
||||
.json-block .n { color: var(--ink); font-weight: 500; }
|
||||
.json-block .p { color: var(--muted); }
|
||||
|
||||
.json-label {
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.28em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
/* Pipe arrow from JSON → infographic */
|
||||
.pipe {
|
||||
position: absolute;
|
||||
left: 580px; top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 90px; height: 2px;
|
||||
background: linear-gradient(to right, var(--hairline), var(--accent), var(--hairline));
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.pipe::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -4px; top: 50%;
|
||||
transform: translateY(-50%) rotate(45deg);
|
||||
width: 8px; height: 8px;
|
||||
border-right: 2px solid var(--accent);
|
||||
border-top: 2px solid var(--accent);
|
||||
}
|
||||
|
||||
/* ============ Infographic (right side) ============ */
|
||||
.infographic {
|
||||
position: absolute;
|
||||
right: 100px; top: 72px;
|
||||
width: 1120px; height: 936px;
|
||||
background: #0A0A0A;
|
||||
border: 1px solid var(--hairline);
|
||||
padding: 56px 64px;
|
||||
opacity: 0;
|
||||
transform: translateY(18px);
|
||||
will-change: opacity, transform;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ig-masthead {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 36px;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.ig-masthead .issue {
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.3em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.ig-masthead .issue .orange { color: var(--accent); }
|
||||
.ig-masthead .dept {
|
||||
font-family: var(--serif-cn);
|
||||
font-weight: 300;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.35em;
|
||||
color: var(--ink-60);
|
||||
}
|
||||
|
||||
.ig-display {
|
||||
font-family: var(--serif-cn);
|
||||
font-weight: 400;
|
||||
font-size: 84px;
|
||||
line-height: 1.02;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--ink);
|
||||
margin-bottom: 6px;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
.ig-display .en {
|
||||
font-family: var(--serif-en);
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: var(--accent);
|
||||
font-feature-settings: "liga" 1, "dlig" 1, "swsh" 1;
|
||||
}
|
||||
|
||||
.ig-deck {
|
||||
font-family: var(--serif-en);
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
font-size: 22px;
|
||||
color: var(--ink-60);
|
||||
letter-spacing: 0.01em;
|
||||
margin-bottom: 44px;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
font-feature-settings: "liga" 1, "dlig" 1;
|
||||
}
|
||||
|
||||
/* Grid of 5 stats */
|
||||
.ig-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.3fr 1fr 1fr 1fr;
|
||||
gap: 32px;
|
||||
margin-bottom: 44px;
|
||||
}
|
||||
.ig-cell {
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
border-top: 2px solid var(--ink);
|
||||
padding-top: 14px;
|
||||
}
|
||||
.ig-cell.accent { border-top-color: var(--accent); }
|
||||
.ig-cell .label {
|
||||
font-family: var(--serif-cn);
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.22em;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.ig-cell .label .en {
|
||||
font-family: var(--mono);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.26em;
|
||||
}
|
||||
.ig-cell .big {
|
||||
font-family: var(--serif-en);
|
||||
font-weight: 300;
|
||||
font-size: 72px;
|
||||
line-height: 0.92;
|
||||
color: var(--ink);
|
||||
letter-spacing: -0.03em;
|
||||
font-variant-numeric: oldstyle-nums proportional-nums;
|
||||
font-feature-settings: "onum" 1, "pnum" 1, "kern" 1;
|
||||
}
|
||||
.ig-cell.accent .big { color: var(--accent); }
|
||||
.ig-cell .big .unit {
|
||||
font-size: 28px;
|
||||
color: var(--ink-60);
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.ig-cell .sub {
|
||||
margin-top: 12px;
|
||||
font-family: var(--serif-en);
|
||||
font-style: italic;
|
||||
font-size: 14px;
|
||||
color: var(--ink-60);
|
||||
line-height: 1.4;
|
||||
font-feature-settings: "liga" 1, "dlig" 1;
|
||||
letter-spacing: 0.005em;
|
||||
}
|
||||
|
||||
/* Comparison bars */
|
||||
.ig-bars {
|
||||
display: grid;
|
||||
grid-template-columns: 140px 1fr 80px;
|
||||
gap: 18px 24px;
|
||||
row-gap: 18px;
|
||||
border-top: 1px solid var(--hairline);
|
||||
padding-top: 28px;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.ig-bars .row-label {
|
||||
font-family: var(--serif-cn);
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
color: var(--ink-80);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.ig-bars .row-label.highlight { color: var(--accent); font-weight: 500; }
|
||||
.ig-bars .row-bar {
|
||||
height: 6px;
|
||||
background: var(--hairline);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ig-bars .row-bar .fill {
|
||||
position: absolute;
|
||||
left: 0; top: 0; bottom: 0;
|
||||
background: var(--ink-80);
|
||||
width: 0%;
|
||||
will-change: width;
|
||||
}
|
||||
.ig-bars .row-bar .fill.accent { background: var(--accent); }
|
||||
.ig-bars .row-val {
|
||||
font-family: var(--serif-en);
|
||||
font-size: 16px;
|
||||
color: var(--ink);
|
||||
text-align: right;
|
||||
font-variant-numeric: oldstyle-nums tabular-nums;
|
||||
font-feature-settings: "onum" 1, "tnum" 1;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.ig-footer {
|
||||
position: absolute;
|
||||
bottom: 40px; left: 64px; right: 64px;
|
||||
display: flex; justify-content: space-between; align-items: baseline;
|
||||
border-top: 1px solid var(--hairline);
|
||||
padding-top: 16px;
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.24em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.ig-footer .folio { color: var(--ink-60); letter-spacing: 0.32em; }
|
||||
|
||||
/* ============ Typography detail zoom ============ */
|
||||
.detail-zoom {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
background: radial-gradient(ellipse at center, #0A0A0A, #000000);
|
||||
z-index: 250;
|
||||
}
|
||||
.detail-word {
|
||||
font-family: var(--serif-en);
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
font-size: 320px;
|
||||
line-height: 0.9;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--ink);
|
||||
/* Enable OpenType ligatures, discretionary ligatures, swashes */
|
||||
font-feature-settings: "liga" 1, "dlig" 1, "swsh" 1, "salt" 1, "calt" 1;
|
||||
text-rendering: optimizeLegibility;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
.detail-word .fi {
|
||||
/* fi ligature is default with "liga" */
|
||||
color: var(--accent);
|
||||
}
|
||||
.detail-annotation {
|
||||
position: absolute;
|
||||
top: calc(50% + 170px); left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.28em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.detail-annotation .dot {
|
||||
color: var(--accent);
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
/* Callout lines pointing to ligature */
|
||||
.callout {
|
||||
position: absolute;
|
||||
left: 50%; top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.callout svg { overflow: visible; display: block; }
|
||||
|
||||
/* ============ Brand Reveal ============ */
|
||||
.brand-wall {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--cd-bg);
|
||||
z-index: 300;
|
||||
opacity: 0;
|
||||
transform: translateY(100%);
|
||||
will-change: transform, opacity;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.brand-wordmark {
|
||||
font-family: var(--serif-en);
|
||||
font-size: 132px;
|
||||
font-weight: 200;
|
||||
color: var(--cd-ink);
|
||||
letter-spacing: -0.04em;
|
||||
line-height: 1;
|
||||
opacity: 0;
|
||||
transform: scale(0.92);
|
||||
will-change: opacity, transform;
|
||||
font-feature-settings: "liga" 1, "dlig" 1;
|
||||
}
|
||||
.brand-wordmark .dot { color: var(--accent); padding: 0 10px; font-weight: 300; }
|
||||
.brand-underline {
|
||||
margin-top: 28px;
|
||||
height: 2px;
|
||||
width: 0;
|
||||
background: var(--accent);
|
||||
will-change: width;
|
||||
}
|
||||
.brand-cn {
|
||||
margin-top: 30px;
|
||||
font-family: var(--serif-cn);
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
color: var(--cd-dim);
|
||||
letter-spacing: 0.4em;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="stage" id="stage">
|
||||
|
||||
<div class="watermark" id="watermark">HUASHU · DESIGN</div>
|
||||
<div class="v2-mark">V2 · 2026</div>
|
||||
|
||||
<!-- Left: JSON data -->
|
||||
<div class="split-left" id="splitLeft" style="opacity:0">
|
||||
<div class="json-label" id="jsonLabel">DATA · benchmarks.json</div>
|
||||
<pre class="json-block" id="jsonBlock"></pre>
|
||||
</div>
|
||||
|
||||
<!-- Pipe arrow -->
|
||||
<div class="pipe" id="pipe"></div>
|
||||
|
||||
<!-- Right: Infographic -->
|
||||
<div class="infographic" id="infographic">
|
||||
|
||||
<div class="ig-masthead" id="igMasthead">
|
||||
<div class="issue">Issue № 05 · <span class="orange">AI Benchmarks</span> · Q2 2026</div>
|
||||
<div class="dept">性 能 报 告</div>
|
||||
</div>
|
||||
|
||||
<h1 class="ig-display" id="igDisplay">
|
||||
大模型<br>
|
||||
<span class="en">benchmarks</span> 之年
|
||||
</h1>
|
||||
|
||||
<p class="ig-deck" id="igDeck">
|
||||
Five frontier models, five numbers, one uncomfortable truth.
|
||||
</p>
|
||||
|
||||
<div class="ig-grid" id="igGrid">
|
||||
<div class="ig-cell accent" data-cell="0">
|
||||
<div class="label">领跑模型 <span class="en">· leader</span></div>
|
||||
<div class="big">Claude 4.7</div>
|
||||
<div class="sub">Sonnet, 1M ctx · Anthropic</div>
|
||||
</div>
|
||||
<div class="ig-cell" data-cell="1">
|
||||
<div class="label"><span class="en">SWE-bench</span></div>
|
||||
<div class="big">77<span class="unit">.2%</span></div>
|
||||
<div class="sub">coding, verified split</div>
|
||||
</div>
|
||||
<div class="ig-cell" data-cell="2">
|
||||
<div class="label"><span class="en">GPQA</span></div>
|
||||
<div class="big">84<span class="unit">.5</span></div>
|
||||
<div class="sub">diamond, graduate science</div>
|
||||
</div>
|
||||
<div class="ig-cell" data-cell="3">
|
||||
<div class="label">价差 <span class="en">· price</span></div>
|
||||
<div class="big">$3<span class="unit">/M</span></div>
|
||||
<div class="sub">input token, typical</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ig-bars" id="igBars">
|
||||
<div class="row-label highlight">Claude 4.7 Sonnet</div>
|
||||
<div class="row-bar"><div class="fill accent" data-w="77.2"></div></div>
|
||||
<div class="row-val">77.2</div>
|
||||
|
||||
<div class="row-label">GPT-5 Turbo</div>
|
||||
<div class="row-bar"><div class="fill" data-w="74.8"></div></div>
|
||||
<div class="row-val">74.8</div>
|
||||
|
||||
<div class="row-label">Gemini 3 Pro</div>
|
||||
<div class="row-bar"><div class="fill" data-w="71.3"></div></div>
|
||||
<div class="row-val">71.3</div>
|
||||
|
||||
<div class="row-label">GLM-5</div>
|
||||
<div class="row-bar"><div class="fill" data-w="68.9"></div></div>
|
||||
<div class="row-val">68.9</div>
|
||||
|
||||
<div class="row-label">Kimi k3</div>
|
||||
<div class="row-bar"><div class="fill" data-w="66.4"></div></div>
|
||||
<div class="row-val">66.4</div>
|
||||
</div>
|
||||
|
||||
<div class="ig-footer" id="igFooter">
|
||||
<span>Set in Noto Serif SC & Source Serif 4</span>
|
||||
<span class="folio">P. 05</span>
|
||||
<span>Data · 2026 Q2, public benchmarks</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Detail zoom: Typography ligature -->
|
||||
<div class="detail-zoom" id="detailZoom">
|
||||
<div class="detail-word" id="detailWord">bench<span class="fi">ma</span>rks</div>
|
||||
<div class="callout" id="callout" style="display:none"></div>
|
||||
<div class="detail-annotation" id="detailAnnotation">
|
||||
SOURCE SERIF 4 <span class="dot">·</span> ITALIC <span class="dot">·</span> OLDSTYLE FIGURES
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Brand Reveal -->
|
||||
<div class="brand-wall" id="brandWall">
|
||||
<div class="brand-wordmark" id="brandWord">huashu<span class="dot">·</span>design</div>
|
||||
<div class="brand-underline" id="brandLine"></div>
|
||||
<div class="brand-cn" id="brandCn">数 据 · 印 刷 级 排 版</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
// ---------- Scale stage to viewport ----------
|
||||
const stage = document.getElementById('stage');
|
||||
function fitStage() {
|
||||
const s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
|
||||
stage.style.transform = `translate(-50%, -50%) scale(${s})`;
|
||||
}
|
||||
fitStage();
|
||||
window.addEventListener('resize', fitStage);
|
||||
|
||||
// ---------- Easing ----------
|
||||
const expoOut = t => t >= 1 ? 1 : 1 - Math.pow(2, -10 * t);
|
||||
const expoIn = t => t <= 0 ? 0 : Math.pow(2, 10 * (t - 1));
|
||||
const cubicOut = t => 1 - Math.pow(1 - t, 3);
|
||||
const cubicInOut = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2;
|
||||
const lerp = (t, a, b, c, d, ease=x=>x) => {
|
||||
if (b === a) return c;
|
||||
const k = Math.max(0, Math.min(1, (t - a) / (b - a)));
|
||||
return c + (d - c) * ease(k);
|
||||
};
|
||||
const seg = (t, a, b) => Math.max(0, Math.min(1, (t - a) / (b - a)));
|
||||
|
||||
// ---------- Refs ----------
|
||||
const splitLeft = document.getElementById('splitLeft');
|
||||
const jsonLabel = document.getElementById('jsonLabel');
|
||||
const jsonBlock = document.getElementById('jsonBlock');
|
||||
const pipe = document.getElementById('pipe');
|
||||
const infographic = document.getElementById('infographic');
|
||||
const igMasthead = document.getElementById('igMasthead');
|
||||
const igDisplay = document.getElementById('igDisplay');
|
||||
const igDeck = document.getElementById('igDeck');
|
||||
const igGrid = document.getElementById('igGrid');
|
||||
const igCells = igGrid.querySelectorAll('.ig-cell');
|
||||
const igBars = document.getElementById('igBars');
|
||||
const igBarFills = igBars.querySelectorAll('.fill');
|
||||
const igFooter = document.getElementById('igFooter');
|
||||
const detailZoom = document.getElementById('detailZoom');
|
||||
const detailWord = document.getElementById('detailWord');
|
||||
const detailAnnotation = document.getElementById('detailAnnotation');
|
||||
const callout = document.getElementById('callout');
|
||||
const brandWall = document.getElementById('brandWall');
|
||||
const brandWord = document.getElementById('brandWord');
|
||||
const brandLine = document.getElementById('brandLine');
|
||||
const brandCn = document.getElementById('brandCn');
|
||||
const watermark = document.getElementById('watermark');
|
||||
|
||||
// ---------- JSON content (for progressive reveal) ----------
|
||||
const jsonRaw = [
|
||||
'{',
|
||||
' "issue": "2026-Q2",',
|
||||
' "leader": "Claude 4.7",',
|
||||
' "models": [',
|
||||
' { "name": "Claude 4.7", "swe": 77.2 },',
|
||||
' { "name": "GPT-5 Turbo", "swe": 74.8 },',
|
||||
' { "name": "Gemini 3 Pro", "swe": 71.3 },',
|
||||
' { "name": "GLM-5", "swe": 68.9 },',
|
||||
' { "name": "Kimi k3", "swe": 66.4 }',
|
||||
' ],',
|
||||
' "gpqa_top": 84.5,',
|
||||
' "price_per_M": 3',
|
||||
'}'
|
||||
];
|
||||
|
||||
function formatJson(lines) {
|
||||
return lines.map(line => {
|
||||
return line
|
||||
.replace(/"([a-zA-Z_]+)":/g, '<span class="k">"$1"</span>:')
|
||||
.replace(/: "([^"]+)"/g, ': <span class="s">"$1"</span>')
|
||||
.replace(/: ([0-9.]+)/g, ': <span class="n">$1</span>')
|
||||
.replace(/([{}\[\],])/g, '<span class="p">$1</span>');
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
// ---------- Timeline ----------
|
||||
const DURATION = 10.0;
|
||||
|
||||
// SFX cue points (played back in ffmpeg post-processing, not browser):
|
||||
// t=0.35 → keyboard/type-fast.mp3 (data entering)
|
||||
// t=2.15 → container/card-snap.mp3 (infographic settles)
|
||||
// t=6.75 → transition/whoosh-fast.mp3 (zoom-in to typography)
|
||||
// t=8.70 → impact/logo-reveal.mp3 (brand reveal chime)
|
||||
const sfxFired = new Set();
|
||||
function fireOnce(key) {
|
||||
if (sfxFired.has(key)) return;
|
||||
sfxFired.add(key);
|
||||
// cue emitted for post-processing; no in-browser playback
|
||||
}
|
||||
|
||||
let startTime = null;
|
||||
let raf;
|
||||
|
||||
function tick(now) {
|
||||
if (startTime == null) startTime = now;
|
||||
const t = (now - startTime) / 1000;
|
||||
|
||||
// ── Beat 1: 0-2s · JSON data appears, types in ─────────
|
||||
// JSON label fade in
|
||||
{
|
||||
const k = cubicOut(seg(t, 0.15, 0.55));
|
||||
jsonLabel.style.opacity = k;
|
||||
splitLeft.style.opacity = '1';
|
||||
}
|
||||
// Progressive type-reveal: reveal N lines of JSON by time
|
||||
{
|
||||
const totalLines = jsonRaw.length;
|
||||
const k = seg(t, 0.3, 1.9);
|
||||
const linesShown = Math.floor(k * totalLines);
|
||||
const shown = jsonRaw.slice(0, Math.max(0, linesShown));
|
||||
jsonBlock.innerHTML = formatJson(shown);
|
||||
if (linesShown >= 3 && t < 1.9) fireOnce('datain');
|
||||
}
|
||||
|
||||
// ── Pipe arrow (1.8 → 2.2) ─────────────────────────────
|
||||
{
|
||||
const k = cubicOut(seg(t, 1.8, 2.2));
|
||||
pipe.style.opacity = k;
|
||||
}
|
||||
|
||||
// ── Beat 2a: 2.0-3.2s · Infographic canvas arrives ─────
|
||||
{
|
||||
const k = expoOut(seg(t, 2.0, 2.8));
|
||||
infographic.style.opacity = k;
|
||||
infographic.style.transform = `translateY(${lerp(t, 2.0, 2.8, 18, 0, expoOut)}px)`;
|
||||
if (t > 2.1) fireOnce('settle');
|
||||
}
|
||||
// Masthead
|
||||
{
|
||||
const k = cubicOut(seg(t, 2.6, 3.1));
|
||||
igMasthead.style.opacity = k;
|
||||
}
|
||||
|
||||
// ── Beat 2b: 3.0-4.2s · Display headline appears ──────
|
||||
{
|
||||
const k = expoOut(seg(t, 3.0, 3.8));
|
||||
igDisplay.style.opacity = k;
|
||||
igDisplay.style.transform = `translateY(${lerp(t, 3.0, 3.8, 16, 0, expoOut)}px)`;
|
||||
}
|
||||
// Deck line (italic)
|
||||
{
|
||||
const k = cubicOut(seg(t, 3.6, 4.2));
|
||||
igDeck.style.opacity = k;
|
||||
}
|
||||
|
||||
// ── Beat 2c: 4.0-5.2s · Grid cells (ripple, 4 cells) ──
|
||||
igCells.forEach((cell, i) => {
|
||||
const start = 4.0 + i * 0.12;
|
||||
const end = start + 0.5;
|
||||
const k = expoOut(seg(t, start, end));
|
||||
cell.style.opacity = k;
|
||||
cell.style.transform = `translateY(${lerp(t, start, end, 14, 0, expoOut)}px)`;
|
||||
});
|
||||
|
||||
// ── Beat 2d: 5.2-6.4s · Comparison bars grow ─────────
|
||||
{
|
||||
const k = cubicOut(seg(t, 5.1, 5.4));
|
||||
igBars.style.opacity = k;
|
||||
}
|
||||
igBarFills.forEach((fill, i) => {
|
||||
const start = 5.3 + i * 0.08;
|
||||
const end = start + 0.7;
|
||||
const w = parseFloat(fill.getAttribute('data-w'));
|
||||
const pct = lerp(t, start, end, 0, w, expoOut);
|
||||
fill.style.width = pct + '%';
|
||||
});
|
||||
// Footer
|
||||
{
|
||||
const k = cubicOut(seg(t, 6.0, 6.6));
|
||||
igFooter.style.opacity = k * 0.9;
|
||||
}
|
||||
|
||||
// ── Beat 2e: 6.6-8.2s · Zoom to typography detail ────
|
||||
if (t >= 6.6 && t < 8.3) {
|
||||
const k = expoOut(seg(t, 6.6, 7.4));
|
||||
// Infographic scales up and fades — simulate push-in
|
||||
const scale = lerp(t, 6.6, 7.4, 1, 3.4, expoOut);
|
||||
const ty = lerp(t, 6.6, 7.4, 0, -140, expoOut);
|
||||
infographic.style.transform = `translateY(${ty}px) scale(${scale})`;
|
||||
infographic.style.opacity = String(1 - k * 0.85);
|
||||
splitLeft.style.opacity = String(1 - k);
|
||||
pipe.style.opacity = String(1 - k);
|
||||
|
||||
// Detail zoom fades in
|
||||
const k2 = expoOut(seg(t, 7.0, 7.7));
|
||||
detailZoom.style.opacity = k2;
|
||||
// Word subtle scale-in (starts from 0.96)
|
||||
detailWord.style.transform = `scale(${lerp(t, 7.0, 7.9, 0.96, 1.0, expoOut)})`;
|
||||
|
||||
// SFX at 6.7
|
||||
if (t > 6.7) fireOnce('zoom');
|
||||
|
||||
// Callout + annotation (7.5 → 8.1)
|
||||
const k3 = cubicOut(seg(t, 7.6, 8.1));
|
||||
callout.style.opacity = k3;
|
||||
detailAnnotation.style.opacity = k3;
|
||||
}
|
||||
|
||||
// ── Beat 3: 8.2-10s · Brand reveal ───────────────────
|
||||
// Detail zoom fades under brand wall
|
||||
if (t >= 8.1) {
|
||||
const k = cubicOut(seg(t, 8.1, 8.5));
|
||||
detailZoom.style.opacity = String(Math.max(0, 1 - k));
|
||||
}
|
||||
// Brand wall slides up from bottom
|
||||
{
|
||||
const k = expoOut(seg(t, 8.1, 8.7));
|
||||
brandWall.style.transform = `translateY(${lerp(t, 8.1, 8.7, 100, 0, expoOut)}%)`;
|
||||
brandWall.style.opacity = k > 0 ? '1' : '0';
|
||||
if (k > 0.55) watermark.classList.add('on-light');
|
||||
else watermark.classList.remove('on-light');
|
||||
}
|
||||
// Wordmark
|
||||
{
|
||||
const k = expoOut(seg(t, 8.6, 9.2));
|
||||
brandWord.style.opacity = k;
|
||||
brandWord.style.transform = `scale(${lerp(t, 8.6, 9.2, 0.92, 1.0, expoOut)})`;
|
||||
if (t > 8.65) fireOnce('chime');
|
||||
}
|
||||
// Underline
|
||||
{
|
||||
const k = expoOut(seg(t, 9.0, 9.6));
|
||||
brandLine.style.width = (280 * k) + 'px';
|
||||
}
|
||||
// CN tagline
|
||||
{
|
||||
const k = cubicOut(seg(t, 9.3, 9.9));
|
||||
brandCn.style.opacity = k * 0.9;
|
||||
}
|
||||
|
||||
// Loop / hold
|
||||
if (t < DURATION) {
|
||||
raf = requestAnimationFrame(tick);
|
||||
} else {
|
||||
if (!window.__recording) {
|
||||
setTimeout(() => {
|
||||
// Reset
|
||||
startTime = null;
|
||||
sfxFired.clear();
|
||||
jsonBlock.innerHTML = '';
|
||||
splitLeft.style.opacity = '0';
|
||||
pipe.style.opacity = '0';
|
||||
infographic.style.opacity = '0';
|
||||
infographic.style.transform = 'translateY(18px) scale(1)';
|
||||
igMasthead.style.opacity = '0';
|
||||
igDisplay.style.opacity = '0';
|
||||
igDeck.style.opacity = '0';
|
||||
igBars.style.opacity = '0';
|
||||
igFooter.style.opacity = '0';
|
||||
igCells.forEach(c => { c.style.opacity = '0'; });
|
||||
igBarFills.forEach(f => { f.style.width = '0%'; });
|
||||
detailZoom.style.opacity = '0';
|
||||
callout.style.opacity = '0';
|
||||
detailAnnotation.style.opacity = '0';
|
||||
brandWall.style.transform = 'translateY(100%)';
|
||||
brandWall.style.opacity = '0';
|
||||
brandWord.style.opacity = '0';
|
||||
brandLine.style.width = '0';
|
||||
brandCn.style.opacity = '0';
|
||||
watermark.classList.remove('on-light');
|
||||
raf = requestAnimationFrame(tick);
|
||||
}, 800);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.__seek = function(s) {
|
||||
startTime = performance.now() - s * 1000;
|
||||
};
|
||||
|
||||
// Wait for fonts, then start
|
||||
(document.fonts ? document.fonts.ready : Promise.resolve()).then(() => {
|
||||
requestAnimationFrame((now) => {
|
||||
startTime = now;
|
||||
window.__ready = true;
|
||||
raf = requestAnimationFrame(tick);
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
885
.claude/skills/huashu-design/demos/c6-expert-review-en.html
Normal file
885
.claude/skills/huashu-design/demos/c6-expert-review-en.html
Normal file
@@ -0,0 +1,885 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>c6 · Five Axes · One Punch List</title>
|
||||
<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=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@200;300;400;500;600&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #000000;
|
||||
--ink: #FFFFFF;
|
||||
--ink-80: rgba(255,255,255,0.82);
|
||||
--ink-60: rgba(255,255,255,0.58);
|
||||
--muted: rgba(255,255,255,0.40);
|
||||
--dim: rgba(255,255,255,0.18);
|
||||
--hairline: rgba(255,255,255,0.12);
|
||||
--accent: #D97757;
|
||||
--accent-deep: #B85D3D;
|
||||
--cd-bg: #F5F4F0;
|
||||
--cd-panel: #FFFFFF;
|
||||
--cd-ink: #1A1918;
|
||||
|
||||
--serif-zh: "Noto Serif SC", "Songti SC", serif;
|
||||
--serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
|
||||
--sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
|
||||
--mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
|
||||
}
|
||||
html, body {
|
||||
margin: 0; padding: 0;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
font-family: var(--sans);
|
||||
color: var(--ink);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
.stage {
|
||||
position: fixed;
|
||||
top: 50%; left: 50%;
|
||||
width: 1920px; height: 1080px;
|
||||
transform-origin: center center;
|
||||
background: var(--bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Film grain */
|
||||
.stage::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='300'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>");
|
||||
opacity: 0.02;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* Chrome */
|
||||
.mark {
|
||||
position: absolute;
|
||||
top: 48px; left: 64px;
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.2em;
|
||||
color: rgba(255,255,255,1);
|
||||
opacity: 0.16;
|
||||
pointer-events: none;
|
||||
z-index: 50;
|
||||
}
|
||||
.mark-right {
|
||||
position: absolute;
|
||||
top: 48px; right: 64px;
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.2em;
|
||||
color: rgba(255,255,255,1);
|
||||
opacity: 0.16;
|
||||
pointer-events: none;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
/* Title */
|
||||
.title-line {
|
||||
position: absolute;
|
||||
top: 108px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.28em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
/* Main composition: camera wrapper for push-in at Beat 3 */
|
||||
.camera {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transform-origin: 1000px 940px; /* center of Fix first-row */
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* ============ LEFT: under-review artwork ============ */
|
||||
.subject {
|
||||
position: absolute;
|
||||
left: 150px;
|
||||
top: 310px;
|
||||
width: 640px;
|
||||
height: 460px;
|
||||
background: #0B0B0B;
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform, filter;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
.subject::after {
|
||||
/* subtle inner vignette */
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
box-shadow: inset 0 0 120px rgba(0,0,0,0.6);
|
||||
pointer-events: none;
|
||||
}
|
||||
.subject-label {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 18px;
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.25em;
|
||||
color: var(--muted);
|
||||
z-index: 3;
|
||||
}
|
||||
.subject-dot {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 18px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: var(--accent);
|
||||
border-radius: 50%;
|
||||
z-index: 3;
|
||||
box-shadow: 0 0 10px rgba(217,119,87,0.6);
|
||||
}
|
||||
/* Subject wireframe: abstract design mockup */
|
||||
.subject-canvas {
|
||||
position: absolute;
|
||||
inset: 50px 36px 36px;
|
||||
}
|
||||
.wf-h1 {
|
||||
width: 62%;
|
||||
height: 18px;
|
||||
background: rgba(255,255,255,0.28);
|
||||
border-radius: 2px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.wf-h2 {
|
||||
width: 38%;
|
||||
height: 10px;
|
||||
background: rgba(255,255,255,0.14);
|
||||
border-radius: 2px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.wf-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.wf-row .bar {
|
||||
height: 8px;
|
||||
background: rgba(255,255,255,0.10);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.wf-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 14px;
|
||||
margin-top: 28px;
|
||||
}
|
||||
.wf-card {
|
||||
height: 82px;
|
||||
background: rgba(255,255,255,0.04);
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
}
|
||||
.wf-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 12px; top: 14px;
|
||||
width: 40%;
|
||||
height: 6px;
|
||||
background: rgba(255,255,255,0.22);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.wf-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 12px; bottom: 16px;
|
||||
width: 64%;
|
||||
height: 4px;
|
||||
background: rgba(255,255,255,0.10);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.wf-card.accent { border-color: rgba(217,119,87,0.55); background: rgba(217,119,87,0.06); }
|
||||
.wf-card.accent::before { background: var(--accent); }
|
||||
.wf-foot {
|
||||
position: absolute;
|
||||
left: 0; right: 0;
|
||||
bottom: 0;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.wf-chip {
|
||||
height: 22px;
|
||||
padding: 0 10px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
border-radius: 11px;
|
||||
flex: 0 0 auto;
|
||||
width: 68px;
|
||||
}
|
||||
.wf-chip.wide { width: 120px; }
|
||||
|
||||
/* ============ Light sweep ============ */
|
||||
.sweep {
|
||||
position: absolute;
|
||||
left: 130px;
|
||||
top: 250px;
|
||||
width: 680px;
|
||||
height: 140px;
|
||||
background: linear-gradient(180deg,
|
||||
rgba(217,119,87,0) 0%,
|
||||
rgba(217,119,87,0.12) 20%,
|
||||
rgba(255,220,200,0.62) 50%,
|
||||
rgba(217,119,87,0.18) 80%,
|
||||
rgba(217,119,87,0) 100%);
|
||||
filter: blur(14px);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
z-index: 4;
|
||||
mix-blend-mode: screen;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
.sweep-line {
|
||||
position: absolute;
|
||||
left: 150px;
|
||||
top: 310px;
|
||||
width: 640px;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(255,220,200,0.2) 10%,
|
||||
rgba(255,220,200,0.9) 50%,
|
||||
rgba(255,220,200,0.2) 90%,
|
||||
transparent 100%);
|
||||
filter: blur(0.6px);
|
||||
box-shadow: 0 0 14px rgba(217,119,87,0.8), 0 0 30px rgba(217,119,87,0.3);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
z-index: 6;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
/* ============ RIGHT: radar chart ============ */
|
||||
.radar-wrap {
|
||||
position: absolute;
|
||||
right: 280px;
|
||||
top: 200px;
|
||||
width: 520px;
|
||||
height: 520px;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
.radar-wrap svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
.radar-grid path {
|
||||
fill: none;
|
||||
stroke: rgba(255,255,255,0.10);
|
||||
stroke-width: 1;
|
||||
}
|
||||
.radar-spoke {
|
||||
stroke: rgba(255,255,255,0.08);
|
||||
stroke-width: 1;
|
||||
}
|
||||
.radar-poly {
|
||||
fill: rgba(217,119,87,0.16);
|
||||
stroke: var(--accent);
|
||||
stroke-width: 2;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.radar-point {
|
||||
fill: var(--accent);
|
||||
stroke: #1A1918;
|
||||
stroke-width: 2;
|
||||
}
|
||||
.radar-label {
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.2em;
|
||||
fill: var(--muted);
|
||||
text-transform: uppercase;
|
||||
opacity: 0;
|
||||
}
|
||||
.radar-label-zh {
|
||||
font-family: var(--serif-en);
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
fill: var(--ink);
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
.radar-score {
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
fill: var(--accent);
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.radar-title {
|
||||
position: absolute;
|
||||
right: 280px;
|
||||
top: 160px;
|
||||
width: 520px;
|
||||
text-align: center;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.28em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.radar-score-total {
|
||||
position: absolute;
|
||||
left: 150px;
|
||||
top: 170px;
|
||||
width: 640px;
|
||||
text-align: left;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.radar-score-total .score-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 24px;
|
||||
}
|
||||
.radar-score-total .score-label {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.28em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.radar-score-total .score-num {
|
||||
font-family: var(--serif-en);
|
||||
font-size: 72px;
|
||||
font-weight: 300;
|
||||
color: var(--ink);
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1;
|
||||
}
|
||||
.radar-score-total .score-num .accent { color: var(--accent); }
|
||||
.radar-score-total .score-total {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.28em;
|
||||
color: var(--muted);
|
||||
margin-top: 8px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* ============ Single Fix row (Concept Card lean) ============ */
|
||||
.fix-lane {
|
||||
position: absolute;
|
||||
left: 150px;
|
||||
bottom: 120px;
|
||||
width: 1620px;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
.fix-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 14px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
}
|
||||
.fix-mark {
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.28em;
|
||||
color: var(--accent);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.fix-zh {
|
||||
font-family: var(--serif-en);
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
color: var(--ink);
|
||||
}
|
||||
.fix-count {
|
||||
margin-left: auto;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.2em;
|
||||
}
|
||||
|
||||
.fix-row {
|
||||
position: relative;
|
||||
font-family: var(--sans);
|
||||
font-size: 28px;
|
||||
font-weight: 300;
|
||||
color: var(--ink);
|
||||
line-height: 1.45;
|
||||
padding: 12px 0;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
.fix-row .idx {
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.2em;
|
||||
flex: 0 0 40px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
.fix-row .mono {
|
||||
font-family: var(--mono);
|
||||
font-size: 26px;
|
||||
letter-spacing: 0;
|
||||
color: var(--accent);
|
||||
font-weight: 400;
|
||||
}
|
||||
.fix-row .arrow {
|
||||
color: var(--muted);
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.fix-severity {
|
||||
display: inline-block;
|
||||
padding: 3px 10px;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.22em;
|
||||
color: var(--accent);
|
||||
border: 1px solid rgba(217,119,87,0.5);
|
||||
border-radius: 3px;
|
||||
margin-right: 10px;
|
||||
vertical-align: 3px;
|
||||
}
|
||||
.fix-pulse {
|
||||
position: absolute;
|
||||
inset: 4px -12px 4px -12px;
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 4px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
will-change: opacity;
|
||||
box-shadow: 0 0 24px rgba(217,119,87,0.35);
|
||||
}
|
||||
|
||||
/* ============ Brand Reveal (hero-v10 signature) ============ */
|
||||
.stage-dimmer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #000000;
|
||||
opacity: 0;
|
||||
z-index: 40;
|
||||
pointer-events: none;
|
||||
will-change: opacity;
|
||||
}
|
||||
.brand-panel {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #F5F4F0;
|
||||
transform: translateY(100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 50;
|
||||
will-change: transform;
|
||||
}
|
||||
.brand-wordmark {
|
||||
font-family: var(--serif-en);
|
||||
font-size: 72px;
|
||||
font-weight: 100;
|
||||
font-variation-settings: "wght" 100;
|
||||
letter-spacing: -0.02em;
|
||||
color: #1A1918;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
will-change: opacity, transform, font-variation-settings, font-weight;
|
||||
}
|
||||
.brand-wordmark .accent { color: #D97757; font-weight: inherit; }
|
||||
.brand-line {
|
||||
margin-top: 60px;
|
||||
height: 2px;
|
||||
width: 0;
|
||||
background: #D97757;
|
||||
align-self: center;
|
||||
will-change: width;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="stage" id="stage">
|
||||
<div class="mark">HUASHU · DESIGN</div>
|
||||
<div class="mark-right">V2 · 2026</div>
|
||||
|
||||
<div class="title-line" id="titleLine">c6 · Expert Review · Five Axes</div>
|
||||
|
||||
<div class="camera" id="camera">
|
||||
<!-- Subject: design under review -->
|
||||
<div class="subject" id="subject">
|
||||
<div class="subject-label">SUBJECT · DRAFT_V3</div>
|
||||
<div class="subject-dot"></div>
|
||||
<div class="subject-canvas">
|
||||
<div class="wf-h1"></div>
|
||||
<div class="wf-h2"></div>
|
||||
<div class="wf-row"><div class="bar" style="width:24%"></div><div class="bar" style="width:14%"></div><div class="bar" style="width:20%"></div></div>
|
||||
<div class="wf-row"><div class="bar" style="width:30%"></div><div class="bar" style="width:10%"></div></div>
|
||||
<div class="wf-grid">
|
||||
<div class="wf-card"></div>
|
||||
<div class="wf-card accent"></div>
|
||||
<div class="wf-card"></div>
|
||||
</div>
|
||||
<div class="wf-foot">
|
||||
<div class="wf-chip wide"></div>
|
||||
<div class="wf-chip"></div>
|
||||
<div class="wf-chip"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scanning light -->
|
||||
<div class="sweep" id="sweep"></div>
|
||||
<div class="sweep-line" id="sweepLine"></div>
|
||||
|
||||
<!-- Radar chart (right) -->
|
||||
<div class="radar-title" id="radarTitle">Five-Axis Diagnosis · Radar</div>
|
||||
<div class="radar-wrap" id="radarWrap">
|
||||
<svg viewBox="-270 -270 540 540" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Grid rings (5 levels) -->
|
||||
<g class="radar-grid" id="radarGrid"></g>
|
||||
<!-- Spokes to 5 axes -->
|
||||
<g id="radarSpokes"></g>
|
||||
<!-- Filled polygon -->
|
||||
<polygon id="radarPoly" class="radar-poly" points="" />
|
||||
<!-- Points -->
|
||||
<g id="radarPoints"></g>
|
||||
<!-- Axis labels -->
|
||||
<g id="radarLabels"></g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="radar-score-total" id="radarTotal">
|
||||
<div class="score-row">
|
||||
<div class="score-num"><span id="scoreNum">0</span><span class="accent">/50</span></div>
|
||||
<div>
|
||||
<div class="score-label">OVERALL · PASSED</div>
|
||||
<div class="score-total">WEIGHTED · 7.4</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Single Fix row: Concept Card lean -->
|
||||
<div class="fix-lane" id="fixLane">
|
||||
<div class="fix-head">
|
||||
<span class="fix-mark">FIX</span>
|
||||
<span class="fix-zh">Fix</span>
|
||||
<span class="fix-count">01 / 01</span>
|
||||
</div>
|
||||
<div class="fix-row">
|
||||
<span class="idx">01</span>
|
||||
<span><span class="fix-severity">⚡</span>Tracking <span class="mono">0.02</span><span class="arrow"> → </span><span class="mono">0.04em</span></span>
|
||||
<div class="fix-pulse" id="fixPulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Brand Reveal (hero-v10 signature) -->
|
||||
<div class="stage-dimmer" id="stageDimmer"></div>
|
||||
<div class="brand-panel" id="brandPanel">
|
||||
<div class="brand-wordmark" id="brandMark">huashu<span class="accent">-</span>design</div>
|
||||
<div class="brand-line" id="brandLine"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Auto-scale
|
||||
function fitStage() {
|
||||
const stage = document.getElementById('stage');
|
||||
const sx = window.innerWidth / 1920;
|
||||
const sy = window.innerHeight / 1080;
|
||||
const s = Math.min(sx, sy);
|
||||
stage.style.transform = `translate(-50%, -50%) scale(${s})`;
|
||||
}
|
||||
fitStage();
|
||||
window.addEventListener('resize', fitStage);
|
||||
|
||||
// Easings
|
||||
const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
|
||||
const expoIn = t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
|
||||
const cubicInOut = t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||
const cubicOut = t => 1 - Math.pow(1 - t, 3);
|
||||
|
||||
function lerp(t, a, b, easing) {
|
||||
if (t <= 0) return a;
|
||||
if (t >= 1) return b;
|
||||
const e = easing ? easing(t) : t;
|
||||
return a + (b - a) * e;
|
||||
}
|
||||
function seg(time, start, end) {
|
||||
if (time <= start) return 0;
|
||||
if (time >= end) return 1;
|
||||
return (time - start) / (end - start);
|
||||
}
|
||||
|
||||
// ============ Build radar SVG ============
|
||||
const RADIUS = 210;
|
||||
const AXES = [
|
||||
{ zh: 'Philosophy', en: 'PHILOSOPHY', score: 8 },
|
||||
{ zh: 'Hierarchy', en: 'HIERARCHY', score: 6 },
|
||||
{ zh: 'Execution', en: 'EXECUTION', score: 8 },
|
||||
{ zh: 'Function', en: 'FUNCTION', score: 7 },
|
||||
{ zh: 'Innovation', en: 'INNOVATION', score: 8 },
|
||||
];
|
||||
const N = AXES.length;
|
||||
|
||||
function axisPoint(i, r) {
|
||||
// Start at top (-90deg), clockwise
|
||||
const angle = -Math.PI / 2 + (2 * Math.PI * i) / N;
|
||||
return [Math.cos(angle) * r, Math.sin(angle) * r];
|
||||
}
|
||||
|
||||
// Grid rings (polygons at 5 levels)
|
||||
const gridG = document.getElementById('radarGrid');
|
||||
for (let level = 1; level <= 5; level++) {
|
||||
const r = (RADIUS * level) / 5;
|
||||
const pts = [];
|
||||
for (let i = 0; i < N; i++) {
|
||||
const [x, y] = axisPoint(i, r);
|
||||
pts.push(`${x.toFixed(2)},${y.toFixed(2)}`);
|
||||
}
|
||||
const poly = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
|
||||
poly.setAttribute('points', pts.join(' '));
|
||||
poly.setAttribute('fill', 'none');
|
||||
poly.setAttribute('stroke', level === 5 ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.07)');
|
||||
poly.setAttribute('stroke-width', '1');
|
||||
gridG.appendChild(poly);
|
||||
}
|
||||
|
||||
// Spokes
|
||||
const spokesG = document.getElementById('radarSpokes');
|
||||
for (let i = 0; i < N; i++) {
|
||||
const [x, y] = axisPoint(i, RADIUS);
|
||||
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
line.setAttribute('x1', 0);
|
||||
line.setAttribute('y1', 0);
|
||||
line.setAttribute('x2', x.toFixed(2));
|
||||
line.setAttribute('y2', y.toFixed(2));
|
||||
line.setAttribute('class', 'radar-spoke');
|
||||
spokesG.appendChild(line);
|
||||
}
|
||||
|
||||
// Labels (position outside). ZH sits at a base radial distance; EN stacks
|
||||
// below it with a fixed vertical offset to avoid overlap on the side axes.
|
||||
const labelsG = document.getElementById('radarLabels');
|
||||
AXES.forEach((axis, i) => {
|
||||
const angle = -Math.PI / 2 + (2 * Math.PI * i) / N;
|
||||
const dirX = Math.cos(angle);
|
||||
const dirY = Math.sin(angle);
|
||||
|
||||
// text-anchor based on horizontal direction
|
||||
let anchor = 'middle';
|
||||
if (dirX > 0.3) anchor = 'start';
|
||||
else if (dirX < -0.3) anchor = 'end';
|
||||
|
||||
const baseRadial = RADIUS + 36;
|
||||
const [bx, by] = axisPoint(i, baseRadial);
|
||||
|
||||
// Title Case serif italic label (only one per axis in EN)
|
||||
const zhText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
zhText.setAttribute('x', bx.toFixed(2));
|
||||
zhText.setAttribute('y', by.toFixed(2));
|
||||
zhText.setAttribute('text-anchor', anchor);
|
||||
zhText.setAttribute('dominant-baseline', 'middle');
|
||||
zhText.setAttribute('class', 'radar-label-zh');
|
||||
zhText.textContent = axis.zh;
|
||||
labelsG.appendChild(zhText);
|
||||
});
|
||||
|
||||
// Points (initial: center)
|
||||
const pointsG = document.getElementById('radarPoints');
|
||||
const pointEls = AXES.map((axis, i) => {
|
||||
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
circle.setAttribute('cx', 0);
|
||||
circle.setAttribute('cy', 0);
|
||||
circle.setAttribute('r', 5);
|
||||
circle.setAttribute('class', 'radar-point');
|
||||
circle.setAttribute('opacity', '0');
|
||||
pointsG.appendChild(circle);
|
||||
return circle;
|
||||
});
|
||||
const radarPoly = document.getElementById('radarPoly');
|
||||
|
||||
// ============ Timeline (10s) ============
|
||||
// Beat 1 (0-2s): title + subject enters
|
||||
// Beat 2 (2-8s):
|
||||
// 2.0-3.8: light sweep top → bottom (1.8s)
|
||||
// 3.2-4.8: radar grid fades in + polygon + points grow from center
|
||||
// 4.8-5.2: score count up
|
||||
// 5.0-6.0: Keep col ripple in
|
||||
// 5.5-6.5: Fix col ripple in
|
||||
// 6.0-7.0: Quick Wins col ripple in
|
||||
// 7.0-8.0: hold
|
||||
// Beat 3 (8-10s): push-in camera to fix[0] + pulse (8-9), brand reveal (8.0-10.0)
|
||||
|
||||
const titleLine = document.getElementById('titleLine');
|
||||
const subject = document.getElementById('subject');
|
||||
const sweep = document.getElementById('sweep');
|
||||
const sweepLine = document.getElementById('sweepLine');
|
||||
const radarTitle = document.getElementById('radarTitle');
|
||||
const radarWrap = document.getElementById('radarWrap');
|
||||
const radarTotal = document.getElementById('radarTotal');
|
||||
const scoreNum = document.getElementById('scoreNum');
|
||||
const fixLane = document.getElementById('fixLane');
|
||||
const fixPulse = document.getElementById('fixPulse');
|
||||
const camera = document.getElementById('camera');
|
||||
const stageDimmer = document.getElementById('stageDimmer');
|
||||
const brandPanel = document.getElementById('brandPanel');
|
||||
const brandMark = document.getElementById('brandMark');
|
||||
const brandLine = document.getElementById('brandLine');
|
||||
|
||||
const DURATION = 10.0;
|
||||
let startTime = null;
|
||||
let loop = true;
|
||||
if (window.__recording === true) loop = false;
|
||||
|
||||
function tick(now) {
|
||||
if (startTime === null) startTime = now;
|
||||
let t = (now - startTime) / 1000;
|
||||
|
||||
if (t >= DURATION) {
|
||||
if (loop) { startTime = now; t = 0; }
|
||||
else { t = DURATION; }
|
||||
}
|
||||
|
||||
// Title fade in/out
|
||||
const titleIn = seg(t, 0.2, 1.2);
|
||||
const titleOut = seg(t, 7.6, 8.0);
|
||||
titleLine.style.opacity = Math.min(cubicOut(titleIn), 1 - titleOut);
|
||||
titleLine.style.transform = `translateX(-50%) translateY(${lerp(titleIn, -6, 0, cubicOut)}px)`;
|
||||
|
||||
// Subject appears Beat 1
|
||||
const subjectIn = seg(t, 0.4, 1.8);
|
||||
subject.style.opacity = expoOut(subjectIn);
|
||||
subject.style.transform = `translateY(${lerp(subjectIn, 14, 0, expoOut)}px)`;
|
||||
|
||||
// Subject dims after sweep completes (during Beat 2 to keep focus right)
|
||||
const subjectDim = seg(t, 4.4, 5.6);
|
||||
const dimFactor = lerp(subjectDim, 1.0, 0.38, cubicInOut);
|
||||
subject.style.filter = `saturate(${lerp(subjectDim, 1.0, 0.5, cubicInOut)}) brightness(${dimFactor})`;
|
||||
|
||||
// Light sweep: 2.0-3.8 top to bottom
|
||||
const sweepProgress = seg(t, 2.0, 3.8);
|
||||
const sweepOp = (t < 2.0 || t > 4.2) ? 0 :
|
||||
(t < 2.2 ? seg(t, 2.0, 2.2) :
|
||||
t < 3.7 ? 1 :
|
||||
1 - seg(t, 3.7, 4.2));
|
||||
sweep.style.opacity = sweepOp * 0.95;
|
||||
sweepLine.style.opacity = sweepOp * 1.0;
|
||||
// Move from y=250 to y=700 (subject top 310 to bottom 770)
|
||||
const sweepY = lerp(sweepProgress, -70, 410, cubicInOut);
|
||||
sweep.style.transform = `translateY(${sweepY}px)`;
|
||||
sweepLine.style.transform = `translateY(${sweepY + 70}px)`;
|
||||
|
||||
// Radar title + wrap appear 3.2
|
||||
const radarIn = seg(t, 3.2, 4.0);
|
||||
radarTitle.style.opacity = cubicOut(radarIn);
|
||||
radarWrap.style.opacity = cubicOut(radarIn);
|
||||
radarWrap.style.transform = `scale(${lerp(radarIn, 0.92, 1.0, expoOut)})`;
|
||||
|
||||
// Radar grid strokes already visible once wrap fades; animate grid via stroke-dasharray trick would be overkill.
|
||||
// Instead, grow polygon + points from center (3.6-4.8)
|
||||
const polyGrow = seg(t, 3.6, 4.8);
|
||||
const polyT = expoOut(polyGrow);
|
||||
const polyPts = [];
|
||||
AXES.forEach((axis, i) => {
|
||||
const targetR = (axis.score / 10) * RADIUS;
|
||||
const r = targetR * polyT;
|
||||
const [x, y] = axisPoint(i, r);
|
||||
polyPts.push(`${x.toFixed(2)},${y.toFixed(2)}`);
|
||||
const pt = pointEls[i];
|
||||
pt.setAttribute('cx', x.toFixed(2));
|
||||
pt.setAttribute('cy', y.toFixed(2));
|
||||
pt.setAttribute('opacity', polyT.toFixed(2));
|
||||
});
|
||||
radarPoly.setAttribute('points', polyPts.join(' '));
|
||||
|
||||
// EN labels fade in slightly later
|
||||
const enLabelIn = seg(t, 4.2, 4.8);
|
||||
document.querySelectorAll('[data-type="en-label"]').forEach(el => {
|
||||
el.setAttribute('opacity', cubicOut(enLabelIn).toFixed(2));
|
||||
});
|
||||
|
||||
// Score count up 4.6-5.4, target total = 37
|
||||
const scoreT = seg(t, 4.6, 5.4);
|
||||
const total = AXES.reduce((s, a) => s + a.score, 0); // 37
|
||||
const shown = Math.round(lerp(scoreT, 0, total, cubicOut));
|
||||
scoreNum.textContent = shown;
|
||||
radarTotal.style.opacity = cubicOut(seg(t, 4.4, 5.0));
|
||||
|
||||
// Fix lane ripple in (5.3-6.1)
|
||||
const fixRip = seg(t, 5.3, 6.1);
|
||||
fixLane.style.opacity = expoOut(fixRip);
|
||||
fixLane.style.transform = `translateY(${lerp(fixRip, 24, 0, expoOut)}px)`;
|
||||
|
||||
// Beat 3: Push-in camera to Fix row + pulse (7.4-8.0)
|
||||
const pushT = seg(t, 7.4, 8.0);
|
||||
const scale = lerp(pushT, 1.0, 1.18, cubicInOut);
|
||||
camera.style.transform = `scale(${scale})`;
|
||||
|
||||
// Fix pulse border: blink 2 times between 7.6-8.0
|
||||
const pulseOp = t < 7.6 ? 0 :
|
||||
t < 8.0 ? (0.4 + 0.6 * Math.abs(Math.sin((t - 7.6) * Math.PI * 2.4))) :
|
||||
0;
|
||||
fixPulse.style.opacity = pulseOp;
|
||||
|
||||
// ============ Brand Reveal (hero-v10 signature, aligned) ============
|
||||
// [T-2.0 → T-1.7s] i.e. 8.0-8.3: scene fade to black (0.3s)
|
||||
const soK = seg(t, 8.0, 8.3);
|
||||
stageDimmer.style.opacity = cubicOut(soK);
|
||||
const sceneFade = seg(t, 8.0, 8.3);
|
||||
camera.style.opacity = 1 - cubicOut(sceneFade);
|
||||
|
||||
// [T-1.7 → T-1.3s] i.e. 8.3-8.7: cream panel slides from bottom (0.4s, expoOut)
|
||||
const panelT = seg(t, 8.3, 8.7);
|
||||
const panelY = lerp(panelT, 100, 0, expoOut);
|
||||
brandPanel.style.transform = `translateY(${panelY}%)`;
|
||||
|
||||
// [T-1.3 → T-0.7s] i.e. 8.7-9.3: wordmark wght 100→500 + y 20→0 + opacity 0→1 (0.6s)
|
||||
const markT = seg(t, 8.7, 9.3);
|
||||
const markE = expoOut(markT);
|
||||
const wght = 100 + (500 - 100) * markE;
|
||||
brandMark.style.opacity = markE;
|
||||
brandMark.style.transform = `translateY(${20 * (1 - markE)}px)`;
|
||||
brandMark.style.fontWeight = Math.round(wght);
|
||||
brandMark.style.fontVariationSettings = `"wght" ${wght.toFixed(0)}`;
|
||||
|
||||
// [T-0.7 → T-0.3s] i.e. 9.3-9.7: orange line width 0→280 (0.4s, cubicOut)
|
||||
const lineT = seg(t, 9.3, 9.7);
|
||||
brandLine.style.width = `${lerp(lineT, 0, 280, cubicOut)}px`;
|
||||
|
||||
// [T-0.3 → T] hold
|
||||
|
||||
if (!window.__ready) window.__ready = true;
|
||||
|
||||
if (loop || t < DURATION) requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
(document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve())
|
||||
.then(() => requestAnimationFrame(tick));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
894
.claude/skills/huashu-design/demos/c6-expert-review.html
Normal file
894
.claude/skills/huashu-design/demos/c6-expert-review.html
Normal file
@@ -0,0 +1,894 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-Hans">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>c6 · 五个维度,给你一份手术单</title>
|
||||
<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=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@200;300;400;500;600&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #000000;
|
||||
--ink: #FFFFFF;
|
||||
--ink-80: rgba(255,255,255,0.82);
|
||||
--ink-60: rgba(255,255,255,0.58);
|
||||
--muted: rgba(255,255,255,0.40);
|
||||
--dim: rgba(255,255,255,0.18);
|
||||
--hairline: rgba(255,255,255,0.12);
|
||||
--accent: #D97757;
|
||||
--accent-deep: #B85D3D;
|
||||
--cd-bg: #F5F4F0;
|
||||
--cd-panel: #FFFFFF;
|
||||
--cd-ink: #1A1918;
|
||||
|
||||
--serif-zh: "Noto Serif SC", "Songti SC", serif;
|
||||
--serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
|
||||
--sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
|
||||
--mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
|
||||
}
|
||||
html, body {
|
||||
margin: 0; padding: 0;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
font-family: var(--sans);
|
||||
color: var(--ink);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
.stage {
|
||||
position: fixed;
|
||||
top: 50%; left: 50%;
|
||||
width: 1920px; height: 1080px;
|
||||
transform-origin: center center;
|
||||
background: var(--bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Film grain */
|
||||
.stage::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='300'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>");
|
||||
opacity: 0.02;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* Chrome */
|
||||
.mark {
|
||||
position: absolute;
|
||||
top: 48px; left: 64px;
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.2em;
|
||||
color: rgba(255,255,255,1);
|
||||
opacity: 0.16;
|
||||
pointer-events: none;
|
||||
z-index: 50;
|
||||
}
|
||||
.mark-right {
|
||||
position: absolute;
|
||||
top: 48px; right: 64px;
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.2em;
|
||||
color: rgba(255,255,255,1);
|
||||
opacity: 0.16;
|
||||
pointer-events: none;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
/* Title */
|
||||
.title-line {
|
||||
position: absolute;
|
||||
top: 108px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.28em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
/* Main composition: camera wrapper for push-in at Beat 3 */
|
||||
.camera {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transform-origin: 1000px 940px; /* center of Fix first-row */
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* ============ LEFT: under-review artwork ============ */
|
||||
.subject {
|
||||
position: absolute;
|
||||
left: 150px;
|
||||
top: 310px;
|
||||
width: 640px;
|
||||
height: 460px;
|
||||
background: #0B0B0B;
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform, filter;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
.subject::after {
|
||||
/* subtle inner vignette */
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
box-shadow: inset 0 0 120px rgba(0,0,0,0.6);
|
||||
pointer-events: none;
|
||||
}
|
||||
.subject-label {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 18px;
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.25em;
|
||||
color: var(--muted);
|
||||
z-index: 3;
|
||||
}
|
||||
.subject-dot {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 18px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: var(--accent);
|
||||
border-radius: 50%;
|
||||
z-index: 3;
|
||||
box-shadow: 0 0 10px rgba(217,119,87,0.6);
|
||||
}
|
||||
/* Subject wireframe: abstract design mockup */
|
||||
.subject-canvas {
|
||||
position: absolute;
|
||||
inset: 50px 36px 36px;
|
||||
}
|
||||
.wf-h1 {
|
||||
width: 62%;
|
||||
height: 18px;
|
||||
background: rgba(255,255,255,0.28);
|
||||
border-radius: 2px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.wf-h2 {
|
||||
width: 38%;
|
||||
height: 10px;
|
||||
background: rgba(255,255,255,0.14);
|
||||
border-radius: 2px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.wf-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.wf-row .bar {
|
||||
height: 8px;
|
||||
background: rgba(255,255,255,0.10);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.wf-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 14px;
|
||||
margin-top: 28px;
|
||||
}
|
||||
.wf-card {
|
||||
height: 82px;
|
||||
background: rgba(255,255,255,0.04);
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
}
|
||||
.wf-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 12px; top: 14px;
|
||||
width: 40%;
|
||||
height: 6px;
|
||||
background: rgba(255,255,255,0.22);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.wf-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 12px; bottom: 16px;
|
||||
width: 64%;
|
||||
height: 4px;
|
||||
background: rgba(255,255,255,0.10);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.wf-card.accent { border-color: rgba(217,119,87,0.55); background: rgba(217,119,87,0.06); }
|
||||
.wf-card.accent::before { background: var(--accent); }
|
||||
.wf-foot {
|
||||
position: absolute;
|
||||
left: 0; right: 0;
|
||||
bottom: 0;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.wf-chip {
|
||||
height: 22px;
|
||||
padding: 0 10px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
border-radius: 11px;
|
||||
flex: 0 0 auto;
|
||||
width: 68px;
|
||||
}
|
||||
.wf-chip.wide { width: 120px; }
|
||||
|
||||
/* ============ Light sweep ============ */
|
||||
.sweep {
|
||||
position: absolute;
|
||||
left: 130px;
|
||||
top: 250px;
|
||||
width: 680px;
|
||||
height: 140px;
|
||||
background: linear-gradient(180deg,
|
||||
rgba(217,119,87,0) 0%,
|
||||
rgba(217,119,87,0.12) 20%,
|
||||
rgba(255,220,200,0.62) 50%,
|
||||
rgba(217,119,87,0.18) 80%,
|
||||
rgba(217,119,87,0) 100%);
|
||||
filter: blur(14px);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
z-index: 4;
|
||||
mix-blend-mode: screen;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
.sweep-line {
|
||||
position: absolute;
|
||||
left: 150px;
|
||||
top: 310px;
|
||||
width: 640px;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(255,220,200,0.2) 10%,
|
||||
rgba(255,220,200,0.9) 50%,
|
||||
rgba(255,220,200,0.2) 90%,
|
||||
transparent 100%);
|
||||
filter: blur(0.6px);
|
||||
box-shadow: 0 0 14px rgba(217,119,87,0.8), 0 0 30px rgba(217,119,87,0.3);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
z-index: 6;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
/* ============ RIGHT: radar chart ============ */
|
||||
.radar-wrap {
|
||||
position: absolute;
|
||||
right: 280px;
|
||||
top: 200px;
|
||||
width: 520px;
|
||||
height: 520px;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
.radar-wrap svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
.radar-grid path {
|
||||
fill: none;
|
||||
stroke: rgba(255,255,255,0.10);
|
||||
stroke-width: 1;
|
||||
}
|
||||
.radar-spoke {
|
||||
stroke: rgba(255,255,255,0.08);
|
||||
stroke-width: 1;
|
||||
}
|
||||
.radar-poly {
|
||||
fill: rgba(217,119,87,0.16);
|
||||
stroke: var(--accent);
|
||||
stroke-width: 2;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.radar-point {
|
||||
fill: var(--accent);
|
||||
stroke: #1A1918;
|
||||
stroke-width: 2;
|
||||
}
|
||||
.radar-label {
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.2em;
|
||||
fill: var(--ink-80);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.radar-label-zh {
|
||||
font-family: var(--serif-zh);
|
||||
font-size: 22px;
|
||||
font-weight: 300;
|
||||
fill: var(--ink);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.radar-score {
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
fill: var(--accent);
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.radar-title {
|
||||
position: absolute;
|
||||
right: 280px;
|
||||
top: 160px;
|
||||
width: 520px;
|
||||
text-align: center;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.28em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.radar-score-total {
|
||||
position: absolute;
|
||||
left: 150px;
|
||||
top: 170px;
|
||||
width: 640px;
|
||||
text-align: left;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.radar-score-total .score-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 24px;
|
||||
}
|
||||
.radar-score-total .score-label {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.28em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.radar-score-total .score-num {
|
||||
font-family: var(--serif-en);
|
||||
font-size: 72px;
|
||||
font-weight: 300;
|
||||
color: var(--ink);
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1;
|
||||
}
|
||||
.radar-score-total .score-num .accent { color: var(--accent); }
|
||||
.radar-score-total .score-total {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.28em;
|
||||
color: var(--muted);
|
||||
margin-top: 8px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* ============ Single Fix row (Concept Card lean) ============ */
|
||||
.fix-lane {
|
||||
position: absolute;
|
||||
left: 150px;
|
||||
bottom: 120px;
|
||||
width: 1620px;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
.fix-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 14px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
}
|
||||
.fix-mark {
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.28em;
|
||||
color: var(--accent);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.fix-zh {
|
||||
font-family: var(--serif-zh);
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
color: var(--ink);
|
||||
}
|
||||
.fix-count {
|
||||
margin-left: auto;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.2em;
|
||||
}
|
||||
|
||||
.fix-row {
|
||||
position: relative;
|
||||
font-family: var(--sans);
|
||||
font-size: 28px;
|
||||
font-weight: 300;
|
||||
color: var(--ink);
|
||||
line-height: 1.45;
|
||||
padding: 12px 0;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
.fix-row .idx {
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.2em;
|
||||
flex: 0 0 40px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
.fix-row .mono {
|
||||
font-family: var(--mono);
|
||||
font-size: 26px;
|
||||
letter-spacing: 0;
|
||||
color: var(--accent);
|
||||
font-weight: 400;
|
||||
}
|
||||
.fix-row .arrow {
|
||||
color: var(--muted);
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.fix-severity {
|
||||
display: inline-block;
|
||||
padding: 3px 10px;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.22em;
|
||||
color: var(--accent);
|
||||
border: 1px solid rgba(217,119,87,0.5);
|
||||
border-radius: 3px;
|
||||
margin-right: 10px;
|
||||
vertical-align: 3px;
|
||||
}
|
||||
.fix-pulse {
|
||||
position: absolute;
|
||||
inset: 4px -12px 4px -12px;
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 4px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
will-change: opacity;
|
||||
box-shadow: 0 0 24px rgba(217,119,87,0.35);
|
||||
}
|
||||
|
||||
/* ============ Brand Reveal (hero-v10 signature) ============ */
|
||||
.stage-dimmer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #000000;
|
||||
opacity: 0;
|
||||
z-index: 40;
|
||||
pointer-events: none;
|
||||
will-change: opacity;
|
||||
}
|
||||
.brand-panel {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #F5F4F0;
|
||||
transform: translateY(100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 50;
|
||||
will-change: transform;
|
||||
}
|
||||
.brand-wordmark {
|
||||
font-family: var(--serif-en);
|
||||
font-size: 72px;
|
||||
font-weight: 100;
|
||||
font-variation-settings: "wght" 100;
|
||||
letter-spacing: -0.02em;
|
||||
color: #1A1918;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
will-change: opacity, transform, font-variation-settings, font-weight;
|
||||
}
|
||||
.brand-wordmark .accent { color: #D97757; font-weight: inherit; }
|
||||
.brand-line {
|
||||
margin-top: 60px;
|
||||
height: 2px;
|
||||
width: 0;
|
||||
background: #D97757;
|
||||
align-self: center;
|
||||
will-change: width;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="stage" id="stage">
|
||||
<div class="mark">HUASHU · DESIGN</div>
|
||||
<div class="mark-right">V2 · 2026</div>
|
||||
|
||||
<div class="title-line" id="titleLine">c6 · 专家评审 · 五个维度</div>
|
||||
|
||||
<div class="camera" id="camera">
|
||||
<!-- Subject: design under review -->
|
||||
<div class="subject" id="subject">
|
||||
<div class="subject-label">SUBJECT · DRAFT_V3</div>
|
||||
<div class="subject-dot"></div>
|
||||
<div class="subject-canvas">
|
||||
<div class="wf-h1"></div>
|
||||
<div class="wf-h2"></div>
|
||||
<div class="wf-row"><div class="bar" style="width:24%"></div><div class="bar" style="width:14%"></div><div class="bar" style="width:20%"></div></div>
|
||||
<div class="wf-row"><div class="bar" style="width:30%"></div><div class="bar" style="width:10%"></div></div>
|
||||
<div class="wf-grid">
|
||||
<div class="wf-card"></div>
|
||||
<div class="wf-card accent"></div>
|
||||
<div class="wf-card"></div>
|
||||
</div>
|
||||
<div class="wf-foot">
|
||||
<div class="wf-chip wide"></div>
|
||||
<div class="wf-chip"></div>
|
||||
<div class="wf-chip"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scanning light -->
|
||||
<div class="sweep" id="sweep"></div>
|
||||
<div class="sweep-line" id="sweepLine"></div>
|
||||
|
||||
<!-- Radar chart (right) -->
|
||||
<div class="radar-title" id="radarTitle">五维诊断 · RADAR</div>
|
||||
<div class="radar-wrap" id="radarWrap">
|
||||
<svg viewBox="-270 -270 540 540" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Grid rings (5 levels) -->
|
||||
<g class="radar-grid" id="radarGrid"></g>
|
||||
<!-- Spokes to 5 axes -->
|
||||
<g id="radarSpokes"></g>
|
||||
<!-- Filled polygon -->
|
||||
<polygon id="radarPoly" class="radar-poly" points="" />
|
||||
<!-- Points -->
|
||||
<g id="radarPoints"></g>
|
||||
<!-- Axis labels -->
|
||||
<g id="radarLabels"></g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="radar-score-total" id="radarTotal">
|
||||
<div class="score-row">
|
||||
<div class="score-num"><span id="scoreNum">0</span><span class="accent">/50</span></div>
|
||||
<div>
|
||||
<div class="score-label">总评 · PASSED</div>
|
||||
<div class="score-total">五维加权 · 7.4</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Single Fix row: Concept Card lean -->
|
||||
<div class="fix-lane" id="fixLane">
|
||||
<div class="fix-head">
|
||||
<span class="fix-mark">FIX</span>
|
||||
<span class="fix-zh">修复</span>
|
||||
<span class="fix-count">01 / 01</span>
|
||||
</div>
|
||||
<div class="fix-row">
|
||||
<span class="idx">01</span>
|
||||
<span><span class="fix-severity">⚡</span>字距 <span class="mono">0.02em</span><span class="arrow"> → </span><span class="mono">0.04em</span></span>
|
||||
<div class="fix-pulse" id="fixPulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Brand Reveal (hero-v10 signature) -->
|
||||
<div class="stage-dimmer" id="stageDimmer"></div>
|
||||
<div class="brand-panel" id="brandPanel">
|
||||
<div class="brand-wordmark" id="brandMark">huashu<span class="accent">-</span>design</div>
|
||||
<div class="brand-line" id="brandLine"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Auto-scale
|
||||
function fitStage() {
|
||||
const stage = document.getElementById('stage');
|
||||
const sx = window.innerWidth / 1920;
|
||||
const sy = window.innerHeight / 1080;
|
||||
const s = Math.min(sx, sy);
|
||||
stage.style.transform = `translate(-50%, -50%) scale(${s})`;
|
||||
}
|
||||
fitStage();
|
||||
window.addEventListener('resize', fitStage);
|
||||
|
||||
// Easings
|
||||
const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
|
||||
const expoIn = t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
|
||||
const cubicInOut = t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||
const cubicOut = t => 1 - Math.pow(1 - t, 3);
|
||||
|
||||
function lerp(t, a, b, easing) {
|
||||
if (t <= 0) return a;
|
||||
if (t >= 1) return b;
|
||||
const e = easing ? easing(t) : t;
|
||||
return a + (b - a) * e;
|
||||
}
|
||||
function seg(time, start, end) {
|
||||
if (time <= start) return 0;
|
||||
if (time >= end) return 1;
|
||||
return (time - start) / (end - start);
|
||||
}
|
||||
|
||||
// ============ Build radar SVG ============
|
||||
const RADIUS = 210;
|
||||
const AXES = [
|
||||
{ zh: '哲学', en: 'PHILOSOPHY', score: 8 },
|
||||
{ zh: '层级', en: 'HIERARCHY', score: 6 },
|
||||
{ zh: '执行', en: 'EXECUTION', score: 8 },
|
||||
{ zh: '功能', en: 'FUNCTION', score: 7 },
|
||||
{ zh: '创新', en: 'INNOVATION', score: 8 },
|
||||
];
|
||||
const N = AXES.length;
|
||||
|
||||
function axisPoint(i, r) {
|
||||
// Start at top (-90deg), clockwise
|
||||
const angle = -Math.PI / 2 + (2 * Math.PI * i) / N;
|
||||
return [Math.cos(angle) * r, Math.sin(angle) * r];
|
||||
}
|
||||
|
||||
// Grid rings (polygons at 5 levels)
|
||||
const gridG = document.getElementById('radarGrid');
|
||||
for (let level = 1; level <= 5; level++) {
|
||||
const r = (RADIUS * level) / 5;
|
||||
const pts = [];
|
||||
for (let i = 0; i < N; i++) {
|
||||
const [x, y] = axisPoint(i, r);
|
||||
pts.push(`${x.toFixed(2)},${y.toFixed(2)}`);
|
||||
}
|
||||
const poly = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
|
||||
poly.setAttribute('points', pts.join(' '));
|
||||
poly.setAttribute('fill', 'none');
|
||||
poly.setAttribute('stroke', level === 5 ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.07)');
|
||||
poly.setAttribute('stroke-width', '1');
|
||||
gridG.appendChild(poly);
|
||||
}
|
||||
|
||||
// Spokes
|
||||
const spokesG = document.getElementById('radarSpokes');
|
||||
for (let i = 0; i < N; i++) {
|
||||
const [x, y] = axisPoint(i, RADIUS);
|
||||
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
line.setAttribute('x1', 0);
|
||||
line.setAttribute('y1', 0);
|
||||
line.setAttribute('x2', x.toFixed(2));
|
||||
line.setAttribute('y2', y.toFixed(2));
|
||||
line.setAttribute('class', 'radar-spoke');
|
||||
spokesG.appendChild(line);
|
||||
}
|
||||
|
||||
// Labels (position outside). ZH sits at a base radial distance; EN stacks
|
||||
// below it with a fixed vertical offset to avoid overlap on the side axes.
|
||||
const labelsG = document.getElementById('radarLabels');
|
||||
AXES.forEach((axis, i) => {
|
||||
const angle = -Math.PI / 2 + (2 * Math.PI * i) / N;
|
||||
const dirX = Math.cos(angle);
|
||||
const dirY = Math.sin(angle);
|
||||
|
||||
// text-anchor based on horizontal direction
|
||||
let anchor = 'middle';
|
||||
if (dirX > 0.3) anchor = 'start';
|
||||
else if (dirX < -0.3) anchor = 'end';
|
||||
|
||||
const baseRadial = RADIUS + 36;
|
||||
const [bx, by] = axisPoint(i, baseRadial);
|
||||
|
||||
// ZH label
|
||||
const zhText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
zhText.setAttribute('x', bx.toFixed(2));
|
||||
zhText.setAttribute('y', by.toFixed(2));
|
||||
zhText.setAttribute('text-anchor', anchor);
|
||||
zhText.setAttribute('dominant-baseline', 'middle');
|
||||
zhText.setAttribute('class', 'radar-label-zh');
|
||||
zhText.textContent = axis.zh;
|
||||
labelsG.appendChild(zhText);
|
||||
|
||||
// EN label stacks vertically below ZH (always +22px in y)
|
||||
const enText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
enText.setAttribute('x', bx.toFixed(2));
|
||||
enText.setAttribute('y', (by + 22).toFixed(2));
|
||||
enText.setAttribute('text-anchor', anchor);
|
||||
enText.setAttribute('dominant-baseline', 'middle');
|
||||
enText.setAttribute('class', 'radar-label');
|
||||
enText.textContent = axis.en;
|
||||
enText.setAttribute('opacity', '0');
|
||||
enText.setAttribute('data-type', 'en-label');
|
||||
labelsG.appendChild(enText);
|
||||
});
|
||||
|
||||
// Points (initial: center)
|
||||
const pointsG = document.getElementById('radarPoints');
|
||||
const pointEls = AXES.map((axis, i) => {
|
||||
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
circle.setAttribute('cx', 0);
|
||||
circle.setAttribute('cy', 0);
|
||||
circle.setAttribute('r', 5);
|
||||
circle.setAttribute('class', 'radar-point');
|
||||
circle.setAttribute('opacity', '0');
|
||||
pointsG.appendChild(circle);
|
||||
return circle;
|
||||
});
|
||||
const radarPoly = document.getElementById('radarPoly');
|
||||
|
||||
// ============ Timeline (10s) ============
|
||||
// Beat 1 (0-2s): title + subject enters
|
||||
// Beat 2 (2-8s):
|
||||
// 2.0-3.8: light sweep top → bottom (1.8s)
|
||||
// 3.2-4.8: radar grid fades in + polygon + points grow from center
|
||||
// 4.8-5.2: score count up
|
||||
// 5.0-6.0: Keep col ripple in
|
||||
// 5.5-6.5: Fix col ripple in
|
||||
// 6.0-7.0: Quick Wins col ripple in
|
||||
// 7.0-8.0: hold
|
||||
// Beat 3 (8-10s): push-in camera to fix[0] + pulse (8-9), brand reveal (8.0-10.0)
|
||||
|
||||
const titleLine = document.getElementById('titleLine');
|
||||
const subject = document.getElementById('subject');
|
||||
const sweep = document.getElementById('sweep');
|
||||
const sweepLine = document.getElementById('sweepLine');
|
||||
const radarTitle = document.getElementById('radarTitle');
|
||||
const radarWrap = document.getElementById('radarWrap');
|
||||
const radarTotal = document.getElementById('radarTotal');
|
||||
const scoreNum = document.getElementById('scoreNum');
|
||||
const fixLane = document.getElementById('fixLane');
|
||||
const fixPulse = document.getElementById('fixPulse');
|
||||
const camera = document.getElementById('camera');
|
||||
const stageDimmer = document.getElementById('stageDimmer');
|
||||
const brandPanel = document.getElementById('brandPanel');
|
||||
const brandMark = document.getElementById('brandMark');
|
||||
const brandLine = document.getElementById('brandLine');
|
||||
|
||||
const DURATION = 10.0;
|
||||
let startTime = null;
|
||||
let loop = true;
|
||||
if (window.__recording === true) loop = false;
|
||||
|
||||
function tick(now) {
|
||||
if (startTime === null) startTime = now;
|
||||
let t = (now - startTime) / 1000;
|
||||
|
||||
if (t >= DURATION) {
|
||||
if (loop) { startTime = now; t = 0; }
|
||||
else { t = DURATION; }
|
||||
}
|
||||
|
||||
// Title fade in/out
|
||||
const titleIn = seg(t, 0.2, 1.2);
|
||||
const titleOut = seg(t, 7.6, 8.0);
|
||||
titleLine.style.opacity = Math.min(cubicOut(titleIn), 1 - titleOut);
|
||||
titleLine.style.transform = `translateX(-50%) translateY(${lerp(titleIn, -6, 0, cubicOut)}px)`;
|
||||
|
||||
// Subject appears Beat 1
|
||||
const subjectIn = seg(t, 0.4, 1.8);
|
||||
subject.style.opacity = expoOut(subjectIn);
|
||||
subject.style.transform = `translateY(${lerp(subjectIn, 14, 0, expoOut)}px)`;
|
||||
|
||||
// Subject dims after sweep completes (during Beat 2 to keep focus right)
|
||||
const subjectDim = seg(t, 4.4, 5.6);
|
||||
const dimFactor = lerp(subjectDim, 1.0, 0.38, cubicInOut);
|
||||
subject.style.filter = `saturate(${lerp(subjectDim, 1.0, 0.5, cubicInOut)}) brightness(${dimFactor})`;
|
||||
|
||||
// Light sweep: 2.0-3.8 top to bottom
|
||||
const sweepProgress = seg(t, 2.0, 3.8);
|
||||
const sweepOp = (t < 2.0 || t > 4.2) ? 0 :
|
||||
(t < 2.2 ? seg(t, 2.0, 2.2) :
|
||||
t < 3.7 ? 1 :
|
||||
1 - seg(t, 3.7, 4.2));
|
||||
sweep.style.opacity = sweepOp * 0.95;
|
||||
sweepLine.style.opacity = sweepOp * 1.0;
|
||||
// Move from y=250 to y=700 (subject top 310 to bottom 770)
|
||||
const sweepY = lerp(sweepProgress, -70, 410, cubicInOut);
|
||||
sweep.style.transform = `translateY(${sweepY}px)`;
|
||||
sweepLine.style.transform = `translateY(${sweepY + 70}px)`;
|
||||
|
||||
// Radar title + wrap appear 3.2
|
||||
const radarIn = seg(t, 3.2, 4.0);
|
||||
radarTitle.style.opacity = cubicOut(radarIn);
|
||||
radarWrap.style.opacity = cubicOut(radarIn);
|
||||
radarWrap.style.transform = `scale(${lerp(radarIn, 0.92, 1.0, expoOut)})`;
|
||||
|
||||
// Radar grid strokes already visible once wrap fades; animate grid via stroke-dasharray trick would be overkill.
|
||||
// Instead, grow polygon + points from center (3.6-4.8)
|
||||
const polyGrow = seg(t, 3.6, 4.8);
|
||||
const polyT = expoOut(polyGrow);
|
||||
const polyPts = [];
|
||||
AXES.forEach((axis, i) => {
|
||||
const targetR = (axis.score / 10) * RADIUS;
|
||||
const r = targetR * polyT;
|
||||
const [x, y] = axisPoint(i, r);
|
||||
polyPts.push(`${x.toFixed(2)},${y.toFixed(2)}`);
|
||||
const pt = pointEls[i];
|
||||
pt.setAttribute('cx', x.toFixed(2));
|
||||
pt.setAttribute('cy', y.toFixed(2));
|
||||
pt.setAttribute('opacity', polyT.toFixed(2));
|
||||
});
|
||||
radarPoly.setAttribute('points', polyPts.join(' '));
|
||||
|
||||
// EN labels fade in slightly later
|
||||
const enLabelIn = seg(t, 4.2, 4.8);
|
||||
document.querySelectorAll('[data-type="en-label"]').forEach(el => {
|
||||
el.setAttribute('opacity', cubicOut(enLabelIn).toFixed(2));
|
||||
});
|
||||
|
||||
// Score count up 4.6-5.4, target total = 37
|
||||
const scoreT = seg(t, 4.6, 5.4);
|
||||
const total = AXES.reduce((s, a) => s + a.score, 0); // 37
|
||||
const shown = Math.round(lerp(scoreT, 0, total, cubicOut));
|
||||
scoreNum.textContent = shown;
|
||||
radarTotal.style.opacity = cubicOut(seg(t, 4.4, 5.0));
|
||||
|
||||
// Fix lane ripple in (5.3-6.1)
|
||||
const fixRip = seg(t, 5.3, 6.1);
|
||||
fixLane.style.opacity = expoOut(fixRip);
|
||||
fixLane.style.transform = `translateY(${lerp(fixRip, 24, 0, expoOut)}px)`;
|
||||
|
||||
// Beat 3: Push-in camera to Fix row + pulse (7.4-8.0)
|
||||
const pushT = seg(t, 7.4, 8.0);
|
||||
const scale = lerp(pushT, 1.0, 1.18, cubicInOut);
|
||||
camera.style.transform = `scale(${scale})`;
|
||||
|
||||
// Fix pulse border: blink 2 times between 7.6-8.0
|
||||
const pulseOp = t < 7.6 ? 0 :
|
||||
t < 8.0 ? (0.4 + 0.6 * Math.abs(Math.sin((t - 7.6) * Math.PI * 2.4))) :
|
||||
0;
|
||||
fixPulse.style.opacity = pulseOp;
|
||||
|
||||
// ============ Brand Reveal (hero-v10 signature, aligned) ============
|
||||
// [T-2.0 → T-1.7s] i.e. 8.0-8.3: scene fade to black (0.3s)
|
||||
const soK = seg(t, 8.0, 8.3);
|
||||
stageDimmer.style.opacity = cubicOut(soK);
|
||||
const sceneFade = seg(t, 8.0, 8.3);
|
||||
camera.style.opacity = 1 - cubicOut(sceneFade);
|
||||
|
||||
// [T-1.7 → T-1.3s] i.e. 8.3-8.7: cream panel slides from bottom (0.4s, expoOut)
|
||||
const panelT = seg(t, 8.3, 8.7);
|
||||
const panelY = lerp(panelT, 100, 0, expoOut);
|
||||
brandPanel.style.transform = `translateY(${panelY}%)`;
|
||||
|
||||
// [T-1.3 → T-0.7s] i.e. 8.7-9.3: wordmark wght 100→500 + y 20→0 + opacity 0→1 (0.6s)
|
||||
const markT = seg(t, 8.7, 9.3);
|
||||
const markE = expoOut(markT);
|
||||
const wght = 100 + (500 - 100) * markE;
|
||||
brandMark.style.opacity = markE;
|
||||
brandMark.style.transform = `translateY(${20 * (1 - markE)}px)`;
|
||||
brandMark.style.fontWeight = Math.round(wght);
|
||||
brandMark.style.fontVariationSettings = `"wght" ${wght.toFixed(0)}`;
|
||||
|
||||
// [T-0.7 → T-0.3s] i.e. 9.3-9.7: orange line width 0→280 (0.4s, cubicOut)
|
||||
const lineT = seg(t, 9.3, 9.7);
|
||||
brandLine.style.width = `${lerp(lineT, 0, 280, cubicOut)}px`;
|
||||
|
||||
// [T-0.3 → T] hold
|
||||
|
||||
if (!window.__ready) window.__ready = true;
|
||||
|
||||
if (loop || t < DURATION) requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
(document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve())
|
||||
.then(() => requestAnimationFrame(tick));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1498
.claude/skills/huashu-design/demos/hero-animation-v10-en.html
Normal file
1498
.claude/skills/huashu-design/demos/hero-animation-v10-en.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,615 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>md还是html,这是个蠢问题 · 解说 demo (v3 · 字幕+持续运动+修溢出)</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/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=Source+Serif+4:wght@300;400;600;700;800&family=Noto+Serif+SC:wght@400;600;700;900&family=JetBrains+Mono:wght@400;500;700&family=Noto+Sans+SC:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { margin: 0; background: #0a0a0a; min-height: 100vh; display: flex; align-items: center; justify-content: center; flex-direction: column; padding: 20px; box-sizing: border-box; font-family: -apple-system, sans-serif; }
|
||||
#root { box-shadow: 0 30px 80px rgba(0,0,0,0.6); border-radius: 4px; overflow: hidden; }
|
||||
* { box-sizing: border-box; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
<script type="text/babel">
|
||||
// ── timeline.json (inline · 精简版,每段含 chunks 用于字幕) ───
|
||||
const TIMELINE = {"title":"md还是html,这是个蠢问题","totalDuration":198.168,"voiceover":"voiceover.mp3","scenes":[
|
||||
{"id":"opening","start":0,"end":22.32,"duration":22.32,"chunks":[
|
||||
{"text":"前两天,","absoluteStart":0,"absoluteEnd":0.984},
|
||||
{"text":"Claude Code 团队的 Thariq 发了篇爆文。标题就一句话,HTML 是新的 markdown。","absoluteStart":0.984,"absoluteEnd":8.5},
|
||||
{"text":"他说他几乎不再写 md 文件了,全让 AI 给他生成 HTML。500 万阅读,X 上立马吵翻了。","absoluteStart":8.5,"absoluteEnd":14.952},
|
||||
{"text":"一派是 md 党,觉得 md 才是 AI 时代的源代码。另一派觉得 HTML 才是终极答案。","absoluteStart":14.952,"absoluteEnd":22.32}
|
||||
],"cues":[{"id":"thariq","absoluteTime":0.984},{"id":"two-camps","absoluteTime":14.952}]},
|
||||
{"id":"md-side","start":22.82,"end":56.516,"duration":33.696,"chunks":[
|
||||
{"text":"md 党的证据其实挺硬的。","absoluteStart":22.82,"absoluteEnd":26.5},
|
||||
{"text":"OpenAI 去年发的 AGENTS.md,60000 多个项目用,","absoluteStart":26.5,"absoluteEnd":31.5},
|
||||
{"text":"AWS、Anthropic、Google、微软、OpenAI,AI 半壁江山一起捐进 Linux Foundation。","absoluteStart":31.5,"absoluteEnd":38.5},
|
||||
{"text":"Karpathy 的 llm-wiki,单一个 CLAUDE.md 文件,5 万 star。","absoluteStart":38.5,"absoluteEnd":45.14},
|
||||
{"text":"Cloudflare 实测,同一篇博客 HTML 一万六千 token,转成 md 只要三千。省 80%。","absoluteStart":45.14,"absoluteEnd":54.764},
|
||||
{"text":"GitHub 官方说:文档不再是描述代码,文档就是代码。","absoluteStart":54.764,"absoluteEnd":56.516}
|
||||
],"cues":[{"id":"agents-md","absoluteTime":27.5},{"id":"token-saving","absoluteTime":45.14},{"id":"doc-is-code","absoluteTime":54.764}]},
|
||||
{"id":"html-side","start":57.016,"end":100.168,"duration":43.152,"chunks":[
|
||||
{"text":"但 html 党也没说错。Thariq 的论据我都同意。","absoluteStart":57.016,"absoluteEnd":62.92},
|
||||
{"text":"第一是空间信息。diff、调用图、架构图本来就有空间维度,html 能左右对照。","absoluteStart":62.92,"absoluteEnd":74.632},
|
||||
{"text":"第二是动态体验。按钮颜色、easing 曲线,文字描述再多没用,html 能让你直接看见。","absoluteStart":74.632,"absoluteEnd":85.864},
|
||||
{"text":"第三是结构化阅读。可折叠章节、tab 代码块、边栏术语表。","absoluteStart":85.864,"absoluteEnd":93},
|
||||
{"text":"Anthropic 的 Live Artifacts,HTML 已升级为可交互、能拉实时数据的 dashboard。","absoluteStart":93,"absoluteEnd":100.168}
|
||||
],"cues":[{"id":"spatial","absoluteTime":62.92},{"id":"dynamic","absoluteTime":74.632},{"id":"structured","absoluteTime":85.864}]},
|
||||
{"id":"the-real-question","start":100.668,"end":117.588,"duration":16.92,"chunks":[
|
||||
{"text":"我看完想说,","absoluteStart":100.668,"absoluteEnd":101.748},
|
||||
{"text":"这俩根本是在争一个蠢问题。","absoluteStart":101.748,"absoluteEnd":106},
|
||||
{"text":"两边都赢了。但赢的是不同的问题。","absoluteStart":106,"absoluteEnd":109.044},
|
||||
{"text":"md 党回答:我们用什么写。","absoluteStart":109.044,"absoluteEnd":112.62},
|
||||
{"text":"html 党回答:我们给人什么看。","absoluteStart":112.62,"absoluteEnd":115.5},
|
||||
{"text":"两个不同问题,怎么会有谁取代谁。","absoluteStart":115.5,"absoluteEnd":117.588}
|
||||
],"cues":[{"id":"reveal","absoluteTime":101.748},{"id":"question-md","absoluteTime":109.044},{"id":"question-html","absoluteTime":112.62}]},
|
||||
{"id":"the-split","start":118.088,"end":158.744,"duration":40.656,"chunks":[
|
||||
{"text":"我觉得真问题是这个。","absoluteStart":118.088,"absoluteEnd":121},
|
||||
{"text":"md 和 html 不是替代,是分工关系。","absoluteStart":121,"absoluteEnd":126.5},
|
||||
{"text":"以前你写 md 自己也看 md,要折中,所以 md 胜出。","absoluteStart":126.5,"absoluteEnd":131},
|
||||
{"text":"AI 出现后,生产成本被 AI 吸收。","absoluteStart":131,"absoluteEnd":135},
|
||||
{"text":"原来要折中的需求,被拆成了两端的极端最优。","absoluteStart":135,"absoluteEnd":140},
|
||||
{"text":"生产端要轻、要快、要 token efficient——那就是 md。","absoluteStart":140,"absoluteEnd":148.28},
|
||||
{"text":"消费端要丰富、要可视化、要好分享——那就是 html。","absoluteStart":148.28,"absoluteEnd":153.464},
|
||||
{"text":"两端各自登顶,中间那个折中位置,没人需要了。","absoluteStart":153.464,"absoluteEnd":158.744}
|
||||
],"cues":[{"id":"split","absoluteTime":122.84},{"id":"ai-changes","absoluteTime":131},{"id":"md-side-win","absoluteTime":148.28},{"id":"html-side-win","absoluteTime":153.464}]},
|
||||
{"id":"activity-proof","start":159.244,"end":184.084,"duration":24.84,"chunks":[
|
||||
{"text":"最干净的活样本是 Thariq 自己。","absoluteStart":159.244,"absoluteEnd":162.5},
|
||||
{"text":"3 月份他发《Skills 指南》,强调核心还是 markdown。","absoluteStart":162.5,"absoluteEnd":167},
|
||||
{"text":"5 月份他发《HTML is the new markdown》。","absoluteStart":167,"absoluteEnd":169.372},
|
||||
{"text":"同一个人,两端各自登顶,互不打架。","absoluteStart":169.372,"absoluteEnd":174},
|
||||
{"text":"Karpathy 和 Lex Fridman 那对组合也一样。","absoluteStart":174,"absoluteEnd":177},
|
||||
{"text":"内核是 markdown wiki,外壳是动态 HTML——是加了一层消费层。","absoluteStart":177,"absoluteEnd":184.084}
|
||||
],"cues":[{"id":"thariq-march","absoluteTime":164.236},{"id":"same-person","absoluteTime":169.372},{"id":"karpathy-lex","absoluteTime":176.764}]},
|
||||
{"id":"closing","start":184.584,"end":197.88,"duration":13.296,"chunks":[
|
||||
{"text":"所以下次你想吵这个的时候,","absoluteStart":184.584,"absoluteEnd":186.672},
|
||||
{"text":"先问自己一句——你面对的是「写」,还是「看」?","absoluteStart":186.672,"absoluteEnd":192},
|
||||
{"text":"写,用 md。","absoluteStart":192,"absoluteEnd":193.704},
|
||||
{"text":"看,用 html。","absoluteStart":193.704,"absoluteEnd":195.5},
|
||||
{"text":"工具替你处理切换,立场可以放下了。","absoluteStart":195.5,"absoluteEnd":197.88}
|
||||
],"cues":[{"id":"final","absoluteTime":186.672},{"id":"md-final","absoluteTime":192},{"id":"html-final","absoluteTime":193.704}]}
|
||||
]};
|
||||
|
||||
// ── narration_stage.jsx (inline) ─────────────────────────────
|
||||
const NarrationStageLib = (() => {
|
||||
const NarrationContext = React.createContext({});
|
||||
function NarrationStage({ timeline, audioSrc, width = 1920, height = 1080, background = '#0e0e0e', controls = true, children }) {
|
||||
const audioRef = React.useRef(null);
|
||||
const [time, setTime] = React.useState(0);
|
||||
const [playing, setPlaying] = React.useState(false);
|
||||
const recording = typeof window !== 'undefined' && window.__recording === true;
|
||||
React.useEffect(() => { if (typeof window !== 'undefined') { window.__totalDuration = timeline.totalDuration; window.__ready = true; } }, [timeline.totalDuration]);
|
||||
React.useEffect(() => {
|
||||
let raf;
|
||||
if (recording) {
|
||||
let startedAt = null;
|
||||
const tick = (now) => {
|
||||
if (startedAt === null) startedAt = now;
|
||||
setTime(Math.min((now - startedAt) / 1000, timeline.totalDuration));
|
||||
raf = requestAnimationFrame(tick);
|
||||
};
|
||||
raf = requestAnimationFrame(tick);
|
||||
if (typeof window !== 'undefined') window.__seek = (t) => { startedAt = performance.now() - t * 1000; setTime(t); };
|
||||
} else {
|
||||
const tick = () => {
|
||||
if (audioRef.current && !audioRef.current.paused) setTime(audioRef.current.currentTime);
|
||||
raf = requestAnimationFrame(tick);
|
||||
};
|
||||
tick();
|
||||
}
|
||||
return () => cancelAnimationFrame(raf);
|
||||
}, [recording, timeline.totalDuration]);
|
||||
const currentScene = React.useMemo(() => {
|
||||
if (!timeline.scenes) return null;
|
||||
for (let i = 0; i < timeline.scenes.length; i++) {
|
||||
const s = timeline.scenes[i]; const next = timeline.scenes[i + 1];
|
||||
if (time >= s.start && (!next || time < next.start)) return s;
|
||||
}
|
||||
return timeline.scenes[0];
|
||||
}, [time, timeline.scenes]);
|
||||
const sceneTime = currentScene ? Math.max(0, time - currentScene.start) : 0;
|
||||
const allCues = React.useMemo(() => { const m = {}; for (const s of timeline.scenes || []) for (const c of s.cues || []) m[c.id] = c; return m; }, [timeline.scenes]);
|
||||
const isCueTriggered = React.useCallback(id => { const c = allCues[id]; return c ? time >= c.absoluteTime : false; }, [allCues, time]);
|
||||
const cueProgress = React.useCallback((id, ramp = 0.6) => { const c = allCues[id]; if (!c) return 0; const dt = time - c.absoluteTime; if (dt <= 0) return 0; if (dt >= ramp) return 1; return dt / ramp; }, [allCues, time]);
|
||||
const ctx = { time, scene: currentScene, sceneTime, isCueTriggered, cueProgress, timeline };
|
||||
return (
|
||||
<NarrationContext.Provider value={ctx}>
|
||||
<div style={{ position: 'relative', width, height, background, overflow: 'hidden', color: '#1a1a1a' }}>{children}</div>
|
||||
{!recording && <audio ref={audioRef} src={audioSrc} preload="auto" onEnded={() => setPlaying(false)} />}
|
||||
{!recording && controls && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 16px', background: '#1a1a1a', color: '#ddd', fontFamily: 'monospace', fontSize: 13, width, boxSizing: 'border-box' }}>
|
||||
<button onClick={() => { if (audioRef.current.paused) { audioRef.current.play(); setPlaying(true); } else { audioRef.current.pause(); setPlaying(false); } }} style={{ padding: '6px 14px', background: '#fff', color: '#000', border: 0, borderRadius: 4, cursor: 'pointer', fontWeight: 600 }}>{playing ? '❚❚ Pause' : '▶ Play'}</button>
|
||||
<input type="range" min={0} max={timeline.totalDuration} step={0.01} value={time} onChange={e => { const t = parseFloat(e.target.value); audioRef.current.currentTime = t; setTime(t); }} style={{ flex: 1 }} />
|
||||
<span style={{ minWidth: 130, textAlign: 'right' }}>{time.toFixed(2)} / {timeline.totalDuration.toFixed(2)}s</span>
|
||||
<span style={{ padding: '4px 10px', background: '#2a2a2a', borderRadius: 4, minWidth: 130, textAlign: 'center' }}>{currentScene ? currentScene.id : '—'}</span>
|
||||
</div>
|
||||
)}
|
||||
</NarrationContext.Provider>
|
||||
);
|
||||
}
|
||||
function useNarration() { return React.useContext(NarrationContext); }
|
||||
function useSceneFade(sceneId, fadeIn = 0.5, fadeOut = 0.5) {
|
||||
const { time, timeline } = React.useContext(NarrationContext);
|
||||
if (!timeline) return 0;
|
||||
const s = timeline.scenes.find(x => x.id === sceneId);
|
||||
if (!s) return 0;
|
||||
const inT = (time - s.start) / fadeIn;
|
||||
const outT = (s.end - time) / fadeOut;
|
||||
return Math.max(0, Math.min(1, Math.min(inT, outT)));
|
||||
}
|
||||
function Cue({ id, ramp = 0.6, children }) {
|
||||
const { isCueTriggered, cueProgress } = React.useContext(NarrationContext);
|
||||
return children(isCueTriggered(id), cueProgress(id, ramp));
|
||||
}
|
||||
return { NarrationStage, Cue, useNarration, useSceneFade };
|
||||
})();
|
||||
const { NarrationStage, Cue, useNarration, useSceneFade } = NarrationStageLib;
|
||||
|
||||
// ── 设计 token ────────────────────────────────────────────
|
||||
const C = {
|
||||
paper: '#f5f1e8', paperDeep: '#ebe5d4',
|
||||
ink: '#1a1a1a', inkSoft: '#3a3a3a', inkMute: '#888',
|
||||
md: '#1B4965', html: '#C04A1A', green: '#7BC47F',
|
||||
};
|
||||
const F = {
|
||||
display: '"Source Serif 4", "Noto Serif SC", Georgia, serif',
|
||||
body: '"Noto Sans SC", "Noto Serif SC", "Source Serif 4", sans-serif',
|
||||
mono: '"JetBrains Mono", Menlo, monospace',
|
||||
};
|
||||
|
||||
// ── easing & interpolate ──────────────────────────────────
|
||||
const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
|
||||
const lerp = (a, b, t) => a + (b - a) * t;
|
||||
const lerpC = (from, to, t) => ({
|
||||
x: lerp(from.x, to.x, t), y: lerp(from.y, to.y, t),
|
||||
scale: lerp(from.scale, to.scale, t),
|
||||
opacity: lerp(from.opacity ?? 1, to.opacity ?? 1, t),
|
||||
});
|
||||
|
||||
// ── HERO 状态表(v3:缩小 scale 避免溢出,y 留给字幕区) ──
|
||||
// 字幕条占 y=88-100 区域,所以 hero y ≤ 70%
|
||||
const HERO_KEYS = {
|
||||
opening: { md: { x: 50, y: 28, scale: 1.0, opacity: 1 }, html: { x: 50, y: 55, scale: 1.0, opacity: 1 } },
|
||||
'md-side': { md: { x: 72, y: 48, scale: 1.4, opacity: 1 }, html: { x: 92, y: 12, scale: 0.3, opacity: 0.5 } },
|
||||
'html-side': { md: { x: 8, y: 12, scale: 0.3, opacity: 0.5 }, html: { x: 28, y: 48, scale: 1.4, opacity: 1 } },
|
||||
'the-real-question': { md: { x: 30, y: 30, scale: 0.85, opacity: 1 }, html: { x: 70, y: 30, scale: 0.85, opacity: 1 } },
|
||||
'the-split': { md: { x: 22, y: 60, scale: 1.15, opacity: 1 }, html: { x: 78, y: 60, scale: 1.15, opacity: 1 } },
|
||||
'activity-proof': { md: { x: 18, y: 18, scale: 0.5, opacity: 1 }, html: { x: 82, y: 18, scale: 0.5, opacity: 1 } },
|
||||
closing: { md: { x: 28, y: 50, scale: 1.3, opacity: 1 }, html: { x: 72, y: 50, scale: 1.3, opacity: 1 } },
|
||||
};
|
||||
const SCENE_ORDER = ['opening', 'md-side', 'html-side', 'the-real-question', 'the-split', 'activity-proof', 'closing'];
|
||||
|
||||
// ── HeroAnchor: 跨 scene hero + 持续微动(消除 3s 静止)──
|
||||
const HeroAnchor = () => {
|
||||
const { time, scene } = useNarration();
|
||||
if (!scene) return null;
|
||||
const idx = SCENE_ORDER.indexOf(scene.id);
|
||||
const prevId = idx > 0 ? SCENE_ORDER[idx - 1] : scene.id;
|
||||
const fromPos = HERO_KEYS[prevId];
|
||||
const toPos = HERO_KEYS[scene.id];
|
||||
const transitionDur = Math.min(2.0, scene.duration * 0.45);
|
||||
const t = expoOut(Math.min(1, Math.max(0, (time - scene.start) / transitionDur)));
|
||||
const md = lerpC(fromPos.md, toPos.md, t);
|
||||
const html = lerpC(fromPos.html, toPos.html, t);
|
||||
|
||||
// ── 持续微动:scale 呼吸 + figure-8 飘移(确保任意 3s 都有变化)──
|
||||
const breath = 1 + Math.sin(time * 0.7) * 0.018;
|
||||
const driftXm = Math.cos(time * 0.32) * 0.6;
|
||||
const driftYm = Math.sin(time * 0.41) * 0.5;
|
||||
const driftXh = Math.sin(time * 0.28) * 0.6;
|
||||
const driftYh = Math.cos(time * 0.37) * 0.5;
|
||||
|
||||
const baseSize = 240; // 缩小 from 360
|
||||
const renderHero = (label, pos, color, dx, dy) => {
|
||||
const px = (pos.x + dx) * 19.2;
|
||||
const py = (pos.y + dy) * 10.8;
|
||||
return (
|
||||
<div key={label} style={{
|
||||
position: 'absolute', left: px, top: py,
|
||||
transform: `translate(-50%, -50%) scale(${pos.scale * breath})`,
|
||||
opacity: pos.opacity,
|
||||
fontSize: baseSize, fontFamily: F.display, fontWeight: 800,
|
||||
color, lineHeight: 1, letterSpacing: '-0.02em',
|
||||
willChange: 'transform, opacity', pointerEvents: 'none',
|
||||
}}>{label}</div>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div style={{ position: 'absolute', inset: 0, perspective: '2400px' }}>
|
||||
<div style={{ position: 'absolute', inset: 0, transformStyle: 'preserve-3d', transform: 'rotateX(2deg) rotateY(-1deg)' }}>
|
||||
{renderHero('md', md, C.md, driftXm, driftYm)}
|
||||
{renderHero('html', html, C.html, driftXh, driftYh)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ── BackgroundDrift ────────────────────────────────────────
|
||||
const BackgroundDrift = () => {
|
||||
const { time } = useNarration();
|
||||
const dx = Math.sin(time * 0.08) * 16;
|
||||
const dy = Math.cos(time * 0.06) * 12;
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute', inset: -40,
|
||||
background: `radial-gradient(ellipse 1400px 800px at ${50 + dx/4}% ${50 + dy/4}%, ${C.paperDeep} 0%, ${C.paper} 60%, ${C.paper} 100%)`,
|
||||
pointerEvents: 'none',
|
||||
}} />
|
||||
);
|
||||
};
|
||||
|
||||
// ── Subtitles: B 站风字幕(白字 + 黑描边,无背景,每行 ≤12 字不截断句子)──
|
||||
// 把每个 chunk 按标点切成短行,按字数比例分配 chunk 时间窗显示
|
||||
|
||||
// 切分算法:先按强标点(。!?\n)切句,每句再按弱标点(,、;:)合并到 maxLen
|
||||
// 中英混合:英文字母按 0.5 字算(视觉宽度近似)
|
||||
function visualLen(s) {
|
||||
let n = 0;
|
||||
for (const ch of s) n += /[a-zA-Z0-9 .,'":;\-]/.test(ch) ? 0.5 : 1;
|
||||
return n;
|
||||
}
|
||||
function splitChunkToLines(text, maxLen = 13) {
|
||||
const lines = [];
|
||||
// 1. 按强标点切句(保留标点)
|
||||
const sentences = [];
|
||||
let buf = '';
|
||||
for (const ch of text) {
|
||||
buf += ch;
|
||||
if ('。!?\n'.includes(ch)) {
|
||||
if (buf.trim()) sentences.push(buf.trim());
|
||||
buf = '';
|
||||
}
|
||||
}
|
||||
if (buf.trim()) sentences.push(buf.trim());
|
||||
|
||||
// 2. 每句按弱标点切并合并到 maxLen 以内(不跨句号边界)
|
||||
for (const sent of sentences) {
|
||||
if (visualLen(sent) <= maxLen) { lines.push(sent); continue; }
|
||||
// 按弱标点切(保留标点跟前段)
|
||||
const parts = [];
|
||||
let pbuf = '';
|
||||
for (const ch of sent) {
|
||||
pbuf += ch;
|
||||
if (',、;:'.includes(ch)) { parts.push(pbuf); pbuf = ''; }
|
||||
}
|
||||
if (pbuf) parts.push(pbuf);
|
||||
// 合并到 maxLen
|
||||
let merged = '';
|
||||
for (const p of parts) {
|
||||
if (visualLen(merged) + visualLen(p) <= maxLen) merged += p;
|
||||
else { if (merged) lines.push(merged); merged = p; }
|
||||
}
|
||||
if (merged) {
|
||||
if (visualLen(merged) <= maxLen) lines.push(merged);
|
||||
else {
|
||||
// 兜底硬切(罕见:单个标点段超 maxLen)
|
||||
let hbuf = '';
|
||||
for (const ch of merged) {
|
||||
hbuf += ch;
|
||||
if (visualLen(hbuf) >= maxLen) { lines.push(hbuf); hbuf = ''; }
|
||||
}
|
||||
if (hbuf) lines.push(hbuf);
|
||||
}
|
||||
}
|
||||
}
|
||||
return lines.filter(l => l.trim());
|
||||
}
|
||||
|
||||
const Subtitles = () => {
|
||||
const { time, scene } = useNarration();
|
||||
if (!scene || !scene.chunks) return null;
|
||||
const active = scene.chunks.find(c => time >= c.absoluteStart && time < c.absoluteEnd);
|
||||
if (!active) return null;
|
||||
const lines = splitChunkToLines(active.text);
|
||||
if (lines.length === 0) return null;
|
||||
// 按字数比例把 chunk 时长分配给每行
|
||||
const totalLen = lines.reduce((s, l) => s + visualLen(l), 0);
|
||||
const chunkDur = active.absoluteEnd - active.absoluteStart;
|
||||
let acc = active.absoluteStart;
|
||||
let activeLine = lines[lines.length - 1];
|
||||
let lineStart = active.absoluteStart;
|
||||
for (const line of lines) {
|
||||
const dur = (visualLen(line) / totalLen) * chunkDur;
|
||||
if (time < acc + dur) { activeLine = line; lineStart = acc; break; }
|
||||
acc += dur;
|
||||
}
|
||||
// 行内淡入 0.15s
|
||||
const lineProg = Math.min(1, (time - lineStart) / 0.15);
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute', left: 0, right: 0, bottom: 90,
|
||||
display: 'flex', justifyContent: 'center', pointerEvents: 'none', zIndex: 50,
|
||||
}}>
|
||||
<div key={lineStart} style={{
|
||||
fontFamily: '"PingFang SC", "Noto Sans SC", -apple-system, sans-serif',
|
||||
fontSize: 32, fontWeight: 600, color: C.ink,
|
||||
letterSpacing: '0.04em', lineHeight: 1.2, textAlign: 'center',
|
||||
// 浅纸白背景上:深墨字 + 极细白色光晕,让字在底上跳出来又不重
|
||||
textShadow: '0 0 6px rgba(245,241,232,0.9), 0 0 12px rgba(245,241,232,0.7), 0 1px 2px rgba(255,255,255,0.5)',
|
||||
opacity: lineProg, transform: `translateY(${(1 - lineProg) * 4}px)`,
|
||||
}}>
|
||||
{activeLine}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ── 段标签 ─────────────────────────────────────────────
|
||||
const SceneLabel = ({ sceneId, text }) => {
|
||||
const op = useSceneFade(sceneId, 0.4, 0.4);
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute', top: 56, left: 80, fontFamily: F.mono, fontSize: 14,
|
||||
color: C.inkMute, letterSpacing: '0.22em', textTransform: 'uppercase', opacity: op,
|
||||
}}>{text}</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ── 各 scene 辅助元素 ──────────────────────────────────
|
||||
const OpeningAux = () => {
|
||||
const op = useSceneFade('opening', 0.6, 1.0);
|
||||
return (
|
||||
<>
|
||||
<Cue id="thariq">{(t, p) => (
|
||||
<div style={{ position: 'absolute', top: 110, left: 100, opacity: op * p, transform: `translateY(${(1-p)*20}px)`, maxWidth: 700 }}>
|
||||
<div style={{ fontFamily: F.mono, fontSize: 14, color: C.inkMute, marginBottom: 10, letterSpacing: '0.12em' }}>2026.05.07 · @THARIQ · CLAUDE CODE</div>
|
||||
<div style={{ fontSize: 56, fontFamily: F.display, fontWeight: 700, lineHeight: 1.05, color: C.ink, fontStyle: 'italic' }}>
|
||||
HTML is the new<br/>markdown.
|
||||
</div>
|
||||
</div>
|
||||
)}</Cue>
|
||||
<Cue id="two-camps">{(t, p) => t && (
|
||||
<div style={{ position: 'absolute', top: 110, right: 100, opacity: op * p, transform: `translateY(${(1-p)*16}px)`, fontFamily: F.mono, fontSize: 18, color: C.inkSoft, textAlign: 'right' }}>
|
||||
<div style={{ fontSize: 38, fontWeight: 700, color: C.ink, letterSpacing: '-0.02em' }}>5,000,000</div>
|
||||
<div style={{ fontSize: 13, color: C.inkMute, letterSpacing: '0.18em', marginTop: 4 }}>阅读 · < 24H</div>
|
||||
</div>
|
||||
)}</Cue>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const MdSideAux = () => {
|
||||
const op = useSceneFade('md-side', 0.6, 0.8);
|
||||
return (
|
||||
<>
|
||||
<Cue id="agents-md">{(t, p) => (
|
||||
<div style={{ position: 'absolute', left: 80, top: 200, opacity: op * p, transform: `translateY(${(1-p)*16}px)` }}>
|
||||
<div style={{ fontFamily: F.mono, fontSize: 13, color: C.inkMute, marginBottom: 6, letterSpacing: '0.12em' }}>AGENTS.md · OpenAI 2025</div>
|
||||
<div style={{ fontSize: 76, fontFamily: F.display, fontWeight: 700, color: C.ink, lineHeight: 0.95 }}>60,000<span style={{ color: C.html }}>+</span></div>
|
||||
<div style={{ fontSize: 18, color: C.inkSoft, marginTop: 4, fontFamily: F.body }}>开源项目采用</div>
|
||||
<div style={{ marginTop: 14, fontFamily: F.mono, fontSize: 12, color: C.inkMute, letterSpacing: '0.1em' }}>AWS · ANTHROPIC · GOOGLE · MICROSOFT · OPENAI</div>
|
||||
</div>
|
||||
)}</Cue>
|
||||
<Cue id="agents-md">{(t, p) => (
|
||||
<div style={{ position: 'absolute', left: 80, top: 460, opacity: op * Math.max(0, p - 0.25) * 1.33, transform: `translateY(${(1-p)*16}px)` }}>
|
||||
<div style={{ fontFamily: F.mono, fontSize: 13, color: C.inkMute, marginBottom: 4, letterSpacing: '0.12em' }}>karpathy/llm-wiki · CLAUDE.md</div>
|
||||
<div style={{ fontSize: 64, fontFamily: F.display, fontWeight: 700, color: C.ink, lineHeight: 0.95 }}>50,000<span style={{ color: C.html }}>★</span></div>
|
||||
</div>
|
||||
)}</Cue>
|
||||
<Cue id="token-saving">{(t, p) => t && (
|
||||
<div style={{ position: 'absolute', left: 80, top: 640, opacity: op * p, transform: `translateY(${(1-p)*14}px)`, padding: '28px 36px', background: C.ink, color: C.paper, minWidth: 540, fontFamily: F.mono }}>
|
||||
<div style={{ fontSize: 11, color: '#999', letterSpacing: '0.2em', marginBottom: 14 }}>CLOUDFLARE 实测 · 同一篇博客</div>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 20, marginBottom: 14 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 11, color: '#777', marginBottom: 2 }}>HTML</div>
|
||||
<div style={{ fontSize: 50, fontWeight: 700, color: C.html, lineHeight: 1 }}>16,180</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 32, color: '#555' }}>→</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 11, color: '#777', marginBottom: 2 }}>md</div>
|
||||
<div style={{ fontSize: 50, fontWeight: 700, color: C.green, lineHeight: 1 }}>3,150</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 70, fontFamily: F.display, fontWeight: 700, color: C.html, lineHeight: 0.95, fontStyle: 'italic' }}>−80% token</div>
|
||||
</div>
|
||||
)}</Cue>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const HtmlSideAux = () => {
|
||||
const op = useSceneFade('html-side', 0.6, 0.8);
|
||||
const items = [
|
||||
{ cue: 'spatial', label: '空间信息', desc: 'diff · 调用图 · 架构图', md: '一行字', html: '左右对照', topPx: 220 },
|
||||
{ cue: 'dynamic', label: '动态体验', desc: '按钮 · easing · 动效', md: '文字描述', html: '直接看见', topPx: 410 },
|
||||
{ cue: 'structured', label: '结构化阅读', desc: '可折叠 · tab · 边栏', md: '线性堆字', html: '真的会读', topPx: 600 },
|
||||
];
|
||||
return (
|
||||
<>
|
||||
{items.map((it, i) => (
|
||||
<Cue key={it.cue} id={it.cue}>{(t, p) => (
|
||||
<div style={{ position: 'absolute', right: 80, top: it.topPx, opacity: op * p, transform: `translateX(${(1-p)*40}px)`, display: 'flex', alignItems: 'baseline', gap: 22, justifyContent: 'flex-end' }}>
|
||||
<div style={{ fontFamily: F.mono, fontSize: 16, color: C.html, fontWeight: 700, letterSpacing: '0.18em' }}>0{i+1}</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ fontSize: 32, fontFamily: F.display, fontWeight: 600, color: C.ink }}>{it.label}</div>
|
||||
<div style={{ fontSize: 16, color: C.inkMute, fontFamily: F.mono, marginTop: 3 }}>{it.desc}</div>
|
||||
<div style={{ marginTop: 10, display: 'flex', alignItems: 'baseline', gap: 12, justifyContent: 'flex-end', fontFamily: F.body }}>
|
||||
<span style={{ fontSize: 19, color: C.inkMute, textDecoration: 'line-through' }}>md: {it.md}</span>
|
||||
<span style={{ fontSize: 16, color: C.html }}>→</span>
|
||||
<span style={{ fontSize: 19, color: C.html, fontWeight: 600 }}>html: {it.html}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}</Cue>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const RealQuestionAux = () => {
|
||||
const op = useSceneFade('the-real-question', 0.4, 0.4);
|
||||
return (
|
||||
<>
|
||||
<Cue id="reveal">{(t, p) => (
|
||||
<div style={{ position: 'absolute', top: 480, left: 0, right: 0, textAlign: 'center', opacity: op * p }}>
|
||||
<div style={{ fontSize: 26, fontFamily: F.body, color: C.inkMute, marginBottom: 14, fontWeight: 300 }}>这俩根本是在争一个</div>
|
||||
<div style={{ fontSize: 170, fontFamily: F.display, fontWeight: 800, color: C.html, lineHeight: 0.95, letterSpacing: '0.05em', fontStyle: 'italic' }}>蠢问题</div>
|
||||
</div>
|
||||
)}</Cue>
|
||||
<Cue id="question-md">{(t, p) => (
|
||||
<div style={{ position: 'absolute', top: 770, left: 200, opacity: op * p, transform: `translateX(${(1-p)*-20}px)`, fontFamily: F.body, fontSize: 32, color: C.ink, textAlign: 'right', maxWidth: 360 }}>
|
||||
<div style={{ fontFamily: F.mono, fontSize: 13, color: C.md, letterSpacing: '0.18em', marginBottom: 8 }}>MD 党在回答</div>
|
||||
我们用什么<span style={{ color: C.md, fontStyle: 'italic', fontWeight: 700 }}>写</span>?
|
||||
</div>
|
||||
)}</Cue>
|
||||
<div style={{ position: 'absolute', top: 800, left: 0, right: 0, fontSize: 48, color: C.inkMute, textAlign: 'center', fontFamily: F.mono, opacity: op * 0.6 }}>≠</div>
|
||||
<Cue id="question-html">{(t, p) => (
|
||||
<div style={{ position: 'absolute', top: 770, right: 200, opacity: op * p, transform: `translateX(${(1-p)*20}px)`, fontFamily: F.body, fontSize: 32, color: C.ink, maxWidth: 360 }}>
|
||||
<div style={{ fontFamily: F.mono, fontSize: 13, color: C.html, letterSpacing: '0.18em', marginBottom: 8 }}>HTML 党在回答</div>
|
||||
我们给人什么<span style={{ color: C.html, fontStyle: 'italic', fontWeight: 700 }}>看</span>?
|
||||
</div>
|
||||
)}</Cue>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SplitAux = () => {
|
||||
const op = useSceneFade('the-split', 0.4, 0.6);
|
||||
return (
|
||||
<>
|
||||
<Cue id="split">{(t, p) => (
|
||||
<div style={{ position: 'absolute', top: 110, left: 0, right: 0, textAlign: 'center', opacity: op * p, transform: `translateY(${(1-p)*15}px)` }}>
|
||||
<div style={{ fontSize: 22, color: C.inkMute, fontFamily: F.body, marginBottom: 6 }}>md 和 html 不是替代,是</div>
|
||||
<div style={{ fontSize: 110, fontFamily: F.display, fontWeight: 800, color: C.ink, letterSpacing: '0.04em', lineHeight: 1 }}>
|
||||
分工<span style={{ color: C.html }}>关系</span>
|
||||
</div>
|
||||
</div>
|
||||
)}</Cue>
|
||||
<Cue id="ai-changes">{(t, p) => t && (
|
||||
<div style={{ position: 'absolute', top: 320, left: 0, right: 0, textAlign: 'center', opacity: op * p, fontFamily: F.body, fontSize: 20, color: C.inkSoft, lineHeight: 1.7, maxWidth: 1100, margin: '0 auto' }}>
|
||||
<div style={{ maxWidth: 980, margin: '0 auto' }}>
|
||||
以前你写 md 自己也看 md,所以折中。<br/>
|
||||
AI 出现后,生产成本被 AI 吸收,原来要折中的需求<strong>被拆成了两端的极端最优。</strong>
|
||||
</div>
|
||||
</div>
|
||||
)}</Cue>
|
||||
{/* 生产端 / 消费端标签放 hero 上方,避免被遮挡 */}
|
||||
<Cue id="md-side-win">{(t, p) => (
|
||||
<div style={{ position: 'absolute', top: 470, left: '22%', transform: `translateX(-50%) translateY(${(1-p)*30}px)`, opacity: op * p, textAlign: 'center' }}>
|
||||
<div style={{ fontFamily: F.mono, fontSize: 13, color: C.md, letterSpacing: '0.22em', marginBottom: 6 }}>生产端</div>
|
||||
<div style={{ fontSize: 19, color: C.inkSoft, fontFamily: F.body }}>轻 · 快 · token-efficient</div>
|
||||
</div>
|
||||
)}</Cue>
|
||||
<Cue id="html-side-win">{(t, p) => (
|
||||
<div style={{ position: 'absolute', top: 470, left: '78%', transform: `translateX(-50%) translateY(${(1-p)*30}px)`, opacity: op * p, textAlign: 'center' }}>
|
||||
<div style={{ fontFamily: F.mono, fontSize: 13, color: C.html, letterSpacing: '0.22em', marginBottom: 6 }}>消费端</div>
|
||||
<div style={{ fontSize: 19, color: C.inkSoft, fontFamily: F.body }}>丰富 · 可视化 · 好分享</div>
|
||||
</div>
|
||||
)}</Cue>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ProofAux = () => {
|
||||
const op = useSceneFade('activity-proof', 0.4, 0.5);
|
||||
return (
|
||||
<>
|
||||
<div style={{ position: 'absolute', top: 320, left: 0, right: 0, textAlign: 'center', opacity: op, fontSize: 28, fontFamily: F.body, color: C.ink }}>
|
||||
最干净的活样本是 <span style={{ color: C.html, fontFamily: F.mono, fontWeight: 700 }}>@thariq</span>
|
||||
</div>
|
||||
<Cue id="thariq-march">{(t, p) => (
|
||||
<div style={{ position: 'absolute', top: 410, left: '50%', transform: `translateX(-50%) translateY(${(1-p)*16}px)`, opacity: op * p, display: 'flex', alignItems: 'center', gap: 22 }}>
|
||||
<div style={{ fontFamily: F.mono, fontSize: 19, color: C.md, fontWeight: 700, minWidth: 90, textAlign: 'right' }}>2026.03</div>
|
||||
<div style={{ width: 12, height: 12, borderRadius: 6, background: C.md }} />
|
||||
<div style={{ fontSize: 23, fontFamily: F.body, color: C.ink, minWidth: 380 }}>《Skills 指南》—— <span style={{ color: C.md }}>核心还是 markdown</span></div>
|
||||
</div>
|
||||
)}</Cue>
|
||||
<Cue id="same-person">{(t, p) => (
|
||||
<div style={{ position: 'absolute', top: 480, left: '50%', transform: `translateX(-50%) translateY(${(1-p)*16}px)`, opacity: op * p, display: 'flex', alignItems: 'center', gap: 22 }}>
|
||||
<div style={{ fontFamily: F.mono, fontSize: 19, color: C.html, fontWeight: 700, minWidth: 90, textAlign: 'right' }}>2026.05</div>
|
||||
<div style={{ width: 12, height: 12, borderRadius: 6, background: C.html }} />
|
||||
<div style={{ fontSize: 23, fontFamily: F.body, color: C.ink, minWidth: 380 }}>《HTML is the new markdown》</div>
|
||||
</div>
|
||||
)}</Cue>
|
||||
<Cue id="same-person">{(t, p) => t && (
|
||||
<div style={{ position: 'absolute', top: 580, left: 0, right: 0, textAlign: 'center', opacity: op * p, fontFamily: F.display, fontSize: 28, color: C.ink, fontStyle: 'italic' }}>
|
||||
同一个人 · 两端各自登顶 · 互不打架
|
||||
</div>
|
||||
)}</Cue>
|
||||
<Cue id="karpathy-lex">{(t, p) => t && (
|
||||
<div style={{ position: 'absolute', top: 700, left: '50%', transform: `translateX(-50%) translateY(${(1-p)*14}px)`, opacity: op * p, padding: '18px 28px', background: C.ink, color: C.paper, display: 'flex', alignItems: 'center', gap: 30 }}>
|
||||
<div style={{ fontFamily: F.mono, fontSize: 12, color: '#999', letterSpacing: '0.2em' }}>KARPATHY × LEX</div>
|
||||
<div style={{ display: 'flex', gap: 20, alignItems: 'center', fontFamily: F.body }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 10, color: '#999', fontFamily: F.mono, marginBottom: 2, letterSpacing: '0.12em' }}>内核</div>
|
||||
<div style={{ fontSize: 19, color: C.md, fontWeight: 600 }}>markdown wiki</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 19, color: '#666' }}>+</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 10, color: '#999', fontFamily: F.mono, marginBottom: 2, letterSpacing: '0.12em' }}>外壳</div>
|
||||
<div style={{ fontSize: 19, color: C.html, fontWeight: 600 }}>动态 HTML</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}</Cue>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ClosingAux = () => {
|
||||
const op = useSceneFade('closing', 0.3, 0.6);
|
||||
return (
|
||||
<>
|
||||
<Cue id="final">{(t, p) => (
|
||||
<div style={{ position: 'absolute', top: 110, left: 0, right: 0, textAlign: 'center', opacity: op * p, transform: `translateY(${(1-p)*12}px)` }}>
|
||||
<div style={{ fontSize: 22, color: C.inkMute, fontFamily: F.body, marginBottom: 12 }}>下次想吵的时候,先问自己 ——</div>
|
||||
<div style={{ fontSize: 68, fontFamily: F.display, fontWeight: 700, color: C.ink, lineHeight: 1.15 }}>
|
||||
你面对的是「<span style={{ color: C.md }}>写</span>」,
|
||||
还是「<span style={{ color: C.html }}>看</span>」?
|
||||
</div>
|
||||
</div>
|
||||
)}</Cue>
|
||||
<Cue id="md-final">{(t, p) => (
|
||||
<div style={{ position: 'absolute', top: 740, left: '28%', transform: `translateX(-50%) translateY(${(1-p)*16}px)`, opacity: op * p, textAlign: 'center' }}>
|
||||
<div style={{ fontSize: 42, fontFamily: F.display, fontWeight: 600, color: C.md, letterSpacing: '0.04em' }}>写</div>
|
||||
<div style={{ fontSize: 22, color: C.inkMute, fontFamily: F.mono, marginTop: 4 }}>↓</div>
|
||||
</div>
|
||||
)}</Cue>
|
||||
<Cue id="html-final">{(t, p) => (
|
||||
<div style={{ position: 'absolute', top: 740, left: '72%', transform: `translateX(-50%) translateY(${(1-p)*16}px)`, opacity: op * p, textAlign: 'center' }}>
|
||||
<div style={{ fontSize: 42, fontFamily: F.display, fontWeight: 600, color: C.html, letterSpacing: '0.04em' }}>看</div>
|
||||
<div style={{ fontSize: 22, color: C.inkMute, fontFamily: F.mono, marginTop: 4 }}>↓</div>
|
||||
</div>
|
||||
)}</Cue>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// ── 主 App ─────────────────────────────────────────
|
||||
const App = () => (
|
||||
<NarrationStage timeline={TIMELINE} audioSrc="_narration/voiceover.mp3" width={1920} height={1080} background={C.paper}>
|
||||
<BackgroundDrift />
|
||||
<HeroAnchor />
|
||||
<SceneLabel sceneId="opening" text="2026.05.07 · X" />
|
||||
<SceneLabel sceneId="md-side" text="MD 党的证据" />
|
||||
<SceneLabel sceneId="html-side" text="HTML 党的证据" />
|
||||
<SceneLabel sceneId="the-real-question" text="真问题" />
|
||||
<SceneLabel sceneId="the-split" text="MD 生产 · HTML 消费" />
|
||||
<SceneLabel sceneId="activity-proof" text="活样本" />
|
||||
<SceneLabel sceneId="closing" text="结语" />
|
||||
<OpeningAux />
|
||||
<MdSideAux />
|
||||
<HtmlSideAux />
|
||||
<RealQuestionAux />
|
||||
<SplitAux />
|
||||
<ProofAux />
|
||||
<ClosingAux />
|
||||
{/* 字幕条放最上层(z-index 自然在 DOM 顺序最后),盖住下方内容 */}
|
||||
<Subtitles />
|
||||
<div style={{ position: 'absolute', bottom: 24, right: 36, fontSize: 11, color: 'rgba(26,26,26,0.35)', letterSpacing: '0.2em', fontFamily: F.mono, pointerEvents: 'none' }}>
|
||||
Created by Huashu-Design
|
||||
</div>
|
||||
</NarrationStage>
|
||||
);
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
title: md还是html,这是个蠢问题
|
||||
gap: 0.5
|
||||
---
|
||||
|
||||
## opening
|
||||
前两天,[[cue:thariq]]Claude Code 团队的 Thariq 发了篇爆文。
|
||||
标题就一句话,HTML 是新的 markdown。
|
||||
他说他几乎不再写 md 文件了,全让 AI 给他生成 HTML。
|
||||
500 万阅读,X 上立马吵翻了。
|
||||
一派是 md 党,[[cue:two-camps]]觉得 md 才是 AI 时代的源代码。
|
||||
另一派觉得 Thariq 说得对,HTML 才是终极答案。
|
||||
|
||||
## md-side
|
||||
md 党的证据其实挺硬的。
|
||||
你看 OpenAI 去年发的 AGENTS.md,[[cue:agents-md]]60000 多个项目用,AWS、Anthropic、Google、微软、OpenAI,AI 半壁江山一起捐进 Linux Foundation 做开放标准。
|
||||
Karpathy 的 llm-wiki,主体就是三层 markdown,单一个 CLAUDE.md 文件,5 万 star。
|
||||
Cloudflare 实测过一组数据,[[cue:token-saving]]同一篇博客,HTML 一万六千 token,转成 md 只要三千。
|
||||
省 80%。
|
||||
GitHub 官方也讲过一句,文档不再是描述代码,[[cue:doc-is-code]]文档就是代码。
|
||||
|
||||
## html-side
|
||||
但 html 党也没说错。
|
||||
Thariq 那篇文章里几条论据我都同意。
|
||||
第一是空间信息。[[cue:spatial]]diff、调用图、架构图,本来就是有空间维度的,md 把它压成一行字,html 能左右对照,理解效率不是一个量级的。
|
||||
第二是动态体验。[[cue:dynamic]]做产品原型,按钮按下去什么颜色、什么 easing 曲线,文字描述再多没用,html 能让你直接看见。
|
||||
第三是结构化阅读。[[cue:structured]]可折叠章节、tab 代码块、边栏术语表,跟同样的字线性堆一遍是两种东西。
|
||||
Anthropic 现在的 Live Artifacts,HTML 已经从静态产物升级成可以交互、能拉实时数据的 dashboard。
|
||||
|
||||
## the-real-question
|
||||
我看完想说,[[cue:reveal]]这俩根本是在争一个蠢问题。
|
||||
两边都赢了。
|
||||
但赢的是不同的问题。
|
||||
md 党回答的是,[[cue:question-md]]我们用什么写。
|
||||
html 党回答的是,[[cue:question-html]]我们给人什么看。
|
||||
这是两个问题。
|
||||
怎么会有谁取代谁。
|
||||
|
||||
## the-split
|
||||
我觉得真问题是这个。
|
||||
md 和 html 不是替代关系,[[cue:split]]是分工关系。
|
||||
以前你写 md 自己也看 md。
|
||||
那时候要折中,所以 md 胜出。
|
||||
但 AI 出现后,[[cue:ai-changes]]第一次有了一个新情况。
|
||||
生产成本可以被 AI 吸收。
|
||||
HTML 那部分太重的代价,AI 替你扛。
|
||||
你只负责消费。
|
||||
原来要折中的需求,被拆成了两端的极端最优。
|
||||
生产端要轻、要快、要 token efficient,[[cue:md-side-win]]那就是 md。
|
||||
消费端要丰富、要可视化、要好分享,[[cue:html-side-win]]那就是 html。
|
||||
两端各自登顶。
|
||||
中间那个折中位置,没人需要了。
|
||||
|
||||
## activity-proof
|
||||
最干净的活样本是 Thariq 自己。
|
||||
3 月份他发了篇 Skills 指南,[[cue:thariq-march]]强调核心还是 markdown。
|
||||
5 月份他发了 HTML 是新 markdown。
|
||||
同一个人,[[cue:same-person]]两端各自登顶,互不打架。
|
||||
Karpathy 和 Lex Fridman 那对组合也一样。
|
||||
内核是 markdown wiki,[[cue:karpathy-lex]]外壳是动态 HTML。
|
||||
不是 Lex 替换了 Karpathy,是他在 Karpathy 的基础上加了一层消费层。
|
||||
|
||||
## closing
|
||||
所以下次你想吵这个的时候,[[cue:final]]先问自己一句。
|
||||
你现在面对的是「写」,还是「看」。
|
||||
写,[[cue:md-final]]用 md。
|
||||
看,[[cue:html-final]]用 html。
|
||||
工具替你处理切换。
|
||||
立场可以放下了。
|
||||
17
.claude/skills/huashu-design/demos/voiceover-demo/script.md
Normal file
17
.claude/skills/huashu-design/demos/voiceover-demo/script.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: 什么是 token
|
||||
gap: 0.4
|
||||
---
|
||||
|
||||
## intro
|
||||
你有没有想过,[[cue:question]]当我们和 AI 对话的时候,AI 到底是怎么理解我们的话的呢。
|
||||
|
||||
## token-1
|
||||
答案是它根本不理解汉字,[[cue:reveal]]它只认识 token。
|
||||
|
||||
## token-2
|
||||
你可以把 token 理解成 AI 的最小信息单位。
|
||||
比如「人工智能」这四个字,[[cue:split]]在 AI 眼里可能是两个 token:人工,智能。
|
||||
|
||||
## ending
|
||||
所以下次看到「百万 token 上下文」这种说法,[[cue:context]]你就知道,它说的是 AI 一次能记住多少个这样的小块。
|
||||
201
.claude/skills/huashu-design/demos/voiceover-demo/什么是token.html
Normal file
201
.claude/skills/huashu-design/demos/voiceover-demo/什么是token.html
Normal file
@@ -0,0 +1,201 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>什么是 token · narration demo</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
body { margin: 0; background: #0a0a0a; font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; min-height: 100vh; display: flex; align-items: center; justify-content: center; flex-direction: column; }
|
||||
#root { box-shadow: 0 20px 60px rgba(0,0,0,0.5); }
|
||||
.scene-padding { padding: 120px; height: 100%; box-sizing: border-box; display: flex; flex-direction: column; justify-content: center; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
<script type="text/babel">
|
||||
// ── timeline.json (inline) ─────────────────────────────────
|
||||
const TIMELINE = {
|
||||
"title": "什么是 token",
|
||||
"voice": null,
|
||||
"speed": 1,
|
||||
"gap": 0.4,
|
||||
"totalDuration": 23.808,
|
||||
"scenes": [
|
||||
{"id":"intro","start":0,"end":4.368,"duration":4.368,"audio":"audio/intro.mp3","text":"你有没有想过,当我们和 AI 对话的时候,AI 到底是怎么理解我们的话的呢。","cues":[{"id":"question","offset":1.08,"absoluteTime":1.08}]},
|
||||
{"id":"token-1","start":4.768,"end":7.576,"duration":2.808,"audio":"audio/token-1.mp3","text":"答案是它根本不理解汉字,它只认识 token。","cues":[{"id":"reveal","offset":1.632,"absoluteTime":6.4}]},
|
||||
{"id":"token-2","start":7.976,"end":16.808,"duration":8.832,"audio":"audio/token-2.mp3","text":"你可以把 token 理解成 AI 的最小信息单位。\n比如「人工智能」这四个字,在 AI 眼里可能是两个 token:人工,智能。","cues":[{"id":"split","offset":5.4,"absoluteTime":13.376}]},
|
||||
{"id":"ending","start":17.208,"end":23.664,"duration":6.456,"audio":"audio/ending.mp3","text":"所以下次看到「百万 token 上下文」这种说法,你就知道,它说的是 AI 一次能记住多少个这样的小块。","cues":[{"id":"context","offset":2.376,"absoluteTime":19.584}]}
|
||||
],
|
||||
"voiceover": "voiceover.mp3"
|
||||
};
|
||||
|
||||
// ── narration_stage.jsx (inline) ───────────────────────────
|
||||
const NarrationStageLib = (() => {
|
||||
const NarrationContext = React.createContext({ time: 0, scene: null, sceneTime: 0, isCueTriggered: () => false, cueProgress: () => 0 });
|
||||
|
||||
function NarrationStage({ timeline, audioSrc, width = 1920, height = 1080, background = '#0e0e0e', controls = true, children }) {
|
||||
const audioRef = React.useRef(null);
|
||||
const [time, setTime] = React.useState(0);
|
||||
const [playing, setPlaying] = React.useState(false);
|
||||
const recording = typeof window !== 'undefined' && window.__recording === true;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
window.__totalDuration = timeline.totalDuration;
|
||||
window.__ready = true;
|
||||
}, [timeline.totalDuration]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let raf;
|
||||
const tick = () => {
|
||||
if (recording) {
|
||||
if (typeof window.__time === 'number') setTime(window.__time);
|
||||
} else if (audioRef.current && !audioRef.current.paused) {
|
||||
setTime(audioRef.current.currentTime);
|
||||
}
|
||||
raf = requestAnimationFrame(tick);
|
||||
};
|
||||
tick();
|
||||
return () => cancelAnimationFrame(raf);
|
||||
}, [recording]);
|
||||
|
||||
const currentScene = React.useMemo(() => {
|
||||
if (!timeline.scenes) return null;
|
||||
for (let i = 0; i < timeline.scenes.length; i++) {
|
||||
const s = timeline.scenes[i];
|
||||
const next = timeline.scenes[i + 1];
|
||||
if (time >= s.start && (!next || time < next.start)) return s;
|
||||
}
|
||||
return timeline.scenes[0];
|
||||
}, [time, timeline.scenes]);
|
||||
|
||||
const sceneTime = currentScene ? Math.max(0, time - currentScene.start) : 0;
|
||||
|
||||
const allCues = React.useMemo(() => {
|
||||
const map = {};
|
||||
for (const s of timeline.scenes || []) for (const c of s.cues || []) map[c.id] = c;
|
||||
return map;
|
||||
}, [timeline.scenes]);
|
||||
|
||||
const isCueTriggered = React.useCallback((cueId) => { const c = allCues[cueId]; return c ? time >= c.absoluteTime : false; }, [allCues, time]);
|
||||
const cueProgress = React.useCallback((cueId, ramp = 0.5) => { const c = allCues[cueId]; if (!c) return 0; const dt = time - c.absoluteTime; if (dt <= 0) return 0; if (dt >= ramp) return 1; return dt / ramp; }, [allCues, time]);
|
||||
|
||||
const ctx = { time, scene: currentScene, sceneTime, isCueTriggered, cueProgress };
|
||||
|
||||
const handlePlayPause = () => { if (!audioRef.current) return; if (audioRef.current.paused) { audioRef.current.play(); setPlaying(true); } else { audioRef.current.pause(); setPlaying(false); } };
|
||||
const handleSeek = (e) => { if (!audioRef.current) return; const t = parseFloat(e.target.value); audioRef.current.currentTime = t; setTime(t); };
|
||||
|
||||
return (
|
||||
<NarrationContext.Provider value={ctx}>
|
||||
<div style={{ position: 'relative', width, height, background, overflow: 'hidden', color: '#fff', fontFamily: '-apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif' }}>
|
||||
{children}
|
||||
</div>
|
||||
{!recording && <audio ref={audioRef} src={audioSrc} preload="auto" onEnded={() => setPlaying(false)} />}
|
||||
{!recording && controls && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 16px', background: '#1a1a1a', color: '#ddd', fontFamily: 'monospace', fontSize: 13, width, boxSizing: 'border-box' }}>
|
||||
<button onClick={handlePlayPause} style={{ padding: '6px 14px', background: '#fff', color: '#000', border: 0, borderRadius: 4, cursor: 'pointer', fontWeight: 600 }}>
|
||||
{playing ? '❚❚ Pause' : '▶ Play'}
|
||||
</button>
|
||||
<input type="range" min={0} max={timeline.totalDuration} step={0.01} value={time} onChange={handleSeek} style={{ flex: 1 }} />
|
||||
<span style={{ minWidth: 110, textAlign: 'right' }}>{time.toFixed(2)} / {timeline.totalDuration.toFixed(2)}s</span>
|
||||
<span style={{ padding: '4px 10px', background: '#2a2a2a', borderRadius: 4, minWidth: 100, textAlign: 'center' }}>{currentScene ? currentScene.id : '—'}</span>
|
||||
</div>
|
||||
)}
|
||||
</NarrationContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Scene({ id, children, keepMounted = false }) {
|
||||
const { scene, sceneTime } = React.useContext(NarrationContext);
|
||||
const isActive = scene && scene.id === id;
|
||||
if (!isActive && !keepMounted) return null;
|
||||
const content = typeof children === 'function' ? children(sceneTime, scene) : children;
|
||||
return <div style={{ position: 'absolute', inset: 0, opacity: isActive ? 1 : 0, pointerEvents: isActive ? 'auto' : 'none', transition: keepMounted ? 'opacity 0.2s' : undefined }}>{content}</div>;
|
||||
}
|
||||
|
||||
function Cue({ id, ramp = 0.5, children }) {
|
||||
const { isCueTriggered, cueProgress } = React.useContext(NarrationContext);
|
||||
return children(isCueTriggered(id), cueProgress(id, ramp));
|
||||
}
|
||||
|
||||
return { NarrationStage, Scene, Cue };
|
||||
})();
|
||||
const { NarrationStage, Scene, Cue } = NarrationStageLib;
|
||||
|
||||
// ── 视觉内容 ─────────────────────────────────────────────
|
||||
const App = () => (
|
||||
<NarrationStage timeline={TIMELINE} audioSrc="_narration_token/voiceover.mp3" width={1920} height={1080} background="#0a0a0a">
|
||||
{/* Scene 1: 大问号引入 */}
|
||||
<Scene id="intro">
|
||||
<div className="scene-padding" style={{ alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Cue id="question">{(triggered, p) => (
|
||||
<div style={{ fontSize: 320, color: triggered ? '#ffd54a' : '#3a3a3a', fontWeight: 200, transition: 'color 0.4s', transform: `scale(${0.8 + p * 0.2})`, lineHeight: 1 }}>?</div>
|
||||
)}</Cue>
|
||||
<div style={{ fontSize: 56, color: '#aaa', marginTop: 60, letterSpacing: '0.05em', fontWeight: 300 }}>AI 是怎么理解我们的话的</div>
|
||||
</div>
|
||||
</Scene>
|
||||
|
||||
{/* Scene 2: reveal 关键词 */}
|
||||
<Scene id="token-1">
|
||||
<div className="scene-padding" style={{ alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ fontSize: 64, color: '#888', marginBottom: 80, fontWeight: 300 }}>它不认识汉字</div>
|
||||
<Cue id="reveal">{(triggered, p) => (
|
||||
<div style={{
|
||||
fontSize: 280, fontWeight: 700, color: '#ffd54a', letterSpacing: '0.05em',
|
||||
opacity: p, transform: `translateY(${(1 - p) * 40}px)`,
|
||||
fontFamily: 'monospace', textShadow: triggered ? '0 0 40px rgba(255, 213, 74, 0.4)' : 'none'
|
||||
}}>
|
||||
token
|
||||
</div>
|
||||
)}</Cue>
|
||||
</div>
|
||||
</Scene>
|
||||
|
||||
{/* Scene 3: 拆字演示 */}
|
||||
<Scene id="token-2">
|
||||
<div className="scene-padding" style={{ alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ fontSize: 48, color: '#aaa', marginBottom: 100, fontWeight: 300 }}>token = AI 的最小信息单位</div>
|
||||
<Cue id="split">{(triggered, p) => (
|
||||
<div style={{ display: 'flex', gap: triggered ? 80 : 8, transition: 'gap 0.6s cubic-bezier(0.16, 1, 0.3, 1)' }}>
|
||||
<div style={{ fontSize: 200, fontWeight: 600, color: triggered ? '#ffd54a' : '#fff', padding: triggered ? '40px 60px' : '40px 20px', border: triggered ? '4px solid #ffd54a' : '4px solid transparent', borderRadius: 24, transition: 'all 0.6s cubic-bezier(0.16, 1, 0.3, 1)', background: triggered ? 'rgba(255, 213, 74, 0.05)' : 'transparent' }}>
|
||||
人工
|
||||
</div>
|
||||
<div style={{ fontSize: 200, fontWeight: 600, color: triggered ? '#ffd54a' : '#fff', padding: triggered ? '40px 60px' : '40px 20px', border: triggered ? '4px solid #ffd54a' : '4px solid transparent', borderRadius: 24, transition: 'all 0.6s cubic-bezier(0.16, 1, 0.3, 1)', background: triggered ? 'rgba(255, 213, 74, 0.05)' : 'transparent' }}>
|
||||
智能
|
||||
</div>
|
||||
</div>
|
||||
)}</Cue>
|
||||
<div style={{ fontSize: 36, color: '#666', marginTop: 60, opacity: 0.6 }}>「人工智能」= 2 个 token</div>
|
||||
</div>
|
||||
</Scene>
|
||||
|
||||
{/* Scene 4: 总结 */}
|
||||
<Scene id="ending">
|
||||
<div className="scene-padding" style={{ alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Cue id="context">{(triggered, p) => (
|
||||
<>
|
||||
<div style={{ fontSize: 96, fontWeight: 700, letterSpacing: '0.02em', marginBottom: 40, color: '#fff', opacity: triggered ? 1 : 0.3, transition: 'opacity 0.5s' }}>
|
||||
<span style={{ color: '#ffd54a' }}>1,000,000</span> token
|
||||
</div>
|
||||
<div style={{ fontSize: 48, color: '#888', fontWeight: 300, opacity: p }}>
|
||||
≈ AI 一次能记住的<span style={{ color: '#fff', fontWeight: 500 }}>「小块」数量</span>
|
||||
</div>
|
||||
</>
|
||||
)}</Cue>
|
||||
</div>
|
||||
</Scene>
|
||||
|
||||
{/* 全局水印 */}
|
||||
<div style={{ position: 'absolute', bottom: 24, right: 32, fontSize: 11, color: 'rgba(255,255,255,0.35)', letterSpacing: '0.15em', fontFamily: 'monospace', pointerEvents: 'none', zIndex: 100 }}>
|
||||
Created by Huashu-Design
|
||||
</div>
|
||||
</NarrationStage>
|
||||
);
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
684
.claude/skills/huashu-design/demos/w1-brand-protocol-en.html
Normal file
684
.claude/skills/huashu-design/demos/w1-brand-protocol-en.html
Normal file
@@ -0,0 +1,684 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>w1 · Brand Protocol · Five steps, no skipping</title>
|
||||
<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=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@200;300;400;500;600&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #000000;
|
||||
--ink: #FFFFFF;
|
||||
--ink-80: rgba(255,255,255,0.82);
|
||||
--ink-60: rgba(255,255,255,0.58);
|
||||
--muted: rgba(255,255,255,0.40);
|
||||
--dim: rgba(255,255,255,0.18);
|
||||
--hairline: rgba(255,255,255,0.12);
|
||||
--accent: #D97757;
|
||||
--accent-deep: #B85D3D;
|
||||
--cd-bg: #F5F4F0;
|
||||
--cd-panel: #FFFFFF;
|
||||
--cd-ink: #1A1918;
|
||||
|
||||
--serif-zh: "Noto Serif SC", "Songti SC", serif;
|
||||
--serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
|
||||
--sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
|
||||
--mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
|
||||
}
|
||||
html, body {
|
||||
margin: 0; padding: 0;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
font-family: var(--sans);
|
||||
color: var(--ink);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
.stage {
|
||||
position: fixed;
|
||||
top: 50%; left: 50%;
|
||||
width: 1920px; height: 1080px;
|
||||
transform-origin: center center;
|
||||
background: var(--bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Film grain texture (very subtle) */
|
||||
.stage::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='300'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>");
|
||||
opacity: 0.02;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* Chrome · watermark */
|
||||
.mark {
|
||||
position: absolute;
|
||||
top: 48px; left: 64px;
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.2em;
|
||||
color: rgba(255,255,255,1);
|
||||
opacity: 0.16;
|
||||
pointer-events: none;
|
||||
z-index: 50;
|
||||
}
|
||||
.mark-right {
|
||||
position: absolute;
|
||||
top: 48px; right: 64px;
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.2em;
|
||||
color: rgba(255,255,255,1);
|
||||
opacity: 0.16;
|
||||
pointer-events: none;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
/* ====== Title (centered, small, top) ====== */
|
||||
.title-line {
|
||||
position: absolute;
|
||||
top: 128px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-family: var(--mono);
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.28em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
/* ====== Chain · 5 cards connected by a line ====== */
|
||||
.chain {
|
||||
position: absolute;
|
||||
top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 1680px;
|
||||
height: 360px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 80px;
|
||||
}
|
||||
|
||||
/* The connecting line behind the cards */
|
||||
.chain-line {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 140px;
|
||||
right: 140px;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(217,119,87,0.0) 2%,
|
||||
rgba(217,119,87,0.8) 12%,
|
||||
rgba(217,119,87,0.8) 88%,
|
||||
rgba(217,119,87,0.0) 98%,
|
||||
transparent 100%);
|
||||
transform-origin: left center;
|
||||
transform: scaleX(0);
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
width: 248px;
|
||||
height: 320px;
|
||||
background: rgba(255,255,255,0.02);
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32px 20px 26px;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
will-change: opacity, transform;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.card.active {
|
||||
border-color: rgba(217,119,87,0.6);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(217,119,87,0.35),
|
||||
0 30px 60px -30px rgba(217,119,87,0.35),
|
||||
0 10px 24px -10px rgba(0,0,0,0.6);
|
||||
}
|
||||
|
||||
.card-num {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.25em;
|
||||
color: var(--muted);
|
||||
}
|
||||
.card.active .card-num {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.card-glyph {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
text-align: center;
|
||||
}
|
||||
.card-label .zh {
|
||||
font-family: var(--serif-en);
|
||||
font-size: 36px;
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: var(--ink);
|
||||
letter-spacing: -0.01em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Glyph · Step 1 · Ask (question mark inside a circle, drawn minimal) */
|
||||
.g-ask {
|
||||
width: 80px; height: 80px;
|
||||
border: 1px solid var(--ink-60);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--serif-en);
|
||||
font-weight: 300;
|
||||
font-size: 44px;
|
||||
color: var(--ink-80);
|
||||
position: relative;
|
||||
transition: border-color 0.3s, color 0.3s;
|
||||
}
|
||||
.card.active .g-ask { border-color: var(--accent); color: var(--accent); }
|
||||
|
||||
/* Glyph · Step 2 · Search (magnifier with crosshair) */
|
||||
.g-search {
|
||||
width: 80px; height: 80px;
|
||||
position: relative;
|
||||
}
|
||||
.g-search .ring {
|
||||
position: absolute;
|
||||
top: 10px; left: 10px;
|
||||
width: 52px; height: 52px;
|
||||
border: 1px solid var(--ink-60);
|
||||
border-radius: 50%;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
.g-search .handle {
|
||||
position: absolute;
|
||||
bottom: 8px; right: 6px;
|
||||
width: 22px; height: 1px;
|
||||
background: var(--ink-60);
|
||||
transform: rotate(45deg);
|
||||
transform-origin: right center;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
.g-search .dot {
|
||||
position: absolute;
|
||||
top: 26px; left: 26px;
|
||||
width: 4px; height: 4px;
|
||||
background: var(--muted);
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s, background 0.3s;
|
||||
}
|
||||
.card.active .g-search .ring { border-color: var(--accent); }
|
||||
.card.active .g-search .handle { background: var(--accent); }
|
||||
.card.active .g-search .dot { opacity: 1; background: var(--accent); }
|
||||
|
||||
/* Glyph · Step 3 · Grab (download arrow into a tray) */
|
||||
.g-grab {
|
||||
width: 80px; height: 80px;
|
||||
position: relative;
|
||||
}
|
||||
.g-grab .arrow {
|
||||
position: absolute;
|
||||
top: 8px; left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 1px; height: 36px;
|
||||
background: var(--ink-60);
|
||||
transition: background 0.3s;
|
||||
}
|
||||
.g-grab .arrow::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px; left: 50%;
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
width: 14px; height: 14px;
|
||||
border-right: 1px solid currentColor;
|
||||
border-bottom: 1px solid currentColor;
|
||||
color: var(--ink-60);
|
||||
transition: color 0.3s;
|
||||
}
|
||||
.g-grab .tray {
|
||||
position: absolute;
|
||||
bottom: 10px; left: 12px; right: 12px;
|
||||
height: 20px;
|
||||
border: 1px solid var(--ink-60);
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
.card.active .g-grab .arrow { background: var(--accent); }
|
||||
.card.active .g-grab .arrow::before { color: var(--accent); }
|
||||
.card.active .g-grab .tray { border-color: var(--accent); }
|
||||
|
||||
/* Glyph · Step 4 · Grep (terminal-like code with highlighted match) */
|
||||
.g-grep {
|
||||
width: 100px; height: 80px;
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
line-height: 1.5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-left: 8px;
|
||||
position: relative;
|
||||
}
|
||||
.g-grep .line { white-space: nowrap; }
|
||||
.g-grep .hit {
|
||||
color: var(--accent);
|
||||
background: rgba(217,119,87,0.12);
|
||||
padding: 1px 3px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Glyph · Step 5 · Lock (a file with lines) */
|
||||
.g-lock {
|
||||
width: 72px; height: 86px;
|
||||
position: relative;
|
||||
}
|
||||
.g-lock .file {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border: 1px solid var(--ink-60);
|
||||
border-radius: 4px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
.g-lock .fold {
|
||||
position: absolute;
|
||||
top: -1px; right: -1px;
|
||||
width: 18px; height: 18px;
|
||||
background: var(--bg);
|
||||
border-left: 1px solid var(--ink-60);
|
||||
border-bottom: 1px solid var(--ink-60);
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
.g-lock .row {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
height: 1px;
|
||||
background: var(--muted);
|
||||
transition: background 0.3s;
|
||||
}
|
||||
.g-lock .row.r1 { top: 22px; width: 40px; }
|
||||
.g-lock .row.r2 { top: 34px; width: 48px; }
|
||||
.g-lock .row.r3 { top: 46px; width: 32px; }
|
||||
.g-lock .row.r4 { top: 58px; width: 44px; }
|
||||
.g-lock .row.r5 { top: 70px; width: 28px; background: var(--accent); }
|
||||
.card.active .g-lock .file { border-color: var(--accent); }
|
||||
.card.active .g-lock .fold { border-color: var(--accent); }
|
||||
|
||||
/* ====== Final · brand-spec.md file ====== */
|
||||
.final-file {
|
||||
position: absolute;
|
||||
top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%) scale(0.9);
|
||||
width: 520px;
|
||||
background: var(--cd-bg);
|
||||
color: var(--cd-ink);
|
||||
border-radius: 10px;
|
||||
padding: 38px 44px 42px;
|
||||
opacity: 0;
|
||||
box-shadow:
|
||||
0 40px 90px -30px rgba(217,119,87,0.4),
|
||||
0 20px 50px -20px rgba(0,0,0,0.6),
|
||||
0 0 0 1px rgba(217,119,87,0.3);
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
.final-file .file-name {
|
||||
font-family: var(--mono);
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--accent-deep);
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.final-file .file-name::before {
|
||||
content: '';
|
||||
width: 6px; height: 6px;
|
||||
background: var(--accent);
|
||||
border-radius: 50%;
|
||||
}
|
||||
.final-file .h1 {
|
||||
font-family: var(--serif-en);
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
margin: 0 0 18px;
|
||||
letter-spacing: -0.015em;
|
||||
}
|
||||
.final-file .kv {
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
line-height: 1.9;
|
||||
color: rgba(26,25,24,0.65);
|
||||
}
|
||||
.final-file .kv .k { color: var(--accent-deep); }
|
||||
.final-file .kv .swatch {
|
||||
display: inline-block;
|
||||
width: 10px; height: 10px;
|
||||
border-radius: 2px;
|
||||
vertical-align: middle;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.final-file .caret {
|
||||
display: inline-block;
|
||||
width: 7px; height: 14px;
|
||||
background: var(--accent);
|
||||
vertical-align: -2px;
|
||||
margin-left: 2px;
|
||||
animation: blink 1.1s steps(2) infinite;
|
||||
}
|
||||
@keyframes blink { 50% { opacity: 0; } }
|
||||
|
||||
/* Brand reveal (final 2 sec, keeps with Motion Spec) */
|
||||
.brand-sheet {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--cd-bg);
|
||||
transform: translateY(100%);
|
||||
will-change: transform;
|
||||
z-index: 80;
|
||||
}
|
||||
.brand-reveal {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 81;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
.brand-reveal .wordmark {
|
||||
font-family: var(--sans);
|
||||
font-weight: 100;
|
||||
font-size: 128px;
|
||||
letter-spacing: -0.045em;
|
||||
color: var(--cd-ink);
|
||||
line-height: 1;
|
||||
}
|
||||
.brand-reveal .wordmark .accent { color: var(--accent); }
|
||||
.brand-reveal .underline {
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: var(--accent);
|
||||
margin-top: 36px;
|
||||
will-change: width;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="stage" id="stage">
|
||||
<div class="mark">HUASHU · DESIGN</div>
|
||||
<div class="mark-right">V2 · 2026</div>
|
||||
|
||||
<div class="title-line" id="titleLine">w1 · brand protocol</div>
|
||||
|
||||
<div class="chain">
|
||||
<div class="chain-line" id="chainLine"></div>
|
||||
|
||||
<div class="card" data-step="1">
|
||||
<div class="card-num">STEP 01</div>
|
||||
<div class="card-glyph"><div class="g-ask">?</div></div>
|
||||
<div class="card-label">
|
||||
<div class="zh">Ask</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" data-step="2">
|
||||
<div class="card-num">STEP 02</div>
|
||||
<div class="card-glyph">
|
||||
<div class="g-search">
|
||||
<div class="ring"></div>
|
||||
<div class="handle"></div>
|
||||
<div class="dot"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-label">
|
||||
<div class="zh">Search</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" data-step="3">
|
||||
<div class="card-num">STEP 03</div>
|
||||
<div class="card-glyph">
|
||||
<div class="g-grab">
|
||||
<div class="arrow"></div>
|
||||
<div class="tray"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-label">
|
||||
<div class="zh">Grab</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" data-step="4">
|
||||
<div class="card-num">STEP 04</div>
|
||||
<div class="card-glyph">
|
||||
<div class="g-grep">
|
||||
<div class="line">#F5F4F0</div>
|
||||
<div class="line"><span class="hit">#D97757</span></div>
|
||||
<div class="line">#1A1918</div>
|
||||
<div class="line">#FFFFFF</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-label">
|
||||
<div class="zh">Grep</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" data-step="5">
|
||||
<div class="card-num">STEP 05</div>
|
||||
<div class="card-glyph">
|
||||
<div class="g-lock">
|
||||
<div class="file"></div>
|
||||
<div class="fold"></div>
|
||||
<div class="row r1"></div>
|
||||
<div class="row r2"></div>
|
||||
<div class="row r3"></div>
|
||||
<div class="row r4"></div>
|
||||
<div class="row r5"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-label">
|
||||
<div class="zh">Lock</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="final-file" id="finalFile">
|
||||
<div class="file-name">brand-spec.md</div>
|
||||
<div class="h1">Assets locked in<span class="caret"></span></div>
|
||||
<div class="kv">
|
||||
<div><span class="k">logo</span> · assets/logo.svg</div>
|
||||
<div><span class="k">hero</span> · product-hero.png</div>
|
||||
<div><span class="k">accent</span> · <span class="swatch" style="background:#D97757"></span>#D97757</div>
|
||||
<div><span class="k">bg</span> · <span class="swatch" style="background:#000;border:1px solid rgba(0,0,0,0.15)"></span>#000000</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="brand-sheet" id="brandSheet"></div>
|
||||
<div class="brand-reveal" id="brandReveal">
|
||||
<div class="wordmark">huashu<span class="accent"> · </span>design</div>
|
||||
<div class="underline" id="brandUnderline"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ── Auto-scale stage to viewport ─────────────────
|
||||
function fitStage() {
|
||||
const stage = document.getElementById('stage');
|
||||
const sx = window.innerWidth / 1920;
|
||||
const sy = window.innerHeight / 1080;
|
||||
const s = Math.min(sx, sy);
|
||||
stage.style.transform = `translate(-50%, -50%) scale(${s})`;
|
||||
}
|
||||
fitStage();
|
||||
window.addEventListener('resize', fitStage);
|
||||
|
||||
// ── Easing functions ─────────────────
|
||||
const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
|
||||
const expoIn = t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
|
||||
const cubicInOut = t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||
const cubicOut = t => 1 - Math.pow(1 - t, 3);
|
||||
|
||||
function lerp(t, a, b, easing) {
|
||||
if (t <= 0) return a;
|
||||
if (t >= 1) return b;
|
||||
const e = easing ? easing(t) : t;
|
||||
return a + (b - a) * e;
|
||||
}
|
||||
function seg(time, start, end) {
|
||||
if (time <= start) return 0;
|
||||
if (time >= end) return 1;
|
||||
return (time - start) / (end - start);
|
||||
}
|
||||
|
||||
// ── Timeline (total 12s) ─────────────────
|
||||
// Beat 1 (0-2s) · Beat 2 (2-10s) · Beat 3 (10-12s)
|
||||
//
|
||||
// Card schedule:
|
||||
// Card 1 enter 0.8-1.6s, active 1.6-3.0
|
||||
// Card 2 enter 2.4-3.2s, active 3.2-4.6
|
||||
// Card 3 enter 4.0-4.8s, active 4.8-6.2
|
||||
// Card 4 enter 5.6-6.4s, active 6.4-7.8
|
||||
// Card 5 enter 7.2-8.0s, active 8.0-9.4
|
||||
// All cards stay visible (frozen after active ends)
|
||||
//
|
||||
// Line draws 0.6-8.0s (while cards come in)
|
||||
// Title fades in 0.2-1.2, fades out 9.6-10.0
|
||||
// Final file: 8.8-9.8 scale in, hold to 10.0
|
||||
// Brand reveal: 10.0-12.0
|
||||
|
||||
const cards = Array.from(document.querySelectorAll('.card'));
|
||||
const cardTimings = [
|
||||
{ enter: [0.8, 1.6], active: [1.6, 3.0] },
|
||||
{ enter: [2.4, 3.2], active: [3.2, 4.6] },
|
||||
{ enter: [4.0, 4.8], active: [4.8, 6.2] },
|
||||
{ enter: [5.6, 6.4], active: [6.4, 7.8] },
|
||||
{ enter: [7.2, 8.0], active: [8.0, 9.4] },
|
||||
];
|
||||
|
||||
const titleLine = document.getElementById('titleLine');
|
||||
const chainLine = document.getElementById('chainLine');
|
||||
const finalFile = document.getElementById('finalFile');
|
||||
const brandSheet = document.getElementById('brandSheet');
|
||||
const brandReveal = document.getElementById('brandReveal');
|
||||
const brandUnderline = document.getElementById('brandUnderline');
|
||||
|
||||
const DURATION = 12.0;
|
||||
let startTime = null;
|
||||
let loop = true;
|
||||
|
||||
// Honor recording flag
|
||||
if (window.__recording === true) loop = false;
|
||||
|
||||
function tick(now) {
|
||||
if (startTime === null) startTime = now;
|
||||
let t = (now - startTime) / 1000;
|
||||
|
||||
if (t >= DURATION) {
|
||||
if (loop) { startTime = now; t = 0; }
|
||||
else { t = DURATION; }
|
||||
}
|
||||
|
||||
// Title
|
||||
const titleIn = seg(t, 0.2, 1.2);
|
||||
const titleOut = seg(t, 9.6, 10.0);
|
||||
const titleOpacity = Math.min(cubicOut(titleIn), 1 - titleOut);
|
||||
titleLine.style.opacity = Math.max(0, titleOpacity);
|
||||
titleLine.style.transform = `translateX(-50%) translateY(${lerp(titleIn, -8, 0, cubicOut)}px)`;
|
||||
|
||||
// Chain line — grows left→right as cards arrive
|
||||
const lineT = seg(t, 0.6, 8.0);
|
||||
chainLine.style.transform = `scaleX(${cubicInOut(lineT)})`;
|
||||
|
||||
// Cards
|
||||
cards.forEach((card, i) => {
|
||||
const { enter, active } = cardTimings[i];
|
||||
const enterT = seg(t, enter[0], enter[1]);
|
||||
|
||||
const baseOp = expoOut(enterT);
|
||||
const ty = lerp(enterT, 20, 0, expoOut);
|
||||
|
||||
// Active state during the card's "spotlight" window
|
||||
const isActive = t >= active[0] && t <= active[1];
|
||||
card.classList.toggle('active', isActive);
|
||||
|
||||
// Cards dim to 25% when final file starts zooming in (8.8-9.6),
|
||||
// then fade fully when brand reveal takes over (10.0-10.4)
|
||||
const dimT = seg(t, 8.8, 9.6);
|
||||
const exitT = seg(t, 10.0, 10.4);
|
||||
const dimFactor = lerp(dimT, 1.0, 0.22, cubicInOut);
|
||||
const finalOp = baseOp * dimFactor * (1 - exitT);
|
||||
|
||||
if (dimT > 0) card.classList.remove('active');
|
||||
|
||||
card.style.opacity = finalOp;
|
||||
card.style.transform = `translateY(${ty - 10 * exitT}px)`;
|
||||
});
|
||||
|
||||
// Chain line also dims when final file zooms, fades with cards at 10.0-10.4
|
||||
const chainDim = seg(t, 8.8, 9.6);
|
||||
const chainExit = seg(t, 10.0, 10.4);
|
||||
chainLine.style.opacity = lerp(chainDim, 1, 0.22, cubicInOut) * (1 - chainExit);
|
||||
|
||||
// Final file: 8.8-9.8 scale+fade in, then 9.8-10.2 scale+settle, hold to ~10.0
|
||||
const finalInT = seg(t, 8.8, 9.8);
|
||||
const finalScale = lerp(finalInT, 0.88, 1.0, expoOut);
|
||||
const finalOp = cubicOut(finalInT);
|
||||
// fade final file out into brand reveal
|
||||
const finalOut = seg(t, 10.0, 10.6);
|
||||
finalFile.style.opacity = finalOp * (1 - finalOut);
|
||||
finalFile.style.transform = `translate(-50%, -50%) scale(${finalScale * (1 - finalOut * 0.04)})`;
|
||||
|
||||
// Brand reveal — sheet slides up from bottom 10.0-10.6, wordmark fades in 10.6-11.4, underline 11.4-11.9
|
||||
const sheetT = seg(t, 10.0, 10.6);
|
||||
brandSheet.style.transform = `translateY(${lerp(sheetT, 100, 0, expoOut)}%)`;
|
||||
|
||||
const wordT = seg(t, 10.6, 11.4);
|
||||
brandReveal.style.opacity = cubicOut(wordT);
|
||||
// NOTE: no scale transform on .brand-reveal — it would compound with the
|
||||
// underline width animation and make the line appear mis-placed. Instead,
|
||||
// scale the wordmark alone via font-variation-settings-safe approach: none here.
|
||||
|
||||
const underT = seg(t, 11.4, 11.9);
|
||||
brandUnderline.style.width = `${lerp(underT, 0, 280, expoOut)}px`;
|
||||
|
||||
// Mark as ready for recorder on first frame
|
||||
if (!window.__ready) window.__ready = true;
|
||||
|
||||
if (loop || t < DURATION) requestAnimationFrame(tick);
|
||||
}
|
||||
// Wait for fonts before first paint so Serif glyphs are correct
|
||||
(document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve())
|
||||
.then(() => requestAnimationFrame(tick));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
696
.claude/skills/huashu-design/demos/w1-brand-protocol.html
Normal file
696
.claude/skills/huashu-design/demos/w1-brand-protocol.html
Normal file
@@ -0,0 +1,696 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-Hans">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>w1 · 品牌协议 · 五步不能跳</title>
|
||||
<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=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@200;300;400;500;600&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #000000;
|
||||
--ink: #FFFFFF;
|
||||
--ink-80: rgba(255,255,255,0.82);
|
||||
--ink-60: rgba(255,255,255,0.58);
|
||||
--muted: rgba(255,255,255,0.40);
|
||||
--dim: rgba(255,255,255,0.18);
|
||||
--hairline: rgba(255,255,255,0.12);
|
||||
--accent: #D97757;
|
||||
--accent-deep: #B85D3D;
|
||||
--cd-bg: #F5F4F0;
|
||||
--cd-panel: #FFFFFF;
|
||||
--cd-ink: #1A1918;
|
||||
|
||||
--serif-zh: "Noto Serif SC", "Songti SC", serif;
|
||||
--serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
|
||||
--sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
|
||||
--mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
|
||||
}
|
||||
html, body {
|
||||
margin: 0; padding: 0;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
font-family: var(--sans);
|
||||
color: var(--ink);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
.stage {
|
||||
position: fixed;
|
||||
top: 50%; left: 50%;
|
||||
width: 1920px; height: 1080px;
|
||||
transform-origin: center center;
|
||||
background: var(--bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Film grain texture (very subtle) */
|
||||
.stage::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='300'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>");
|
||||
opacity: 0.02;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* Chrome · watermark */
|
||||
.mark {
|
||||
position: absolute;
|
||||
top: 48px; left: 64px;
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.2em;
|
||||
color: rgba(255,255,255,1);
|
||||
opacity: 0.16;
|
||||
pointer-events: none;
|
||||
z-index: 50;
|
||||
}
|
||||
.mark-right {
|
||||
position: absolute;
|
||||
top: 48px; right: 64px;
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.2em;
|
||||
color: rgba(255,255,255,1);
|
||||
opacity: 0.16;
|
||||
pointer-events: none;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
/* ====== Title (centered, small, top) ====== */
|
||||
.title-line {
|
||||
position: absolute;
|
||||
top: 128px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-family: var(--mono);
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.28em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
/* ====== Chain · 5 cards connected by a line ====== */
|
||||
.chain {
|
||||
position: absolute;
|
||||
top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 1680px;
|
||||
height: 360px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 80px;
|
||||
}
|
||||
|
||||
/* The connecting line behind the cards */
|
||||
.chain-line {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 140px;
|
||||
right: 140px;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(217,119,87,0.0) 2%,
|
||||
rgba(217,119,87,0.8) 12%,
|
||||
rgba(217,119,87,0.8) 88%,
|
||||
rgba(217,119,87,0.0) 98%,
|
||||
transparent 100%);
|
||||
transform-origin: left center;
|
||||
transform: scaleX(0);
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
width: 248px;
|
||||
height: 320px;
|
||||
background: rgba(255,255,255,0.02);
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32px 20px 26px;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
will-change: opacity, transform;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.card.active {
|
||||
border-color: rgba(217,119,87,0.6);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(217,119,87,0.35),
|
||||
0 30px 60px -30px rgba(217,119,87,0.35),
|
||||
0 10px 24px -10px rgba(0,0,0,0.6);
|
||||
}
|
||||
|
||||
.card-num {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.25em;
|
||||
color: var(--muted);
|
||||
}
|
||||
.card.active .card-num {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.card-glyph {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
text-align: center;
|
||||
}
|
||||
.card-label .zh {
|
||||
font-family: var(--serif-zh);
|
||||
font-size: 32px;
|
||||
font-weight: 300;
|
||||
color: var(--ink);
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.card-label .en {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.22em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Glyph · Step 1 · Ask (question mark inside a circle, drawn minimal) */
|
||||
.g-ask {
|
||||
width: 80px; height: 80px;
|
||||
border: 1px solid var(--ink-60);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--serif-en);
|
||||
font-weight: 300;
|
||||
font-size: 44px;
|
||||
color: var(--ink-80);
|
||||
position: relative;
|
||||
transition: border-color 0.3s, color 0.3s;
|
||||
}
|
||||
.card.active .g-ask { border-color: var(--accent); color: var(--accent); }
|
||||
|
||||
/* Glyph · Step 2 · Search (magnifier with crosshair) */
|
||||
.g-search {
|
||||
width: 80px; height: 80px;
|
||||
position: relative;
|
||||
}
|
||||
.g-search .ring {
|
||||
position: absolute;
|
||||
top: 10px; left: 10px;
|
||||
width: 52px; height: 52px;
|
||||
border: 1px solid var(--ink-60);
|
||||
border-radius: 50%;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
.g-search .handle {
|
||||
position: absolute;
|
||||
bottom: 8px; right: 6px;
|
||||
width: 22px; height: 1px;
|
||||
background: var(--ink-60);
|
||||
transform: rotate(45deg);
|
||||
transform-origin: right center;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
.g-search .dot {
|
||||
position: absolute;
|
||||
top: 26px; left: 26px;
|
||||
width: 4px; height: 4px;
|
||||
background: var(--muted);
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s, background 0.3s;
|
||||
}
|
||||
.card.active .g-search .ring { border-color: var(--accent); }
|
||||
.card.active .g-search .handle { background: var(--accent); }
|
||||
.card.active .g-search .dot { opacity: 1; background: var(--accent); }
|
||||
|
||||
/* Glyph · Step 3 · Grab (download arrow into a tray) */
|
||||
.g-grab {
|
||||
width: 80px; height: 80px;
|
||||
position: relative;
|
||||
}
|
||||
.g-grab .arrow {
|
||||
position: absolute;
|
||||
top: 8px; left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 1px; height: 36px;
|
||||
background: var(--ink-60);
|
||||
transition: background 0.3s;
|
||||
}
|
||||
.g-grab .arrow::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px; left: 50%;
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
width: 14px; height: 14px;
|
||||
border-right: 1px solid currentColor;
|
||||
border-bottom: 1px solid currentColor;
|
||||
color: var(--ink-60);
|
||||
transition: color 0.3s;
|
||||
}
|
||||
.g-grab .tray {
|
||||
position: absolute;
|
||||
bottom: 10px; left: 12px; right: 12px;
|
||||
height: 20px;
|
||||
border: 1px solid var(--ink-60);
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
.card.active .g-grab .arrow { background: var(--accent); }
|
||||
.card.active .g-grab .arrow::before { color: var(--accent); }
|
||||
.card.active .g-grab .tray { border-color: var(--accent); }
|
||||
|
||||
/* Glyph · Step 4 · Grep (terminal-like code with highlighted match) */
|
||||
.g-grep {
|
||||
width: 100px; height: 80px;
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
line-height: 1.5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-left: 8px;
|
||||
position: relative;
|
||||
}
|
||||
.g-grep .line { white-space: nowrap; }
|
||||
.g-grep .hit {
|
||||
color: var(--accent);
|
||||
background: rgba(217,119,87,0.12);
|
||||
padding: 1px 3px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Glyph · Step 5 · Lock (a file with lines) */
|
||||
.g-lock {
|
||||
width: 72px; height: 86px;
|
||||
position: relative;
|
||||
}
|
||||
.g-lock .file {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border: 1px solid var(--ink-60);
|
||||
border-radius: 4px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
.g-lock .fold {
|
||||
position: absolute;
|
||||
top: -1px; right: -1px;
|
||||
width: 18px; height: 18px;
|
||||
background: var(--bg);
|
||||
border-left: 1px solid var(--ink-60);
|
||||
border-bottom: 1px solid var(--ink-60);
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
.g-lock .row {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
height: 1px;
|
||||
background: var(--muted);
|
||||
transition: background 0.3s;
|
||||
}
|
||||
.g-lock .row.r1 { top: 22px; width: 40px; }
|
||||
.g-lock .row.r2 { top: 34px; width: 48px; }
|
||||
.g-lock .row.r3 { top: 46px; width: 32px; }
|
||||
.g-lock .row.r4 { top: 58px; width: 44px; }
|
||||
.g-lock .row.r5 { top: 70px; width: 28px; background: var(--accent); }
|
||||
.card.active .g-lock .file { border-color: var(--accent); }
|
||||
.card.active .g-lock .fold { border-color: var(--accent); }
|
||||
|
||||
/* ====== Final · brand-spec.md file ====== */
|
||||
.final-file {
|
||||
position: absolute;
|
||||
top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%) scale(0.9);
|
||||
width: 520px;
|
||||
background: var(--cd-bg);
|
||||
color: var(--cd-ink);
|
||||
border-radius: 10px;
|
||||
padding: 38px 44px 42px;
|
||||
opacity: 0;
|
||||
box-shadow:
|
||||
0 40px 90px -30px rgba(217,119,87,0.4),
|
||||
0 20px 50px -20px rgba(0,0,0,0.6),
|
||||
0 0 0 1px rgba(217,119,87,0.3);
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
.final-file .file-name {
|
||||
font-family: var(--mono);
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--accent-deep);
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.final-file .file-name::before {
|
||||
content: '';
|
||||
width: 6px; height: 6px;
|
||||
background: var(--accent);
|
||||
border-radius: 50%;
|
||||
}
|
||||
.final-file .h1 {
|
||||
font-family: var(--serif-zh);
|
||||
font-size: 26px;
|
||||
font-weight: 400;
|
||||
margin: 0 0 18px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.final-file .kv {
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
line-height: 1.9;
|
||||
color: rgba(26,25,24,0.65);
|
||||
}
|
||||
.final-file .kv .k { color: var(--accent-deep); }
|
||||
.final-file .kv .swatch {
|
||||
display: inline-block;
|
||||
width: 10px; height: 10px;
|
||||
border-radius: 2px;
|
||||
vertical-align: middle;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.final-file .caret {
|
||||
display: inline-block;
|
||||
width: 7px; height: 14px;
|
||||
background: var(--accent);
|
||||
vertical-align: -2px;
|
||||
margin-left: 2px;
|
||||
animation: blink 1.1s steps(2) infinite;
|
||||
}
|
||||
@keyframes blink { 50% { opacity: 0; } }
|
||||
|
||||
/* Brand reveal (final 2 sec, keeps with Motion Spec) */
|
||||
.brand-sheet {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--cd-bg);
|
||||
transform: translateY(100%);
|
||||
will-change: transform;
|
||||
z-index: 80;
|
||||
}
|
||||
.brand-reveal {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 81;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
.brand-reveal .wordmark {
|
||||
font-family: var(--sans);
|
||||
font-weight: 100;
|
||||
font-size: 128px;
|
||||
letter-spacing: -0.045em;
|
||||
color: var(--cd-ink);
|
||||
line-height: 1;
|
||||
}
|
||||
.brand-reveal .wordmark .accent { color: var(--accent); }
|
||||
.brand-reveal .underline {
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: var(--accent);
|
||||
margin-top: 36px;
|
||||
will-change: width;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="stage" id="stage">
|
||||
<div class="mark">HUASHU · DESIGN</div>
|
||||
<div class="mark-right">V2 · 2026</div>
|
||||
|
||||
<div class="title-line" id="titleLine">w1 · 品牌协议</div>
|
||||
|
||||
<div class="chain">
|
||||
<div class="chain-line" id="chainLine"></div>
|
||||
|
||||
<div class="card" data-step="1">
|
||||
<div class="card-num">STEP 01</div>
|
||||
<div class="card-glyph"><div class="g-ask">?</div></div>
|
||||
<div class="card-label">
|
||||
<div class="zh">问</div>
|
||||
<div class="en">Ask</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" data-step="2">
|
||||
<div class="card-num">STEP 02</div>
|
||||
<div class="card-glyph">
|
||||
<div class="g-search">
|
||||
<div class="ring"></div>
|
||||
<div class="handle"></div>
|
||||
<div class="dot"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-label">
|
||||
<div class="zh">搜</div>
|
||||
<div class="en">Search</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" data-step="3">
|
||||
<div class="card-num">STEP 03</div>
|
||||
<div class="card-glyph">
|
||||
<div class="g-grab">
|
||||
<div class="arrow"></div>
|
||||
<div class="tray"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-label">
|
||||
<div class="zh">下</div>
|
||||
<div class="en">Grab</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" data-step="4">
|
||||
<div class="card-num">STEP 04</div>
|
||||
<div class="card-glyph">
|
||||
<div class="g-grep">
|
||||
<div class="line">#F5F4F0</div>
|
||||
<div class="line"><span class="hit">#D97757</span></div>
|
||||
<div class="line">#1A1918</div>
|
||||
<div class="line">#FFFFFF</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-label">
|
||||
<div class="zh">grep</div>
|
||||
<div class="en">Extract</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" data-step="5">
|
||||
<div class="card-num">STEP 05</div>
|
||||
<div class="card-glyph">
|
||||
<div class="g-lock">
|
||||
<div class="file"></div>
|
||||
<div class="fold"></div>
|
||||
<div class="row r1"></div>
|
||||
<div class="row r2"></div>
|
||||
<div class="row r3"></div>
|
||||
<div class="row r4"></div>
|
||||
<div class="row r5"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-label">
|
||||
<div class="zh">定</div>
|
||||
<div class="en">Lock</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="final-file" id="finalFile">
|
||||
<div class="file-name">brand-spec.md</div>
|
||||
<div class="h1">资产已固化<span class="caret"></span></div>
|
||||
<div class="kv">
|
||||
<div><span class="k">logo</span> · assets/logo.svg</div>
|
||||
<div><span class="k">hero</span> · product-hero.png</div>
|
||||
<div><span class="k">accent</span> · <span class="swatch" style="background:#D97757"></span>#D97757</div>
|
||||
<div><span class="k">bg</span> · <span class="swatch" style="background:#000;border:1px solid rgba(0,0,0,0.15)"></span>#000000</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="brand-sheet" id="brandSheet"></div>
|
||||
<div class="brand-reveal" id="brandReveal">
|
||||
<div class="wordmark">huashu<span class="accent"> · </span>design</div>
|
||||
<div class="underline" id="brandUnderline"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ── Auto-scale stage to viewport ─────────────────
|
||||
function fitStage() {
|
||||
const stage = document.getElementById('stage');
|
||||
const sx = window.innerWidth / 1920;
|
||||
const sy = window.innerHeight / 1080;
|
||||
const s = Math.min(sx, sy);
|
||||
stage.style.transform = `translate(-50%, -50%) scale(${s})`;
|
||||
}
|
||||
fitStage();
|
||||
window.addEventListener('resize', fitStage);
|
||||
|
||||
// ── Easing functions ─────────────────
|
||||
const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
|
||||
const expoIn = t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
|
||||
const cubicInOut = t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||
const cubicOut = t => 1 - Math.pow(1 - t, 3);
|
||||
|
||||
function lerp(t, a, b, easing) {
|
||||
if (t <= 0) return a;
|
||||
if (t >= 1) return b;
|
||||
const e = easing ? easing(t) : t;
|
||||
return a + (b - a) * e;
|
||||
}
|
||||
function seg(time, start, end) {
|
||||
if (time <= start) return 0;
|
||||
if (time >= end) return 1;
|
||||
return (time - start) / (end - start);
|
||||
}
|
||||
|
||||
// ── Timeline (total 12s) ─────────────────
|
||||
// Beat 1 (0-2s) · Beat 2 (2-10s) · Beat 3 (10-12s)
|
||||
//
|
||||
// Card schedule:
|
||||
// Card 1 enter 0.8-1.6s, active 1.6-3.0
|
||||
// Card 2 enter 2.4-3.2s, active 3.2-4.6
|
||||
// Card 3 enter 4.0-4.8s, active 4.8-6.2
|
||||
// Card 4 enter 5.6-6.4s, active 6.4-7.8
|
||||
// Card 5 enter 7.2-8.0s, active 8.0-9.4
|
||||
// All cards stay visible (frozen after active ends)
|
||||
//
|
||||
// Line draws 0.6-8.0s (while cards come in)
|
||||
// Title fades in 0.2-1.2, fades out 9.6-10.0
|
||||
// Final file: 8.8-9.8 scale in, hold to 10.0
|
||||
// Brand reveal: 10.0-12.0
|
||||
|
||||
const cards = Array.from(document.querySelectorAll('.card'));
|
||||
const cardTimings = [
|
||||
{ enter: [0.8, 1.6], active: [1.6, 3.0] },
|
||||
{ enter: [2.4, 3.2], active: [3.2, 4.6] },
|
||||
{ enter: [4.0, 4.8], active: [4.8, 6.2] },
|
||||
{ enter: [5.6, 6.4], active: [6.4, 7.8] },
|
||||
{ enter: [7.2, 8.0], active: [8.0, 9.4] },
|
||||
];
|
||||
|
||||
const titleLine = document.getElementById('titleLine');
|
||||
const chainLine = document.getElementById('chainLine');
|
||||
const finalFile = document.getElementById('finalFile');
|
||||
const brandSheet = document.getElementById('brandSheet');
|
||||
const brandReveal = document.getElementById('brandReveal');
|
||||
const brandUnderline = document.getElementById('brandUnderline');
|
||||
|
||||
const DURATION = 12.0;
|
||||
let startTime = null;
|
||||
let loop = true;
|
||||
|
||||
// Honor recording flag
|
||||
if (window.__recording === true) loop = false;
|
||||
|
||||
function tick(now) {
|
||||
if (startTime === null) startTime = now;
|
||||
let t = (now - startTime) / 1000;
|
||||
|
||||
if (t >= DURATION) {
|
||||
if (loop) { startTime = now; t = 0; }
|
||||
else { t = DURATION; }
|
||||
}
|
||||
|
||||
// Title
|
||||
const titleIn = seg(t, 0.2, 1.2);
|
||||
const titleOut = seg(t, 9.6, 10.0);
|
||||
const titleOpacity = Math.min(cubicOut(titleIn), 1 - titleOut);
|
||||
titleLine.style.opacity = Math.max(0, titleOpacity);
|
||||
titleLine.style.transform = `translateX(-50%) translateY(${lerp(titleIn, -8, 0, cubicOut)}px)`;
|
||||
|
||||
// Chain line — grows left→right as cards arrive
|
||||
const lineT = seg(t, 0.6, 8.0);
|
||||
chainLine.style.transform = `scaleX(${cubicInOut(lineT)})`;
|
||||
|
||||
// Cards
|
||||
cards.forEach((card, i) => {
|
||||
const { enter, active } = cardTimings[i];
|
||||
const enterT = seg(t, enter[0], enter[1]);
|
||||
|
||||
const baseOp = expoOut(enterT);
|
||||
const ty = lerp(enterT, 20, 0, expoOut);
|
||||
|
||||
// Active state during the card's "spotlight" window
|
||||
const isActive = t >= active[0] && t <= active[1];
|
||||
card.classList.toggle('active', isActive);
|
||||
|
||||
// Cards dim to 25% when final file starts zooming in (8.8-9.6),
|
||||
// then fade fully when brand reveal takes over (10.0-10.4)
|
||||
const dimT = seg(t, 8.8, 9.6);
|
||||
const exitT = seg(t, 10.0, 10.4);
|
||||
const dimFactor = lerp(dimT, 1.0, 0.22, cubicInOut);
|
||||
const finalOp = baseOp * dimFactor * (1 - exitT);
|
||||
|
||||
if (dimT > 0) card.classList.remove('active');
|
||||
|
||||
card.style.opacity = finalOp;
|
||||
card.style.transform = `translateY(${ty - 10 * exitT}px)`;
|
||||
});
|
||||
|
||||
// Chain line also dims when final file zooms, fades with cards at 10.0-10.4
|
||||
const chainDim = seg(t, 8.8, 9.6);
|
||||
const chainExit = seg(t, 10.0, 10.4);
|
||||
chainLine.style.opacity = lerp(chainDim, 1, 0.22, cubicInOut) * (1 - chainExit);
|
||||
|
||||
// Final file: 8.8-9.8 scale+fade in, then 9.8-10.2 scale+settle, hold to ~10.0
|
||||
const finalInT = seg(t, 8.8, 9.8);
|
||||
const finalScale = lerp(finalInT, 0.88, 1.0, expoOut);
|
||||
const finalOp = cubicOut(finalInT);
|
||||
// fade final file out into brand reveal
|
||||
const finalOut = seg(t, 10.0, 10.6);
|
||||
finalFile.style.opacity = finalOp * (1 - finalOut);
|
||||
finalFile.style.transform = `translate(-50%, -50%) scale(${finalScale * (1 - finalOut * 0.04)})`;
|
||||
|
||||
// Brand reveal — sheet slides up from bottom 10.0-10.6, wordmark fades in 10.6-11.4, underline 11.4-11.9
|
||||
const sheetT = seg(t, 10.0, 10.6);
|
||||
brandSheet.style.transform = `translateY(${lerp(sheetT, 100, 0, expoOut)}%)`;
|
||||
|
||||
const wordT = seg(t, 10.6, 11.4);
|
||||
brandReveal.style.opacity = cubicOut(wordT);
|
||||
// NOTE: no scale transform on .brand-reveal — it would compound with the
|
||||
// underline width animation and make the line appear mis-placed. Instead,
|
||||
// scale the wordmark alone via font-variation-settings-safe approach: none here.
|
||||
|
||||
const underT = seg(t, 11.4, 11.9);
|
||||
brandUnderline.style.width = `${lerp(underT, 0, 280, expoOut)}px`;
|
||||
|
||||
// Mark as ready for recorder on first frame
|
||||
if (!window.__ready) window.__ready = true;
|
||||
|
||||
if (loop || t < DURATION) requestAnimationFrame(tick);
|
||||
}
|
||||
// Wait for fonts before first paint so Serif glyphs are correct
|
||||
(document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve())
|
||||
.then(() => requestAnimationFrame(tick));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
983
.claude/skills/huashu-design/demos/w2-junior-designer-en.html
Normal file
983
.claude/skills/huashu-design/demos/w2-junior-designer-en.html
Normal file
@@ -0,0 +1,983 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>w2 · Rough draft now beats perfect draft later</title>
|
||||
<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=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@200;300;400;500;600&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #000000;
|
||||
--ink: #FFFFFF;
|
||||
--ink-80: rgba(255,255,255,0.82);
|
||||
--ink-60: rgba(255,255,255,0.58);
|
||||
--muted: rgba(255,255,255,0.40);
|
||||
--dim: rgba(255,255,255,0.18);
|
||||
--hairline: rgba(255,255,255,0.12);
|
||||
--accent: #D97757;
|
||||
--accent-deep: #B85D3D;
|
||||
--bad: #6E3A2E; /* 失败暗红调,不刺眼 */
|
||||
--bad-strong: #C85A42; /* 失败叉号强调,对比度提升 */
|
||||
--cool: rgba(255,255,255,0.42); /* 冷色参考线(左路径) */
|
||||
--cd-bg: #F5F4F0;
|
||||
--cd-panel: #FFFFFF;
|
||||
--cd-ink: #1A1918;
|
||||
|
||||
--serif-zh: "Noto Serif SC", "Songti SC", serif;
|
||||
--serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
|
||||
--sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
|
||||
--mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
|
||||
}
|
||||
html, body {
|
||||
margin: 0; padding: 0;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
font-family: var(--sans);
|
||||
color: var(--ink);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
.stage {
|
||||
position: fixed;
|
||||
top: 50%; left: 50%;
|
||||
width: 1920px; height: 1080px;
|
||||
transform-origin: center center;
|
||||
background: var(--bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Film grain */
|
||||
.stage::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='300'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>");
|
||||
opacity: 0.02;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* Chrome · watermark */
|
||||
.mark {
|
||||
position: absolute;
|
||||
top: 48px; left: 64px;
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.2em;
|
||||
color: rgba(255,255,255,1);
|
||||
opacity: 0.16;
|
||||
pointer-events: none;
|
||||
z-index: 50;
|
||||
}
|
||||
.mark-right {
|
||||
position: absolute;
|
||||
top: 48px; right: 64px;
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.2em;
|
||||
color: rgba(255,255,255,1);
|
||||
opacity: 0.16;
|
||||
pointer-events: none;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
/* Title */
|
||||
.title-line {
|
||||
position: absolute;
|
||||
top: 112px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-family: var(--mono);
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.28em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Splitter — horizontal line dividing the two halves */
|
||||
.splitter {
|
||||
position: absolute;
|
||||
left: 160px;
|
||||
right: 160px;
|
||||
top: 50%;
|
||||
height: 1px;
|
||||
background: var(--hairline);
|
||||
transform: scaleX(0);
|
||||
transform-origin: left center;
|
||||
will-change: transform;
|
||||
z-index: 5;
|
||||
}
|
||||
.splitter-label {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: var(--bg);
|
||||
padding: 0 28px;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.32em;
|
||||
color: var(--muted);
|
||||
z-index: 6;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ======================================================
|
||||
* TOP HALF · 闷头一把梭(3 hours, all at once)
|
||||
* ====================================================== */
|
||||
.half-top {
|
||||
position: absolute;
|
||||
top: 200px;
|
||||
left: 160px;
|
||||
right: 160px;
|
||||
height: 300px;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.half-label {
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.24em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.half-label .tag {
|
||||
padding: 3px 10px;
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 2px;
|
||||
color: var(--ink-60);
|
||||
}
|
||||
.half-top .half-label .tag { border-color: rgba(160,74,56,0.4); color: rgba(200,120,100,0.85); }
|
||||
.half-label .zh {
|
||||
font-family: var(--serif-zh);
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--ink-80);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
/* Single huge terminal panel */
|
||||
.terminal-big {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background: rgba(20, 20, 20, 1);
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255,255,255,0.02),
|
||||
0 40px 80px -30px rgba(0,0,0,0.7);
|
||||
position: relative;
|
||||
}
|
||||
.tty-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
background: rgba(255,255,255,0.02);
|
||||
}
|
||||
.tty-head .d {
|
||||
width: 10px; height: 10px; border-radius: 50%;
|
||||
background: var(--hairline);
|
||||
}
|
||||
.tty-title {
|
||||
margin-left: 14px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-family: var(--mono);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.tty-body {
|
||||
padding: 28px 30px;
|
||||
font-family: var(--mono);
|
||||
font-size: 17px;
|
||||
line-height: 1.6;
|
||||
color: rgba(255,255,255,0.86);
|
||||
}
|
||||
.tty-body .line {
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.tty-body .prompt { color: var(--accent); margin-right: 10px; }
|
||||
.tty-body .dim { color: var(--muted); }
|
||||
|
||||
/* The long running progress bar (simulated "3-hour render") */
|
||||
.progress-row {
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
font-family: var(--mono);
|
||||
font-size: 14px;
|
||||
color: var(--ink-60);
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
background: var(--hairline);
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-bar-fill {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
width: 0%;
|
||||
will-change: width, background;
|
||||
}
|
||||
.progress-bar.failed .progress-bar-fill {
|
||||
background: var(--bad-strong);
|
||||
}
|
||||
.progress-pct {
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: 0.04em;
|
||||
min-width: 54px;
|
||||
text-align: right;
|
||||
}
|
||||
.progress-hours {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
.progress-row.failed {
|
||||
color: var(--bad-strong);
|
||||
}
|
||||
|
||||
/* Big X overlay for failure stamp */
|
||||
.fail-stamp {
|
||||
position: absolute;
|
||||
right: 32px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) rotate(-8deg);
|
||||
width: 120px; height: 120px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
z-index: 10;
|
||||
}
|
||||
.fail-stamp svg { width: 100%; height: 100%; }
|
||||
.fail-stamp .stamp-text {
|
||||
position: absolute;
|
||||
bottom: -22px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.32em;
|
||||
color: var(--bad-strong);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ======================================================
|
||||
* BOTTOM HALF · 尽早 show(small iterations)
|
||||
* ====================================================== */
|
||||
.half-bot {
|
||||
position: absolute;
|
||||
top: 580px;
|
||||
left: 160px;
|
||||
right: 160px;
|
||||
height: 340px;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.half-bot .half-label .tag {
|
||||
border-color: rgba(217,119,87,0.35);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.iter-row {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
align-items: flex-end;
|
||||
height: 240px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.iter-panel {
|
||||
flex: 1;
|
||||
background: rgba(20, 20, 20, 1);
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
will-change: opacity, transform;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.iter-panel .ip-head {
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.16em;
|
||||
color: var(--muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.iter-panel .ip-version {
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
.iter-panel .ip-body {
|
||||
flex: 1;
|
||||
padding: 16px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
/* Rough mockup blocks that grow more detailed each iteration */
|
||||
.iter-panel .m-block {
|
||||
height: 8px;
|
||||
background: var(--dim);
|
||||
border-radius: 2px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.iter-panel .m-block.accent { background: var(--accent); opacity: 0.8; }
|
||||
.iter-panel .m-block.short { width: 40%; }
|
||||
.iter-panel .m-block.med { width: 70%; }
|
||||
.iter-panel .m-block.full { width: 100%; }
|
||||
.iter-panel .m-block.tall { height: 24px; }
|
||||
.iter-panel .m-block.big { height: 40px; }
|
||||
|
||||
.iter-panel .nod {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 14px;
|
||||
width: 16px; height: 16px;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
.iter-panel .nod svg {
|
||||
width: 100%; height: 100%;
|
||||
stroke: var(--accent);
|
||||
fill: none;
|
||||
stroke-width: 2;
|
||||
}
|
||||
.iter-panel .ip-minutes {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 14px;
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* Rising curve visualization for bottom half */
|
||||
.curve-wrap {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 340px;
|
||||
height: 180px;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.curve-wrap svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
.curve-wrap .axis {
|
||||
stroke: var(--hairline);
|
||||
stroke-width: 1;
|
||||
fill: none;
|
||||
}
|
||||
.curve-wrap .curve-path {
|
||||
stroke: var(--accent);
|
||||
stroke-width: 2;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.curve-wrap .curve-dot {
|
||||
fill: var(--accent);
|
||||
r: 3;
|
||||
}
|
||||
.curve-wrap .curve-label {
|
||||
font-family: var(--mono);
|
||||
font-size: 9px;
|
||||
fill: var(--muted);
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
|
||||
/* ======================================================
|
||||
* BEAT 3 · Full comparison chart crossfade
|
||||
* ====================================================== */
|
||||
.final-chart {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 1280px;
|
||||
height: 620px;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
z-index: 60;
|
||||
}
|
||||
.final-chart svg {
|
||||
width: 100%; height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
.final-chart .axis {
|
||||
stroke: var(--hairline);
|
||||
stroke-width: 1;
|
||||
fill: none;
|
||||
}
|
||||
.final-chart .axis-label {
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
fill: var(--muted);
|
||||
letter-spacing: 0.16em;
|
||||
}
|
||||
.final-chart .tick-label {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
fill: var(--dim);
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
.final-chart .curve-a {
|
||||
stroke: var(--cool);
|
||||
stroke-width: 2;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.final-chart .curve-a-dash {
|
||||
stroke: var(--bad-strong);
|
||||
stroke-width: 2.5;
|
||||
fill: none;
|
||||
stroke-dasharray: 5 7;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
.final-chart .curve-b {
|
||||
stroke: var(--accent);
|
||||
stroke-width: 3;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.final-chart .curve-b-glow {
|
||||
stroke: var(--accent);
|
||||
stroke-width: 6;
|
||||
fill: none;
|
||||
opacity: 0.18;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.final-chart .curve-dot {
|
||||
fill: var(--accent);
|
||||
}
|
||||
.final-chart .fail-dot {
|
||||
fill: none;
|
||||
stroke: var(--bad-strong);
|
||||
stroke-width: 2.5;
|
||||
}
|
||||
.final-chart .cool-dot {
|
||||
fill: var(--cool);
|
||||
}
|
||||
.final-chart .anchor-label {
|
||||
font-family: var(--serif-zh);
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.final-chart .anchor-en {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* ======================================================
|
||||
* BRAND REVEAL — 统一动作
|
||||
* ====================================================== */
|
||||
.brand-sheet {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--cd-bg);
|
||||
transform: translateY(100%);
|
||||
will-change: transform;
|
||||
z-index: 80;
|
||||
}
|
||||
.brand-reveal {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 81;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.brand-reveal .wordmark {
|
||||
font-family: var(--sans);
|
||||
font-weight: 100;
|
||||
font-size: 128px;
|
||||
letter-spacing: -0.045em;
|
||||
color: var(--cd-ink);
|
||||
line-height: 1;
|
||||
}
|
||||
.brand-reveal .wordmark .accent { color: var(--accent-deep); }
|
||||
.brand-reveal .underline {
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: var(--accent);
|
||||
margin-top: 36px;
|
||||
will-change: width;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="stage" id="stage">
|
||||
<div class="mark">HUASHU · DESIGN</div>
|
||||
<div class="mark-right">V2 · 2026</div>
|
||||
|
||||
<div class="title-line" id="titleLine">w2 · rough draft now beats perfect draft later</div>
|
||||
|
||||
<!-- Splitter -->
|
||||
<div class="splitter" id="splitter"></div>
|
||||
<div class="splitter-label" id="splitterLabel">VS</div>
|
||||
|
||||
<!-- ============ TOP HALF: All-at-once ============ -->
|
||||
<div class="half-top" id="halfTop">
|
||||
<div class="half-label">
|
||||
<span class="tag">A</span>
|
||||
<span class="zh" style="font-family: var(--serif-en); font-style: italic; letter-spacing: 0.01em;">All-at-once</span>
|
||||
<span style="font-family: var(--mono); color: var(--muted); letter-spacing: 0.18em; font-size: 11px; margin-left: auto;">3 HOUR SESSION</span>
|
||||
</div>
|
||||
<div class="terminal-big">
|
||||
<div class="tty-head">
|
||||
<div class="d"></div><div class="d"></div><div class="d"></div>
|
||||
<div class="tty-title">designer@studio · 3h session</div>
|
||||
</div>
|
||||
<div class="tty-body">
|
||||
<div class="line" id="ttyL1"><span class="prompt">$</span>build final_design.html <span class="dim">// v1.0 · ship it all at once</span></div>
|
||||
<div class="progress-row" id="progRow">
|
||||
<div class="progress-bar" id="progBar">
|
||||
<div class="progress-bar-fill" id="progFill"></div>
|
||||
</div>
|
||||
<span class="progress-pct" id="progPct">0%</span>
|
||||
<span class="progress-hours" id="progHours">03:00:00</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fail-stamp" id="failStamp">
|
||||
<svg viewBox="0 0 120 120">
|
||||
<circle cx="60" cy="60" r="52" fill="none" stroke="#A04A38" stroke-width="3"/>
|
||||
<path d="M 38 38 L 82 82 M 82 38 L 38 82" stroke="#A04A38" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<div class="stamp-text">REJECTED</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ BOTTOM HALF: Show early ============ -->
|
||||
<div class="half-bot" id="halfBot">
|
||||
<div class="half-label">
|
||||
<span class="tag">B</span>
|
||||
<span class="zh" style="font-family: var(--serif-en); font-style: italic; letter-spacing: 0.01em;">Show early</span>
|
||||
<span style="font-family: var(--mono); color: var(--muted); letter-spacing: 0.18em; font-size: 11px; margin-left: auto;">SMALL ITERATIONS</span>
|
||||
</div>
|
||||
<div class="iter-row">
|
||||
<div class="iter-panel" id="iter1">
|
||||
<div class="ip-head">
|
||||
<span>draft · v1</span>
|
||||
<span class="ip-version">15 min</span>
|
||||
</div>
|
||||
<div class="ip-body">
|
||||
<div class="m-block short"></div>
|
||||
<div class="m-block med"></div>
|
||||
<div class="m-block short"></div>
|
||||
</div>
|
||||
<div class="nod" id="nod1">
|
||||
<svg viewBox="0 0 16 16"><path d="M3 8 L7 12 L13 4"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="iter-panel" id="iter2">
|
||||
<div class="ip-head">
|
||||
<span>draft · v2</span>
|
||||
<span class="ip-version">25 min</span>
|
||||
</div>
|
||||
<div class="ip-body">
|
||||
<div class="m-block full tall"></div>
|
||||
<div class="m-block med"></div>
|
||||
<div class="m-block short"></div>
|
||||
<div class="m-block med accent"></div>
|
||||
</div>
|
||||
<div class="nod" id="nod2">
|
||||
<svg viewBox="0 0 16 16"><path d="M3 8 L7 12 L13 4"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="iter-panel" id="iter3">
|
||||
<div class="ip-head">
|
||||
<span>draft · v3</span>
|
||||
<span class="ip-version">35 min</span>
|
||||
</div>
|
||||
<div class="ip-body">
|
||||
<div class="m-block full big"></div>
|
||||
<div class="m-block full tall accent"></div>
|
||||
<div class="m-block med"></div>
|
||||
<div class="m-block full"></div>
|
||||
<div class="m-block short"></div>
|
||||
</div>
|
||||
<div class="nod" id="nod3">
|
||||
<svg viewBox="0 0 16 16"><path d="M3 8 L7 12 L13 4"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ Beat 3 · Final comparison chart ============ -->
|
||||
<div class="final-chart" id="finalChart">
|
||||
<svg viewBox="0 0 1280 620" preserveAspectRatio="xMidYMid meet">
|
||||
<!-- Axes -->
|
||||
<line class="axis" x1="110" y1="60" x2="110" y2="520"/>
|
||||
<line class="axis" x1="110" y1="520" x2="1200" y2="520"/>
|
||||
|
||||
<!-- Y-axis label -->
|
||||
<text class="axis-label" x="58" y="290" transform="rotate(-90 58 290)" text-anchor="middle">QUALITY</text>
|
||||
<!-- X-axis label -->
|
||||
<text class="axis-label" x="655" y="570" text-anchor="middle">TIME</text>
|
||||
|
||||
<!-- Tick marks -->
|
||||
<text class="tick-label" x="110" y="545" text-anchor="middle">0</text>
|
||||
<text class="tick-label" x="290" y="545" text-anchor="middle">15m</text>
|
||||
<text class="tick-label" x="480" y="545" text-anchor="middle">25m</text>
|
||||
<text class="tick-label" x="680" y="545" text-anchor="middle">35m</text>
|
||||
<text class="tick-label" x="1200" y="545" text-anchor="middle">3h</text>
|
||||
|
||||
<!-- Curve A (All-at-once): flat crawl near zero, late spike, then crash -->
|
||||
<path class="curve-a" id="curveA"
|
||||
d="M 110 500 L 400 495 L 700 490 L 1000 485 L 1140 180" />
|
||||
<path class="curve-a-dash" id="curveACrash"
|
||||
d="M 1140 180 L 1200 510" />
|
||||
<circle class="fail-dot" id="failDot" cx="1140" cy="180" r="9"/>
|
||||
<g id="failX" opacity="0">
|
||||
<line x1="1130" y1="170" x2="1150" y2="190" stroke="#C85A42" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<line x1="1150" y1="170" x2="1130" y2="190" stroke="#C85A42" stroke-width="2.5" stroke-linecap="round"/>
|
||||
</g>
|
||||
|
||||
<text class="anchor-label" x="1200" y="150" fill="#C85A42" text-anchor="end" style="font-family: var(--serif-en); font-style: italic;">All-at-once</text>
|
||||
<text class="anchor-en" x="1200" y="170" fill="#C85A42" text-anchor="end">REJECTED</text>
|
||||
|
||||
<!-- Curve B (Show early): steady step rise across first 35 min -->
|
||||
<path class="curve-b-glow" id="curveBGlow"
|
||||
d="M 110 500 L 290 380 L 480 270 L 680 140" />
|
||||
<path class="curve-b" id="curveB"
|
||||
d="M 110 500 L 290 380 L 480 270 L 680 140" />
|
||||
<circle class="curve-dot" cx="290" cy="380" r="6"/>
|
||||
<circle class="curve-dot" cx="480" cy="270" r="6"/>
|
||||
<circle class="curve-dot" cx="680" cy="140" r="8"/>
|
||||
|
||||
<text class="anchor-label" x="680" y="115" fill="#D97757" text-anchor="middle" style="font-family: var(--serif-en); font-style: italic;">Show early</text>
|
||||
<text class="anchor-en" x="680" y="96" fill="#D97757" text-anchor="middle">SHIPPED</text>
|
||||
|
||||
<text class="tick-label" x="555" y="477" text-anchor="middle" fill="rgba(255,255,255,0.3)" style="letter-spacing: 0.12em;">— 3 hours silence —</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Brand reveal -->
|
||||
<div class="brand-sheet" id="brandSheet"></div>
|
||||
<div class="brand-reveal" id="brandReveal">
|
||||
<div class="wordmark">huashu<span class="accent"> · </span>design</div>
|
||||
<div class="underline" id="brandUnderline"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Auto-scale stage
|
||||
function fitStage() {
|
||||
const stage = document.getElementById('stage');
|
||||
const sx = window.innerWidth / 1920;
|
||||
const sy = window.innerHeight / 1080;
|
||||
const s = Math.min(sx, sy);
|
||||
stage.style.transform = `translate(-50%, -50%) scale(${s})`;
|
||||
}
|
||||
fitStage();
|
||||
window.addEventListener('resize', fitStage);
|
||||
|
||||
// Easings
|
||||
const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
|
||||
const expoIn = t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
|
||||
const cubicInOut = t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||
const cubicOut = t => 1 - Math.pow(1 - t, 3);
|
||||
const cubicIn = t => t * t * t;
|
||||
|
||||
function lerp(t, a, b, easing) {
|
||||
if (t <= 0) return a;
|
||||
if (t >= 1) return b;
|
||||
const e = easing ? easing(t) : t;
|
||||
return a + (b - a) * e;
|
||||
}
|
||||
function seg(time, start, end) {
|
||||
if (time <= start) return 0;
|
||||
if (time >= end) return 1;
|
||||
return (time - start) / (end - start);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────
|
||||
// Timeline — total 12s (Beat 1: 0-2 · Beat 2: 2-10 · Beat 3: 10-12)
|
||||
//
|
||||
// 0.0-0.6 title + splitter grow
|
||||
// 0.6-1.4 two half-labels fade in (top first, then bot)
|
||||
// 1.4-2.0 top terminal line 1 types; bot panel 1 enters
|
||||
//
|
||||
// Top track (闷头):
|
||||
// 2.0-7.8 progress bar crawls from 0 to 99% (slow, painful)
|
||||
// 7.8-8.4 stuck at 99%
|
||||
// 8.4-8.9 fail stamp lands + bar turns red + bar drops to 0
|
||||
//
|
||||
// Bottom track (尽早):
|
||||
// 2.0-2.6 iter1 enters, nod1 appears @ 2.8
|
||||
// 3.6-4.2 iter2 enters, nod2 appears @ 4.4
|
||||
// 5.6-6.2 iter3 enters, nod3 appears @ 6.4 (final tick — biggest)
|
||||
//
|
||||
// 8.8-9.8 both halves dim; final chart crossfades in
|
||||
// (curves draw via stroke-dasharray)
|
||||
// 9.8-10.4 chart settles, anchor labels bloom
|
||||
// 10.0-12.0 brand reveal (sheet + wordmark + underline)
|
||||
// ────────────────────────────────────
|
||||
|
||||
const el = {
|
||||
title: document.getElementById('titleLine'),
|
||||
splitter: document.getElementById('splitter'),
|
||||
splitterLb: document.getElementById('splitterLabel'),
|
||||
halfTop: document.getElementById('halfTop'),
|
||||
halfBot: document.getElementById('halfBot'),
|
||||
ttyL1: document.getElementById('ttyL1'),
|
||||
progRow: document.getElementById('progRow'),
|
||||
progBar: document.getElementById('progBar'),
|
||||
progFill: document.getElementById('progFill'),
|
||||
progPct: document.getElementById('progPct'),
|
||||
progHours: document.getElementById('progHours'),
|
||||
failStamp: document.getElementById('failStamp'),
|
||||
iter1: document.getElementById('iter1'),
|
||||
iter2: document.getElementById('iter2'),
|
||||
iter3: document.getElementById('iter3'),
|
||||
nod1: document.getElementById('nod1'),
|
||||
nod2: document.getElementById('nod2'),
|
||||
nod3: document.getElementById('nod3'),
|
||||
finalChart: document.getElementById('finalChart'),
|
||||
brandSheet: document.getElementById('brandSheet'),
|
||||
brandReveal:document.getElementById('brandReveal'),
|
||||
brandUnder: document.getElementById('brandUnderline'),
|
||||
curveA: document.getElementById('curveA'),
|
||||
curveACrash:document.getElementById('curveACrash'),
|
||||
curveB: document.getElementById('curveB'),
|
||||
curveBGlow: document.getElementById('curveBGlow'),
|
||||
};
|
||||
|
||||
// Precompute path lengths for draw-on animation
|
||||
const lenA = el.curveA.getTotalLength();
|
||||
const lenACrash = el.curveACrash.getTotalLength();
|
||||
const lenB = el.curveB.getTotalLength();
|
||||
|
||||
el.curveA.style.strokeDasharray = `${lenA} ${lenA}`;
|
||||
el.curveA.style.strokeDashoffset = lenA;
|
||||
el.curveACrash.style.strokeDasharray = `${lenACrash} ${lenACrash}`;
|
||||
el.curveACrash.style.strokeDashoffset = lenACrash;
|
||||
el.curveB.style.strokeDasharray = `${lenB} ${lenB}`;
|
||||
el.curveB.style.strokeDashoffset = lenB;
|
||||
el.curveBGlow.style.strokeDasharray = `${lenB} ${lenB}`;
|
||||
el.curveBGlow.style.strokeDashoffset = lenB;
|
||||
|
||||
// Also precompute chart dot selections (hide initially)
|
||||
const chartDots = el.finalChart.querySelectorAll('circle');
|
||||
const chartAnchors = el.finalChart.querySelectorAll('.anchor-label, .anchor-en');
|
||||
const chartTicks = el.finalChart.querySelectorAll('.tick-label, .axis-label');
|
||||
|
||||
const DURATION = 12.0;
|
||||
let startTime = null;
|
||||
let loop = true;
|
||||
if (window.__recording === true) loop = false;
|
||||
|
||||
function tick(now) {
|
||||
if (startTime === null) startTime = now;
|
||||
let t = (now - startTime) / 1000;
|
||||
|
||||
if (t >= DURATION) {
|
||||
if (loop) { startTime = now; t = 0; }
|
||||
else { t = DURATION; }
|
||||
}
|
||||
|
||||
// ────── Title
|
||||
const titleIn = seg(t, 0.1, 1.0);
|
||||
const titleOut = seg(t, 9.2, 9.8);
|
||||
el.title.style.opacity = Math.max(0, Math.min(cubicOut(titleIn), 1 - titleOut));
|
||||
|
||||
// ────── Splitter (fade out earlier so Beat 3 is clean)
|
||||
const splitT = seg(t, 0.0, 0.8);
|
||||
const splitOut = seg(t, 8.4, 8.9);
|
||||
el.splitter.style.transform = `scaleX(${expoOut(splitT) * (1 - splitOut)})`;
|
||||
const splitLabelT = seg(t, 0.4, 1.0);
|
||||
const splitLabelOut = seg(t, 8.2, 8.7);
|
||||
el.splitterLb.style.opacity = Math.max(0, Math.min(cubicOut(splitLabelT), 1 - splitLabelOut));
|
||||
|
||||
// ────── Halves fade in / out (fade out earlier to clear for Beat 3 chart)
|
||||
const topIn = seg(t, 0.6, 1.4);
|
||||
const topOut = seg(t, 8.4, 9.0);
|
||||
el.halfTop.style.opacity = Math.max(0, Math.min(cubicOut(topIn), 1 - topOut));
|
||||
|
||||
const botIn = seg(t, 1.0, 1.8);
|
||||
const botOut = seg(t, 8.4, 9.0);
|
||||
el.halfBot.style.opacity = Math.max(0, Math.min(cubicOut(botIn), 1 - botOut));
|
||||
|
||||
// ────── TOP track: terminal line + progress bar
|
||||
const ttyL1In = seg(t, 1.4, 1.8);
|
||||
el.ttyL1.style.opacity = cubicOut(ttyL1In);
|
||||
|
||||
// Progress bar appears @ 1.8, starts crawling 2.0-7.8, stuck 7.8-8.4, fails @ 8.4
|
||||
const progRowIn = seg(t, 1.8, 2.2);
|
||||
el.progRow.style.opacity = cubicOut(progRowIn);
|
||||
|
||||
let pct = 0;
|
||||
let hoursTxt = '03:00:00';
|
||||
if (t >= 2.0 && t < 7.8) {
|
||||
const p = seg(t, 2.0, 7.8);
|
||||
// Easing: starts fast, slows down to 99% (mimics the "last 10% takes forever" trope)
|
||||
pct = 99 * (1 - Math.pow(1 - p, 2.2));
|
||||
const remaining = Math.max(0, (1 - p) * 3 * 60 * 60);
|
||||
const hh = String(Math.floor(remaining / 3600)).padStart(2, '0');
|
||||
const mm = String(Math.floor((remaining % 3600) / 60)).padStart(2, '0');
|
||||
const ss = String(Math.floor(remaining % 60)).padStart(2, '0');
|
||||
hoursTxt = `${hh}:${mm}:${ss}`;
|
||||
} else if (t >= 7.8 && t < 8.4) {
|
||||
pct = 99;
|
||||
// Micro-jitter to show "stuck"
|
||||
const jitter = Math.sin(t * 30) * 0.1;
|
||||
pct = 99 + jitter;
|
||||
hoursTxt = '00:00:12';
|
||||
} else if (t >= 8.4 && t < 8.7) {
|
||||
// Fail animation — pct stays at 99 briefly then snaps to 0
|
||||
pct = 99;
|
||||
hoursTxt = '— REJECTED —';
|
||||
} else if (t >= 8.7) {
|
||||
pct = 0;
|
||||
hoursTxt = '— REJECTED —';
|
||||
}
|
||||
|
||||
el.progFill.style.width = `${pct}%`;
|
||||
el.progPct.textContent = `${Math.floor(Math.max(0, pct))}%`;
|
||||
el.progHours.textContent = hoursTxt;
|
||||
|
||||
// Fail state toggle
|
||||
if (t >= 8.4) {
|
||||
el.progBar.classList.add('failed');
|
||||
el.progRow.classList.add('failed');
|
||||
} else {
|
||||
el.progBar.classList.remove('failed');
|
||||
el.progRow.classList.remove('failed');
|
||||
}
|
||||
|
||||
// Fail stamp lands at 8.4
|
||||
const stampIn = seg(t, 8.4, 8.7);
|
||||
if (stampIn > 0) {
|
||||
el.failStamp.style.opacity = cubicOut(stampIn);
|
||||
const scale = lerp(stampIn, 1.6, 1.0, expoOut);
|
||||
el.failStamp.style.transform = `translateY(-50%) rotate(-8deg) scale(${scale})`;
|
||||
} else {
|
||||
el.failStamp.style.opacity = 0;
|
||||
}
|
||||
|
||||
// ────── BOTTOM track: 3 iter panels
|
||||
const iterTimings = [
|
||||
{ enter: [2.0, 2.6], nod: [2.8, 3.2] },
|
||||
{ enter: [3.6, 4.2], nod: [4.4, 4.8] },
|
||||
{ enter: [5.6, 6.2], nod: [6.4, 6.9] },
|
||||
];
|
||||
|
||||
[el.iter1, el.iter2, el.iter3].forEach((panel, i) => {
|
||||
const { enter } = iterTimings[i];
|
||||
const p = seg(t, enter[0], enter[1]);
|
||||
const op = expoOut(p);
|
||||
const ty = lerp(p, 20, 0, expoOut);
|
||||
panel.style.opacity = op;
|
||||
panel.style.transform = `translateY(${ty}px)`;
|
||||
});
|
||||
|
||||
[el.nod1, el.nod2, el.nod3].forEach((n, i) => {
|
||||
const { nod } = iterTimings[i];
|
||||
const p = seg(t, nod[0], nod[1]);
|
||||
const op = expoOut(p);
|
||||
const scale = lerp(p, 0.4, 1.0, expoOut);
|
||||
n.style.opacity = op;
|
||||
n.style.transform = `scale(${scale})`;
|
||||
});
|
||||
|
||||
// ────── Beat 3 · final chart crossfade (chart appears as halves fade)
|
||||
const chartIn = seg(t, 8.5, 9.2);
|
||||
el.finalChart.style.opacity = cubicOut(chartIn);
|
||||
|
||||
const curveBT = seg(t, 8.8, 9.8);
|
||||
el.curveB.style.strokeDashoffset = lenB * (1 - expoOut(curveBT));
|
||||
el.curveBGlow.style.strokeDashoffset = lenB * (1 - expoOut(curveBT));
|
||||
|
||||
const curveAT = seg(t, 8.9, 9.7);
|
||||
el.curveA.style.strokeDashoffset = lenA * (1 - cubicOut(curveAT));
|
||||
const curveACrashT = seg(t, 9.7, 9.95);
|
||||
el.curveACrash.style.strokeDashoffset = lenACrash * (1 - expoOut(curveACrashT));
|
||||
const failXT = seg(t, 9.65, 9.85);
|
||||
const failXEl = document.getElementById('failX');
|
||||
if (failXEl) {
|
||||
failXEl.style.opacity = cubicOut(failXT);
|
||||
failXEl.style.transform = `scale(${lerp(failXT, 1.6, 1.0, expoOut)})`;
|
||||
failXEl.style.transformOrigin = '1140px 180px';
|
||||
}
|
||||
|
||||
chartDots.forEach((dot, i) => {
|
||||
const dotT = seg(t, 9.0 + i * 0.12, 9.3 + i * 0.12);
|
||||
dot.style.opacity = cubicOut(dotT);
|
||||
});
|
||||
chartAnchors.forEach((a) => {
|
||||
const aT = seg(t, 9.5, 9.95);
|
||||
a.style.opacity = cubicOut(aT);
|
||||
});
|
||||
chartTicks.forEach((tk) => {
|
||||
const tkT = seg(t, 8.7, 9.3);
|
||||
tk.style.opacity = cubicOut(tkT) * 0.9;
|
||||
});
|
||||
|
||||
// ────── Brand reveal 10.0-12.0
|
||||
const sheetT = seg(t, 10.0, 10.6);
|
||||
el.brandSheet.style.transform = `translateY(${lerp(sheetT, 100, 0, expoOut)}%)`;
|
||||
|
||||
const wordT = seg(t, 10.6, 11.4);
|
||||
el.brandReveal.style.opacity = cubicOut(wordT);
|
||||
|
||||
const underT = seg(t, 11.4, 11.9);
|
||||
el.brandUnder.style.width = `${lerp(underT, 0, 280, expoOut)}px`;
|
||||
|
||||
// Mark ready for recorder
|
||||
if (!window.__ready) window.__ready = true;
|
||||
|
||||
if (loop || t < DURATION) requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
(document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve())
|
||||
.then(() => requestAnimationFrame(tick));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
994
.claude/skills/huashu-design/demos/w2-junior-designer.html
Normal file
994
.claude/skills/huashu-design/demos/w2-junior-designer.html
Normal file
@@ -0,0 +1,994 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-Hans">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>w2 · 粗糙的第一版,好过完美的大招</title>
|
||||
<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=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@200;300;400;500;600&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #000000;
|
||||
--ink: #FFFFFF;
|
||||
--ink-80: rgba(255,255,255,0.82);
|
||||
--ink-60: rgba(255,255,255,0.58);
|
||||
--muted: rgba(255,255,255,0.40);
|
||||
--dim: rgba(255,255,255,0.18);
|
||||
--hairline: rgba(255,255,255,0.12);
|
||||
--accent: #D97757;
|
||||
--accent-deep: #B85D3D;
|
||||
--bad: #6E3A2E; /* 失败暗红调,不刺眼 */
|
||||
--bad-strong: #C85A42; /* 失败叉号强调,对比度提升 */
|
||||
--cool: rgba(255,255,255,0.42); /* 冷色参考线(左路径) */
|
||||
--cd-bg: #F5F4F0;
|
||||
--cd-panel: #FFFFFF;
|
||||
--cd-ink: #1A1918;
|
||||
|
||||
--serif-zh: "Noto Serif SC", "Songti SC", serif;
|
||||
--serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
|
||||
--sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
|
||||
--mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
|
||||
}
|
||||
html, body {
|
||||
margin: 0; padding: 0;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
font-family: var(--sans);
|
||||
color: var(--ink);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
.stage {
|
||||
position: fixed;
|
||||
top: 50%; left: 50%;
|
||||
width: 1920px; height: 1080px;
|
||||
transform-origin: center center;
|
||||
background: var(--bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Film grain */
|
||||
.stage::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='300'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>");
|
||||
opacity: 0.02;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* Chrome · watermark */
|
||||
.mark {
|
||||
position: absolute;
|
||||
top: 48px; left: 64px;
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.2em;
|
||||
color: rgba(255,255,255,1);
|
||||
opacity: 0.16;
|
||||
pointer-events: none;
|
||||
z-index: 50;
|
||||
}
|
||||
.mark-right {
|
||||
position: absolute;
|
||||
top: 48px; right: 64px;
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.2em;
|
||||
color: rgba(255,255,255,1);
|
||||
opacity: 0.16;
|
||||
pointer-events: none;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
/* Title */
|
||||
.title-line {
|
||||
position: absolute;
|
||||
top: 112px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-family: var(--mono);
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.28em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Splitter — horizontal line dividing the two halves */
|
||||
.splitter {
|
||||
position: absolute;
|
||||
left: 160px;
|
||||
right: 160px;
|
||||
top: 50%;
|
||||
height: 1px;
|
||||
background: var(--hairline);
|
||||
transform: scaleX(0);
|
||||
transform-origin: left center;
|
||||
will-change: transform;
|
||||
z-index: 5;
|
||||
}
|
||||
.splitter-label {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: var(--bg);
|
||||
padding: 0 28px;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.32em;
|
||||
color: var(--muted);
|
||||
z-index: 6;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ======================================================
|
||||
* TOP HALF · 闷头一把梭(3 hours, all at once)
|
||||
* ====================================================== */
|
||||
.half-top {
|
||||
position: absolute;
|
||||
top: 200px;
|
||||
left: 160px;
|
||||
right: 160px;
|
||||
height: 300px;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.half-label {
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.24em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.half-label .tag {
|
||||
padding: 3px 10px;
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 2px;
|
||||
color: var(--ink-60);
|
||||
}
|
||||
.half-top .half-label .tag { border-color: rgba(160,74,56,0.4); color: rgba(200,120,100,0.85); }
|
||||
.half-label .zh {
|
||||
font-family: var(--serif-zh);
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--ink-80);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
/* Single huge terminal panel */
|
||||
.terminal-big {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background: rgba(20, 20, 20, 1);
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255,255,255,0.02),
|
||||
0 40px 80px -30px rgba(0,0,0,0.7);
|
||||
position: relative;
|
||||
}
|
||||
.tty-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
background: rgba(255,255,255,0.02);
|
||||
}
|
||||
.tty-head .d {
|
||||
width: 10px; height: 10px; border-radius: 50%;
|
||||
background: var(--hairline);
|
||||
}
|
||||
.tty-title {
|
||||
margin-left: 14px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-family: var(--mono);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.tty-body {
|
||||
padding: 28px 30px;
|
||||
font-family: var(--mono);
|
||||
font-size: 17px;
|
||||
line-height: 1.6;
|
||||
color: rgba(255,255,255,0.86);
|
||||
}
|
||||
.tty-body .line {
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.tty-body .prompt { color: var(--accent); margin-right: 10px; }
|
||||
.tty-body .dim { color: var(--muted); }
|
||||
|
||||
/* The long running progress bar (simulated "3-hour render") */
|
||||
.progress-row {
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
font-family: var(--mono);
|
||||
font-size: 14px;
|
||||
color: var(--ink-60);
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
background: var(--hairline);
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-bar-fill {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
width: 0%;
|
||||
will-change: width, background;
|
||||
}
|
||||
.progress-bar.failed .progress-bar-fill {
|
||||
background: var(--bad-strong);
|
||||
}
|
||||
.progress-pct {
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: 0.04em;
|
||||
min-width: 54px;
|
||||
text-align: right;
|
||||
}
|
||||
.progress-hours {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
.progress-row.failed {
|
||||
color: var(--bad-strong);
|
||||
}
|
||||
|
||||
/* Big X overlay for failure stamp */
|
||||
.fail-stamp {
|
||||
position: absolute;
|
||||
right: 32px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) rotate(-8deg);
|
||||
width: 120px; height: 120px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
z-index: 10;
|
||||
}
|
||||
.fail-stamp svg { width: 100%; height: 100%; }
|
||||
.fail-stamp .stamp-text {
|
||||
position: absolute;
|
||||
bottom: -22px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.32em;
|
||||
color: var(--bad-strong);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ======================================================
|
||||
* BOTTOM HALF · 尽早 show(small iterations)
|
||||
* ====================================================== */
|
||||
.half-bot {
|
||||
position: absolute;
|
||||
top: 580px;
|
||||
left: 160px;
|
||||
right: 160px;
|
||||
height: 340px;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.half-bot .half-label .tag {
|
||||
border-color: rgba(217,119,87,0.35);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.iter-row {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
align-items: flex-end;
|
||||
height: 240px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.iter-panel {
|
||||
flex: 1;
|
||||
background: rgba(20, 20, 20, 1);
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
will-change: opacity, transform;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.iter-panel .ip-head {
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.16em;
|
||||
color: var(--muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.iter-panel .ip-version {
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
.iter-panel .ip-body {
|
||||
flex: 1;
|
||||
padding: 16px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
/* Rough mockup blocks that grow more detailed each iteration */
|
||||
.iter-panel .m-block {
|
||||
height: 8px;
|
||||
background: var(--dim);
|
||||
border-radius: 2px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.iter-panel .m-block.accent { background: var(--accent); opacity: 0.8; }
|
||||
.iter-panel .m-block.short { width: 40%; }
|
||||
.iter-panel .m-block.med { width: 70%; }
|
||||
.iter-panel .m-block.full { width: 100%; }
|
||||
.iter-panel .m-block.tall { height: 24px; }
|
||||
.iter-panel .m-block.big { height: 40px; }
|
||||
|
||||
.iter-panel .nod {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 14px;
|
||||
width: 16px; height: 16px;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
.iter-panel .nod svg {
|
||||
width: 100%; height: 100%;
|
||||
stroke: var(--accent);
|
||||
fill: none;
|
||||
stroke-width: 2;
|
||||
}
|
||||
.iter-panel .ip-minutes {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 14px;
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* Rising curve visualization for bottom half */
|
||||
.curve-wrap {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 340px;
|
||||
height: 180px;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.curve-wrap svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
.curve-wrap .axis {
|
||||
stroke: var(--hairline);
|
||||
stroke-width: 1;
|
||||
fill: none;
|
||||
}
|
||||
.curve-wrap .curve-path {
|
||||
stroke: var(--accent);
|
||||
stroke-width: 2;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.curve-wrap .curve-dot {
|
||||
fill: var(--accent);
|
||||
r: 3;
|
||||
}
|
||||
.curve-wrap .curve-label {
|
||||
font-family: var(--mono);
|
||||
font-size: 9px;
|
||||
fill: var(--muted);
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
|
||||
/* ======================================================
|
||||
* BEAT 3 · Full comparison chart crossfade
|
||||
* ====================================================== */
|
||||
.final-chart {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 1280px;
|
||||
height: 620px;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
z-index: 60;
|
||||
}
|
||||
.final-chart svg {
|
||||
width: 100%; height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
.final-chart .axis {
|
||||
stroke: var(--hairline);
|
||||
stroke-width: 1;
|
||||
fill: none;
|
||||
}
|
||||
.final-chart .axis-label {
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
fill: var(--muted);
|
||||
letter-spacing: 0.16em;
|
||||
}
|
||||
.final-chart .tick-label {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
fill: var(--dim);
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
.final-chart .curve-a {
|
||||
stroke: var(--cool);
|
||||
stroke-width: 2;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.final-chart .curve-a-dash {
|
||||
stroke: var(--bad-strong);
|
||||
stroke-width: 2.5;
|
||||
fill: none;
|
||||
stroke-dasharray: 5 7;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
.final-chart .curve-b {
|
||||
stroke: var(--accent);
|
||||
stroke-width: 3;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.final-chart .curve-b-glow {
|
||||
stroke: var(--accent);
|
||||
stroke-width: 6;
|
||||
fill: none;
|
||||
opacity: 0.18;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.final-chart .curve-dot {
|
||||
fill: var(--accent);
|
||||
}
|
||||
.final-chart .fail-dot {
|
||||
fill: none;
|
||||
stroke: var(--bad-strong);
|
||||
stroke-width: 2.5;
|
||||
}
|
||||
.final-chart .cool-dot {
|
||||
fill: var(--cool);
|
||||
}
|
||||
.final-chart .anchor-label {
|
||||
font-family: var(--serif-zh);
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.final-chart .anchor-en {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* ======================================================
|
||||
* BRAND REVEAL — 统一动作
|
||||
* ====================================================== */
|
||||
.brand-sheet {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--cd-bg);
|
||||
transform: translateY(100%);
|
||||
will-change: transform;
|
||||
z-index: 80;
|
||||
}
|
||||
.brand-reveal {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 81;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.brand-reveal .wordmark {
|
||||
font-family: var(--sans);
|
||||
font-weight: 100;
|
||||
font-size: 128px;
|
||||
letter-spacing: -0.045em;
|
||||
color: var(--cd-ink);
|
||||
line-height: 1;
|
||||
}
|
||||
.brand-reveal .wordmark .accent { color: var(--accent-deep); }
|
||||
.brand-reveal .underline {
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: var(--accent);
|
||||
margin-top: 36px;
|
||||
will-change: width;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="stage" id="stage">
|
||||
<div class="mark">HUASHU · DESIGN</div>
|
||||
<div class="mark-right">V2 · 2026</div>
|
||||
|
||||
<div class="title-line" id="titleLine">w2 · 粗糙的第一版,好过完美的大招</div>
|
||||
|
||||
<!-- Splitter -->
|
||||
<div class="splitter" id="splitter"></div>
|
||||
<div class="splitter-label" id="splitterLabel">VS</div>
|
||||
|
||||
<!-- ============ TOP HALF: All-at-once ============ -->
|
||||
<div class="half-top" id="halfTop">
|
||||
<div class="half-label">
|
||||
<span class="tag">A</span>
|
||||
<span class="zh">闷头一把梭</span>
|
||||
<span style="font-family: var(--mono); color: var(--muted); letter-spacing: 0.18em; font-size: 11px; margin-left: auto;">ALL AT ONCE</span>
|
||||
</div>
|
||||
<div class="terminal-big">
|
||||
<div class="tty-head">
|
||||
<div class="d"></div><div class="d"></div><div class="d"></div>
|
||||
<div class="tty-title">designer@studio · 3h session</div>
|
||||
</div>
|
||||
<div class="tty-body">
|
||||
<div class="line" id="ttyL1"><span class="prompt">$</span>build final_design.html <span class="dim">// v1.0 · 一次做完</span></div>
|
||||
<div class="progress-row" id="progRow">
|
||||
<div class="progress-bar" id="progBar">
|
||||
<div class="progress-bar-fill" id="progFill"></div>
|
||||
</div>
|
||||
<span class="progress-pct" id="progPct">0%</span>
|
||||
<span class="progress-hours" id="progHours">03:00:00</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fail-stamp" id="failStamp">
|
||||
<svg viewBox="0 0 120 120">
|
||||
<circle cx="60" cy="60" r="52" fill="none" stroke="#A04A38" stroke-width="3"/>
|
||||
<path d="M 38 38 L 82 82 M 82 38 L 38 82" stroke="#A04A38" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<div class="stamp-text">REJECTED</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ BOTTOM HALF: Show early ============ -->
|
||||
<div class="half-bot" id="halfBot">
|
||||
<div class="half-label">
|
||||
<span class="tag">B</span>
|
||||
<span class="zh">尽早 show</span>
|
||||
<span style="font-family: var(--mono); color: var(--muted); letter-spacing: 0.18em; font-size: 11px; margin-left: auto;">SHOW EARLY</span>
|
||||
</div>
|
||||
<div class="iter-row">
|
||||
<div class="iter-panel" id="iter1">
|
||||
<div class="ip-head">
|
||||
<span>draft · v1</span>
|
||||
<span class="ip-version">15 min</span>
|
||||
</div>
|
||||
<div class="ip-body">
|
||||
<div class="m-block short"></div>
|
||||
<div class="m-block med"></div>
|
||||
<div class="m-block short"></div>
|
||||
</div>
|
||||
<div class="nod" id="nod1">
|
||||
<svg viewBox="0 0 16 16"><path d="M3 8 L7 12 L13 4"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="iter-panel" id="iter2">
|
||||
<div class="ip-head">
|
||||
<span>draft · v2</span>
|
||||
<span class="ip-version">25 min</span>
|
||||
</div>
|
||||
<div class="ip-body">
|
||||
<div class="m-block full tall"></div>
|
||||
<div class="m-block med"></div>
|
||||
<div class="m-block short"></div>
|
||||
<div class="m-block med accent"></div>
|
||||
</div>
|
||||
<div class="nod" id="nod2">
|
||||
<svg viewBox="0 0 16 16"><path d="M3 8 L7 12 L13 4"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="iter-panel" id="iter3">
|
||||
<div class="ip-head">
|
||||
<span>draft · v3</span>
|
||||
<span class="ip-version">35 min</span>
|
||||
</div>
|
||||
<div class="ip-body">
|
||||
<div class="m-block full big"></div>
|
||||
<div class="m-block full tall accent"></div>
|
||||
<div class="m-block med"></div>
|
||||
<div class="m-block full"></div>
|
||||
<div class="m-block short"></div>
|
||||
</div>
|
||||
<div class="nod" id="nod3">
|
||||
<svg viewBox="0 0 16 16"><path d="M3 8 L7 12 L13 4"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ Beat 3 · Final comparison chart ============ -->
|
||||
<div class="final-chart" id="finalChart">
|
||||
<svg viewBox="0 0 1280 620" preserveAspectRatio="xMidYMid meet">
|
||||
<!-- Axes -->
|
||||
<line class="axis" x1="110" y1="60" x2="110" y2="520"/>
|
||||
<line class="axis" x1="110" y1="520" x2="1200" y2="520"/>
|
||||
|
||||
<!-- Y-axis label -->
|
||||
<text class="axis-label" x="58" y="290" transform="rotate(-90 58 290)" text-anchor="middle">QUALITY</text>
|
||||
<!-- X-axis label -->
|
||||
<text class="axis-label" x="655" y="570" text-anchor="middle">TIME</text>
|
||||
|
||||
<!-- Tick marks -->
|
||||
<text class="tick-label" x="110" y="545" text-anchor="middle">0</text>
|
||||
<text class="tick-label" x="290" y="545" text-anchor="middle">15m</text>
|
||||
<text class="tick-label" x="480" y="545" text-anchor="middle">25m</text>
|
||||
<text class="tick-label" x="680" y="545" text-anchor="middle">35m</text>
|
||||
<text class="tick-label" x="1200" y="545" text-anchor="middle">3h</text>
|
||||
|
||||
<!-- Curve A (All-at-once): flat crawl near zero, late spike, then crash -->
|
||||
<!-- Narrative: 3 hours of silent work → finally reveal at 99% → rejected → drops -->
|
||||
<path class="curve-a" id="curveA"
|
||||
d="M 110 500 L 400 495 L 700 490 L 1000 485 L 1140 180" />
|
||||
<!-- Fall after rejection, red dashed -->
|
||||
<path class="curve-a-dash" id="curveACrash"
|
||||
d="M 1140 180 L 1200 510" />
|
||||
<circle class="fail-dot" id="failDot" cx="1140" cy="180" r="9"/>
|
||||
<!-- Small X marker on top of the fail dot -->
|
||||
<g id="failX" opacity="0">
|
||||
<line x1="1130" y1="170" x2="1150" y2="190" stroke="#C85A42" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<line x1="1150" y1="170" x2="1130" y2="190" stroke="#C85A42" stroke-width="2.5" stroke-linecap="round"/>
|
||||
</g>
|
||||
|
||||
<!-- Anchor for A (right side, top near the spike) -->
|
||||
<text class="anchor-label" x="1200" y="150" fill="#C85A42" text-anchor="end">闷头一把梭</text>
|
||||
<text class="anchor-en" x="1200" y="170" fill="#C85A42" text-anchor="end">REJECTED</text>
|
||||
|
||||
<!-- Curve B (Show early): steady step rise across first 35 min -->
|
||||
<path class="curve-b-glow" id="curveBGlow"
|
||||
d="M 110 500 L 290 380 L 480 270 L 680 140" />
|
||||
<path class="curve-b" id="curveB"
|
||||
d="M 110 500 L 290 380 L 480 270 L 680 140" />
|
||||
<circle class="curve-dot" cx="290" cy="380" r="6"/>
|
||||
<circle class="curve-dot" cx="480" cy="270" r="6"/>
|
||||
<circle class="curve-dot" cx="680" cy="140" r="8"/>
|
||||
|
||||
<!-- Anchor for B (above the peak dot on left-ish side) -->
|
||||
<text class="anchor-label" x="680" y="115" fill="#D97757" text-anchor="middle">尽早 show</text>
|
||||
<text class="anchor-en" x="680" y="96" fill="#D97757" text-anchor="middle">SHIPPED</text>
|
||||
|
||||
<!-- Legend hint: tiny label on A's plateau -->
|
||||
<text class="tick-label" x="555" y="477" text-anchor="middle" fill="rgba(255,255,255,0.3)" style="letter-spacing: 0.12em;">— 3 hours silence —</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Brand reveal -->
|
||||
<div class="brand-sheet" id="brandSheet"></div>
|
||||
<div class="brand-reveal" id="brandReveal">
|
||||
<div class="wordmark">huashu<span class="accent"> · </span>design</div>
|
||||
<div class="underline" id="brandUnderline"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Auto-scale stage
|
||||
function fitStage() {
|
||||
const stage = document.getElementById('stage');
|
||||
const sx = window.innerWidth / 1920;
|
||||
const sy = window.innerHeight / 1080;
|
||||
const s = Math.min(sx, sy);
|
||||
stage.style.transform = `translate(-50%, -50%) scale(${s})`;
|
||||
}
|
||||
fitStage();
|
||||
window.addEventListener('resize', fitStage);
|
||||
|
||||
// Easings
|
||||
const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
|
||||
const expoIn = t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
|
||||
const cubicInOut = t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||
const cubicOut = t => 1 - Math.pow(1 - t, 3);
|
||||
const cubicIn = t => t * t * t;
|
||||
|
||||
function lerp(t, a, b, easing) {
|
||||
if (t <= 0) return a;
|
||||
if (t >= 1) return b;
|
||||
const e = easing ? easing(t) : t;
|
||||
return a + (b - a) * e;
|
||||
}
|
||||
function seg(time, start, end) {
|
||||
if (time <= start) return 0;
|
||||
if (time >= end) return 1;
|
||||
return (time - start) / (end - start);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────
|
||||
// Timeline — total 12s (Beat 1: 0-2 · Beat 2: 2-10 · Beat 3: 10-12)
|
||||
//
|
||||
// 0.0-0.6 title + splitter grow
|
||||
// 0.6-1.4 two half-labels fade in (top first, then bot)
|
||||
// 1.4-2.0 top terminal line 1 types; bot panel 1 enters
|
||||
//
|
||||
// Top track (闷头):
|
||||
// 2.0-7.8 progress bar crawls from 0 to 99% (slow, painful)
|
||||
// 7.8-8.4 stuck at 99%
|
||||
// 8.4-8.9 fail stamp lands + bar turns red + bar drops to 0
|
||||
//
|
||||
// Bottom track (尽早):
|
||||
// 2.0-2.6 iter1 enters, nod1 appears @ 2.8
|
||||
// 3.6-4.2 iter2 enters, nod2 appears @ 4.4
|
||||
// 5.6-6.2 iter3 enters, nod3 appears @ 6.4 (final tick — biggest)
|
||||
//
|
||||
// 8.8-9.8 both halves dim; final chart crossfades in
|
||||
// (curves draw via stroke-dasharray)
|
||||
// 9.8-10.4 chart settles, anchor labels bloom
|
||||
// 10.0-12.0 brand reveal (sheet + wordmark + underline)
|
||||
// ────────────────────────────────────
|
||||
|
||||
const el = {
|
||||
title: document.getElementById('titleLine'),
|
||||
splitter: document.getElementById('splitter'),
|
||||
splitterLb: document.getElementById('splitterLabel'),
|
||||
halfTop: document.getElementById('halfTop'),
|
||||
halfBot: document.getElementById('halfBot'),
|
||||
ttyL1: document.getElementById('ttyL1'),
|
||||
progRow: document.getElementById('progRow'),
|
||||
progBar: document.getElementById('progBar'),
|
||||
progFill: document.getElementById('progFill'),
|
||||
progPct: document.getElementById('progPct'),
|
||||
progHours: document.getElementById('progHours'),
|
||||
failStamp: document.getElementById('failStamp'),
|
||||
iter1: document.getElementById('iter1'),
|
||||
iter2: document.getElementById('iter2'),
|
||||
iter3: document.getElementById('iter3'),
|
||||
nod1: document.getElementById('nod1'),
|
||||
nod2: document.getElementById('nod2'),
|
||||
nod3: document.getElementById('nod3'),
|
||||
finalChart: document.getElementById('finalChart'),
|
||||
brandSheet: document.getElementById('brandSheet'),
|
||||
brandReveal:document.getElementById('brandReveal'),
|
||||
brandUnder: document.getElementById('brandUnderline'),
|
||||
curveA: document.getElementById('curveA'),
|
||||
curveACrash:document.getElementById('curveACrash'),
|
||||
curveB: document.getElementById('curveB'),
|
||||
curveBGlow: document.getElementById('curveBGlow'),
|
||||
};
|
||||
|
||||
// Precompute path lengths for draw-on animation
|
||||
const lenA = el.curveA.getTotalLength();
|
||||
const lenACrash = el.curveACrash.getTotalLength();
|
||||
const lenB = el.curveB.getTotalLength();
|
||||
|
||||
el.curveA.style.strokeDasharray = `${lenA} ${lenA}`;
|
||||
el.curveA.style.strokeDashoffset = lenA;
|
||||
el.curveACrash.style.strokeDasharray = `${lenACrash} ${lenACrash}`;
|
||||
el.curveACrash.style.strokeDashoffset = lenACrash;
|
||||
el.curveB.style.strokeDasharray = `${lenB} ${lenB}`;
|
||||
el.curveB.style.strokeDashoffset = lenB;
|
||||
el.curveBGlow.style.strokeDasharray = `${lenB} ${lenB}`;
|
||||
el.curveBGlow.style.strokeDashoffset = lenB;
|
||||
|
||||
// Also precompute chart dot selections (hide initially)
|
||||
const chartDots = el.finalChart.querySelectorAll('circle');
|
||||
const chartAnchors = el.finalChart.querySelectorAll('.anchor-label, .anchor-en');
|
||||
const chartTicks = el.finalChart.querySelectorAll('.tick-label, .axis-label');
|
||||
|
||||
const DURATION = 12.0;
|
||||
let startTime = null;
|
||||
let loop = true;
|
||||
if (window.__recording === true) loop = false;
|
||||
|
||||
function tick(now) {
|
||||
if (startTime === null) startTime = now;
|
||||
let t = (now - startTime) / 1000;
|
||||
|
||||
if (t >= DURATION) {
|
||||
if (loop) { startTime = now; t = 0; }
|
||||
else { t = DURATION; }
|
||||
}
|
||||
|
||||
// ────── Title
|
||||
const titleIn = seg(t, 0.1, 1.0);
|
||||
const titleOut = seg(t, 9.2, 9.8);
|
||||
el.title.style.opacity = Math.max(0, Math.min(cubicOut(titleIn), 1 - titleOut));
|
||||
|
||||
// ────── Splitter (fade out earlier so Beat 3 is clean)
|
||||
const splitT = seg(t, 0.0, 0.8);
|
||||
const splitOut = seg(t, 8.4, 8.9);
|
||||
el.splitter.style.transform = `scaleX(${expoOut(splitT) * (1 - splitOut)})`;
|
||||
const splitLabelT = seg(t, 0.4, 1.0);
|
||||
const splitLabelOut = seg(t, 8.2, 8.7);
|
||||
el.splitterLb.style.opacity = Math.max(0, Math.min(cubicOut(splitLabelT), 1 - splitLabelOut));
|
||||
|
||||
// ────── Halves fade in / out (fade out earlier to clear for Beat 3 chart)
|
||||
const topIn = seg(t, 0.6, 1.4);
|
||||
const topOut = seg(t, 8.4, 9.0);
|
||||
el.halfTop.style.opacity = Math.max(0, Math.min(cubicOut(topIn), 1 - topOut));
|
||||
|
||||
const botIn = seg(t, 1.0, 1.8);
|
||||
const botOut = seg(t, 8.4, 9.0);
|
||||
el.halfBot.style.opacity = Math.max(0, Math.min(cubicOut(botIn), 1 - botOut));
|
||||
|
||||
// ────── TOP track: terminal line + progress bar
|
||||
const ttyL1In = seg(t, 1.4, 1.8);
|
||||
el.ttyL1.style.opacity = cubicOut(ttyL1In);
|
||||
|
||||
// Progress bar appears @ 1.8, starts crawling 2.0-7.8, stuck 7.8-8.4, fails @ 8.4
|
||||
const progRowIn = seg(t, 1.8, 2.2);
|
||||
el.progRow.style.opacity = cubicOut(progRowIn);
|
||||
|
||||
let pct = 0;
|
||||
let hoursTxt = '03:00:00';
|
||||
if (t >= 2.0 && t < 7.8) {
|
||||
const p = seg(t, 2.0, 7.8);
|
||||
// Easing: starts fast, slows down to 99% (mimics the "last 10% takes forever" trope)
|
||||
pct = 99 * (1 - Math.pow(1 - p, 2.2));
|
||||
const remaining = Math.max(0, (1 - p) * 3 * 60 * 60);
|
||||
const hh = String(Math.floor(remaining / 3600)).padStart(2, '0');
|
||||
const mm = String(Math.floor((remaining % 3600) / 60)).padStart(2, '0');
|
||||
const ss = String(Math.floor(remaining % 60)).padStart(2, '0');
|
||||
hoursTxt = `${hh}:${mm}:${ss}`;
|
||||
} else if (t >= 7.8 && t < 8.4) {
|
||||
pct = 99;
|
||||
// Micro-jitter to show "stuck"
|
||||
const jitter = Math.sin(t * 30) * 0.1;
|
||||
pct = 99 + jitter;
|
||||
hoursTxt = '00:00:12';
|
||||
} else if (t >= 8.4 && t < 8.7) {
|
||||
// Fail animation — pct stays at 99 briefly then snaps to 0
|
||||
pct = 99;
|
||||
hoursTxt = '— REJECTED —';
|
||||
} else if (t >= 8.7) {
|
||||
pct = 0;
|
||||
hoursTxt = '— REJECTED —';
|
||||
}
|
||||
|
||||
el.progFill.style.width = `${pct}%`;
|
||||
el.progPct.textContent = `${Math.floor(Math.max(0, pct))}%`;
|
||||
el.progHours.textContent = hoursTxt;
|
||||
|
||||
// Fail state toggle
|
||||
if (t >= 8.4) {
|
||||
el.progBar.classList.add('failed');
|
||||
el.progRow.classList.add('failed');
|
||||
} else {
|
||||
el.progBar.classList.remove('failed');
|
||||
el.progRow.classList.remove('failed');
|
||||
}
|
||||
|
||||
// Fail stamp lands at 8.4
|
||||
const stampIn = seg(t, 8.4, 8.7);
|
||||
if (stampIn > 0) {
|
||||
el.failStamp.style.opacity = cubicOut(stampIn);
|
||||
const scale = lerp(stampIn, 1.6, 1.0, expoOut);
|
||||
el.failStamp.style.transform = `translateY(-50%) rotate(-8deg) scale(${scale})`;
|
||||
} else {
|
||||
el.failStamp.style.opacity = 0;
|
||||
}
|
||||
|
||||
// ────── BOTTOM track: 3 iter panels
|
||||
const iterTimings = [
|
||||
{ enter: [2.0, 2.6], nod: [2.8, 3.2] },
|
||||
{ enter: [3.6, 4.2], nod: [4.4, 4.8] },
|
||||
{ enter: [5.6, 6.2], nod: [6.4, 6.9] },
|
||||
];
|
||||
|
||||
[el.iter1, el.iter2, el.iter3].forEach((panel, i) => {
|
||||
const { enter } = iterTimings[i];
|
||||
const p = seg(t, enter[0], enter[1]);
|
||||
const op = expoOut(p);
|
||||
const ty = lerp(p, 20, 0, expoOut);
|
||||
panel.style.opacity = op;
|
||||
panel.style.transform = `translateY(${ty}px)`;
|
||||
});
|
||||
|
||||
[el.nod1, el.nod2, el.nod3].forEach((n, i) => {
|
||||
const { nod } = iterTimings[i];
|
||||
const p = seg(t, nod[0], nod[1]);
|
||||
const op = expoOut(p);
|
||||
const scale = lerp(p, 0.4, 1.0, expoOut);
|
||||
n.style.opacity = op;
|
||||
n.style.transform = `scale(${scale})`;
|
||||
});
|
||||
|
||||
// ────── Beat 3 · final chart crossfade (chart appears as halves fade)
|
||||
const chartIn = seg(t, 8.5, 9.2);
|
||||
el.finalChart.style.opacity = cubicOut(chartIn);
|
||||
|
||||
// Curve B draws first (our hero path, 8.8-9.8), curve A follows (9.0-9.6 flat + spike)
|
||||
const curveBT = seg(t, 8.8, 9.8);
|
||||
el.curveB.style.strokeDashoffset = lenB * (1 - expoOut(curveBT));
|
||||
el.curveBGlow.style.strokeDashoffset = lenB * (1 - expoOut(curveBT));
|
||||
|
||||
const curveAT = seg(t, 8.9, 9.7);
|
||||
el.curveA.style.strokeDashoffset = lenA * (1 - cubicOut(curveAT));
|
||||
// Crash dash — only after curveA reaches peak AND the X lands
|
||||
const curveACrashT = seg(t, 9.7, 9.95);
|
||||
el.curveACrash.style.strokeDashoffset = lenACrash * (1 - expoOut(curveACrashT));
|
||||
// Fail X pops in right when curve A hits the spike
|
||||
const failXT = seg(t, 9.65, 9.85);
|
||||
const failXEl = document.getElementById('failX');
|
||||
if (failXEl) {
|
||||
failXEl.style.opacity = cubicOut(failXT);
|
||||
failXEl.style.transform = `scale(${lerp(failXT, 1.6, 1.0, expoOut)})`;
|
||||
failXEl.style.transformOrigin = '1140px 180px';
|
||||
}
|
||||
|
||||
// Dots fade in progressively (skip the fail-dot which is handled via X)
|
||||
chartDots.forEach((dot, i) => {
|
||||
// curve-dot for B (3 dots), fail-dot (1 dot)
|
||||
const dotT = seg(t, 9.0 + i * 0.12, 9.3 + i * 0.12);
|
||||
dot.style.opacity = cubicOut(dotT);
|
||||
});
|
||||
chartAnchors.forEach((a) => {
|
||||
const aT = seg(t, 9.5, 9.95);
|
||||
a.style.opacity = cubicOut(aT);
|
||||
});
|
||||
chartTicks.forEach((tk) => {
|
||||
const tkT = seg(t, 8.7, 9.3);
|
||||
tk.style.opacity = cubicOut(tkT) * 0.9;
|
||||
});
|
||||
|
||||
// ────── Brand reveal 10.0-12.0
|
||||
const sheetT = seg(t, 10.0, 10.6);
|
||||
el.brandSheet.style.transform = `translateY(${lerp(sheetT, 100, 0, expoOut)}%)`;
|
||||
|
||||
const wordT = seg(t, 10.6, 11.4);
|
||||
el.brandReveal.style.opacity = cubicOut(wordT);
|
||||
|
||||
const underT = seg(t, 11.4, 11.9);
|
||||
el.brandUnder.style.width = `${lerp(underT, 0, 280, expoOut)}px`;
|
||||
|
||||
// Mark ready for recorder
|
||||
if (!window.__ready) window.__ready = true;
|
||||
|
||||
if (loop || t < DURATION) requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
(document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve())
|
||||
.then(() => requestAnimationFrame(tick));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
647
.claude/skills/huashu-design/demos/w3-fallback-advisor-en.html
Normal file
647
.claude/skills/huashu-design/demos/w3-fallback-advisor-en.html
Normal file
@@ -0,0 +1,647 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>w3 · Fallback Advisor (English)</title>
|
||||
<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=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Inter:wght@200;300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #000000;
|
||||
--ink: #FFFFFF;
|
||||
--ink-80: rgba(255,255,255,0.82);
|
||||
--ink-60: rgba(255,255,255,0.58);
|
||||
--muted: rgba(255,255,255,0.40);
|
||||
--dim: rgba(255,255,255,0.18);
|
||||
--hairline: rgba(255,255,255,0.12);
|
||||
--accent: #D97757;
|
||||
--accent-deep: #B85D3D;
|
||||
--cd-bg: #F5F4F0;
|
||||
--cd-ink: #1A1918;
|
||||
|
||||
--serif-en: "Source Serif 4", Georgia, serif;
|
||||
--sans: "Inter", -apple-system, system-ui, sans-serif;
|
||||
--mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
|
||||
}
|
||||
html, body {
|
||||
margin: 0; padding: 0;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
font-family: var(--sans);
|
||||
color: var(--ink);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
.stage {
|
||||
position: fixed;
|
||||
top: 50%; left: 50%;
|
||||
width: 1920px; height: 1080px;
|
||||
transform-origin: center center;
|
||||
background: var(--bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Watermarks */
|
||||
.watermark-tl {
|
||||
position: absolute;
|
||||
top: 40px; left: 56px;
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.2em;
|
||||
color: rgba(255,255,255,0.16);
|
||||
z-index: 200;
|
||||
pointer-events: none;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.watermark-br {
|
||||
position: absolute;
|
||||
bottom: 32px; right: 40px;
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.24em;
|
||||
color: rgba(255,255,255,0.14);
|
||||
z-index: 200;
|
||||
pointer-events: none;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Top title — English uses Serif Display */
|
||||
.top-title {
|
||||
position: absolute;
|
||||
top: 82px; left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-family: var(--serif-en);
|
||||
font-weight: 300;
|
||||
font-size: 46px;
|
||||
font-style: italic;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--ink-80);
|
||||
text-align: center;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
z-index: 120;
|
||||
line-height: 1.12;
|
||||
}
|
||||
.top-title .accent { color: var(--accent); font-style: italic; }
|
||||
|
||||
.sub-caption {
|
||||
position: absolute;
|
||||
top: 148px; left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-family: var(--sans);
|
||||
font-weight: 300;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.34em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
z-index: 120;
|
||||
}
|
||||
|
||||
/* Philosophy wall */
|
||||
.wall-viewport {
|
||||
position: absolute;
|
||||
top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 1480px;
|
||||
height: 760px;
|
||||
perspective: 2400px;
|
||||
perspective-origin: 50% 50%;
|
||||
will-change: transform, opacity, filter;
|
||||
}
|
||||
.wall-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-template-rows: repeat(4, 1fr);
|
||||
gap: 18px;
|
||||
transform: rotateX(10deg) rotateY(-6deg);
|
||||
transform-style: preserve-3d;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
.cell {
|
||||
position: relative;
|
||||
background: #0f0f0f;
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform, filter;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
.cell .glyph {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
.cell .name {
|
||||
position: relative;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--muted);
|
||||
z-index: 2;
|
||||
align-self: flex-end;
|
||||
}
|
||||
.cell .num {
|
||||
position: relative;
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
color: var(--dim);
|
||||
letter-spacing: 0.1em;
|
||||
z-index: 2;
|
||||
}
|
||||
.cell.selected {
|
||||
border-color: var(--accent);
|
||||
background: #1a0f0a;
|
||||
}
|
||||
.cell.selected .name { color: var(--accent); }
|
||||
|
||||
/* Scan light */
|
||||
.scan-light {
|
||||
position: absolute;
|
||||
left: -5%;
|
||||
right: -5%;
|
||||
top: -15%;
|
||||
height: 200px;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(217, 119, 87, 0) 0%,
|
||||
rgba(217, 119, 87, 0.18) 40%,
|
||||
rgba(255, 220, 200, 0.45) 50%,
|
||||
rgba(217, 119, 87, 0.18) 60%,
|
||||
rgba(217, 119, 87, 0) 100%
|
||||
);
|
||||
filter: blur(8px);
|
||||
z-index: 80;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Foreground 3 cards */
|
||||
.fg-row {
|
||||
position: absolute;
|
||||
top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
gap: 56px;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
z-index: 100;
|
||||
}
|
||||
.fg-card {
|
||||
width: 440px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
opacity: 0;
|
||||
transform: translateZ(-800px) scale(0.4);
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
.fg-card .card-body {
|
||||
background: #0f0f0f;
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 12px;
|
||||
padding: 32px 30px;
|
||||
box-shadow:
|
||||
0 30px 80px -20px rgba(217,119,87,0.25),
|
||||
0 10px 30px -10px rgba(0,0,0,0.6);
|
||||
}
|
||||
.fg-card .label {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.18em;
|
||||
color: var(--accent);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.fg-card .title-main {
|
||||
font-family: var(--serif-en);
|
||||
font-style: italic;
|
||||
font-size: 40px;
|
||||
font-weight: 300;
|
||||
letter-spacing: -0.01em;
|
||||
line-height: 1.08;
|
||||
color: var(--ink);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.fg-card .title-sub {
|
||||
font-family: var(--sans);
|
||||
font-weight: 300;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-60);
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
.fg-card .feature {
|
||||
font-family: var(--sans);
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.03em;
|
||||
color: var(--muted);
|
||||
line-height: 1.6;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid var(--hairline);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.fg-card .thumb-wrap {
|
||||
margin-top: 14px;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
background: #0a0a0a;
|
||||
border: 1px solid var(--hairline);
|
||||
opacity: 0;
|
||||
will-change: opacity, height;
|
||||
}
|
||||
.fg-card .thumb-wrap img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Brand reveal */
|
||||
.brand-panel {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--cd-bg);
|
||||
opacity: 0;
|
||||
transform: translateY(100%);
|
||||
will-change: opacity, transform;
|
||||
z-index: 300;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
.brand-mark {
|
||||
font-family: var(--serif-en);
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
font-size: 112px;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--cd-ink);
|
||||
opacity: 0;
|
||||
transform: scale(0.92);
|
||||
will-change: opacity, transform;
|
||||
line-height: 1;
|
||||
}
|
||||
.brand-mark .dot { color: var(--accent); font-style: normal; padding: 0 6px; }
|
||||
.brand-mark .accent { color: var(--accent); font-style: italic; }
|
||||
.brand-underline {
|
||||
margin-top: 34px;
|
||||
height: 2px;
|
||||
width: 0;
|
||||
background: var(--accent);
|
||||
will-change: width;
|
||||
}
|
||||
.brand-tag {
|
||||
margin-top: 22px;
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.32em;
|
||||
color: rgba(26,25,24,0.54);
|
||||
text-transform: uppercase;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="stage" id="stage">
|
||||
|
||||
<div class="watermark-tl">HUASHU · DESIGN</div>
|
||||
<div class="watermark-br">V2 · 2026 · w3</div>
|
||||
|
||||
<!-- English version: parallel rewrite, fewer words, more breathing room -->
|
||||
<div class="top-title" id="topTitle">
|
||||
Not sure? <span class="accent">Here are 3 roads.</span>
|
||||
</div>
|
||||
<div class="sub-caption" id="subCaption">20 Philosophies · 3 Directions</div>
|
||||
|
||||
<div class="scan-light" id="scanLight"></div>
|
||||
|
||||
<div class="wall-viewport" id="wallViewport">
|
||||
<div class="wall-grid" id="wallGrid">
|
||||
<!-- 20 cells injected by JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fg-row" id="fgRow">
|
||||
<div class="fg-card" id="card1">
|
||||
<div class="card-body">
|
||||
<div class="label">Road 01 · Eastern Space</div>
|
||||
<div class="title-main">Kenya Hara</div>
|
||||
<div class="title-sub">Ma / Emptiness</div>
|
||||
<div class="feature">Terracotta · Vast whitespace · Paper grain</div>
|
||||
</div>
|
||||
<div class="thumb-wrap" id="thumb1">
|
||||
<img src="demo-takram.png" alt="demo takram" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="fg-card" id="card2">
|
||||
<div class="card-body">
|
||||
<div class="label">Road 02 · Information Architecture</div>
|
||||
<div class="title-main">Pentagram</div>
|
||||
<div class="title-sub">Grid / Rigor</div>
|
||||
<div class="feature">Strict grid · High contrast · Editorial</div>
|
||||
</div>
|
||||
<div class="thumb-wrap" id="thumb2">
|
||||
<img src="demo-pentagram.png" alt="demo pentagram" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="fg-card" id="card3">
|
||||
<div class="card-body">
|
||||
<div class="label">Road 03 · Experimental Edge</div>
|
||||
<div class="title-main">David Carson</div>
|
||||
<div class="title-sub">Raw / Punk</div>
|
||||
<div class="feature">Broken type · Brutal geometry · Visual shock</div>
|
||||
</div>
|
||||
<div class="thumb-wrap" id="thumb3">
|
||||
<img src="demo-build.png" alt="demo build" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="brand-panel" id="brandPanel">
|
||||
<div class="brand-mark" id="brandMark">huashu<span class="dot">·</span><span class="accent">design</span></div>
|
||||
<div class="brand-underline" id="brandUnderline"></div>
|
||||
<div class="brand-tag" id="brandTag">HTML as Designer's Medium</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
function scaleStage(){
|
||||
const stage = document.getElementById('stage');
|
||||
const sx = window.innerWidth / 1920;
|
||||
const sy = window.innerHeight / 1080;
|
||||
const s = Math.min(sx, sy);
|
||||
stage.style.transform = `translate(-50%, -50%) scale(${s})`;
|
||||
}
|
||||
window.addEventListener('resize', scaleStage);
|
||||
scaleStage();
|
||||
|
||||
// 20 philosophies — identical structure to zh.html (designer names are brand identifiers, kept as-is)
|
||||
const PHILOSOPHIES = [
|
||||
{ name: 'Pentagram', glyph: 'grid' },
|
||||
{ name: 'M. Vignelli', glyph: 'bars' },
|
||||
{ name: 'Apple HIG', glyph: 'radius' },
|
||||
{ name: 'Spin', glyph: 'slash' },
|
||||
{ name: 'Build', glyph: 'type' },
|
||||
{ name: 'Field.io', glyph: 'wave' },
|
||||
{ name: 'Active Theory',glyph: 'orbit' },
|
||||
{ name: 'Hi-Res!', glyph: 'dots' },
|
||||
{ name: 'Locomotive', glyph: 'arrow' },
|
||||
{ name: 'Takram', glyph: 'circle' },
|
||||
{ name: 'Kenya Hara', glyph: 'ma' },
|
||||
{ name: 'D. Rams', glyph: 'square' },
|
||||
{ name: 'J. Ive', glyph: 'arc' },
|
||||
{ name: 'J. Morrison', glyph: 'minimal' },
|
||||
{ name: 'S. Ogata', glyph: 'line' },
|
||||
{ name: 'D. Carson', glyph: 'collage' },
|
||||
{ name: 'S. Sagmeister',glyph: 'stamp' },
|
||||
{ name: 'P. Scher', glyph: 'poster' },
|
||||
{ name: 'M. Glaser', glyph: 'heart' },
|
||||
{ name: 'K. Sato', glyph: 'logo' },
|
||||
];
|
||||
const SELECTED = [10, 0, 15];
|
||||
|
||||
function makeGlyph(kind){
|
||||
const svgs = {
|
||||
grid: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1" fill="none">
|
||||
<rect x="6" y="8" width="28" height="18"/><rect x="38" y="8" width="28" height="18"/><rect x="70" y="8" width="24" height="44"/>
|
||||
<rect x="6" y="30" width="60" height="22"/></g></svg>`,
|
||||
bars: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g fill="rgba(255,255,255,0.22)">
|
||||
<rect x="10" y="40" width="8" height="16"/><rect x="22" y="28" width="8" height="28"/><rect x="34" y="16" width="8" height="40"/>
|
||||
<rect x="46" y="24" width="8" height="32"/><rect x="58" y="10" width="8" height="46"/><rect x="70" y="34" width="8" height="22"/>
|
||||
<rect x="82" y="22" width="8" height="34"/></g></svg>`,
|
||||
radius: `<svg viewBox="0 0 100 60" width="72%" height="58%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1.2" fill="none">
|
||||
<rect x="14" y="10" width="72" height="40" rx="20" ry="20"/></g></svg>`,
|
||||
slash: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1.4" fill="none" stroke-linecap="square">
|
||||
<path d="M 14 50 L 52 10"/><path d="M 36 50 L 74 10"/><path d="M 58 50 L 86 22"/></g></svg>`,
|
||||
type: `<svg viewBox="0 0 100 60" width="78%" height="62%"><text x="50" y="42" text-anchor="middle" font-family="Source Serif 4, serif" font-size="40" font-style="italic" fill="rgba(255,255,255,0.22)">Aa</text></svg>`,
|
||||
wave: `<svg viewBox="0 0 100 60" width="82%" height="62%"><path d="M 6 30 Q 20 8, 34 30 T 62 30 T 90 30" stroke="rgba(255,255,255,0.22)" stroke-width="1.3" fill="none"/></svg>`,
|
||||
orbit: `<svg viewBox="0 0 100 60" width="74%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1.1" fill="none"><ellipse cx="50" cy="30" rx="36" ry="14"/><ellipse cx="50" cy="30" rx="14" ry="22"/><circle cx="50" cy="30" r="2" fill="rgba(255,255,255,0.32)"/></g></svg>`,
|
||||
dots: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g fill="rgba(255,255,255,0.22)"><circle cx="14" cy="18" r="2"/><circle cx="30" cy="18" r="2"/><circle cx="46" cy="18" r="2"/><circle cx="62" cy="18" r="2"/><circle cx="78" cy="18" r="2"/><circle cx="14" cy="30" r="2"/><circle cx="30" cy="30" r="2"/><circle cx="46" cy="30" r="3"/><circle cx="62" cy="30" r="2"/><circle cx="78" cy="30" r="2"/><circle cx="14" cy="42" r="2"/><circle cx="30" cy="42" r="2"/><circle cx="46" cy="42" r="2"/><circle cx="62" cy="42" r="2"/><circle cx="78" cy="42" r="2"/></g></svg>`,
|
||||
arrow: `<svg viewBox="0 0 100 60" width="78%" height="52%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1.2" fill="none" stroke-linecap="square"><path d="M 14 30 L 80 30"/><path d="M 68 18 L 82 30 L 68 42"/></g></svg>`,
|
||||
circle: `<svg viewBox="0 0 100 60" width="62%" height="62%"><circle cx="50" cy="30" r="22" stroke="rgba(255,255,255,0.22)" stroke-width="1.2" fill="none"/></svg>`,
|
||||
ma: `<svg viewBox="0 0 100 60" width="72%" height="62%"><g fill="none" stroke="rgba(255,255,255,0.22)" stroke-width="0.9"><rect x="18" y="14" width="64" height="32"/></g><circle cx="50" cy="30" r="1.4" fill="rgba(255,255,255,0.32)"/></svg>`,
|
||||
square: `<svg viewBox="0 0 100 60" width="62%" height="62%"><rect x="30" y="10" width="40" height="40" stroke="rgba(255,255,255,0.22)" stroke-width="1.2" fill="none"/></svg>`,
|
||||
arc: `<svg viewBox="0 0 100 60" width="78%" height="62%"><path d="M 14 46 Q 50 6, 86 46" stroke="rgba(255,255,255,0.22)" stroke-width="1.3" fill="none"/></svg>`,
|
||||
minimal: `<svg viewBox="0 0 100 60" width="78%" height="32%"><line x1="18" y1="30" x2="82" y2="30" stroke="rgba(255,255,255,0.22)" stroke-width="1.2"/></svg>`,
|
||||
line: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="0.9" fill="none"><line x1="14" y1="16" x2="86" y2="16"/><line x1="14" y1="30" x2="86" y2="30"/><line x1="14" y1="44" x2="60" y2="44"/></g></svg>`,
|
||||
collage: `<svg viewBox="0 0 100 60" width="82%" height="62%"><g fill="none" stroke="rgba(255,255,255,0.22)" stroke-width="1"><rect x="8" y="8" width="24" height="18" transform="rotate(-8 20 17)"/><rect x="36" y="18" width="28" height="20" transform="rotate(5 50 28)"/><rect x="60" y="6" width="32" height="24" transform="rotate(-4 76 18)"/></g><text x="50" y="56" text-anchor="middle" font-family="Source Serif 4, serif" font-size="14" font-style="italic" fill="rgba(255,255,255,0.3)">RAY</text></svg>`,
|
||||
stamp: `<svg viewBox="0 0 100 60" width="70%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1.2" fill="none"><circle cx="50" cy="30" r="22"/><text x="50" y="35" text-anchor="middle" font-family="Source Serif 4" font-size="16" font-weight="500" fill="rgba(255,255,255,0.3)">S</text></g></svg>`,
|
||||
poster: `<svg viewBox="0 0 100 60" width="82%" height="62%"><g fill="rgba(255,255,255,0.22)"><rect x="8" y="8" width="22" height="44"/><rect x="34" y="8" width="22" height="44"/><rect x="60" y="8" width="22" height="44"/></g></svg>`,
|
||||
heart: `<svg viewBox="0 0 100 60" width="58%" height="58%"><path d="M 50 48 C 30 32, 18 20, 30 14 C 40 10, 50 22, 50 22 C 50 22, 60 10, 70 14 C 82 20, 70 32, 50 48 Z" fill="rgba(217,119,87,0.28)"/></svg>`,
|
||||
logo: `<svg viewBox="0 0 100 60" width="60%" height="60%"><circle cx="50" cy="30" r="20" stroke="rgba(255,255,255,0.22)" stroke-width="1.3" fill="none"/><circle cx="50" cy="30" r="6" fill="rgba(255,255,255,0.22)"/></svg>`,
|
||||
};
|
||||
return svgs[kind] || svgs.minimal;
|
||||
}
|
||||
|
||||
const wallGrid = document.getElementById('wallGrid');
|
||||
PHILOSOPHIES.forEach((p, idx) => {
|
||||
const cell = document.createElement('div');
|
||||
cell.className = 'cell';
|
||||
cell.dataset.idx = idx;
|
||||
const row = Math.floor(idx / 5);
|
||||
const col = idx % 5;
|
||||
const dr = row - 1.5;
|
||||
const dc = col - 2;
|
||||
const dist = Math.sqrt(dr * dr + dc * dc);
|
||||
cell.dataset.dist = dist.toFixed(3);
|
||||
cell.innerHTML = `
|
||||
<div class="glyph">${makeGlyph(p.glyph)}</div>
|
||||
<div class="num">${String(idx + 1).padStart(2, '0')}</div>
|
||||
<div class="name">${p.name}</div>
|
||||
`;
|
||||
wallGrid.appendChild(cell);
|
||||
});
|
||||
|
||||
const cells = Array.from(wallGrid.querySelectorAll('.cell'));
|
||||
const maxDist = Math.max(...cells.map(c => parseFloat(c.dataset.dist)));
|
||||
|
||||
const T_TOTAL = 12.0;
|
||||
const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
|
||||
const cubicInOut = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2;
|
||||
const cubicOut = t => 1 - Math.pow(1 - t, 3);
|
||||
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||
const clamp01 = v => clamp(v, 0, 1);
|
||||
const lerp = (a, b, t) => a + (b - a) * t;
|
||||
|
||||
const topTitle = document.getElementById('topTitle');
|
||||
const subCap = document.getElementById('subCaption');
|
||||
const wallViewport = document.getElementById('wallViewport');
|
||||
const scanLight = document.getElementById('scanLight');
|
||||
const fgRow = document.getElementById('fgRow');
|
||||
const card1 = document.getElementById('card1');
|
||||
const card2 = document.getElementById('card2');
|
||||
const card3 = document.getElementById('card3');
|
||||
const thumb1 = document.getElementById('thumb1');
|
||||
const thumb2 = document.getElementById('thumb2');
|
||||
const thumb3 = document.getElementById('thumb3');
|
||||
const brandPanel = document.getElementById('brandPanel');
|
||||
const brandMark = document.getElementById('brandMark');
|
||||
const brandUnderline = document.getElementById('brandUnderline');
|
||||
const brandTag = document.getElementById('brandTag');
|
||||
|
||||
function tick(t){
|
||||
t = Math.max(0, Math.min(T_TOTAL, t));
|
||||
|
||||
// Ripple in 20 cells
|
||||
const rippleStart = 0.15;
|
||||
cells.forEach(cell => {
|
||||
const d = parseFloat(cell.dataset.dist);
|
||||
const delay = (d / maxDist) * 0.85;
|
||||
const cellT = clamp01((t - rippleStart - delay * 0.55) / 0.7);
|
||||
const eased = expoOut(cellT);
|
||||
const idx = parseInt(cell.dataset.idx, 10);
|
||||
const isSel = SELECTED.includes(idx);
|
||||
cell.style.opacity = (eased * (isSel ? 1.0 : 0.85)).toFixed(3);
|
||||
const ty = lerp(30, 0, eased);
|
||||
const scale = lerp(0.88, 1, eased);
|
||||
cell.style.transform = `translateY(${ty}px) scale(${scale})`;
|
||||
});
|
||||
|
||||
// Scan light
|
||||
const scanStart = 2.6;
|
||||
const scanEnd = 4.0;
|
||||
const scanT = clamp01((t - scanStart) / (scanEnd - scanStart));
|
||||
if (scanT > 0 && scanT < 1) {
|
||||
scanLight.style.opacity = Math.min(1, Math.sin(scanT * Math.PI) * 1.3).toFixed(3);
|
||||
const py = lerp(-180, 820, cubicInOut(scanT));
|
||||
scanLight.style.transform = `translateY(${py}px)`;
|
||||
} else {
|
||||
scanLight.style.opacity = 0;
|
||||
}
|
||||
|
||||
// Light up selected, dim others
|
||||
const lightStart = 4.0;
|
||||
const lightEnd = 4.8;
|
||||
const lightT = clamp01((t - lightStart) / (lightEnd - lightStart));
|
||||
const lightE = expoOut(lightT);
|
||||
cells.forEach(cell => {
|
||||
const idx = parseInt(cell.dataset.idx, 10);
|
||||
const isSel = SELECTED.includes(idx);
|
||||
if (isSel) {
|
||||
cell.classList.toggle('selected', lightT > 0.05);
|
||||
} else {
|
||||
if (t >= lightStart) {
|
||||
const dimmedOpacity = lerp(0.85, 0.08, lightE);
|
||||
cell.style.opacity = dimmedOpacity.toFixed(3);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Foreground cards break out
|
||||
const breakStart = 4.8;
|
||||
if (t >= breakStart - 0.1) fgRow.style.opacity = 1;
|
||||
else fgRow.style.opacity = 0;
|
||||
|
||||
[card1, card2, card3].forEach((card, i) => {
|
||||
const stagger = i * 0.18;
|
||||
const cT = clamp01((t - breakStart - stagger) / 0.85);
|
||||
const cE = expoOut(cT);
|
||||
card.style.opacity = cE.toFixed(3);
|
||||
const tz = lerp(-800, 0, cE);
|
||||
const sc = lerp(0.45, 1, cE);
|
||||
const ty = lerp(40, 0, cE);
|
||||
card.style.transform = `translateZ(${tz}px) scale(${sc}) translateY(${ty}px)`;
|
||||
});
|
||||
|
||||
// Dim wall background
|
||||
if (t >= breakStart) {
|
||||
const dimT = clamp01((t - breakStart) / 0.9);
|
||||
const dimE = expoOut(dimT);
|
||||
wallViewport.style.opacity = lerp(1, 0.25, dimE).toFixed(3);
|
||||
wallViewport.style.filter = `blur(${lerp(0, 6, dimE).toFixed(1)}px)`;
|
||||
} else {
|
||||
wallViewport.style.opacity = 1;
|
||||
wallViewport.style.filter = 'blur(0px)';
|
||||
}
|
||||
|
||||
// Demo thumbnails grow
|
||||
const thumbStart = 6.6;
|
||||
[thumb1, thumb2, thumb3].forEach((thumb, i) => {
|
||||
const stagger = i * 0.32;
|
||||
const ttT = clamp01((t - thumbStart - stagger) / 1.0);
|
||||
const ttE = cubicOut(ttT);
|
||||
thumb.style.opacity = ttE.toFixed(3);
|
||||
const h = lerp(0, 250, ttE);
|
||||
thumb.style.height = `${h}px`;
|
||||
});
|
||||
|
||||
// Top title fade
|
||||
const titleStart = 7.2;
|
||||
const titleT = clamp01((t - titleStart) / 0.9);
|
||||
const titleE = cubicOut(titleT);
|
||||
topTitle.style.opacity = titleE.toFixed(3);
|
||||
topTitle.style.transform = `translateX(-50%) translateY(${lerp(-14, 0, titleE)}px)`;
|
||||
subCap.style.opacity = (titleE * 0.95).toFixed(3);
|
||||
|
||||
// Brand reveal
|
||||
const brandStart = 9.8;
|
||||
const panelT = clamp01((t - brandStart) / 0.7);
|
||||
const panelE = expoOut(panelT);
|
||||
brandPanel.style.opacity = panelE.toFixed(3);
|
||||
brandPanel.style.transform = `translateY(${lerp(100, 0, panelE)}%)`;
|
||||
|
||||
const markStart = 10.3;
|
||||
const markT = clamp01((t - markStart) / 0.6);
|
||||
const markE = expoOut(markT);
|
||||
brandMark.style.opacity = markE.toFixed(3);
|
||||
brandMark.style.transform = `scale(${lerp(0.92, 1, markE)})`;
|
||||
|
||||
const ulStart = 10.7;
|
||||
const ulT = clamp01((t - ulStart) / 0.55);
|
||||
brandUnderline.style.width = `${lerp(0, 280, expoOut(ulT))}px`;
|
||||
|
||||
const tagStart = 11.1;
|
||||
const tagT = clamp01((t - tagStart) / 0.5);
|
||||
brandTag.style.opacity = cubicOut(tagT).toFixed(3);
|
||||
}
|
||||
|
||||
window.__ready = false;
|
||||
window.__duration = T_TOTAL;
|
||||
let startTime = null;
|
||||
let paused = false;
|
||||
const recording = window.__recording === true;
|
||||
|
||||
function loop(now){
|
||||
if (paused) return;
|
||||
if (startTime === null) startTime = now;
|
||||
const t = (now - startTime) / 1000;
|
||||
tick(t);
|
||||
if (t < T_TOTAL) requestAnimationFrame(loop);
|
||||
else if (!recording) { startTime = now; requestAnimationFrame(loop); }
|
||||
}
|
||||
|
||||
tick(0);
|
||||
window.__ready = true;
|
||||
requestAnimationFrame(loop);
|
||||
|
||||
window.__pause = function(){ paused = true; };
|
||||
window.__resume = function(){
|
||||
if (!paused) return;
|
||||
paused = false; startTime = null;
|
||||
requestAnimationFrame(loop);
|
||||
};
|
||||
window.__setTime = function(t){ paused = true; tick(t); };
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
704
.claude/skills/huashu-design/demos/w3-fallback-advisor.html
Normal file
704
.claude/skills/huashu-design/demos/w3-fallback-advisor.html
Normal file
@@ -0,0 +1,704 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-Hans">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>w3 · Fallback Advisor(中文版)</title>
|
||||
<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=Source+Serif+4:ital,opsz,wght@0,8..60,300..700&family=Noto+Serif+SC:wght@300;400;500;600&family=Inter:wght@200;300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #000000;
|
||||
--ink: #FFFFFF;
|
||||
--ink-80: rgba(255,255,255,0.82);
|
||||
--ink-60: rgba(255,255,255,0.58);
|
||||
--muted: rgba(255,255,255,0.40);
|
||||
--dim: rgba(255,255,255,0.18);
|
||||
--hairline: rgba(255,255,255,0.12);
|
||||
--accent: #D97757;
|
||||
--accent-deep: #B85D3D;
|
||||
--cd-bg: #F5F4F0;
|
||||
--cd-ink: #1A1918;
|
||||
|
||||
--serif-cn: "Noto Serif SC", "Songti SC", serif;
|
||||
--serif-en: "Source Serif 4", Georgia, serif;
|
||||
--sans: "Inter", -apple-system, "PingFang SC", system-ui, sans-serif;
|
||||
--mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
|
||||
}
|
||||
html, body {
|
||||
margin: 0; padding: 0;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
font-family: var(--sans);
|
||||
color: var(--ink);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
.stage {
|
||||
position: fixed;
|
||||
top: 50%; left: 50%;
|
||||
width: 1920px; height: 1080px;
|
||||
transform-origin: center center;
|
||||
background: var(--bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ============ Watermark ============ */
|
||||
.watermark-tl {
|
||||
position: absolute;
|
||||
top: 40px; left: 56px;
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.2em;
|
||||
color: rgba(255,255,255,0.16);
|
||||
z-index: 200;
|
||||
pointer-events: none;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.watermark-br {
|
||||
position: absolute;
|
||||
bottom: 32px; right: 40px;
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.24em;
|
||||
color: rgba(255,255,255,0.14);
|
||||
z-index: 200;
|
||||
pointer-events: none;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* ============ Top Title ============ */
|
||||
.top-title {
|
||||
position: absolute;
|
||||
top: 88px; left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-family: var(--serif-cn);
|
||||
font-weight: 300;
|
||||
font-size: 42px;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--ink-80);
|
||||
text-align: center;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
z-index: 120;
|
||||
}
|
||||
.top-title .accent { color: var(--accent); font-weight: 400; }
|
||||
|
||||
.sub-caption {
|
||||
position: absolute;
|
||||
top: 148px; left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-family: var(--sans);
|
||||
font-weight: 300;
|
||||
font-size: 15px;
|
||||
letter-spacing: 0.32em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
z-index: 120;
|
||||
}
|
||||
|
||||
/* ============ Philosophy Wall (4 rows × 5 cols) ============ */
|
||||
.wall-viewport {
|
||||
position: absolute;
|
||||
top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 1480px;
|
||||
height: 760px;
|
||||
perspective: 2400px;
|
||||
perspective-origin: 50% 50%;
|
||||
will-change: transform, opacity, filter;
|
||||
}
|
||||
.wall-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-template-rows: repeat(4, 1fr);
|
||||
gap: 18px;
|
||||
transform: rotateX(10deg) rotateY(-6deg);
|
||||
transform-style: preserve-3d;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
.cell {
|
||||
position: relative;
|
||||
background: #0f0f0f;
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform, filter;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
/* abstract glyph per cell — geometric, no imagery */
|
||||
.cell .glyph {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
.cell .name {
|
||||
position: relative;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--muted);
|
||||
z-index: 2;
|
||||
align-self: flex-end;
|
||||
}
|
||||
.cell .num {
|
||||
position: relative;
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
color: var(--dim);
|
||||
letter-spacing: 0.1em;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Selected cells — lit up */
|
||||
.cell.selected {
|
||||
border-color: var(--accent);
|
||||
background: #1a0f0a;
|
||||
}
|
||||
.cell.selected .name { color: var(--accent); }
|
||||
|
||||
/* ============ Scan light ============ */
|
||||
.scan-light {
|
||||
position: absolute;
|
||||
left: -5%;
|
||||
right: -5%;
|
||||
top: -15%;
|
||||
height: 200px;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(217, 119, 87, 0) 0%,
|
||||
rgba(217, 119, 87, 0.18) 40%,
|
||||
rgba(255, 220, 200, 0.45) 50%,
|
||||
rgba(217, 119, 87, 0.18) 60%,
|
||||
rgba(217, 119, 87, 0) 100%
|
||||
);
|
||||
filter: blur(8px);
|
||||
z-index: 80;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ============ Foreground 3 cards ============ */
|
||||
.fg-row {
|
||||
position: absolute;
|
||||
top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
gap: 56px;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
z-index: 100;
|
||||
}
|
||||
.fg-card {
|
||||
width: 440px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
opacity: 0;
|
||||
transform: translateZ(-800px) scale(0.4);
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
.fg-card .card-body {
|
||||
background: #0f0f0f;
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 12px;
|
||||
padding: 32px 30px;
|
||||
box-shadow:
|
||||
0 30px 80px -20px rgba(217,119,87,0.25),
|
||||
0 10px 30px -10px rgba(0,0,0,0.6);
|
||||
}
|
||||
.fg-card .label {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.18em;
|
||||
color: var(--accent);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.fg-card .title-cn {
|
||||
font-family: var(--serif-cn);
|
||||
font-size: 36px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.01em;
|
||||
line-height: 1.15;
|
||||
color: var(--ink);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.fg-card .title-en {
|
||||
font-family: var(--serif-en);
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
font-size: 17px;
|
||||
letter-spacing: 0.01em;
|
||||
color: var(--ink-60);
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
.fg-card .feature {
|
||||
font-family: var(--sans);
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--muted);
|
||||
line-height: 1.6;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid var(--hairline);
|
||||
}
|
||||
.fg-card .thumb-wrap {
|
||||
margin-top: 14px;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
background: #0a0a0a;
|
||||
border: 1px solid var(--hairline);
|
||||
opacity: 0;
|
||||
will-change: opacity, height;
|
||||
}
|
||||
.fg-card .thumb-wrap img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ============ Brand Reveal (米色盖层) ============ */
|
||||
.brand-panel {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--cd-bg);
|
||||
opacity: 0;
|
||||
transform: translateY(100%);
|
||||
will-change: opacity, transform;
|
||||
z-index: 300;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
.brand-mark {
|
||||
font-family: var(--serif-en);
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
font-size: 112px;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--cd-ink);
|
||||
opacity: 0;
|
||||
transform: scale(0.92);
|
||||
will-change: opacity, transform;
|
||||
line-height: 1;
|
||||
}
|
||||
.brand-mark .accent { color: var(--accent); font-style: italic; }
|
||||
.brand-mark .dot { color: var(--accent); font-style: normal; padding: 0 6px; }
|
||||
.brand-underline {
|
||||
margin-top: 34px;
|
||||
height: 2px;
|
||||
width: 0;
|
||||
background: var(--accent);
|
||||
will-change: width;
|
||||
}
|
||||
.brand-tag {
|
||||
margin-top: 22px;
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.32em;
|
||||
color: rgba(26,25,24,0.54);
|
||||
text-transform: uppercase;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="stage" id="stage">
|
||||
|
||||
<!-- 水印 -->
|
||||
<div class="watermark-tl">HUASHU · DESIGN</div>
|
||||
<div class="watermark-br">V2 · 2026 · w3</div>
|
||||
|
||||
<!-- 顶部标题 -->
|
||||
<div class="top-title" id="topTitle">
|
||||
不知道要什么?<span class="accent">先给你 3 个方向</span>
|
||||
</div>
|
||||
<div class="sub-caption" id="subCaption">20 Philosophies · 3 Directions</div>
|
||||
|
||||
<!-- 扫描光 -->
|
||||
<div class="scan-light" id="scanLight"></div>
|
||||
|
||||
<!-- 4×5 哲学墙 -->
|
||||
<div class="wall-viewport" id="wallViewport">
|
||||
<div class="wall-grid" id="wallGrid">
|
||||
<!-- 20 cells injected by JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 前景 3 张方向卡 -->
|
||||
<div class="fg-row" id="fgRow">
|
||||
<!-- card 1: Kenya Hara · 东方极简 -->
|
||||
<div class="fg-card" id="card1">
|
||||
<div class="card-body">
|
||||
<div class="label">方向 01 · 东方空间</div>
|
||||
<div class="title-cn">原研哉式留白</div>
|
||||
<div class="title-en">Kenya Hara</div>
|
||||
<div class="feature">赤土橙 · 大量留白 · 宣纸质感</div>
|
||||
</div>
|
||||
<div class="thumb-wrap" id="thumb1">
|
||||
<img src="demo-takram.png" alt="demo takram" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- card 2: Pentagram · 信息建筑 -->
|
||||
<div class="fg-card" id="card2">
|
||||
<div class="card-body">
|
||||
<div class="label">方向 02 · 信息建筑</div>
|
||||
<div class="title-cn">Pentagram 秩序</div>
|
||||
<div class="title-en">Pentagram</div>
|
||||
<div class="feature">强网格 · 高对比 · 理性版式</div>
|
||||
</div>
|
||||
<div class="thumb-wrap" id="thumb2">
|
||||
<img src="demo-pentagram.png" alt="demo pentagram" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- card 3: David Carson · 实验先锋 -->
|
||||
<div class="fg-card" id="card3">
|
||||
<div class="card-body">
|
||||
<div class="label">方向 03 · 实验先锋</div>
|
||||
<div class="title-cn">David Carson 式</div>
|
||||
<div class="title-en">Experimental Edge</div>
|
||||
<div class="feature">破格排印 · 粗野几何 · 视觉冲击</div>
|
||||
</div>
|
||||
<div class="thumb-wrap" id="thumb3">
|
||||
<img src="demo-build.png" alt="demo build" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Brand Reveal -->
|
||||
<div class="brand-panel" id="brandPanel">
|
||||
<div class="brand-mark" id="brandMark">huashu<span class="dot">·</span><span class="accent">design</span></div>
|
||||
<div class="brand-underline" id="brandUnderline"></div>
|
||||
<div class="brand-tag" id="brandTag">HTML as Designer's Medium</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
// ============ Stage auto-scale ============
|
||||
function scaleStage(){
|
||||
const stage = document.getElementById('stage');
|
||||
const sx = window.innerWidth / 1920;
|
||||
const sy = window.innerHeight / 1080;
|
||||
const s = Math.min(sx, sy);
|
||||
stage.style.transform = `translate(-50%, -50%) scale(${s})`;
|
||||
}
|
||||
window.addEventListener('resize', scaleStage);
|
||||
scaleStage();
|
||||
|
||||
// ============ 20 Philosophies ============
|
||||
// 4 rows × 5 cols = 20. Selected: idx 0 (Pentagram), idx 9 (Kenya Hara), idx 12 (David Carson)
|
||||
const PHILOSOPHIES = [
|
||||
// row 1 — 信息建筑派
|
||||
{ name: 'Pentagram', glyph: 'grid' },
|
||||
{ name: 'M. Vignelli', glyph: 'bars' },
|
||||
{ name: 'Apple HIG', glyph: 'radius' },
|
||||
{ name: 'Spin', glyph: 'slash' },
|
||||
{ name: 'Build', glyph: 'type' },
|
||||
// row 2 — 运动诗学派
|
||||
{ name: 'Field.io', glyph: 'wave' },
|
||||
{ name: 'Active Theory',glyph: 'orbit' },
|
||||
{ name: 'Hi-Res!', glyph: 'dots' },
|
||||
{ name: 'Locomotive', glyph: 'arrow' },
|
||||
{ name: 'Takram', glyph: 'circle' },
|
||||
// row 3 — 极简/东方
|
||||
{ name: 'Kenya Hara', glyph: 'ma' },
|
||||
{ name: 'D. Rams', glyph: 'square' },
|
||||
{ name: 'J. Ive', glyph: 'arc' },
|
||||
{ name: 'J. Morrison', glyph: 'minimal' },
|
||||
{ name: 'S. Ogata', glyph: 'line' },
|
||||
// row 4 — 实验 & 海报
|
||||
{ name: 'D. Carson', glyph: 'collage' },
|
||||
{ name: 'S. Sagmeister',glyph: 'stamp' },
|
||||
{ name: 'P. Scher', glyph: 'poster' },
|
||||
{ name: 'M. Glaser', glyph: 'heart' },
|
||||
{ name: 'K. Sato', glyph: 'logo' },
|
||||
];
|
||||
|
||||
// selected indices — 3 differentiated directions
|
||||
const SELECTED = [10, 0, 15]; // Kenya Hara, Pentagram, David Carson
|
||||
|
||||
function makeGlyph(kind){
|
||||
// Simple geometric SVG glyphs — one per cell, no real logos
|
||||
const svgs = {
|
||||
grid: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1" fill="none">
|
||||
<rect x="6" y="8" width="28" height="18"/><rect x="38" y="8" width="28" height="18"/><rect x="70" y="8" width="24" height="44"/>
|
||||
<rect x="6" y="30" width="60" height="22"/></g></svg>`,
|
||||
bars: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g fill="rgba(255,255,255,0.22)">
|
||||
<rect x="10" y="40" width="8" height="16"/><rect x="22" y="28" width="8" height="28"/><rect x="34" y="16" width="8" height="40"/>
|
||||
<rect x="46" y="24" width="8" height="32"/><rect x="58" y="10" width="8" height="46"/><rect x="70" y="34" width="8" height="22"/>
|
||||
<rect x="82" y="22" width="8" height="34"/></g></svg>`,
|
||||
radius: `<svg viewBox="0 0 100 60" width="72%" height="58%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1.2" fill="none">
|
||||
<rect x="14" y="10" width="72" height="40" rx="20" ry="20"/></g></svg>`,
|
||||
slash: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1.4" fill="none" stroke-linecap="square">
|
||||
<path d="M 14 50 L 52 10"/><path d="M 36 50 L 74 10"/><path d="M 58 50 L 86 22"/></g></svg>`,
|
||||
type: `<svg viewBox="0 0 100 60" width="78%" height="62%"><text x="50" y="42" text-anchor="middle" font-family="Source Serif 4, serif" font-size="40" font-style="italic" fill="rgba(255,255,255,0.22)">Aa</text></svg>`,
|
||||
wave: `<svg viewBox="0 0 100 60" width="82%" height="62%"><path d="M 6 30 Q 20 8, 34 30 T 62 30 T 90 30" stroke="rgba(255,255,255,0.22)" stroke-width="1.3" fill="none"/></svg>`,
|
||||
orbit: `<svg viewBox="0 0 100 60" width="74%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1.1" fill="none"><ellipse cx="50" cy="30" rx="36" ry="14"/><ellipse cx="50" cy="30" rx="14" ry="22"/><circle cx="50" cy="30" r="2" fill="rgba(255,255,255,0.32)"/></g></svg>`,
|
||||
dots: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g fill="rgba(255,255,255,0.22)"><circle cx="14" cy="18" r="2"/><circle cx="30" cy="18" r="2"/><circle cx="46" cy="18" r="2"/><circle cx="62" cy="18" r="2"/><circle cx="78" cy="18" r="2"/><circle cx="14" cy="30" r="2"/><circle cx="30" cy="30" r="2"/><circle cx="46" cy="30" r="3"/><circle cx="62" cy="30" r="2"/><circle cx="78" cy="30" r="2"/><circle cx="14" cy="42" r="2"/><circle cx="30" cy="42" r="2"/><circle cx="46" cy="42" r="2"/><circle cx="62" cy="42" r="2"/><circle cx="78" cy="42" r="2"/></g></svg>`,
|
||||
arrow: `<svg viewBox="0 0 100 60" width="78%" height="52%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1.2" fill="none" stroke-linecap="square"><path d="M 14 30 L 80 30"/><path d="M 68 18 L 82 30 L 68 42"/></g></svg>`,
|
||||
circle: `<svg viewBox="0 0 100 60" width="62%" height="62%"><circle cx="50" cy="30" r="22" stroke="rgba(255,255,255,0.22)" stroke-width="1.2" fill="none"/></svg>`,
|
||||
ma: `<svg viewBox="0 0 100 60" width="72%" height="62%"><g fill="none" stroke="rgba(255,255,255,0.22)" stroke-width="0.9"><rect x="18" y="14" width="64" height="32"/></g><circle cx="50" cy="30" r="1.4" fill="rgba(255,255,255,0.32)"/></svg>`,
|
||||
square: `<svg viewBox="0 0 100 60" width="62%" height="62%"><rect x="30" y="10" width="40" height="40" stroke="rgba(255,255,255,0.22)" stroke-width="1.2" fill="none"/></svg>`,
|
||||
arc: `<svg viewBox="0 0 100 60" width="78%" height="62%"><path d="M 14 46 Q 50 6, 86 46" stroke="rgba(255,255,255,0.22)" stroke-width="1.3" fill="none"/></svg>`,
|
||||
minimal: `<svg viewBox="0 0 100 60" width="78%" height="32%"><line x1="18" y1="30" x2="82" y2="30" stroke="rgba(255,255,255,0.22)" stroke-width="1.2"/></svg>`,
|
||||
line: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="0.9" fill="none"><line x1="14" y1="16" x2="86" y2="16"/><line x1="14" y1="30" x2="86" y2="30"/><line x1="14" y1="44" x2="60" y2="44"/></g></svg>`,
|
||||
collage: `<svg viewBox="0 0 100 60" width="82%" height="62%"><g fill="none" stroke="rgba(255,255,255,0.22)" stroke-width="1"><rect x="8" y="8" width="24" height="18" transform="rotate(-8 20 17)"/><rect x="36" y="18" width="28" height="20" transform="rotate(5 50 28)"/><rect x="60" y="6" width="32" height="24" transform="rotate(-4 76 18)"/></g><text x="50" y="56" text-anchor="middle" font-family="Source Serif 4, serif" font-size="14" font-style="italic" fill="rgba(255,255,255,0.3)">RAY</text></svg>`,
|
||||
stamp: `<svg viewBox="0 0 100 60" width="70%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1.2" fill="none"><circle cx="50" cy="30" r="22"/><text x="50" y="35" text-anchor="middle" font-family="Source Serif 4" font-size="16" font-weight="500" fill="rgba(255,255,255,0.3)">S</text></g></svg>`,
|
||||
poster: `<svg viewBox="0 0 100 60" width="82%" height="62%"><g fill="rgba(255,255,255,0.22)"><rect x="8" y="8" width="22" height="44"/><rect x="34" y="8" width="22" height="44"/><rect x="60" y="8" width="22" height="44"/></g></svg>`,
|
||||
heart: `<svg viewBox="0 0 100 60" width="58%" height="58%"><path d="M 50 48 C 30 32, 18 20, 30 14 C 40 10, 50 22, 50 22 C 50 22, 60 10, 70 14 C 82 20, 70 32, 50 48 Z" fill="rgba(217,119,87,0.28)"/></svg>`,
|
||||
logo: `<svg viewBox="0 0 100 60" width="60%" height="60%"><circle cx="50" cy="30" r="20" stroke="rgba(255,255,255,0.22)" stroke-width="1.3" fill="none"/><circle cx="50" cy="30" r="6" fill="rgba(255,255,255,0.22)"/></svg>`,
|
||||
};
|
||||
return svgs[kind] || svgs.minimal;
|
||||
}
|
||||
|
||||
// Build the wall
|
||||
const wallGrid = document.getElementById('wallGrid');
|
||||
PHILOSOPHIES.forEach((p, idx) => {
|
||||
const cell = document.createElement('div');
|
||||
cell.className = 'cell';
|
||||
cell.dataset.idx = idx;
|
||||
const row = Math.floor(idx / 5);
|
||||
const col = idx % 5;
|
||||
// precompute distance from grid center (2, 1.5)
|
||||
const dr = row - 1.5;
|
||||
const dc = col - 2;
|
||||
const dist = Math.sqrt(dr * dr + dc * dc);
|
||||
cell.dataset.dist = dist.toFixed(3);
|
||||
cell.innerHTML = `
|
||||
<div class="glyph">${makeGlyph(p.glyph)}</div>
|
||||
<div class="num">${String(idx + 1).padStart(2, '0')}</div>
|
||||
<div class="name">${p.name}</div>
|
||||
`;
|
||||
wallGrid.appendChild(cell);
|
||||
});
|
||||
|
||||
const cells = Array.from(wallGrid.querySelectorAll('.cell'));
|
||||
const maxDist = Math.max(...cells.map(c => parseFloat(c.dataset.dist)));
|
||||
|
||||
// ============ Timeline ============
|
||||
const T_TOTAL = 12.0; // seconds (flow type w)
|
||||
const fps = 25;
|
||||
const frameDur = 1 / fps;
|
||||
|
||||
// Easing
|
||||
const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
|
||||
const expoIn = t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
|
||||
const cubicInOut = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2;
|
||||
const cubicOut = t => 1 - Math.pow(1 - t, 3);
|
||||
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||
const clamp01 = v => clamp(v, 0, 1);
|
||||
const lerp = (a, b, t) => a + (b - a) * t;
|
||||
|
||||
// Element refs
|
||||
const topTitle = document.getElementById('topTitle');
|
||||
const subCap = document.getElementById('subCaption');
|
||||
const wallViewport = document.getElementById('wallViewport');
|
||||
const wallGridEl = wallGrid;
|
||||
const scanLight = document.getElementById('scanLight');
|
||||
const fgRow = document.getElementById('fgRow');
|
||||
const card1 = document.getElementById('card1');
|
||||
const card2 = document.getElementById('card2');
|
||||
const card3 = document.getElementById('card3');
|
||||
const thumb1 = document.getElementById('thumb1');
|
||||
const thumb2 = document.getElementById('thumb2');
|
||||
const thumb3 = document.getElementById('thumb3');
|
||||
const brandPanel = document.getElementById('brandPanel');
|
||||
const brandMark = document.getElementById('brandMark');
|
||||
const brandUnderline = document.getElementById('brandUnderline');
|
||||
const brandTag = document.getElementById('brandTag');
|
||||
|
||||
function tick(t){
|
||||
// Clamp
|
||||
t = Math.max(0, Math.min(T_TOTAL, t));
|
||||
|
||||
// ========== Phase 1: 0 - 2.5s — Ripple in 20 cells ==========
|
||||
const rippleStart = 0.15;
|
||||
const rippleSpan = 1.8;
|
||||
cells.forEach(cell => {
|
||||
const d = parseFloat(cell.dataset.dist);
|
||||
// delay scaled by distance-from-center (hero v10 formula)
|
||||
const delay = (d / maxDist) * 0.85;
|
||||
const cellT = clamp01((t - rippleStart - delay * 0.55) / 0.7);
|
||||
const eased = expoOut(cellT);
|
||||
const idx = parseInt(cell.dataset.idx, 10);
|
||||
const isSel = SELECTED.includes(idx);
|
||||
cell.style.opacity = (eased * (isSel ? 1.0 : 0.85)).toFixed(3);
|
||||
const ty = lerp(30, 0, eased);
|
||||
const scale = lerp(0.88, 1, eased);
|
||||
cell.style.transform = `translateY(${ty}px) scale(${scale})`;
|
||||
});
|
||||
|
||||
// ========== Phase 2: 2.5 - 4.0s — scan light sweeps down ==========
|
||||
const scanStart = 2.6;
|
||||
const scanEnd = 4.0;
|
||||
const scanT = clamp01((t - scanStart) / (scanEnd - scanStart));
|
||||
if (scanT > 0 && scanT < 1) {
|
||||
scanLight.style.opacity = Math.min(1, Math.sin(scanT * Math.PI) * 1.3).toFixed(3);
|
||||
// travel from top to bottom across the wall (-150 to 860px within wallViewport-ish)
|
||||
const py = lerp(-180, 820, cubicInOut(scanT));
|
||||
scanLight.style.transform = `translateY(${py}px)`;
|
||||
} else {
|
||||
scanLight.style.opacity = 0;
|
||||
}
|
||||
|
||||
// ========== Phase 3: 4.0 - 4.8s — 3 cells light up, others dim ==========
|
||||
const lightStart = 4.0;
|
||||
const lightEnd = 4.8;
|
||||
const lightT = clamp01((t - lightStart) / (lightEnd - lightStart));
|
||||
const lightE = expoOut(lightT);
|
||||
cells.forEach(cell => {
|
||||
const idx = parseInt(cell.dataset.idx, 10);
|
||||
const isSel = SELECTED.includes(idx);
|
||||
if (isSel) {
|
||||
cell.classList.toggle('selected', lightT > 0.05);
|
||||
} else {
|
||||
// dim non-selected from 0.85 → 0.08
|
||||
const base = 0.85;
|
||||
const dimmedOpacity = lerp(base, 0.08, lightE);
|
||||
// only override after ripple is done
|
||||
if (t >= lightStart) {
|
||||
cell.style.opacity = dimmedOpacity.toFixed(3);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ========== Phase 4: 4.8 - 6.5s — 3 cells break out to foreground ==========
|
||||
// We don't literally move the wall cells; we fade in fg-cards "bursting from the wall"
|
||||
const breakStart = 4.8;
|
||||
const breakEnd = 6.5;
|
||||
const breakT = clamp01((t - breakStart) / (breakEnd - breakStart));
|
||||
const breakE = expoOut(breakT);
|
||||
|
||||
if (t >= breakStart - 0.1) {
|
||||
fgRow.style.opacity = 1;
|
||||
} else {
|
||||
fgRow.style.opacity = 0;
|
||||
}
|
||||
|
||||
[card1, card2, card3].forEach((card, i) => {
|
||||
const stagger = i * 0.18; // pop × 3 staggered
|
||||
const cT = clamp01((t - breakStart - stagger) / 0.85);
|
||||
const cE = expoOut(cT);
|
||||
card.style.opacity = cE.toFixed(3);
|
||||
// Z-rush: from translateZ(-800) to 0, scale 0.4 → 1
|
||||
const tz = lerp(-800, 0, cE);
|
||||
const sc = lerp(0.45, 1, cE);
|
||||
const ty = lerp(40, 0, cE);
|
||||
card.style.transform = `translateZ(${tz}px) scale(${sc}) translateY(${ty}px)`;
|
||||
});
|
||||
|
||||
// Dim the wall (behind) when cards come forward
|
||||
if (t >= breakStart) {
|
||||
const dimT = clamp01((t - breakStart) / 0.9);
|
||||
const dimE = expoOut(dimT);
|
||||
wallViewport.style.opacity = lerp(1, 0.25, dimE).toFixed(3);
|
||||
wallViewport.style.filter = `blur(${lerp(0, 6, dimE).toFixed(1)}px)`;
|
||||
} else {
|
||||
wallViewport.style.opacity = 1;
|
||||
wallViewport.style.filter = 'blur(0px)';
|
||||
}
|
||||
|
||||
// ========== Phase 5: 6.5 - 9.5s — thumbnails grow below each card ==========
|
||||
const thumbStart = 6.6;
|
||||
const thumbs = [thumb1, thumb2, thumb3];
|
||||
thumbs.forEach((thumb, i) => {
|
||||
const stagger = i * 0.32;
|
||||
const ttT = clamp01((t - thumbStart - stagger) / 1.0);
|
||||
const ttE = cubicOut(ttT);
|
||||
thumb.style.opacity = ttE.toFixed(3);
|
||||
// height from 0 to 250px
|
||||
const h = lerp(0, 250, ttE);
|
||||
thumb.style.height = `${h}px`;
|
||||
});
|
||||
|
||||
// ========== Top title fade in 7.2 - 8.0 ==========
|
||||
const titleStart = 7.2;
|
||||
const titleT = clamp01((t - titleStart) / 0.9);
|
||||
const titleE = cubicOut(titleT);
|
||||
topTitle.style.opacity = titleE.toFixed(3);
|
||||
topTitle.style.transform = `translateX(-50%) translateY(${lerp(-14, 0, titleE)}px)`;
|
||||
subCap.style.opacity = (titleE * 0.95).toFixed(3);
|
||||
|
||||
// ========== Phase 6: 9.8 - 12.0s — Brand Reveal ==========
|
||||
const brandStart = 9.8;
|
||||
const panelT = clamp01((t - brandStart) / 0.7);
|
||||
const panelE = expoOut(panelT);
|
||||
brandPanel.style.opacity = panelE.toFixed(3);
|
||||
brandPanel.style.transform = `translateY(${lerp(100, 0, panelE)}%)`;
|
||||
|
||||
const markStart = 10.3;
|
||||
const markT = clamp01((t - markStart) / 0.6);
|
||||
const markE = expoOut(markT);
|
||||
brandMark.style.opacity = markE.toFixed(3);
|
||||
brandMark.style.transform = `scale(${lerp(0.92, 1, markE)})`;
|
||||
|
||||
const ulStart = 10.7;
|
||||
const ulT = clamp01((t - ulStart) / 0.55);
|
||||
brandUnderline.style.width = `${lerp(0, 280, expoOut(ulT))}px`;
|
||||
|
||||
const tagStart = 11.1;
|
||||
const tagT = clamp01((t - tagStart) / 0.5);
|
||||
brandTag.style.opacity = cubicOut(tagT).toFixed(3);
|
||||
}
|
||||
|
||||
// ============ Animation loop ============
|
||||
window.__ready = false;
|
||||
window.__duration = T_TOTAL;
|
||||
let startTime = null;
|
||||
let paused = false;
|
||||
const recording = window.__recording === true;
|
||||
|
||||
function loop(now){
|
||||
if (paused) return;
|
||||
if (startTime === null) startTime = now;
|
||||
const t = (now - startTime) / 1000;
|
||||
tick(t);
|
||||
if (t < T_TOTAL) {
|
||||
requestAnimationFrame(loop);
|
||||
} else if (!recording) {
|
||||
startTime = now;
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
}
|
||||
|
||||
// First-frame sync BEFORE requesting next frame
|
||||
tick(0);
|
||||
window.__ready = true;
|
||||
requestAnimationFrame(loop);
|
||||
|
||||
// Pause raf loop — tests & recorder call this before seeking
|
||||
window.__pause = function(){ paused = true; };
|
||||
window.__resume = function(){
|
||||
if (!paused) return;
|
||||
paused = false;
|
||||
startTime = null;
|
||||
requestAnimationFrame(loop);
|
||||
};
|
||||
// Expose for video recorder (scripts/render-video.js uses __setTime)
|
||||
window.__setTime = function(t){ paused = true; tick(t); };
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user