fix(www): prevent flicker and reveal macos-hint on landing page (#27)
Hide entry-animated elements at first paint (html.anime-ready guard in global.css) so the landing page no longer flashes visible content before anime.js loads. Adds a <noscript> fallback and a JS load-error fallback to guarantee content is never permanently hidden, and reveals the hero immediately for prefers-reduced-motion users. Includes #macos-hint in the hero entrance timeline so the macOS command box animates in correctly.
This commit is contained in:
+17
-3
@@ -122,9 +122,16 @@
|
|||||||
const script = document.createElement("script");
|
const script = document.createElement("script");
|
||||||
script.src = "/vendor/anime.min.js";
|
script.src = "/vendor/anime.min.js";
|
||||||
script.onload = () => callback(window.anime);
|
script.onload = () => callback(window.anime);
|
||||||
|
script.onerror = () => revealAnimatedContent();
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Revert the entry-animation hiding (html.anime-ready CSS) so content is
|
||||||
|
// always visible even if the animation library never loads.
|
||||||
|
function revealAnimatedContent() {
|
||||||
|
document.documentElement.classList.remove("anime-ready");
|
||||||
|
}
|
||||||
|
|
||||||
loadAnime((anime) => {
|
loadAnime((anime) => {
|
||||||
// Hero entrance
|
// Hero entrance
|
||||||
const hero = document.getElementById("hero");
|
const hero = document.getElementById("hero");
|
||||||
@@ -134,17 +141,24 @@
|
|||||||
const subtitle = hero.querySelector(".hero-animate > p:nth-of-type(2)");
|
const subtitle = hero.querySelector(".hero-animate > p:nth-of-type(2)");
|
||||||
const buttons = hero.querySelector(".hero-animate > div");
|
const buttons = hero.querySelector(".hero-animate > div");
|
||||||
const screenshot = hero.querySelector(".hero-screenshot");
|
const screenshot = hero.querySelector(".hero-screenshot");
|
||||||
|
const macosHint = hero.querySelector("#macos-hint");
|
||||||
|
|
||||||
const heroTl = anime.timeline({ easing: "easeOutExpo" });
|
if (reducedMotion) {
|
||||||
|
// Reduced motion: reveal the hero immediately (CSS hides it at first
|
||||||
|
// paint to avoid flicker, but the timeline below is skipped). Section
|
||||||
|
// reveals below still run through the IntersectionObserver.
|
||||||
|
anime.set([eyebrow, h1, subtitle, buttons, screenshot, macosHint], { opacity: 1, translateY: 0 });
|
||||||
|
} else {
|
||||||
|
const heroTl = anime.timeline({ easing: "easeOutExpo" });
|
||||||
|
|
||||||
if (!reducedMotion) {
|
anime.set([eyebrow, h1, subtitle, buttons, screenshot, macosHint], { opacity: 0, translateY: 20 });
|
||||||
anime.set([eyebrow, h1, subtitle, buttons, screenshot], { opacity: 0, translateY: 20 });
|
|
||||||
anime.set(screenshot, { opacity: 0, scale: 0.96, translateY: 0 });
|
anime.set(screenshot, { opacity: 0, scale: 0.96, translateY: 0 });
|
||||||
|
|
||||||
if (eyebrow) heroTl.add({ targets: eyebrow, opacity: [0, 1], translateY: [15, 0] }, 0);
|
if (eyebrow) heroTl.add({ targets: eyebrow, opacity: [0, 1], translateY: [15, 0] }, 0);
|
||||||
if (h1) heroTl.add({ targets: h1, opacity: [0, 1], translateY: [30, 0], duration: 800 }, 100);
|
if (h1) heroTl.add({ targets: h1, opacity: [0, 1], translateY: [30, 0], duration: 800 }, 100);
|
||||||
if (subtitle) heroTl.add({ targets: subtitle, opacity: [0, 1], translateY: [20, 0] }, 250);
|
if (subtitle) heroTl.add({ targets: subtitle, opacity: [0, 1], translateY: [20, 0] }, 250);
|
||||||
if (buttons) heroTl.add({ targets: buttons, opacity: [0, 1], translateY: [20, 0] }, 350);
|
if (buttons) heroTl.add({ targets: buttons, opacity: [0, 1], translateY: [20, 0] }, 350);
|
||||||
|
if (macosHint) heroTl.add({ targets: macosHint, opacity: [0, 1], translateY: [20, 0] }, 400);
|
||||||
if (screenshot) heroTl.add({ targets: screenshot, opacity: [0, 1], scale: [0.96, 1], duration: 900 }, 450);
|
if (screenshot) heroTl.add({ targets: screenshot, opacity: [0, 1], scale: [0.96, 1], duration: 900 }, 450);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const isProd = import.meta.env.PROD;
|
|||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en" data-theme-auto>
|
<html lang="en" data-theme-auto class="anime-ready">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
@@ -64,6 +64,21 @@ const isProd = import.meta.env.PROD;
|
|||||||
|
|
||||||
<Seo jsonLd={jsonLd} />
|
<Seo jsonLd={jsonLd} />
|
||||||
|
|
||||||
|
{
|
||||||
|
// No-JS safety net: without JavaScript the anime.js entrance timeline
|
||||||
|
// never runs, so force every entry-animated element back to visible.
|
||||||
|
// (global.css hides them via the html.anime-ready selector.)
|
||||||
|
}
|
||||||
|
<noscript>
|
||||||
|
<style>
|
||||||
|
html.anime-ready .hero-animate > *,
|
||||||
|
html.anime-ready .hero-screenshot,
|
||||||
|
html.anime-ready .section-animate > * {
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</noscript>
|
||||||
|
|
||||||
{
|
{
|
||||||
isProd && (
|
isProd && (
|
||||||
<script
|
<script
|
||||||
|
|||||||
@@ -32,6 +32,20 @@ html.light {
|
|||||||
--accent: #d97706;
|
--accent: #d97706;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Entry-animated elements start hidden (opacity 0) to prevent a flash of
|
||||||
|
* content before landing.js loads anime.js and runs the entrance timeline.
|
||||||
|
* The html.anime-ready class is present from first paint; animated elements
|
||||||
|
* are revealed by the anime.js timeline / IntersectionObserver (immediately
|
||||||
|
* for prefers-reduced-motion users). A <noscript> override in Layout.astro
|
||||||
|
* plus a JS script-load fallback guarantee content is never left hidden.
|
||||||
|
*/
|
||||||
|
html.anime-ready .hero-animate > *,
|
||||||
|
html.anime-ready .hero-screenshot,
|
||||||
|
html.anime-ready .section-animate > * {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
background-color: var(--bg);
|
background-color: var(--bg);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
|||||||
Reference in New Issue
Block a user