// Renders the 1024x1024 MultiSimAdmin app icon with CoreGraphics only. // Usage: swift Design/generate_app_icon.swift // // The mark is the web favicon grown up: a flat gold SIM plate with three // ascending slots cut out of it, on a neutral graphite field. Two colours, one // light source from the upper left, no metal texture, so it still reads at the // 29pt settings size. The plate keeps the classic single SIM cut, top-left. // // The field is deliberately neutral (R = G = B): the moment it drifts cool the // whole icon reads as "blue icon" on a lit home screen. The gold is pushed warm // and deep enough that it stays metal rather than mustard. // // 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 the top-left corner cut at 45 degrees. func simPath(_ rect: CGRect, corner: CGFloat, chamfer: CGFloat) -> CGPath { let bevel = corner * 0.26 let vertices: [(point: CGPoint, radius: CGFloat)] = [ (CGPoint(x: rect.minX, y: rect.minY + chamfer), bevel), (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), ] 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, gone 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, (32, 32, 33, 255)), (0.55, (19, 19, 20, 255)), (1.00, (10, 10, 11, 255)), ] as [Stop], CGPoint(x: side * 0.06, y: 0), CGPoint(x: side * 0.94, y: side) ) fillRadial( CGPoint(x: side * 0.22, y: -side * 0.10), CGFloat(side * 0.86), [(0.0, (232, 226, 214, 24)), (0.60, (232, 226, 214, 6)), (1.0, (232, 226, 214, 0))] as [Stop] ) // MARK: - Plate let plateSide = envDouble("ICON_PLATE_SIDE", 592) let plateCenterY = envDouble("ICON_PLATE_Y", 500) let plateCorner = envDouble("ICON_PLATE_CORNER", 122) let plateChamfer = envDouble("ICON_PLATE_CHAMFER", 168) let plateRect = CGRect( x: (side - plateSide) / 2, y: plateCenterY - plateSide / 2, width: plateSide, height: plateSide ) let plate = simPath(plateRect, corner: plateCorner, chamfer: plateChamfer) ctx.saveGState() ctx.setShadow(offset: CGSize(width: 0, height: -22), blur: 46, color: rgb(0, 3, 2, 150)) ctx.addPath(plate) ctx.setFillColor(rgb(0, 0, 0)) ctx.fillPath() ctx.restoreGState() fillLinear( plate, [ (0.00, (250, 221, 152, 255)), (0.46, (238, 191, 100, 255)), (1.00, (199, 141, 47, 255)), ] as [Stop], CGPoint(x: plateRect.minX, y: plateRect.minY), CGPoint(x: plateRect.maxX, y: plateRect.maxY) ) directionalRim( plate, plateRect, plateSide * 0.007, [ (0.00, (255, 246, 224, 140)), (0.36, (255, 240, 206, 32)), (0.68, (255, 236, 196, 0)), ] as [Stop] ) // MARK: - Slots let slotWidth = envDouble("ICON_SLOT_W", 92) let slotGap = envDouble("ICON_SLOT_GAP", 54) let slotBottom = envDouble("ICON_SLOT_BOTTOM", 640) let slotHeights: [CGFloat] = [136, 232, 328] let slotsWidth = CGFloat(slotHeights.count) * slotWidth + CGFloat(slotHeights.count - 1) * slotGap let slotsStartX = (side - slotsWidth) / 2 for (index, height) in slotHeights.enumerated() { let rect = CGRect( x: slotsStartX + CGFloat(index) * (slotWidth + slotGap), y: slotBottom - height, width: slotWidth, height: height ) let radius = slotWidth * 0.30 let path = roundedPath(rect, radius) fillLinear( path, [ (0.00, (16, 16, 17, 255)), (1.00, (32, 32, 33, 255)), ] as [Stop], CGPoint(x: rect.minX, y: rect.minY), CGPoint(x: rect.minX, y: rect.maxY) ) directionalRim( path, rect, radius * 0.30, [ (0.00, (112, 74, 16, 100)), (0.52, (112, 74, 16, 18)), (1.00, (255, 236, 190, 60)), ] as [Stop] ) } 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)