Initial commit: Flights iOS app
Flight search app built on FlightConnections.com API data. Features: airport search with autocomplete, browse by country/state/map, flight schedules by route and date, multi-airline support with per-airline schedule loading. Includes 4,561-airport GPS database for map browsing. Adaptive light/dark mode UI inspired by Flighty. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
723
design/generate_icon_options.swift
Normal file
723
design/generate_icon_options.swift
Normal file
@@ -0,0 +1,723 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
let canvasSize = CGSize(width: 1024, height: 1024)
|
||||
|
||||
enum Palette {
|
||||
static let accent = NSColor(hex: 0x6366F1)
|
||||
static let accentLight = NSColor(hex: 0x818CF8)
|
||||
static let boarding = NSColor(hex: 0x3B82F6)
|
||||
static let mint = NSColor(hex: 0x10B981)
|
||||
static let slate = NSColor(hex: 0x0F172A)
|
||||
static let sky = NSColor(hex: 0x38BDF8)
|
||||
static let cloud = NSColor(hex: 0xE2E8F0)
|
||||
static let mist = NSColor(hex: 0xF8FAFC)
|
||||
static let ink = NSColor(hex: 0x111827)
|
||||
}
|
||||
|
||||
struct IconOption {
|
||||
let fileName: String
|
||||
let label: String
|
||||
let draw: (CGRect) -> Void
|
||||
}
|
||||
|
||||
extension NSColor {
|
||||
convenience init(hex: UInt32, alpha: CGFloat = 1.0) {
|
||||
self.init(
|
||||
calibratedRed: CGFloat((hex >> 16) & 0xFF) / 255.0,
|
||||
green: CGFloat((hex >> 8) & 0xFF) / 255.0,
|
||||
blue: CGFloat(hex & 0xFF) / 255.0,
|
||||
alpha: alpha
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension CGRect {
|
||||
var center: CGPoint { CGPoint(x: midX, y: midY) }
|
||||
}
|
||||
|
||||
func roundedRectPath(_ rect: CGRect, radius: CGFloat) -> NSBezierPath {
|
||||
NSBezierPath(roundedRect: rect, xRadius: radius, yRadius: radius)
|
||||
}
|
||||
|
||||
func circleRect(center: CGPoint, radius: CGFloat) -> CGRect {
|
||||
CGRect(x: center.x - radius, y: center.y - radius, width: radius * 2, height: radius * 2)
|
||||
}
|
||||
|
||||
func withGraphicsState(_ draw: () -> Void) {
|
||||
NSGraphicsContext.saveGraphicsState()
|
||||
draw()
|
||||
NSGraphicsContext.restoreGraphicsState()
|
||||
}
|
||||
|
||||
func applyShadow(
|
||||
color: NSColor,
|
||||
blur: CGFloat,
|
||||
x: CGFloat = 0,
|
||||
y: CGFloat = 0,
|
||||
draw: () -> Void
|
||||
) {
|
||||
withGraphicsState {
|
||||
let shadow = NSShadow()
|
||||
shadow.shadowColor = color
|
||||
shadow.shadowBlurRadius = blur
|
||||
shadow.shadowOffset = NSSize(width: x, height: y)
|
||||
shadow.set()
|
||||
draw()
|
||||
}
|
||||
}
|
||||
|
||||
func fillPath(_ path: NSBezierPath, colors: [NSColor], angle: CGFloat) {
|
||||
guard let gradient = NSGradient(colors: colors) else { return }
|
||||
gradient.draw(in: path, angle: angle)
|
||||
}
|
||||
|
||||
func strokePath(_ path: NSBezierPath, color: NSColor, lineWidth: CGFloat) {
|
||||
color.setStroke()
|
||||
path.lineWidth = lineWidth
|
||||
path.stroke()
|
||||
}
|
||||
|
||||
func fillCircle(center: CGPoint, radius: CGFloat, color: NSColor) {
|
||||
let path = NSBezierPath(ovalIn: circleRect(center: center, radius: radius))
|
||||
color.setFill()
|
||||
path.fill()
|
||||
}
|
||||
|
||||
func fillRoundedRect(_ rect: CGRect, radius: CGFloat, colors: [NSColor], angle: CGFloat) {
|
||||
let path = roundedRectPath(rect, radius: radius)
|
||||
fillPath(path, colors: colors, angle: angle)
|
||||
}
|
||||
|
||||
func drawSymbol(
|
||||
_ name: String,
|
||||
in rect: CGRect,
|
||||
pointSize: CGFloat,
|
||||
tint: NSColor,
|
||||
weight: NSFont.Weight = .regular,
|
||||
scale: NSImage.SymbolScale = .large,
|
||||
rotationDegrees: CGFloat = 0,
|
||||
alpha: CGFloat = 1.0
|
||||
) {
|
||||
guard
|
||||
let symbol = NSImage(systemSymbolName: name, accessibilityDescription: nil),
|
||||
let configured = symbol.withSymbolConfiguration(.init(pointSize: pointSize, weight: weight, scale: scale))
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
let tinted = NSImage(size: configured.size)
|
||||
tinted.lockFocus()
|
||||
let imageRect = NSRect(origin: .zero, size: configured.size)
|
||||
configured.draw(in: imageRect, from: NSRect.zero, operation: NSCompositingOperation.sourceOver, fraction: 1.0)
|
||||
tint.withAlphaComponent(alpha).set()
|
||||
imageRect.fill(using: NSCompositingOperation.sourceAtop)
|
||||
tinted.unlockFocus()
|
||||
|
||||
withGraphicsState {
|
||||
let transform = NSAffineTransform()
|
||||
transform.translateX(by: rect.midX, yBy: rect.midY)
|
||||
transform.rotate(byDegrees: rotationDegrees)
|
||||
transform.translateX(by: -rect.midX, yBy: -rect.midY)
|
||||
transform.concat()
|
||||
tinted.draw(in: rect, from: NSRect.zero, operation: NSCompositingOperation.sourceOver, fraction: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
func quadraticPath(start: CGPoint, control: CGPoint, end: CGPoint) -> NSBezierPath {
|
||||
let cp1 = CGPoint(
|
||||
x: start.x + ((control.x - start.x) * 2.0 / 3.0),
|
||||
y: start.y + ((control.y - start.y) * 2.0 / 3.0)
|
||||
)
|
||||
let cp2 = CGPoint(
|
||||
x: end.x + ((control.x - end.x) * 2.0 / 3.0),
|
||||
y: end.y + ((control.y - end.y) * 2.0 / 3.0)
|
||||
)
|
||||
let path = NSBezierPath()
|
||||
path.move(to: start)
|
||||
path.curve(to: end, controlPoint1: cp1, controlPoint2: cp2)
|
||||
return path
|
||||
}
|
||||
|
||||
func quadraticPoint(start: CGPoint, control: CGPoint, end: CGPoint, t: CGFloat) -> CGPoint {
|
||||
let oneMinusT = 1 - t
|
||||
let x = (oneMinusT * oneMinusT * start.x) + (2 * oneMinusT * t * control.x) + (t * t * end.x)
|
||||
let y = (oneMinusT * oneMinusT * start.y) + (2 * oneMinusT * t * control.y) + (t * t * end.y)
|
||||
return CGPoint(x: x, y: y)
|
||||
}
|
||||
|
||||
func quadraticAngle(start: CGPoint, control: CGPoint, end: CGPoint, t: CGFloat) -> CGFloat {
|
||||
let dx = (2 * (1 - t) * (control.x - start.x)) + (2 * t * (end.x - control.x))
|
||||
let dy = (2 * (1 - t) * (control.y - start.y)) + (2 * t * (end.y - control.y))
|
||||
return atan2(dy, dx) * 180 / .pi
|
||||
}
|
||||
|
||||
func drawRouteArc(
|
||||
start: CGPoint,
|
||||
control: CGPoint,
|
||||
end: CGPoint,
|
||||
color: NSColor,
|
||||
lineWidth: CGFloat,
|
||||
dash: [CGFloat] = [22, 18]
|
||||
) {
|
||||
let path = quadraticPath(start: start, control: control, end: end)
|
||||
color.setStroke()
|
||||
path.lineWidth = lineWidth
|
||||
path.lineCapStyle = .round
|
||||
path.setLineDash(dash, count: dash.count, phase: 0)
|
||||
path.stroke()
|
||||
}
|
||||
|
||||
func drawRing(center: CGPoint, radius: CGFloat, width: CGFloat, color: NSColor, alpha: CGFloat = 1.0) {
|
||||
let path = NSBezierPath(ovalIn: circleRect(center: center, radius: radius))
|
||||
strokePath(path, color: color.withAlphaComponent(alpha), lineWidth: width)
|
||||
}
|
||||
|
||||
func drawGlobe(
|
||||
center: CGPoint,
|
||||
radius: CGFloat,
|
||||
fillColors: [NSColor],
|
||||
landTint: NSColor,
|
||||
gridColor: NSColor,
|
||||
highlightColor: NSColor,
|
||||
landAlpha: CGFloat = 0.35,
|
||||
strokeColor: NSColor? = nil,
|
||||
glowColor: NSColor? = nil
|
||||
) {
|
||||
let globeRect = circleRect(center: center, radius: radius)
|
||||
let globePath = NSBezierPath(ovalIn: globeRect)
|
||||
|
||||
if let glowColor {
|
||||
applyShadow(color: glowColor, blur: 48) {
|
||||
fillCircle(center: center, radius: radius * 0.96, color: glowColor.withAlphaComponent(0.35))
|
||||
}
|
||||
}
|
||||
|
||||
fillPath(globePath, colors: fillColors, angle: 90)
|
||||
|
||||
withGraphicsState {
|
||||
globePath.addClip()
|
||||
|
||||
let continentsRect = globeRect.insetBy(dx: radius * 0.12, dy: radius * 0.12)
|
||||
drawSymbol(
|
||||
"globe.americas.fill",
|
||||
in: continentsRect.offsetBy(dx: radius * 0.03, dy: radius * -0.02),
|
||||
pointSize: radius * 1.3,
|
||||
tint: landTint,
|
||||
weight: .regular,
|
||||
rotationDegrees: 0,
|
||||
alpha: landAlpha
|
||||
)
|
||||
|
||||
let meridians: [CGFloat] = [0.42, 0.72]
|
||||
for scale in meridians {
|
||||
let ellipse = NSBezierPath(
|
||||
ovalIn: CGRect(
|
||||
x: center.x - radius * scale,
|
||||
y: center.y - radius,
|
||||
width: radius * scale * 2,
|
||||
height: radius * 2
|
||||
)
|
||||
)
|
||||
strokePath(ellipse, color: gridColor.withAlphaComponent(0.9), lineWidth: 5)
|
||||
}
|
||||
|
||||
let parallels: [CGFloat] = [0.30, 0.62, 0.86]
|
||||
for scale in parallels {
|
||||
let ellipse = NSBezierPath(
|
||||
ovalIn: CGRect(
|
||||
x: center.x - radius,
|
||||
y: center.y - radius * scale,
|
||||
width: radius * 2,
|
||||
height: radius * scale * 2
|
||||
)
|
||||
)
|
||||
strokePath(ellipse, color: gridColor.withAlphaComponent(0.68), lineWidth: 4)
|
||||
}
|
||||
|
||||
let specular = NSBezierPath(
|
||||
ovalIn: CGRect(
|
||||
x: center.x - radius * 0.62,
|
||||
y: center.y + radius * 0.16,
|
||||
width: radius * 0.70,
|
||||
height: radius * 0.36
|
||||
)
|
||||
)
|
||||
highlightColor.withAlphaComponent(0.18).setFill()
|
||||
specular.fill()
|
||||
}
|
||||
|
||||
if let strokeColor {
|
||||
strokePath(globePath, color: strokeColor, lineWidth: 6)
|
||||
}
|
||||
}
|
||||
|
||||
func drawTopographicLines(rect: CGRect, color: NSColor, count: Int, inset: CGFloat) {
|
||||
for index in 0..<count {
|
||||
let y = rect.minY + inset + (CGFloat(index) * ((rect.height - inset * 2) / CGFloat(count)))
|
||||
let path = NSBezierPath()
|
||||
path.move(to: CGPoint(x: rect.minX + inset, y: y))
|
||||
path.curve(
|
||||
to: CGPoint(x: rect.maxX - inset, y: y + 24),
|
||||
controlPoint1: CGPoint(x: rect.minX + rect.width * 0.28, y: y - 38),
|
||||
controlPoint2: CGPoint(x: rect.minX + rect.width * 0.68, y: y + 64)
|
||||
)
|
||||
path.lineWidth = 3
|
||||
path.setLineDash([18, 14], count: 2, phase: CGFloat(index) * 6)
|
||||
color.setStroke()
|
||||
path.stroke()
|
||||
}
|
||||
}
|
||||
|
||||
func drawStarField(rect: CGRect, stars: [(CGFloat, CGFloat, CGFloat, CGFloat)]) {
|
||||
for (x, y, size, alpha) in stars {
|
||||
fillCircle(
|
||||
center: CGPoint(x: rect.minX + x * rect.width, y: rect.minY + y * rect.height),
|
||||
radius: size,
|
||||
color: NSColor.white.withAlphaComponent(alpha)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func drawOrbitalRoute(rect: CGRect) {
|
||||
fillRoundedRect(
|
||||
rect,
|
||||
radius: 220,
|
||||
colors: [
|
||||
NSColor(hex: 0x08111F),
|
||||
NSColor(hex: 0x1D2A64),
|
||||
NSColor(hex: 0x4F46E5)
|
||||
],
|
||||
angle: 42
|
||||
)
|
||||
|
||||
fillCircle(center: CGPoint(x: 214, y: 860), radius: 170, color: Palette.accentLight.withAlphaComponent(0.14))
|
||||
fillCircle(center: CGPoint(x: 832, y: 248), radius: 200, color: Palette.sky.withAlphaComponent(0.18))
|
||||
|
||||
drawGlobe(
|
||||
center: CGPoint(x: 332, y: 368),
|
||||
radius: 302,
|
||||
fillColors: [NSColor(hex: 0x183B6C), NSColor(hex: 0x0C1834)],
|
||||
landTint: NSColor.white,
|
||||
gridColor: NSColor.white.withAlphaComponent(0.34),
|
||||
highlightColor: NSColor.white,
|
||||
landAlpha: 0.18,
|
||||
strokeColor: NSColor.white.withAlphaComponent(0.12),
|
||||
glowColor: Palette.sky.withAlphaComponent(0.2)
|
||||
)
|
||||
|
||||
let start = CGPoint(x: 272, y: 464)
|
||||
let control = CGPoint(x: 500, y: 940)
|
||||
let end = CGPoint(x: 830, y: 744)
|
||||
drawRouteArc(
|
||||
start: start,
|
||||
control: control,
|
||||
end: end,
|
||||
color: NSColor.white.withAlphaComponent(0.50),
|
||||
lineWidth: 16
|
||||
)
|
||||
|
||||
fillCircle(center: start, radius: 22, color: Palette.mint)
|
||||
applyShadow(color: NSColor.black.withAlphaComponent(0.28), blur: 24, y: -8) {
|
||||
let planeCenter = quadraticPoint(start: start, control: control, end: end, t: 0.58)
|
||||
let planeAngle = quadraticAngle(start: start, control: control, end: end, t: 0.58) - 6
|
||||
drawSymbol(
|
||||
"airplane",
|
||||
in: CGRect(x: planeCenter.x - 92, y: planeCenter.y - 64, width: 184, height: 128),
|
||||
pointSize: 150,
|
||||
tint: NSColor.white,
|
||||
weight: .semibold,
|
||||
rotationDegrees: planeAngle
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func drawWindowBadge(rect: CGRect) {
|
||||
fillRoundedRect(
|
||||
rect,
|
||||
radius: 220,
|
||||
colors: [
|
||||
NSColor(hex: 0xE0F2FE),
|
||||
NSColor(hex: 0xC7D2FE),
|
||||
NSColor(hex: 0x93C5FD)
|
||||
],
|
||||
angle: 65
|
||||
)
|
||||
|
||||
drawRing(center: CGPoint(x: 300, y: 832), radius: 116, width: 30, color: NSColor.white, alpha: 0.20)
|
||||
fillCircle(center: CGPoint(x: 796, y: 226), radius: 180, color: Palette.accent.withAlphaComponent(0.12))
|
||||
|
||||
let badgeCenter = CGPoint(x: 512, y: 528)
|
||||
let badgeRadius: CGFloat = 356
|
||||
applyShadow(color: NSColor.black.withAlphaComponent(0.18), blur: 38, y: -14) {
|
||||
drawGlobe(
|
||||
center: badgeCenter,
|
||||
radius: badgeRadius,
|
||||
fillColors: [NSColor(hex: 0x5046E5), NSColor(hex: 0x1E1B4B)],
|
||||
landTint: NSColor.white,
|
||||
gridColor: NSColor.white.withAlphaComponent(0.55),
|
||||
highlightColor: NSColor.white,
|
||||
landAlpha: 0.15,
|
||||
strokeColor: NSColor.white.withAlphaComponent(0.14),
|
||||
glowColor: Palette.accent.withAlphaComponent(0.2)
|
||||
)
|
||||
}
|
||||
|
||||
fillCircle(center: CGPoint(x: 406, y: 662), radius: 104, color: NSColor.white.withAlphaComponent(0.10))
|
||||
|
||||
let orbit = NSBezierPath(ovalIn: circleRect(center: badgeCenter, radius: badgeRadius + 58))
|
||||
orbit.lineWidth = 22
|
||||
orbit.setLineDash([170, 44], count: 2, phase: 82)
|
||||
NSColor.white.withAlphaComponent(0.58).setStroke()
|
||||
orbit.stroke()
|
||||
|
||||
let planeRect = CGRect(x: 674, y: 680, width: 220, height: 160)
|
||||
applyShadow(color: NSColor.black.withAlphaComponent(0.28), blur: 24, y: -10) {
|
||||
drawSymbol(
|
||||
"airplane",
|
||||
in: planeRect,
|
||||
pointSize: 170,
|
||||
tint: NSColor.white,
|
||||
weight: .semibold,
|
||||
rotationDegrees: 32
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func drawAtlasTile(rect: CGRect) {
|
||||
fillRoundedRect(
|
||||
rect,
|
||||
radius: 220,
|
||||
colors: [
|
||||
NSColor(hex: 0xF8FAFC),
|
||||
NSColor(hex: 0xEEF2FF),
|
||||
NSColor(hex: 0xDBEAFE)
|
||||
],
|
||||
angle: 90
|
||||
)
|
||||
|
||||
drawTopographicLines(rect: rect, color: Palette.accent.withAlphaComponent(0.14), count: 10, inset: 84)
|
||||
|
||||
let roundelRect = CGRect(x: 176, y: 176, width: 672, height: 672)
|
||||
let roundelPath = NSBezierPath(roundedRect: roundelRect, xRadius: 190, yRadius: 190)
|
||||
applyShadow(color: NSColor.black.withAlphaComponent(0.12), blur: 24, y: -10) {
|
||||
fillPath(
|
||||
roundelPath,
|
||||
colors: [
|
||||
NSColor(hex: 0x0F172A),
|
||||
NSColor(hex: 0x1E293B),
|
||||
NSColor(hex: 0x312E81)
|
||||
],
|
||||
angle: 50
|
||||
)
|
||||
}
|
||||
|
||||
drawGlobe(
|
||||
center: CGPoint(x: 512, y: 512),
|
||||
radius: 244,
|
||||
fillColors: [NSColor(hex: 0x1D4ED8), NSColor(hex: 0x1E3A8A)],
|
||||
landTint: NSColor.white,
|
||||
gridColor: NSColor.white.withAlphaComponent(0.55),
|
||||
highlightColor: NSColor.white,
|
||||
landAlpha: 0.20,
|
||||
strokeColor: NSColor.white.withAlphaComponent(0.10),
|
||||
glowColor: Palette.boarding.withAlphaComponent(0.10)
|
||||
)
|
||||
|
||||
let orbit = NSBezierPath()
|
||||
orbit.move(to: CGPoint(x: 228, y: 384))
|
||||
orbit.curve(
|
||||
to: CGPoint(x: 786, y: 670),
|
||||
controlPoint1: CGPoint(x: 322, y: 804),
|
||||
controlPoint2: CGPoint(x: 650, y: 870)
|
||||
)
|
||||
orbit.lineWidth = 18
|
||||
orbit.lineCapStyle = .round
|
||||
orbit.setLineDash([28, 22], count: 2, phase: 0)
|
||||
Palette.accentLight.withAlphaComponent(0.82).setStroke()
|
||||
orbit.stroke()
|
||||
|
||||
fillCircle(center: CGPoint(x: 786, y: 670), radius: 18, color: Palette.mint)
|
||||
|
||||
applyShadow(color: NSColor.black.withAlphaComponent(0.28), blur: 24, y: -6) {
|
||||
drawSymbol(
|
||||
"airplane",
|
||||
in: CGRect(x: 356, y: 408, width: 320, height: 230),
|
||||
pointSize: 246,
|
||||
tint: NSColor.white,
|
||||
weight: .bold,
|
||||
rotationDegrees: 28
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func drawNightRing(rect: CGRect) {
|
||||
fillRoundedRect(
|
||||
rect,
|
||||
radius: 220,
|
||||
colors: [
|
||||
NSColor(hex: 0x020617),
|
||||
NSColor(hex: 0x111827),
|
||||
NSColor(hex: 0x172554)
|
||||
],
|
||||
angle: 50
|
||||
)
|
||||
|
||||
drawStarField(
|
||||
rect: rect,
|
||||
stars: [
|
||||
(0.18, 0.82, 5, 0.70),
|
||||
(0.29, 0.74, 3, 0.42),
|
||||
(0.77, 0.81, 4, 0.58),
|
||||
(0.67, 0.69, 3, 0.36),
|
||||
(0.86, 0.34, 5, 0.62),
|
||||
(0.15, 0.30, 4, 0.44)
|
||||
]
|
||||
)
|
||||
|
||||
let center = CGPoint(x: 512, y: 512)
|
||||
fillCircle(center: center, radius: 350, color: Palette.sky.withAlphaComponent(0.07))
|
||||
|
||||
drawRing(center: center, radius: 338, width: 28, color: Palette.sky, alpha: 0.92)
|
||||
drawRing(center: center, radius: 378, width: 6, color: NSColor.white, alpha: 0.20)
|
||||
|
||||
drawGlobe(
|
||||
center: center,
|
||||
radius: 252,
|
||||
fillColors: [NSColor(hex: 0x0F766E), NSColor(hex: 0x0F172A)],
|
||||
landTint: NSColor.white,
|
||||
gridColor: NSColor.white.withAlphaComponent(0.44),
|
||||
highlightColor: NSColor.white,
|
||||
landAlpha: 0.16,
|
||||
strokeColor: NSColor.white.withAlphaComponent(0.10),
|
||||
glowColor: Palette.sky.withAlphaComponent(0.18)
|
||||
)
|
||||
|
||||
fillCircle(center: CGPoint(x: 512, y: 512), radius: 178, color: Palette.mint.withAlphaComponent(0.10))
|
||||
|
||||
applyShadow(color: NSColor.black.withAlphaComponent(0.30), blur: 20, y: -6) {
|
||||
drawSymbol(
|
||||
"airplane",
|
||||
in: CGRect(x: 668, y: 676, width: 194, height: 140),
|
||||
pointSize: 148,
|
||||
tint: NSColor.white,
|
||||
weight: .semibold,
|
||||
rotationDegrees: 36
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func drawRouteCard(rect: CGRect) {
|
||||
fillRoundedRect(
|
||||
rect,
|
||||
radius: 220,
|
||||
colors: [
|
||||
NSColor(hex: 0xF8FAFC),
|
||||
NSColor(hex: 0xEEF2FF),
|
||||
NSColor(hex: 0xE0E7FF)
|
||||
],
|
||||
angle: 70
|
||||
)
|
||||
|
||||
let backCard = CGRect(x: 176, y: 204, width: 684, height: 614)
|
||||
let frontCard = CGRect(x: 132, y: 246, width: 760, height: 560)
|
||||
|
||||
let backPath = roundedRectPath(backCard, radius: 88)
|
||||
let frontPath = roundedRectPath(frontCard, radius: 96)
|
||||
|
||||
applyShadow(color: NSColor.black.withAlphaComponent(0.10), blur: 18, y: -8) {
|
||||
fillPath(backPath, colors: [Palette.accentLight.withAlphaComponent(0.30), Palette.boarding.withAlphaComponent(0.18)], angle: 12)
|
||||
}
|
||||
|
||||
applyShadow(color: NSColor.black.withAlphaComponent(0.12), blur: 24, y: -10) {
|
||||
fillPath(frontPath, colors: [NSColor.white, NSColor(hex: 0xEEF2FF)], angle: 90)
|
||||
}
|
||||
strokePath(frontPath, color: Palette.cloud.withAlphaComponent(0.88), lineWidth: 4)
|
||||
|
||||
let globeCenter = CGPoint(x: 314, y: 584)
|
||||
drawGlobe(
|
||||
center: globeCenter,
|
||||
radius: 156,
|
||||
fillColors: [Palette.accentLight, Palette.accent],
|
||||
landTint: NSColor.white,
|
||||
gridColor: NSColor.white.withAlphaComponent(0.52),
|
||||
highlightColor: NSColor.white,
|
||||
landAlpha: 0.18,
|
||||
strokeColor: NSColor.white.withAlphaComponent(0.10),
|
||||
glowColor: Palette.accent.withAlphaComponent(0.12)
|
||||
)
|
||||
|
||||
let start = CGPoint(x: 346, y: 438)
|
||||
let control = CGPoint(x: 548, y: 754)
|
||||
let end = CGPoint(x: 780, y: 538)
|
||||
drawRouteArc(
|
||||
start: start,
|
||||
control: control,
|
||||
end: end,
|
||||
color: Palette.ink.withAlphaComponent(0.32),
|
||||
lineWidth: 16,
|
||||
dash: [20, 18]
|
||||
)
|
||||
fillCircle(center: start, radius: 20, color: Palette.mint)
|
||||
fillCircle(center: end, radius: 20, color: Palette.boarding)
|
||||
|
||||
let planeCenter = quadraticPoint(start: start, control: control, end: end, t: 0.58)
|
||||
let planeAngle = quadraticAngle(start: start, control: control, end: end, t: 0.58) - 10
|
||||
applyShadow(color: NSColor.black.withAlphaComponent(0.18), blur: 16, y: -4) {
|
||||
drawSymbol(
|
||||
"airplane",
|
||||
in: CGRect(x: planeCenter.x - 102, y: planeCenter.y - 72, width: 204, height: 144),
|
||||
pointSize: 166,
|
||||
tint: Palette.ink,
|
||||
weight: .bold,
|
||||
rotationDegrees: planeAngle
|
||||
)
|
||||
}
|
||||
|
||||
let detailPill = roundedRectPath(CGRect(x: 226, y: 318, width: 360, height: 28), radius: 14)
|
||||
fillPath(detailPill, colors: [Palette.cloud.withAlphaComponent(0.92), Palette.cloud.withAlphaComponent(0.92)], angle: 0)
|
||||
|
||||
let accentPill = roundedRectPath(CGRect(x: 226, y: 272, width: 220, height: 28), radius: 14)
|
||||
fillPath(
|
||||
accentPill,
|
||||
colors: [Palette.accent.withAlphaComponent(0.16), Palette.boarding.withAlphaComponent(0.16)],
|
||||
angle: 0
|
||||
)
|
||||
|
||||
let actionChip = roundedRectPath(CGRect(x: 620, y: 270, width: 164, height: 54), radius: 27)
|
||||
fillPath(actionChip, colors: [Palette.accent, Palette.boarding], angle: 0)
|
||||
|
||||
drawSymbol(
|
||||
"airplane.departure",
|
||||
in: CGRect(x: 662, y: 282, width: 82, height: 34),
|
||||
pointSize: 46,
|
||||
tint: NSColor.white,
|
||||
weight: .bold
|
||||
)
|
||||
}
|
||||
|
||||
func renderBitmap(size: CGSize = canvasSize, draw: (CGRect) -> Void) -> NSBitmapImageRep {
|
||||
let rep = NSBitmapImageRep(
|
||||
bitmapDataPlanes: nil,
|
||||
pixelsWide: Int(size.width),
|
||||
pixelsHigh: Int(size.height),
|
||||
bitsPerSample: 8,
|
||||
samplesPerPixel: 4,
|
||||
hasAlpha: true,
|
||||
isPlanar: false,
|
||||
colorSpaceName: .deviceRGB,
|
||||
bytesPerRow: 0,
|
||||
bitsPerPixel: 0
|
||||
)!
|
||||
|
||||
rep.size = size
|
||||
|
||||
let graphicsContext = NSGraphicsContext(bitmapImageRep: rep)!
|
||||
NSGraphicsContext.saveGraphicsState()
|
||||
NSGraphicsContext.current = graphicsContext
|
||||
draw(CGRect(origin: .zero, size: size))
|
||||
graphicsContext.flushGraphics()
|
||||
NSGraphicsContext.restoreGraphicsState()
|
||||
return rep
|
||||
}
|
||||
|
||||
func savePNG(_ rep: NSBitmapImageRep, to url: URL) throws {
|
||||
guard let data = rep.representation(using: .png, properties: [:]) else {
|
||||
throw NSError(domain: "IconGenerator", code: 1)
|
||||
}
|
||||
try data.write(to: url)
|
||||
}
|
||||
|
||||
func drawComparisonSheet(options: [IconOption], imageDirectory: URL, outputURL: URL) throws {
|
||||
let sheetSize = CGSize(width: 2190, height: 1620)
|
||||
let rep = renderBitmap(size: sheetSize) { rect in
|
||||
fillRoundedRect(
|
||||
rect,
|
||||
radius: 120,
|
||||
colors: [NSColor(hex: 0xF8FAFC), NSColor(hex: 0xEEF2FF)],
|
||||
angle: 90
|
||||
)
|
||||
|
||||
let titleAttributes: [NSAttributedString.Key: Any] = [
|
||||
.font: NSFont.systemFont(ofSize: 82, weight: .bold),
|
||||
.foregroundColor: Palette.slate
|
||||
]
|
||||
let subtitleAttributes: [NSAttributedString.Key: Any] = [
|
||||
.font: NSFont.systemFont(ofSize: 34, weight: .medium),
|
||||
.foregroundColor: Palette.ink.withAlphaComponent(0.70)
|
||||
]
|
||||
NSAttributedString(string: "Flights App Icon Concepts", attributes: titleAttributes)
|
||||
.draw(at: CGPoint(x: 112, y: 1470))
|
||||
NSAttributedString(
|
||||
string: "1 to 5. Same palette, different attitude.",
|
||||
attributes: subtitleAttributes
|
||||
)
|
||||
.draw(at: CGPoint(x: 118, y: 1412))
|
||||
|
||||
let cardWidth: CGFloat = 354
|
||||
let cardHeight: CGFloat = 474
|
||||
let horizontalGap: CGFloat = 52
|
||||
let verticalGap: CGFloat = 50
|
||||
let startX: CGFloat = 112
|
||||
let startY: CGFloat = 856
|
||||
|
||||
for (index, option) in options.enumerated() {
|
||||
let row = index / 3
|
||||
let column = index % 3
|
||||
let x = startX + CGFloat(column) * (cardWidth + horizontalGap)
|
||||
let y = startY - CGFloat(row) * (cardHeight + verticalGap)
|
||||
let cardRect = CGRect(x: x, y: y, width: cardWidth, height: cardHeight)
|
||||
|
||||
let cardPath = roundedRectPath(cardRect, radius: 52)
|
||||
applyShadow(color: NSColor.black.withAlphaComponent(0.10), blur: 20, y: -8) {
|
||||
fillPath(cardPath, colors: [NSColor.white, NSColor(hex: 0xF8FAFC)], angle: 90)
|
||||
}
|
||||
|
||||
let imageURL = imageDirectory.appendingPathComponent(option.fileName)
|
||||
if let image = NSImage(contentsOf: imageURL) {
|
||||
image.draw(in: CGRect(x: x + 27, y: y + 116, width: 300, height: 300))
|
||||
}
|
||||
|
||||
let labelAttributes: [NSAttributedString.Key: Any] = [
|
||||
.font: NSFont.monospacedSystemFont(ofSize: 30, weight: .bold),
|
||||
.foregroundColor: Palette.accent
|
||||
]
|
||||
let titleAttributes: [NSAttributedString.Key: Any] = [
|
||||
.font: NSFont.systemFont(ofSize: 34, weight: .semibold),
|
||||
.foregroundColor: Palette.slate
|
||||
]
|
||||
NSAttributedString(string: "\(index + 1).", attributes: labelAttributes)
|
||||
.draw(at: CGPoint(x: x + 28, y: y + 58))
|
||||
NSAttributedString(string: option.label, attributes: titleAttributes)
|
||||
.draw(at: CGPoint(x: x + 82, y: y + 54))
|
||||
}
|
||||
}
|
||||
|
||||
try savePNG(rep, to: outputURL)
|
||||
}
|
||||
|
||||
let options: [IconOption] = [
|
||||
.init(fileName: "01_orbital-route.png", label: "Orbital Route", draw: drawOrbitalRoute),
|
||||
.init(fileName: "02_window-badge.png", label: "Window Badge", draw: drawWindowBadge),
|
||||
.init(fileName: "03_atlas-tile.png", label: "Atlas Tile", draw: drawAtlasTile),
|
||||
.init(fileName: "04_night-ring.png", label: "Night Ring", draw: drawNightRing),
|
||||
.init(fileName: "05_route-card.png", label: "Route Card", draw: drawRouteCard)
|
||||
]
|
||||
|
||||
let fileManager = FileManager.default
|
||||
let root = URL(fileURLWithPath: fileManager.currentDirectoryPath)
|
||||
let outputDirectory = root.appendingPathComponent("design/icon-options", isDirectory: true)
|
||||
try fileManager.createDirectory(at: outputDirectory, withIntermediateDirectories: true)
|
||||
|
||||
for option in options {
|
||||
let rep = renderBitmap(draw: option.draw)
|
||||
try savePNG(rep, to: outputDirectory.appendingPathComponent(option.fileName))
|
||||
}
|
||||
|
||||
try drawComparisonSheet(
|
||||
options: options,
|
||||
imageDirectory: outputDirectory,
|
||||
outputURL: outputDirectory.appendingPathComponent("comparison-sheet.png")
|
||||
)
|
||||
|
||||
print("Generated \(options.count) icons in \(outputDirectory.path)")
|
||||
BIN
design/icon-options/01_orbital-route.png
Normal file
BIN
design/icon-options/01_orbital-route.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 929 KiB |
BIN
design/icon-options/02_window-badge.png
Normal file
BIN
design/icon-options/02_window-badge.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 920 KiB |
BIN
design/icon-options/03_atlas-tile.png
Normal file
BIN
design/icon-options/03_atlas-tile.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 613 KiB |
BIN
design/icon-options/04_night-ring.png
Normal file
BIN
design/icon-options/04_night-ring.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 934 KiB |
BIN
design/icon-options/05_route-card.png
Normal file
BIN
design/icon-options/05_route-card.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 674 KiB |
14
design/icon-options/README.md
Normal file
14
design/icon-options/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
Generated app icon concepts for `Flights`.
|
||||
|
||||
Files:
|
||||
- `01_orbital-route.png`: dark, route-led, strongest "fly everywhere" story.
|
||||
- `02_window-badge.png`: bright and premium, centered globe with orbital motion.
|
||||
- `03_atlas-tile.png`: editorial/lightweight, dark inset tile on an airy base.
|
||||
- `04_night-ring.png`: bold and futuristic, orbit-ring treatment on a dark field.
|
||||
- `05_route-card.png`: clean iOS card language using the app's UI motif.
|
||||
- `comparison-sheet.png`: all five options on one board.
|
||||
|
||||
Regenerate:
|
||||
```sh
|
||||
swift -module-cache-path /tmp/swift-module-cache design/generate_icon_options.swift
|
||||
```
|
||||
BIN
design/icon-options/comparison-sheet.png
Normal file
BIN
design/icon-options/comparison-sheet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
Reference in New Issue
Block a user