diff --git a/pagerite/themes/eyes/banner.css b/pagerite/themes/eyes/banner.css index 6f89d72..6b35d08 100644 --- a/pagerite/themes/eyes/banner.css +++ b/pagerite/themes/eyes/banner.css @@ -1,3 +1,12 @@ /* Eyes banner design: a canvas critter watching the cursor from the grass (banner.html — markup + styles + script inlined by the backend - into #page-banner). Fixed-height stage matching the canvas. */ + into #page-banner). The canvas fills the banner; the scene composes + against the 13rem layout box and extends flat grass into any overflow + a theme adds below (summer's cross-fade strip). */ + +/* Opt out of the base parallax (scale overscan + --pry drift): scaling + from the bottom edge would crop the top of the composed scene — pushing + the critter out of view — and this canvas animates on its own. */ +#page-banner>#eyes { + transform: none; +} diff --git a/pagerite/themes/eyes/banner.html b/pagerite/themes/eyes/banner.html index f5dc0c2..cca15a4 100644 --- a/pagerite/themes/eyes/banner.html +++ b/pagerite/themes/eyes/banner.html @@ -2,7 +2,7 @@ @@ -10,19 +10,23 @@ (() => { const c = document.getElementById('eyes') const ctx = c.getContext('2d') - const DPR = devicePixelRatio || 1 - const fit = () => { - const w = Math.max(1, c.clientWidth) - const h = Math.max(1, c.clientHeight) - c.width = Math.round(w * DPR) - c.height = Math.round(h * DPR) + // 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) } - fit() - addEventListener('resize', fit) - let mx = 0 let my = 0 let lastMove = 0 @@ -69,7 +73,7 @@ ] const ridgeY = (x, w, h) => - h * 0.72 + + h * 0.83 + Math.sin(x * 0.012) * 10 + Math.sin(x * 0.003 + 1.4) * 16 + Math.sin(x * 0.02 + 0.7) * 3 @@ -129,7 +133,7 @@ for (let i = 0; i < 5; i++) { const x = (i + 0.5) * w / 5 - const y = h * 0.58 + Math.sin(i * 1.7) * 8 + const y = h * 0.69 + Math.sin(i * 1.7) * 8 ctx.fillStyle = '#5f8d4e' ctx.beginPath() @@ -377,8 +381,17 @@ const dt = Math.min(now - prev, 100) / 16.7 prev = now + syncSize() + const w = c.clientWidth - const h = c.clientHeight + // Compose the scene against the banner's layout box, not the canvas: + // a theme may extend #page-banner past #banner (e.g. summer overflows + // the artwork into the page for a masked cross-fade), and the critter + // must stay in the visible part. The overflow strip is filled with the + // flat meadow color below — a hard canvas edge would show through the + // fade, a grass extension just blends. + const h = Math.min(c.clientHeight, + c.closest('#banner')?.clientHeight || c.clientHeight) const R = Math.min(h * 0.11, 38) if (now > nextMove && !hidePhase) { @@ -414,6 +427,16 @@ vy *= Math.pow(0.85, dt) yoff += vy * dt + // Clip the scene to the composed area: the ducking critter travels + // below it, and the overflow strip is only flat meadow painted after — + // without the clip the critter would leave trails there as it sinks. + // Clipping against grass-on-grass is invisible, so the duck still + // reads as sinking into the meadow. + ctx.save() + ctx.beginPath() + ctx.rect(0, 0, w, h) + ctx.clip() + drawBackground(w, h) const cx0 = gx * w @@ -430,6 +453,18 @@ drawCritter(cx0, eyeY, R, now, dt) drawForeground(w, h) + ctx.restore() + + // Extend the meadow into any overflow below the composed scene, with + // the same two layers drawForeground leaves at the bottom (base grass + // plus the dark under-band) so the joint is invisible. + if (c.clientHeight > h) { + ctx.fillStyle = '#69ae4b' + ctx.fillRect(0, h, w, c.clientHeight - h) + ctx.fillStyle = 'rgba(48,102,34,0.18)' + ctx.fillRect(0, h, w, c.clientHeight - h) + } + requestAnimationFrame(frame) } diff --git a/pagerite/themes/stars/banner.css b/pagerite/themes/stars/banner.css index ad44a78..9f588e3 100644 --- a/pagerite/themes/stars/banner.css +++ b/pagerite/themes/stars/banner.css @@ -1,3 +1,7 @@ /* Stars banner design: a drifting starfield (banner.html — canvas + script - inlined by the backend into #page-banner). Fixed-height stage matching - the canvas. */ + inlined by the backend into #page-banner). The canvas takes the banner's + 13rem layout height directly so it never renders unclipped before the + main stylesheet loads; the starfield scales to any height. The design's + opt-out of theme banner overflow/fade effects also lives in banner.html's + inline