#!/usr/bin/env bash # # Regenerates the Android adaptive-icon foreground from the iOS icon. # # scripts/gen-adaptive-icon.sh # check, and generate if missing # scripts/gen-adaptive-icon.sh --force # regenerate even if one exists but differs # # Android launcher masks (round, squircle, teardrop — the launcher decides) crop the icon. The art is # a full-bleed render on a black backdrop, so a mask ate its edges. The fix is to shrink the art inside # the same canvas and let the transparent margin absorb whatever the mask takes. # # Only Android needs this. iOS draws icon.png full-bleed with its own rounding, so insetting there # would just add a black border — icon.png is never touched. # # Runs from the `prebuild` npm script, because `expo prebuild` is what turns these assets into # android/app/src/main/res/*.webp. The generated PNG is also committed, so a machine without # ImageMagick still builds a correct icon — this regenerates it, it is not the only source. That is # also why calling `expo prebuild` directly instead of `bun run prebuild` is a mistake: it skips this # silently. set -euo pipefail # Android's adaptive-icon spec: the outer 1/3 of the foreground is reserved for mask and parallax, # leaving a guaranteed-visible circle of ~66%. 72% sits just above that — as large as the art can be # drawn while no supported mask clips it. readonly INSET_PERCENT=72 ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ICON="$ROOT/assets/icon.png" ADAPTIVE="$ROOT/assets/adaptive-icon.png" FORCE=false for a in "$@"; do [[ "$a" == "--force" ]] && FORCE=true; done if ! command -v magick >/dev/null 2>&1; then echo "!! ImageMagick not found — skipping adaptive-icon generation." >&2 echo "!! The committed adaptive-icon.png is used as-is. brew install imagemagick" >&2 exit 0 fi # True when `existing` already IS `icon` inset — compared by pixel content, not by bytes and not by # geometry. # # Geometry alone is not enough: checking only that the art fills ~72% of the canvas passes an adaptive # icon built from SUPERSEDED art, so swapping in new icon.png art leaves the Android icon silently # stale. Bytes are not enough in the other direction: re-encoding an identical image can differ, which # would rewrite a megabyte on every prebuild. RMSE of the decoded pixels is exact here, so the # threshold only absorbs a future encoder change. matches_icon() { local existing="$1" icon="$2" size="$3" art="$4" local tmp="$existing.expected.png" magick "$icon" -resize "${art}x${art}" -background none -gravity center \ -extent "${size}x${size}" "$tmp" 2>/dev/null || { rm -f "$tmp"; return 1; } # Differing canvas sizes make compare fail outright, which is itself a mismatch. local rmse rmse="$(magick compare -metric RMSE "$existing" "$tmp" null: 2>&1 || true)" rmse="${rmse%% *}" rm -f "$tmp" awk -v v="$rmse" 'BEGIN { exit !(v ~ /^[0-9.]+$/ && v < 100) }' } [[ -f "$ICON" ]] || { echo "!! no assets/icon.png" >&2; exit 1; } # Same canvas as the source; only the art inside it shrinks. Rounded, not truncated. size="$(magick identify -format '%w' "$ICON")" art=$(((size * INSET_PERCENT + 50) / 100)) if [[ -f "$ADAPTIVE" ]] && matches_icon "$ADAPTIVE" "$ICON" "$size" "$art"; then echo "adaptive icon: ok" elif [[ -f "$ADAPTIVE" ]] && [[ "$FORCE" != true ]]; then # Deliberately does NOT overwrite. An existing adaptive icon that does not match icon.png is # ambiguous: it is either stale art (regenerate) or art framed on purpose. Say so and let a human # decide; --force applies the regeneration. echo "adaptive icon: DIFFERS from icon.png — not overwritten (--force to regenerate)" else magick "$ICON" -resize "${art}x${art}" \ -background none -gravity center -extent "${size}x${size}" "$ADAPTIVE" echo "adaptive icon: regenerated (${size} canvas, ${art} art)" fi # The adaptive background sits behind the inset art, so it must match the art's own backdrop or a seam # shows at the edges. This only reports — app.json is easy to edit by hand and silently rewriting it # is worse than saying what to change. corner="#$(magick "$ICON" -format '%[hex:p{0,0}]' info: | tr '[:lower:]' '[:upper:]')" configured="$(grep -oE 'backgroundColor.{0,4}#[0-9A-Fa-f]{6}' "$ROOT/app.json" 2>/dev/null \ | grep -oE '#[0-9A-Fa-f]{6}' | head -1 | tr '[:lower:]' '[:upper:]' || true)" if [[ -z "$configured" ]]; then echo " no android.adaptiveIcon.backgroundColor set — use $corner" elif [[ "$corner" != "$configured" ]]; then echo " backgroundColor is $configured, art corner is $corner" fi