// Generates AppIcon.iconset — 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. // // Everything is drawn from vector paths here rather than exported from a // design tool, so a size can be tuned independently (small sizes need fatter // strokes than a straight downscale gives) and nothing binary but the derived // .icns is committed. Deliberately not derived from Claude's own icon assets. // // Usage: // swift icon/generate-icon.swift icon/AppIcon.iconset // iconutil -c icns icon/AppIcon.iconset -o icon/AppIcon.icns // rm -rf icon/AppIcon.iconset import Cocoa // Claude's coral and cloud, plus tints of each for depth. The lapel is a // lighter grey than the coat and the back coats are darker corals — tone // separation survives downscaling, where outlines don't. private enum Palette { static 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) } static let tile = rgb(0xD9_77_57) static let coat = rgb(0xF0_EE_E6) static let lapel = rgb(0xB3_AA_97) static let pocket = rgb(0xB8_AF_9C) static let backLeft = rgb(0x8A_3E_2A) static let backLeftPocket = rgb(0x6E_35_24) static let backRight = rgb(0xB4_61_4A) static let backRightPocket = rgb(0x94_4A_34) } private func p(_ x: CGFloat, _ y: CGFloat) -> NSPoint { NSPoint(x: x, y: y) } // MARK: - Shapes, drawn in a 100x100 space with y pointing down // Eight rays of slightly uneven length, a few degrees off the compass points — // a mechanically regular asterisk reads as a snowflake. Wider than tall, so it // doesn't look like it's standing on end. private func asterisk() -> NSBezierPath { let centre = p(50, 24) let tips = [p(65, 24), p(59, 33.3), p(49.5, 38.5), p(40.8, 32.6), p(35, 23.7), p(40.8, 14.5), p(50.2, 9.8), p(58.6, 15.1)] let path = NSBezierPath() for tip in tips { path.move(to: centre); path.line(to: tip) } path.lineWidth = 5.6 path.lineCapStyle = .round return path } // One coat in its own 40x44 box, so the three can be placed at different // scales and angles. The centre split is a true hole, so the tile shows through. private func coatBody() -> NSBezierPath { let b = NSBezierPath() b.move(to: p(16, 2)) b.curve(to: p(3, 6.5), controlPoint1: p(12, 2.5), controlPoint2: p(6, 4)) b.curve(to: p(2.5, 42), controlPoint1: p(1.5, 16), controlPoint2: p(1.5, 30)) b.line(to: p(37.5, 42)) b.curve(to: p(37, 6.5), controlPoint1: p(38.5, 30), controlPoint2: p(38.5, 16)) b.curve(to: p(24, 2), controlPoint1: p(34, 4), controlPoint2: p(28, 2.5)) b.line(to: p(20, 26)) b.close() b.appendRect(NSRect(x: 19.15, y: 26, width: 1.7, height: 16)) b.windingRule = .evenOdd return b } // One connected band round the neck rather than two tabs on the shoulders, // dipping at centre where the lapel opening begins. Its top corners sit at // x15/x25 so the lapels cover them — any further out and they poke through as // spikes either side of the neck. private func coatCollar() -> NSBezierPath { let c = NSBezierPath() c.move(to: p(15, -1.6)); c.line(to: p(25, -1.6)) c.line(to: p(29, 2.6)); c.line(to: p(21.6, 2.6)) c.line(to: p(20, 1)); c.line(to: p(18.4, 2.6)) c.line(to: p(11, 2.6)) c.close() return c } // Notch lapels, each one shape running from the collar's outer corner down to // the button point. The notch sits at (12.5, 8) — pulled down and outward // deliberately, because nearer the inner edge it pinches the polygon to a // hairline and the lapel renders as two separate triangles. private func coatLapels() -> NSBezierPath { let l = NSBezierPath() l.move(to: p(15, -1.6)); l.line(to: p(10.5, 3.5)); l.line(to: p(12.5, 8)) l.line(to: p(9, 14)); l.line(to: p(20, 26)); l.close() l.move(to: p(25, -1.6)); l.line(to: p(29.5, 3.5)); l.line(to: p(27.5, 8)) l.line(to: p(31, 14)); l.line(to: p(20, 26)); l.close() return l } // Filled marks, not holes — a hole would show whichever coat is behind it. private func coatPockets() -> NSBezierPath { let path = NSBezierPath() path.appendRect(NSRect(x: 6.5, y: 22, width: 9, height: 1.7)) path.appendRect(NSRect(x: 24.5, y: 22, width: 9, height: 1.7)) return path } // A hairline along the lapel edges separating the grey lapel from the tile // showing through the opening. Deliberately fine: a detail for 128px and up. // Thickening it to survive smaller sizes only blurs the lapel edge, because at // this diagonal a one-pixel stroke covers a fraction of each pixel it crosses. private func coatOpeningBorder() -> NSBezierPath { let path = NSBezierPath() path.move(to: p(15, -1.6)); path.line(to: p(20, 26)); path.line(to: p(25, -1.6)) path.lineWidth = 0.7 path.lineJoinStyle = .round path.lineCapStyle = .round return path } private func placed(_ path: NSBezierPath, x: CGFloat, y: CGFloat, rotation: CGFloat, scale: CGFloat) -> NSBezierPath { let transform = NSAffineTransform() transform.translateX(by: x, yBy: y) if rotation != 0 { transform.rotate(byDegrees: rotation) } transform.scale(by: scale) let copy = path.copy() as! NSBezierPath copy.transform(using: transform as AffineTransform) return copy } // Each coat is finished before the next is laid over it. Drawing all three // bodies and then all three sets of details would put the back coats' collars // and pockets on top of the front coat. private func drawCoat(x: CGFloat, y: CGFloat, rotation: CGFloat, scale: CGFloat, body: NSColor, pocket: NSColor, lapel: NSColor? = nil) { body.setFill() placed(coatBody(), x: x, y: y, rotation: rotation, scale: scale).fill() placed(coatCollar(), x: x, y: y, rotation: rotation, scale: scale).fill() if let lapel { lapel.setFill() placed(coatLapels(), x: x, y: y, rotation: rotation, scale: scale).fill() body.setStroke() let border = placed(coatOpeningBorder(), x: x, y: y, rotation: rotation, scale: scale) border.lineWidth = 0.7 * scale border.stroke() } pocket.setFill() placed(coatPockets(), x: x, y: y, rotation: rotation, scale: scale).fill() } private func drawIcon() { Palette.tile.setFill() NSBezierPath(roundedRect: NSRect(x: 0, y: 0, width: 100, height: 100), xRadius: 22, yRadius: 22).fill() Palette.coat.setStroke() asterisk().stroke() drawCoat(x: 11, y: 50, rotation: -8, scale: 0.78, body: Palette.backLeft, pocket: Palette.backLeftPocket) drawCoat(x: 60, y: 47, rotation: 8, scale: 0.78, body: Palette.backRight, pocket: Palette.backRightPocket) drawCoat(x: 31.5, y: 44, rotation: 0, scale: 0.92, body: Palette.coat, pocket: Palette.pocket, lapel: Palette.lapel) } // MARK: - Rendering // Renders at exact pixel dimensions via an explicit NSBitmapImageRep rather // than NSImage.lockFocus() — lockFocus rasterizes at the *main screen's* // backing scale factor, which would silently double every size on a Retina // display instead of producing the exact px asked for. private func renderIcon(pixels: Int) -> Data { let side = CGFloat(pixels) 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 { fatalError("could not create bitmap rep") } rep.size = NSSize(width: side, height: side) NSGraphicsContext.saveGraphicsState() NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep) NSGraphicsContext.current?.imageInterpolation = .high // The shapes are authored y-down, the way the drawing reads; flip once // here rather than mirroring every coordinate. let flip = NSAffineTransform() flip.translateX(by: 0, yBy: side) flip.scaleX(by: side / 100, yBy: -side / 100) flip.concat() drawIcon() NSGraphicsContext.restoreGraphicsState() guard let png = rep.representation(using: .png, properties: [:]) else { fatalError("failed to encode PNG at \(pixels)px") } return png } let sizes: [(name: String, px: 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), ] let outDir = CommandLine.arguments.count > 1 ? CommandLine.arguments[1] : "AppIcon.iconset" try? FileManager.default.createDirectory(atPath: outDir, withIntermediateDirectories: true) for (name, px) in sizes { let path = "\(outDir)/\(name).png" try? renderIcon(pixels: px).write(to: URL(fileURLWithPath: path)) print("wrote \(path)") }