home: scroll-enter lands at the very top; bloomword motion front-loaded; webshops copy

Scroll-enter: the triggering gesture's MOMENTUM outlives the 800ms wave (a trackpad
flick trails ~1.5s), and those ticks scrolled the revealed page so you landed below
the hero. The scroll lock now holds until the input goes quiet (250ms without a
wheel/touch event, 3s cap) and releases exactly at the top. Verified with a simulated
14-tick 1.5s flick: lands scrollY=0, and a fresh gesture afterwards scrolls normally.

Bloomword recut: the loop opened with a 1s bloom hold so hover felt dead (movement
backloaded). Now a 0.2s hold then a brisk 3x retract into the full regrow, closing bob
at the end; still frame0==frameN (seam 0.0000), 390KB.

Webshops tagline: 'One platform, unlimited possibilities.'
This commit is contained in:
jwinterm
2026-07-12 08:19:45 -04:00
parent e3ce7bdbba
commit e1f7f3c49d
3 changed files with 40 additions and 3 deletions
Binary file not shown.
+39 -2
View File
@@ -107,9 +107,46 @@ export function HomeShell({ children }: { children: ReactNode }) {
sessionStorage.setItem("such:entered", "1");
setEntered(true);
setLeaving(true);
// Scroll-intent entries arrive with wheel/touch MOMENTUM still flowing; without a
// lock those trailing events scroll the freshly-revealed page down during the wave,
// so you land below the hero instead of at the very top (same as a click would).
// Freeze scrolling for the transition, then release exactly at the top.
const html = document.documentElement;
const prevOverflow = html.style.overflow;
html.style.overflow = "hidden";
window.scrollTo(0, 0);
// Any scroll that sneaks past the overflow lock gets yanked back to 0.
const pin = () => window.scrollTo(0, 0);
window.addEventListener("scroll", pin, { passive: true });
// The gesture that triggered the enter keeps emitting MOMENTUM (a trackpad flick
// can trail for ~1.5s — longer than the 800ms wave). Releasing the lock on a fixed
// timer let those trailing ticks scroll the revealed page, so you landed below the
// hero instead of at the very top. Hold the lock until the input goes QUIET
// (250ms with no wheel/touch), capped at 3s so a deliberate scroller is never trapped.
let lastInput = performance.now();
const note = () => { lastInput = performance.now(); };
window.addEventListener("wheel", note, { passive: true });
window.addEventListener("touchmove", note, { passive: true });
const release = () => {
window.removeEventListener("scroll", pin);
window.removeEventListener("wheel", note);
window.removeEventListener("touchmove", note);
html.style.overflow = prevOverflow;
window.scrollTo(0, 0);
};
const started = performance.now();
const tryRelease = () => {
const now = performance.now();
if (now - lastInput > 250 || now - started > 3000) release();
else setTimeout(tryRelease, 100);
};
// Keep the splash mounted briefly as a fixed, fading overlay so it cross-fades
// into the revealed content instead of hard-cutting.
setTimeout(() => setSplashUp(false), 800);
// into the revealed content instead of hard-cutting; release the scroll lock only
// once the wave is done AND the triggering gesture's momentum has died down.
setTimeout(() => {
setSplashUp(false);
tryRelease();
}, 800);
};
return (
@@ -67,7 +67,7 @@ type Item = {
// (r2,c3) / (r3,c1-2) / (r3,c3).
// Videos play on hover/focus on desktop and while on screen on no-hover (touch) devices.
const items: Item[] = [
{ name: "Webshops", tagline: "Custom storefronts we build and host. One platform, five completely different shops.", color: "emerald", span: 2, rows: 2, image: "/showcase/reels/webshops.jpg", alt: "Five differently themed demo storefronts built on the same platform", href: "/products/webshops", vignette: { webm: "/showcase/reels/webshops.webm", poster: "/showcase/reels/webshops.jpg", mp4: "/showcase/reels/webshops.mp4", fit: "cover" } },
{ name: "Webshops", tagline: "Custom storefronts we build and host. One platform, unlimited possibilities.", color: "emerald", span: 2, rows: 2, image: "/showcase/reels/webshops.jpg", alt: "Differently themed demo storefronts built on the same platform", href: "/products/webshops", vignette: { webm: "/showcase/reels/webshops.webm", poster: "/showcase/reels/webshops.jpg", mp4: "/showcase/reels/webshops.mp4", fit: "cover" } },
{ name: "Smirk Wallet", tagline: "Non-custodial browser wallet with social tipping.", color: "amber", span: 1, image: "/images/products/smirk-wallet.png", alt: "Smirk Wallet browser extension", href: "https://smirk.cash", external: true },
// Transparent chroma-keyed vignette (the engine can't emit real alpha — BW_FLAT magenta key):
// full-bloom tree at rest, hover retracts and regrows it. Palindrome loop, seam 0.0.