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
73 changes: 70 additions & 3 deletions apps/site/src/components/Aurora.astro
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ import { version as gmtVersion } from "../../node_modules/@northguild/gmt/packag
import type { SiteTimes } from "../scripts/aurora.ahead";
import type {
AuroraMessage,
PageBox,
SkyLabel,
SkyLine,
WorkerMessage,
Expand Down Expand Up @@ -206,6 +207,61 @@ import { version as gmtVersion } from "../../node_modules/@northguild/gmt/packag
const info = document.querySelector<HTMLButtonElement>(".sky-info-button");
let post: ((message: AuroraMessage) => void) | null = null;

// Huginn's fly-in draws in the header, title, intro and cards as he
// reaches them. Base.astro hides them (`ng-await`) until the worker says
// he has reached each one; without a fly-in they come in by the usual
// entrance instead. The panes go first (the header, then the cards):
// the worker plans his path through the first two.
const page = document.documentElement;
const panes = page.classList.contains("ng-await")
? [
...document.querySelectorAll<HTMLElement>(".ng-enter-pane"),
...document.querySelectorAll<HTMLElement>(".ng-enter-text"),
]
: [];
function isAwaiting(): boolean {
return page.classList.contains("ng-await");
}
function paneBoxes(): PageBox[] {
if (!isAwaiting()) return [];
return panes.map((el) => {
const r = el.getBoundingClientRect();
return [r.left, r.top, r.width, r.height];
});
}
function showAllPanes(): void {
page.classList.remove("ng-await");
}
let lastAngle = 135;
let backstop = 0;
// Sweep the pane in toward `angle` (a CSS gradient angle), drifting a
// little along his line, over `duration` seconds.
function brushPane(i: number, angle: number, duration: number): void {
const el = panes[i];
if (!el || !isAwaiting() || el.classList.contains("ng-brushed")) return;
lastAngle = angle;
const a = (angle * Math.PI) / 180;
const [dx, dy] = [Math.sin(a), -Math.cos(a)];
const k = Math.max(Math.abs(dx), Math.abs(dy)) || 1;
el.style.setProperty("--ng-angle", `${angle}deg`);
el.style.setProperty(
"--ng-from",
`${50 + (50 * dx) / k}% ${50 + (50 * dy) / k}%`,
);
el.style.setProperty(
"--ng-to",
`${50 - (50 * dx) / k}% ${50 - (50 * dy) / k}%`,
);
el.style.setProperty("--ng-dx", dx.toFixed(3));
el.style.setProperty("--ng-dy", dy.toFixed(3));
el.style.setProperty("--ng-brush-duration", `${duration}s`);
el.classList.add("ng-brushed");
// Should the rest never come (the worker stalled), they follow anyway.
backstop ||= window.setTimeout(() => {
panes.forEach((_, j) => brushPane(j, lastAngle, 1.6));
}, 8000);
}

// All WebGL runs in the worker. Without OffscreenCanvas the CSS gradient
// stays, so the main thread never pays for the effect.
const canvas = document.querySelector<HTMLCanvasElement>(".aurora canvas");
Expand Down Expand Up @@ -235,12 +291,16 @@ import { version as gmtVersion } from "../../node_modules/@northguild/gmt/packag
site: chosen,
still: motion.matches,
hidden: document.hidden,
targets: paneBoxes(),
},
[offscreen],
);
worker.onmessage = ({ data }: MessageEvent<WorkerMessage>) => {
if (typeof data === "object") {
if ("pick" in data) {
if ("brush" in data) {
if (data.brush < 0) showAllPanes();
else brushPane(data.brush, data.angle ?? 135, data.duration ?? 1.6);
} else if ("pick" in data) {
showPick(data.pick);
} else if ("times" in data) {
showSiteTimes(data.times, data.zone);
Expand All @@ -251,7 +311,10 @@ import { version as gmtVersion } from "../../node_modules/@northguild/gmt/packag
return;
}
const on = data === "on";
if (!on) showPick(null);
if (!on) {
showPick(null);
showAllPanes();
}
canvas.classList.toggle("on", on);
if (info) info.hidden = !on;
};
Expand All @@ -270,12 +333,16 @@ import { version as gmtVersion } from "../../node_modules/@northguild/gmt/packag
type: "size",
width: canvas.clientWidth,
height: canvas.clientHeight,
targets: paneBoxes(),
}),
150,
);
}).observe(canvas);
}
if ("requestIdleCallback" in window) {
// The header and cards wait for the fly-in: start at once.
if (isAwaiting()) {
startWorker(canvas);
} else if ("requestIdleCallback" in window) {
requestIdleCallback(() => startWorker(canvas), { timeout: 1500 });
} else {
setTimeout(() => startWorker(canvas), 200);
Expand Down
101 changes: 101 additions & 0 deletions apps/site/src/layouts/Base.astro
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,27 @@ const {
type="font/woff2"
crossorigin
/>
{
entrance && (
<script is:inline>
// Huginn's fly-in paints the header and cards in as he passes
// (Aurora.astro); they wait for him, hidden. Only where the sky can
// run with motion, and never for more than 4 s: if he has not
// passed the first by then, the usual entrance plays.
if (
!matchMedia("(prefers-reduced-motion: reduce)").matches &&
"transferControlToOffscreen" in HTMLCanvasElement.prototype
) {
document.documentElement.classList.add("ng-await");
setTimeout(function showIfHuginnIsLate() {
if (!document.querySelector(".ng-brushed")) {
document.documentElement.classList.remove("ng-await");
}
}, 4000);
}
</script>
)
}
</head>
<body>
<Header entrance={entrance} />
Expand Down Expand Up @@ -160,6 +181,86 @@ const {
}
}

/* ---- Huginn's wake ----
* On the home page the header, title, intro and cards wait, hidden, for
* the fly-in
* (`ng-await`, set in the head). As Huginn passes each one the page adds
* `ng-brushed` with his heading: the same aurora light sweeps across it in
* the direction he flies (`--ng-angle`, from `--ng-from` to `--ng-to`),
* paced to him (`--ng-brush-duration`), and the pane drifts a few pixels
* along his line into place, as if his wings brushed it there.
*
* The mask is three times the pane's size, so the sweep's edge enters the
* pane a quarter of the way through and leaves it at three quarters: at a
* steady pace it trails just behind him as he crosses. */
html.ng-await :is(.ng-enter-pane, .ng-enter-text):not(.ng-brushed) {
animation-name: none;
opacity: 0;
}
:is(.ng-enter-pane, .ng-enter-text).ng-brushed {
--ng-sweep: linear-gradient(
var(--ng-angle, 135deg),
#000 38%,
transparent 62%
);
mask-size: 300% 300%;
mask-repeat: no-repeat;
animation-duration: var(--ng-brush-duration, 1.6s);
animation-timing-function: linear;
animation-delay: 0s;
}
.ng-enter-pane.ng-brushed {
animation-name: ng-brush-pane;
}
.ng-enter-text.ng-brushed {
animation-name: ng-brush-text;
}
@keyframes ng-brush-text {
0% {
mask-image: var(--ng-sweep);
mask-position: var(--ng-from, 100% 100%);
translate: calc(var(--ng-dx, 0.7) * -10px) calc(var(--ng-dy, 0.7) * -10px);
filter: blur(12px);
color: var(--ng-aurora);
text-shadow: 0 0 24px color(from var(--ng-aurora) srgb r g b / 0.7);
box-shadow: 0 0 48px 32px transparent;
}
45% {
filter: blur(3px);
color: var(--ng-aurora);
text-shadow: 0 0 18px color(from var(--ng-aurora) srgb r g b / 0.45);
}
100% {
mask-image: var(--ng-sweep);
mask-position: var(--ng-to, 0% 0%);
translate: 0 0;
filter: blur(0);
text-shadow: 0 0 0 transparent;
box-shadow: 0 0 48px 32px transparent;
}
}
@keyframes ng-brush-pane {
0% {
mask-image: var(--ng-sweep);
mask-position: var(--ng-from, 100% 100%);
translate: calc(var(--ng-dx, 0.7) * -10px) calc(var(--ng-dy, 0.7) * -10px);
filter: blur(10px);
border-color: color(from var(--ng-aurora) srgb r g b / 0.7);
box-shadow: 0 0 44px -4px color(from var(--ng-aurora) srgb r g b / 0.5);
}
45% {
filter: blur(2px);
border-color: color(from var(--ng-aurora) srgb r g b / 0.45);
box-shadow: 0 0 32px -6px color(from var(--ng-aurora) srgb r g b / 0.3);
}
100% {
mask-image: var(--ng-sweep);
mask-position: var(--ng-to, 0% 0%);
translate: 0 0;
filter: blur(0);
}
}

/* Reduced motion: no drifting light or blur, just a short fade. */
@media (prefers-reduced-motion: reduce) {
.ng-enter-text,
Expand Down
7 changes: 4 additions & 3 deletions apps/site/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@ const repos = [
pointer-events: auto;
}

/* The entrance (`.ng-enter-*`, Base.astro): the header leads, then the
title, the line under it, and the cards in turn, landing as the sky's own
fade-in (1.6s) finishes. */
/* Usually Huginn's fly-in draws these in as he reaches them (Aurora.astro).
Without it, the entrance (`.ng-enter-*`, Base.astro) plays: the header
leads, then the title, the line under it, and the cards in turn, landing
as the sky's own fade-in (1.6s) finishes. */
h1 {
--ng-delay: 0.25s;
}
Expand Down
Loading
Loading