You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

56 lines
3.2 KiB
JavaScript

// Non-project imagery: team portraits, team group, principal, OG image.
// Usage: node tools/images-extra.js (after tools/images.js; before build)
const fs = require('fs');
const path = require('path');
const sharp = require('sharp');
const ROOT = path.join(__dirname, '..');
const OUT = path.join(ROOT, 'dist', 'assets', 'img');
const site = JSON.parse(fs.readFileSync(path.join(ROOT, 'src', 'data', 'site.json'), 'utf8'));
const home = JSON.parse(fs.readFileSync(path.join(ROOT, 'src', 'data', 'home.json'), 'utf8'));
const projects = JSON.parse(fs.readFileSync(path.join(ROOT, 'src', 'data', 'projects.json'), 'utf8'));
const slugify = s => s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
async function variants(srcAbs, destDir, baseName, widths, jpegWidth) {
fs.mkdirSync(destDir, { recursive: true });
const base = sharp(srcAbs).rotate();
const meta = await base.metadata();
const srcW = meta.autoOrient ? meta.autoOrient.width : meta.width;
const srcH = meta.autoOrient ? meta.autoOrient.height : meta.height;
const entry = { variants: [] };
for (const w of widths) {
const eff = Math.min(w, srcW);
const file = `${baseName}-${w}.webp`;
await base.clone().resize({ width: eff, withoutEnlargement: true }).webp({ quality: 78 }).toFile(path.join(destDir, file));
entry.variants.push({ file: path.relative(path.join(ROOT, 'dist'), path.join(destDir, file)).replace(/\\/g, '/'), width: eff, height: Math.round(srcH * eff / srcW), format: 'webp' });
}
const jw = Math.min(jpegWidth, srcW);
const jfile = `${baseName}-${jpegWidth}.jpg`;
await base.clone().resize({ width: jw, withoutEnlargement: true }).jpeg({ quality: 74, mozjpeg: true }).toFile(path.join(destDir, jfile));
entry.variants.push({ file: path.relative(path.join(ROOT, 'dist'), path.join(destDir, jfile)).replace(/\\/g, '/'), width: jw, height: Math.round(srcH * jw / srcW), format: 'jpeg' });
return entry;
}
(async () => {
const out = {};
for (const t of site.team) {
const src = path.join(ROOT, t.photo);
if (!fs.existsSync(src)) { console.error('MISSING team photo:', t.photo); continue; }
out['team:' + t.name] = await variants(src, path.join(OUT, 'team'), slugify(t.name), [480], 480);
}
out['principal'] = await variants(path.join(ROOT, site.principal.photo), path.join(OUT, 'team'), 'principal', [480, 800], 800);
const group = path.join(ROOT, 'img', 'team', 'team group.jpg');
if (fs.existsSync(group)) out['team-group'] = await variants(group, path.join(OUT, 'team'), 'team-group', [800, 1600], 1200);
// OG image 1200x630 from the homepage hero pick
const heroProj = projects.find(p => p.slug === home.hero.slug);
const heroSrc = path.join(ROOT, heroProj.images[home.hero.index]);
fs.mkdirSync(OUT, { recursive: true });
await sharp(heroSrc).rotate().resize(1200, 630, { fit: 'cover', position: 'attention' }).jpeg({ quality: 78, mozjpeg: true }).toFile(path.join(OUT, 'og.jpg'));
out['og'] = { variants: [{ file: 'assets/img/og.jpg', width: 1200, height: 630, format: 'jpeg' }] };
fs.writeFileSync(path.join(ROOT, 'src', 'data', 'images-extra.json'), JSON.stringify(out, null, 1));
console.log('DONE — extra images:', Object.keys(out).length);
})();