// Crop the configurator shell renders down to the product band for the model // chooser (src/site/partials/model-chooser.html). The source renders are // 2160-square with large transparent margins; cropping once at build-asset // time lets the chooser scale them with object-fit: contain (no CSS cropping, // nothing cut off at any card size). // // One shared crop box (union of the four shells' alpha bounds + margin) keeps // the four renders pixel-aligned so the color crossfade doesn't jump. // // node scripts/crop-chooser-renders.mjs // // Input: public/images/home/Dynamic Configurator/DYN_bs2---softstrap-customcushion.webp // Output: public/images/home/model-chooser/chooser-shell-.webp import fs from "node:fs"; import sharp from "sharp"; const SRC = (c) => `public/images/home/Dynamic Configurator/DYN_bs2--${c}-softstrap-customcushion.webp`; const OUT_DIR = "public/images/home/model-chooser"; const OUT = (c) => `${OUT_DIR}/chooser-shell-${c}.webp`; const SHELLS = ["clear", "orange", "black", "purple"]; const MARGIN = 24; // px of breathing room around the union bounds const ALPHA_FLOOR = 8; // ignore near-invisible antialiasing haze async function bounds(file) { const { data, info } = await sharp(file).ensureAlpha().raw().toBuffer({ resolveWithObject: true }); let minX = info.width, minY = info.height, maxX = 0, maxY = 0; for (let y = 0; y < info.height; y++) { for (let x = 0; x < info.width; x++) { if (data[(y * info.width + x) * 4 + 3] > ALPHA_FLOOR) { if (x < minX) minX = x; if (x > maxX) maxX = x; if (y < minY) minY = y; if (y > maxY) maxY = y; } } } return { minX, minY, maxX, maxY, width: info.width, height: info.height }; } const all = await Promise.all(SHELLS.map((c) => bounds(SRC(c)))); const union = all.reduce((u, b) => ({ minX: Math.min(u.minX, b.minX), minY: Math.min(u.minY, b.minY), maxX: Math.max(u.maxX, b.maxX), maxY: Math.max(u.maxY, b.maxY), width: b.width, height: b.height, })); const left = Math.max(0, union.minX - MARGIN); const top = Math.max(0, union.minY - MARGIN); const width = Math.min(union.width, union.maxX + MARGIN + 1) - left; const height = Math.min(union.height, union.maxY + MARGIN + 1) - top; console.log(`union crop: ${width}x${height} at ${left},${top} (ratio ${(width / height).toFixed(2)})`); fs.mkdirSync(OUT_DIR, { recursive: true }); const OUT_WIDTH = 1600; // cards render ~900px max; 1600 covers retina comfortably for (const c of SHELLS) { await sharp(SRC(c)).extract({ left, top, width, height }) .resize({ width: OUT_WIDTH }).webp({ quality: 82 }).toFile(OUT(c)); const kb = (fs.statSync(OUT(c)).size / 1024).toFixed(0); console.log(`wrote ${OUT(c)} (${kb} KB)`); }