diff --git a/CMakeLists.txt b/CMakeLists.txt index 5254959dd6..a70cf15be2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1505,7 +1505,8 @@ if(TargetBin) endif() if(TVOS) - add_executable(${TargetBin} MACOSX_BUNDLE ${NativeAssets} ${BigFontAssets} ${UIImages} ${SHADER_FILES} ${THEME_FILE} ${DEBUGGER_FILES} ${WEB_UPLOAD_FILES} ${FLASH0_FILES} ${LANG_FILES} ${NativeAppSource}) + set(AssetCatalog "${CMAKE_SOURCE_DIR}/ios/tvOS-assets.xcassets") + add_executable(${TargetBin} MACOSX_BUNDLE ${NativeAssets} ${BigFontAssets} ${UIImages} ${AssetCatalog} ${SHADER_FILES} ${THEME_FILE} ${DEBUGGER_FILES} ${WEB_UPLOAD_FILES} ${FLASH0_FILES} ${LANG_FILES} ${NativeAppSource}) elseif(IOS) set(AssetCatalog "${CMAKE_SOURCE_DIR}/ios/assets.xcassets") add_executable(${TargetBin} MACOSX_BUNDLE ${NativeAssets} ${BigFontAssets} ${UIImages} ${AssetCatalog} ${SHADER_FILES} ${THEME_FILE} ${DEBUGGER_FILES} ${WEB_UPLOAD_FILES} ${FLASH0_FILES} ${LANG_FILES} ${NativeAppSource} "ios/Launch Screen.storyboard") @@ -1658,11 +1659,11 @@ if(IOS AND NOT LIBRETRO) XCODE_ATTRIBUTE_INSTALL_PATH "$(LOCAL_APPS_DIR)" ) if(TVOS) - # The launch storyboard and the asset catalog's icons are iPhone/iPad only. tvOS needs a - # layered App Icon & Top Shelf brand asset, which we don't have yet. + # The launch storyboard and the iOS asset catalog are iPhone/iPad only. tvOS has a layered + # icon and Top Shelf images of its own, made by ios/generate_tvos_icons.py. set_target_properties(${TargetBin} PROPERTIES - RESOURCE "" - XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "" + RESOURCE "ios/tvOS-assets.xcassets" + XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon" XCODE_ATTRIBUTE_IPHONEOS_DEPLOYMENT_TARGET "" XCODE_ATTRIBUTE_TVOS_DEPLOYMENT_TARGET ${DEPLOYMENT_TARGET} XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "3" diff --git a/ios/generate_tvos_icons.py b/ios/generate_tvos_icons.py new file mode 100644 index 0000000000..6b56d2ba21 --- /dev/null +++ b/ios/generate_tvos_icons.py @@ -0,0 +1,130 @@ +# Generates the tvOS app icon and Top Shelf images in tvOS-assets.xcassets from the iOS icon art. +# Run from the ios directory: python3 generate_tvos_icons.py +# +# tvOS icons are layered for the parallax effect when focused: the logo floats over the background. + +import json +import os +import shutil + +from PIL import Image, ImageDraw, ImageFilter, ImageFont + +LOGO = "assets.xcassets/AppIcon.appiconset/dark.png" # The logo on transparency. +FONT = "../assets/Roboto_Condensed-Bold.ttf" +OUTPUT = "tvOS-assets.xcassets" +BRAND = "AppIcon.brandassets" + +# The background of the iOS icon, lightened a bit toward the top. +BG_TOP = (61, 92, 110) +BG_BOTTOM = (45, 69, 83) +TEXT = (226, 240, 246) + +INFO = {"author": "xcode", "version": 1} + + +def write_json(path, data): + with open(os.path.join(path, "Contents.json"), "w", newline="\n") as f: + json.dump(data, f, indent=2) + f.write("\n") + + +def background(w, h): + img = Image.new("RGB", (w, h)) + draw = ImageDraw.Draw(img) + for y in range(h): + t = y / max(1, h - 1) + color = tuple(round(a + (b - a) * t) for a, b in zip(BG_TOP, BG_BOTTOM)) + draw.line([(0, y), (w, y)], fill=color) + return img + + +def logo(size): + src = Image.open(LOGO).convert("RGBA") + src = src.crop(src.getchannel("A").getbbox()) + return src.resize((size, size), Image.LANCZOS) + + +def with_shadow(layer, radius): + # A soft shadow under whatever is opaque in the layer, slightly offset down. + alpha = layer.getchannel("A") + shadow = Image.new("RGBA", layer.size, (0, 0, 0, 0)) + shadow.putalpha(alpha.point(lambda a: a * 0.55)) + shadow = shadow.filter(ImageFilter.GaussianBlur(radius)) + out = Image.new("RGBA", layer.size, (0, 0, 0, 0)) + out.alpha_composite(shadow, (0, int(radius * 0.6))) + out.alpha_composite(layer) + return out + + +def front_layer(w, h): + # tvOS crops the edges as the focused icon moves, so keep the logo well inside. + size = round(h * 0.72) + layer = Image.new("RGBA", (w, h), (0, 0, 0, 0)) + layer.alpha_composite(logo(size), ((w - size) // 2, (h - size) // 2)) + return with_shadow(layer, h / 60) + + +def top_shelf(w, h): + img = background(w, h).convert("RGBA") + size = round(h * 0.6) + font = ImageFont.truetype(FONT, round(h * 0.3)) + text = "PPSSPP" + draw = ImageDraw.Draw(img) + left, top, right, bottom = draw.textbbox((0, 0), text, font=font) + gap = round(h * 0.08) + total = size + gap + (right - left) + x = (w - total) // 2 + fg = Image.new("RGBA", (w, h), (0, 0, 0, 0)) + fg.alpha_composite(logo(size), (x, (h - size) // 2)) + ImageDraw.Draw(fg).text((x + size + gap - left, (h - (bottom - top)) // 2 - top), text, font=font, fill=TEXT) + img.alpha_composite(with_shadow(fg, h / 80)) + return img.convert("RGB") + + +def image_stack(name, w, h, scales): + stack = os.path.join(OUTPUT, BRAND, name + ".imagestack") + layers = [("Front", lambda sw, sh: front_layer(sw, sh)), ("Back", lambda sw, sh: background(sw, sh))] + for layer_name, make in layers: + layer_dir = os.path.join(stack, layer_name + ".imagestacklayer") + content = os.path.join(layer_dir, "Content.imageset") + os.makedirs(content) + images = [] + for scale in scales: + filename = "%s%s.png" % (layer_name.lower(), "" if scale == 1 else "@%dx" % scale) + make(w * scale, h * scale).save(os.path.join(content, filename)) + images.append({"filename": filename, "idiom": "tv", "scale": "%dx" % scale}) + write_json(content, {"images": images, "info": INFO}) + write_json(layer_dir, {"info": INFO}) + write_json(stack, {"info": INFO, "layers": [{"filename": n + ".imagestacklayer"} for n, _ in layers]}) + + +def image_set(name, w, h): + path = os.path.join(OUTPUT, BRAND, name + ".imageset") + os.makedirs(path) + images = [] + for scale in (1, 2): + filename = "%s%s.png" % (name.replace(" ", "_").lower(), "" if scale == 1 else "@2x") + top_shelf(w * scale, h * scale).save(os.path.join(path, filename)) + images.append({"filename": filename, "idiom": "tv", "scale": "%dx" % scale}) + write_json(path, {"images": images, "info": INFO}) + + +shutil.rmtree(OUTPUT, ignore_errors=True) +os.makedirs(os.path.join(OUTPUT, BRAND)) +write_json(OUTPUT, {"info": INFO}) + +image_stack("App Icon", 400, 240, (1, 2)) +image_stack("App Icon - App Store", 1280, 768, (1,)) +image_set("Top Shelf Image", 1920, 720) +image_set("Top Shelf Image Wide", 2320, 720) + +write_json(os.path.join(OUTPUT, BRAND), { + "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": INFO, +}) +print("Wrote", OUTPUT) diff --git a/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000000..97501a3a36 --- /dev/null +++ b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images": [ + { + "filename": "back.png", + "idiom": "tv", + "scale": "1x" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/back.png b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/back.png new file mode 100644 index 0000000000..ac8b5d2c9d Binary files /dev/null and b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/back.png differ diff --git a/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 0000000000..74d6a722cf --- /dev/null +++ b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Contents.json b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Contents.json new file mode 100644 index 0000000000..2e6160f6c9 --- /dev/null +++ b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Contents.json @@ -0,0 +1,14 @@ +{ + "info": { + "author": "xcode", + "version": 1 + }, + "layers": [ + { + "filename": "Front.imagestacklayer" + }, + { + "filename": "Back.imagestacklayer" + } + ] +} diff --git a/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000000..9c493b4c54 --- /dev/null +++ b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images": [ + { + "filename": "front.png", + "idiom": "tv", + "scale": "1x" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/front.png b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/front.png new file mode 100644 index 0000000000..4dd45c51ee Binary files /dev/null and b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/front.png differ diff --git a/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 0000000000..74d6a722cf --- /dev/null +++ b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000000..bca25e8d76 --- /dev/null +++ b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images": [ + { + "filename": "back.png", + "idiom": "tv", + "scale": "1x" + }, + { + "filename": "back@2x.png", + "idiom": "tv", + "scale": "2x" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/back.png b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/back.png new file mode 100644 index 0000000000..66b7d42820 Binary files /dev/null and b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/back.png differ diff --git a/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/back@2x.png b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/back@2x.png new file mode 100644 index 0000000000..02c376f6e5 Binary files /dev/null and b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/back@2x.png differ diff --git a/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 0000000000..74d6a722cf --- /dev/null +++ b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Contents.json b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Contents.json new file mode 100644 index 0000000000..2e6160f6c9 --- /dev/null +++ b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Contents.json @@ -0,0 +1,14 @@ +{ + "info": { + "author": "xcode", + "version": 1 + }, + "layers": [ + { + "filename": "Front.imagestacklayer" + }, + { + "filename": "Back.imagestacklayer" + } + ] +} diff --git a/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000000..a4a0d32caf --- /dev/null +++ b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images": [ + { + "filename": "front.png", + "idiom": "tv", + "scale": "1x" + }, + { + "filename": "front@2x.png", + "idiom": "tv", + "scale": "2x" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/front.png b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/front.png new file mode 100644 index 0000000000..66cf50fb37 Binary files /dev/null and b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/front.png differ diff --git a/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/front@2x.png b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/front@2x.png new file mode 100644 index 0000000000..ca289754c2 Binary files /dev/null and b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/front@2x.png differ diff --git a/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 0000000000..74d6a722cf --- /dev/null +++ b/ios/tvOS-assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/ios/tvOS-assets.xcassets/AppIcon.brandassets/Contents.json b/ios/tvOS-assets.xcassets/AppIcon.brandassets/Contents.json new file mode 100644 index 0000000000..69ed7a52c1 --- /dev/null +++ b/ios/tvOS-assets.xcassets/AppIcon.brandassets/Contents.json @@ -0,0 +1,32 @@ +{ + "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 + } +} diff --git a/ios/tvOS-assets.xcassets/AppIcon.brandassets/Top Shelf Image Wide.imageset/Contents.json b/ios/tvOS-assets.xcassets/AppIcon.brandassets/Top Shelf Image Wide.imageset/Contents.json new file mode 100644 index 0000000000..7869f37f7d --- /dev/null +++ b/ios/tvOS-assets.xcassets/AppIcon.brandassets/Top Shelf Image Wide.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images": [ + { + "filename": "top_shelf_image_wide.png", + "idiom": "tv", + "scale": "1x" + }, + { + "filename": "top_shelf_image_wide@2x.png", + "idiom": "tv", + "scale": "2x" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/ios/tvOS-assets.xcassets/AppIcon.brandassets/Top Shelf Image Wide.imageset/top_shelf_image_wide.png b/ios/tvOS-assets.xcassets/AppIcon.brandassets/Top Shelf Image Wide.imageset/top_shelf_image_wide.png new file mode 100644 index 0000000000..005f9b8ff9 Binary files /dev/null and b/ios/tvOS-assets.xcassets/AppIcon.brandassets/Top Shelf Image Wide.imageset/top_shelf_image_wide.png differ diff --git a/ios/tvOS-assets.xcassets/AppIcon.brandassets/Top Shelf Image Wide.imageset/top_shelf_image_wide@2x.png b/ios/tvOS-assets.xcassets/AppIcon.brandassets/Top Shelf Image Wide.imageset/top_shelf_image_wide@2x.png new file mode 100644 index 0000000000..cd29191e94 Binary files /dev/null and b/ios/tvOS-assets.xcassets/AppIcon.brandassets/Top Shelf Image Wide.imageset/top_shelf_image_wide@2x.png differ diff --git a/ios/tvOS-assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/Contents.json b/ios/tvOS-assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/Contents.json new file mode 100644 index 0000000000..b271d10496 --- /dev/null +++ b/ios/tvOS-assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images": [ + { + "filename": "top_shelf_image.png", + "idiom": "tv", + "scale": "1x" + }, + { + "filename": "top_shelf_image@2x.png", + "idiom": "tv", + "scale": "2x" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/ios/tvOS-assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/top_shelf_image.png b/ios/tvOS-assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/top_shelf_image.png new file mode 100644 index 0000000000..6e37468454 Binary files /dev/null and b/ios/tvOS-assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/top_shelf_image.png differ diff --git a/ios/tvOS-assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/top_shelf_image@2x.png b/ios/tvOS-assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/top_shelf_image@2x.png new file mode 100644 index 0000000000..3dea3ad6f8 Binary files /dev/null and b/ios/tvOS-assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/top_shelf_image@2x.png differ diff --git a/ios/tvOS-assets.xcassets/Contents.json b/ios/tvOS-assets.xcassets/Contents.json new file mode 100644 index 0000000000..74d6a722cf --- /dev/null +++ b/ios/tvOS-assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info": { + "author": "xcode", + "version": 1 + } +}