Initial: pi-skill — 68 skills, 43 extensions, 11 themes for Pi

This commit is contained in:
Kunthawat Greethong
2026-05-25 16:38:02 +07:00
commit 69f7d8bdda
1689 changed files with 342427 additions and 0 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>

989
skills/demos/c4-tweaks.html Normal file
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,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>