Summer theme gets seamless transition between banner and page. Banners updated with support for this mode.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<style>
|
||||
#eyes {
|
||||
width: 100%;
|
||||
height: 240px;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <style>, so it applies atomically with the markup (this file would
|
||||
load a beat later and let the overflow flash through mid-transition). */
|
||||
|
||||
@@ -2,27 +2,47 @@
|
||||
<style>
|
||||
#stars {
|
||||
width: 100%;
|
||||
height: 240px;
|
||||
/* 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')
|
||||
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)
|
||||
|
||||
const stars = Array.from({ length: 110 }, () => ({
|
||||
x: Math.random(),
|
||||
y: Math.random(),
|
||||
@@ -33,6 +53,7 @@
|
||||
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)
|
||||
|
||||
@@ -62,10 +62,29 @@ body {
|
||||
color: #3d5223;
|
||||
}
|
||||
|
||||
/* The banner frame ties into the meadow beneath it. */
|
||||
/* No separation between banner and page: the frame loses its background,
|
||||
border and shadow, and the artwork overflows into the document below,
|
||||
masked by a transparency gradient so it cross-fades into the body's fixed
|
||||
meadow gradient. A plain color match is impossible — both sides are
|
||||
gradients, and the parallax drift keeps the banner side moving. */
|
||||
#banner {
|
||||
border-bottom-color: #4f913b30;
|
||||
box-shadow: 0 0.3rem 1rem #4f913b22;
|
||||
background: none;
|
||||
border-bottom: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#page-banner {
|
||||
inset: 0 0 -6rem;
|
||||
/* Paints over the page background in the overlap, but never swallows its
|
||||
clicks. */
|
||||
pointer-events: none;
|
||||
mask-image: linear-gradient(180deg, #000 calc(100% - 6rem), transparent);
|
||||
}
|
||||
|
||||
/* The mask above replaced the SVG's flat-color bottom fade (meadowFade) —
|
||||
fading to a single --bg would reintroduce a seam against the gradient. */
|
||||
#page-banner .summer-fade {
|
||||
stop-opacity: 0;
|
||||
}
|
||||
|
||||
/* Cheerful oversized tilted brand in a sky→grass→flower gradient. */
|
||||
@@ -115,14 +134,26 @@ body {
|
||||
}
|
||||
|
||||
/* Content sits in a soft wash of sunlit meadow rather than on a white
|
||||
card. */
|
||||
card. The wash lives on a pseudo-element so it can carry a vertical
|
||||
mask: it fades in from transparent over the strip where the banner
|
||||
artwork overflows into the page, so the two never fight — no matter
|
||||
which side paints on top. */
|
||||
main {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
main::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
background: linear-gradient(90deg,
|
||||
transparent,
|
||||
#f0f8dc80 15%,
|
||||
#f0f8dc8c 50%,
|
||||
#f0f8dc80 85%,
|
||||
transparent);
|
||||
mask-image: linear-gradient(180deg, transparent, #000 6rem);
|
||||
}
|
||||
|
||||
/* The sidebar is a piece of the same meadow: glassy green with a light
|
||||
@@ -132,6 +163,8 @@ main {
|
||||
border-right: 1px solid #ffffff80;
|
||||
border-bottom: 1px solid var(--line);
|
||||
box-shadow: 0 0.3rem 1rem #4f913b1f;
|
||||
border-radius: 1rem;
|
||||
margin-inline: 0.5rem;
|
||||
}
|
||||
|
||||
#sidebar a {
|
||||
|
||||
Reference in New Issue
Block a user