Files

141 lines
4.8 KiB
Swift

#!/usr/bin/env swift
import AppKit
import Foundation
// Renders AppIcon.icns from code — no design tool, no binary asset to keep in sync.
// Run: swift Tools/makeicon.swift
// Edison Voice: an amber lightbulb on a deep slate tile.
let background = NSColor(srgbRed: 0.13, green: 0.14, blue: 0.17, alpha: 1)
let bulbColor = NSColor(srgbRed: 1.00, green: 0.78, blue: 0.28, alpha: 1)
func drawIcon(size: CGFloat) -> NSImage {
let image = NSImage(size: NSSize(width: size, height: size))
image.lockFocus()
guard let ctx = NSGraphicsContext.current?.cgContext else {
image.unlockFocus()
return image
}
ctx.setShouldAntialias(true)
ctx.interpolationQuality = .high
// macOS Big Sur+ icon grid: art occupies the middle ~82%, leaving the shadow gutter.
let inset = size * 0.09
let rect = CGRect(x: inset, y: inset, width: size - inset * 2, height: size - inset * 2)
// Apple's squircle is ~22.37% of the tile's edge.
let radius = rect.width * 0.2237
let squircle = CGPath(roundedRect: rect, cornerWidth: radius, cornerHeight: radius, transform: nil)
// Drop shadow under the tile.
ctx.saveGState()
ctx.setShadow(
offset: CGSize(width: 0, height: -size * 0.012),
blur: size * 0.035,
color: NSColor.black.withAlphaComponent(0.32).cgColor
)
ctx.addPath(squircle)
ctx.setFillColor(background.cgColor)
ctx.fillPath()
ctx.restoreGState()
// Soft top highlight so the tile reads as glass rather than flat fill.
ctx.saveGState()
ctx.addPath(squircle)
ctx.clip()
let highlight = CGGradient(
colorsSpace: CGColorSpaceCreateDeviceRGB(),
colors: [
NSColor.white.withAlphaComponent(0.10).cgColor,
NSColor.white.withAlphaComponent(0.0).cgColor,
] as CFArray,
locations: [0, 1]
)!
ctx.drawLinearGradient(
highlight,
start: CGPoint(x: rect.midX, y: rect.maxY),
end: CGPoint(x: rect.midX, y: rect.midY),
options: []
)
ctx.restoreGState()
// Lightbulb glyph, tinted amber.
guard let base = NSImage(systemSymbolName: "lightbulb.fill", accessibilityDescription: nil) else {
image.unlockFocus()
return image
}
let pointSize = rect.width * 0.52
let config = NSImage.SymbolConfiguration(pointSize: pointSize, weight: .semibold)
.applying(NSImage.SymbolConfiguration(hierarchicalColor: bulbColor))
let bulb = base.withSymbolConfiguration(config) ?? base
// Center the glyph. SF Symbols report their baseline-aware size, so frame it by its own
// intrinsic size rather than assuming a square.
let glyphSize = bulb.size
let drawRect = CGRect(
x: rect.midX - glyphSize.width / 2,
y: rect.midY - glyphSize.height / 2,
width: glyphSize.width,
height: glyphSize.height
)
ctx.saveGState()
ctx.setShadow(
offset: CGSize(width: 0, height: -size * 0.006),
blur: size * 0.02,
color: NSColor.black.withAlphaComponent(0.30).cgColor
)
bulb.draw(in: drawRect)
ctx.restoreGState()
image.unlockFocus()
return image
}
func png(_ image: NSImage, pixels: Int) -> Data? {
guard let rep = NSBitmapImageRep(
bitmapDataPlanes: nil,
pixelsWide: pixels, pixelsHigh: pixels,
bitsPerSample: 8, samplesPerPixel: 4,
hasAlpha: true, isPlanar: false,
colorSpaceName: .deviceRGB,
bytesPerRow: 0, bitsPerPixel: 0
) else { return nil }
rep.size = NSSize(width: pixels, height: pixels)
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep)
// Redraw at native pixel size rather than scaling a single render — keeps the small
// sizes crisp instead of muddy.
drawIcon(size: CGFloat(pixels)).draw(
in: NSRect(x: 0, y: 0, width: pixels, height: pixels),
from: .zero, operation: .sourceOver, fraction: 1
)
NSGraphicsContext.restoreGraphicsState()
return rep.representation(using: .png, properties: [:])
}
let fm = FileManager.default
let root = URL(fileURLWithPath: fm.currentDirectoryPath)
let iconset = root.appendingPathComponent("Resources/AppIcon.iconset")
try? fm.removeItem(at: iconset)
try fm.createDirectory(at: iconset, withIntermediateDirectories: true)
// (point size, scale) pairs iconutil expects.
let variants: [(Int, Int)] = [
(16, 1), (16, 2), (32, 1), (32, 2), (128, 1), (128, 2),
(256, 1), (256, 2), (512, 1), (512, 2),
]
for (points, scale) in variants {
let pixels = points * scale
guard let data = png(NSImage(), pixels: pixels) else {
print("failed at \(pixels)px"); exit(1)
}
let suffix = scale == 2 ? "@2x" : ""
let name = "icon_\(points)x\(points)\(suffix).png"
try data.write(to: iconset.appendingPathComponent(name))
}
print("wrote \(variants.count) PNGs to Resources/AppIcon.iconset")