chore: ruler files update

Signed-off-by: Dmytro Stanchiev <git@dmytros.dev>
This commit is contained in:
2026-05-24 21:03:49 -04:00
parent 97b3ddd653
commit abb472c83d
303 changed files with 46670 additions and 25369 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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>

View 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>

View 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 &#8594; 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 &#8470; 05 <span class="orange">&#183; AI Benchmarks</span> &#183; 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">&#183; Q2</span></div>
<div class="big">Claude 4.7</div>
<div class="sub">Sonnet, 1M ctx &#183; 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">&#183; 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 &amp; JetBrains Mono</span>
<span class="folio">P. 05</span>
<span>Data &#183; 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 &#183; 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>

View 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 &amp; 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>

View 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>

View 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>

File diff suppressed because it is too large Load Diff

View File

@@ -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.md60000 多个项目用,","absoluteStart":26.5,"absoluteEnd":31.5},
{"text":"AWS、Anthropic、Google、微软、OpenAIAI 半壁江山一起捐进 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 ArtifactsHTML 已升级为可交互、能拉实时数据的 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 }}>阅读 · &lt; 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>

View File

@@ -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、微软、OpenAIAI 半壁江山一起捐进 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 ArtifactsHTML 已经从静态产物升级成可以交互、能拉实时数据的 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。
工具替你处理切换。
立场可以放下了。

View 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 一次能记住多少个这样的小块。

View 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>

View 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>

View 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>

View 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 · 尽早 showsmall 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&nbsp;HOUR&nbsp;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&nbsp;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>

View 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 · 尽早 showsmall 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&nbsp;AT&nbsp;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&nbsp;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>

View 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>

View 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>