Add iPad support, auto-pinning, and comprehensive logging
- Adaptive iPhone/iPad layout with NavigationSplitView sidebar - Auto-detect SSL-pinned domains, fall back to passthrough - Certificate install via local HTTP server (Safari profile flow) - App Group-backed CA, per-domain leaf cert LRU cache - DB-backed config repository, Darwin notification throttling - Rules engine, breakpoint rules, pinned domain tracking - os.Logger instrumentation across tunnel/proxy/mitm/capture/cert/rules/db/ipc/ui - Fix dyld framework embed, race conditions, thread safety
This commit is contained in:
660
Scripts/generate_icon_concepts.swift
Normal file
660
Scripts/generate_icon_concepts.swift
Normal file
@@ -0,0 +1,660 @@
|
||||
import AppKit
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
|
||||
enum IconStyle: String {
|
||||
case flat = "Flat iOS-native"
|
||||
case glossy = "Glossy iOS 26"
|
||||
}
|
||||
|
||||
enum Concept: CaseIterable {
|
||||
case routeLens
|
||||
case splitLock
|
||||
case packetPrism
|
||||
case tunnelPulse
|
||||
case stackedRequests
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .routeLens: return "Route Lens"
|
||||
case .splitLock: return "Split Lock"
|
||||
case .packetPrism: return "Packet Prism"
|
||||
case .tunnelPulse: return "Tunnel Pulse"
|
||||
case .stackedRequests: return "Stacked Requests"
|
||||
}
|
||||
}
|
||||
|
||||
var summary: String {
|
||||
switch self {
|
||||
case .routeLens:
|
||||
return "Inspect a live network path through a focused lens."
|
||||
case .splitLock:
|
||||
return "Show secure traffic opening into readable traces."
|
||||
case .packetPrism:
|
||||
return "Decode one incoming packet into multiple visible layers."
|
||||
case .tunnelPulse:
|
||||
return "Represent proxy transit as a bright pulse through a tunnel."
|
||||
case .stackedRequests:
|
||||
return "Frame the app as a polished request browser."
|
||||
}
|
||||
}
|
||||
|
||||
var notes: [String] {
|
||||
switch self {
|
||||
case .routeLens:
|
||||
return [
|
||||
"Best default option for broad appeal.",
|
||||
"Reads clearly at small sizes.",
|
||||
"Feels more like inspection than blocking."
|
||||
]
|
||||
case .splitLock:
|
||||
return [
|
||||
"Most explicit HTTPS inspection metaphor.",
|
||||
"Feels technical without looking hostile.",
|
||||
"Strong choice if SSL proxying is central to the brand."
|
||||
]
|
||||
case .packetPrism:
|
||||
return [
|
||||
"Highest-end and most design-forward direction.",
|
||||
"Communicates transform and decode cleanly.",
|
||||
"Best fit for a premium developer tool."
|
||||
]
|
||||
case .tunnelPulse:
|
||||
return [
|
||||
"Least literal and most atmospheric option.",
|
||||
"Feels modern, fast, and infrastructural.",
|
||||
"Works well if you want less obvious symbolism."
|
||||
]
|
||||
case .stackedRequests:
|
||||
return [
|
||||
"Most product-led and browser-like.",
|
||||
"Hints at request rows and response status.",
|
||||
"Useful if the app should feel approachable."
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Page {
|
||||
static let size = CGSize(width: 900, height: 1200)
|
||||
}
|
||||
|
||||
let outputDirectory = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
|
||||
.appendingPathComponent("Design/icon_concepts", isDirectory: true)
|
||||
let pdfURL = outputDirectory.appendingPathComponent("proxy_icon_concepts.pdf")
|
||||
let pngURL = outputDirectory.appendingPathComponent("proxy_icon_concepts_preview.png")
|
||||
|
||||
try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true)
|
||||
|
||||
renderPDF(to: pdfURL)
|
||||
renderPreviewPNG(to: pngURL)
|
||||
|
||||
print("Generated:")
|
||||
print(pdfURL.path)
|
||||
print(pngURL.path)
|
||||
|
||||
func renderPDF(to url: URL) {
|
||||
var mediaBox = CGRect(origin: .zero, size: Page.size)
|
||||
guard let context = CGContext(url as CFURL, mediaBox: &mediaBox, nil) else {
|
||||
fatalError("Unable to create PDF context")
|
||||
}
|
||||
|
||||
beginPDFPage(context, mediaBox) {
|
||||
drawCoverPage(in: mediaBox)
|
||||
}
|
||||
|
||||
for concept in Concept.allCases {
|
||||
beginPDFPage(context, mediaBox) {
|
||||
drawDetailPage(for: concept, in: mediaBox)
|
||||
}
|
||||
}
|
||||
|
||||
context.closePDF()
|
||||
}
|
||||
|
||||
func renderPreviewPNG(to url: URL) {
|
||||
let previewSize = CGSize(width: 1500, height: 2000)
|
||||
let image = NSImage(size: previewSize)
|
||||
image.lockFocus()
|
||||
NSGraphicsContext.current?.imageInterpolation = .high
|
||||
let context = NSGraphicsContext.current?.cgContext
|
||||
let scale = previewSize.width / Page.size.width
|
||||
context?.saveGState()
|
||||
context?.scaleBy(x: scale, y: scale)
|
||||
drawCoverPage(in: CGRect(origin: .zero, size: Page.size))
|
||||
context?.restoreGState()
|
||||
image.unlockFocus()
|
||||
|
||||
guard let tiff = image.tiffRepresentation,
|
||||
let rep = NSBitmapImageRep(data: tiff),
|
||||
let png = rep.representation(using: .png, properties: [:]) else {
|
||||
fatalError("Unable to create PNG preview")
|
||||
}
|
||||
|
||||
try? png.write(to: url)
|
||||
}
|
||||
|
||||
func beginPDFPage(_ context: CGContext, _ mediaBox: CGRect, draw: () -> Void) {
|
||||
context.beginPDFPage(nil)
|
||||
let graphicsContext = NSGraphicsContext(cgContext: context, flipped: false)
|
||||
NSGraphicsContext.saveGraphicsState()
|
||||
NSGraphicsContext.current = graphicsContext
|
||||
draw()
|
||||
NSGraphicsContext.restoreGraphicsState()
|
||||
context.endPDFPage()
|
||||
}
|
||||
|
||||
func drawCoverPage(in rect: CGRect) {
|
||||
fill(rect, color: NSColor(hex: 0xF4F6FB))
|
||||
|
||||
drawText(
|
||||
"Proxy App Icon Concepts",
|
||||
in: CGRect(x: 70, y: rect.height - 120, width: rect.width - 140, height: 44),
|
||||
font: .systemFont(ofSize: 32, weight: .bold),
|
||||
color: NSColor(hex: 0x101828)
|
||||
)
|
||||
drawText(
|
||||
"Five directions, each tuned for an iOS-native flat pass and a layered glossy pass.",
|
||||
in: CGRect(x: 70, y: rect.height - 165, width: rect.width - 140, height: 28),
|
||||
font: .systemFont(ofSize: 16, weight: .medium),
|
||||
color: NSColor(hex: 0x475467)
|
||||
)
|
||||
|
||||
let cellWidth = (rect.width - 70 * 2 - 28) / 2
|
||||
let cellHeight: CGFloat = 300
|
||||
let rows: [(Concept, CGRect)] = [
|
||||
(.routeLens, CGRect(x: 70, y: 760, width: cellWidth, height: cellHeight)),
|
||||
(.splitLock, CGRect(x: 70 + cellWidth + 28, y: 760, width: cellWidth, height: cellHeight)),
|
||||
(.packetPrism, CGRect(x: 70, y: 430, width: cellWidth, height: cellHeight)),
|
||||
(.tunnelPulse, CGRect(x: 70 + cellWidth + 28, y: 430, width: cellWidth, height: cellHeight)),
|
||||
(.stackedRequests, CGRect(x: 70, y: 100, width: cellWidth, height: cellHeight))
|
||||
]
|
||||
|
||||
for (concept, frame) in rows {
|
||||
drawCoverCard(for: concept, in: frame)
|
||||
}
|
||||
|
||||
drawSelectionCard(
|
||||
in: CGRect(x: 70 + cellWidth + 28, y: 100, width: cellWidth, height: cellHeight),
|
||||
accent: NSColor(hex: 0x4F86FF)
|
||||
)
|
||||
}
|
||||
|
||||
func drawDetailPage(for concept: Concept, in rect: CGRect) {
|
||||
fill(rect, color: NSColor(hex: 0xF7F8FC))
|
||||
|
||||
drawText(
|
||||
concept.title,
|
||||
in: CGRect(x: 72, y: rect.height - 120, width: rect.width - 144, height: 40),
|
||||
font: .systemFont(ofSize: 34, weight: .bold),
|
||||
color: NSColor(hex: 0x101828)
|
||||
)
|
||||
drawText(
|
||||
concept.summary,
|
||||
in: CGRect(x: 72, y: rect.height - 162, width: rect.width - 144, height: 26),
|
||||
font: .systemFont(ofSize: 17, weight: .medium),
|
||||
color: NSColor(hex: 0x475467)
|
||||
)
|
||||
|
||||
let cardWidth = (rect.width - 72 * 2 - 30) / 2
|
||||
let cardHeight: CGFloat = 520
|
||||
let y: CGFloat = 430
|
||||
|
||||
drawVariantCard(
|
||||
title: IconStyle.flat.rawValue,
|
||||
subtitle: "Crisp, simplified, and system-friendly.",
|
||||
concept: concept,
|
||||
style: .flat,
|
||||
frame: CGRect(x: 72, y: y, width: cardWidth, height: cardHeight)
|
||||
)
|
||||
|
||||
drawVariantCard(
|
||||
title: IconStyle.glossy.rawValue,
|
||||
subtitle: "Layered, luminous, and ready for Liquid Glass.",
|
||||
concept: concept,
|
||||
style: .glossy,
|
||||
frame: CGRect(x: 72 + cardWidth + 30, y: y, width: cardWidth, height: cardHeight)
|
||||
)
|
||||
|
||||
drawNotesCard(
|
||||
notes: concept.notes,
|
||||
frame: CGRect(x: 72, y: 88, width: rect.width - 144, height: 250)
|
||||
)
|
||||
}
|
||||
|
||||
func drawCoverCard(for concept: Concept, in frame: CGRect) {
|
||||
drawSurface(frame, fill: NSColor.white, stroke: NSColor(hex: 0xD8DEEA))
|
||||
|
||||
let iconRect = CGRect(x: frame.minX + 24, y: frame.minY + 72, width: 156, height: 156)
|
||||
drawIcon(concept: concept, style: .glossy, in: iconRect)
|
||||
|
||||
drawText(
|
||||
concept.title,
|
||||
in: CGRect(x: frame.minX + 204, y: frame.maxY - 70, width: frame.width - 224, height: 30),
|
||||
font: .systemFont(ofSize: 22, weight: .semibold),
|
||||
color: NSColor(hex: 0x111827)
|
||||
)
|
||||
drawText(
|
||||
concept.summary,
|
||||
in: CGRect(x: frame.minX + 204, y: frame.maxY - 132, width: frame.width - 228, height: 64),
|
||||
font: .systemFont(ofSize: 14, weight: .medium),
|
||||
color: NSColor(hex: 0x475467)
|
||||
)
|
||||
|
||||
drawPill("Glossy preview", in: CGRect(x: frame.minX + 204, y: frame.minY + 28, width: 110, height: 28), fill: NSColor(hex: 0xEAF1FF), textColor: NSColor(hex: 0x2457D6))
|
||||
}
|
||||
|
||||
func drawSelectionCard(in frame: CGRect, accent: NSColor) {
|
||||
drawSurface(frame, fill: NSColor(hex: 0x101828), stroke: accent.withAlphaComponent(0.3))
|
||||
|
||||
drawText(
|
||||
"Pick One",
|
||||
in: CGRect(x: frame.minX + 26, y: frame.maxY - 76, width: frame.width - 52, height: 28),
|
||||
font: .systemFont(ofSize: 24, weight: .bold),
|
||||
color: .white
|
||||
)
|
||||
drawText(
|
||||
"Choose the direction with the strongest metaphor. I can then iterate on color, shape weight, gloss, and app-store readability.",
|
||||
in: CGRect(x: frame.minX + 26, y: frame.maxY - 154, width: frame.width - 52, height: 84),
|
||||
font: .systemFont(ofSize: 15, weight: .medium),
|
||||
color: NSColor.white.withAlphaComponent(0.84)
|
||||
)
|
||||
|
||||
let checklist = [
|
||||
"Best all-around: Route Lens",
|
||||
"Best technical: Split Lock",
|
||||
"Best premium: Packet Prism"
|
||||
]
|
||||
|
||||
for (index, item) in checklist.enumerated() {
|
||||
let rowY = frame.minY + 112 - CGFloat(index) * 34
|
||||
let dotRect = CGRect(x: frame.minX + 28, y: rowY + 6, width: 10, height: 10)
|
||||
fillEllipse(dotRect, color: accent)
|
||||
drawText(
|
||||
item,
|
||||
in: CGRect(x: frame.minX + 48, y: rowY, width: frame.width - 72, height: 20),
|
||||
font: .systemFont(ofSize: 15, weight: .semibold),
|
||||
color: .white
|
||||
)
|
||||
}
|
||||
|
||||
drawPill("PDF pages include flat + glossy", in: CGRect(x: frame.minX + 26, y: frame.minY + 24, width: 200, height: 30), fill: accent.withAlphaComponent(0.18), textColor: .white)
|
||||
}
|
||||
|
||||
func drawVariantCard(title: String, subtitle: String, concept: Concept, style: IconStyle, frame: CGRect) {
|
||||
drawSurface(frame, fill: NSColor.white, stroke: NSColor(hex: 0xD8DEEA))
|
||||
|
||||
drawText(
|
||||
title,
|
||||
in: CGRect(x: frame.minX + 28, y: frame.maxY - 54, width: frame.width - 56, height: 26),
|
||||
font: .systemFont(ofSize: 22, weight: .bold),
|
||||
color: NSColor(hex: 0x111827)
|
||||
)
|
||||
drawText(
|
||||
subtitle,
|
||||
in: CGRect(x: frame.minX + 28, y: frame.maxY - 82, width: frame.width - 56, height: 22),
|
||||
font: .systemFont(ofSize: 14, weight: .medium),
|
||||
color: NSColor(hex: 0x667085)
|
||||
)
|
||||
|
||||
let iconRect = CGRect(
|
||||
x: frame.midX - 132,
|
||||
y: frame.midY - 40,
|
||||
width: 264,
|
||||
height: 264
|
||||
)
|
||||
drawIcon(concept: concept, style: style, in: iconRect)
|
||||
|
||||
let note = style == .flat
|
||||
? "Keep the idea direct, centered, and clean."
|
||||
: "Preserve the same concept but express it with layered depth."
|
||||
drawText(
|
||||
note,
|
||||
in: CGRect(x: frame.minX + 28, y: frame.minY + 40, width: frame.width - 56, height: 40),
|
||||
font: .systemFont(ofSize: 15, weight: .medium),
|
||||
color: NSColor(hex: 0x475467)
|
||||
)
|
||||
}
|
||||
|
||||
func drawNotesCard(notes: [String], frame: CGRect) {
|
||||
drawSurface(frame, fill: NSColor.white, stroke: NSColor(hex: 0xD8DEEA))
|
||||
|
||||
drawText(
|
||||
"What To Evaluate",
|
||||
in: CGRect(x: frame.minX + 26, y: frame.maxY - 46, width: frame.width - 52, height: 24),
|
||||
font: .systemFont(ofSize: 22, weight: .bold),
|
||||
color: NSColor(hex: 0x111827)
|
||||
)
|
||||
|
||||
for (index, note) in notes.enumerated() {
|
||||
let rowY = frame.maxY - 92 - CGFloat(index) * 46
|
||||
fillEllipse(CGRect(x: frame.minX + 30, y: rowY + 8, width: 10, height: 10), color: NSColor(hex: 0x4F86FF))
|
||||
drawText(
|
||||
note,
|
||||
in: CGRect(x: frame.minX + 52, y: rowY, width: frame.width - 82, height: 30),
|
||||
font: .systemFont(ofSize: 16, weight: .medium),
|
||||
color: NSColor(hex: 0x475467)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func drawIcon(concept: Concept, style: IconStyle, in rect: CGRect) {
|
||||
drawIconBackground(style: style, in: rect)
|
||||
|
||||
switch concept {
|
||||
case .routeLens:
|
||||
drawRouteLens(style: style, in: rect.insetBy(dx: 18, dy: 18))
|
||||
case .splitLock:
|
||||
drawSplitLock(style: style, in: rect.insetBy(dx: 18, dy: 18))
|
||||
case .packetPrism:
|
||||
drawPacketPrism(style: style, in: rect.insetBy(dx: 18, dy: 18))
|
||||
case .tunnelPulse:
|
||||
drawTunnelPulse(style: style, in: rect.insetBy(dx: 18, dy: 18))
|
||||
case .stackedRequests:
|
||||
drawStackedRequests(style: style, in: rect.insetBy(dx: 18, dy: 18))
|
||||
}
|
||||
}
|
||||
|
||||
func drawIconBackground(style: IconStyle, in rect: CGRect) {
|
||||
let path = NSBezierPath(roundedRect: rect, xRadius: rect.width * 0.23, yRadius: rect.height * 0.23)
|
||||
|
||||
switch style {
|
||||
case .flat:
|
||||
let gradient = NSGradient(colors: [
|
||||
NSColor(hex: 0xFAFBFF),
|
||||
NSColor(hex: 0xEEF3FF)
|
||||
])
|
||||
gradient?.draw(in: path, angle: 90)
|
||||
NSColor.white.withAlphaComponent(0.55).setStroke()
|
||||
path.lineWidth = 1.5
|
||||
path.stroke()
|
||||
case .glossy:
|
||||
let gradient = NSGradient(colors: [
|
||||
NSColor(hex: 0xF4F8FF),
|
||||
NSColor(hex: 0xDCE8FF),
|
||||
NSColor(hex: 0xC6D9FF)
|
||||
])
|
||||
gradient?.draw(in: path, angle: 90)
|
||||
|
||||
let topHighlight = NSBezierPath(roundedRect: rect.insetBy(dx: 18, dy: 18).offsetBy(dx: 0, dy: 24), xRadius: 40, yRadius: 40)
|
||||
NSColor.white.withAlphaComponent(0.28).setFill()
|
||||
topHighlight.fill()
|
||||
|
||||
NSColor.white.withAlphaComponent(0.7).setStroke()
|
||||
path.lineWidth = 1.7
|
||||
path.stroke()
|
||||
}
|
||||
}
|
||||
|
||||
func drawRouteLens(style: IconStyle, in rect: CGRect) {
|
||||
let line = NSBezierPath()
|
||||
line.move(to: CGPoint(x: rect.minX + 12, y: rect.midY - 24))
|
||||
line.curve(
|
||||
to: CGPoint(x: rect.maxX - 8, y: rect.midY + 12),
|
||||
controlPoint1: CGPoint(x: rect.minX + 56, y: rect.maxY - 8),
|
||||
controlPoint2: CGPoint(x: rect.maxX - 92, y: rect.minY + 30)
|
||||
)
|
||||
line.lineWidth = style == .flat ? 12 : 14
|
||||
line.lineCapStyle = .round
|
||||
NSColor(hex: style == .flat ? 0xA8C4FF : 0x85B5FF).setStroke()
|
||||
line.stroke()
|
||||
|
||||
let lensRect = CGRect(x: rect.midX - 44, y: rect.midY - 12, width: 88, height: 88)
|
||||
let lens = NSBezierPath(ovalIn: lensRect)
|
||||
if style == .flat {
|
||||
NSColor.white.setFill()
|
||||
lens.fill()
|
||||
NSColor(hex: 0x2764EA).setStroke()
|
||||
} else {
|
||||
let gradient = NSGradient(colors: [
|
||||
NSColor.white.withAlphaComponent(0.92),
|
||||
NSColor(hex: 0xEAF2FF).withAlphaComponent(0.92)
|
||||
])
|
||||
gradient?.draw(in: lens, angle: 90)
|
||||
NSColor(hex: 0x3F7CFF).setStroke()
|
||||
}
|
||||
lens.lineWidth = style == .flat ? 12 : 10
|
||||
lens.stroke()
|
||||
|
||||
let handle = NSBezierPath()
|
||||
handle.move(to: CGPoint(x: lensRect.maxX - 4, y: lensRect.minY + 8))
|
||||
handle.line(to: CGPoint(x: rect.maxX - 24, y: rect.minY + 22))
|
||||
handle.lineWidth = style == .flat ? 14 : 16
|
||||
handle.lineCapStyle = .round
|
||||
NSColor(hex: 0x235BDA).setStroke()
|
||||
handle.stroke()
|
||||
|
||||
fillEllipse(CGRect(x: rect.midX - 10, y: rect.midY + 14, width: 20, height: 20), color: NSColor(hex: 0x3AD4FF))
|
||||
}
|
||||
|
||||
func drawSplitLock(style: IconStyle, in rect: CGRect) {
|
||||
let bodyRect = CGRect(x: rect.midX - 66, y: rect.midY - 34, width: 132, height: 104)
|
||||
let body = NSBezierPath(roundedRect: bodyRect, xRadius: 28, yRadius: 28)
|
||||
|
||||
if style == .flat {
|
||||
NSColor(hex: 0xF7FBFF).setFill()
|
||||
body.fill()
|
||||
NSColor(hex: 0x0F172A).setStroke()
|
||||
body.lineWidth = 7
|
||||
body.stroke()
|
||||
} else {
|
||||
let gradient = NSGradient(colors: [
|
||||
NSColor.white.withAlphaComponent(0.92),
|
||||
NSColor(hex: 0xDCE8FF).withAlphaComponent(0.9)
|
||||
])
|
||||
gradient?.draw(in: body, angle: 90)
|
||||
NSColor.white.withAlphaComponent(0.88).setStroke()
|
||||
body.lineWidth = 6
|
||||
body.stroke()
|
||||
}
|
||||
|
||||
let shackle = NSBezierPath()
|
||||
shackle.move(to: CGPoint(x: rect.midX - 42, y: bodyRect.maxY - 2))
|
||||
shackle.curve(
|
||||
to: CGPoint(x: rect.midX + 14, y: bodyRect.maxY + 52),
|
||||
controlPoint1: CGPoint(x: rect.midX - 44, y: bodyRect.maxY + 54),
|
||||
controlPoint2: CGPoint(x: rect.midX + 14, y: bodyRect.maxY + 54)
|
||||
)
|
||||
shackle.lineWidth = 16
|
||||
shackle.lineCapStyle = .round
|
||||
NSColor(hex: style == .flat ? 0x0F172A : 0xF8FAFC).setStroke()
|
||||
shackle.stroke()
|
||||
|
||||
let splitMask = NSBezierPath(rect: CGRect(x: rect.midX - 2, y: bodyRect.minY - 10, width: bodyRect.width / 2 + 18, height: bodyRect.height + 96))
|
||||
NSGraphicsContext.saveGraphicsState()
|
||||
splitMask.addClip()
|
||||
let leftFill = NSBezierPath(roundedRect: bodyRect, xRadius: 28, yRadius: 28)
|
||||
NSColor(hex: style == .flat ? 0x2B6CF3 : 0xA6CAFF).setFill()
|
||||
leftFill.fill()
|
||||
NSGraphicsContext.restoreGraphicsState()
|
||||
|
||||
let traces = [
|
||||
(CGPoint(x: rect.midX + 8, y: rect.midY + 20), CGPoint(x: rect.maxX - 18, y: rect.midY + 44)),
|
||||
(CGPoint(x: rect.midX + 14, y: rect.midY), CGPoint(x: rect.maxX - 10, y: rect.midY + 4)),
|
||||
(CGPoint(x: rect.midX + 10, y: rect.midY - 22), CGPoint(x: rect.maxX - 24, y: rect.midY - 38))
|
||||
]
|
||||
|
||||
for (start, end) in traces {
|
||||
let trace = NSBezierPath()
|
||||
trace.move(to: start)
|
||||
trace.curve(
|
||||
to: end,
|
||||
controlPoint1: CGPoint(x: start.x + 28, y: start.y),
|
||||
controlPoint2: CGPoint(x: end.x - 22, y: end.y)
|
||||
)
|
||||
trace.lineWidth = 7
|
||||
trace.lineCapStyle = .round
|
||||
NSColor(hex: 0x56A7FF).setStroke()
|
||||
trace.stroke()
|
||||
fillEllipse(CGRect(x: end.x - 6, y: end.y - 6, width: 12, height: 12), color: NSColor(hex: 0x56A7FF))
|
||||
}
|
||||
}
|
||||
|
||||
func drawPacketPrism(style: IconStyle, in rect: CGRect) {
|
||||
let prism = NSBezierPath()
|
||||
prism.move(to: CGPoint(x: rect.midX - 18, y: rect.midY + 52))
|
||||
prism.line(to: CGPoint(x: rect.midX + 52, y: rect.midY))
|
||||
prism.line(to: CGPoint(x: rect.midX - 18, y: rect.midY - 52))
|
||||
prism.close()
|
||||
|
||||
if style == .flat {
|
||||
NSColor(hex: 0xEFF5FF).setFill()
|
||||
prism.fill()
|
||||
NSColor(hex: 0x2563EB).setStroke()
|
||||
prism.lineWidth = 7
|
||||
prism.stroke()
|
||||
} else {
|
||||
let gradient = NSGradient(colors: [
|
||||
NSColor.white.withAlphaComponent(0.92),
|
||||
NSColor(hex: 0xD7E6FF).withAlphaComponent(0.88)
|
||||
])
|
||||
gradient?.draw(in: prism, angle: 90)
|
||||
NSColor.white.withAlphaComponent(0.9).setStroke()
|
||||
prism.lineWidth = 6
|
||||
prism.stroke()
|
||||
}
|
||||
|
||||
let incoming = NSBezierPath()
|
||||
incoming.move(to: CGPoint(x: rect.minX + 18, y: rect.midY))
|
||||
incoming.line(to: CGPoint(x: rect.midX - 18, y: rect.midY))
|
||||
incoming.lineWidth = 10
|
||||
incoming.lineCapStyle = .round
|
||||
NSColor(hex: 0x2D6BFF).setStroke()
|
||||
incoming.stroke()
|
||||
fillEllipse(CGRect(x: rect.minX + 8, y: rect.midY - 10, width: 20, height: 20), color: NSColor(hex: 0x2D6BFF))
|
||||
|
||||
let strands: [(CGFloat, Int)] = [(34, 0x2D6BFF), (0, 0x2ED1FF), (-34, 0x46C57A)]
|
||||
for (offset, colorHex) in strands {
|
||||
let strand = NSBezierPath()
|
||||
strand.move(to: CGPoint(x: rect.midX + 42, y: rect.midY + offset * 0.25))
|
||||
strand.curve(
|
||||
to: CGPoint(x: rect.maxX - 18, y: rect.midY + offset),
|
||||
controlPoint1: CGPoint(x: rect.midX + 72, y: rect.midY + offset * 0.28),
|
||||
controlPoint2: CGPoint(x: rect.maxX - 44, y: rect.midY + offset)
|
||||
)
|
||||
strand.lineWidth = 9
|
||||
strand.lineCapStyle = .round
|
||||
NSColor(hex: colorHex).setStroke()
|
||||
strand.stroke()
|
||||
}
|
||||
}
|
||||
|
||||
func drawTunnelPulse(style: IconStyle, in rect: CGRect) {
|
||||
let outerArc = NSBezierPath()
|
||||
outerArc.appendArc(
|
||||
withCenter: CGPoint(x: rect.midX, y: rect.midY - 4),
|
||||
radius: rect.width * 0.34,
|
||||
startAngle: 208,
|
||||
endAngle: -28,
|
||||
clockwise: false
|
||||
)
|
||||
outerArc.lineWidth = style == .flat ? 26 : 30
|
||||
outerArc.lineCapStyle = .round
|
||||
NSColor(hex: style == .flat ? 0x0F305F : 0x1C3F7C).setStroke()
|
||||
outerArc.stroke()
|
||||
|
||||
let innerArc = NSBezierPath()
|
||||
innerArc.appendArc(
|
||||
withCenter: CGPoint(x: rect.midX, y: rect.midY - 4),
|
||||
radius: rect.width * 0.22,
|
||||
startAngle: 212,
|
||||
endAngle: -32,
|
||||
clockwise: false
|
||||
)
|
||||
innerArc.lineWidth = style == .flat ? 16 : 18
|
||||
innerArc.lineCapStyle = .round
|
||||
NSColor(hex: style == .flat ? 0x55A9FF : 0x7DCAFF).setStroke()
|
||||
innerArc.stroke()
|
||||
|
||||
let pulseRect = CGRect(x: rect.midX - 24, y: rect.midY - 24, width: 48, height: 48)
|
||||
if style == .glossy {
|
||||
let halo = NSBezierPath(ovalIn: pulseRect.insetBy(dx: -18, dy: -18))
|
||||
NSColor(hex: 0x6DEBFF).withAlphaComponent(0.18).setFill()
|
||||
halo.fill()
|
||||
}
|
||||
fillEllipse(pulseRect, color: NSColor(hex: style == .flat ? 0x19D7FF : 0x56F2FF))
|
||||
fillEllipse(pulseRect.insetBy(dx: 12, dy: 12), color: NSColor.white.withAlphaComponent(0.92))
|
||||
}
|
||||
|
||||
func drawStackedRequests(style: IconStyle, in rect: CGRect) {
|
||||
let back = CGRect(x: rect.midX - 76, y: rect.midY + 18, width: 132, height: 90)
|
||||
let middle = CGRect(x: rect.midX - 94, y: rect.midY - 10, width: 148, height: 100)
|
||||
let front = CGRect(x: rect.midX - 112, y: rect.midY - 46, width: 164, height: 116)
|
||||
|
||||
drawMiniCard(back, fill: style == .flat ? NSColor(hex: 0xE6EEFf) : NSColor.white.withAlphaComponent(0.48))
|
||||
drawMiniCard(middle, fill: style == .flat ? NSColor(hex: 0xF5F8FF) : NSColor.white.withAlphaComponent(0.70))
|
||||
drawMiniCard(front, fill: style == .flat ? NSColor.white : NSColor.white.withAlphaComponent(0.88))
|
||||
|
||||
drawPill("GET", in: CGRect(x: front.minX + 18, y: front.maxY - 34, width: 40, height: 22), fill: NSColor(hex: 0xDBF3E4), textColor: NSColor(hex: 0x1E8C4A))
|
||||
drawPill("200", in: CGRect(x: front.minX + 64, y: front.maxY - 34, width: 44, height: 22), fill: NSColor(hex: 0xE6EEFF), textColor: NSColor(hex: 0x275FD7))
|
||||
drawText("api/profile", in: CGRect(x: front.minX + 18, y: front.maxY - 64, width: 110, height: 18), font: .systemFont(ofSize: 11, weight: .semibold), color: NSColor(hex: 0x111827))
|
||||
fill(CGRect(x: front.minX + 18, y: front.maxY - 78, width: 120, height: 4), color: NSColor(hex: 0x4F86FF))
|
||||
fill(CGRect(x: front.minX + 18, y: front.maxY - 92, width: 96, height: 4), color: NSColor(hex: 0xD0D9E8))
|
||||
fill(CGRect(x: front.minX + 18, y: front.maxY - 104, width: 78, height: 4), color: NSColor(hex: 0xD0D9E8))
|
||||
}
|
||||
|
||||
func drawMiniCard(_ rect: CGRect, fill color: NSColor) {
|
||||
let path = NSBezierPath(roundedRect: rect, xRadius: 22, yRadius: 22)
|
||||
color.setFill()
|
||||
path.fill()
|
||||
NSColor(hex: 0xD6DDEA).withAlphaComponent(0.75).setStroke()
|
||||
path.lineWidth = 1
|
||||
path.stroke()
|
||||
}
|
||||
|
||||
func drawSurface(_ rect: CGRect, fill: NSColor, stroke: NSColor) {
|
||||
let shadow = NSShadow()
|
||||
shadow.shadowColor = NSColor.black.withAlphaComponent(0.08)
|
||||
shadow.shadowOffset = CGSize(width: 0, height: -6)
|
||||
shadow.shadowBlurRadius = 18
|
||||
|
||||
NSGraphicsContext.saveGraphicsState()
|
||||
shadow.set()
|
||||
let path = NSBezierPath(roundedRect: rect, xRadius: 26, yRadius: 26)
|
||||
fill.setFill()
|
||||
path.fill()
|
||||
NSGraphicsContext.restoreGraphicsState()
|
||||
|
||||
let strokePath = NSBezierPath(roundedRect: rect, xRadius: 26, yRadius: 26)
|
||||
stroke.setStroke()
|
||||
strokePath.lineWidth = 1.2
|
||||
strokePath.stroke()
|
||||
}
|
||||
|
||||
func drawPill(_ title: String, in rect: CGRect, fill: NSColor, textColor: NSColor) {
|
||||
let pill = NSBezierPath(roundedRect: rect, xRadius: rect.height / 2, yRadius: rect.height / 2)
|
||||
fill.setFill()
|
||||
pill.fill()
|
||||
drawText(title, in: rect.offsetBy(dx: 0, dy: 2), font: .systemFont(ofSize: 11, weight: .bold), color: textColor, alignment: .center)
|
||||
}
|
||||
|
||||
func drawText(_ text: String, in rect: CGRect, font: NSFont, color: NSColor, alignment: NSTextAlignment = .left) {
|
||||
let paragraph = NSMutableParagraphStyle()
|
||||
paragraph.alignment = alignment
|
||||
paragraph.lineBreakMode = .byWordWrapping
|
||||
let attributes: [NSAttributedString.Key: Any] = [
|
||||
.font: font,
|
||||
.foregroundColor: color,
|
||||
.paragraphStyle: paragraph
|
||||
]
|
||||
NSString(string: text).draw(with: rect, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: attributes)
|
||||
}
|
||||
|
||||
func fill(_ rect: CGRect, color: NSColor) {
|
||||
color.setFill()
|
||||
NSBezierPath(rect: rect).fill()
|
||||
}
|
||||
|
||||
func fillEllipse(_ rect: CGRect, color: NSColor) {
|
||||
color.setFill()
|
||||
NSBezierPath(ovalIn: rect).fill()
|
||||
}
|
||||
|
||||
extension NSColor {
|
||||
convenience init(hex: Int, alpha: CGFloat = 1.0) {
|
||||
let red = CGFloat((hex >> 16) & 0xFF) / 255.0
|
||||
let green = CGFloat((hex >> 8) & 0xFF) / 255.0
|
||||
let blue = CGFloat(hex & 0xFF) / 255.0
|
||||
self.init(calibratedRed: red, green: green, blue: blue, alpha: alpha)
|
||||
}
|
||||
}
|
||||
538
Scripts/generate_split_lock_variants.swift
Normal file
538
Scripts/generate_split_lock_variants.swift
Normal file
@@ -0,0 +1,538 @@
|
||||
import AppKit
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
|
||||
enum SplitLockVariant: CaseIterable {
|
||||
case balancedGate
|
||||
case glassCore
|
||||
case bracketTrace
|
||||
case shieldLock
|
||||
case signalKeyhole
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .balancedGate: return "Balanced Gate"
|
||||
case .glassCore: return "Glass Core"
|
||||
case .bracketTrace: return "Bracket Trace"
|
||||
case .shieldLock: return "Shield Lock"
|
||||
case .signalKeyhole: return "Signal Keyhole"
|
||||
}
|
||||
}
|
||||
|
||||
var summary: String {
|
||||
switch self {
|
||||
case .balancedGate:
|
||||
return "Closest to the original idea. Balanced lock and readable traces."
|
||||
case .glassCore:
|
||||
return "More premium and layered. Feels the most iOS 26-native."
|
||||
case .bracketTrace:
|
||||
return "Leans developer-tool. The right half reads closer to code."
|
||||
case .shieldLock:
|
||||
return "Feels safer and more consumer-trust oriented than technical."
|
||||
case .signalKeyhole:
|
||||
return "Adds a pulse/keyhole center so inspection feels active, not static."
|
||||
}
|
||||
}
|
||||
|
||||
var accent: NSColor {
|
||||
switch self {
|
||||
case .balancedGate: return NSColor(hex: 0x4F86FF)
|
||||
case .glassCore: return NSColor(hex: 0x67B8FF)
|
||||
case .bracketTrace: return NSColor(hex: 0x5A9DFF)
|
||||
case .shieldLock: return NSColor(hex: 0x41A6FF)
|
||||
case .signalKeyhole: return NSColor(hex: 0x4DE3FF)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum LockStyle {
|
||||
case flat
|
||||
case glossy
|
||||
}
|
||||
|
||||
struct Page {
|
||||
static let size = CGSize(width: 960, height: 1400)
|
||||
}
|
||||
|
||||
let outputDirectory = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
|
||||
.appendingPathComponent("Design/icon_concepts", isDirectory: true)
|
||||
let pdfURL = outputDirectory.appendingPathComponent("split_lock_variants.pdf")
|
||||
let pngURL = outputDirectory.appendingPathComponent("split_lock_variants_preview.png")
|
||||
|
||||
try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true)
|
||||
|
||||
renderPDF(to: pdfURL)
|
||||
renderPreview(to: pngURL)
|
||||
|
||||
print("Generated:")
|
||||
print(pdfURL.path)
|
||||
print(pngURL.path)
|
||||
|
||||
func renderPDF(to url: URL) {
|
||||
var mediaBox = CGRect(origin: .zero, size: Page.size)
|
||||
guard let context = CGContext(url as CFURL, mediaBox: &mediaBox, nil) else {
|
||||
fatalError("Unable to create PDF context")
|
||||
}
|
||||
|
||||
beginPDFPage(context, mediaBox) {
|
||||
drawOverviewPage(in: mediaBox)
|
||||
}
|
||||
|
||||
beginPDFPage(context, mediaBox) {
|
||||
drawLargeIconsPage(in: mediaBox)
|
||||
}
|
||||
|
||||
context.closePDF()
|
||||
}
|
||||
|
||||
func renderPreview(to url: URL) {
|
||||
let previewSize = CGSize(width: 1500, height: 2188)
|
||||
let image = NSImage(size: previewSize)
|
||||
image.lockFocus()
|
||||
NSGraphicsContext.current?.imageInterpolation = .high
|
||||
let scale = previewSize.width / Page.size.width
|
||||
let context = NSGraphicsContext.current?.cgContext
|
||||
context?.saveGState()
|
||||
context?.scaleBy(x: scale, y: scale)
|
||||
drawOverviewPage(in: CGRect(origin: .zero, size: Page.size))
|
||||
context?.restoreGState()
|
||||
image.unlockFocus()
|
||||
|
||||
guard let tiff = image.tiffRepresentation,
|
||||
let rep = NSBitmapImageRep(data: tiff),
|
||||
let png = rep.representation(using: .png, properties: [:]) else {
|
||||
fatalError("Unable to create PNG preview")
|
||||
}
|
||||
|
||||
try? png.write(to: url)
|
||||
}
|
||||
|
||||
func beginPDFPage(_ context: CGContext, _ mediaBox: CGRect, draw: () -> Void) {
|
||||
context.beginPDFPage(nil)
|
||||
let graphicsContext = NSGraphicsContext(cgContext: context, flipped: false)
|
||||
NSGraphicsContext.saveGraphicsState()
|
||||
NSGraphicsContext.current = graphicsContext
|
||||
draw()
|
||||
NSGraphicsContext.restoreGraphicsState()
|
||||
context.endPDFPage()
|
||||
}
|
||||
|
||||
func drawOverviewPage(in rect: CGRect) {
|
||||
fill(rect, color: NSColor(hex: 0xF5F7FC))
|
||||
|
||||
drawText(
|
||||
"Split Lock Explorations",
|
||||
in: CGRect(x: 68, y: rect.height - 116, width: rect.width - 136, height: 40),
|
||||
font: .systemFont(ofSize: 34, weight: .bold),
|
||||
color: NSColor(hex: 0x0F172A)
|
||||
)
|
||||
drawText(
|
||||
"Five directions built from the Split Lock concept. Each card shows the glossy primary icon with a small flat simplification inset.",
|
||||
in: CGRect(x: 68, y: rect.height - 160, width: rect.width - 136, height: 34),
|
||||
font: .systemFont(ofSize: 16, weight: .medium),
|
||||
color: NSColor(hex: 0x475467)
|
||||
)
|
||||
|
||||
let cardWidth = (rect.width - 68 * 2 - 26) / 2
|
||||
let cardHeight: CGFloat = 300
|
||||
let layouts: [(SplitLockVariant, CGRect)] = [
|
||||
(.balancedGate, CGRect(x: 68, y: 900, width: cardWidth, height: cardHeight)),
|
||||
(.glassCore, CGRect(x: 68 + cardWidth + 26, y: 900, width: cardWidth, height: cardHeight)),
|
||||
(.bracketTrace, CGRect(x: 68, y: 574, width: cardWidth, height: cardHeight)),
|
||||
(.shieldLock, CGRect(x: 68 + cardWidth + 26, y: 574, width: cardWidth, height: cardHeight)),
|
||||
(.signalKeyhole, CGRect(x: 68, y: 248, width: cardWidth, height: cardHeight))
|
||||
]
|
||||
|
||||
for (variant, frame) in layouts {
|
||||
drawVariantCard(variant: variant, in: frame)
|
||||
}
|
||||
|
||||
drawDecisionCard(
|
||||
in: CGRect(x: 68 + cardWidth + 26, y: 248, width: cardWidth, height: cardHeight)
|
||||
)
|
||||
}
|
||||
|
||||
func drawLargeIconsPage(in rect: CGRect) {
|
||||
fill(rect, color: NSColor(hex: 0xF8FAFD))
|
||||
|
||||
drawText(
|
||||
"Large Icon Comparison",
|
||||
in: CGRect(x: 68, y: rect.height - 114, width: rect.width - 136, height: 38),
|
||||
font: .systemFont(ofSize: 32, weight: .bold),
|
||||
color: NSColor(hex: 0x0F172A)
|
||||
)
|
||||
drawText(
|
||||
"This page removes most of the annotation so you can judge silhouette, weight, and app-store readability.",
|
||||
in: CGRect(x: 68, y: rect.height - 156, width: rect.width - 136, height: 28),
|
||||
font: .systemFont(ofSize: 15, weight: .medium),
|
||||
color: NSColor(hex: 0x475467)
|
||||
)
|
||||
|
||||
let rowHeight: CGFloat = 220
|
||||
for (index, variant) in SplitLockVariant.allCases.enumerated() {
|
||||
let y = rect.height - 250 - CGFloat(index) * (rowHeight + 16)
|
||||
let frame = CGRect(x: 68, y: y, width: rect.width - 136, height: rowHeight)
|
||||
drawSurface(frame, fill: NSColor.white, stroke: NSColor(hex: 0xD9E0EC))
|
||||
drawIconBackground(in: CGRect(x: frame.minX + 30, y: frame.minY + 26, width: 168, height: 168))
|
||||
drawSplitLock(variant: variant, style: .glossy, in: CGRect(x: frame.minX + 42, y: frame.minY + 38, width: 144, height: 144))
|
||||
drawText(
|
||||
variant.title,
|
||||
in: CGRect(x: frame.minX + 228, y: frame.maxY - 62, width: frame.width - 252, height: 30),
|
||||
font: .systemFont(ofSize: 26, weight: .bold),
|
||||
color: NSColor(hex: 0x111827)
|
||||
)
|
||||
drawText(
|
||||
variant.summary,
|
||||
in: CGRect(x: frame.minX + 228, y: frame.maxY - 118, width: frame.width - 252, height: 60),
|
||||
font: .systemFont(ofSize: 16, weight: .medium),
|
||||
color: NSColor(hex: 0x475467)
|
||||
)
|
||||
drawPill(
|
||||
"glossy primary",
|
||||
in: CGRect(x: frame.minX + 228, y: frame.minY + 28, width: 112, height: 28),
|
||||
fill: variant.accent.withAlphaComponent(0.14),
|
||||
textColor: NSColor(hex: 0x275FD7)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func drawVariantCard(variant: SplitLockVariant, in frame: CGRect) {
|
||||
drawSurface(frame, fill: NSColor.white, stroke: NSColor(hex: 0xD9E0EC))
|
||||
|
||||
let iconFrame = CGRect(x: frame.minX + 24, y: frame.minY + 62, width: 158, height: 158)
|
||||
drawIconBackground(in: iconFrame)
|
||||
drawSplitLock(variant: variant, style: .glossy, in: iconFrame.insetBy(dx: 14, dy: 14))
|
||||
|
||||
let flatFrame = CGRect(x: frame.minX + 30, y: frame.minY + 196, width: 58, height: 58)
|
||||
drawMiniBadge(flatFrame, variant: variant)
|
||||
|
||||
drawText(
|
||||
variant.title,
|
||||
in: CGRect(x: frame.minX + 204, y: frame.maxY - 70, width: frame.width - 226, height: 28),
|
||||
font: .systemFont(ofSize: 22, weight: .bold),
|
||||
color: NSColor(hex: 0x111827)
|
||||
)
|
||||
|
||||
drawText(
|
||||
variant.summary,
|
||||
in: CGRect(x: frame.minX + 204, y: frame.maxY - 138, width: frame.width - 226, height: 78),
|
||||
font: .systemFont(ofSize: 14, weight: .medium),
|
||||
color: NSColor(hex: 0x475467)
|
||||
)
|
||||
|
||||
drawPill(
|
||||
"flat inset",
|
||||
in: CGRect(x: frame.minX + 100, y: frame.minY + 208, width: 72, height: 22),
|
||||
fill: NSColor(hex: 0xEDF2FF),
|
||||
textColor: NSColor(hex: 0x3A63D1)
|
||||
)
|
||||
}
|
||||
|
||||
func drawDecisionCard(in frame: CGRect) {
|
||||
drawSurface(frame, fill: NSColor(hex: 0x17223A), stroke: NSColor(hex: 0x17223A))
|
||||
|
||||
drawText(
|
||||
"What To Pick For",
|
||||
in: CGRect(x: frame.minX + 26, y: frame.maxY - 68, width: frame.width - 52, height: 28),
|
||||
font: .systemFont(ofSize: 24, weight: .bold),
|
||||
color: .white
|
||||
)
|
||||
|
||||
let bullets = [
|
||||
"Most balanced: Balanced Gate",
|
||||
"Most premium: Glass Core",
|
||||
"Most dev-tool: Bracket Trace",
|
||||
"Most trustworthy: Shield Lock",
|
||||
"Most active / dynamic: Signal Keyhole"
|
||||
]
|
||||
|
||||
for (index, bullet) in bullets.enumerated() {
|
||||
let rowY = frame.maxY - 118 - CGFloat(index) * 34
|
||||
fillEllipse(CGRect(x: frame.minX + 28, y: rowY + 6, width: 10, height: 10), color: NSColor(hex: 0x6DA1FF))
|
||||
drawText(
|
||||
bullet,
|
||||
in: CGRect(x: frame.minX + 48, y: rowY, width: frame.width - 72, height: 22),
|
||||
font: .systemFont(ofSize: 15, weight: .semibold),
|
||||
color: NSColor.white.withAlphaComponent(0.94)
|
||||
)
|
||||
}
|
||||
|
||||
drawText(
|
||||
"Pick one and I’ll iterate on thickness, color, background, or make it more minimal.",
|
||||
in: CGRect(x: frame.minX + 26, y: frame.minY + 28, width: frame.width - 52, height: 42),
|
||||
font: .systemFont(ofSize: 15, weight: .medium),
|
||||
color: NSColor.white.withAlphaComponent(0.82)
|
||||
)
|
||||
}
|
||||
|
||||
func drawIconBackground(in rect: CGRect) {
|
||||
let path = NSBezierPath(roundedRect: rect, xRadius: rect.width * 0.23, yRadius: rect.height * 0.23)
|
||||
let gradient = NSGradient(colors: [
|
||||
NSColor(hex: 0xF6F9FF),
|
||||
NSColor(hex: 0xDFE9FF),
|
||||
NSColor(hex: 0xD1DEFF)
|
||||
])
|
||||
gradient?.draw(in: path, angle: 90)
|
||||
|
||||
let highlight = NSBezierPath(roundedRect: rect.insetBy(dx: 16, dy: 16).offsetBy(dx: 0, dy: 20), xRadius: 34, yRadius: 34)
|
||||
NSColor.white.withAlphaComponent(0.24).setFill()
|
||||
highlight.fill()
|
||||
|
||||
NSColor.white.withAlphaComponent(0.74).setStroke()
|
||||
path.lineWidth = 1.6
|
||||
path.stroke()
|
||||
}
|
||||
|
||||
func drawMiniBadge(_ rect: CGRect, variant: SplitLockVariant) {
|
||||
let path = NSBezierPath(roundedRect: rect, xRadius: rect.width * 0.28, yRadius: rect.height * 0.28)
|
||||
let gradient = NSGradient(colors: [NSColor(hex: 0xFCFDFF), NSColor(hex: 0xEEF3FF)])
|
||||
gradient?.draw(in: path, angle: 90)
|
||||
NSColor.white.withAlphaComponent(0.8).setStroke()
|
||||
path.lineWidth = 1.2
|
||||
path.stroke()
|
||||
drawSplitLock(variant: variant, style: .flat, in: rect.insetBy(dx: 8, dy: 8))
|
||||
}
|
||||
|
||||
func drawSplitLock(variant: SplitLockVariant, style: LockStyle, in rect: CGRect) {
|
||||
switch variant {
|
||||
case .balancedGate:
|
||||
drawBalancedGate(style: style, in: rect)
|
||||
case .glassCore:
|
||||
drawGlassCore(style: style, in: rect)
|
||||
case .bracketTrace:
|
||||
drawBracketTrace(style: style, in: rect)
|
||||
case .shieldLock:
|
||||
drawShieldLock(style: style, in: rect)
|
||||
case .signalKeyhole:
|
||||
drawSignalKeyhole(style: style, in: rect)
|
||||
}
|
||||
}
|
||||
|
||||
func drawBalancedGate(style: LockStyle, in rect: CGRect) {
|
||||
drawBaseLock(style: style, in: rect, bodyInset: 0, shackleLift: 0)
|
||||
drawTraceLines(
|
||||
in: rect,
|
||||
color: NSColor(hex: style == .flat ? 0x5EA4FF : 0x7EB8FF),
|
||||
spread: 26,
|
||||
weight: style == .flat ? 6 : 7
|
||||
)
|
||||
}
|
||||
|
||||
func drawGlassCore(style: LockStyle, in rect: CGRect) {
|
||||
drawBaseLock(style: style, in: rect, bodyInset: 4, shackleLift: 6)
|
||||
|
||||
let glowRect = CGRect(x: rect.midX - 18, y: rect.midY - 14, width: 36, height: 36)
|
||||
if style == .glossy {
|
||||
fillEllipse(glowRect.insetBy(dx: -10, dy: -10), color: NSColor(hex: 0x84DAFF).withAlphaComponent(0.16))
|
||||
}
|
||||
fillEllipse(glowRect, color: NSColor(hex: style == .flat ? 0x6CCFFF : 0x86E5FF))
|
||||
|
||||
drawTraceLines(
|
||||
in: rect,
|
||||
color: NSColor(hex: style == .flat ? 0x74B0FF : 0xA5CCFF),
|
||||
spread: 20,
|
||||
weight: style == .flat ? 5 : 6
|
||||
)
|
||||
}
|
||||
|
||||
func drawBracketTrace(style: LockStyle, in rect: CGRect) {
|
||||
drawBaseLock(style: style, in: rect, bodyInset: 0, shackleLift: 0)
|
||||
|
||||
let bracket = NSBezierPath()
|
||||
bracket.move(to: CGPoint(x: rect.midX + 10, y: rect.midY + 34))
|
||||
bracket.line(to: CGPoint(x: rect.maxX - 30, y: rect.midY + 34))
|
||||
bracket.line(to: CGPoint(x: rect.maxX - 30, y: rect.midY + 10))
|
||||
bracket.move(to: CGPoint(x: rect.midX + 10, y: rect.midY - 34))
|
||||
bracket.line(to: CGPoint(x: rect.maxX - 30, y: rect.midY - 34))
|
||||
bracket.line(to: CGPoint(x: rect.maxX - 30, y: rect.midY - 10))
|
||||
bracket.lineWidth = style == .flat ? 6 : 7
|
||||
bracket.lineCapStyle = .round
|
||||
bracket.lineJoinStyle = .round
|
||||
NSColor(hex: style == .flat ? 0x67A8FF : 0x8FC1FF).setStroke()
|
||||
bracket.stroke()
|
||||
|
||||
drawTraceLines(
|
||||
in: rect,
|
||||
color: NSColor(hex: style == .flat ? 0x4F86FF : 0x69A8FF),
|
||||
spread: 24,
|
||||
weight: style == .flat ? 5 : 6
|
||||
)
|
||||
}
|
||||
|
||||
func drawShieldLock(style: LockStyle, in rect: CGRect) {
|
||||
let shield = NSBezierPath()
|
||||
shield.move(to: CGPoint(x: rect.midX, y: rect.maxY - 6))
|
||||
shield.curve(
|
||||
to: CGPoint(x: rect.minX + 32, y: rect.midY + 18),
|
||||
controlPoint1: CGPoint(x: rect.midX - 40, y: rect.maxY - 8),
|
||||
controlPoint2: CGPoint(x: rect.minX + 26, y: rect.maxY - 26)
|
||||
)
|
||||
shield.line(to: CGPoint(x: rect.midX, y: rect.minY + 6))
|
||||
shield.line(to: CGPoint(x: rect.maxX - 32, y: rect.midY + 18))
|
||||
shield.curve(
|
||||
to: CGPoint(x: rect.midX, y: rect.maxY - 6),
|
||||
controlPoint1: CGPoint(x: rect.maxX - 26, y: rect.maxY - 26),
|
||||
controlPoint2: CGPoint(x: rect.midX + 40, y: rect.maxY - 8)
|
||||
)
|
||||
shield.close()
|
||||
|
||||
if style == .glossy {
|
||||
NSColor.white.withAlphaComponent(0.18).setFill()
|
||||
shield.fill()
|
||||
} else {
|
||||
NSColor(hex: 0xEDF4FF).setFill()
|
||||
shield.fill()
|
||||
}
|
||||
|
||||
drawBaseLock(style: style, in: rect.insetBy(dx: 10, dy: 10), bodyInset: 2, shackleLift: 2)
|
||||
drawTraceLines(
|
||||
in: rect.insetBy(dx: 10, dy: 10),
|
||||
color: NSColor(hex: style == .flat ? 0x5F9FFF : 0x8CC1FF),
|
||||
spread: 22,
|
||||
weight: style == .flat ? 5 : 6
|
||||
)
|
||||
}
|
||||
|
||||
func drawSignalKeyhole(style: LockStyle, in rect: CGRect) {
|
||||
drawBaseLock(style: style, in: rect, bodyInset: 1, shackleLift: 0)
|
||||
|
||||
let keyholeCircle = CGRect(x: rect.midX - 14, y: rect.midY + 2, width: 28, height: 28)
|
||||
let keyholeStem = NSBezierPath(roundedRect: CGRect(x: rect.midX - 7, y: rect.midY - 24, width: 14, height: 30), xRadius: 7, yRadius: 7)
|
||||
if style == .glossy {
|
||||
fillEllipse(keyholeCircle.insetBy(dx: -8, dy: -8), color: NSColor(hex: 0x7FE4FF).withAlphaComponent(0.16))
|
||||
}
|
||||
fillEllipse(keyholeCircle, color: NSColor(hex: style == .flat ? 0x143A6F : 0xF8FBFF))
|
||||
let stemColor = style == .flat ? NSColor(hex: 0x143A6F) : NSColor(hex: 0xF8FBFF)
|
||||
stemColor.setFill()
|
||||
keyholeStem.fill()
|
||||
|
||||
drawTraceLines(
|
||||
in: rect,
|
||||
color: NSColor(hex: style == .flat ? 0x49CFFF : 0x5CE7FF),
|
||||
spread: 28,
|
||||
weight: style == .flat ? 5 : 6
|
||||
)
|
||||
}
|
||||
|
||||
func drawBaseLock(style: LockStyle, in rect: CGRect, bodyInset: CGFloat, shackleLift: CGFloat) {
|
||||
let bodyRect = CGRect(
|
||||
x: rect.midX - 42 + bodyInset,
|
||||
y: rect.midY - 34,
|
||||
width: 96 - bodyInset * 2,
|
||||
height: 76
|
||||
)
|
||||
let body = NSBezierPath(roundedRect: bodyRect, xRadius: 22, yRadius: 22)
|
||||
|
||||
switch style {
|
||||
case .flat:
|
||||
NSColor(hex: 0xF7FAFF).setFill()
|
||||
body.fill()
|
||||
NSColor(hex: 0x1E3A72).setStroke()
|
||||
body.lineWidth = 5.5
|
||||
body.stroke()
|
||||
case .glossy:
|
||||
let gradient = NSGradient(colors: [
|
||||
NSColor.white.withAlphaComponent(0.92),
|
||||
NSColor(hex: 0xE0EBFF).withAlphaComponent(0.88)
|
||||
])
|
||||
gradient?.draw(in: body, angle: 90)
|
||||
NSColor.white.withAlphaComponent(0.82).setStroke()
|
||||
body.lineWidth = 5
|
||||
body.stroke()
|
||||
}
|
||||
|
||||
let shackle = NSBezierPath()
|
||||
shackle.move(to: CGPoint(x: rect.midX - 32, y: bodyRect.maxY - 1))
|
||||
shackle.curve(
|
||||
to: CGPoint(x: rect.midX + 12, y: bodyRect.maxY + 40 + shackleLift),
|
||||
controlPoint1: CGPoint(x: rect.midX - 34, y: bodyRect.maxY + 42 + shackleLift),
|
||||
controlPoint2: CGPoint(x: rect.midX + 12, y: bodyRect.maxY + 42 + shackleLift)
|
||||
)
|
||||
shackle.lineWidth = style == .flat ? 11 : 12
|
||||
shackle.lineCapStyle = .round
|
||||
NSColor(hex: style == .flat ? 0x1E3A72 : 0xF8FBFF).setStroke()
|
||||
shackle.stroke()
|
||||
|
||||
let leftFillRect = CGRect(x: bodyRect.minX, y: bodyRect.minY, width: bodyRect.width * 0.52, height: bodyRect.height)
|
||||
let leftFill = NSBezierPath(roundedRect: leftFillRect, xRadius: 18, yRadius: 18)
|
||||
NSColor(hex: style == .flat ? 0x76A8FF : 0xBCD4FF).setFill()
|
||||
leftFill.fill()
|
||||
}
|
||||
|
||||
func drawTraceLines(in rect: CGRect, color: NSColor, spread: CGFloat, weight: CGFloat) {
|
||||
let traces: [(CGFloat, CGFloat)] = [(spread, 0.94), (0, 1.0), (-spread, 0.92)]
|
||||
|
||||
for (offset, scale) in traces {
|
||||
let start = CGPoint(x: rect.midX + 6, y: rect.midY + offset * 0.32)
|
||||
let end = CGPoint(x: rect.maxX - 20, y: rect.midY + offset)
|
||||
let path = NSBezierPath()
|
||||
path.move(to: start)
|
||||
path.curve(
|
||||
to: end,
|
||||
controlPoint1: CGPoint(x: start.x + 22, y: start.y),
|
||||
controlPoint2: CGPoint(x: end.x - 18, y: end.y)
|
||||
)
|
||||
path.lineWidth = weight * scale
|
||||
path.lineCapStyle = .round
|
||||
color.setStroke()
|
||||
path.stroke()
|
||||
fillEllipse(CGRect(x: end.x - 5, y: end.y - 5, width: 10, height: 10), color: color)
|
||||
}
|
||||
}
|
||||
|
||||
func drawSurface(_ rect: CGRect, fill: NSColor, stroke: NSColor) {
|
||||
let shadow = NSShadow()
|
||||
shadow.shadowColor = NSColor.black.withAlphaComponent(0.08)
|
||||
shadow.shadowOffset = CGSize(width: 0, height: -7)
|
||||
shadow.shadowBlurRadius = 18
|
||||
|
||||
NSGraphicsContext.saveGraphicsState()
|
||||
shadow.set()
|
||||
let path = NSBezierPath(roundedRect: rect, xRadius: 26, yRadius: 26)
|
||||
fill.setFill()
|
||||
path.fill()
|
||||
NSGraphicsContext.restoreGraphicsState()
|
||||
|
||||
let strokePath = NSBezierPath(roundedRect: rect, xRadius: 26, yRadius: 26)
|
||||
stroke.setStroke()
|
||||
strokePath.lineWidth = 1.1
|
||||
strokePath.stroke()
|
||||
}
|
||||
|
||||
func drawPill(_ title: String, in rect: CGRect, fill: NSColor, textColor: NSColor) {
|
||||
let path = NSBezierPath(roundedRect: rect, xRadius: rect.height / 2, yRadius: rect.height / 2)
|
||||
fill.setFill()
|
||||
path.fill()
|
||||
drawText(title, in: rect.offsetBy(dx: 0, dy: 2), font: .systemFont(ofSize: 11, weight: .bold), color: textColor, alignment: .center)
|
||||
}
|
||||
|
||||
func drawText(_ text: String, in rect: CGRect, font: NSFont, color: NSColor, alignment: NSTextAlignment = .left) {
|
||||
let paragraph = NSMutableParagraphStyle()
|
||||
paragraph.alignment = alignment
|
||||
paragraph.lineBreakMode = .byWordWrapping
|
||||
let attributes: [NSAttributedString.Key: Any] = [
|
||||
.font: font,
|
||||
.foregroundColor: color,
|
||||
.paragraphStyle: paragraph
|
||||
]
|
||||
NSString(string: text).draw(with: rect, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: attributes)
|
||||
}
|
||||
|
||||
func fill(_ rect: CGRect, color: NSColor) {
|
||||
color.setFill()
|
||||
NSBezierPath(rect: rect).fill()
|
||||
}
|
||||
|
||||
func fillEllipse(_ rect: CGRect, color: NSColor) {
|
||||
color.setFill()
|
||||
NSBezierPath(ovalIn: rect).fill()
|
||||
}
|
||||
|
||||
extension NSColor {
|
||||
convenience init(hex: Int, alpha: CGFloat = 1.0) {
|
||||
let red = CGFloat((hex >> 16) & 0xFF) / 255.0
|
||||
let green = CGFloat((hex >> 8) & 0xFF) / 255.0
|
||||
let blue = CGFloat(hex & 0xFF) / 255.0
|
||||
self.init(calibratedRed: red, green: green, blue: blue, alpha: alpha)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user