Add logo loading screen gated on animation readiness
- Full-screen loader whose only content is the logo animating: the "B" draws in and fills, the base facets trace in, then a wave runs along them until the page is ready (pure CSS, plays before JS) - Loader gate holds the loader until GSAP + fonts and every 3D scene's first frame are ready, then waits 1s (min 1.4s total) before leaving; hard 8s limit, head-script fallback if JS never boots - Scroll reveals, SplitText and scene entry animations start only once the loader leaves, so above-the-fold entries play in view - Internal links fade the loader back in before navigating, so page changes are one seamless dark transition - No loader for no-JS or reduced-motion visitors Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { gsap } from "gsap";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
import { SplitText } from "gsap/SplitText";
|
||||
import { hideLoader, onLoaderDone, showLoader } from "../loader/gate";
|
||||
|
||||
/**
|
||||
* # Site-wide motion system
|
||||
@@ -19,6 +20,8 @@ import { SplitText } from "gsap/SplitText";
|
||||
*
|
||||
* Elements are pre-hidden by CSS only once `html.motion` is set by the inline
|
||||
* head script, so no-JS / reduced-motion visitors always see content.
|
||||
* Nothing is animated until the page loader is dismissed (../loader/gate.ts),
|
||||
* so above-the-fold entries play in view instead of behind the overlay.
|
||||
*/
|
||||
|
||||
type Variant =
|
||||
@@ -193,8 +196,9 @@ function scan(root: ParentNode) {
|
||||
});
|
||||
}
|
||||
|
||||
/** Fade the page out before following internal links. */
|
||||
/** Cover the page with the loader before following internal links. */
|
||||
function setupPageExit() {
|
||||
let leaving = false;
|
||||
const onClick = (e: MouseEvent) => {
|
||||
if (e.defaultPrevented || e.button !== 0) return;
|
||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
||||
@@ -206,17 +210,15 @@ function setupPageExit() {
|
||||
if (/\.(pdf|png|jpe?g|webp|gif|svg|zip)$/i.test(url.pathname)) return;
|
||||
|
||||
e.preventDefault();
|
||||
gsap.to("main", {
|
||||
autoAlpha: 0,
|
||||
y: -12,
|
||||
duration: 0.28,
|
||||
ease: "power2.in",
|
||||
onComplete: () => location.assign(url.href),
|
||||
});
|
||||
if (leaving) return;
|
||||
leaving = true;
|
||||
showLoader().then(() => location.assign(url.href));
|
||||
};
|
||||
// restore if the page comes back from the back/forward cache
|
||||
const onShow = (e: PageTransitionEvent) => {
|
||||
if (e.persisted) gsap.set("main", { clearProps: "all" });
|
||||
if (!e.persisted) return;
|
||||
leaving = false;
|
||||
hideLoader();
|
||||
};
|
||||
document.addEventListener("click", onClick);
|
||||
window.addEventListener("pageshow", onShow);
|
||||
@@ -241,8 +243,15 @@ export function initMotion() {
|
||||
registered = true;
|
||||
}
|
||||
|
||||
scan(document);
|
||||
setupPageExit();
|
||||
cleanups.push(onLoaderDone(start));
|
||||
|
||||
return destroyMotion;
|
||||
}
|
||||
|
||||
/** Runs once the loader leaves: animate what's on the page, watch for more. */
|
||||
function start() {
|
||||
scan(document);
|
||||
|
||||
// Pick up content React mounts later (tabs, lists, client fetches).
|
||||
observer = new MutationObserver((mutations) => {
|
||||
@@ -264,8 +273,6 @@ export function initMotion() {
|
||||
window.addEventListener("load", onLoad);
|
||||
document.fonts?.ready.then(onLoad).catch(() => {});
|
||||
cleanups.push(() => window.removeEventListener("load", onLoad));
|
||||
|
||||
return destroyMotion;
|
||||
}
|
||||
|
||||
function destroyMotion() {
|
||||
|
||||
Reference in New Issue
Block a user