feat(apple/tvOS): parallax app icon + top shelf images from the brand layers
ci / rust (push) Has been cancelled
Icon Composer doesn't cover tvOS — tvOS app icons are the older parallax format: flat layers in an asset-catalog "App Icon & Top Shelf Image" brand asset. Generated from the same Affinity layer exports the Icon Composer .icon uses, mirroring its composition (violet automatic-gradient background → light circle → dark circle → blob in front), via scripts/render-tvos-icon.swift (checked in for regeneration): - App Icon.imagestack 400×240 @1x/@2x + App Icon - App Store.imagestack 1280×768, four layers each so the focus engine gets real parallax depth. - Top Shelf Image (1920×720) + Wide (2320×720) @1x/@2x as flat composites. - ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image" on the tvOS configs; verified on the Apple TV simulator home screen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"filename": "back@1x.png",
|
||||
"idiom": "tv",
|
||||
"scale": "1x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 171 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"filename": "circle1@1x.png",
|
||||
"idiom": "tv",
|
||||
"scale": "1x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"filename": "circle2@1x.png",
|
||||
"idiom": "tv",
|
||||
"scale": "1x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"layers": [
|
||||
{
|
||||
"filename": "Front.imagestacklayer"
|
||||
},
|
||||
{
|
||||
"filename": "Circle2.imagestacklayer"
|
||||
},
|
||||
{
|
||||
"filename": "Circle1.imagestacklayer"
|
||||
},
|
||||
{
|
||||
"filename": "Back.imagestacklayer"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"filename": "front@1x.png",
|
||||
"idiom": "tv",
|
||||
"scale": "1x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 27 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"filename": "back@1x.png",
|
||||
"idiom": "tv",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"filename": "back@2x.png",
|
||||
"idiom": "tv",
|
||||
"scale": "2x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 79 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"filename": "circle1@1x.png",
|
||||
"idiom": "tv",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"filename": "circle1@2x.png",
|
||||
"idiom": "tv",
|
||||
"scale": "2x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"filename": "circle2@1x.png",
|
||||
"idiom": "tv",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"filename": "circle2@2x.png",
|
||||
"idiom": "tv",
|
||||
"scale": "2x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"layers": [
|
||||
{
|
||||
"filename": "Front.imagestacklayer"
|
||||
},
|
||||
{
|
||||
"filename": "Circle2.imagestacklayer"
|
||||
},
|
||||
{
|
||||
"filename": "Circle1.imagestacklayer"
|
||||
},
|
||||
{
|
||||
"filename": "Back.imagestacklayer"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"filename": "front@1x.png",
|
||||
"idiom": "tv",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"filename": "front@2x.png",
|
||||
"idiom": "tv",
|
||||
"scale": "2x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"filename": "shelf-wide@1x.png",
|
||||
"idiom": "tv",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"filename": "shelf-wide@2x.png",
|
||||
"idiom": "tv",
|
||||
"scale": "2x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 262 KiB |
|
After Width: | Height: | Size: 933 KiB |
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"filename": "shelf@1x.png",
|
||||
"idiom": "tv",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"filename": "shelf@2x.png",
|
||||
"idiom": "tv",
|
||||
"scale": "2x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 788 KiB |
@@ -489,6 +489,7 @@
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = F4H37KF6WC;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Punktfunkempfänger";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
@@ -501,6 +502,7 @@
|
||||
MARKETING_VERSION = 0.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.unom.punktfunk.tvos;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
|
||||
SDKROOT = appletvos;
|
||||
SUPPORTED_PLATFORMS = "appletvos appletvsimulator";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -515,6 +517,7 @@
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = F4H37KF6WC;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Punktfunkempfänger";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
@@ -527,6 +530,7 @@
|
||||
MARKETING_VERSION = 0.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.unom.punktfunk.tvos;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
|
||||
SDKROOT = appletvos;
|
||||
SUPPORTED_PLATFORMS = "appletvos appletvsimulator";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -539,15 +543,6 @@
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
CC000000000000000000000A /* Build configuration list for PBXNativeTarget "Punktfunk-tvOS" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
CC0000000000000000000012 /* Debug */,
|
||||
CC0000000000000000000013 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
AA000000000000000000000A /* Build configuration list for PBXNativeTarget "Punktfunk" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
@@ -575,6 +570,15 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
CC000000000000000000000A /* Build configuration list for PBXNativeTarget "Punktfunk-tvOS" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
CC0000000000000000000012 /* Debug */,
|
||||
CC0000000000000000000013 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// Usage: swift scripts/render-tvos-icon.swift <layer-export-dir>
|
||||
// "clients/apple/App/Assets.xcassets/App Icon & Top Shelf Image.brandassets"
|
||||
// Icon Composer has no tvOS support — tvOS wants flat parallax layers (brand assets),
|
||||
// so this renders them from the same Affinity layer exports the .icon bundle uses.
|
||||
//
|
||||
// Renders the tvOS parallax icon layers from the flat Icon Composer layer exports:
|
||||
// gradient background (the icon.json automatic-gradient violet) + the three art layers
|
||||
// on transparent canvases, at every size the brand asset needs.
|
||||
import AppKit
|
||||
|
||||
let export = CommandLine.arguments[1]
|
||||
let outDir = CommandLine.arguments[2]
|
||||
|
||||
func bitmap(_ size: NSSize, draw: () -> Void) -> Data {
|
||||
let rep = NSBitmapImageRep(
|
||||
bitmapDataPlanes: nil, pixelsWide: Int(size.width), pixelsHigh: Int(size.height),
|
||||
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false,
|
||||
colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0)!
|
||||
rep.size = size
|
||||
NSGraphicsContext.saveGraphicsState()
|
||||
NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep)
|
||||
draw()
|
||||
NSGraphicsContext.restoreGraphicsState()
|
||||
return rep.representation(using: .png, properties: [:])!
|
||||
}
|
||||
|
||||
func write(_ data: Data, _ path: String) {
|
||||
let url = URL(fileURLWithPath: outDir).appendingPathComponent(path)
|
||||
try! FileManager.default.createDirectory(
|
||||
at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||||
try! data.write(to: url)
|
||||
print(path)
|
||||
}
|
||||
|
||||
// icon.json: automatic-gradient display-p3 (0.395, 0.306, 0.963) — approximated as a
|
||||
// vertical sRGB gradient around the brand violet.
|
||||
func gradientPNG(_ size: NSSize) -> Data {
|
||||
bitmap(size) {
|
||||
let top = NSColor(srgbRed: 0.49, green: 0.42, blue: 0.97, alpha: 1)
|
||||
let bottom = NSColor(srgbRed: 0.35, green: 0.26, blue: 0.91, alpha: 1)
|
||||
NSGradient(starting: top, ending: bottom)!
|
||||
.draw(in: NSRect(origin: .zero, size: size), angle: -90)
|
||||
}
|
||||
}
|
||||
|
||||
func layerPNG(_ name: String, _ size: NSSize, heightFraction: CGFloat) -> Data {
|
||||
let image = NSImage(contentsOfFile: "\(export)/\(name)")!
|
||||
return bitmap(size) {
|
||||
let h = size.height * heightFraction
|
||||
let rect = NSRect(x: (size.width - h) / 2, y: (size.height - h) / 2, width: h, height: h)
|
||||
image.draw(in: rect, from: .zero, operation: .sourceOver, fraction: 1)
|
||||
}
|
||||
}
|
||||
|
||||
let l1 = "punktfunk_Minimal_Icon-Composer_Layer-1.png" // light circle (back-most art)
|
||||
let l2 = "punktfunk_Minimal_Icon-Composer_Layer-2.png" // dark circle
|
||||
let l3 = "punktfunk_Minimal_Icon-Composer_Layer-3.png" // blob (front)
|
||||
|
||||
// App icon stacks: art at 92% of canvas height (tvOS crops edges in the focus effect).
|
||||
for (stack, sizes) in [
|
||||
("App Icon.imagestack", [("@1x", NSSize(width: 400, height: 240)),
|
||||
("@2x", NSSize(width: 800, height: 480))]),
|
||||
("App Icon - App Store.imagestack", [("@1x", NSSize(width: 1280, height: 768))]),
|
||||
] {
|
||||
for (suffix, size) in sizes {
|
||||
write(gradientPNG(size), "\(stack)/Back.imagestacklayer/Content.imageset/back\(suffix).png")
|
||||
write(layerPNG(l1, size, heightFraction: 0.92), "\(stack)/Circle1.imagestacklayer/Content.imageset/circle1\(suffix).png")
|
||||
write(layerPNG(l2, size, heightFraction: 0.92), "\(stack)/Circle2.imagestacklayer/Content.imageset/circle2\(suffix).png")
|
||||
write(layerPNG(l3, size, heightFraction: 0.92), "\(stack)/Front.imagestacklayer/Content.imageset/front\(suffix).png")
|
||||
}
|
||||
}
|
||||
|
||||
// Top shelf images: flat composite (no parallax stack), mark at 70% height.
|
||||
func shelfPNG(_ size: NSSize) -> Data {
|
||||
let images = [l1, l2, l3].map { NSImage(contentsOfFile: "\(export)/\($0)")! }
|
||||
return bitmap(size) {
|
||||
let top = NSColor(srgbRed: 0.49, green: 0.42, blue: 0.97, alpha: 1)
|
||||
let bottom = NSColor(srgbRed: 0.35, green: 0.26, blue: 0.91, alpha: 1)
|
||||
NSGradient(starting: top, ending: bottom)!
|
||||
.draw(in: NSRect(origin: .zero, size: size), angle: -90)
|
||||
let h = size.height * 0.7
|
||||
let rect = NSRect(x: (size.width - h) / 2, y: (size.height - h) / 2, width: h, height: h)
|
||||
for image in images {
|
||||
image.draw(in: rect, from: .zero, operation: .sourceOver, fraction: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
write(shelfPNG(NSSize(width: 1920, height: 720)), "Top Shelf Image.imageset/shelf@1x.png")
|
||||
write(shelfPNG(NSSize(width: 3840, height: 1440)), "Top Shelf Image.imageset/shelf@2x.png")
|
||||
write(shelfPNG(NSSize(width: 2320, height: 720)), "Top Shelf Image Wide.imageset/shelf-wide@1x.png")
|
||||
write(shelfPNG(NSSize(width: 4640, height: 1440)), "Top Shelf Image Wide.imageset/shelf-wide@2x.png")
|
||||