@@ -0,0 +1,122 @@
|
||||
---
|
||||
/**
|
||||
* 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>
|
||||
Reference in New Issue
Block a user