Mobile responsive pass

- PixelLogo scales to viewport via a responsive hook so FACERE no
  longer overflows narrow screens.
- styles.css mobile breakpoints (760px / 480px / short-landscape)
  unwrap headlines/body copy, compact the HUD, allow the install
  command to wrap on tiny phones, and re-aim the bg pan origins so
  the meaningful slice of each landscape image stays in frame.
- Shorten scroll-track to 480vh on phones — the GSAP timeline still
  scrubs by progress so the choreography is preserved, just less
  exhausting to swipe through.
- Bump cache-busting query string on css/jsx.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-03 03:04:56 +08:00
parent 5029c5db6f
commit 277d161a95
3 changed files with 129 additions and 22 deletions

47
app.jsx
View File

@@ -68,11 +68,46 @@ const GLYPHS = {
],
};
function PixelLogo({ word = "FACERE", scale = 11, gap = 0 }) {
function useResponsiveLogoScale(word = "FACERE") {
const [scale, setScale] = useState(() => computeLogoScale(word));
useEffect(() => {
let raf = 0;
const onResize = () => {
cancelAnimationFrame(raf);
raf = requestAnimationFrame(() => setScale(computeLogoScale(word)));
};
window.addEventListener("resize", onResize);
window.addEventListener("orientationchange", onResize);
return () => {
cancelAnimationFrame(raf);
window.removeEventListener("resize", onResize);
window.removeEventListener("orientationchange", onResize);
};
}, [word]);
return scale;
}
function computeLogoScale(word) {
// Total logo width = letters * GLYPH_W * scale + (letters-1) * scale * 1.6
// Solve for scale that fits the viewport with side margins.
if (typeof window === "undefined") return 11;
const vw = window.innerWidth;
const letters = word.length;
const sideMargin = vw < 480 ? 0.10 : vw < 760 ? 0.12 : 0.16; // each side
const usable = vw * (1 - 2 * sideMargin);
const denom = letters * GLYPH_W + (letters - 1) * 1.6;
const fit = Math.floor(usable / denom);
// Clamp so it doesn't get too small or absurdly large on ultrawide.
return Math.max(4, Math.min(11, fit));
}
function PixelLogo({ word = "FACERE", scale, gap = 0 }) {
const responsiveScale = useResponsiveLogoScale(word);
const px = scale ?? responsiveScale;
const letters = word.split("");
return (
<div className="pixel-logo" data-text={word}>
<div className="pixel-logo-grid" style={{ gap: `0 ${scale * 1.6}px` }}>
<div className="pixel-logo-grid" style={{ gap: `0 ${px * 1.6}px` }}>
{letters.map((ch, li) => {
const g = GLYPHS[ch];
if (!g) return null;
@@ -81,8 +116,8 @@ function PixelLogo({ word = "FACERE", scale = 11, gap = 0 }) {
key={li}
className="pixel-letter"
style={{
gridTemplateColumns: `repeat(${GLYPH_W}, ${scale}px)`,
gridTemplateRows: `repeat(${GLYPH_H}, ${scale}px)`,
gridTemplateColumns: `repeat(${GLYPH_W}, ${px}px)`,
gridTemplateRows: `repeat(${GLYPH_H}, ${px}px)`,
gap: `${gap}px`,
}}
>
@@ -100,10 +135,10 @@ function PixelLogo({ word = "FACERE", scale = 11, gap = 0 }) {
</div>
{/* Ghost layers for subtle electric jitter */}
<div className="pixel-logo-ghost ghost-r" aria-hidden="true">
<PixelLogoStatic word={word} scale={scale} gap={gap} color="rgba(255,80,120,0.45)" />
<PixelLogoStatic word={word} scale={px} gap={gap} color="rgba(255,80,120,0.45)" />
</div>
<div className="pixel-logo-ghost ghost-c" aria-hidden="true">
<PixelLogoStatic word={word} scale={scale} gap={gap} color="rgba(33,234,255,0.55)" />
<PixelLogoStatic word={word} scale={px} gap={gap} color="rgba(33,234,255,0.55)" />
</div>
</div>
);