125 lines
3.5 KiB
Plaintext
125 lines
3.5 KiB
Plaintext
---
|
|
import { Picture } from 'astro:assets';
|
|
import poster from '../../assets/images/hero-field.png';
|
|
import { t, type Lang } from '../../i18n/ui';
|
|
|
|
interface Props { lang: Lang }
|
|
const { lang } = Astro.props;
|
|
const ui = t(lang);
|
|
---
|
|
<section class="hero night" aria-labelledby="hero-title">
|
|
<div class="hero__media">
|
|
<!-- Abstract and decorative, so neither layer carries alt text. The poster is a frame of
|
|
the noise field, so it carries the hero until the canvas is drawing, and stays for
|
|
anyone who never gets one: reduced motion, no WebGL, no script. -->
|
|
<div class="hero__layers">
|
|
<Picture
|
|
src={poster}
|
|
alt=""
|
|
quality={70}
|
|
formats={['avif']}
|
|
fallbackFormat="webp"
|
|
widths={[640, 960, 1440, 1920, 2400]}
|
|
sizes="100vw"
|
|
loading="eager"
|
|
fetchpriority="high"
|
|
decoding="async"
|
|
class="hero__img img-cover"
|
|
pictureAttributes={{ class: 'hero__picture' }}
|
|
/>
|
|
<canvas class="hero__field" aria-hidden="true" data-hero-field></canvas>
|
|
</div>
|
|
<div class="hero__overlay"></div>
|
|
</div>
|
|
|
|
<div class="container hero__content">
|
|
<h1 id="hero-title" class="display hero__title">
|
|
{ui.hero.lines.map((line, i) => (
|
|
<span class="hero__line" style={`--i: ${i}`}>{line}</span>
|
|
))}
|
|
</h1>
|
|
</div>
|
|
</section>
|
|
|
|
<style>
|
|
.hero {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: flex-end;
|
|
/* Runs up under the floating header so the glass bar sits on the image, fills the screen,
|
|
and is never less than the headline needs. */
|
|
margin-top: calc(-1 * var(--header-offset));
|
|
min-height: max(28rem, 100svh);
|
|
overflow: hidden;
|
|
}
|
|
.hero__media { position: absolute; inset: 0; }
|
|
:global(.hero__picture) { display: block; width: 100%; height: 100%; }
|
|
.hero__img,
|
|
:global(.hero__img) {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
object-position: 50% 50%;
|
|
}
|
|
.hero__layers {
|
|
position: absolute;
|
|
inset: 0;
|
|
transform-origin: 50% 60%;
|
|
animation: hero-settle var(--dur-hero) var(--ease) both;
|
|
}
|
|
.hero__field {
|
|
position: absolute;
|
|
inset: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
opacity: 0;
|
|
transition: opacity var(--dur-slow) var(--ease);
|
|
}
|
|
/* Set by the first drawn frame, so the poster never cuts to black. */
|
|
.hero__field[data-ready] { opacity: 1; }
|
|
.hero__overlay {
|
|
position: absolute;
|
|
inset: 0;
|
|
background: var(--overlay);
|
|
}
|
|
.hero__content {
|
|
position: relative;
|
|
width: 100%;
|
|
padding-block: var(--space-9);
|
|
}
|
|
.hero__title {
|
|
max-width: 12em;
|
|
font-size: var(--step-hero);
|
|
}
|
|
.hero__line {
|
|
display: block;
|
|
opacity: 0;
|
|
animation: hero-rise 700ms var(--ease) forwards;
|
|
animation-delay: calc(240ms + var(--i) * 160ms);
|
|
}
|
|
|
|
@keyframes hero-settle {
|
|
from { transform: scale(1.045); }
|
|
to { transform: none; }
|
|
}
|
|
@keyframes hero-rise {
|
|
from { opacity: 0; transform: translateY(0.18em); }
|
|
to { opacity: 1; transform: none; }
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.hero__layers, .hero__line { animation: none; opacity: 1; transform: none; }
|
|
.hero__field { display: none; }
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
import { startNoiseField } from '../../scripts/noise-field';
|
|
|
|
const canvas = document.querySelector<HTMLCanvasElement>('[data-hero-field]');
|
|
const hero = canvas?.closest<HTMLElement>('.hero');
|
|
if (canvas && hero && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
startNoiseField(canvas, hero);
|
|
}
|
|
</script>
|