Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 3 additions & 13 deletions src/app/globals.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -110,23 +110,13 @@ body {
}
}

/* Masthead logo layering:
- `.logo-touch-only` is the full 2D SVG with corner triangles —
shown on touch / small screens, hidden on desktop.
- `.logo-desktop-only` is a static "sphere placeholder" — hidden on
touch, shown on desktop so first paint is not a blank slot while
the 3D chunk loads. The real 3D mounts on top once ready and
covers the placeholder cleanly (same circular silhouette). */
.logo-desktop-only {
display: none;
}
/* Masthead logo: hide the SVG fallback on desktop (real mouse + ≥ 768px)
so it never paints alongside the 3D sphere. Touch / small screens
ignore this and keep the SVG as the only logo. */
@media (pointer: fine) and (min-width: 768px) {
.logo-touch-only {
display: none;
}
.logo-desktop-only {
display: block;
}
}

/* Tabular nums for everything that displays numbers */
Expand Down
80 changes: 15 additions & 65 deletions src/components/site-logo-switcher.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,74 +2,32 @@

/**
* Hybrid masthead logo: 2D SVG on touch / small screens, interactive 3D
* sphere on desktop. The 3D bundle is loaded with `next/dynamic` so it
* only ships when actually mounted.
* sphere on desktop.
*
* Three layers stacked in the same slot:
* 1. `<SiteLogo>` (logo-touch-only) — the full brand SVG with corner
* triangles; visible on touch / small screens only.
* 2. `<SpherePlaceholder>` (logo-desktop-only) — a tiny static SVG of
* a sphere with the C-mark centred; paints instantly on desktop's
* first frame, so the slot is never blank while the 3D chunk loads.
* 3. `<SiteLogo3D>` — mounted once enable3D + chunk are ready; covers
* the placeholder byte-for-byte (same circular silhouette), so the
* swap is visually invisible.
* The 3D component is imported statically (not via next/dynamic) so the
* sphere mounts as soon as the main JS executes — no separate chunk to
* download. Trade-off: the three.js code ends up in the shared client
* bundle (~150 KB gz), which mobile users also fetch even though they
* never mount the component. Worth it because the previous dynamic
* setup left a visible blank slot while the chunk loaded.
*
* Layering:
* - `<SiteLogo>` (logo-touch-only) — the full brand SVG with corner
* triangles; visible on touch / small screens only.
* - `<SiteLogo3D>` — overlaid on top once enable3D flips to true on
* desktop. The SVG is CSS-hidden on desktop (`logo-touch-only`) so
* nothing ever paints alongside the 3D there.
*
* Detection: `(pointer: fine) and (min-width: 768px)`. Touchscreen
* laptops with a mouse still qualify as desktop.
*/

import dynamic from "next/dynamic";
import { useEffect, useState } from "react";
import { SiteLogo } from "@/components/site-logo";
import { SiteLogo3D } from "@/components/site-logo-3d";

const DESKTOP_MQ = "(pointer: fine) and (min-width: 768px)";

const SiteLogo3D = dynamic(
() => import("@/components/site-logo-3d").then((m) => m.SiteLogo3D),
{ ssr: false, loading: () => null },
);

/**
* Static SVG that mimics the 3D sphere's silhouette + centred C-mark.
* Identical proportions to <SiteLogo3D> so the real 3D canvas can
* cover this byte-for-byte without a visible transition.
*/
function SpherePlaceholder({ size }: { size: number }) {
return (
<svg
width={size}
height={size}
viewBox="0 0 100 100"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
>
{/* Sphere body — matches --color-surface so it blends with the
header just like the 3D sphere does. */}
<circle cx="50" cy="50" r="42" fill="var(--color-surface)" />
{/* C-mark centred inside the sphere — same geometry as <SiteLogo>
(cx=45 cy=50 r=45, inner ellipse rx=22 ry=40, right rect),
scaled to ~55% so it sits in the visible front face the same
way it does on the 3D sphere. */}
<g transform="translate(50 50) scale(0.55) translate(-45 -50)">
<mask id="site-logo-3d-fallback-mask">
<rect width="100" height="100" fill="white" />
<ellipse cx="45" cy="50" rx="22" ry="40" fill="black" />
<rect x="45" y="38" width="55" height="24" fill="black" />
</mask>
<circle
cx="45"
cy="50"
r="45"
fill="var(--color-ink)"
mask="url(#site-logo-3d-fallback-mask)"
/>
</g>
</svg>
);
}

export function SiteLogoSwitcher({ size = 22 }: { size?: number }) {
const [enable3D, setEnable3D] = useState(false);

Expand All@@ -78,11 +36,6 @@ export function SiteLogoSwitcher({ size = 22 }: { size?: number }) {
const sync = () => setEnable3D(mq.matches);
sync();
mq.addEventListener("change", sync);

// Eagerly fetch the 3D chunk on desktop so it is in the browser
// cache by the time React tries to mount <SiteLogo3D>.
if (mq.matches) import("@/components/site-logo-3d");

return () => mq.removeEventListener("change", sync);
}, []);

Expand All@@ -91,9 +44,6 @@ export function SiteLogoSwitcher({ size = 22 }: { size?: number }) {
<div className="absolute inset-0 logo-touch-only">
<SiteLogo size={size} />
</div>
<div className="absolute inset-0 logo-desktop-only">
<SpherePlaceholder size={size} />
</div>
{enable3D && (
<div className="absolute inset-0">
<SiteLogo3D size={size} />
Expand Down
Loading