diff --git a/Sources/TVAnarchy/Assets.xcassets/AppIcon.appiconset/Contents.json b/Sources/TVAnarchy/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..74b3caf --- /dev/null +++ b/Sources/TVAnarchy/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { "idiom" : "mac", "scale" : "1x", "size" : "16x16", "filename" : "icon_16.png" }, + { "idiom" : "mac", "scale" : "2x", "size" : "16x16", "filename" : "icon_32.png" }, + { "idiom" : "mac", "scale" : "1x", "size" : "32x32", "filename" : "icon_32.png" }, + { "idiom" : "mac", "scale" : "2x", "size" : "32x32", "filename" : "icon_64.png" }, + { "idiom" : "mac", "scale" : "1x", "size" : "128x128", "filename" : "icon_128.png" }, + { "idiom" : "mac", "scale" : "2x", "size" : "128x128", "filename" : "icon_256.png" }, + { "idiom" : "mac", "scale" : "1x", "size" : "256x256", "filename" : "icon_256.png" }, + { "idiom" : "mac", "scale" : "2x", "size" : "256x256", "filename" : "icon_512.png" }, + { "idiom" : "mac", "scale" : "1x", "size" : "512x512", "filename" : "icon_512.png" }, + { "idiom" : "mac", "scale" : "2x", "size" : "512x512", "filename" : "icon_1024.png" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/Sources/TVAnarchy/Assets.xcassets/AppIcon.appiconset/icon_1024.png b/Sources/TVAnarchy/Assets.xcassets/AppIcon.appiconset/icon_1024.png new file mode 100644 index 0000000..afaa9be Binary files /dev/null and b/Sources/TVAnarchy/Assets.xcassets/AppIcon.appiconset/icon_1024.png differ diff --git a/Sources/TVAnarchy/Assets.xcassets/AppIcon.appiconset/icon_128.png b/Sources/TVAnarchy/Assets.xcassets/AppIcon.appiconset/icon_128.png new file mode 100644 index 0000000..1aca2d8 Binary files /dev/null and b/Sources/TVAnarchy/Assets.xcassets/AppIcon.appiconset/icon_128.png differ diff --git a/Sources/TVAnarchy/Assets.xcassets/AppIcon.appiconset/icon_16.png b/Sources/TVAnarchy/Assets.xcassets/AppIcon.appiconset/icon_16.png new file mode 100644 index 0000000..c3ee554 Binary files /dev/null and b/Sources/TVAnarchy/Assets.xcassets/AppIcon.appiconset/icon_16.png differ diff --git a/Sources/TVAnarchy/Assets.xcassets/AppIcon.appiconset/icon_256.png b/Sources/TVAnarchy/Assets.xcassets/AppIcon.appiconset/icon_256.png new file mode 100644 index 0000000..952ee11 Binary files /dev/null and b/Sources/TVAnarchy/Assets.xcassets/AppIcon.appiconset/icon_256.png differ diff --git a/Sources/TVAnarchy/Assets.xcassets/AppIcon.appiconset/icon_32.png b/Sources/TVAnarchy/Assets.xcassets/AppIcon.appiconset/icon_32.png new file mode 100644 index 0000000..6222115 Binary files /dev/null and b/Sources/TVAnarchy/Assets.xcassets/AppIcon.appiconset/icon_32.png differ diff --git a/Sources/TVAnarchy/Assets.xcassets/AppIcon.appiconset/icon_512.png b/Sources/TVAnarchy/Assets.xcassets/AppIcon.appiconset/icon_512.png new file mode 100644 index 0000000..2b14c55 Binary files /dev/null and b/Sources/TVAnarchy/Assets.xcassets/AppIcon.appiconset/icon_512.png differ diff --git a/Sources/TVAnarchy/Assets.xcassets/AppIcon.appiconset/icon_64.png b/Sources/TVAnarchy/Assets.xcassets/AppIcon.appiconset/icon_64.png new file mode 100644 index 0000000..6e0acce Binary files /dev/null and b/Sources/TVAnarchy/Assets.xcassets/AppIcon.appiconset/icon_64.png differ diff --git a/Sources/TVAnarchy/Assets.xcassets/Contents.json b/Sources/TVAnarchy/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Sources/TVAnarchy/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/project.yml b/project.yml index 0da0b64..7b41cf0 100644 --- a/project.yml +++ b/project.yml @@ -49,6 +49,7 @@ targets: base: PRODUCT_BUNDLE_IDENTIFIER: local.lilith.TVAnarchy GENERATE_INFOPLIST_FILE: "NO" + ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon # Sources/TVAnarchy/Assets.xcassets # The human version (CFBundleShortVersionString) comes from MARKETING_VERSION # via info.properties above — reliable. The dynamic git SHA / build time live # in a generated Swift constant (Sources/TVAnarchyCore/BuildStamp.swift, diff --git a/tools/make-icon.swift b/tools/make-icon.swift new file mode 100644 index 0000000..299893f --- /dev/null +++ b/tools/make-icon.swift @@ -0,0 +1,179 @@ +#!/usr/bin/env swift +// Renders the TVAnarchy app icon at every size the macOS asset catalog needs, +// plus a standalone 1024 master, using only CoreGraphics (no external deps). +// +// Concept: a sleek black television showing a glowing crimson anarchist +// circle-A. TV (rounded screen, bezel, antennae, faint CRT scanlines) + +// Anarchy (the overshooting circle-A). The circle-A is the hero so the mark +// stays legible at 16px when the finer TV details drop out. +// +// Usage: swift tools/make-icon.swift +// writes icon_.png for each size into . + +import AppKit +import CoreGraphics +import Foundation + +let outDir = CommandLine.arguments.count > 1 ? CommandLine.arguments[1] : "." + +// Unique pixel sizes referenced by the macOS AppIcon set (1x/2x of 16…512). +let sizes = [16, 32, 64, 128, 256, 512, 1024] + +func color(_ r: CGFloat, _ g: CGFloat, _ b: CGFloat, _ a: CGFloat = 1) -> CGColor { + CGColor(red: r/255, green: g/255, blue: b/255, alpha: a) +} + +let crimson = color(255, 38, 71) // hero red +let crimsonDeep = color(190, 18, 45) +let screenTop = color(28, 28, 32) // TV body / screen, dark charcoal +let screenBottom = color(6, 6, 8) +let bezel = color(44, 44, 50) +let scanline = color(120, 200, 255, 0.05) + +func renderIcon(px: Int) -> CGImage { + let s = CGFloat(px) + let cs = CGColorSpaceCreateDeviceRGB() + let ctx = CGContext(data: nil, width: px, height: px, bitsPerComponent: 8, + bytesPerRow: 0, space: cs, + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)! + ctx.setAllowsAntialiasing(true) + ctx.interpolationQuality = .high + + // Detail fades out at tiny sizes so the icon doesn't turn to mud. + let fine = s >= 64 + + // --- rounded-rect TV body (the macOS "squircle" footprint) ----------- + let inset = s * 0.085 + let bodyRect = CGRect(x: inset, y: inset, width: s - inset*2, height: s - inset*2) + let corner = s * 0.225 + let bodyPath = CGPath(roundedRect: bodyRect, cornerWidth: corner, cornerHeight: corner, transform: nil) + + // Drop shadow under the body for depth (skip on the smallest tiles). + if fine { + ctx.saveGState() + ctx.setShadow(offset: CGSize(width: 0, height: -s*0.012), blur: s*0.04, + color: color(0, 0, 0, 0.55)) + ctx.addPath(bodyPath); ctx.setFillColor(screenBottom); ctx.fillPath() + ctx.restoreGState() + } + + // Body gradient (top-lit charcoal → near-black). + ctx.saveGState() + ctx.addPath(bodyPath); ctx.clip() + let grad = CGGradient(colorsSpace: cs, colors: [screenTop, screenBottom] as CFArray, + locations: [0, 1])! + ctx.drawLinearGradient(grad, start: CGPoint(x: 0, y: s), end: CGPoint(x: 0, y: 0), options: []) + + // Faint CRT scanlines across the screen. + if fine { + ctx.setFillColor(scanline) + let step = max(s * 0.018, 2) + var y = bodyRect.minY + while y < bodyRect.maxY { + ctx.fill(CGRect(x: bodyRect.minX, y: y, width: bodyRect.width, height: step*0.45)) + y += step + } + // Soft screen vignette / glow toward center. + let glow = CGGradient(colorsSpace: cs, + colors: [color(255, 38, 71, 0.16), color(255, 38, 71, 0)] as CFArray, + locations: [0, 1])! + ctx.drawRadialGradient(glow, + startCenter: CGPoint(x: s/2, y: s*0.52), startRadius: 0, + endCenter: CGPoint(x: s/2, y: s*0.52), endRadius: s*0.42, options: []) + } + ctx.restoreGState() + + // Inner bezel stroke (screen edge). + if fine { + ctx.addPath(bodyPath) + ctx.setStrokeColor(bezel) + ctx.setLineWidth(s * 0.012) + ctx.strokePath() + } + + // --- antennae (two thin rods rising in a V from behind the body) ----- + if fine { + let apexL = CGPoint(x: s*0.40, y: s*0.985) + let apexR = CGPoint(x: s*0.60, y: s*0.985) + let base = CGPoint(x: s*0.50, y: bodyRect.maxY - s*0.01) + ctx.setStrokeColor(bezel) + ctx.setLineWidth(s * 0.018) + ctx.setLineCap(.round) + ctx.beginPath(); ctx.move(to: base); ctx.addLine(to: apexL); ctx.strokePath() + ctx.beginPath(); ctx.move(to: base); ctx.addLine(to: apexR); ctx.strokePath() + for p in [apexL, apexR] { + ctx.setFillColor(crimson) + ctx.fillEllipse(in: CGRect(x: p.x - s*0.018, y: p.y - s*0.018, width: s*0.036, height: s*0.036)) + } + } + + // --- anarchist circle-A (hero mark) ---------------------------------- + let cx = s * 0.5, cy = s * 0.5 + let R = s * 0.255 // ring radius + let lw = s * 0.062 // stroke weight + + // Glow halo behind the mark. + if fine { + ctx.saveGState() + ctx.setShadow(offset: .zero, blur: s*0.05, color: color(255, 38, 71, 0.9)) + ctx.setStrokeColor(crimson) + ctx.setLineWidth(lw) + ctx.addArc(center: CGPoint(x: cx, y: cy), radius: R, startAngle: 0, endAngle: .pi*2, clockwise: false) + ctx.strokePath() + ctx.restoreGState() + } + + // The ring. + let ringGrad = fine + ctx.setLineWidth(lw) + ctx.setLineCap(.butt) + if ringGrad { + ctx.saveGState() + ctx.addArc(center: CGPoint(x: cx, y: cy), radius: R, startAngle: 0, endAngle: .pi*2, clockwise: false) + ctx.setLineWidth(lw); ctx.replacePathWithStrokedPath(); ctx.clip() + let g = CGGradient(colorsSpace: cs, colors: [crimson, crimsonDeep] as CFArray, locations: [0, 1])! + ctx.drawLinearGradient(g, start: CGPoint(x: 0, y: cy+R), end: CGPoint(x: 0, y: cy-R), options: []) + ctx.restoreGState() + } else { + ctx.setStrokeColor(crimson) + ctx.addArc(center: CGPoint(x: cx, y: cy), radius: R, startAngle: 0, endAngle: .pi*2, clockwise: false) + ctx.strokePath() + } + + // The "A": two legs from an apex, plus the crossbar that overshoots the + // ring on both sides (the defining feature of the anarchy symbol). + let apexY = cy + R * 0.92 + let footY = cy - R * 0.86 + let halfW = R * 0.62 + let apex = CGPoint(x: cx, y: apexY) + let footL = CGPoint(x: cx - halfW, y: footY) + let footR = CGPoint(x: cx + halfW, y: footY) + + ctx.setStrokeColor(crimson) + ctx.setLineWidth(lw) + ctx.setLineCap(.round) + ctx.setLineJoin(.round) + // Legs. + ctx.beginPath(); ctx.move(to: footL); ctx.addLine(to: apex); ctx.addLine(to: footR); ctx.strokePath() + // Overshooting crossbar. + let barY = cy - R * 0.04 + let over = R * (fine ? 0.34 : 0.22) + ctx.beginPath() + ctx.move(to: CGPoint(x: cx - halfW*0.52 - over, y: barY)) + ctx.addLine(to: CGPoint(x: cx + halfW*0.52 + over, y: barY)) + ctx.strokePath() + + return ctx.makeImage()! +} + +for px in sizes { + let img = renderIcon(px: px) + let rep = NSBitmapImageRep(cgImage: img) + rep.size = NSSize(width: px, height: px) + guard let data = rep.representation(using: .png, properties: [:]) else { + FileHandle.standardError.write("failed to encode \(px)\n".data(using: .utf8)!); exit(1) + } + let url = URL(fileURLWithPath: outDir).appendingPathComponent("icon_\(px).png") + try! data.write(to: url) + print("wrote \(url.lastPathComponent)") +}