Replaces the placeholder coat silhouette with the app's actual subject: Claude's asterisk above a rack of overcoats, on Claude's own coral. The wardrobe is the app in one picture — several coats to pick between, one currently worn. The menu bar stops scaling the app icon down and draws its own glyph instead. At 18pt the icon's three coats, lapels and pockets collapse into a smudge, and its colours ignore the menu bar entirely. The glyph is a template image, so macOS tints it for light and dark and for the open-menu inversion, and re-tints it itself when the theme changes. That also drove its shape: six rays rather than eight, because at that size eight have under a pixel between arms and merge into a blob. The coloured dots beside it don't get that for free — ProfileColor picks a brightness for the current appearance at the moment it's asked, and the title is only rebuilt when profiles start or stop. Observing effectiveAppearance rebuilds it, so a theme switch doesn't leave dots mixed for the old appearance sitting in the menu bar. Everything is still drawn from vector paths rather than exported from a design tool, so each size can be tuned — small sizes need fatter strokes than a straight downscale gives — and nothing binary but the derived assets is committed. The README gains a hero banner, composed from the generated icon so it can't drift out of step with it. About gains the tagline under the icon, names itself in the description rather than opening with a bare verb, and drops the author line.
84 lines
3.4 KiB
Swift
84 lines
3.4 KiB
Swift
// Generates docs/hero.png — the banner at the top of the README.
|
|
//
|
|
// Composes the already-generated app icon with the wordmark and tagline rather
|
|
// than redrawing the coats, so the banner can never drift out of step with the
|
|
// icon itself. Run generate-icon.swift first.
|
|
//
|
|
// Usage:
|
|
// swift icon/generate-icon.swift icon/AppIcon.iconset
|
|
// cp icon/AppIcon.iconset/icon_512x512.png icon/shannoncoat.png
|
|
// iconutil -c icns icon/AppIcon.iconset -o icon/AppIcon.icns
|
|
// rm -rf icon/AppIcon.iconset
|
|
// swift icon/generate-hero.swift icon/shannoncoat.png docs/hero.png
|
|
import Cocoa
|
|
|
|
private func rgb(_ hex: UInt32) -> NSColor {
|
|
NSColor(srgbRed: CGFloat((hex >> 16) & 255) / 255, green: CGFloat((hex >> 8) & 255) / 255,
|
|
blue: CGFloat(hex & 255) / 255, alpha: 1)
|
|
}
|
|
|
|
// Slate ground rather than cloud: GitHub renders READMEs on both a light and a
|
|
// dark page, and a dark banner sits comfortably on either, where a near-white
|
|
// one glares against a dark page. It also lets the coral icon carry the colour.
|
|
private let ground = rgb(0x26_26_25)
|
|
private let wordmark = rgb(0xF0_EE_E6)
|
|
private let subtitle = rgb(0xA8_A2_98)
|
|
|
|
private let width = 1200
|
|
private let height = 360
|
|
|
|
let args = CommandLine.arguments
|
|
let iconPath = args.count > 1 ? args[1] : "icon/shannoncoat.png"
|
|
let outPath = args.count > 2 ? args[2] : "docs/hero.png"
|
|
|
|
guard let icon = NSImage(contentsOfFile: iconPath) else {
|
|
fatalError("couldn't read \(iconPath) — run generate-icon.swift first")
|
|
}
|
|
|
|
guard let rep = NSBitmapImageRep(
|
|
bitmapDataPlanes: nil, pixelsWide: width, pixelsHigh: height,
|
|
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false,
|
|
colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0
|
|
) else { fatalError("could not create bitmap rep") }
|
|
rep.size = NSSize(width: width, height: height)
|
|
|
|
NSGraphicsContext.saveGraphicsState()
|
|
NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep)
|
|
NSGraphicsContext.current?.imageInterpolation = .high
|
|
|
|
ground.setFill()
|
|
NSBezierPath(rect: NSRect(x: 0, y: 0, width: width, height: height)).fill()
|
|
|
|
let iconSide: CGFloat = 184
|
|
icon.draw(in: NSRect(x: 96, y: (CGFloat(height) - iconSide) / 2, width: iconSide, height: iconSide))
|
|
|
|
let textX: CGFloat = 96 + iconSide + 56
|
|
let name = NSAttributedString(string: "shannoncoat", attributes: [
|
|
.font: NSFont.systemFont(ofSize: 76, weight: .semibold),
|
|
.foregroundColor: wordmark,
|
|
])
|
|
let tagline = NSAttributedString(string: "which coat will Claude wear today?", attributes: [
|
|
.font: NSFont.systemFont(ofSize: 30, weight: .regular),
|
|
.foregroundColor: subtitle,
|
|
])
|
|
|
|
// Stacked around the vertical centre, using each string's own measured height
|
|
// so the pair sits optically centred against the icon rather than by guesswork.
|
|
let nameSize = name.size(), taglineSize = tagline.size()
|
|
let gap: CGFloat = 14
|
|
let blockHeight = nameSize.height + gap + taglineSize.height
|
|
let top = (CGFloat(height) + blockHeight) / 2
|
|
name.draw(at: NSPoint(x: textX, y: top - nameSize.height))
|
|
tagline.draw(at: NSPoint(x: textX + 3, y: top - nameSize.height - gap - taglineSize.height))
|
|
|
|
NSGraphicsContext.restoreGraphicsState()
|
|
|
|
let outURL = URL(fileURLWithPath: outPath)
|
|
try? FileManager.default.createDirectory(
|
|
at: outURL.deletingLastPathComponent(), withIntermediateDirectories: true)
|
|
guard let png = rep.representation(using: .png, properties: [:]) else {
|
|
fatalError("failed to encode hero PNG")
|
|
}
|
|
try! png.write(to: outURL)
|
|
print("wrote \(outPath)")
|