59 lines
2.7 KiB
HTML
59 lines
2.7 KiB
HTML
<!doctype html>
|
|||
|
|
<html lang="en">
|
||
|
|
<head>
|
||
|
|
<meta charset="utf-8" />
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
|
|
<title>loading</title>
|
||
|
|
<!-- GSAP core from CDN — no build step. Add plugins the same way if needed:
|
||
|
|
https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/ScrollTrigger.min.js -->
|
||
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/gsap.min.js"></script>
|
||
|
|
<style>
|
||
|
|
:root { color-scheme: dark; }
|
||
|
|
* { margin: 0; box-sizing: border-box; }
|
||
|
|
body {
|
||
|
|
min-height: 100svh; display: grid; place-items: center;
|
||
|
|
background: radial-gradient(120% 120% at 50% 0%, #1b1f3b 0%, #0a0a12 60%);
|
||
|
|
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||
|
|
color: #f4f5ff; overflow: hidden;
|
||
|
|
}
|
||
|
|
.stage { text-align: center; padding: 6vmin; }
|
||
|
|
.title { font-size: clamp(2.5rem, 9vw, 6rem); font-weight: 800; letter-spacing: -0.03em; line-height: 1; }
|
||
|
|
.sub { font-size: clamp(1rem, 3.2vw, 1.6rem); margin-top: 1.2rem; color: #aab; }
|
||
|
|
.cta { display: inline-block; margin-top: 2.4rem; padding: 0.9rem 1.8rem;
|
||
|
|
border-radius: 999px; background: #5b6cff; color: #fff; font-weight: 600; }
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<main class="stage">
|
||
|
|
<h1 class="title">Ship it.</h1>
|
||
|
|
<p class="sub">A standalone, screenshot-verifiable GSAP scene.</p>
|
||
|
|
<a class="cta" href="#">Get started</a>
|
||
|
|
</main>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||
|
|
|
||
|
|
// ── One master timeline: a single playhead the harness can seek ──────────
|
||
|
|
const tl = gsap.timeline({ defaults: { ease: "power3.out", duration: 0.7 } });
|
||
|
|
|
||
|
|
if (reduce) {
|
||
|
|
// Reduced motion: no spatial movement — just make the final state visible.
|
||
|
|
gsap.set([".title", ".sub", ".cta"], { opacity: 1, y: 0 });
|
||
|
|
} else {
|
||
|
|
tl.from(".title", { yPercent: 110, opacity: 0, ease: "power4.out", duration: 0.9 })
|
||
|
|
.from(".sub", { y: 24, opacity: 0 }, "-=0.45")
|
||
|
|
.from(".cta", { scale: 0.9, opacity: 0, ease: "back.out(1.7)" }, "-=0.3");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Seek harness: ?t=N freezes the timeline at N seconds for a clean shot ─
|
||
|
|
const t = new URLSearchParams(location.search).get("t");
|
||
|
|
if (t !== null) { tl.pause(); tl.seek(parseFloat(t)); } // deterministic frozen frame
|
||
|
|
// no ?t → plays normally on load
|
||
|
|
|
||
|
|
// ── Ready signals for headless verification ──────────────────────────────
|
||
|
|
window.__ready = true;
|
||
|
|
document.title = "ready";
|
||
|
|
console.log("duration", tl.duration()); // end time for the ?t=<dur> shot
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|