import AppKit import Foundation let canvasSize = CGSize(width: 1024, height: 1024) enum Palette { static let accent = NSColor(hex: 0x6366F1) static let accentLight = NSColor(hex: 0x818CF8) static let boarding = NSColor(hex: 0x3B82F6) static let mint = NSColor(hex: 0x10B981) static let slate = NSColor(hex: 0x0F172A) static let sky = NSColor(hex: 0x38BDF8) static let cloud = NSColor(hex: 0xE2E8F0) static let mist = NSColor(hex: 0xF8FAFC) static let ink = NSColor(hex: 0x111827) } struct IconOption { let fileName: String let label: String let draw: (CGRect) -> Void } extension NSColor { convenience init(hex: UInt32, alpha: CGFloat = 1.0) { self.init( calibratedRed: CGFloat((hex >> 16) & 0xFF) / 255.0, green: CGFloat((hex >> 8) & 0xFF) / 255.0, blue: CGFloat(hex & 0xFF) / 255.0, alpha: alpha ) } } extension CGRect { var center: CGPoint { CGPoint(x: midX, y: midY) } } func roundedRectPath(_ rect: CGRect, radius: CGFloat) -> NSBezierPath { NSBezierPath(roundedRect: rect, xRadius: radius, yRadius: radius) } func circleRect(center: CGPoint, radius: CGFloat) -> CGRect { CGRect(x: center.x - radius, y: center.y - radius, width: radius * 2, height: radius * 2) } func withGraphicsState(_ draw: () -> Void) { NSGraphicsContext.saveGraphicsState() draw() NSGraphicsContext.restoreGraphicsState() } func applyShadow( color: NSColor, blur: CGFloat, x: CGFloat = 0, y: CGFloat = 0, draw: () -> Void ) { withGraphicsState { let shadow = NSShadow() shadow.shadowColor = color shadow.shadowBlurRadius = blur shadow.shadowOffset = NSSize(width: x, height: y) shadow.set() draw() } } func fillPath(_ path: NSBezierPath, colors: [NSColor], angle: CGFloat) { guard let gradient = NSGradient(colors: colors) else { return } gradient.draw(in: path, angle: angle) } func strokePath(_ path: NSBezierPath, color: NSColor, lineWidth: CGFloat) { color.setStroke() path.lineWidth = lineWidth path.stroke() } func fillCircle(center: CGPoint, radius: CGFloat, color: NSColor) { let path = NSBezierPath(ovalIn: circleRect(center: center, radius: radius)) color.setFill() path.fill() } func fillRoundedRect(_ rect: CGRect, radius: CGFloat, colors: [NSColor], angle: CGFloat) { let path = roundedRectPath(rect, radius: radius) fillPath(path, colors: colors, angle: angle) } func drawSymbol( _ name: String, in rect: CGRect, pointSize: CGFloat, tint: NSColor, weight: NSFont.Weight = .regular, scale: NSImage.SymbolScale = .large, rotationDegrees: CGFloat = 0, alpha: CGFloat = 1.0 ) { guard let symbol = NSImage(systemSymbolName: name, accessibilityDescription: nil), let configured = symbol.withSymbolConfiguration(.init(pointSize: pointSize, weight: weight, scale: scale)) else { return } let tinted = NSImage(size: configured.size) tinted.lockFocus() let imageRect = NSRect(origin: .zero, size: configured.size) configured.draw(in: imageRect, from: NSRect.zero, operation: NSCompositingOperation.sourceOver, fraction: 1.0) tint.withAlphaComponent(alpha).set() imageRect.fill(using: NSCompositingOperation.sourceAtop) tinted.unlockFocus() withGraphicsState { let transform = NSAffineTransform() transform.translateX(by: rect.midX, yBy: rect.midY) transform.rotate(byDegrees: rotationDegrees) transform.translateX(by: -rect.midX, yBy: -rect.midY) transform.concat() tinted.draw(in: rect, from: NSRect.zero, operation: NSCompositingOperation.sourceOver, fraction: 1.0) } } func quadraticPath(start: CGPoint, control: CGPoint, end: CGPoint) -> NSBezierPath { let cp1 = CGPoint( x: start.x + ((control.x - start.x) * 2.0 / 3.0), y: start.y + ((control.y - start.y) * 2.0 / 3.0) ) let cp2 = CGPoint( x: end.x + ((control.x - end.x) * 2.0 / 3.0), y: end.y + ((control.y - end.y) * 2.0 / 3.0) ) let path = NSBezierPath() path.move(to: start) path.curve(to: end, controlPoint1: cp1, controlPoint2: cp2) return path } func quadraticPoint(start: CGPoint, control: CGPoint, end: CGPoint, t: CGFloat) -> CGPoint { let oneMinusT = 1 - t let x = (oneMinusT * oneMinusT * start.x) + (2 * oneMinusT * t * control.x) + (t * t * end.x) let y = (oneMinusT * oneMinusT * start.y) + (2 * oneMinusT * t * control.y) + (t * t * end.y) return CGPoint(x: x, y: y) } func quadraticAngle(start: CGPoint, control: CGPoint, end: CGPoint, t: CGFloat) -> CGFloat { let dx = (2 * (1 - t) * (control.x - start.x)) + (2 * t * (end.x - control.x)) let dy = (2 * (1 - t) * (control.y - start.y)) + (2 * t * (end.y - control.y)) return atan2(dy, dx) * 180 / .pi } func drawRouteArc( start: CGPoint, control: CGPoint, end: CGPoint, color: NSColor, lineWidth: CGFloat, dash: [CGFloat] = [22, 18] ) { let path = quadraticPath(start: start, control: control, end: end) color.setStroke() path.lineWidth = lineWidth path.lineCapStyle = .round path.setLineDash(dash, count: dash.count, phase: 0) path.stroke() } func drawRing(center: CGPoint, radius: CGFloat, width: CGFloat, color: NSColor, alpha: CGFloat = 1.0) { let path = NSBezierPath(ovalIn: circleRect(center: center, radius: radius)) strokePath(path, color: color.withAlphaComponent(alpha), lineWidth: width) } func drawGlobe( center: CGPoint, radius: CGFloat, fillColors: [NSColor], landTint: NSColor, gridColor: NSColor, highlightColor: NSColor, landAlpha: CGFloat = 0.35, strokeColor: NSColor? = nil, glowColor: NSColor? = nil ) { let globeRect = circleRect(center: center, radius: radius) let globePath = NSBezierPath(ovalIn: globeRect) if let glowColor { applyShadow(color: glowColor, blur: 48) { fillCircle(center: center, radius: radius * 0.96, color: glowColor.withAlphaComponent(0.35)) } } fillPath(globePath, colors: fillColors, angle: 90) withGraphicsState { globePath.addClip() let continentsRect = globeRect.insetBy(dx: radius * 0.12, dy: radius * 0.12) drawSymbol( "globe.americas.fill", in: continentsRect.offsetBy(dx: radius * 0.03, dy: radius * -0.02), pointSize: radius * 1.3, tint: landTint, weight: .regular, rotationDegrees: 0, alpha: landAlpha ) let meridians: [CGFloat] = [0.42, 0.72] for scale in meridians { let ellipse = NSBezierPath( ovalIn: CGRect( x: center.x - radius * scale, y: center.y - radius, width: radius * scale * 2, height: radius * 2 ) ) strokePath(ellipse, color: gridColor.withAlphaComponent(0.9), lineWidth: 5) } let parallels: [CGFloat] = [0.30, 0.62, 0.86] for scale in parallels { let ellipse = NSBezierPath( ovalIn: CGRect( x: center.x - radius, y: center.y - radius * scale, width: radius * 2, height: radius * scale * 2 ) ) strokePath(ellipse, color: gridColor.withAlphaComponent(0.68), lineWidth: 4) } let specular = NSBezierPath( ovalIn: CGRect( x: center.x - radius * 0.62, y: center.y + radius * 0.16, width: radius * 0.70, height: radius * 0.36 ) ) highlightColor.withAlphaComponent(0.18).setFill() specular.fill() } if let strokeColor { strokePath(globePath, color: strokeColor, lineWidth: 6) } } func drawTopographicLines(rect: CGRect, color: NSColor, count: Int, inset: CGFloat) { for index in 0.. Void) -> NSBitmapImageRep { 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 let graphicsContext = NSGraphicsContext(bitmapImageRep: rep)! NSGraphicsContext.saveGraphicsState() NSGraphicsContext.current = graphicsContext draw(CGRect(origin: .zero, size: size)) graphicsContext.flushGraphics() NSGraphicsContext.restoreGraphicsState() return rep } func savePNG(_ rep: NSBitmapImageRep, to url: URL) throws { guard let data = rep.representation(using: .png, properties: [:]) else { throw NSError(domain: "IconGenerator", code: 1) } try data.write(to: url) } func drawComparisonSheet(options: [IconOption], imageDirectory: URL, outputURL: URL) throws { let sheetSize = CGSize(width: 2190, height: 1620) let rep = renderBitmap(size: sheetSize) { rect in fillRoundedRect( rect, radius: 120, colors: [NSColor(hex: 0xF8FAFC), NSColor(hex: 0xEEF2FF)], angle: 90 ) let titleAttributes: [NSAttributedString.Key: Any] = [ .font: NSFont.systemFont(ofSize: 82, weight: .bold), .foregroundColor: Palette.slate ] let subtitleAttributes: [NSAttributedString.Key: Any] = [ .font: NSFont.systemFont(ofSize: 34, weight: .medium), .foregroundColor: Palette.ink.withAlphaComponent(0.70) ] NSAttributedString(string: "Flights App Icon Concepts", attributes: titleAttributes) .draw(at: CGPoint(x: 112, y: 1470)) NSAttributedString( string: "1 to 5. Same palette, different attitude.", attributes: subtitleAttributes ) .draw(at: CGPoint(x: 118, y: 1412)) let cardWidth: CGFloat = 354 let cardHeight: CGFloat = 474 let horizontalGap: CGFloat = 52 let verticalGap: CGFloat = 50 let startX: CGFloat = 112 let startY: CGFloat = 856 for (index, option) in options.enumerated() { let row = index / 3 let column = index % 3 let x = startX + CGFloat(column) * (cardWidth + horizontalGap) let y = startY - CGFloat(row) * (cardHeight + verticalGap) let cardRect = CGRect(x: x, y: y, width: cardWidth, height: cardHeight) let cardPath = roundedRectPath(cardRect, radius: 52) applyShadow(color: NSColor.black.withAlphaComponent(0.10), blur: 20, y: -8) { fillPath(cardPath, colors: [NSColor.white, NSColor(hex: 0xF8FAFC)], angle: 90) } let imageURL = imageDirectory.appendingPathComponent(option.fileName) if let image = NSImage(contentsOf: imageURL) { image.draw(in: CGRect(x: x + 27, y: y + 116, width: 300, height: 300)) } let labelAttributes: [NSAttributedString.Key: Any] = [ .font: NSFont.monospacedSystemFont(ofSize: 30, weight: .bold), .foregroundColor: Palette.accent ] let titleAttributes: [NSAttributedString.Key: Any] = [ .font: NSFont.systemFont(ofSize: 34, weight: .semibold), .foregroundColor: Palette.slate ] NSAttributedString(string: "\(index + 1).", attributes: labelAttributes) .draw(at: CGPoint(x: x + 28, y: y + 58)) NSAttributedString(string: option.label, attributes: titleAttributes) .draw(at: CGPoint(x: x + 82, y: y + 54)) } } try savePNG(rep, to: outputURL) } let options: [IconOption] = [ .init(fileName: "01_orbital-route.png", label: "Orbital Route", draw: drawOrbitalRoute), .init(fileName: "02_window-badge.png", label: "Window Badge", draw: drawWindowBadge), .init(fileName: "03_atlas-tile.png", label: "Atlas Tile", draw: drawAtlasTile), .init(fileName: "04_night-ring.png", label: "Night Ring", draw: drawNightRing), .init(fileName: "05_route-card.png", label: "Route Card", draw: drawRouteCard) ] let fileManager = FileManager.default let root = URL(fileURLWithPath: fileManager.currentDirectoryPath) let outputDirectory = root.appendingPathComponent("design/icon-options", isDirectory: true) try fileManager.createDirectory(at: outputDirectory, withIntermediateDirectories: true) for option in options { let rep = renderBitmap(draw: option.draw) try savePNG(rep, to: outputDirectory.appendingPathComponent(option.fileName)) } try drawComparisonSheet( options: options, imageDirectory: outputDirectory, outputURL: outputDirectory.appendingPathComponent("comparison-sheet.png") ) print("Generated \(options.count) icons in \(outputDirectory.path)")