#!/usr/bin/env bash
# Captures clean, chrome-free App Store stills: each of the 7 curated presets at peak
# density, per device size. Uses the DEBUG hooks PHYSARUM_BOOT_PRESET=<index> (boot a
# specific filled slot) + PHYSARUM_HIDE_UI=1 (launch chrome-free — reuses fullscreen, so
# topbar, panels, status bar + home indicator are all hidden). tvOS ignores HIDE_UI (its
# ambient view is already chrome-free without remote interaction).
#
# Output: docs/appstore/screenshots/<device>/<NN>-<name>.png
# Usage: scripts/shoot-appstore-stills.sh [device-label ...] (default: all three)
# labels: iphone-6.9 ipad-13 appletv
# Requires: Xcode + simulators (6.9" iPhone Pro Max, 13" iPad Pro, Apple TV 4K).
# Run ONLY when no other xcodebuild is active (concurrent builds on the same project race).
set -euo pipefail
cd "$(dirname "$0")/.."
SCHEME="Physarum"
PROJ="apple/PhysarumApp/Physarum.xcodeproj"
BUNDLE_ID="de.jkaindl.physarum"
OUT="docs/appstore/screenshots"
PRESETS=(aurora terminal-haze neon-waves solar-veil dancing-ink mandala mystic)
PEAK=15 # seconds to let each preset's pattern reach full density before the shot
SAT_MIN=2 # min SATAVG: a vibrant preset is ≫2; the grey tvOS home screen is ~0.2 (relaunch
# sometimes fails to foreground the app on the tvOS simulator → re-shoot)
# Integer SATAVG (average chroma) of a PNG, 0 if unreadable. Discriminates a vibrant preset
# from the near-grey home/Settings screen the tvOS simulator shows when a launch misses.
satavg() {
local s
s=$(ffmpeg -i "$1" -vf "signalstats,metadata=print" -f null - 2>&1 \
| grep -oE "signalstats.SATAVG=[0-9.]+" | head -1 | cut -d= -f2)
s=${s%%.*}; echo "${s:-0}"
}
( cd apple/PhysarumApp && xcodegen generate )
shoot_device() {
local sim="$1" dir="$2" sdk="$3"
echo "==> $dir"
local udid
udid=$(xcrun simctl list devices available | grep -E "$sim" | grep -oiE '[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}' | head -1 || true)
[ -n "$udid" ] || { echo "skip: no simulator matching /$sim/"; return 0; }
xcrun simctl boot "$udid" 2>/dev/null || true
xcrun simctl bootstatus "$udid" -b
local dd="${TMPDIR:-/tmp}/physarum-stills-$dir"
xcodebuild -project "$PROJ" -scheme "$SCHEME" -destination "id=$udid" -derivedDataPath "$dd" build >/dev/null
local app
app=$(find "$dd/Build/Products" -name 'Physarum.app' -path "*$sdk*" | head -1)
[ -n "$app" ] || { echo "skip: no .app built for $dir"; return 0; }
xcrun simctl install "$udid" "$app"
mkdir -p "$OUT/$dir"
local i name out attempt sat
for i in $(seq 0 6); do
name="${PRESETS[$i]}"
out="$OUT/$dir/$(printf '%02d' $((i + 1)))-$name.png"
printf ' [%d/7] %s …\n' "$((i + 1))" "$name"
# Re-run friendly: keep an already-valid shot, only (re)capture missing/broken ones.
if [ -f "$out" ] && [ "$(satavg "$out")" -ge "$SAT_MIN" ]; then
echo " ✓ valid shot exists — skip"; continue
fi
attempt=0
while :; do
attempt=$((attempt + 1))
xcrun simctl terminate "$udid" "$BUNDLE_ID" >/dev/null 2>&1 || true
sleep 1
SIMCTL_CHILD_PHYSARUM_BOOT_PRESET="$i" SIMCTL_CHILD_PHYSARUM_HIDE_UI=1 \
xcrun simctl launch "$udid" "$BUNDLE_ID" >/dev/null 2>&1 || true
sleep "$PEAK"
xcrun simctl io "$udid" screenshot "$out" >/dev/null 2>&1 || true
sat=$(satavg "$out")
if [ "$sat" -ge "$SAT_MIN" ]; then echo " ✓ sat=$sat (try $attempt)"; break; fi
if [ "$attempt" -ge 5 ]; then echo " ⚠ low sat=$sat after $attempt tries — keeping"; break; fi
echo " retry (sat=$sat — app not foreground?)"
done
done
echo " wrote 7 stills to $OUT/$dir/"
}
DEVICES="${*:-iphone-6.9 ipad-13 appletv}"
for d in $DEVICES; do
case "$d" in
iphone-6.9) shoot_device "iPhone 1[5-9] Pro Max" "iphone-6.9" "iphonesimulator" ;;
ipad-13) shoot_device "iPad Pro 13-inch" "ipad-13" "iphonesimulator" ;;
appletv) shoot_device "Apple TV 4K" "appletv" "appletvsimulator" ;;
*) echo "unknown device label: $d (use iphone-6.9 | ipad-13 | appletv)" >&2 ;;
esac
done
echo "Done. Review $OUT/*/"