// Renders the 1024x1024 MultiSimAdmin app icon with CoreGraphics only. // Usage: swift Design/generate_app_icon.swift // // The mark is the same geometry as the web favicon: a gold SIM plate carrying // three ascending signal bars. One flat silhouette, one light source, and no // photographic detail, so it still reads at the 29pt settings size. The plate // keeps the classic SIM chamfer, moved to the top-left so the tallest bar can // sit under the intact top-right corner. // // Every coordinate is in visual space (origin top-left, y grows down). import AppKit import CoreGraphics import Foundation let side: CGFloat = 1024 let output = CommandLine.arguments.count > 1 ? CommandLine.arguments[1] : "Design/AppIcon.png" func envDouble(_ name: String, _ fallback: Double) -> CGFloat { guard let raw = ProcessInfo.processInfo.environment[name], let value = Double(raw) else { return CGFloat(fallback) } return CGFloat(value) } func rgb(_ r: Int, _ g: Int, _ b: Int, _ a: Int = 255) -> CGColor { CGColor(srgbRed: CGFloat(r) / 255, green: CGFloat(g) / 255, blue: CGFloat(b) / 255, alpha: CGFloat(a) / 255) } guard let space = CGColorSpace(name: CGColorSpace.sRGB), let ctx = CGContext( data: nil, width: Int(side), height: Int(side), bitsPerComponent: 8, bytesPerRow: 0, space: space, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { FileHandle.standardError.write("cannot create bitmap context\n".data(using: .utf8)!) exit(1) } ctx.translateBy(x: 0, y: side) ctx.scaleBy(x: 1, y: -1) typealias Stop = (location: CGFloat, color: (Int, Int, Int, Int)) func makeGradient(_ stops: [Stop]) -> CGGradient? { var components: [CGFloat] = [] for stop in stops { components.append(CGFloat(stop.color.0) / 255) components.append(CGFloat(stop.color.1) / 255) components.append(CGFloat(stop.color.2) / 255) components.append(CGFloat(stop.color.3) / 255) } let locations = stops.map(\.location) return CGGradient( colorSpace: space, colorComponents: components, locations: locations, count: stops.count ) } func fillLinear(_ path: CGPath, _ stops: [Stop], _ start: CGPoint, _ end: CGPoint) { guard let gradient = makeGradient(stops) else { return } ctx.saveGState() ctx.addPath(path) ctx.clip() ctx.drawLinearGradient( gradient, start: start, end: end, options: [.drawsBeforeStartLocation, .drawsAfterEndLocation] ) ctx.restoreGState() } func fillRadial(_ center: CGPoint, _ radius: CGFloat, _ stops: [Stop]) { guard let gradient = makeGradient(stops) else { return } ctx.saveGState() ctx.drawRadialGradient( gradient, startCenter: center, startRadius: 0, endCenter: center, endRadius: radius, options: [.drawsAfterEndLocation] ) ctx.restoreGState() } func rectPath(_ rect: CGRect) -> CGPath { CGPath(rect: rect, transform: nil) } func roundedPath(_ rect: CGRect, _ radius: CGFloat) -> CGPath { CGPath(roundedRect: rect, cornerWidth: radius, cornerHeight: radius, transform: nil) } /// SIM plate outline: rounded square with one cut top corner. func platePath(_ rect: CGRect, corner: CGFloat, chamfer: CGFloat) -> CGPath { let bevel = corner * 0.28 let vertices: [(point: CGPoint, radius: CGFloat)] = [ (CGPoint(x: rect.minX + chamfer, y: rect.minY), bevel), (CGPoint(x: rect.maxX, y: rect.minY), corner), (CGPoint(x: rect.maxX, y: rect.maxY), corner), (CGPoint(x: rect.minX, y: rect.maxY), corner), (CGPoint(x: rect.minX, y: rect.minY + chamfer), bevel), ] let path = CGMutablePath() path.move(to: vertices[0].point) for index in vertices.indices { path.addArc( tangent1End: vertices[index].point, tangent2End: vertices[(index + 1) % vertices.count].point, radius: vertices[index].radius ) } path.closeSubpath() return path } /// Bright along the upper-left edges, dead by the lower-right: an even outline /// reads like a sticker cut rather than a plate lit from above. func directionalRim(_ path: CGPath, _ rect: CGRect, _ width: CGFloat, _ stops: [Stop]) { ctx.saveGState() ctx.addPath(path) ctx.clip() ctx.addPath(path) ctx.setLineWidth(width) ctx.replacePathWithStrokedPath() ctx.clip() fillLinear(rectPath(rect.insetBy(dx: -8, dy: -8)), stops, CGPoint(x: rect.minX, y: rect.minY), CGPoint(x: rect.maxX, y: rect.maxY)) ctx.restoreGState() } // MARK: - Field fillLinear( rectPath(CGRect(x: 0, y: 0, width: side, height: side)), [ (0.00, (28, 66, 128, 255)), (0.46, (13, 36, 80, 255)), (1.00, (4, 11, 27, 255)), ] as [Stop], CGPoint(x: side * 0.10, y: 0), CGPoint(x: side * 0.92, y: side) ) fillRadial( CGPoint(x: side * 0.26, y: -side * 0.04), CGFloat(side * 0.92), [(0.0, (150, 196, 255, 64)), (0.55, (112, 162, 230, 20)), (1.0, (112, 162, 230, 0))] as [Stop] ) fillRadial( CGPoint(x: side * 0.5, y: side * 0.52), CGFloat(side * 0.78), [(0.55, (2, 7, 18, 0)), (1.0, (2, 7, 18, 150))] as [Stop] ) // MARK: - Plate let plateSide = envDouble("ICON_PLATE_SIDE", 620) let plateCenterY = envDouble("ICON_PLATE_Y", 528) let plateCorner = envDouble("ICON_PLATE_CORNER", 134) let plateChamfer = envDouble("ICON_PLATE_CHAMFER", 202) let plateRect = CGRect( x: (side - plateSide) / 2, y: plateCenterY - plateSide / 2, width: plateSide, height: plateSide ) let plate = platePath(plateRect, corner: plateCorner, chamfer: plateChamfer) ctx.saveGState() ctx.setShadow(offset: CGSize(width: 0, height: -30), blur: 62, color: rgb(2, 8, 20, 168)) ctx.addPath(plate) ctx.setFillColor(rgb(0, 0, 0)) ctx.fillPath() ctx.restoreGState() fillLinear( plate, [ (0.00, (255, 244, 205, 255)), (0.30, (250, 206, 106, 255)), (0.62, (214, 150, 40, 255)), (1.00, (146, 92, 16, 255)), ] as [Stop], CGPoint(x: plateRect.minX, y: plateRect.minY), CGPoint(x: plateRect.maxX, y: plateRect.maxY) ) directionalRim( plate, plateRect, plateSide * 0.016, [ (0.00, (255, 251, 232, 230)), (0.40, (255, 240, 190, 66)), (0.74, (255, 232, 170, 0)), ] as [Stop] ) ctx.saveGState() ctx.addPath(plate) ctx.clip() ctx.move(to: CGPoint(x: plateRect.minX - 40, y: plateRect.minY + plateSide * 0.52)) ctx.addLine(to: CGPoint(x: plateRect.maxX + 40, y: plateRect.minY + plateSide * 0.10)) ctx.addLine(to: CGPoint(x: plateRect.maxX + 40, y: plateRect.minY + plateSide * 0.22)) ctx.addLine(to: CGPoint(x: plateRect.minX - 40, y: plateRect.minY + plateSide * 0.64)) ctx.closePath() ctx.setFillColor(rgb(255, 252, 236, 5)) ctx.fillPath() ctx.restoreGState() // MARK: - Signal bars let barWidth = envDouble("ICON_BAR_W", 88) let barGap = envDouble("ICON_BAR_GAP", 62) let barBottom = envDouble("ICON_BAR_BOTTOM", 688) let barHeights: [CGFloat] = [142, 242, 342] let barsWidth = CGFloat(barHeights.count) * barWidth + CGFloat(barHeights.count - 1) * barGap let barsStartX = (side - barsWidth) / 2 for (index, height) in barHeights.enumerated() { let rect = CGRect( x: barsStartX + CGFloat(index) * (barWidth + barGap), y: barBottom - height, width: barWidth, height: height ) let radius = barWidth * 0.30 let path = roundedPath(rect, radius) ctx.saveGState() ctx.setShadow(offset: CGSize(width: 0, height: 5), blur: 16, color: rgb(255, 236, 176, 104)) ctx.addPath(path) ctx.setFillColor(rgb(0, 0, 0)) ctx.fillPath() ctx.restoreGState() fillLinear( path, [ (0.00, (5, 13, 30, 255)), (0.58, (9, 23, 50, 255)), (1.00, (17, 38, 74, 255)), ] as [Stop], CGPoint(x: rect.minX, y: rect.minY), CGPoint(x: rect.minX, y: rect.maxY) ) directionalRim( path, rect, radius * 0.34, [ (0.00, (0, 0, 0, 120)), (0.48, (0, 0, 0, 26)), (0.84, (255, 226, 152, 70)), (1.00, (255, 236, 176, 118)), ] as [Stop] ) } // MARK: - Surface sheen ctx.saveGState() ctx.addPath(rectPath(CGRect(x: 0, y: 0, width: side, height: side))) ctx.clip() ctx.move(to: CGPoint(x: -120, y: 430)) ctx.addLine(to: CGPoint(x: 1140, y: 40)) ctx.addLine(to: CGPoint(x: 1140, y: 214)) ctx.addLine(to: CGPoint(x: -120, y: 604)) ctx.closePath() ctx.setFillColor(rgb(255, 255, 255, 4)) ctx.fillPath() ctx.restoreGState() guard let image = ctx.makeImage() else { FileHandle.standardError.write("cannot finalize image\n".data(using: .utf8)!) exit(1) } let rep = NSBitmapImageRep(cgImage: image) guard let data = rep.representation(using: .png, properties: [:]) else { FileHandle.standardError.write("cannot encode png\n".data(using: .utf8)!) exit(1) } try! data.write(to: URL(fileURLWithPath: output)) print(output)