| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159 |
- #!/usr/bin/env node
- import fs from 'fs';
- import os from 'os';
- import path from 'path';
- import { execFileSync } from 'child_process';
- import { fileURLToPath } from 'url';
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
- const projectRoot = path.resolve(__dirname, '..');
- const iconsDir = path.join(projectRoot, 'build', 'icons');
- const publicAssetsDir = path.join(projectRoot, 'public', 'assets');
- const sourcePngPath = path.join(publicAssetsDir, 'vlcode-lite-source.png');
- const svgPath = path.join(iconsDir, 'vlcode-lite.svg');
- const icnsPath = path.join(iconsDir, 'vlcode-lite.icns');
- const publicSvgPath = path.join(publicAssetsDir, 'vlcode-lite-icon.svg');
- const publicPngPath = path.join(publicAssetsDir, 'vlcode-lite-icon.png');
- const publicFavicon32Path = path.join(publicAssetsDir, 'vlcode-lite-favicon-32.png');
- const publicFavicon16Path = path.join(publicAssetsDir, 'vlcode-lite-favicon-16.png');
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vlcode-icon-'));
- const iconsetDir = path.join(tempDir, 'vlcode-lite.iconset');
- const iconSizes = [
- { size: 16, scale: 1 },
- { size: 16, scale: 2 },
- { size: 32, scale: 1 },
- { size: 32, scale: 2 },
- { size: 128, scale: 1 },
- { size: 128, scale: 2 },
- { size: 256, scale: 1 },
- { size: 256, scale: 2 },
- { size: 512, scale: 1 },
- { size: 512, scale: 2 },
- ];
- function run(command, args) {
- execFileSync(command, args, { stdio: 'inherit' });
- }
- function readImageSize(filePath) {
- const output = execFileSync('sips', ['-g', 'pixelWidth', '-g', 'pixelHeight', filePath], { encoding: 'utf8' });
- const width = Number(output.match(/pixelWidth:\s*(\d+)/)?.[1] || 0);
- const height = Number(output.match(/pixelHeight:\s*(\d+)/)?.[1] || 0);
- if (!width || !height) {
- throw new Error(`Could not determine image dimensions for ${filePath}`);
- }
- return { width, height };
- }
- function buildSvgFromPng(pngPath) {
- const canvasSize = 1024;
- const panelInset = 44;
- const panelSize = canvasSize - panelInset * 2;
- const panelRadius = 232;
- const pngBase64 = fs.readFileSync(pngPath).toString('base64');
- const { width, height } = readImageSize(pngPath);
- const maxWidth = 900;
- const maxHeight = 672;
- const scale = Math.min(maxWidth / width, maxHeight / height);
- const drawWidth = Math.round(width * scale);
- const drawHeight = Math.round(height * scale);
- const x = Math.round((canvasSize - drawWidth) / 2);
- const y = Math.round((canvasSize - drawHeight) / 2);
- return `<svg width="${canvasSize}" height="${canvasSize}" viewBox="0 0 ${canvasSize} ${canvasSize}" fill="none" xmlns="http://www.w3.org/2000/svg">
- <defs>
- <linearGradient id="bg" x1="120" y1="72" x2="912" y2="968" gradientUnits="userSpaceOnUse">
- <stop stop-color="#081018"/>
- <stop offset="1" stop-color="#0C1724"/>
- </linearGradient>
- <radialGradient id="glow" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(512 288) rotate(90) scale(560 660)">
- <stop stop-color="#3C9BFF" stop-opacity="0.32"/>
- <stop offset="0.56" stop-color="#39C8D6" stop-opacity="0.14"/>
- <stop offset="1" stop-color="#39C8D6" stop-opacity="0"/>
- </radialGradient>
- <linearGradient id="shine" x1="200" y1="124" x2="704" y2="652" gradientUnits="userSpaceOnUse">
- <stop stop-color="#FFFFFF" stop-opacity="0.10"/>
- <stop offset="0.42" stop-color="#FFFFFF" stop-opacity="0.02"/>
- <stop offset="1" stop-color="#FFFFFF" stop-opacity="0"/>
- </linearGradient>
- <filter id="shadow" x="0" y="0" width="${canvasSize}" height="${canvasSize}" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
- <feDropShadow dx="0" dy="22" stdDeviation="26" flood-color="#03060C" flood-opacity="0.42"/>
- </filter>
- <filter id="logoMaskFilter" x="0" y="0" width="${canvasSize}" height="${canvasSize}" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
- <feColorMatrix in="SourceGraphic" type="matrix" values="
- 0 0 0 0 0
- 0 0 0 0 0
- 0 0 0 0 0
- 3 3 3 0 -0.35
- "/>
- </filter>
- <clipPath id="panel">
- <rect x="${panelInset}" y="${panelInset}" width="${panelSize}" height="${panelSize}" rx="${panelRadius}"/>
- </clipPath>
- <mask id="logoMask" x="${x}" y="${y}" width="${drawWidth}" height="${drawHeight}" maskUnits="userSpaceOnUse" maskContentUnits="userSpaceOnUse" style="mask-type:alpha">
- <image href="data:image/png;base64,${pngBase64}" x="${x}" y="${y}" width="${drawWidth}" height="${drawHeight}" preserveAspectRatio="xMidYMid meet" filter="url(#logoMaskFilter)"/>
- </mask>
- </defs>
- <g clip-path="url(#panel)">
- <rect x="${panelInset}" y="${panelInset}" width="${panelSize}" height="${panelSize}" rx="${panelRadius}" fill="url(#bg)"/>
- <rect x="${panelInset}" y="${panelInset}" width="${panelSize}" height="${panelSize}" rx="${panelRadius}" fill="url(#glow)"/>
- <rect x="${panelInset}" y="${panelInset}" width="${panelSize}" height="${panelSize}" rx="${panelRadius}" fill="url(#shine)"/>
- <g filter="url(#shadow)">
- <image href="data:image/png;base64,${pngBase64}" x="${x}" y="${y}" width="${drawWidth}" height="${drawHeight}" preserveAspectRatio="xMidYMid meet" mask="url(#logoMask)"/>
- </g>
- </g>
- </svg>
- `;
- }
- function findRenderedPng(outputDir) {
- const matches = fs.readdirSync(outputDir)
- .filter((entry) => entry.endsWith('.png'))
- .map((entry) => path.join(outputDir, entry));
- if (matches.length === 0) {
- throw new Error('Quick Look did not produce a PNG preview for the SVG icon');
- }
- return matches[0];
- }
- try {
- fs.mkdirSync(iconsDir, { recursive: true });
- fs.mkdirSync(publicAssetsDir, { recursive: true });
- if (fs.existsSync(sourcePngPath)) {
- fs.writeFileSync(svgPath, buildSvgFromPng(sourcePngPath), 'utf8');
- }
- if (!fs.existsSync(svgPath)) {
- throw new Error(`Missing source SVG: ${svgPath}`);
- }
- fs.copyFileSync(svgPath, publicSvgPath);
- fs.mkdirSync(iconsetDir, { recursive: true });
- run('qlmanage', ['-t', '-s', '1024', '-o', tempDir, svgPath]);
- const basePng = findRenderedPng(tempDir);
- fs.copyFileSync(basePng, publicPngPath);
- run('sips', ['-z', '32', '32', basePng, '--out', publicFavicon32Path]);
- run('sips', ['-z', '16', '16', basePng, '--out', publicFavicon16Path]);
- for (const icon of iconSizes) {
- const pixels = icon.size * icon.scale;
- const suffix = icon.scale === 2 ? '@2x' : '';
- const outputPath = path.join(iconsetDir, `icon_${icon.size}x${icon.size}${suffix}.png`);
- run('sips', ['-z', String(pixels), String(pixels), basePng, '--out', outputPath]);
- }
- fs.rmSync(icnsPath, { force: true });
- run('iconutil', ['-c', 'icns', iconsetDir, '-o', icnsPath]);
- console.log(`[build-icon] Wrote ${svgPath}`);
- console.log(`[build-icon] Wrote ${publicSvgPath}`);
- console.log(`[build-icon] Wrote ${publicPngPath}`);
- console.log(`[build-icon] Wrote ${publicFavicon32Path}`);
- console.log(`[build-icon] Wrote ${publicFavicon16Path}`);
- console.log(`[build-icon] Wrote ${icnsPath}`);
- } finally {
- fs.rmSync(tempDir, { recursive: true, force: true });
- }
|