74 lines
2.4 KiB
HTML
74 lines
2.4 KiB
HTML
<canvas id="stars"></canvas>
|
|
<style>
|
|
#stars {
|
|
width: 100%;
|
|
/* 13rem — the banner's layout height, not 100%: a percentage only
|
|
resolves after the main stylesheet sizes #page-banner, and until
|
|
then the canvas would render at its intrinsic height, unclipped,
|
|
over the page. (This design always opts out of theme overflows —
|
|
below — so the layout height is always the right one.) */
|
|
height: 13rem;
|
|
display: block;
|
|
}
|
|
|
|
/* The night sky stays a windowed stage: undo the summer theme's banner
|
|
overflow/cross-fade — a starfield must not bleed into a daylit page.
|
|
Kept in this inlined <style> (not banner.css) so the opt-out applies
|
|
atomically with the markup; a separate stylesheet can arrive a beat
|
|
later and let the overflow flash through mid-transition. Later in
|
|
document order than theme.css, so same-specificity rules win. */
|
|
#page-banner {
|
|
inset: 0;
|
|
mask-image: none;
|
|
}
|
|
</style>
|
|
<script><!--
|
|
(() => {
|
|
const c = document.getElementById('stars')
|
|
const ctx = c.getContext('2d')
|
|
|
|
// Sync the backing store to the canvas' laid-out size. Checked every
|
|
// frame: this inline script runs before the stylesheets that size
|
|
// #page-banner, so observers/load events can still miss the transition.
|
|
// Assigning width/height also clears the canvas. DPR is read here, not
|
|
// captured: it changes with browser zoom.
|
|
const syncSize = () => {
|
|
const DPR = devicePixelRatio || 1
|
|
const w = Math.round(Math.max(1, c.clientWidth) * DPR)
|
|
const h = Math.round(Math.max(1, c.clientHeight) * DPR)
|
|
if (c.width !== w || c.height !== h) {
|
|
c.width = w
|
|
c.height = h
|
|
}
|
|
ctx.setTransform(DPR, 0, 0, DPR, 0, 0)
|
|
}
|
|
|
|
const stars = Array.from({ length: 110 }, () => ({
|
|
x: Math.random(),
|
|
y: Math.random(),
|
|
r: Math.random() * 1.4 + 0.3,
|
|
v: Math.random() * 0.05 + 0.01
|
|
}))
|
|
|
|
let prev = performance.now()
|
|
;(function frame(now) {
|
|
if (!c.isConnected) return
|
|
syncSize()
|
|
const w = c.clientWidth
|
|
const h = c.clientHeight
|
|
const dt = Math.min(now - prev, 100)
|
|
prev = now
|
|
ctx.fillStyle = '#0b0e1d'
|
|
ctx.fillRect(0, 0, w, h)
|
|
ctx.fillStyle = '#cdd6ff'
|
|
for (const s of stars) {
|
|
s.x = (s.x + (s.v * dt) / 1000) % 1
|
|
ctx.beginPath()
|
|
ctx.arc(s.x * w, s.y * h, s.r, 0, 7)
|
|
ctx.fill()
|
|
}
|
|
requestAnimationFrame(frame)
|
|
})(prev)
|
|
})()
|
|
</script>
|