HH/.agents/skills/huashu-design/demos/c3-motion-design.html
ismail c5f76b3855
Some checks are pending
Build and Push Docker Image / build (push) Waiting to run
updates
2026-05-11 14:45:30 +03:00

1135 lines
36 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="zh-Hans">
<head>
<meta charset="utf-8" />
<title>huashu-design · c3 motion design中文版</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;600&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);
--hair-strong: rgba(255,255,255,0.22);
--accent: #D97757;
--accent-deep: #B85D3D;
--accent-dim: rgba(217,119,87,0.25);
--serif-cn: "Noto Serif SC", "Songti SC", "STSong", 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;
}
/* Subtle film grain overlay, 2% */
.stage::after {
content: '';
position: absolute; inset: 0;
pointer-events: none;
opacity: 0.025;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='300'><filter id='n'><feTurbulence baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
mix-blend-mode: overlay;
z-index: 200;
}
/* Watermark */
.watermark-tl {
position: absolute;
top: 40px; left: 56px;
font-family: var(--mono);
font-size: 14px;
letter-spacing: 0.2em;
color: rgba(255,255,255,0.16);
z-index: 50;
text-transform: none;
font-weight: 500;
}
.watermark-br {
position: absolute;
bottom: 32px; right: 48px;
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.24em;
color: rgba(255,255,255,0.22);
z-index: 100;
text-transform: uppercase;
opacity: 0;
transition: opacity 0.6s;
}
.watermark-br.visible { opacity: 1; }
/* Scene container */
.scene {
position: absolute; inset: 0;
opacity: 0;
visibility: hidden;
will-change: opacity;
}
.scene.visible { visibility: visible; }
/* ============ Split layout ============ */
.split {
position: absolute; inset: 0;
}
.split-top {
position: absolute;
top: 0; left: 0;
width: 100%; height: 48%;
display: flex;
align-items: center;
justify-content: center;
}
.split-bottom {
position: absolute;
bottom: 0; left: 0;
width: 100%; height: 52%;
}
/* Horizontal divider hairline */
.split-divider {
position: absolute;
left: 160px; right: 160px;
top: 48%;
height: 1px;
background: var(--hairline);
z-index: 5;
}
/* Section label (top-left of each half) */
.panel-label {
position: absolute;
top: 32px;
left: 160px;
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.3em;
color: var(--muted);
text-transform: uppercase;
}
.split-bottom .panel-label { top: 32px; }
.panel-label .accent { color: var(--accent); font-weight: 500; }
/* ============ Top: Timeline ============ */
.timeline-wrap {
width: 1600px;
position: relative;
margin-top: 40px;
}
.timeline-track {
position: relative;
height: 2px;
background: var(--hairline);
width: 100%;
}
.timeline-track .fill {
position: absolute;
top: 0; left: 0;
height: 100%;
background: linear-gradient(90deg, var(--accent) 0%, rgba(217,119,87,0.4) 100%);
width: 0%;
will-change: width;
}
/* Tick marks */
.tick {
position: absolute;
width: 1px;
height: 10px;
background: var(--muted);
top: -4px;
transform: translateX(-0.5px);
}
.tick.major { height: 14px; top: -6px; background: var(--ink-60); }
.tick-label {
position: absolute;
top: 18px;
font-family: var(--mono);
font-size: 11px;
color: var(--muted);
letter-spacing: 0.1em;
transform: translateX(-50%);
}
/* Playhead */
.playhead {
position: absolute;
top: -28px;
left: 0;
width: 2px;
height: 58px;
background: var(--accent);
transform: translateX(-1px);
will-change: transform;
z-index: 10;
box-shadow: 0 0 20px rgba(217,119,87,0.5);
}
.playhead::before {
content: '';
position: absolute;
top: -8px;
left: 50%;
transform: translateX(-50%);
width: 14px; height: 14px;
background: var(--accent);
border-radius: 50%;
box-shadow: 0 0 16px rgba(217,119,87,0.6);
}
.playhead::after {
content: '';
position: absolute;
top: -6px;
left: 50%;
transform: translateX(-50%);
width: 6px; height: 6px;
background: var(--bg);
border-radius: 50%;
z-index: 2;
}
/* API capsules on timeline */
.api-capsule {
position: absolute;
top: -92px;
transform: translateX(-50%);
padding: 10px 20px;
border: 1px solid var(--hairline);
border-radius: 999px;
background: rgba(0,0,0,0.6);
backdrop-filter: blur(8px);
font-family: var(--mono);
font-size: 18px;
font-weight: 500;
color: var(--ink-60);
letter-spacing: 0.02em;
transition: none;
will-change: color, border-color, transform, box-shadow;
white-space: nowrap;
}
.api-capsule.lit {
color: var(--accent);
border-color: var(--accent);
box-shadow: 0 0 30px rgba(217,119,87,0.35);
}
.api-capsule .tiny {
font-size: 10px;
color: var(--muted);
letter-spacing: 0.2em;
margin-right: 10px;
display: inline-block;
vertical-align: middle;
opacity: 0.7;
}
.api-capsule.lit .tiny { color: var(--accent); opacity: 0.9; }
/* Tick connector (short vertical line from capsule to timeline) */
.capsule-stem {
position: absolute;
top: -48px;
width: 1px;
height: 44px;
background: var(--hairline);
transform: translateX(-0.5px);
z-index: 1;
}
.capsule-stem.lit { background: var(--accent); }
/* ============ Bottom: Driven stage ============ */
.driven-stage {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
}
.viz {
position: absolute;
top: 46%; left: 50%;
transform: translate(-50%, -50%);
width: 1000px; height: 400px;
opacity: 0;
will-change: opacity;
display: flex;
align-items: center;
justify-content: center;
}
/* viz 1: useTime — clock */
.viz-clock {
position: relative;
width: 280px; height: 280px;
border: 1.5px solid var(--hair-strong);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.viz-clock .tickmark {
position: absolute;
width: 1px;
height: 8px;
background: var(--muted);
top: 10px;
left: 50%;
transform-origin: 50% 130px;
}
.viz-clock .tickmark.q {
width: 2px;
height: 14px;
background: var(--ink-60);
}
.viz-clock .hand-h {
position: absolute;
width: 3px; height: 80px;
background: var(--ink);
left: 50%;
bottom: 50%;
transform-origin: 50% 100%;
transform: translateX(-50%) rotate(30deg);
border-radius: 2px;
will-change: transform;
}
.viz-clock .hand-m {
position: absolute;
width: 2px; height: 110px;
background: var(--ink-80);
left: 50%;
bottom: 50%;
transform-origin: 50% 100%;
transform: translateX(-50%) rotate(120deg);
border-radius: 2px;
will-change: transform;
}
.viz-clock .hand-s {
position: absolute;
width: 1.5px; height: 120px;
background: var(--accent);
left: 50%;
bottom: 50%;
transform-origin: 50% 100%;
transform: translateX(-50%) rotate(0deg);
border-radius: 2px;
will-change: transform;
box-shadow: 0 0 10px rgba(217,119,87,0.4);
}
.viz-clock .center-dot {
width: 12px; height: 12px;
border-radius: 50%;
background: var(--accent);
z-index: 5;
box-shadow: 0 0 10px rgba(217,119,87,0.6);
}
.viz-clock-label {
position: absolute;
bottom: -48px;
left: 50%;
transform: translateX(-50%);
font-family: var(--mono);
font-size: 13px;
color: var(--muted);
letter-spacing: 0.12em;
white-space: nowrap;
}
.viz-clock-label .val {
color: var(--accent);
font-variant-numeric: tabular-nums;
}
/* viz 2: interpolate — morph box */
.viz-morph {
display: flex;
gap: 80px;
align-items: center;
justify-content: center;
width: 100%;
}
.morph-box {
width: 260px; height: 260px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.morph-rect {
background: var(--accent);
border-radius: 4px;
will-change: width, height, background, border-radius, transform;
box-shadow: 0 0 40px rgba(217,119,87,0.25);
}
.morph-label {
position: absolute;
bottom: -48px;
left: 50%;
transform: translateX(-50%);
font-family: var(--mono);
font-size: 12px;
color: var(--muted);
letter-spacing: 0.12em;
white-space: nowrap;
}
.morph-label .val { color: var(--accent); font-variant-numeric: tabular-nums; }
.morph-arrow {
font-family: var(--mono);
font-size: 28px;
color: var(--muted);
letter-spacing: 0.2em;
}
/* viz 3: Easing — curves */
.viz-curves {
position: relative;
width: 720px; height: 320px;
display: flex;
align-items: center;
justify-content: center;
}
.curves-svg {
width: 100%; height: 100%;
}
.curve-label {
position: absolute;
font-family: var(--mono);
font-size: 12px;
color: var(--muted);
letter-spacing: 0.08em;
white-space: nowrap;
}
/* viewBox 720x320 → right edge ≈ 680 of 720 → 94%. Vertical:
y=40 is visual top (output value 1), y=260 is bottom (value 0).
Labels go at right side, vertically aligned with where each curve
approaches its asymptote at t≈0.7.
expoOut at t=0.7 ~ 0.99 (≈ y=42)
cubicOut at t=0.7 ~ 0.973 (≈ y=46)
linear at t=0.7 ~ 0.7 (≈ y=106)
So spatial order top→bottom: expoOut, cubicOut, linear
*/
.curve-label.l-expo { top: 6%; right: 4%; color: var(--accent); }
.curve-label.l-cubic { top: 16%; right: 4%; color: rgba(255,255,255,0.78); }
.curve-label.l-linear { top: 36%; right: 4%; color: rgba(255,255,255,0.42); }
.curve-dot {
position: absolute;
width: 10px; height: 10px;
border-radius: 50%;
background: var(--accent);
transform: translate(-50%, -50%);
box-shadow: 0 0 14px rgba(217,119,87,0.6);
will-change: left, top;
}
/* viz 4: useSprite — choreographed grid */
.viz-sprites {
display: grid;
grid-template-columns: repeat(6, 60px);
grid-template-rows: repeat(4, 60px);
gap: 18px;
justify-content: center;
align-content: center;
padding: 40px 0;
}
.sprite {
width: 60px; height: 60px;
background: var(--hairline);
border: 1px solid var(--dim);
will-change: transform, opacity, background;
opacity: 0;
border-radius: 2px;
}
.sprite-label {
position: absolute;
bottom: -6px;
left: 50%;
transform: translateX(-50%);
font-family: var(--mono);
font-size: 12px;
color: var(--muted);
letter-spacing: 0.12em;
white-space: nowrap;
}
.sprite-label .val { color: var(--accent); font-variant-numeric: tabular-nums; }
/* ============ Scene 0: Opening title ============ */
.scene-intro {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.scene-intro .title {
font-family: var(--serif-cn);
font-size: 108px;
font-weight: 300;
letter-spacing: -0.02em;
color: var(--ink);
line-height: 1.05;
will-change: opacity, transform, font-weight;
}
.scene-intro .title .accent { color: var(--accent); }
.scene-intro .sub {
margin-top: 28px;
font-family: var(--mono);
font-size: 16px;
color: var(--muted);
letter-spacing: 0.3em;
}
/* ============ Scene 2: Brand reveal (米色面板标准动作) ============ */
.scene-brand {
background: transparent;
pointer-events: none;
z-index: 150;
}
.brand-panel {
position: absolute;
inset: 0;
background: #F5F4F0;
transform: translateY(100%);
will-change: transform;
}
.brand-wordmark {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, calc(-50% + 20px));
font-family: "Source Serif 4", Georgia, serif;
font-size: 72px;
font-weight: 100;
font-variation-settings: "wght" 100;
letter-spacing: -0.01em;
color: #1A1918;
text-align: center;
line-height: 1;
opacity: 0;
white-space: nowrap;
will-change: opacity, transform, font-weight, font-variation-settings;
}
.brand-wordmark .accent { color: #D97757; font-weight: inherit; }
.brand-line {
position: absolute;
top: calc(50% + 60px);
left: 50%;
transform: translateX(-50%);
height: 2px;
width: 0px;
background: #D97757;
will-change: width;
}
/* ============ Replay button (hidden during record) ============ */
.replay-btn {
position: absolute;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
padding: 12px 32px;
border: 1px solid var(--hair-strong);
border-radius: 999px;
background: transparent;
color: var(--ink-60);
font-family: var(--mono);
font-size: 13px;
letter-spacing: 0.2em;
cursor: pointer;
opacity: 0;
pointer-events: none;
transition: opacity 0.4s;
z-index: 300;
}
.replay-btn.visible {
opacity: 1;
pointer-events: auto;
}
</style>
</head>
<body>
<div class="stage" id="stage">
<!-- Top-left watermark (always on) -->
<div class="watermark-tl">HUASHU · DESIGN</div>
<!-- ============ Scene 0: Intro (0 → 1.6s) ============ -->
<div class="scene scene-intro" id="scene-intro">
<div class="title" id="introTitle">时间轴 <span class="accent">=</span> 代码</div>
<div class="sub" id="introSub">TIMELINE · MOTION · ENGINE</div>
</div>
<!-- ============ Scene 1: Split view (1.6 → 8.2s) ============ -->
<div class="scene" id="scene-main">
<div class="split">
<!-- TOP: Timeline -->
<div class="split-top">
<div class="panel-label">TIMELINE · <span class="accent">PLAYHEAD</span></div>
<div class="timeline-wrap">
<div class="timeline-track">
<div class="fill" id="timelineFill"></div>
<!-- Tick marks (10 ticks for 10s) -->
<div class="tick" style="left: 0%;"></div>
<div class="tick major" style="left: 0%;"></div>
<div class="tick" style="left: 10%;"></div>
<div class="tick major" style="left: 20%;"></div>
<div class="tick" style="left: 30%;"></div>
<div class="tick major" style="left: 40%;"></div>
<div class="tick" style="left: 50%;"></div>
<div class="tick major" style="left: 60%;"></div>
<div class="tick" style="left: 70%;"></div>
<div class="tick major" style="left: 80%;"></div>
<div class="tick" style="left: 90%;"></div>
<div class="tick major" style="left: 100%;"></div>
<div class="tick-label" style="left: 0%;">0s</div>
<div class="tick-label" style="left: 20%;">2s</div>
<div class="tick-label" style="left: 40%;">4s</div>
<div class="tick-label" style="left: 60%;">6s</div>
<div class="tick-label" style="left: 80%;">8s</div>
<div class="tick-label" style="left: 100%;">10s</div>
<!-- API capsules anchored at their trigger points -->
<!-- Scene-main spans 1.6→8.2; timeline maps 0→10s globally for clarity.
cap positions here mirror when each API is "active" on the lower viz. -->
<!-- useTime: global t = 1.8 → 3.3 → center ~2.5s → 25% -->
<div class="capsule-stem" id="stem-time" style="left: 18%;"></div>
<div class="api-capsule" id="cap-time" style="left: 18%;">
<span class="tiny">01</span>useTime
</div>
<!-- interpolate: 3.5 → 5s → center 4.2s → 42% -->
<div class="capsule-stem" id="stem-interp" style="left: 38%;"></div>
<div class="api-capsule" id="cap-interp" style="left: 38%;">
<span class="tiny">02</span>interpolate
</div>
<!-- Easing: 5 → 6.5s → center 5.7s → 57% -->
<div class="capsule-stem" id="stem-easing" style="left: 58%;"></div>
<div class="api-capsule" id="cap-easing" style="left: 58%;">
<span class="tiny">03</span>Easing
</div>
<!-- useSprite: 6.5 → 8s → center 7.2s → 72% -->
<div class="capsule-stem" id="stem-sprite" style="left: 80%;"></div>
<div class="api-capsule" id="cap-sprite" style="left: 80%;">
<span class="tiny">04</span>useSprite
</div>
<!-- Playhead -->
<div class="playhead" id="playhead"></div>
</div>
</div>
</div>
<!-- Divider -->
<div class="split-divider"></div>
<!-- BOTTOM: Driven stage -->
<div class="split-bottom">
<div class="panel-label">DRIVEN · <span class="accent">STAGE</span></div>
<div class="driven-stage">
<!-- viz 1: useTime — clock -->
<div class="viz" id="viz-time">
<div class="viz-clock" id="clockRoot">
<!-- 12 tick marks -->
<div class="tickmark q" style="transform: translate(-50%, 0) rotate(0deg);"></div>
<div class="tickmark" style="transform: translate(-50%, 0) rotate(30deg);"></div>
<div class="tickmark" style="transform: translate(-50%, 0) rotate(60deg);"></div>
<div class="tickmark q" style="transform: translate(-50%, 0) rotate(90deg);"></div>
<div class="tickmark" style="transform: translate(-50%, 0) rotate(120deg);"></div>
<div class="tickmark" style="transform: translate(-50%, 0) rotate(150deg);"></div>
<div class="tickmark q" style="transform: translate(-50%, 0) rotate(180deg);"></div>
<div class="tickmark" style="transform: translate(-50%, 0) rotate(210deg);"></div>
<div class="tickmark" style="transform: translate(-50%, 0) rotate(240deg);"></div>
<div class="tickmark q" style="transform: translate(-50%, 0) rotate(270deg);"></div>
<div class="tickmark" style="transform: translate(-50%, 0) rotate(300deg);"></div>
<div class="tickmark" style="transform: translate(-50%, 0) rotate(330deg);"></div>
<div class="hand-h" id="handH"></div>
<div class="hand-m" id="handM"></div>
<div class="hand-s" id="handS"></div>
<div class="center-dot"></div>
<div class="viz-clock-label">
t = <span class="val" id="timeVal">0.00s</span>
</div>
</div>
</div>
<!-- viz 2: interpolate — morph -->
<div class="viz" id="viz-interp">
<div class="viz-morph">
<div class="morph-box">
<div class="morph-rect" id="morphFrom" style="width: 80px; height: 80px; background: var(--hair-strong); border-radius: 2px;"></div>
<div class="morph-label">FROM · <span class="val">0 → 100</span></div>
</div>
<div class="morph-arrow">──────→</div>
<div class="morph-box">
<div class="morph-rect" id="morphTo"></div>
<div class="morph-label">INTERPOLATE · <span class="val" id="interpVal">0.00</span></div>
</div>
</div>
</div>
<!-- viz 3: Easing — 3 curves drawn in parallel -->
<div class="viz" id="viz-easing">
<div class="viz-curves">
<svg class="curves-svg" viewBox="0 0 720 320" preserveAspectRatio="none">
<!-- Grid -->
<line x1="60" y1="260" x2="680" y2="260" stroke="rgba(255,255,255,0.18)" stroke-width="1"/>
<line x1="60" y1="260" x2="60" y2="40" stroke="rgba(255,255,255,0.18)" stroke-width="1"/>
<!-- Axis labels -->
<text x="50" y="266" text-anchor="end" fill="rgba(255,255,255,0.4)" font-family="JetBrains Mono, monospace" font-size="11">0</text>
<text x="50" y="48" text-anchor="end" fill="rgba(255,255,255,0.4)" font-family="JetBrains Mono, monospace" font-size="11">1</text>
<text x="680" y="282" text-anchor="end" fill="rgba(255,255,255,0.4)" font-family="JetBrains Mono, monospace" font-size="11">t</text>
<!-- Curves -->
<path id="pathLinear" d="M 60 260 L 60 260" stroke="rgba(255,255,255,0.42)" stroke-width="1.5" fill="none" stroke-linecap="round"/>
<path id="pathCubic" d="M 60 260 L 60 260" stroke="rgba(255,255,255,0.75)" stroke-width="1.8" fill="none" stroke-linecap="round"/>
<path id="pathExpo" d="M 60 260 L 60 260" stroke="#D97757" stroke-width="2.2" fill="none" stroke-linecap="round"/>
</svg>
<div class="curve-label l-linear">linear</div>
<div class="curve-label l-cubic">cubicOut</div>
<div class="curve-label l-expo">expoOut</div>
</div>
</div>
<!-- viz 4: useSprite — 24 sprites -->
<div class="viz" id="viz-sprite">
<div class="viz-sprites" id="spriteGrid">
<!-- 24 sprites (6x4), filled by JS -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ============ Scene 2: Brand reveal (米色面板, 8.0 → 10s) ============ -->
<div class="scene scene-brand" id="scene-brand">
<div class="brand-panel" id="brandPanel"></div>
<div class="brand-wordmark" id="wordmark">huashu<span class="accent">-</span>design</div>
<div class="brand-line" id="brandLine"></div>
</div>
<!-- Bottom-right watermark -->
<div class="watermark-br" id="watermarkBR">V2 · 2026</div>
<!-- Replay button (hidden during recording) -->
<button class="replay-btn no-record" id="replayBtn">REPLAY</button>
</div>
<script>
(function() {
// =============== Timing ===============
const T = {
DURATION: 10.0,
// Scene 0: intro
intro_in: [0.0, 0.5],
intro_out: [1.3, 1.6],
// Scene 1: main (timeline + driven stage)
main_in: [1.5, 1.9], // fade in
// Playhead sweeps from 0% (at t=1.6) to 100% (at t=8.2).
// API activations use GLOBAL time. Their capsule position is placed so
// that playhead passes under the capsule right when the API peaks.
main_t0: 1.6,
main_t_end: 8.2,
main_out: [8.0, 8.4],
// API activations (GLOBAL time)
// Each API: [activate_start, peak, deactivate_end]
// Capsule x% = (peak - 1.6) / (8.2 - 1.6) * 100
useTime: [2.0, 2.8, 3.6], // capsule @ ~18%
interpolate: [3.6, 4.1, 4.8], // capsule @ ~38%
Easing: [4.8, 5.4, 6.2], // capsule @ ~58%
useSprite: [6.2, 6.9, 7.9], // capsule @ ~80%
// Scene 2: Brand reveal (米色面板 standard, last 2s of T=10)
// [T-2.0 → T-1.7]: main fade 1→0 (already handled by main_out 8.0-8.4)
// [T-1.7 → T-1.3]: beige panel translateY 100%→0, expoOut
// [T-1.3 → T-0.7]: wordmark weight 100→500 + y:20→0 + opacity 0→1, expoOut
// [T-0.7 → T-0.3]: orange line width 0→280px, cubicOut
// [T-0.3 → T]: hold
brand_panel: [8.3, 8.7],
brand_word: [8.7, 9.3],
brand_line: [9.3, 9.7],
};
// =============== 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 cubicOut = t => 1 - Math.pow(1 - t, 3);
const cubicIn = t => t * t * t;
const cubicInOut = t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
const easeInOut = cubicInOut;
const linear = t => t;
// =============== Utils ===============
const clamp = (v, lo = 0, hi = 1) => Math.max(lo, Math.min(hi, v));
const clampLerp = (t, t0, t1) => clamp((t - t0) / (t1 - t0));
function lerp(t, t0, t1, v0, v1, easing = linear) {
const p = clampLerp(t, t0, t1);
return v0 + (v1 - v0) * easing(p);
}
// =============== DOM refs ===============
const scenes = {
intro: document.getElementById('scene-intro'),
main: document.getElementById('scene-main'),
brand: document.getElementById('scene-brand'),
};
const introTitle = document.getElementById('introTitle');
const introSub = document.getElementById('introSub');
const timelineFill = document.getElementById('timelineFill');
const playhead = document.getElementById('playhead');
const capTime = document.getElementById('cap-time');
const capInterp = document.getElementById('cap-interp');
const capEasing = document.getElementById('cap-easing');
const capSprite = document.getElementById('cap-sprite');
const stemTime = document.getElementById('stem-time');
const stemInterp = document.getElementById('stem-interp');
const stemEasing = document.getElementById('stem-easing');
const stemSprite = document.getElementById('stem-sprite');
const vizTime = document.getElementById('viz-time');
const vizInterp = document.getElementById('viz-interp');
const vizEasing = document.getElementById('viz-easing');
const vizSprite = document.getElementById('viz-sprite');
const handS = document.getElementById('handS');
const handM = document.getElementById('handM');
const handH = document.getElementById('handH');
const timeVal = document.getElementById('timeVal');
const morphTo = document.getElementById('morphTo');
const interpVal = document.getElementById('interpVal');
const pathLinear = document.getElementById('pathLinear');
const pathCubic = document.getElementById('pathCubic');
const pathExpo = document.getElementById('pathExpo');
const spriteGrid = document.getElementById('spriteGrid');
const wordmark = document.getElementById('wordmark');
const brandLine = document.getElementById('brandLine');
const brandPanel = document.getElementById('brandPanel');
const watermarkBR = document.getElementById('watermarkBR');
const replayBtn = document.getElementById('replayBtn');
// Build 24 sprites (6x4 grid)
const SPRITE_COLS = 6, SPRITE_ROWS = 4;
const spriteEls = [];
for (let r = 0; r < SPRITE_ROWS; r++) {
for (let c = 0; c < SPRITE_COLS; c++) {
const el = document.createElement('div');
el.className = 'sprite';
// center distance for ripple
const dc = c - (SPRITE_COLS - 1) / 2;
const dr = r - (SPRITE_ROWS - 1) / 2;
const dist = Math.sqrt(dc * dc + dr * dr);
const maxDist = Math.sqrt(((SPRITE_COLS - 1) / 2) ** 2 + ((SPRITE_ROWS - 1) / 2) ** 2);
el.dataset.delay = (dist / maxDist).toFixed(3);
spriteGrid.appendChild(el);
spriteEls.push(el);
}
}
// =============== Scene helpers ===============
function showScene(el, opacity) {
if (opacity > 0.001) el.classList.add('visible');
else el.classList.remove('visible');
el.style.opacity = opacity;
}
// =============== API activation logic ===============
function apiState(t_local, api) {
// Returns { on: bool, strength: 0-1 }
const [a, peak, b] = T[api];
if (t_local < a || t_local > b) return { on: false, strength: 0 };
if (t_local < peak) {
return { on: true, strength: expoOut(clampLerp(t_local, a, peak)) };
} else {
return { on: true, strength: 1 - cubicIn(clampLerp(t_local, peak, b)) };
}
}
// =============== Draw easing curves progressively ===============
function easingPath(easingFn, progress) {
// progress 0-1 draws the curve from left to right
// x range: 60 → 680, y range: 260 (0) → 40 (1)
const X0 = 60, X1 = 680, Y0 = 260, Y1 = 40;
const steps = Math.max(2, Math.floor(progress * 80));
let d = `M ${X0} ${Y0}`;
for (let i = 1; i <= steps; i++) {
const t = (i / 80) * progress;
const x = X0 + (X1 - X0) * t;
const y = Y0 + (Y1 - Y0) * easingFn(t);
d += ` L ${x.toFixed(2)} ${y.toFixed(2)}`;
}
return d;
}
// =============== Render ===============
function render(t) {
// ============ Scene 0: Intro ============
if (t < T.main_in[1]) {
let op = 0;
if (t < T.intro_in[1]) op = clampLerp(t, T.intro_in[0], T.intro_in[1]);
else if (t < T.intro_out[0]) op = 1;
else op = 1 - clampLerp(t, T.intro_out[0], T.intro_out[1]);
showScene(scenes.intro, op);
// weight morph + rise
const morphP = expoOut(clampLerp(t, T.intro_in[0], T.intro_in[1] + 0.3));
const w = 150 + (400 - 150) * morphP;
introTitle.style.fontWeight = Math.round(w);
const rise = lerp(t, T.intro_in[0], T.intro_in[1], 16, 0, expoOut);
introTitle.style.transform = `translate3d(0, ${rise}px, 0)`;
introSub.style.opacity = clampLerp(t, T.intro_in[1], T.intro_in[1] + 0.4);
} else {
showScene(scenes.intro, 0);
}
// ============ Scene 1: Main (split view) ============
if (t >= T.main_in[0] - 0.1 && t < T.main_out[1]) {
let op;
if (t < T.main_in[1]) op = clampLerp(t, T.main_in[0], T.main_in[1]);
else if (t < T.main_out[0]) op = 1;
else op = 1 - clampLerp(t, T.main_out[0], T.main_out[1]);
showScene(scenes.main, op);
// Playhead sweeps 0% → 100% across the window [main_t0, main_t_end]
const phP = clampLerp(t, T.main_t0, T.main_t_end);
const phPct = phP * 100;
playhead.style.left = phPct + '%';
// Keep: use t directly for API state
const t_local_clamped = t;
// Timeline fill
timelineFill.style.width = phPct + '%';
// API capsules: lit state driven by apiState
const stTime = apiState(t_local_clamped, 'useTime');
const stInterp = apiState(t_local_clamped, 'interpolate');
const stEasing = apiState(t_local_clamped, 'Easing');
const stSprite = apiState(t_local_clamped, 'useSprite');
setLit(capTime, stemTime, stTime);
setLit(capInterp, stemInterp, stInterp);
setLit(capEasing, stemEasing, stEasing);
setLit(capSprite, stemSprite, stSprite);
// Viz opacities — each viz only visible during its API's window
vizTime.style.opacity = stTime.on ? stTime.strength : 0;
vizInterp.style.opacity = stInterp.on ? stInterp.strength : 0;
vizEasing.style.opacity = stEasing.on ? stEasing.strength : 0;
vizSprite.style.opacity = stSprite.on ? stSprite.strength : 0;
// ========= viz 1: clock =========
// Continuous rotation (not just when active) so transition looks natural
// But only animate hands when api is near-active, to avoid wasted cpu
{
const [a, _peak, b] = T.useTime;
// Second hand: one revolution over the active window
const localP = clampLerp(t_local_clamped, a, b);
// Multi-revolution: 1.5 turns over the window
const sDeg = localP * 540;
const mDeg = localP * 180 + 120;
const hDeg = localP * 60 + 30;
handS.style.transform = `translateX(-50%) rotate(${sDeg}deg)`;
handM.style.transform = `translateX(-50%) rotate(${mDeg}deg)`;
handH.style.transform = `translateX(-50%) rotate(${hDeg}deg)`;
// Display value as t in seconds mapping 0→1.50
const displayVal = (localP * 1.5).toFixed(2);
timeVal.textContent = displayVal + 's';
}
// ========= viz 2: interpolate =========
{
const [a, _peak, b] = T.interpolate;
const localP = clampLerp(t_local_clamped, a, b);
const eased = easeInOut(localP);
// morph from 80×80 black → 220×160 orange, rounded
const W = 80 + (240 - 80) * eased;
const H = 80 + (160 - 80) * eased;
const bright = Math.round(30 + (217 - 30) * eased);
const brightG = Math.round(30 + (119 - 30) * eased);
const brightB = Math.round(30 + (87 - 30) * eased);
const rad = 2 + (20 - 2) * eased;
morphTo.style.width = W + 'px';
morphTo.style.height = H + 'px';
morphTo.style.background = `rgb(${bright}, ${brightG}, ${brightB})`;
morphTo.style.borderRadius = rad + 'px';
interpVal.textContent = eased.toFixed(2);
}
// ========= viz 3: easing curves =========
{
const [a, _peak, b] = T.Easing;
const localP = clampLerp(t_local_clamped, a, b);
pathLinear.setAttribute('d', easingPath(linear, localP));
pathCubic.setAttribute('d', easingPath(cubicOut, localP));
pathExpo.setAttribute('d', easingPath(expoOut, localP));
}
// ========= viz 4: sprites =========
{
const [a, _peak, b] = T.useSprite;
const localP = clampLerp(t_local_clamped, a, b);
for (const el of spriteEls) {
const delay = parseFloat(el.dataset.delay);
const spriteLocalT = clamp((localP - delay * 0.5) / 0.5, 0, 1);
const op = expoOut(spriteLocalT);
el.style.opacity = op;
const scale = 0.5 + 0.5 * op;
const y = (1 - op) * 14;
el.style.transform = `translateY(${y}px) scale(${scale})`;
el.style.background = op > 0.85 ? 'var(--accent)' : 'var(--hairline)';
}
}
} else {
showScene(scenes.main, 0);
}
// ============ Scene 2: Brand reveal (米色面板标准动作) ============
if (t >= T.brand_panel[0] - 0.1) {
showScene(scenes.brand, 1);
// [T-1.7 → T-1.3]: beige panel slides up, expoOut
const panelP = expoOut(clampLerp(t, T.brand_panel[0], T.brand_panel[1]));
brandPanel.style.transform = `translateY(${(1 - panelP) * 100}%)`;
// [T-1.3 → T-0.7]: wordmark weight 100→500 + y:20→0 + opacity:0→1, expoOut
const wordP = expoOut(clampLerp(t, T.brand_word[0], T.brand_word[1]));
const w = 100 + (500 - 100) * wordP;
wordmark.style.fontVariationSettings = `"wght" ${w.toFixed(0)}`;
wordmark.style.fontWeight = Math.round(w);
wordmark.style.opacity = wordP;
const wRise = (1 - wordP) * 20;
wordmark.style.transform = `translate(-50%, calc(-50% + ${wRise}px))`;
// [T-0.7 → T-0.3]: orange line expands 0→280px, cubicOut
const lineP = cubicOut(clampLerp(t, T.brand_line[0], T.brand_line[1]));
brandLine.style.width = (lineP * 280) + 'px';
} else {
showScene(scenes.brand, 0);
brandPanel.style.transform = 'translateY(100%)';
wordmark.style.opacity = 0;
brandLine.style.width = '0px';
}
// Watermark visible from start of main until end
if (t >= T.main_in[0] && t < T.DURATION - 0.15) {
watermarkBR.classList.add('visible');
} else {
watermarkBR.classList.remove('visible');
}
}
function setLit(capsule, stem, state) {
if (state.on && state.strength > 0.15) {
capsule.classList.add('lit');
stem.classList.add('lit');
// Subtle scale pulse centered on peak (simplistic)
const scale = 1.0 + state.strength * 0.06;
capsule.style.transform = `translateX(-50%) scale(${scale})`;
} else {
capsule.classList.remove('lit');
stem.classList.remove('lit');
capsule.style.transform = 'translateX(-50%)';
}
}
// =============== Driver ===============
let manualT = null;
let startMs = null;
let hasFinishedOnce = false;
function tick(now) {
if (manualT != null) {
render(manualT);
} else {
if (startMs == null) startMs = now;
const elapsed = (now - startMs) / 1000;
const recording = window.__recording === true;
let t;
if (recording) {
t = Math.min(elapsed, T.DURATION - 0.001);
if (elapsed >= T.DURATION && !hasFinishedOnce) hasFinishedOnce = true;
} else {
t = elapsed % T.DURATION;
// Show replay button when we've played at least once
if (elapsed >= T.DURATION) {
replayBtn.classList.add('visible');
}
}
render(t);
}
requestAnimationFrame(tick);
}
// First paint signal for renderer
document.fonts.ready.then(() => {
render(0);
requestAnimationFrame(() => {
window.__ready = true;
requestAnimationFrame(tick);
});
});
// ========= Stage scaling (fit viewport) =========
function fitStage() {
const stage = document.getElementById('stage');
const scaleX = window.innerWidth / 1920;
const scaleY = window.innerHeight / 1080;
const scale = Math.min(scaleX, scaleY);
stage.style.transform = `translate(-50%, -50%) scale(${scale})`;
}
fitStage();
window.addEventListener('resize', fitStage);
// Replay
replayBtn.addEventListener('click', () => {
startMs = null;
replayBtn.classList.remove('visible');
});
// =============== Expose for frame-accurate rendering ===============
window.__setTime = (t) => { manualT = t; render(t); };
window.__resume = () => { manualT = null; startMs = null; };
window.__duration = T.DURATION;
window.__render = render;
})();
</script>
</body>
</html>