feat: tighten the wordmark's letter-spacing for the panel
The logotype is drawn with wide spacing — handsome on a website, and a third of the panel's header width spent on nothing. The spacing is artwork, not typesetting, so no property adjusts it; but every pixel column between two letters is blank, and a blank column carries no shape. Dropping most of them moves the letters together without altering a single letterform. At the same 260px the capitals are now a third taller, which the hairlines needed more than the spacing did: the A of ITA reads at a glance instead of being rescued by a threshold. Done at full resolution, before the downscale. Compressing columns that are already an average of ink and paper would smear exactly the strokes this logo can least afford to lose. The rendered size is now read from the PNG's own header rather than kept as a constant beside it. The spacing is adjustable and every value changes the height; a number copied into the renderer would have drifted and squashed the logo with nothing failing to say so. `npm run brand:variants` renders the spacings side by side, magnified, since this is a decision to be looked at rather than reasoned about. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
This commit is contained in:
+36
-20
@@ -15,10 +15,11 @@ import path from 'node:path';
|
||||
|
||||
import sharp from 'sharp';
|
||||
|
||||
import { tighten } from '../lib/brand/tighten';
|
||||
import { encodePng, toBitmap } from '../lib/screen/encode';
|
||||
|
||||
const WIDTHS = [260, 320, 400];
|
||||
const THRESHOLDS = [127, 165, 195];
|
||||
/** How many blank pixel columns may remain between two letters. */
|
||||
const GAPS = [40, 14, 8, 4];
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const out = path.join(process.cwd(), '.preview', 'logo');
|
||||
@@ -27,29 +28,44 @@ async function main(): Promise<void> {
|
||||
const source = await readFile(path.join(process.cwd(), 'public', 'brand', 'logo.png'));
|
||||
const band = await findWordmarkBand(source);
|
||||
|
||||
// Full resolution, before any downscale: compressing averaged columns would
|
||||
// smear the hairlines this logo can least afford to lose.
|
||||
const full = await sharp(source)
|
||||
.extract(band)
|
||||
.flatten({ background: '#ffffff' })
|
||||
.greyscale()
|
||||
.raw()
|
||||
.toBuffer({ resolveWithObject: true });
|
||||
|
||||
const rows: string[] = [];
|
||||
|
||||
for (const width of WIDTHS) {
|
||||
for (const threshold of THRESHOLDS) {
|
||||
const raw = await sharp(source)
|
||||
.extract(band)
|
||||
.resize({ width })
|
||||
.flatten({ background: '#ffffff' })
|
||||
.ensureAlpha()
|
||||
.raw()
|
||||
.toBuffer({ resolveWithObject: true });
|
||||
for (const gap of GAPS) {
|
||||
const tightened = tighten(
|
||||
{ data: full.data, width: full.info.width, height: full.info.height },
|
||||
gap,
|
||||
);
|
||||
|
||||
const bitmap = toBitmap(raw.data, raw.info.width, raw.info.height, { threshold });
|
||||
const name = `w${width}-s${threshold}`;
|
||||
await writeFile(path.join(out, `${name}.png`), encodePng(bitmap));
|
||||
const raw = await sharp(tightened.data, {
|
||||
raw: { width: tightened.width, height: tightened.height, channels: 1 },
|
||||
})
|
||||
.resize({ width: 260 })
|
||||
.ensureAlpha()
|
||||
.raw()
|
||||
.toBuffer({ resolveWithObject: true });
|
||||
|
||||
// Magnified to the same apparent size, so the variants compare fairly.
|
||||
await sharp(path.join(out, `${name}.png`))
|
||||
.resize({ width: 1040, kernel: 'nearest' })
|
||||
.toFile(path.join(out, `${name}-zoom.png`));
|
||||
const bitmap = toBitmap(raw.data, raw.info.width, raw.info.height, { threshold: 195 });
|
||||
const name = `gap${String(gap).padStart(2, '0')}`;
|
||||
await writeFile(path.join(out, `${name}.png`), encodePng(bitmap));
|
||||
|
||||
rows.push(`${name} ${raw.info.width}x${raw.info.height}`);
|
||||
}
|
||||
// Magnified to the same apparent size, so the variants compare fairly.
|
||||
await sharp(path.join(out, `${name}.png`))
|
||||
.resize({ width: 1040, kernel: 'nearest' })
|
||||
.toFile(path.join(out, `${name}-zoom.png`));
|
||||
|
||||
// Narrower source at the same final width means larger letters.
|
||||
rows.push(
|
||||
`${name} source ${tightened.width}px (-${full.info.width - tightened.width}) -> 260x${raw.info.height}`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(rows.join('\n'));
|
||||
|
||||
Reference in New Issue
Block a user