990 lines
30 KiB
HTML
990 lines
30 KiB
HTML
<!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>
|