72 lines
3.1 KiB
Swift
72 lines
3.1 KiB
Swift
|
|
// Renders the CocotteAI app icon (✦ mark on the rose brand background) and writes
|
||
|
|
// a full .iconset directory. Run on macOS: `swift make-icon.swift <out.iconset>`.
|
||
|
|
// The shell packager then `iconutil -c icns`s it. Colors match DesignTokens' rose
|
||
|
|
// accent (r7 #c75872) over the dark canvas (#100c0e).
|
||
|
|
import AppKit
|
||
|
|
|
||
|
|
// Initialize the shared app so AppKit font/graphics services are available in this
|
||
|
|
// CLI context (no run loop needed — we never call run()).
|
||
|
|
_ = NSApplication.shared
|
||
|
|
|
||
|
|
let outDir = CommandLine.arguments.count > 1 ? CommandLine.arguments[1] : "AppIcon.iconset"
|
||
|
|
try? FileManager.default.createDirectory(atPath: outDir, withIntermediateDirectories: true)
|
||
|
|
|
||
|
|
func color(_ hex: UInt32) -> NSColor {
|
||
|
|
NSColor(srgbRed: CGFloat((hex >> 16) & 0xff) / 255,
|
||
|
|
green: CGFloat((hex >> 8) & 0xff) / 255,
|
||
|
|
blue: CGFloat(hex & 0xff) / 255, alpha: 1)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Draw the icon at `size` px (square) into a fresh bitmap context and return PNG
|
||
|
|
/// data. Draws straight into an NSBitmapImageRep context (not NSImage.lockFocus),
|
||
|
|
/// which is the reliable path in a command-line tool.
|
||
|
|
func render(_ size: Int) -> Data {
|
||
|
|
let s = CGFloat(size)
|
||
|
|
guard let rep = NSBitmapImageRep(
|
||
|
|
bitmapDataPlanes: nil, pixelsWide: size, pixelsHigh: size,
|
||
|
|
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false,
|
||
|
|
colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0) else {
|
||
|
|
fatalError("bitmap alloc failed at \(size)")
|
||
|
|
}
|
||
|
|
NSGraphicsContext.saveGraphicsState()
|
||
|
|
defer { NSGraphicsContext.restoreGraphicsState() }
|
||
|
|
NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep)
|
||
|
|
|
||
|
|
// Rounded-rect canvas with a subtle rose vertical gradient (macOS Big Sur look:
|
||
|
|
// full-bleed rounded square, the system masks corners on display).
|
||
|
|
let rect = NSRect(x: 0, y: 0, width: s, height: s)
|
||
|
|
let radius = s * 0.22
|
||
|
|
let path = NSBezierPath(roundedRect: rect, xRadius: radius, yRadius: radius)
|
||
|
|
path.addClip()
|
||
|
|
NSGradient(colors: [color(0x1c1619), color(0x100c0e)])?.draw(in: rect, angle: -90)
|
||
|
|
|
||
|
|
// The ✦ mark, rose accent, centered.
|
||
|
|
let fontSize = s * 0.62
|
||
|
|
let attrs: [NSAttributedString.Key: Any] = [
|
||
|
|
.font: NSFont.systemFont(ofSize: fontSize, weight: .semibold),
|
||
|
|
.foregroundColor: color(0xc75872),
|
||
|
|
]
|
||
|
|
let str = NSAttributedString(string: "✦", attributes: attrs)
|
||
|
|
let bounds = str.size()
|
||
|
|
str.draw(at: NSPoint(x: (s - bounds.width) / 2, y: (s - bounds.height) / 2))
|
||
|
|
|
||
|
|
guard let png = rep.representation(using: .png, properties: [:]) else {
|
||
|
|
fatalError("PNG encode failed at size \(size)")
|
||
|
|
}
|
||
|
|
return png
|
||
|
|
}
|
||
|
|
|
||
|
|
// Standard macOS iconset matrix.
|
||
|
|
let specs: [(name: String, size: Int)] = [
|
||
|
|
("icon_16x16", 16), ("icon_16x16@2x", 32),
|
||
|
|
("icon_32x32", 32), ("icon_32x32@2x", 64),
|
||
|
|
("icon_128x128", 128), ("icon_128x128@2x", 256),
|
||
|
|
("icon_256x256", 256), ("icon_256x256@2x", 512),
|
||
|
|
("icon_512x512", 512), ("icon_512x512@2x", 1024),
|
||
|
|
]
|
||
|
|
for spec in specs {
|
||
|
|
let url = URL(fileURLWithPath: "\(outDir)/\(spec.name).png")
|
||
|
|
try! render(spec.size).write(to: url)
|
||
|
|
}
|
||
|
|
FileHandle.standardError.write("wrote \(specs.count) icons to \(outDir)\n".data(using: .utf8)!)
|