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
Binary file added public/assets/mru-144.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/assets/mru-96.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/mru-light-144.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/assets/mru-light-96.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 5 additions & 2 deletions scripts/sync-tokens.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,11 @@ const FILES = [
'ibm-plex-mono-500',
].map((f) => [`public/fonts/${f}.woff2`, `public/fonts/${f}.woff2`]),
...['jost', 'sourceserif4', 'ibmplexmono'].map((f) => [`public/fonts/OFL-${f}.txt`, `public/fonts/OFL-${f}.txt`]),
// The animated mark in the header (Mark.astro).
...['mru', 'mru-light'].map((f) => [`public/assets/${f}.gif`, `public/assets/${f}.gif`]),
// The animated mark (Mark.astro): the originals and the header copies.
...['mru', 'mru-light', 'mru-96', 'mru-light-96', 'mru-144', 'mru-light-144'].map((f) => [
`public/assets/${f}.gif`,
`public/assets/${f}.gif`,
]),
];

const check = process.argv.includes('--check');
Expand Down
31 changes: 2 additions & 29 deletions src/components/DocsTop.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// On phones the bar shows the brand and a Menu button, as on mru.space; the
// menu holds the docs nav, the header links, then the terms link and the
// theme toggle (the sidebar's foot, which phones do not show).
import MarkSvg from './MarkSvg.astro';
import Mark from './Mark.astro';
import DocsNav from './DocsNav.astro';
import ThemeToggle from './ThemeToggle.astro';
import { SITE } from '../data/site';
Expand All @@ -26,34 +26,7 @@ const links = [
<header class="mru part docs-top">
<div class="bar" style="padding-inline: var(--gut); max-width: 1440px; margin: 0 auto">
<a class="brand" href="/" aria-label="Mru docs home">
{/* The animated mark, as Mark.astro draws it, but from the 96 px copies
(mru-96.gif, about 140 KB, sharp at 2x) in place of the 2 MB
originals, which would hold up the page text on a slow link. */}
<span class="mark-anim">
<picture>
<img
class="on-light"
src="/assets/mru-light-96.gif"
alt=""
width="96"
height="96"
loading="lazy"
decoding="async"
fetchpriority="low"
/>
<img
class="on-dark"
src="/assets/mru-96.gif"
alt=""
width="96"
height="96"
loading="lazy"
decoding="async"
fetchpriority="low"
/>
</picture>
</span>
<MarkSvg class="mark-static" />
<Mark small alt="" />
<span class="word">Mru</span>
<span class="sub">Docs</span>
</a>
Expand Down
88 changes: 62 additions & 26 deletions src/components/Mark.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,49 @@
// The animated mark. The GIFs carry no alpha: light art multiplies on paper,
// dark art screens on ink (see README). No transform, filter, opacity or
// z-index may sit on this element or any ancestor, or the blend breaks.
// Both images are lazy: only the one the theme shows gets a layout box, so
// the other never downloads. Nothing stands in while the GIF loads: the
// space stays empty, then the animation shows. The GIFs are about 2 MB, so
// they fetch at low priority: the fonts and the hero text paint first.
// Reduced motion hides both and shows the SVG.
// One <picture> holds both arts: the <source> picks the dark GIF when the
// system prefers dark, the <img> carries the light one. Only one downloads,
// and the request leaves with the HTML, before the stylesheet arrives. The
// script swaps the source when the viewer has chosen a theme (data-theme on
// <html>) and whenever that choice, or the system theme, changes.
// `small` (the header and footer brand, 48 px on the page) loads 96 px and
// 144 px copies, sharp at 2x and 3x, about 125 KB and 230 KB, at once. The
// rest load the 600 px originals, about 2 MB, lazily and at low priority:
// the fonts and the page text paint first. Nothing stands in while the GIF
// loads: the space stays empty, then the animation shows. Reduced motion
// hides the GIF and shows the SVG.
import MarkSvg from './MarkSvg.astro';

interface Props {
size?: number;
staticSize?: number;
gap?: number;
alt?: string;
small?: boolean;
}
const { size, staticSize, gap, alt = 'The Mru mark: a ring, a tilted orbit and an M' } = Astro.props;
const { size, staticSize, gap, alt = 'The Mru mark: a ring, a tilted orbit and an M', small = false } = Astro.props;
// Inline sizes only where a page sets them, as on the canvas; otherwise the
// stylesheet sizes the mark (104 px, 88 px on phones).
const style = (s?: number) =>
[s === undefined ? '' : `width: ${s}px; height: ${s}px`, gap === undefined ? '' : `margin-bottom: ${gap}px`]
.filter(Boolean)
.join('; ') || undefined;
const px = small ? 96 : 600;
const darkSet = small ? '/assets/mru-96.gif 2x, /assets/mru-144.gif 3x' : '/assets/mru.gif';
const lightSet = small ? '/assets/mru-light-96.gif 2x, /assets/mru-light-144.gif 3x' : undefined;
const light = small ? '/assets/mru-light-96.gif' : '/assets/mru-light.gif';
---

<span class="mark-anim" style={style(size)}>
<picture>
<source media="(prefers-color-scheme: dark)" srcset={darkSet} />
<img
class="on-light"
src="/assets/mru-light.gif"
src={light}
srcset={lightSet}
alt={alt}
width="600"
height="600"
loading="lazy"
decoding="async"
fetchpriority="low"
/>
<img
class="on-dark"
src="/assets/mru.gif"
alt=""
width="600"
height="600"
loading="lazy"
width={px}
height={px}
loading={small ? undefined : 'lazy'}
decoding="async"
fetchpriority="low"
/>
Expand All @@ -52,10 +54,44 @@ const style = (s?: number) =>

<script>
// Show each GIF once it has loaded, so a slow or failed load shows an empty
// space, never a half-drawn frame or a broken-image icon.
for (const img of document.querySelectorAll<HTMLImageElement>('.mark-anim img')) {
const play = () => img.closest('.mark-anim')?.classList.add('playing');
if (img.complete && img.naturalWidth > 0) play();
else img.addEventListener('load', play, { once: true });
// space, never a half-drawn frame or a broken-image icon. Keep the picture's
// choice of art in step with the theme: the <source> only knows the system
// theme, so a viewer's own choice (data-theme) swaps it here. While the
// other art loads the GIF hides again, or the old frame would show through
// the new blend mode as a solid square.
const system = matchMedia('(prefers-color-scheme: dark)');
const QUERY = '(prefers-color-scheme: dark)';
const marks = [...document.querySelectorAll<HTMLElement>('.mark-anim')].flatMap((box) => {
const source = box.querySelector('source');
const img = box.querySelector('img');
// At parse time the picture chose by the system theme alone.
return source && img ? [{ box, source, img, dark: system.matches, pending: false }] : [];
});
const wantDark = () => {
const chosen = document.documentElement.getAttribute('data-theme');
return chosen === 'dark' || (chosen !== 'light' && system.matches);
};
const sync = () => {
const want = wantDark();
const chosen = document.documentElement.getAttribute('data-theme');
const media = chosen === 'dark' ? 'all' : chosen === 'light' ? 'not all' : QUERY;
for (const m of marks) {
if (m.dark !== want) {
m.dark = want;
m.pending = true;
m.box.classList.remove('playing');
}
if (m.source.media !== media) m.source.media = media;
}
};
for (const m of marks) {
m.img.addEventListener('load', () => {
m.pending = false;
m.box.classList.add('playing');
});
}
sync();
for (const m of marks) if (!m.pending && m.img.complete && m.img.naturalWidth > 0) m.box.classList.add('playing');
new MutationObserver(sync).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
system.addEventListener('change', sync);
</script>
22 changes: 2 additions & 20 deletions src/styles/shared/mru.css
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,8 @@
padding-top: 12px;
}

/* The mark */
/* The mark. One <img> per mark (Mark.astro): the <picture> picks the art
for the theme, so only the blend mode changes here. */
.mark-anim {
width: 104px;
height: 104px;
Expand All @@ -487,9 +488,6 @@
height: 100%;
mix-blend-mode: multiply;
}
.mark-anim .on-dark {
display: none;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) .mark-anim img {
mix-blend-mode: screen;
Expand All @@ -498,22 +496,6 @@
:root[data-theme='dark'] .mark-anim img {
mix-blend-mode: screen;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) .mark-anim .on-light {
display: none;
}
}
:root[data-theme='dark'] .mark-anim .on-light {
display: none;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) .mark-anim .on-dark {
display: block;
}
}
:root[data-theme='dark'] .mark-anim .on-dark {
display: block;
}
.mark-static {
display: none;
}
Expand Down