123 lines
4.3 KiB
Plaintext
123 lines
4.3 KiB
Plaintext
---
|
|
/**
|
|
* Island: the animated colour field behind the hero.
|
|
*
|
|
* The renderer is in scripts/color-bends.ts; this is the layer it draws into and the
|
|
* fallback for everyone who never reaches it. The canvas is decorative, so it is
|
|
* hidden from assistive technology and takes no pointer events: the hero's search box
|
|
* sits on top of it and has to stay clickable.
|
|
*
|
|
* With JavaScript off, without WebGL, or in the moment before the first frame, the
|
|
* gradient below is all there is — the same colour in the same place, holding still.
|
|
* The hero never renders as a blank band.
|
|
*/
|
|
import type { ColorBendsOptions } from '../scripts/color-bends';
|
|
|
|
interface Props extends ColorBendsOptions {
|
|
/** Opacity of the whole layer. The shader parameters stay as tuned; this is the mix. */
|
|
opacity?: number;
|
|
class?: string;
|
|
}
|
|
|
|
const { opacity = 0.55, class: className, ...bends } = Astro.props;
|
|
|
|
/**
|
|
* Serialised for the module script, which is a separate file and cannot read props.
|
|
* The renderer's own defaults fill in anything not passed here.
|
|
*/
|
|
const config = JSON.stringify(bends);
|
|
const first = bends.colors?.[0] ?? '#A78BFA';
|
|
---
|
|
|
|
<div class={['color-bends', className].filter(Boolean).join(' ')} style={`--cb-opacity:${opacity};--cb-tint:${first}`}>
|
|
<canvas data-color-bends data-config={config} aria-hidden="true"></canvas>
|
|
</div>
|
|
|
|
<style>
|
|
.color-bends {
|
|
position: absolute;
|
|
inset: 0;
|
|
overflow: hidden;
|
|
/* Decoration, behind the hero's own content and deaf to the pointer. */
|
|
z-index: 0;
|
|
pointer-events: none;
|
|
opacity: var(--cb-opacity);
|
|
/*
|
|
* The field is a wash, not a panel: it fades out before it reaches any edge, so
|
|
* there is no rectangle for the eye to find and no seam where it meets the pulse
|
|
* strip below. Both axes, multiplied together by the two mask layers.
|
|
*/
|
|
-webkit-mask-image:
|
|
linear-gradient(to bottom, transparent, #000 22%, #000 62%, transparent 100%),
|
|
linear-gradient(to right, transparent, #000 18%, #000 82%, transparent);
|
|
-webkit-mask-composite: source-in;
|
|
mask-image:
|
|
linear-gradient(to bottom, transparent, #000 22%, #000 62%, transparent 100%),
|
|
linear-gradient(to right, transparent, #000 18%, #000 82%, transparent);
|
|
mask-composite: intersect;
|
|
}
|
|
|
|
/*
|
|
* The base wash, under the canvas and never removed: the bands are sparse at this
|
|
* scale and it is what fills the space between them. It doubles as the fallback,
|
|
* which is why nothing has to be swapped in when there is no WebGL — what is left is
|
|
* this, and it reads as a deliberate glow rather than a hole. `--cb-tint` is the
|
|
* first colour passed to the shader, so the two agree without repeating the hex.
|
|
*/
|
|
.color-bends::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0;
|
|
background: radial-gradient(120% 80% at 50% 18%, var(--cb-tint), transparent 62%);
|
|
opacity: .5;
|
|
}
|
|
|
|
.color-bends canvas {
|
|
position: absolute;
|
|
inset: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
display: block;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
import { createColorBends, type ColorBendsOptions, type ColorBendsHandle } from '../scripts/color-bends';
|
|
import { onLeave, onReady } from '../scripts/reveal';
|
|
|
|
const setup = () => {
|
|
const canvas = document.querySelector<HTMLCanvasElement>('[data-color-bends]');
|
|
if (!canvas) return;
|
|
|
|
let options: ColorBendsOptions = {};
|
|
try {
|
|
options = JSON.parse(canvas.dataset.config ?? '{}') as ColorBendsOptions;
|
|
} catch {
|
|
// Unreadable config is not worth a dead hero: the renderer's defaults will do.
|
|
}
|
|
|
|
let handle: ColorBendsHandle | null = null;
|
|
/*
|
|
* One frame's delay. The hero's own entrance is the first thing that happens on
|
|
* this page and compiling a shader on the same frame stutters it; nothing is
|
|
* missing in the meantime, because the gradient fallback is already on screen.
|
|
*/
|
|
const armed = requestAnimationFrame(() => {
|
|
handle = createColorBends(canvas, options);
|
|
});
|
|
|
|
/*
|
|
* The router replaces this page's DOM but not its JavaScript. Without this the
|
|
* context outlives its canvas, and browsers drop the oldest live context once
|
|
* enough of them pile up — which would blank a hero somebody navigated back to.
|
|
*/
|
|
onLeave(() => {
|
|
cancelAnimationFrame(armed);
|
|
handle?.destroy();
|
|
handle = null;
|
|
});
|
|
};
|
|
|
|
onReady(setup);
|
|
</script>
|