feat: V2 重制 9 个演示 GIF + 新增英文版 + hero 升级到 v10

亮点:
- 统一 VI:全部对齐 hero-v10 色板 (#000000 + #D97757) 和 Brand Reveal 米色面板 signature
- 文字克制:每个 demo 控制在 20-48 字 / 15-36 词,从解释性文字转为锚点
- 中英平行:英文版不是字幕翻译,画面元素重新设计
- 高清 GIF:1280×720 · 25fps · full palette · sierra2_4a dither
- Concept-driven:每个 demo 都有清晰的核心传达隐喻,哑剧测试可过

文件变更:
- 9 个能力/流程 demo 全部 V2 重做(c1-c6, w1-w3),中英双版
- hero animation 从 v9 升级到 v10(中英双版)
- README.md 引用更新 v9 → v10
- README.en.md 所有 GIF 加 -en 后缀 + note 更新
- 移除冗余:20 个 -60fps.mp4(README 未引用)+ 18 个 demo MP4

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
alchainandClaude Opus 4.7 committed 2026-04-21 09:42:24 +08:00
1 parent b876d634ee
commit fed775c447
22 files changed
+19505 -6576

No files matched your search

+15 -15
View File
@@ -27,22 +27,22 @@ npx skills add alchaincyf/huashu-design
[See it work](#demo-gallery) · [Install](#install) · [What it does](#what-it-does) · [How it works](#core-mechanics) · [vs. Claude Design](#vs-claude-design)
> 📖 **Note for English readers**: this skill is built by a Chinese-speaking developer for a Chinese-speaking audience. The skill's agent prompts (`SKILL.md`, `references/*.md`) and all demo assets below are in **Chinese** — they work with English tasks just fine (the agent is bilingual), but the visible text in screenshots/GIFs is Chinese. The core design principles translate universally.
> 📖 **Note for English readers**: this skill is built by a Chinese-speaking developer. The skill's agent prompts (`SKILL.md`, `references/*.md`) are in Chinese but the agent is bilingual — works fine with English tasks. The demos below are the English parallel versions; the Chinese ones are in the default-named files (see the Chinese [README.md](README.md)).
</div>
---
<p align="center">
<video src="demos/hero-animation-v9.mp4" autoplay muted loop playsinline width="100%">
Your browser doesn't support inline video. <a href="demos/hero-animation-v9.mp4">Download MP4</a>.
<video src="demos/hero-animation-v10-en.mp4" autoplay muted loop playsinline width="100%">
Your browser doesn't support inline video. <a href="demos/hero-animation-v10-en.mp4">Download MP4</a>.
</video>
</p>
<p align="center"><sub>▲ 10-second hero animation showing what huashu-design does (<a href="demos/hero-animation-v9.mp4">download MP4</a> if autoplay doesn't work)</sub></p>
<p align="center"><sub>▲ 10-second hero animation showing what huashu-design does (<a href="demos/hero-animation-v10-en.mp4">download MP4</a> if autoplay doesn't work)</sub></p>
<p align="center">
<img src="demos/w3-fallback-advisor.gif" alt="Fallback design advisor · 3 differentiated directions from 20 design philosophies" width="100%">
<img src="demos/w3-fallback-advisor-en.gif" alt="Fallback design advisor · 3 differentiated directions from 20 design philosophies" width="100%">
</p>
---
@@ -82,61 +82,61 @@ No buttons, no panels, no Figma plugin. Agent-agnostic — drops into Claude Cod
## Demo Gallery
> The demos below use Chinese text in UI but the mechanics apply to any language.
> English parallel versions of the demos. Chinese versions live at the default filenames (see the Chinese README).
### Design Direction Advisor
The fallback for vague briefs: pick 3 differentiated directions from 5 schools × 20 philosophies, generate all 3 demos in parallel, let the user choose.
<p align="center"><img src="demos/w3-fallback-advisor.gif" width="100%"></p>
<p align="center"><img src="demos/w3-fallback-advisor-en.gif" width="100%"></p>
### iOS App Prototype
Pixel-accurate iPhone 15 Pro body (Dynamic Island / status bar / Home Indicator) · state-driven multi-screen navigation · real images pulled from Wikimedia/Met/Unsplash · Playwright click tests before delivery.
<p align="center"><img src="demos/c1-ios-prototype.gif" width="100%"></p>
<p align="center"><img src="demos/c1-ios-prototype-en.gif" width="100%"></p>
### Motion Design Engine
Stage + Sprite time-slice model · `useTime` / `useSprite` / `interpolate` / `Easing` — four APIs cover every animation need · one command exports MP4 / GIF / 60fps-interpolated / BGM-scored finals.
<p align="center"><img src="demos/c3-motion-design.gif" width="100%"></p>
<p align="center"><img src="demos/c3-motion-design-en.gif" width="100%"></p>
### HTML Slides → Editable PPTX
HTML decks for browser presentation · `html2pptx.js` reads DOM computed styles and translates each element into real PowerPoint objects · exports are **actual text frames**, not image-bed fakes.
<p align="center"><img src="demos/c2-slides-pptx.gif" width="100%"></p>
<p align="center"><img src="demos/c2-slides-pptx-en.gif" width="100%"></p>
### Tweaks · Live Variation Switching
Colors / typography / information density parameterized · side panel toggle · pure-frontend + `localStorage` persistence · survives reload.
<p align="center"><img src="demos/c4-tweaks.gif" width="100%"></p>
<p align="center"><img src="demos/c4-tweaks-en.gif" width="100%"></p>
### Infographic / Data Viz
Magazine-grade typography · precise CSS Grid columns · `text-wrap: pretty` typographic details · driven by real data · exports to vector PDF / 300dpi PNG / SVG.
<p align="center"><img src="demos/c5-infographic.gif" width="100%"></p>
<p align="center"><img src="demos/c5-infographic-en.gif" width="100%"></p>
### 5-Dimension Expert Critique
Philosophical coherence · visual hierarchy · execution craft · functionality · innovation — each scored 0–10 · radar-chart visualization · outputs Keep / Fix / Quick Wins punch list.
<p align="center"><img src="demos/c6-expert-review.gif" width="100%"></p>
<p align="center"><img src="demos/c6-expert-review-en.gif" width="100%"></p>
### Junior Designer Workflow
No heroic one-shot attempts: start with assumptions + placeholders + reasoning, show it to the user early, then iterate. Fixing a misunderstanding early is 100× cheaper than fixing it late.
<p align="center"><img src="demos/w2-junior-designer.gif" width="100%"></p>
<p align="center"><img src="demos/w2-junior-designer-en.gif" width="100%"></p>
### Core Asset Protocol · 5-step hard process
Mandatory whenever the task involves a specific brand: ask → search → download (three fallback paths) → verify + extract → write `brand-spec.md` covering **logo, product shots, UI screenshots, colors, fonts** — all required assets, not just colors.
<p align="center"><img src="demos/w1-brand-protocol.gif" width="100%"></p>
<p align="center"><img src="demos/w1-brand-protocol-en.gif" width="100%"></p>
---
+2 -2
View File
@@ -34,13 +34,13 @@ npx skills add alchaincyf/huashu-design
---
<p align="center">
<img src="demos/hero-animation-v9.gif" alt="huashu-design Hero · 打字 → 选方向 → 画廊展开 → 聚焦 → 品牌显形" width="100%">
<img src="demos/hero-animation-v10.gif" alt="huashu-design Hero · 打字 → 选方向 → 画廊展开 → 聚焦 → 品牌显形" width="100%">
</p>
<p align="center"><sub>
▲ 25 秒 · Terminal → 4 方向 → Gallery ripple → 4 次 Focus → Brand reveal<br>
👉 <a href="https://www.huasheng.ai/huashu-design-hero/">访问带音效的 HTML 互动版</a> ·
<a href="demos/hero-animation-v9.mp4">下载 MP4(含 BGM+SFX · 10MB)</a>
<a href="demos/hero-animation-v10.mp4">下载 MP4(含 BGM+SFX · 10MB)</a>
</sub></p>
<p align="center">
File diff suppressed because it is too large. Load diff
+1123 -702
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+1027 -950
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+1087 -509
View File
File diff suppressed because it is too large. Load diff
+989
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>
+959 -732
View File
File diff suppressed because it is too large. Load diff
+816
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>
+792 -746
View File
File diff suppressed because it is too large. Load diff
+885
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>
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
+684
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>
File diff suppressed because it is too large. Load diff
+983
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 · 尽早 show(small iterations)
* ====================================================== */
.half-bot {
position: absolute;
top: 580px;
left: 160px;
right: 160px;
height: 340px;
opacity: 0;
will-change: opacity;
}
.half-bot .half-label .tag {
border-color: rgba(217,119,87,0.35);
color: var(--accent);
}
.iter-row {
display: flex;
gap: 32px;
align-items: flex-end;
height: 240px;
margin-top: 12px;
}
.iter-panel {
flex: 1;
background: rgba(20, 20, 20, 1);
border: 1px solid var(--hairline);
border-radius: 8px;
overflow: hidden;
height: 100%;
position: relative;
opacity: 0;
transform: translateY(20px);
will-change: opacity, transform;
display: flex;
flex-direction: column;
}
.iter-panel .ip-head {
padding: 10px 14px;
border-bottom: 1px solid var(--hairline);
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.16em;
color: var(--muted);
display: flex;
align-items: center;
justify-content: space-between;
}
.iter-panel .ip-version {
color: var(--accent);
font-weight: 500;
}
.iter-panel .ip-body {
flex: 1;
padding: 16px 18px;
display: flex;
flex-direction: column;
justify-content: center;
gap: 10px;
}
/* Rough mockup blocks that grow more detailed each iteration */
.iter-panel .m-block {
height: 8px;
background: var(--dim);
border-radius: 2px;
opacity: 0.8;
}
.iter-panel .m-block.accent { background: var(--accent); opacity: 0.8; }
.iter-panel .m-block.short { width: 40%; }
.iter-panel .m-block.med { width: 70%; }
.iter-panel .m-block.full { width: 100%; }
.iter-panel .m-block.tall { height: 24px; }
.iter-panel .m-block.big { height: 40px; }
.iter-panel .nod {
position: absolute;
top: 10px;
right: 14px;
width: 16px; height: 16px;
opacity: 0;
will-change: opacity, transform;
}
.iter-panel .nod svg {
width: 100%; height: 100%;
stroke: var(--accent);
fill: none;
stroke-width: 2;
}
.iter-panel .ip-minutes {
position: absolute;
bottom: 10px;
left: 14px;
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.12em;
color: var(--muted);
}
/* Rising curve visualization for bottom half */
.curve-wrap {
position: absolute;
right: 0;
bottom: 0;
width: 340px;
height: 180px;
opacity: 0;
will-change: opacity;
}
.curve-wrap svg {
width: 100%;
height: 100%;
overflow: visible;
}
.curve-wrap .axis {
stroke: var(--hairline);
stroke-width: 1;
fill: none;
}
.curve-wrap .curve-path {
stroke: var(--accent);
stroke-width: 2;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.curve-wrap .curve-dot {
fill: var(--accent);
r: 3;
}
.curve-wrap .curve-label {
font-family: var(--mono);
font-size: 9px;
fill: var(--muted);
letter-spacing: 0.12em;
}
/* ======================================================
* BEAT 3 · Full comparison chart crossfade
* ====================================================== */
.final-chart {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 1280px;
height: 620px;
opacity: 0;
will-change: opacity;
z-index: 60;
}
.final-chart svg {
width: 100%; height: 100%;
overflow: visible;
}
.final-chart .axis {
stroke: var(--hairline);
stroke-width: 1;
fill: none;
}
.final-chart .axis-label {
font-family: var(--mono);
font-size: 13px;
fill: var(--muted);
letter-spacing: 0.16em;
}
.final-chart .tick-label {
font-family: var(--mono);
font-size: 11px;
fill: var(--dim);
letter-spacing: 0.06em;
}
.final-chart .curve-a {
stroke: var(--cool);
stroke-width: 2;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.final-chart .curve-a-dash {
stroke: var(--bad-strong);
stroke-width: 2.5;
fill: none;
stroke-dasharray: 5 7;
stroke-linecap: round;
}
.final-chart .curve-b {
stroke: var(--accent);
stroke-width: 3;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.final-chart .curve-b-glow {
stroke: var(--accent);
stroke-width: 6;
fill: none;
opacity: 0.18;
stroke-linecap: round;
stroke-linejoin: round;
}
.final-chart .curve-dot {
fill: var(--accent);
}
.final-chart .fail-dot {
fill: none;
stroke: var(--bad-strong);
stroke-width: 2.5;
}
.final-chart .cool-dot {
fill: var(--cool);
}
.final-chart .anchor-label {
font-family: var(--serif-zh);
font-size: 20px;
font-weight: 400;
letter-spacing: 0.02em;
}
.final-chart .anchor-en {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
}
/* ======================================================
* BRAND REVEAL — 统一动作
* ====================================================== */
.brand-sheet {
position: absolute;
inset: 0;
background: var(--cd-bg);
transform: translateY(100%);
will-change: transform;
z-index: 80;
}
.brand-reveal {
position: absolute;
inset: 0;
z-index: 81;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
opacity: 0;
will-change: opacity;
}
.brand-reveal .wordmark {
font-family: var(--sans);
font-weight: 100;
font-size: 128px;
letter-spacing: -0.045em;
color: var(--cd-ink);
line-height: 1;
}
.brand-reveal .wordmark .accent { color: var(--accent-deep); }
.brand-reveal .underline {
width: 0;
height: 2px;
background: var(--accent);
margin-top: 36px;
will-change: width;
}
</style>
</head>
<body>
<div class="stage" id="stage">
<div class="mark">HUASHU · DESIGN</div>
<div class="mark-right">V2 · 2026</div>
<div class="title-line" id="titleLine">w2 · rough draft now beats perfect draft later</div>
<!-- Splitter -->
<div class="splitter" id="splitter"></div>
<div class="splitter-label" id="splitterLabel">VS</div>
<!-- ============ TOP HALF: All-at-once ============ -->
<div class="half-top" id="halfTop">
<div class="half-label">
<span class="tag">A</span>
<span class="zh" style="font-family: var(--serif-en); font-style: italic; letter-spacing: 0.01em;">All-at-once</span>
<span style="font-family: var(--mono); color: var(--muted); letter-spacing: 0.18em; font-size: 11px; margin-left: auto;">3&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>
File diff suppressed because it is too large. Load diff
+647
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>
File diff suppressed because it is too large. Load diff