import Foundation import AppKit // Zeichnet das App-Symbol von Mawari (回り, „reihum“): eine Diensttafel aus Hinoki-Holz // mit drei hängenden Namensschildern. Das mittlere ist auf die rote Seite // gedreht – „ist dran“. Grund in Tokiwa-Grün. // // Aufruf: swift Tools/make-icon.swift (schreibt App/Resources/AppIcon.icns // und design/icon-1024.png) func rgb(_ hex: UInt32, _ a: CGFloat = 1) -> CGColor { CGColor(red: CGFloat((hex >> 16) & 0xFF) / 255, green: CGFloat((hex >> 8) & 0xFF) / 255, blue: CGFloat(hex & 0xFF) / 255, alpha: a) } func drawIcon(size: CGFloat) -> CGImage { let cs = CGColorSpaceCreateDeviceRGB() let ctx = CGContext(data: nil, width: Int(size), height: Int(size), bitsPerComponent: 8, bytesPerRow: 0, space: cs, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)! let s = size / 1024 let inset = 100 * s let rect = CGRect(x: inset, y: inset, width: size - inset * 2, height: size - inset * 2) let radius = rect.width * 0.2237 // Schatten unter der Kachel ctx.saveGState() ctx.setShadow(offset: CGSize(width: 0, height: -10 * s), blur: 24 * s, color: rgb(0x000000, 0.28)) ctx.addPath(CGPath(roundedRect: rect, cornerWidth: radius, cornerHeight: radius, transform: nil)) ctx.setFillColor(rgb(0x2F6B4F)); ctx.fillPath() ctx.restoreGState() ctx.saveGState() ctx.addPath(CGPath(roundedRect: rect, cornerWidth: radius, cornerHeight: radius, transform: nil)) ctx.clip() let bg = CGGradient(colorsSpace: cs, colors: [rgb(0x1E4A36), rgb(0x2F6B4F), rgb(0x3E8462)] as CFArray, locations: [0, 0.55, 1])! ctx.drawLinearGradient(bg, start: CGPoint(x: 0, y: rect.minY), end: CGPoint(x: 0, y: rect.maxY), options: []) // Holztafel let board = CGRect(x: rect.minX + 118 * s, y: rect.minY + 170 * s, width: rect.width - 236 * s, height: rect.height - 330 * s) ctx.saveGState() ctx.setShadow(offset: CGSize(width: 0, height: -12 * s), blur: 20 * s, color: rgb(0x0B1F16, 0.45)) ctx.addPath(CGPath(roundedRect: board, cornerWidth: 34 * s, cornerHeight: 34 * s, transform: nil)) ctx.setFillColor(rgb(0xE6CC9C)); ctx.fillPath() ctx.restoreGState() // Maserung, ganz zart ctx.saveGState() ctx.addPath(CGPath(roundedRect: board, cornerWidth: 34 * s, cornerHeight: 34 * s, transform: nil)) ctx.clip() ctx.setStrokeColor(rgb(0xC9A86A, 0.35)); ctx.setLineWidth(5 * s) for i in 0..<7 { let y = board.minY + CGFloat(i) * board.height / 6.5 + 18 * s ctx.move(to: CGPoint(x: board.minX, y: y)) ctx.addCurve(to: CGPoint(x: board.maxX, y: y + 10 * s), control1: CGPoint(x: board.minX + board.width * 0.35, y: y + 22 * s), control2: CGPoint(x: board.minX + board.width * 0.65, y: y - 18 * s)) } ctx.strokePath() ctx.restoreGState() // Leiste oben mit drei Haken let rail = CGRect(x: board.minX + 40 * s, y: board.maxY - 96 * s, width: board.width - 80 * s, height: 22 * s) ctx.addPath(CGPath(roundedRect: rail, cornerWidth: 11 * s, cornerHeight: 11 * s, transform: nil)) ctx.setFillColor(rgb(0xB89260)); ctx.fillPath() // Namensschilder: hoch und schmal, wie in Japan üblich let plateW = 128 * s, plateH = 330 * s, gap = 40 * s let total = plateW * 3 + gap * 2 let x0 = board.midX - total / 2 let top = rail.minY - 20 * s for i in 0..<3 { let x = x0 + CGFloat(i) * (plateW + gap) let plate = CGRect(x: x, y: top - plateH, width: plateW, height: plateH) let onDuty = i == 1 // Haken ctx.setFillColor(rgb(0x6B5135)) ctx.fillEllipse(in: CGRect(x: plate.midX - 13 * s, y: rail.midY - 13 * s, width: 26 * s, height: 26 * s)) ctx.setStrokeColor(rgb(0x6B5135)); ctx.setLineWidth(6 * s) ctx.move(to: CGPoint(x: plate.midX, y: rail.midY)); ctx.addLine(to: CGPoint(x: plate.midX, y: plate.maxY - 10 * s)); ctx.strokePath() // Schild ctx.saveGState() ctx.setShadow(offset: CGSize(width: 0, height: -6 * s), blur: 10 * s, color: rgb(0x3D2A14, 0.35)) ctx.addPath(CGPath(roundedRect: plate, cornerWidth: 18 * s, cornerHeight: 18 * s, transform: nil)) ctx.setFillColor(onDuty ? rgb(0xB5452B) : rgb(0xFBF6EC)); ctx.fillPath() ctx.restoreGState() // "Name": drei kurze Zeilen wie Schrift (bewusst ohne Zeichen, die als Symbol gelesen werden könnten) ctx.setStrokeColor(onDuty ? rgb(0xFBF6EC) : rgb(0x2B2A27, 0.85)); ctx.setLineCap(.round); ctx.setLineWidth(16 * s) let lines: [(CGFloat, CGFloat)] = [(0.30, 0.62), (0.45, 0.44), (0.60, 0.54)] for (y, w) in lines { let yy = plate.maxY - plate.height * y ctx.move(to: CGPoint(x: plate.midX - plate.width * w / 2, y: yy)) ctx.addLine(to: CGPoint(x: plate.midX + plate.width * w / 2, y: yy)) } ctx.strokePath() } ctx.restoreGState() return ctx.makeImage()! } func png(_ img: CGImage, to url: URL) { let rep = NSBitmapImageRep(cgImage: img) try! rep.representation(using: .png, properties: [:])!.write(to: url) } let root = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) let iconset = root.appendingPathComponent("build/AppIcon.iconset") try? FileManager.default.removeItem(at: iconset) try! FileManager.default.createDirectory(at: iconset, withIntermediateDirectories: true) for base in [16, 32, 128, 256, 512] { png(drawIcon(size: CGFloat(base)), to: iconset.appendingPathComponent("icon_\(base)x\(base).png")) png(drawIcon(size: CGFloat(base * 2)), to: iconset.appendingPathComponent("icon_\(base)x\(base)@2x.png")) } try? FileManager.default.createDirectory(at: root.appendingPathComponent("design"), withIntermediateDirectories: true) png(drawIcon(size: 1024), to: root.appendingPathComponent("design/icon-1024.png")) let p = Process() p.executableURL = URL(fileURLWithPath: "/usr/bin/iconutil") p.arguments = ["-c", "icns", iconset.path, "-o", root.appendingPathComponent("App/Resources/AppIcon.icns").path] try! p.run(); p.waitUntilExit() print(p.terminationStatus == 0 ? "AppIcon.icns geschrieben" : "iconutil fehlgeschlagen")