Add web animation skills (iart-ai/web-animation-skills)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GYeokZtSmqg42ZGWsLXX2C
This commit is contained in:
42 files changed
+4170
No files matched your search
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* hero-timeline.js — complete, runnable GSAP hero entrance.
|
||||
*
|
||||
* Markup expected:
|
||||
* <section class="hero">
|
||||
* <p class="hero__eyebrow">EYEBROW</p>
|
||||
* <h1 class="hero__title">Big bold headline that wraps</h1>
|
||||
* <p class="hero__sub">Supporting copy.</p>
|
||||
* <div class="hero__actions"><a class="btn">Primary</a><a class="btn btn--ghost">Secondary</a></div>
|
||||
* <img class="hero__art" src="art.svg" alt="" />
|
||||
* </section>
|
||||
*
|
||||
* CSS prerequisite: each split line wrapper needs overflow:hidden for the mask reveal:
|
||||
* .hero__title .line { overflow: hidden; }
|
||||
*
|
||||
* Install: npm i gsap @gsap/react
|
||||
*/
|
||||
|
||||
import gsap from "gsap";
|
||||
import { SplitText } from "gsap/SplitText";
|
||||
|
||||
gsap.registerPlugin(SplitText);
|
||||
|
||||
export function playHeroIntro(root = document) {
|
||||
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
const q = gsap.utils.selector(root);
|
||||
|
||||
// Reduced motion: reveal everything instantly, no animation.
|
||||
if (reduceMotion) {
|
||||
gsap.set(q(".hero__eyebrow, .hero__sub, .hero__actions, .hero__art"), { autoAlpha: 1 });
|
||||
gsap.set(q(".hero__title"), { autoAlpha: 1 });
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Wait for fonts so SplitText computes correct line breaks.
|
||||
return document.fonts.ready.then(() => {
|
||||
const titleEl = q(".hero__title")[0];
|
||||
const split = new SplitText(titleEl, {
|
||||
type: "lines",
|
||||
linesClass: "line",
|
||||
autoSplit: true, // GSAP 3.13+: re-split on resize/font swap
|
||||
});
|
||||
|
||||
// Hidden start state (prevents flash of unstyled content).
|
||||
gsap.set(q(".hero__eyebrow, .hero__sub, .hero__actions, .hero__art"), { autoAlpha: 0 });
|
||||
gsap.set(titleEl, { autoAlpha: 1 });
|
||||
|
||||
const tl = gsap.timeline({
|
||||
defaults: { ease: "power3.out", duration: 0.8 },
|
||||
onComplete: () => split.revert(), // restore clean DOM once done (optional)
|
||||
});
|
||||
|
||||
tl.from(".hero__eyebrow", { y: 16, autoAlpha: 0, duration: 0.5 })
|
||||
.from(split.lines, { yPercent: 110, stagger: 0.12, duration: 0.9, ease: "power4.out" }, "-=0.2")
|
||||
.from(".hero__sub", { y: 20, autoAlpha: 0 }, "-=0.6")
|
||||
.from(".hero__actions .btn", { y: 12, autoAlpha: 0, stagger: 0.1, duration: 0.5 }, "-=0.4")
|
||||
.from(".hero__art", { scale: 0.92, autoAlpha: 0, ease: "back.out(1.4)", duration: 1.1 }, "<0.1");
|
||||
|
||||
return tl;
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-run on DOM ready when imported directly in a page.
|
||||
if (typeof window !== "undefined" && document.querySelector(".hero")) {
|
||||
if (document.readyState !== "loading") playHeroIntro();
|
||||
else document.addEventListener("DOMContentLoaded", () => playHeroIntro());
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<!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>
|
||||
Reference in new issue
Block a user