forked from boos/BoosAPI
feat: add masked token placeholders and desktop icons
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { mkdir } from 'node:fs/promises';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import sharp from 'sharp';
|
||||
|
||||
export const DESKTOP_ICON_SIZE = 512;
|
||||
export const DESKTOP_ICON_PADDING = 40;
|
||||
export const DESKTOP_ICON_RADIUS = 96;
|
||||
|
||||
function createRoundedMask(size, cornerRadius) {
|
||||
return Buffer.from(
|
||||
`<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg"><rect width="${size}" height="${size}" rx="${cornerRadius}" ry="${cornerRadius}" fill="#fff"/></svg>`,
|
||||
);
|
||||
}
|
||||
|
||||
async function renderDesktopIconBuffer({
|
||||
sourcePath,
|
||||
size = DESKTOP_ICON_SIZE,
|
||||
padding = DESKTOP_ICON_PADDING,
|
||||
cornerRadius = DESKTOP_ICON_RADIUS,
|
||||
}) {
|
||||
const innerSize = size - padding * 2;
|
||||
const roundedMask = createRoundedMask(innerSize, Math.min(cornerRadius, Math.floor(innerSize / 2)));
|
||||
|
||||
const roundedInner = await sharp(sourcePath)
|
||||
.resize(innerSize, innerSize, {
|
||||
fit: 'contain',
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
||||
})
|
||||
.composite([{ input: roundedMask, blend: 'dest-in' }])
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
return sharp({
|
||||
create: {
|
||||
width: size,
|
||||
height: size,
|
||||
channels: 4,
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
||||
},
|
||||
})
|
||||
.composite([{ input: roundedInner, left: padding, top: padding }])
|
||||
.png()
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
export async function generateDesktopIconAssets({
|
||||
sourcePath = join(process.cwd(), 'src', 'web', 'public', 'logo.png'),
|
||||
buildOutputPath = join(process.cwd(), 'build', 'desktop-icon.png'),
|
||||
webOutputPath = join(process.cwd(), 'src', 'web', 'public', 'desktop-icon.png'),
|
||||
size = DESKTOP_ICON_SIZE,
|
||||
padding = DESKTOP_ICON_PADDING,
|
||||
cornerRadius = DESKTOP_ICON_RADIUS,
|
||||
} = {}) {
|
||||
const outputBuffer = await renderDesktopIconBuffer({
|
||||
sourcePath,
|
||||
size,
|
||||
padding,
|
||||
cornerRadius,
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
mkdir(dirname(buildOutputPath), { recursive: true }),
|
||||
mkdir(dirname(webOutputPath), { recursive: true }),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
sharp(outputBuffer).toFile(buildOutputPath),
|
||||
sharp(outputBuffer).toFile(webOutputPath),
|
||||
]);
|
||||
|
||||
return {
|
||||
buildOutputPath,
|
||||
webOutputPath,
|
||||
};
|
||||
}
|
||||
|
||||
const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
||||
|
||||
if (isDirectRun) {
|
||||
const outputs = await generateDesktopIconAssets();
|
||||
console.log(`[metapi-desktop] Generated desktop icons:
|
||||
- ${outputs.buildOutputPath}
|
||||
- ${outputs.webOutputPath}`);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import sharp from 'sharp';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
async function makeTempDir() {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'metapi-desktop-icons-'));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
function alphaAt(buffer: Buffer, width: number, x: number, y: number) {
|
||||
return buffer[(y * width + x) * 4 + 3];
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
describe('generateDesktopIconAssets', () => {
|
||||
it('writes rounded desktop icon outputs for build and runtime usage', async () => {
|
||||
const { generateDesktopIconAssets } = await import('./generate-icons.mjs');
|
||||
|
||||
const dir = await makeTempDir();
|
||||
const sourcePath = join(dir, 'logo.png');
|
||||
const buildOutputPath = join(dir, 'build.png');
|
||||
const webOutputPath = join(dir, 'desktop-icon.png');
|
||||
|
||||
await sharp({
|
||||
create: {
|
||||
width: 512,
|
||||
height: 512,
|
||||
channels: 4,
|
||||
background: { r: 255, g: 112, b: 48, alpha: 1 },
|
||||
},
|
||||
}).png().toFile(sourcePath);
|
||||
|
||||
await generateDesktopIconAssets({
|
||||
sourcePath,
|
||||
buildOutputPath,
|
||||
webOutputPath,
|
||||
});
|
||||
|
||||
const buildMeta = await sharp(buildOutputPath).metadata();
|
||||
const webMeta = await sharp(webOutputPath).metadata();
|
||||
|
||||
expect(buildMeta.width).toBe(512);
|
||||
expect(buildMeta.height).toBe(512);
|
||||
expect(webMeta.width).toBe(512);
|
||||
expect(webMeta.height).toBe(512);
|
||||
|
||||
const { data, info } = await sharp(buildOutputPath)
|
||||
.ensureAlpha()
|
||||
.raw()
|
||||
.toBuffer({ resolveWithObject: true });
|
||||
|
||||
expect(alphaAt(data, info.width, 0, 0)).toBe(0);
|
||||
expect(alphaAt(data, info.width, 12, 12)).toBe(0);
|
||||
expect(alphaAt(data, info.width, Math.floor(info.width / 2), 6)).toBe(0);
|
||||
expect(alphaAt(data, info.width, Math.floor(info.width / 2), Math.floor(info.height / 2))).toBe(255);
|
||||
|
||||
expect(await sharp(buildOutputPath).png().toBuffer())
|
||||
.toEqual(await sharp(webOutputPath).png().toBuffer());
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user