XY-mode Lissajous display renders stereo audio on a canvas inside a warm champagne bezel with recessed CRT bay, labeled control sections (Vertical/Horizontal/Trigger), rotary knobs, and power LED. Uses modern AnalyserNode + rAF pipeline instead of deprecated ScriptProcessor. Audio: "Spirals" by Jerobeam Fenderson (CC BY-NC-SA 4.0) Visual: Nick Watton, adapted from gist by rsp2k
157 lines
3.5 KiB
Plaintext
157 lines
3.5 KiB
Plaintext
---
|
|
/**
|
|
* Custom Hero override for Starlight.
|
|
* On splash pages: injects the oscilloscope display in the image area.
|
|
* On other pages: delegates to the default Starlight Hero.
|
|
*/
|
|
import { Image } from 'astro:assets';
|
|
import { LinkButton } from '@astrojs/starlight/components';
|
|
import OscilloscopeDisplay from './OscilloscopeDisplay.astro';
|
|
|
|
const PAGE_TITLE_ID = '_top';
|
|
|
|
const { data } = Astro.locals.starlightRoute.entry;
|
|
const { title = data.title, tagline, image, actions = [] } = data.hero || {};
|
|
const isSplash = data.template === 'splash';
|
|
|
|
const imageAttrs = {
|
|
loading: 'eager' as const,
|
|
decoding: 'async' as const,
|
|
width: 400,
|
|
height: 400,
|
|
alt: image?.alt || '',
|
|
};
|
|
|
|
let darkImage: ImageMetadata | undefined;
|
|
let lightImage: ImageMetadata | undefined;
|
|
let rawHtml: string | undefined;
|
|
|
|
if (image) {
|
|
if ('file' in image) {
|
|
darkImage = image.file;
|
|
} else if ('dark' in image) {
|
|
darkImage = image.dark;
|
|
lightImage = image.light;
|
|
} else {
|
|
rawHtml = image.html;
|
|
}
|
|
}
|
|
---
|
|
|
|
<div class="hero">
|
|
{isSplash ? (
|
|
<div class="hero-html sl-flex">
|
|
<OscilloscopeDisplay />
|
|
</div>
|
|
) : (
|
|
<>
|
|
{darkImage && (
|
|
<Image
|
|
src={darkImage}
|
|
{...imageAttrs}
|
|
class:list={{ 'light:sl-hidden': Boolean(lightImage) }}
|
|
/>
|
|
)}
|
|
{lightImage && <Image src={lightImage} {...imageAttrs} class="dark:sl-hidden" />}
|
|
{rawHtml && <div class="hero-html sl-flex" set:html={rawHtml} />}
|
|
</>
|
|
)}
|
|
<div class="sl-flex stack">
|
|
<div class="sl-flex copy">
|
|
<h1 id={PAGE_TITLE_ID} data-page-title set:html={title} />
|
|
{tagline && <div class="tagline" set:html={tagline} />}
|
|
</div>
|
|
{actions.length > 0 && (
|
|
<div class="sl-flex actions">
|
|
{actions.map(
|
|
({ attrs: { class: className, ...attrs } = {}, icon, link: href, text, variant }) => (
|
|
<LinkButton {href} {variant} icon={icon?.name} class:list={[className]} {...attrs}>
|
|
{text}
|
|
{icon?.html && <Fragment set:html={icon.html} />}
|
|
</LinkButton>
|
|
)
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
@layer starlight.core {
|
|
.hero {
|
|
display: grid;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
padding-bottom: 1rem;
|
|
}
|
|
|
|
.hero > img,
|
|
.hero > .hero-html {
|
|
object-fit: contain;
|
|
width: min(70%, 20rem);
|
|
height: auto;
|
|
margin-inline: auto;
|
|
}
|
|
|
|
.stack {
|
|
flex-direction: column;
|
|
gap: clamp(1.5rem, calc(1.5rem + 1vw), 2rem);
|
|
text-align: center;
|
|
}
|
|
|
|
.copy {
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.copy > * {
|
|
max-width: 50ch;
|
|
}
|
|
|
|
h1 {
|
|
font-size: clamp(var(--sl-text-3xl), calc(0.25rem + 5vw), var(--sl-text-6xl));
|
|
line-height: var(--sl-line-height-headings);
|
|
font-weight: 600;
|
|
color: var(--sl-color-white);
|
|
}
|
|
|
|
.tagline {
|
|
font-size: clamp(var(--sl-text-base), calc(0.0625rem + 2vw), var(--sl-text-xl));
|
|
color: var(--sl-color-gray-2);
|
|
}
|
|
|
|
.actions {
|
|
gap: 1rem 2rem;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
}
|
|
|
|
@media (min-width: 50rem) {
|
|
.hero {
|
|
grid-template-columns: 7fr 4fr;
|
|
gap: 3%;
|
|
padding-block: clamp(2.5rem, calc(1rem + 10vmin), 10rem);
|
|
}
|
|
|
|
.hero > img,
|
|
.hero > .hero-html {
|
|
order: 2;
|
|
width: min(100%, 25rem);
|
|
}
|
|
|
|
.stack {
|
|
text-align: start;
|
|
}
|
|
|
|
.copy {
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.actions {
|
|
justify-content: flex-start;
|
|
}
|
|
}
|
|
}
|
|
</style>
|