// One-off placeholder generator: crops the paw glyph out of assets/icon.png // (detected by its marigold-orange color, since the glyph sits to the left // of the "PawFeed" wordmark) and derives Android adaptive-icon layers from // it. This is explicitly a stand-in — see mobile/README.md — real // foreground/background/monochrome art should replace these. const sharp = require("sharp"); const path = require("path"); const ASSETS = path.join(__dirname, "..", "assets"); const CANVAS = 1024; const MARIGOLD = { r: 249, g: 115, b: 22 }; const PORCH_WHITE = "#fafafa"; function isMarigold(r, g, b) { return Math.abs(r - MARIGOLD.r) < 40 && Math.abs(g - MARIGOLD.g) < 40 && Math.abs(b - MARIGOLD.b) < 40; } async function findGlyphBounds(iconPath) { const { data, info } = await sharp(iconPath).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++) { const i = (y * info.width + x) * info.channels; if (isMarigold(data[i], data[i + 1], data[i + 2])) { if (x < minX) minX = x; if (x > maxX) maxX = x; if (y < minY) minY = y; if (y > maxY) maxY = y; } } } return { left: minX, top: minY, width: maxX - minX + 1, height: maxY - minY + 1 }; } // The crop still carries the icon's near-white background as opaque // pixels — key those out so only the orange glyph stays visible, with a // soft-edged alpha falloff so it doesn't look hard-cut at low resolution. async function makeGlyphTransparent(croppedBuffer) { const { data, info } = await sharp(croppedBuffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true }); for (let p = 0; p < data.length; p += info.channels) { const r = data[p], g = data[p + 1], b = data[p + 2]; const distance = Math.abs(r - MARIGOLD.r) + Math.abs(g - MARIGOLD.g) + Math.abs(b - MARIGOLD.b); data[p + 3] = distance < 60 ? 255 : distance < 140 ? 255 - Math.round(((distance - 60) / 80) * 255) : 0; } return sharp(data, { raw: { width: info.width, height: info.height, channels: info.channels } }).png().toBuffer(); } async function main() { const iconPath = path.join(ASSETS, "icon.png"); const bounds = await findGlyphBounds(iconPath); const opaqueCrop = await sharp(iconPath).extract(bounds).toBuffer(); const glyph = await makeGlyphTransparent(opaqueCrop); const glyphMeta = await sharp(glyph).metadata(); // Scale the glyph to ~45% of the canvas, centered — comfortably inside // Android's adaptive-icon safe zone regardless of launcher mask shape. const targetSize = Math.round(CANVAS * 0.45); const scale = targetSize / Math.max(glyphMeta.width, glyphMeta.height); const resizedWidth = Math.round(glyphMeta.width * scale); const resizedHeight = Math.round(glyphMeta.height * scale); const resizedGlyph = await sharp(glyph).resize(resizedWidth, resizedHeight).toBuffer(); const left = Math.round((CANVAS - resizedWidth) / 2); const top = Math.round((CANVAS - resizedHeight) / 2); // Foreground: the orange glyph, centered, transparent background. await sharp({ create: { width: CANVAS, height: CANVAS, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } } }) .composite([{ input: resizedGlyph, left, top }]) .png() .toFile(path.join(ASSETS, "android-icon-foreground.png")); // Background: solid porch-white, matching the web app's canvas color. await sharp({ create: { width: CANVAS, height: CANVAS, channels: 4, background: PORCH_WHITE } }) .png() .toFile(path.join(ASSETS, "android-icon-background.png")); // Monochrome: same glyph silhouette, recolored solid white (Android tints // this layer itself for themed/Material You icons). const whiteGlyph = await sharp(resizedGlyph) .ensureAlpha() .composite([{ input: Buffer.from([255, 255, 255, 255]), raw: { width: 1, height: 1, channels: 4 }, tile: true, blend: "in" }]) .toBuffer(); await sharp({ create: { width: CANVAS, height: CANVAS, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } } }) .composite([{ input: whiteGlyph, left, top }]) .png() .toFile(path.join(ASSETS, "android-icon-monochrome.png")); // Splash icon: same as foreground (transparent glyph), matches Expo's // splash-screen convention of a centered logo on a solid background. await sharp({ create: { width: CANVAS, height: CANVAS, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } } }) .composite([{ input: resizedGlyph, left, top }]) .png() .toFile(path.join(ASSETS, "splash-icon.png")); console.log("Derived adaptive icon layers from", bounds); } main().catch((err) => { console.error(err); process.exit(1); });