fix: Bug - Android app is zoomed in

Scale launcher and splash brand assets to the adaptive-icon safe zone and a smaller splash ratio so circular launcher masks and the launch splash no longer crop the cat face.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-11 21:20:01 +02:00
parent bdea95511d
commit a01abbb1bf
31 changed files with 89 additions and 14 deletions

View File

@@ -23,6 +23,11 @@ const RES_DIR = resolve(REPO_ROOT, 'toju-app/android/app/src/main/res');
const BRAND_BACKGROUND = { r: 74, g: 33, b: 122 };
const BRAND_BACKGROUND_HEX = '#4A217A';
/** Adaptive-icon safe zone (66dp inside 108dp). Keep in sync with the rules file. */
const ADAPTIVE_FOREGROUND_ICON_RATIO = 66 / 108;
const LEGACY_LAUNCHER_ICON_RATIO = ADAPTIVE_FOREGROUND_ICON_RATIO;
const SPLASH_ICON_RATIO = 0.32;
/** Legacy square/round launcher bitmap edge length per density. */
const LEGACY_ICON_PX = { mdpi: 48, hdpi: 72, xhdpi: 96, xxhdpi: 144, xxxhdpi: 192 };
@@ -65,20 +70,36 @@ async function centeredIconOnBackground(width, height, iconRatio, background) {
.toBuffer();
}
/** Compose the brand icon centred on a transparent canvas for adaptive foreground layers. */
async function centeredIconOnTransparentCanvas(canvasSize, iconRatio) {
const iconSize = Math.round(canvasSize * iconRatio);
const icon = await squareIcon(iconSize);
return sharp({
create: {
width: canvasSize,
height: canvasSize,
channels: 4,
background: { r: 0, g: 0, b: 0, alpha: 0 }
}
})
.composite([{ input: icon, gravity: 'center' }])
.png()
.toBuffer();
}
async function generateLauncherIcons() {
const written = [];
for (const [density, size] of Object.entries(LEGACY_ICON_PX)) {
const bitmap = await squareIcon(size);
const background = { ...BRAND_BACKGROUND, alpha: 1 };
const bitmap = await centeredIconOnBackground(size, size, LEGACY_LAUNCHER_ICON_RATIO, background);
written.push(await writePng(bitmap, `mipmap-${density}/ic_launcher.png`));
written.push(await writePng(bitmap, `mipmap-${density}/ic_launcher_round.png`));
}
// Adaptive foreground: full-bleed circle on transparent canvas. The adaptive
// background layer paints the brand purple, so the masked result matches the
// source disc on every launcher mask shape.
for (const [density, size] of Object.entries(FOREGROUND_PX)) {
const foreground = await squareIcon(size);
const foreground = await centeredIconOnTransparentCanvas(size, ADAPTIVE_FOREGROUND_ICON_RATIO);
written.push(await writePng(foreground, `mipmap-${density}/ic_launcher_foreground.png`));
}
@@ -97,14 +118,13 @@ async function generateSplashScreens() {
const background = { ...BRAND_BACKGROUND, alpha: 1 };
for (const [density, [portW, portH]] of Object.entries(SPLASH_PORTRAIT)) {
const portrait = await centeredIconOnBackground(portW, portH, 0.4, background);
const landscape = await centeredIconOnBackground(portH, portW, 0.4, background);
const portrait = await centeredIconOnBackground(portW, portH, SPLASH_ICON_RATIO, background);
const landscape = await centeredIconOnBackground(portH, portW, SPLASH_ICON_RATIO, background);
written.push(await writePng(portrait, `drawable-port-${density}/splash.png`));
written.push(await writePng(landscape, `drawable-land-${density}/splash.png`));
}
// Base fallback splash mirrors the landscape mdpi geometry (Capacitor default).
const base = await centeredIconOnBackground(480, 320, 0.4, background);
const base = await centeredIconOnBackground(480, 320, SPLASH_ICON_RATIO, background);
written.push(await writePng(base, 'drawable/splash.png'));
return written;