Zum Inhalt

scripts/gen-tvos-icons.sh

#!/usr/bin/env bash
# Regenerates the tvOS Brand Assets from assets/icon*.svg — PNGs *and* every
# Contents.json, so the whole TVBrandAssets.brandassets is reproducible from one run:
#   ./scripts/gen-tvos-icons.sh
# App icons are 2-layer parallax stacks (BACK = brand gradient, FRONT = rings motif).
# The home-screen icon ships @1x + @2x; the App Store icon is @1x; Top Shelf images @1x + @2x.
# Requires rsvg-convert (brew install librsvg).
#
# NB: the PNGs are the canonical, byte-reproducible artifacts. The emitted Contents.json
# get re-formatted by biome on commit (lefthook), so re-running this produces whitespace-
# only diffs on the JSON — that is expected and cosmetic (semantics + PNGs are identical).
set -euo pipefail
cd "$(dirname "$0")/.."

ICON="assets/icon.svg"
BGSVG="assets/icon-tv-bg.svg"
MOTIF="assets/icon-tv-motif.svg"
BG="#050608"   # icon's outer gradient stop → seamless top-shelf margins
BA="apple/PhysarumApp/Resources/Assets.xcassets/TVBrandAssets.brandassets"
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT

command -v rsvg-convert >/dev/null || { echo "missing rsvg-convert (brew install librsvg)" >&2; exit 1; }
for f in "$ICON" "$BGSVG" "$MOTIF"; do [ -f "$f" ] || { echo "missing $f" >&2; exit 1; }; done

xc() { printf '%s\n' '{ "info" : { "author" : "xcode", "version" : 1 } }' > "$1"; }   # bare Contents.json

# motif (square, transparent) centered on a transparent W×H canvas → app-icon FRONT layer
front_png() {  # <W> <H> <out>
  local W="$1" H="$2" out="$3" x=$(( ($1 - $2) / 2 ))
  rsvg-convert -w "$H" -h "$H" "$MOTIF" -o "$TMP/motif-$H.png"
  cat > "$TMP/front-$H.svg" <<EOF
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="$W" height="$H">
  <image x="$x" y="0" width="$H" height="$H" xlink:href="motif-$H.png"/>
</svg>
EOF
  rsvg-convert -w "$W" -h "$H" "$TMP/front-$H.svg" -o "$out"
}

rm -rf "$BA"; mkdir -p "$BA"

cat > "$BA/Contents.json" <<'EOF'
{
  "assets" : [
    { "filename" : "App Icon - App Store.imagestack", "idiom" : "tv", "role" : "primary-app-icon", "size" : "1280x768" },
    { "filename" : "App Icon.imagestack", "idiom" : "tv", "role" : "primary-app-icon", "size" : "400x240" },
    { "filename" : "Top Shelf Image Wide.imageset", "idiom" : "tv", "role" : "top-shelf-image-wide", "size" : "2320x720" },
    { "filename" : "Top Shelf Image.imageset", "idiom" : "tv", "role" : "top-shelf-image", "size" : "1920x720" }
  ],
  "info" : { "author" : "xcode", "version" : 1 }
}
EOF

# a 2-layer app-icon stack. <dir> <W> <H> <2x:0|1>
stack() {
  local dir="$1" W="$2" H="$3" twoX="${4:-0}"
  local back="$dir/Back.imagestacklayer" front="$dir/Front.imagestacklayer"
  mkdir -p "$back/Content.imageset" "$front/Content.imageset"
  cat > "$dir/Contents.json" <<'EOF'
{
  "info" : { "author" : "xcode", "version" : 1 },
  "layers" : [ { "filename" : "Front.imagestacklayer" }, { "filename" : "Back.imagestacklayer" } ]
}
EOF
  xc "$back/Contents.json"; xc "$front/Contents.json"
  rsvg-convert -w "$W" -h "$H" "$BGSVG" -o "$back/Content.imageset/back.png"
  front_png "$W" "$H" "$front/Content.imageset/front.png"
  if [ "$twoX" = "1" ]; then
    rsvg-convert -w "$((W*2))" -h "$((H*2))" "$BGSVG" -o "$back/Content.imageset/back@2x.png"
    front_png "$((W*2))" "$((H*2))" "$front/Content.imageset/front@2x.png"
    printf '%s\n' '{ "images" : [ { "idiom" : "tv", "filename" : "back.png", "scale" : "1x" }, { "idiom" : "tv", "filename" : "back@2x.png", "scale" : "2x" } ], "info" : { "author" : "xcode", "version" : 1 } }' > "$back/Content.imageset/Contents.json"
    printf '%s\n' '{ "images" : [ { "idiom" : "tv", "filename" : "front.png", "scale" : "1x" }, { "idiom" : "tv", "filename" : "front@2x.png", "scale" : "2x" } ], "info" : { "author" : "xcode", "version" : 1 } }' > "$front/Content.imageset/Contents.json"
  else
    printf '%s\n' '{ "images" : [ { "idiom" : "tv", "filename" : "back.png", "scale" : "1x" } ], "info" : { "author" : "xcode", "version" : 1 } }' > "$back/Content.imageset/Contents.json"
    printf '%s\n' '{ "images" : [ { "idiom" : "tv", "filename" : "front.png", "scale" : "1x" } ], "info" : { "author" : "xcode", "version" : 1 } }' > "$front/Content.imageset/Contents.json"
  fi
}

# a flat Top Shelf imageset (@1x + @2x), full icon centered on the brand bg
topshelf() {  # <setdir> <basename> <W1> <H1>
  local dir="$1" base="$2" W="$3" H="$4"
  mkdir -p "$dir"
  printf '%s\n' "{ \"images\" : [ { \"idiom\" : \"tv\", \"filename\" : \"$base-${W}x${H}.png\", \"scale\" : \"1x\" }, { \"idiom\" : \"tv\", \"filename\" : \"$base-$((W*2))x$((H*2)).png\", \"scale\" : \"2x\" } ], \"info\" : { \"author\" : \"xcode\", \"version\" : 1 } }" > "$dir/Contents.json"
  for s in 1 2; do
    local w=$((W*s)) h=$((H*s)) x; x=$(( (W*s - H*s) / 2 ))
    rsvg-convert -w "$h" -h "$h" "$ICON" -o "$TMP/full-$h.png"
    cat > "$TMP/ts.svg" <<EOF
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="$w" height="$h">
  <rect width="$w" height="$h" fill="$BG"/>
  <image x="$x" y="0" width="$h" height="$h" xlink:href="full-$h.png"/>
</svg>
EOF
    rsvg-convert -w "$w" -h "$h" "$TMP/ts.svg" -o "$dir/$base-${w}x${h}.png"
  done
}

stack "$BA/App Icon.imagestack" 400 240 1            # home-screen icon: @1x + @2x
stack "$BA/App Icon - App Store.imagestack" 1280 768 0   # App Store icon: @1x
topshelf "$BA/Top Shelf Image.imageset" topshelf 1920 720
topshelf "$BA/Top Shelf Image Wide.imageset" topshelfwide 2320 720

echo "tvOS brand assets rebuilt:"
find "$BA" -name "*.png" | sort