feat: animate the logo in the navbar
This commit is contained in:
@@ -3,21 +3,43 @@ import { Link } from "react-router";
|
||||
import { cx, FOCUS_RING } from "./styles";
|
||||
import type { NavSurface } from "./types";
|
||||
|
||||
/* Exported from Figma logo/sharp-er (317:5571) and logo/sharp-er-dark
|
||||
(317:7812). Intrinsic 334x46; the navigation draws it at 100px wide, which
|
||||
is the designed size in every navigation frame (desktop and mobile). */
|
||||
const LOGO_SRC: Record<NavSurface, string> = {
|
||||
light: "/assets/sharp-er-logo.png",
|
||||
dark: "/assets/sharp-er-logo-dark.png",
|
||||
};
|
||||
/** One half of the animated brand lockup, drawn as its own SVG file. */
|
||||
interface BrandMark {
|
||||
src: string;
|
||||
/** Intrinsic size of the file, for the aspect ratio the browser reserves. */
|
||||
width: number;
|
||||
height: number;
|
||||
/** The mark's own drawn width, so the pair flexes to one scale. */
|
||||
drawnWidth: number;
|
||||
}
|
||||
|
||||
const LOGO_INTRINSIC_WIDTH = 334;
|
||||
const LOGO_INTRINSIC_HEIGHT = 46;
|
||||
/**
|
||||
* The SHARP-ER lockup, split into its two drawn pieces so the "-ER" can slide
|
||||
* out from behind the P and fade in once the page has settled (Navigation.css).
|
||||
* Each surface ships its own export of the two pieces: white on the dark
|
||||
* surface, the two-tone ink on the light surface. Both exports of a piece are
|
||||
* the exact same box (218x46 and 112x46), so the pair shares one scale; each
|
||||
* mark takes the share of the row its box gives it, and the suffix rides flush
|
||||
* against the P's right edge.
|
||||
*/
|
||||
const BRAND_LOCKUP: Record<
|
||||
NavSurface,
|
||||
{ base: BrandMark; suffix: BrandMark }
|
||||
> = {
|
||||
light: {
|
||||
base: { src: "/assets/sharp-wordmark.svg", width: 218, height: 46, drawnWidth: 218 },
|
||||
suffix: { src: "/assets/sharp-er-suffix.svg", width: 112, height: 46, drawnWidth: 112 },
|
||||
},
|
||||
dark: {
|
||||
base: { src: "/assets/sharp-wordmark-inverse.svg", width: 218, height: 46, drawnWidth: 218 },
|
||||
suffix: { src: "/assets/sharp-er-suffix-inverse.svg", width: 112, height: 46, drawnWidth: 112 },
|
||||
},
|
||||
};
|
||||
|
||||
export interface BrandProps {
|
||||
/** Locale-prefixed home path, e.g. "/en". */
|
||||
href: string;
|
||||
/** Accessible name of the link; the image itself is decorative. */
|
||||
/** Accessible name of the link; the images are decorative. */
|
||||
label: string;
|
||||
surface: NavSurface;
|
||||
className?: string;
|
||||
@@ -25,6 +47,8 @@ export interface BrandProps {
|
||||
|
||||
/** The SHARP-ER wordmark, linking home. Sits at the inline start of the bar. */
|
||||
export function Brand({ href, label, surface, className }: BrandProps) {
|
||||
const { base, suffix } = BRAND_LOCKUP[surface];
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={href}
|
||||
@@ -35,16 +59,39 @@ export function Brand({ href, label, surface, className }: BrandProps) {
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={LOGO_SRC[surface]}
|
||||
alt=""
|
||||
width={LOGO_INTRINSIC_WIDTH}
|
||||
height={LOGO_INTRINSIC_HEIGHT}
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
draggable={false}
|
||||
className="block h-auto w-[100px]"
|
||||
/>
|
||||
{/* `dir="ltr"` because a Latin wordmark never mirrors; its position on
|
||||
the bar still follows the page's direction. */}
|
||||
<span dir="ltr" className="flex w-[100px] items-start">
|
||||
<img
|
||||
src={base.src}
|
||||
alt=""
|
||||
width={base.width}
|
||||
height={base.height}
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
draggable={false}
|
||||
style={{ flex: `${base.drawnWidth} 1 0%` }}
|
||||
className="block h-auto min-w-0"
|
||||
/>
|
||||
|
||||
{/* The clipping box starts at the P's right edge, so the mark inside
|
||||
it can slide out from behind the P (Navigation.css). */}
|
||||
<span
|
||||
style={{ flex: `${suffix.drawnWidth} 1 0%` }}
|
||||
className="sh-brand-suffix block min-w-0"
|
||||
>
|
||||
<img
|
||||
src={suffix.src}
|
||||
alt=""
|
||||
width={suffix.width}
|
||||
height={suffix.height}
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
draggable={false}
|
||||
className="sh-brand-suffix__mark block h-auto w-full"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,54 @@
|
||||
animation: sh-nav-menu-in var(--motion-duration-base)
|
||||
var(--motion-ease-entrance) both;
|
||||
}
|
||||
|
||||
/* The "-ER" after "SHARP" in the brand, sliding out from behind the P and
|
||||
fading in once the page has settled. The same reveal the home hero used to
|
||||
run on its wordmark, now owned by the navigation. The suffix sits in its
|
||||
own box whose inline-start edge is the P's right edge, and that box clips;
|
||||
the mark starts a full box-width back - entirely beside "SHARP", so
|
||||
nothing overlaps it - and travels forward to rest, with the opacity
|
||||
reaching 1 before the travel ends, so the glyphs read as solid for the
|
||||
last, slowest part of the decelerating slide. The translate is physical
|
||||
(translateX) on purpose: the lockup row is pinned to dir="ltr" because a
|
||||
Latin wordmark never mirrors, so the P is always left of the suffix, in
|
||||
Arabic too. Pure CSS, so it runs from the server-rendered HTML without
|
||||
waiting for hydration, and the `both` fill keeps the suffix hidden through
|
||||
the delay instead of flashing in first. It replays whenever the navigation
|
||||
mounts again, e.g. on a client-side navigation back to a page. Under
|
||||
prefers-reduced-motion the animation is removed outright - app.css only
|
||||
collapses the duration and would leave the delay holding the suffix
|
||||
hidden - so the full name is simply there from the start. */
|
||||
.sh-brand-suffix {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sh-brand-suffix__mark {
|
||||
animation: sh-brand-suffix-in var(--motion-duration-reveal)
|
||||
var(--motion-ease-entrance) var(--motion-delay-reveal) both;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.sh-brand-suffix__mark {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sh-brand-suffix-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
60% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sh-nav-menu-in {
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
type Locale,
|
||||
} from "../LanguageSwitcher";
|
||||
|
||||
import "./Navigation.css";
|
||||
|
||||
import { Brand } from "./Brand";
|
||||
import { DesktopNav } from "./DesktopNav";
|
||||
import { MobileNav } from "./MobileNav";
|
||||
|
||||
Reference in New Issue
Block a user