| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989 |
- <!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>
|