11#!/usr/bin/env swift
22
3- // Generates Veil's app icon: a white bolt on a purple→indigo gradient with
4- // a macOS-style rounded squircle. Renders all sizes Apple expects in an
5- // .iconset, then `iconutil` packages it (called from the Makefile) .
3+ // Generates Veil's app icon: a soft-pastel neo-brutalist sticker — a
4+ // rounded square with a heavy black border and chunky offset shadow,
5+ // centered on a thick "V" mark .
66//
77// Run: swift Scripts/make_icon.swift
8- //
9- // Output: Resources/AppIcon.iconset/icon_<size>.png × N
108
119import AppKit
1210
@@ -16,7 +14,6 @@ let outputDir = projectRoot + "/Resources/AppIcon.iconset"
1614try ? FileManager . default. removeItem ( atPath: outputDir)
1715try ! FileManager . default. createDirectory ( atPath: outputDir, withIntermediateDirectories: true )
1816
19- /// macOS .iconset expected entries: (size, scale, suffix).
2017let entries : [ ( size: Int , scale: Int , suffix: String ) ] = [
2118 ( 16 , 1 , " 16x16 " ) ,
2219 ( 16 , 2 , " 16x16@2x " ) ,
@@ -30,69 +27,78 @@ let entries: [(size: Int, scale: Int, suffix: String)] = [
3027 ( 512 , 2 , " 512x512@2x " ) ,
3128]
3229
30+ // Tonal palette — dusty blush card paired with a deep wine ink.
31+ // Same hue family (red), light tint vs dark shade. Reads soft at a glance,
32+ // holds high contrast at every size.
33+ let card = NSColor ( srgbRed: 0.96 , green: 0.84 , blue: 0.84 , alpha: 1 ) // dusty blush
34+ let ink = NSColor ( srgbRed: 0.27 , green: 0.10 , blue: 0.16 , alpha: 1 ) // deep wine
35+
36+ func roundedRectPath( _ rect: CGRect , radius: CGFloat ) -> CGPath {
37+ CGPath ( roundedRect: rect, cornerWidth: radius, cornerHeight: radius, transform: nil )
38+ }
39+
3340func render( size: CGFloat ) -> NSImage {
3441 let image = NSImage ( size: NSSize ( width: size, height: size) )
3542 image. lockFocus ( )
3643 guard let ctx = NSGraphicsContext . current? . cgContext else {
3744 fatalError ( " No graphics context " )
3845 }
3946
40- // Rounded squircle background. macOS Big Sur+ uses ~22% corner radius.
41- let radius = size * 0.225
42- let rect = CGRect ( x: 0 , y: 0 , width: size, height: size)
43- let path = CGPath ( roundedRect: rect, cornerWidth: radius, cornerHeight: radius, transform: nil )
44-
45- ctx. saveGState ( )
46- ctx. addPath ( path)
47- ctx. clip ( )
48-
49- // Vertical gradient: indigo → violet.
50- let colors = [
51- NSColor ( srgbRed: 0.36 , green: 0.31 , blue: 0.78 , alpha: 1 ) . cgColor, // indigo
52- NSColor ( srgbRed: 0.59 , green: 0.34 , blue: 0.91 , alpha: 1 ) . cgColor, // violet
53- ] as CFArray
54- let gradient = CGGradient ( colorsSpace: CGColorSpaceCreateDeviceRGB ( ) , colors: colors, locations: [ 0 , 1 ] ) !
55- ctx. drawLinearGradient (
56- gradient,
57- start: CGPoint ( x: 0 , y: size) ,
58- end: CGPoint ( x: 0 , y: 0 ) ,
59- options: [ ]
47+ // Sticker fills the canvas. Card sits top-left, shadow peeks bottom-right.
48+ // No backdrop fill — area outside the sticker stays transparent so the
49+ // dock / Finder show whatever's behind.
50+ let shadowOffset = size * 0.07
51+ let cardRect = CGRect (
52+ x: 0 ,
53+ y: shadowOffset,
54+ width: size - shadowOffset,
55+ height: size - shadowOffset
6056 )
61-
62- // Subtle inner highlight along the top edge.
63- let highlight = NSColor . white. withAlphaComponent ( 0.18 ) . cgColor
64- ctx. setFillColor ( highlight)
65- let highlightRect = CGRect ( x: 0 , y: size * 0.7 , width: size, height: size * 0.3 )
66- ctx. fill ( highlightRect)
67-
68- ctx. restoreGState ( )
69-
70- // Draw the bolt symbol centered, white, ~55% of canvas.
71- let symbolSize = size * 0.58
72- if let bolt = NSImage ( systemSymbolName: " bolt.fill " , accessibilityDescription: nil ) {
73- let config = NSImage . SymbolConfiguration ( pointSize: symbolSize, weight: . heavy)
74- let configured = bolt. withSymbolConfiguration ( config) ?? bolt
75-
76- let originX = ( size - symbolSize) / 2
77- let originY = ( size - symbolSize) / 2
78- let symbolRect = NSRect ( x: originX, y: originY, width: symbolSize, height: symbolSize)
79-
80- // Render the symbol white via NSGraphicsContext composite mode.
81- ctx. setFillColor ( NSColor . white. cgColor)
82- let cgImage = configured. cgImage ( forProposedRect: nil , context: nil , hints: nil )
83- if let cg = cgImage {
84- ctx. saveGState ( )
85- ctx. translateBy ( x: 0 , y: size)
86- ctx. scaleBy ( x: 1 , y: - 1 )
87- let flippedRect = NSRect ( x: originX, y: size - originY - symbolSize, width: symbolSize, height: symbolSize)
88- ctx. clip ( to: flippedRect, mask: cg)
89- ctx. fill ( rect)
90- ctx. restoreGState ( )
91- } else {
92- // Fallback — draw the symbol normally (will be tinted by template handling).
93- configured. draw ( in: symbolRect)
94- }
95- }
57+ let shadowRect = cardRect. offsetBy ( dx: shadowOffset, dy: - shadowOffset)
58+ let cardRadius = size * 0.20
59+ let borderWidth = size * 0.05
60+
61+ // Chunky offset shadow.
62+ ctx. setFillColor ( ink. cgColor)
63+ ctx. addPath ( roundedRectPath ( shadowRect, radius: cardRadius) )
64+ ctx. fillPath ( )
65+
66+ // Card fill.
67+ ctx. setFillColor ( card. cgColor)
68+ ctx. addPath ( roundedRectPath ( cardRect, radius: cardRadius) )
69+ ctx. fillPath ( )
70+
71+ // Card border.
72+ let borderInset = borderWidth / 2
73+ let borderRect = cardRect. insetBy ( dx: borderInset, dy: borderInset)
74+ let borderRadius = max ( 0 , cardRadius - borderInset)
75+ ctx. setStrokeColor ( ink. cgColor)
76+ ctx. setLineWidth ( borderWidth)
77+ ctx. addPath ( roundedRectPath ( borderRect, radius: borderRadius) )
78+ ctx. strokePath ( )
79+
80+ // V monogram — two stroked diagonals meeting at the apex. Centered on
81+ // the card, not the canvas, so the offset shadow doesn't cause a shift.
82+ let vWidth = cardRect. width * 0.62
83+ let vHeight = cardRect. height * 0.50
84+ let strokeWidth = size * 0.13
85+ let vOriginX = cardRect. midX - vWidth / 2
86+ let vOriginY = cardRect. midY - vHeight / 2
87+ let topY = vOriginY + vHeight
88+ let bottomY = vOriginY
89+
90+ let vPath = CGMutablePath ( )
91+ vPath. move ( to: CGPoint ( x: vOriginX, y: topY) )
92+ vPath. addLine ( to: CGPoint ( x: vOriginX + vWidth / 2 , y: bottomY) )
93+ vPath. addLine ( to: CGPoint ( x: vOriginX + vWidth, y: topY) )
94+
95+ ctx. setStrokeColor ( ink. cgColor)
96+ ctx. setLineWidth ( strokeWidth)
97+ ctx. setLineJoin ( . miter)
98+ ctx. setLineCap ( . butt)
99+ ctx. setMiterLimit ( 20 )
100+ ctx. addPath ( vPath)
101+ ctx. strokePath ( )
96102
97103 image. unlockFocus ( )
98104 return image
0 commit comments