107 lines
6.6 KiB
JavaScript
107 lines
6.6 KiB
JavaScript
// Run with Electron, like export.cjs. Canvas lays out the supplied raster marks and real font.
|
|
const { app, BrowserWindow } = require('electron');
|
|
const fs = require('node:fs/promises');
|
|
const path = require('node:path');
|
|
const assert = require('node:assert/strict');
|
|
app.disableHardwareAcceleration();
|
|
const timeout = setTimeout(() => app.exit(1), 60000);
|
|
app.whenReady().then(async () => {
|
|
const win = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true, sandbox: true } });
|
|
await win.loadFile(path.join(__dirname, 'index.html'));
|
|
const results = await win.webContents.executeJavaScript(`(async () => {
|
|
await document.fonts.load('600 144px Oswald');
|
|
if (![...document.fonts].some(face => face.family === 'Oswald' && face.status === 'loaded')) throw new Error('Bundled Oswald must load');
|
|
const load = async src => { const image = new Image(); image.src = src; await image.decode(); return image; };
|
|
const color = await load('assets/logos/logo.png');
|
|
const mono = await load('assets/logos/mono-source.png');
|
|
const white = await load('assets/logos/white-source.png');
|
|
const files = [];
|
|
const d = 600, pad = 150, gap = 60, lineGap = 48, wordWidth = 800;
|
|
const measure = (ctx, text, size) => {
|
|
ctx.font = '600 ' + size + 'px Oswald';
|
|
const m = ctx.measureText(text);
|
|
return { text, size, left: m.actualBoundingBoxLeft, width: m.actualBoundingBoxLeft + m.actualBoundingBoxRight,
|
|
ascent: m.actualBoundingBoxAscent, height: m.actualBoundingBoxAscent + m.actualBoundingBoxDescent };
|
|
};
|
|
for (const layout of ['mark', 'horizontal', 'vertical']) {
|
|
for (const variant of ['color-light', 'color-dark', 'black', 'white']) {
|
|
if (layout === 'mark' && variant !== 'white') continue;
|
|
for (const background of ['transparent', 'background']) {
|
|
const canvas = document.createElement('canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
let size = 144;
|
|
for (let i = 0; i < 3; i++) size *= wordWidth / measure(ctx, 'ДЕТЕКТИВ', size).width;
|
|
const lower = measure(ctx, 'ДЕТЕКТИВ', size);
|
|
const upper = measure(ctx, 'ВЕЧЕРНИЙ', size * 0.45);
|
|
if (Math.abs(lower.width - wordWidth) > 1) throw new Error('Wordmark width must be 4/3 of emblem diameter');
|
|
const textHeight = upper.height + lineGap + lower.height;
|
|
const contentWidth = layout === 'horizontal' ? d + gap + Math.ceil(lower.width) : layout === 'mark' ? d : Math.ceil(lower.width);
|
|
const contentHeight = layout === 'vertical' ? Math.ceil(d + gap + textHeight) : d;
|
|
canvas.width = contentWidth + pad * 2;
|
|
canvas.height = contentHeight + pad * 2;
|
|
const dark = variant === 'color-dark' || variant === 'white';
|
|
if (background === 'background') { ctx.fillStyle = dark ? '#111111' : '#f4ede0'; ctx.fillRect(0,0,canvas.width,canvas.height); }
|
|
ctx.filter = variant === 'black' ? 'brightness(0)' : 'none';
|
|
const imageX = layout === 'vertical' ? (canvas.width - d) / 2 : pad;
|
|
ctx.save();
|
|
if (variant === 'white') {
|
|
ctx.beginPath();
|
|
ctx.arc(imageX + d / 2, pad + d / 2, d * 0.48, 0, Math.PI * 2);
|
|
ctx.clip();
|
|
}
|
|
ctx.drawImage(variant === 'white' ? white : variant.startsWith('color') ? color : mono, imageX, pad, d, d);
|
|
ctx.restore();
|
|
ctx.filter = 'none';
|
|
if (layout !== 'mark') {
|
|
ctx.fillStyle = variant === 'white' ? '#ffffff' : variant === 'black' ? '#000000' : dark ? '#ffffff' : '#111111';
|
|
ctx.textBaseline = 'alphabetic';
|
|
ctx.textAlign = 'left';
|
|
const top = layout === 'vertical' ? pad + d + gap : pad + (d - textHeight) / 2;
|
|
for (const [line, y] of [[upper, top], [lower, top + upper.height + lineGap]]) {
|
|
const x = layout === 'vertical' ? (canvas.width - line.width) / 2 : pad + d + gap;
|
|
if (x < pad - 1 || x + line.width > canvas.width - pad + 1 || y < pad || y + line.height > canvas.height - pad + 1)
|
|
throw new Error('Text exceeds safety margins');
|
|
ctx.font = '600 ' + line.size + 'px Oswald';
|
|
ctx.fillText(line.text, x + line.left, y + line.ascent);
|
|
}
|
|
// Match the ring accent; symmetric rules keep the stacked lockup balanced.
|
|
ctx.fillStyle = variant.startsWith('color') ? '#fcbf02' : variant === 'white' ? '#ffffff' : '#000000';
|
|
const blockX = layout === 'vertical' ? (canvas.width - lower.width) / 2 : pad + d + gap;
|
|
const upperX = layout === 'vertical' ? (canvas.width - upper.width) / 2 : blockX;
|
|
const ruleY = Math.round(top + upper.height / 2 - 4);
|
|
const ruleStart = upperX + upper.width + 36;
|
|
const ruleEnd = blockX + lower.width;
|
|
if (ruleEnd <= ruleStart) throw new Error('Accent line needs space after upper word');
|
|
ctx.fillRect(ruleStart, ruleY, ruleEnd - ruleStart, 8);
|
|
if (layout === 'vertical') ctx.fillRect(blockX, ruleY, upperX - 36 - blockX, 8);
|
|
// The horizontal wordmark is the magnifier handle; its glint sits below the gold edge.
|
|
if (layout === 'horizontal') {
|
|
ctx.strokeStyle = dark ? '#ffffff' : '#000000';
|
|
ctx.lineWidth = 8;
|
|
ctx.lineCap = 'round';
|
|
const start = ruleStart + 36, end = ruleEnd - 60;
|
|
const y = ruleY + 33;
|
|
if (y + 8 >= top + upper.height + lineGap) throw new Error('Handle glint overlaps lower word');
|
|
ctx.beginPath();
|
|
ctx.moveTo(start, y);
|
|
ctx.lineTo(start + 18, y);
|
|
ctx.moveTo(start + 40, y);
|
|
ctx.lineTo(end, y);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
const pixels = ctx.getImageData(0,0,canvas.width,canvas.height).data;
|
|
if (background === 'transparent' && pixels[3] !== 0) throw new Error('Transparent padding required');
|
|
if (background === 'background' && pixels[3] !== 255) throw new Error('Opaque background required');
|
|
files.push({ name: layout + '-' + variant + '-' + background + '.png', width: canvas.width, height: canvas.height, data: canvas.toDataURL('image/png').split(',')[1] });
|
|
}
|
|
}
|
|
}
|
|
return files;
|
|
})()`);
|
|
assert.equal(results.length, 18);
|
|
for (const result of results) await fs.writeFile(path.join(__dirname, 'assets/logos', result.name), Buffer.from(result.data, 'base64'));
|
|
console.log('Exported 16 wordmark files and two white-rim standalone marks, with transparent padding or opaque backgrounds.');
|
|
win.destroy(); clearTimeout(timeout); app.quit();
|
|
}).catch(error => { console.error(error); app.exit(1); });
|