feat(home): swap in the higher-fidelity hero artwork #98
No files matched your search
@@ -193,13 +193,17 @@
|
|||||||
drawn SVG, so its colours are in the file, not here. What is left is the
|
drawn SVG, so its colours are in the file, not here. What is left is the
|
||||||
ground the artwork sits on and fades into.
|
ground the artwork sits on and fades into.
|
||||||
|
|
||||||
--hero-ground is sampled from the artwork's own copy area (#FDF9F3),
|
--hero-ground is sampled from the artwork's own copy area, not from
|
||||||
not from Sand. The artwork reserves an empty cream region on the left
|
Sand — a scrim in Sand (#F5EFE6) is far enough off to leave a visible
|
||||||
for the headline, and the scrim that guarantees text contrast fades from
|
seam straight down the hero.
|
||||||
this colour into it — a scrim in Sand (#F5EFE6, eight to thirteen points
|
|
||||||
darker per channel) leaves a visible seam straight down the hero. */
|
The artwork's copy area is not one flat colour: it runs from a peach
|
||||||
--hero-ground: #FDF9F3;
|
#FEE8D2 at the top to a cream #FDF3E7 around 43% height, and below ~48%
|
||||||
--hero-ground-rgb: 253, 249, 243;
|
the left edge is foliage rather than cream. This value is sampled from
|
||||||
|
the middle of that pale run, where the headline and search actually sit;
|
||||||
|
the scrim is what covers the foliage further down. */
|
||||||
|
--hero-ground: #FEF2E1;
|
||||||
|
--hero-ground-rgb: 254, 242, 225;
|
||||||
|
|
||||||
/* ── Geometry & motion ──────────────────────────────────────────────
|
/* ── Geometry & motion ──────────────────────────────────────────────
|
||||||
"Soft shapes, rounded corners" — the guideline's geometry is markedly
|
"Soft shapes, rounded corners" — the guideline's geometry is markedly
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 3.0 MiB |
@@ -323,15 +323,21 @@
|
|||||||
padding: 1.75rem 1.5rem 1.75rem;
|
padding: 1.75rem 1.5rem 1.75rem;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
/* 13rem, not 11: at 860px an 11rem band is 4.5:1, and squeezing the 3.2:1
|
||||||
|
crop into that throws away enough height to cut the flag off the roof and
|
||||||
|
the base off the building. 13rem holds it at about 3.8:1, where the
|
||||||
|
building stays whole. */
|
||||||
.heroArt {
|
.heroArt {
|
||||||
position: static;
|
position: static;
|
||||||
order: -1;
|
order: -1;
|
||||||
height: 11rem;
|
height: 13rem;
|
||||||
}
|
}
|
||||||
/* The band crop centres the schoolhouse at about 65% across, so squeezing
|
/* The band crop puts the schoolhouse at 73% across — reported by
|
||||||
toward 4.9:1 crops the empty sides rather than the subject. */
|
scripts/build-hero-images.js, which derives it from the crop box rather
|
||||||
|
than leaving the two to drift. Squeezing narrower then crops the emptier
|
||||||
|
left side rather than the subject. */
|
||||||
.heroArt img {
|
.heroArt img {
|
||||||
object-position: 65% center;
|
object-position: 73% center;
|
||||||
}
|
}
|
||||||
/* The scrim exists to protect text sitting on the artwork. Nothing sits on
|
/* The scrim exists to protect text sitting on the artwork. Nothing sits on
|
||||||
it here, and leaving it would wash the band out. */
|
it here, and leaving it would wash the band out. */
|
||||||
@@ -372,8 +378,12 @@
|
|||||||
font-size: 1.75rem;
|
font-size: 1.75rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
/* 10rem, not 8.5: the band is widest-per-height right at this breakpoint —
|
||||||
|
at 640px an 8.5rem band is 4.2:1, worse than anything above it, because
|
||||||
|
the height steps down here while the width does not. 10rem keeps it near
|
||||||
|
3.6:1, in line with the rest of the range. */
|
||||||
.heroArt {
|
.heroArt {
|
||||||
height: 8.5rem;
|
height: 10rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,8 +40,8 @@
|
|||||||
const SIZES = '(min-width: 1400px) 1352px, (min-width: 769px) calc(100vw - 3rem), calc(100vw - 2rem)';
|
const SIZES = '(min-width: 1400px) 1352px, (min-width: 769px) calc(100vw - 3rem), calc(100vw - 2rem)';
|
||||||
|
|
||||||
/** Intrinsic size of the wide crop — the aspect hint that prevents reflow. */
|
/** Intrinsic size of the wide crop — the aspect hint that prevents reflow. */
|
||||||
const WIDE_W = 1672;
|
const WIDE_W = 2000;
|
||||||
const WIDE_H = 941;
|
const WIDE_H = 1125;
|
||||||
|
|
||||||
export function HeroIllustration() {
|
export function HeroIllustration() {
|
||||||
return (
|
return (
|
||||||
@@ -51,13 +51,13 @@ export function HeroIllustration() {
|
|||||||
<source
|
<source
|
||||||
media="(max-width: 860px)"
|
media="(max-width: 860px)"
|
||||||
type="image/avif"
|
type="image/avif"
|
||||||
srcSet="/brand/hero-band-500.avif 500w, /brand/hero-band-700.avif 700w, /brand/hero-band-972.avif 972w"
|
srcSet="/brand/hero-band-600.avif 600w, /brand/hero-band-900.avif 900w, /brand/hero-band-1344.avif 1344w"
|
||||||
sizes={SIZES}
|
sizes={SIZES}
|
||||||
/>
|
/>
|
||||||
<source
|
<source
|
||||||
media="(max-width: 860px)"
|
media="(max-width: 860px)"
|
||||||
type="image/webp"
|
type="image/webp"
|
||||||
srcSet="/brand/hero-band-500.webp 500w, /brand/hero-band-700.webp 700w, /brand/hero-band-972.webp 972w"
|
srcSet="/brand/hero-band-600.webp 600w, /brand/hero-band-900.webp 900w, /brand/hero-band-1344.webp 1344w"
|
||||||
sizes={SIZES}
|
sizes={SIZES}
|
||||||
/>
|
/>
|
||||||
{/*
|
{/*
|
||||||
@@ -71,15 +71,15 @@ export function HeroIllustration() {
|
|||||||
media query does, and it sits after the modern formats so they still
|
media query does, and it sits after the modern formats so they still
|
||||||
win where supported.
|
win where supported.
|
||||||
*/}
|
*/}
|
||||||
<source media="(max-width: 860px)" srcSet="/brand/hero-band-700.jpg" />
|
<source media="(max-width: 860px)" srcSet="/brand/hero-band-900.jpg" />
|
||||||
<source
|
<source
|
||||||
type="image/avif"
|
type="image/avif"
|
||||||
srcSet="/brand/hero-wide-900.avif 900w, /brand/hero-wide-1200.avif 1200w, /brand/hero-wide-1672.avif 1672w"
|
srcSet="/brand/hero-wide-1000.avif 1000w, /brand/hero-wide-1400.avif 1400w, /brand/hero-wide-2000.avif 2000w"
|
||||||
sizes={SIZES}
|
sizes={SIZES}
|
||||||
/>
|
/>
|
||||||
<source
|
<source
|
||||||
type="image/webp"
|
type="image/webp"
|
||||||
srcSet="/brand/hero-wide-900.webp 900w, /brand/hero-wide-1200.webp 1200w, /brand/hero-wide-1672.webp 1672w"
|
srcSet="/brand/hero-wide-1000.webp 1000w, /brand/hero-wide-1400.webp 1400w, /brand/hero-wide-2000.webp 2000w"
|
||||||
sizes={SIZES}
|
sizes={SIZES}
|
||||||
/>
|
/>
|
||||||
{/*
|
{/*
|
||||||
@@ -90,7 +90,7 @@ export function HeroIllustration() {
|
|||||||
*/}
|
*/}
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<img
|
<img
|
||||||
src="/brand/hero-wide-1200.jpg"
|
src="/brand/hero-wide-1400.jpg"
|
||||||
alt=""
|
alt=""
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
width={WIDE_W}
|
width={WIDE_W}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ The landing hero illustration: a path climbing through hills to a schoolhouse,
|
|||||||
with an empty cream area on the left reserved for the headline.
|
with an empty cream area on the left reserved for the headline.
|
||||||
|
|
||||||
- **Source:** supplied by the project owner
|
- **Source:** supplied by the project owner
|
||||||
- **Master:** `nextjs-app/assets/hero-source.png` (1672×941 PNG), committed so
|
- **Master:** `nextjs-app/assets/hero-source.png` (2000×1125 PNG), committed so
|
||||||
the derivatives can be regenerated
|
the derivatives can be regenerated
|
||||||
- **Derivatives:** `node scripts/build-hero-images.js` — never hand-edit these
|
- **Derivatives:** `node scripts/build-hero-images.js` — never hand-edit these
|
||||||
- **Licence:** **to be confirmed by the project owner**
|
- **Licence:** **to be confirmed by the project owner**
|
||||||
|
|||||||
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 20 KiB |
@@ -45,14 +45,24 @@ const SRC = path.join(__dirname, '..', 'assets', 'hero-source.png');
|
|||||||
const OUT = path.join(__dirname, '..', 'public', 'brand');
|
const OUT = path.join(__dirname, '..', 'public', 'brand');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Band crop, in source pixels. The schoolhouse occupies roughly x 1180–1540,
|
* Band crop, in source pixels, measured against the artwork rather than
|
||||||
* y 300–520, so this keeps it whole with headroom above the flag and enough
|
* guessed: the schoolhouse spans roughly x 1420–1860, y 420–660, with its flag
|
||||||
* hillside below to read as landscape rather than as a cutout.
|
* up at y 330.
|
||||||
|
*
|
||||||
|
* The crop is flush to the right edge because it has to be. The school sits at
|
||||||
|
* about 82% across the frame, so there is only ~140px of artwork to its right
|
||||||
|
* — not enough to centre it. Anchoring right and taking 1050px back puts the
|
||||||
|
* school at ~66% across the crop, which is what BAND_FOCUS_X below reports to
|
||||||
|
* the CSS so the narrowest band crops the empty left side rather than the
|
||||||
|
* subject.
|
||||||
*/
|
*/
|
||||||
const BAND = { left: 700, top: 260, width: 972, height: 380 };
|
const BAND = { left: 656, top: 300, width: 1344, height: 420 };
|
||||||
|
|
||||||
const WIDE_WIDTHS = [1672, 1200, 900];
|
/** Where the schoolhouse lands in the band crop, for CSS object-position. */
|
||||||
const BAND_WIDTHS = [972, 700, 500];
|
const BAND_FOCUS_X = Math.round(((1420 + 1860) / 2 - BAND.left) / BAND.width * 100);
|
||||||
|
|
||||||
|
const WIDE_WIDTHS = [2000, 1400, 1000];
|
||||||
|
const BAND_WIDTHS = [1344, 900, 600];
|
||||||
|
|
||||||
async function emit(pipeline, name, width) {
|
async function emit(pipeline, name, width) {
|
||||||
const base = pipeline.clone().resize({ width, withoutEnlargement: true });
|
const base = pipeline.clone().resize({ width, withoutEnlargement: true });
|
||||||
@@ -67,25 +77,26 @@ async function main() {
|
|||||||
|
|
||||||
const meta = await sharp(SRC).metadata();
|
const meta = await sharp(SRC).metadata();
|
||||||
console.log(`source ${meta.width}×${meta.height}`);
|
console.log(`source ${meta.width}×${meta.height}`);
|
||||||
|
console.log(`band focus x: ${BAND_FOCUS_X}% (keep .heroArt object-position in step)`);
|
||||||
|
|
||||||
const wide = sharp(SRC);
|
const wide = sharp(SRC);
|
||||||
for (const w of WIDE_WIDTHS) await emit(wide, 'hero-wide', w);
|
for (const w of WIDE_WIDTHS) await emit(wide, 'hero-wide', w);
|
||||||
// Fallback for the <img> itself. Flattened onto the artwork's own cream so a
|
// Fallback for the <img> itself. Flattened onto the artwork's own cream so a
|
||||||
// JPEG (no alpha) can never composite against black.
|
// JPEG (no alpha) can never composite against black.
|
||||||
await sharp(SRC)
|
await sharp(SRC)
|
||||||
.resize({ width: 1200 })
|
.resize({ width: 1400 })
|
||||||
.flatten({ background: '#FDF9F3' })
|
.flatten({ background: '#FEF2E1' })
|
||||||
.jpeg({ quality: 82, mozjpeg: true })
|
.jpeg({ quality: 82, mozjpeg: true })
|
||||||
.toFile(path.join(OUT, 'hero-wide-1200.jpg'));
|
.toFile(path.join(OUT, 'hero-wide-1400.jpg'));
|
||||||
|
|
||||||
const band = sharp(SRC).extract(BAND);
|
const band = sharp(SRC).extract(BAND);
|
||||||
for (const w of BAND_WIDTHS) await emit(band, 'hero-band', w);
|
for (const w of BAND_WIDTHS) await emit(band, 'hero-band', w);
|
||||||
await sharp(SRC)
|
await sharp(SRC)
|
||||||
.extract(BAND)
|
.extract(BAND)
|
||||||
.resize({ width: 700 })
|
.resize({ width: 900 })
|
||||||
.flatten({ background: '#FDF9F3' })
|
.flatten({ background: '#FEF2E1' })
|
||||||
.jpeg({ quality: 82, mozjpeg: true })
|
.jpeg({ quality: 82, mozjpeg: true })
|
||||||
.toFile(path.join(OUT, 'hero-band-700.jpg'));
|
.toFile(path.join(OUT, 'hero-band-900.jpg'));
|
||||||
|
|
||||||
const files = fs.readdirSync(OUT).filter(f => f.startsWith('hero-')).sort();
|
const files = fs.readdirSync(OUT).filter(f => f.startsWith('hero-')).sort();
|
||||||
let total = 0;
|
let total = 0;
|
||||||
|
|||||||