diff --git a/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/back.png b/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/back.png index 4fc1add..9776b34 100644 Binary files a/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/back.png and b/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/back.png differ diff --git a/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/front.png b/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/front.png index f0aec62..60ce44b 100644 Binary files a/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/front.png and b/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/front.png differ diff --git a/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/back.png b/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/back.png index 2d16d20..a555162 100644 Binary files a/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/back.png and b/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/back.png differ diff --git a/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/back@2x.png b/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/back@2x.png index ce0d3b9..3181404 100644 Binary files a/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/back@2x.png and b/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/back@2x.png differ diff --git a/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/front.png b/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/front.png index 10e2d1f..42944b0 100644 Binary files a/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/front.png and b/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/front.png differ diff --git a/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/front@2x.png b/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/front@2x.png index d0aeae2..2189c22 100644 Binary files a/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/front@2x.png and b/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/front@2x.png differ diff --git a/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/topshelfwide.png b/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/topshelfwide.png index 4d0dceb..30b9a40 100644 Binary files a/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/topshelfwide.png and b/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/topshelfwide.png differ diff --git a/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/topshelfwide@2x.png b/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/topshelfwide@2x.png index aca2855..2bd6211 100644 Binary files a/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/topshelfwide@2x.png and b/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/topshelfwide@2x.png differ diff --git a/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/topshelf.png b/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/topshelf.png index b151f3c..32d912e 100644 Binary files a/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/topshelf.png and b/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/topshelf.png differ diff --git a/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/topshelf@2x.png b/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/topshelf@2x.png index 7018415..d35cb2e 100644 Binary files a/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/topshelf@2x.png and b/FlixyTV/Resources/TVAssets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/topshelf@2x.png differ diff --git a/Tools/TVBrandAssets.py b/Tools/TVBrandAssets.py new file mode 100644 index 0000000..93ff67f --- /dev/null +++ b/Tools/TVBrandAssets.py @@ -0,0 +1,195 @@ +"""Baut die tvOS-Markenbilder aus derselben Vorlage wie das iOS-Icon. + +**Warum von Hand und nicht mit Icon Composer.** Das Werkzeug deckt in +Xcode 26 iOS, macOS und watchOS ab, tvOS nicht: ein Fernseher verlangt +gestapelte Bilder, damit die Parallaxe beim Wischen etwas zu bewegen hat. + +**Was hier nicht geraten ist.** Die Form kommt aus `play.svg` — dasselbe +gleichseitige Dreieck mit denselben runden Ecken, aus den Koordinaten des +Pfades gerechnet statt nachempfunden. Die Farben sind aus dem **gerenderten** +iOS-Icon gemessen (1024er Fassung im Bau, hier über die 152er Ausgabe), nicht +aus icon.json geschätzt: dort stehen Ausgangsfarben für einen automatischen +Verlauf, und wie Icon Composer den aufspannt, steht nirgends. +""" +import json +import math +from pathlib import Path +from PIL import Image, ImageDraw, ImageFilter + +WURZEL = Path("FlixyTV/Resources/TVAssets.xcassets") +MARKE = WURZEL / "App Icon & Top Shelf Image.brandassets" + +# Gemessen am gerenderten iOS-Icon, Spalte neben dem Dreieck bzw. durch es. +GRUND_OBEN = (160, 112, 255) +GRUND_UNTEN = (120, 69, 255) +ZEICHEN_OBEN = (224, 152, 255) +ZEICHEN_UNTEN = (170, 97, 255) + +# Aus play.svg gerechnet. Der Pfad beschreibt ein gleichseitiges Dreieck mit +# 592,76 Punkten Seite und 54,7 Punkten Eckenradius; sein sichtbarer Kasten ist +# dadurch 0,8385 mal so hoch und 0,8923 mal so breit wie das ideale Dreieck. +ECKENRADIUS_ZU_SEITE = 54.706 / 592.76 +KASTEN_ZU_SEITE_HOCH = 497.03 / 592.76 +# Wie hoch das Zeichen im Verhältnis zur kürzeren Kante steht — am iOS-Icon +# gemessen: 87 von 152. +ZEICHEN_ANTEIL = 87 / 152 +# Und wie weit es aus der Mitte nach rechts rückt. Ein Dreieck wirkt sonst +# linkslastig. +# +# **Bezogen auf das Zeichen, nicht auf die Leinwand.** Am quadratischen +# iOS-Icon war das dasselbe; auf der breiten Fernseherleinwand nicht mehr, und +# als Anteil der Breite gerechnet rutschte das Dreieck sichtbar nach rechts. +VERSATZ_ANTEIL = 6.5 / 79 + +SS = 4 # Überabtastung gegen Treppen + + +def verlauf(größe, oben, unten): + breite, höhe = größe + bild = Image.new("RGB", (breite, höhe)) + zeichner = ImageDraw.Draw(bild) + for y in range(höhe): + t = y / max(höhe - 1, 1) + farbe = tuple(round(oben[k] + (unten[k] - oben[k]) * t) for k in range(3)) + zeichner.line([(0, y), (breite, y)], fill=farbe) + return bild + + +def zeichen_maske(größe): + """Die Silhouette aus play.svg, in der Grösse dieses Bildes.""" + breite, höhe = größe + bild = Image.new("L", (breite * SS, höhe * SS), 0) + zeichner = ImageDraw.Draw(bild) + + kasten_höhe = höhe * ZEICHEN_ANTEIL * SS + seite = kasten_höhe / KASTEN_ZU_SEITE_HOCH + radius = seite * ECKENRADIUS_ZU_SEITE + umkreis = seite / math.sqrt(3) + inkreis = seite / (2 * math.sqrt(3)) + + mitte_x = breite * SS / 2 + kasten_höhe * 0.908 * VERSATZ_ANTEIL + mitte_y = höhe * SS / 2 + + # **Eingerücktes Dreieck plus runder Strich.** Das ist nicht eine + # Annäherung an die runden Ecken, sondern genau sie: ein Strich der Breite + # 2r mit runden Gehrungen um das um r eingerückte Dreieck ergibt dieselbe + # Figur wie die Bögen im Pfad. + einzug = 1 - radius / inkreis + ecken = [] + for winkel in (0, 120, 240): + rad = math.radians(winkel) + ecken.append(( + mitte_x + umkreis * einzug * math.cos(rad), + mitte_y + umkreis * einzug * math.sin(rad), + )) + + zeichner.polygon(ecken, fill=255) + # Zwei Punkte über den Ring hinaus: `joint="curve"` rundet nur **innere** + # Ecken. Endete die Linie beim ersten Punkt, bliebe genau dort eine spitze + # Kerbe stehen — und zwar nur an einer, was wie ein Zufall aussieht. + zeichner.line(ecken + [ecken[0], ecken[1]], fill=255, + width=max(round(radius * 2), 1), joint="curve") + + return bild.resize((breite, höhe), Image.LANCZOS) + + +def hinten(größe): + return verlauf(größe, GRUND_OBEN, GRUND_UNTEN).convert("RGBA") + + +def vorn(größe): + maske = zeichen_maske(größe) + bild = verlauf(größe, ZEICHEN_OBEN, ZEICHEN_UNTEN).convert("RGBA") + bild.putalpha(maske) + + # **Der helle Saum.** Am iOS-Icon zieht sich eine dünne, hellere Kante um + # das Zeichen — sie kommt dort aus der Durchscheinlichkeit der Gruppe. Ohne + # sie wirkt die Fläche flach aufgeklebt statt aufliegend. Nachgebaut als + # Differenz zwischen der Maske und einer leicht geschrumpften: das ist + # genau die Randzone. + saumbreite = max(round(größe[1] * 0.004), 1) + innen = maske.filter(ImageFilter.MinFilter(saumbreite * 2 + 1)) + saum = Image.new("RGBA", größe, (255, 232, 255, 0)) + saum.putalpha(Image.eval( + Image.merge("L", (maske,)).point(lambda v: v), + lambda v: v + ).point(lambda v: v)) + saum.putalpha(Image.composite( + maske.point(lambda v: min(v, 150)), Image.new("L", größe, 0), innen.point(lambda v: 255 - v) + )) + bild.alpha_composite(saum) + + # Der weiche Schatten aus icon.json (neutral, halbe Deckkraft). Er gehört + # auf dieselbe Ebene wie das Zeichen — eine eigene dritte Ebene würde beim + # Neigen gegen das Zeichen verrutschen, und ein Schatten, der sich von + # seinem Gegenstand löst, sieht nach Fehler aus. + weich = max(round(größe[1] * 0.02), 1) + schatten = Image.new("RGBA", größe, (0, 0, 0, 0)) + schatten.putalpha(maske.filter(ImageFilter.GaussianBlur(weich)).point(lambda v: v // 2)) + schatten = Image.alpha_composite( + Image.new("RGBA", größe, (0, 0, 0, 0)), + Image.merge("RGBA", (*Image.new("RGB", größe, (0, 0, 0)).split(), schatten.split()[3])), + ) + versatz = Image.new("RGBA", größe, (0, 0, 0, 0)) + versatz.paste(schatten, (0, weich)) + versatz.alpha_composite(bild) + return versatz + + +def schreibe_imageset(ordner, name, bilder): + ordner.mkdir(parents=True, exist_ok=True) + einträge = [] + for skala, bild in bilder: + datei = name + ("" if skala == 1 else "@%dx" % skala) + ".png" + bild.save(ordner / datei) + einträge.append({"idiom": "tv", "filename": datei, "scale": "%dx" % skala}) + (ordner / "Contents.json").write_text(json.dumps( + {"images": einträge, "info": {"author": "xcode", "version": 1}}, indent=2) + "\n") + + +def schreibe_stapel(ordner, größe, skalen): + ordner.mkdir(parents=True, exist_ok=True) + ebenen = [("Back", hinten), ("Front", vorn)] + for name, macher in ebenen: + ebene = ordner / (name + ".imagestacklayer") + ebene.mkdir(parents=True, exist_ok=True) + (ebene / "Contents.json").write_text(json.dumps( + {"info": {"author": "xcode", "version": 1}}, indent=2) + "\n") + schreibe_imageset(ebene / "Content.imageset", name.lower(), + [(s, macher((größe[0] * s, größe[1] * s))) for s in skalen]) + (ordner / "Contents.json").write_text(json.dumps( + {"layers": [{"filename": n + ".imagestacklayer"} for n, _ in reversed(ebenen)], + "info": {"author": "xcode", "version": 1}}, indent=2) + "\n") + + +schreibe_stapel(MARKE / "App Icon.imagestack", (400, 240), [1, 2]) +schreibe_stapel(MARKE / "App Icon - App Store.imagestack", (1280, 768), [1]) + + +def regal(größe): + """Das Top-Shelf-Bild — ein Bild, kein Stapel.""" + bild = hinten(größe) + bild.alpha_composite(vorn(größe)) + return bild + + +schreibe_imageset(MARKE / "Top Shelf Image.imageset", "topshelf", + [(1, regal((1920, 720))), (2, regal((3840, 1440)))]) +schreibe_imageset(MARKE / "Top Shelf Image Wide.imageset", "topshelfwide", + [(1, regal((2320, 720))), (2, regal((4640, 1440)))]) + +(MARKE / "Contents.json").write_text(json.dumps({ + "assets": [ + {"filename": "App Icon.imagestack", "idiom": "tv", + "role": "primary-app-icon", "size": "400x240"}, + {"filename": "App Icon - App Store.imagestack", "idiom": "tv", + "role": "primary-app-icon", "size": "1280x768"}, + {"filename": "Top Shelf Image.imageset", "idiom": "tv", + "role": "top-shelf-image", "size": "1920x720"}, + {"filename": "Top Shelf Image Wide.imageset", "idiom": "tv", + "role": "top-shelf-image-wide", "size": "2320x720"}, + ], + "info": {"author": "xcode", "version": 1}}, indent=2) + "\n") +(WURZEL / "Contents.json").write_text(json.dumps( + {"info": {"author": "xcode", "version": 1}}, indent=2) + "\n") +print("gebaut")