mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
210 lines
11 KiB
HTML
210 lines
11 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=1080, height=1200" />
|
||
<title>hanzo — an afternoon</title>
|
||
<script src="assets/gsap.min.js"></script>
|
||
<style>
|
||
|
||
@font-face { font-family: "Zen"; src: url("./assets/zen.woff2") format("woff2"); font-weight: 100 900; font-display: block; }
|
||
@font-face { font-family: "Zen Mono"; src: url("./assets/zen-mono.woff2") format("woff2"); font-weight: 100 900; font-display: block; }
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
html, body { width: 1080px; height: 1200px; overflow: hidden; background: #06070a; }
|
||
#root { position: relative; width: 1080px; height: 1200px; overflow: hidden;
|
||
--accent: #8b5cf6; --ink: #e7ebf3; --dim: #7c8698; --line: #1d2330;
|
||
--panel: #0e1117; --panel2: #131822;
|
||
font-family: "Zen", ui-sans-serif, system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
|
||
.field { position: absolute; inset: 0;
|
||
background:
|
||
radial-gradient(120% 80% at 50% -10%, color-mix(in srgb, var(--accent) 20%, transparent), transparent 62%),
|
||
#06070a; }
|
||
.grid { position: absolute; inset: 0;
|
||
background-image:
|
||
linear-gradient(to right, rgba(255,255,255,.025) 1px, transparent 1px),
|
||
linear-gradient(to bottom, rgba(255,255,255,.025) 1px, transparent 1px);
|
||
background-size: 60px 60px; }
|
||
/* Each beat is centred on the frame, so a short beat and a tall one share
|
||
one optical middle and a responsive crop takes background from both
|
||
edges rather than emptying the bottom half. */
|
||
.safe { position: absolute; left: 100px; top: 0;
|
||
width: 880px; height: 1200px; }
|
||
.beat { position: absolute; left: 0; width: 100%; top: 50%;
|
||
transform: translateY(-50%); }
|
||
|
||
/* beat 1 — the mode row */
|
||
.modes {
|
||
display: flex; gap: 10px; justify-content: center; }
|
||
.mode { height: 52px; padding: 0 26px; display: flex; align-items: center;
|
||
border: 1px solid var(--line); border-radius: 999px; background: var(--panel2);
|
||
color: var(--dim); font-size: 19px; font-weight: 600; }
|
||
.mode.on { border-color: color-mix(in srgb, var(--accent) 55%, transparent);
|
||
background: color-mix(in srgb, var(--accent) 16%, var(--panel2));
|
||
color: var(--ink); }
|
||
.cursor { position: absolute; width: 22px; height: 22px; opacity: 0;
|
||
border-radius: 50%; background: #fff; box-shadow: 0 0 0 6px rgba(255,255,255,.14); }
|
||
|
||
/* beat 2 — the request */
|
||
.ask { opacity: 0;
|
||
border: 1px solid var(--line); border-radius: 20px; background: var(--panel);
|
||
padding: 26px 28px; }
|
||
.chips { display: flex; gap: 10px; margin-bottom: 20px; }
|
||
.chip { height: 38px; padding: 0 16px; display: flex; align-items: center; gap: 8px;
|
||
border: 1px solid var(--line); border-radius: 10px; background: var(--panel2);
|
||
color: var(--dim); font-size: 15px; font-weight: 600; opacity: .35; }
|
||
.chip.on { opacity: 1; color: var(--ink);
|
||
border-color: color-mix(in srgb, var(--accent) 45%, transparent); }
|
||
.chip i { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); }
|
||
.line { font-size: 27px; line-height: 1.35; color: var(--ink); min-height: 74px; }
|
||
.caret { display: inline-block; width: 3px; height: 27px; background: var(--accent);
|
||
vertical-align: -4px; margin-left: 3px; }
|
||
|
||
/* beat 3 — the work */
|
||
.work { opacity: 0; }
|
||
.step { display: flex; align-items: center; gap: 16px; height: 74px;
|
||
border-bottom: 1px solid #161c27; opacity: .3; }
|
||
.step.on { opacity: 1; }
|
||
.tick { width: 26px; height: 26px; flex: 0 0 26px; border-radius: 50%;
|
||
border: 1px solid var(--line); position: relative; }
|
||
.step.on .tick { border-color: var(--accent);
|
||
background: color-mix(in srgb, var(--accent) 22%, transparent); }
|
||
.verb { font-family: "Zen Mono", ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 15px;
|
||
color: var(--accent); width: 52px; flex: 0 0 52px; }
|
||
.path { font-family: "Zen Mono", ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 19px; color: var(--ink); }
|
||
|
||
/* beat 4 — the artifact */
|
||
.made { opacity: 0; }
|
||
.shop { border: 1px solid var(--line); border-radius: 18px; background: var(--panel);
|
||
padding: 22px; display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
|
||
.card { height: 152px; border-radius: 12px;
|
||
background: linear-gradient(150deg, color-mix(in srgb, var(--accent) 26%, #1b2231), #171d29);
|
||
opacity: 0; }
|
||
.cost { margin-top: 22px; display: flex; align-items: center; gap: 12px;
|
||
font-family: "Zen Mono", ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 17px; color: var(--dim); }
|
||
.cost b { color: var(--accent); font-weight: 600; }
|
||
|
||
/* beat 5 — the line */
|
||
.close { opacity: 0;
|
||
text-align: center; font-size: 40px; font-weight: 600; line-height: 1.25;
|
||
color: var(--ink); letter-spacing: -.6px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="root" data-composition-id="promo" data-start="0" data-duration="26"
|
||
data-fps="30" data-width="1080" data-height="1200">
|
||
<div class="field"></div>
|
||
<div class="grid"></div>
|
||
|
||
<div class="safe">
|
||
<div class="beat modes" id="modes">
|
||
<div class="mode" id="mode0">Chat</div>
|
||
<div class="mode" id="mode1">Agent</div>
|
||
<div class="mode" id="mode2">Batch</div>
|
||
</div>
|
||
<div class="cursor" id="cursor"></div>
|
||
|
||
<div class="beat ask" id="ask">
|
||
<div class="chips">
|
||
<div class="chip" id="chip0"><i></i>Sandboxes</div>
|
||
<div class="chip" id="chip1"><i></i>Vector</div>
|
||
<div class="chip" id="chip2"><i></i>Sites</div>
|
||
</div>
|
||
<div class="line"><span id="typed"></span><span class="caret" id="caret"></span></div>
|
||
</div>
|
||
|
||
<div class="beat work" id="work">
|
||
<div class="step" id="step0">
|
||
<span class="tick"></span><span class="verb">POST</span><span class="path">/v1/sandboxes</span>
|
||
</div>
|
||
<div class="step" id="step1">
|
||
<span class="tick"></span><span class="verb">POST</span><span class="path">/v1/vector</span>
|
||
</div>
|
||
<div class="step" id="step2">
|
||
<span class="tick"></span><span class="verb">POST</span><span class="path">/v1/sites/deploy</span>
|
||
</div>
|
||
<div class="step" id="step3">
|
||
<span class="tick"></span><span class="verb">POST</span><span class="path">/v1/sites/{slug}/publish</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="beat made" id="made">
|
||
<div class="shop">
|
||
<div class="card" id="card0"></div>
|
||
<div class="card" id="card1"></div>
|
||
<div class="card" id="card2"></div>
|
||
<div class="card" id="card3"></div>
|
||
<div class="card" id="card4"></div>
|
||
<div class="card" id="card5"></div>
|
||
</div>
|
||
<div class="cost"><b>GET /v1/billing/usage</b><span>Watch what it cost.</span></div>
|
||
</div>
|
||
|
||
<div class="beat close" id="close">A store that did not exist this morning.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
(function () {
|
||
var tl = gsap.timeline({ paused: true });
|
||
var ASK_TEXT = "Build a spaceship shop.";
|
||
var $ = function (id) { return document.getElementById(id); };
|
||
|
||
// BEAT 1 · 0.0–3.0 — one control, one cursor, one choice.
|
||
gsap.set('#cursor', { x: 700, y: 470 });
|
||
tl.to('#cursor', { opacity: 1, duration: .35 }, 0.35)
|
||
.to('#cursor', { x: 452, y: 326, duration: 1.1, ease: 'power2.inOut' }, 0.8)
|
||
.to('#mode1', { onStart: function () { $('mode1').classList.add('on'); }, duration: .01 }, 2.0)
|
||
.to('#cursor', { scale: .82, duration: .12, yoyo: true, repeat: 1 }, 2.0)
|
||
.to('#cursor', { opacity: 0, duration: .3 }, 2.5)
|
||
|
||
// BEAT 2 · 3.0–8.0 — the surface, its three capabilities, the request.
|
||
.to('#modes', { opacity: 0, y: -18, duration: .45, ease: 'power2.in' }, 2.9)
|
||
.to('#ask', { opacity: 1, y: 0, duration: .5, ease: 'power2.out' }, 3.2)
|
||
.to('#chip0', { onStart: function () { $('chip0').classList.add('on'); }, duration: .01 }, 3.70)
|
||
.to('#chip1', { onStart: function () { $('chip1').classList.add('on'); }, duration: .01 }, 4.10)
|
||
.to('#chip2', { onStart: function () { $('chip2').classList.add('on'); }, duration: .01 }, 4.50)
|
||
.to({}, {
|
||
duration: 2.4,
|
||
onUpdate: function () {
|
||
var n = Math.round(this.progress() * ASK_TEXT.length);
|
||
$('typed').textContent = ASK_TEXT.slice(0, n);
|
||
},
|
||
}, 5.0)
|
||
.to('#caret', { opacity: 0, duration: .2 }, 7.6)
|
||
|
||
// BEAT 3 · 8.0–16.0 — four real calls, one focal point at a time.
|
||
.to('#ask', { opacity: 0, y: -22, duration: .5, ease: 'power2.in' }, 7.9)
|
||
.to('#work', { opacity: 1, duration: .5, ease: 'power2.out' }, 8.3)
|
||
.to('#step0', { onStart: function () { $('step0').classList.add('on'); }, duration: .01 }, 8.80)
|
||
.to('#step1', { onStart: function () { $('step1').classList.add('on'); }, duration: .01 }, 10.50)
|
||
.to('#step2', { onStart: function () { $('step2').classList.add('on'); }, duration: .01 }, 12.20)
|
||
.to('#step3', { onStart: function () { $('step3').classList.add('on'); }, duration: .01 }, 13.90)
|
||
|
||
// BEAT 4 · 16.0–21.0 — the thing it made, and what it cost.
|
||
.to('#work', { opacity: 0, y: -26, duration: .5, ease: 'power2.in' }, 15.7)
|
||
.to('#made', { opacity: 1, duration: .5, ease: 'power2.out' }, 16.1)
|
||
.to('#card0', { opacity: 1, duration: .4, ease: 'power2.out' }, 16.50)
|
||
.to('#card1', { opacity: 1, duration: .4, ease: 'power2.out' }, 16.66)
|
||
.to('#card2', { opacity: 1, duration: .4, ease: 'power2.out' }, 16.82)
|
||
.to('#card3', { opacity: 1, duration: .4, ease: 'power2.out' }, 16.98)
|
||
.to('#card4', { opacity: 1, duration: .4, ease: 'power2.out' }, 17.14)
|
||
.to('#card5', { opacity: 1, duration: .4, ease: 'power2.out' }, 17.30)
|
||
|
||
// BEAT 5 · 21.0–24.0 — the line, alone.
|
||
.to('#made', { opacity: 0, duration: .6, ease: 'power2.in' }, 20.6)
|
||
.to('#close', { opacity: 1, duration: .7, ease: 'power2.out' }, 21.1)
|
||
|
||
// BEAT 6 · 24.0–26.0 — back to the frame it opened on, so the loop has
|
||
// no seam: the line leaves, the mode row returns unselected, and the
|
||
// last frame is the first.
|
||
.to('#close', { opacity: 0, duration: .6, ease: 'power2.in' }, 23.8)
|
||
.to('#mode1', { onStart: function () { $('mode1').classList.remove('on'); }, duration: .01 }, 24.2)
|
||
.to('#modes', { opacity: 1, y: 0, duration: .7, ease: 'power2.out' }, 24.4);
|
||
|
||
window.__timelines = window.__timelines || {};
|
||
window.__timelines['promo'] = tl;
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|