Initial commit of Edison Voice menu-bar dictation app
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env swift
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
// Renders AppIcon.icns from code — no design tool, no binary asset to keep in sync.
|
||||
// Run: swift Tools/makeicon.swift
|
||||
|
||||
// Edison Voice: an amber lightbulb on a deep slate tile.
|
||||
let background = NSColor(srgbRed: 0.13, green: 0.14, blue: 0.17, alpha: 1)
|
||||
let bulbColor = NSColor(srgbRed: 1.00, green: 0.78, blue: 0.28, alpha: 1)
|
||||
|
||||
func drawIcon(size: CGFloat) -> NSImage {
|
||||
let image = NSImage(size: NSSize(width: size, height: size))
|
||||
image.lockFocus()
|
||||
guard let ctx = NSGraphicsContext.current?.cgContext else {
|
||||
image.unlockFocus()
|
||||
return image
|
||||
}
|
||||
ctx.setShouldAntialias(true)
|
||||
ctx.interpolationQuality = .high
|
||||
|
||||
// macOS Big Sur+ icon grid: art occupies the middle ~82%, leaving the shadow gutter.
|
||||
let inset = size * 0.09
|
||||
let rect = CGRect(x: inset, y: inset, width: size - inset * 2, height: size - inset * 2)
|
||||
// Apple's squircle is ~22.37% of the tile's edge.
|
||||
let radius = rect.width * 0.2237
|
||||
let squircle = CGPath(roundedRect: rect, cornerWidth: radius, cornerHeight: radius, transform: nil)
|
||||
|
||||
// Drop shadow under the tile.
|
||||
ctx.saveGState()
|
||||
ctx.setShadow(
|
||||
offset: CGSize(width: 0, height: -size * 0.012),
|
||||
blur: size * 0.035,
|
||||
color: NSColor.black.withAlphaComponent(0.32).cgColor
|
||||
)
|
||||
ctx.addPath(squircle)
|
||||
ctx.setFillColor(background.cgColor)
|
||||
ctx.fillPath()
|
||||
ctx.restoreGState()
|
||||
|
||||
// Soft top highlight so the tile reads as glass rather than flat fill.
|
||||
ctx.saveGState()
|
||||
ctx.addPath(squircle)
|
||||
ctx.clip()
|
||||
let highlight = CGGradient(
|
||||
colorsSpace: CGColorSpaceCreateDeviceRGB(),
|
||||
colors: [
|
||||
NSColor.white.withAlphaComponent(0.10).cgColor,
|
||||
NSColor.white.withAlphaComponent(0.0).cgColor,
|
||||
] as CFArray,
|
||||
locations: [0, 1]
|
||||
)!
|
||||
ctx.drawLinearGradient(
|
||||
highlight,
|
||||
start: CGPoint(x: rect.midX, y: rect.maxY),
|
||||
end: CGPoint(x: rect.midX, y: rect.midY),
|
||||
options: []
|
||||
)
|
||||
ctx.restoreGState()
|
||||
|
||||
// Lightbulb glyph, tinted amber.
|
||||
guard let base = NSImage(systemSymbolName: "lightbulb.fill", accessibilityDescription: nil) else {
|
||||
image.unlockFocus()
|
||||
return image
|
||||
}
|
||||
let pointSize = rect.width * 0.52
|
||||
let config = NSImage.SymbolConfiguration(pointSize: pointSize, weight: .semibold)
|
||||
.applying(NSImage.SymbolConfiguration(hierarchicalColor: bulbColor))
|
||||
let bulb = base.withSymbolConfiguration(config) ?? base
|
||||
|
||||
// Center the glyph. SF Symbols report their baseline-aware size, so frame it by its own
|
||||
// intrinsic size rather than assuming a square.
|
||||
let glyphSize = bulb.size
|
||||
let drawRect = CGRect(
|
||||
x: rect.midX - glyphSize.width / 2,
|
||||
y: rect.midY - glyphSize.height / 2,
|
||||
width: glyphSize.width,
|
||||
height: glyphSize.height
|
||||
)
|
||||
|
||||
ctx.saveGState()
|
||||
ctx.setShadow(
|
||||
offset: CGSize(width: 0, height: -size * 0.006),
|
||||
blur: size * 0.02,
|
||||
color: NSColor.black.withAlphaComponent(0.30).cgColor
|
||||
)
|
||||
bulb.draw(in: drawRect)
|
||||
ctx.restoreGState()
|
||||
|
||||
image.unlockFocus()
|
||||
return image
|
||||
}
|
||||
|
||||
func png(_ image: NSImage, pixels: Int) -> Data? {
|
||||
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 { return nil }
|
||||
rep.size = NSSize(width: pixels, height: pixels)
|
||||
|
||||
NSGraphicsContext.saveGraphicsState()
|
||||
NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep)
|
||||
// Redraw at native pixel size rather than scaling a single render — keeps the small
|
||||
// sizes crisp instead of muddy.
|
||||
drawIcon(size: CGFloat(pixels)).draw(
|
||||
in: NSRect(x: 0, y: 0, width: pixels, height: pixels),
|
||||
from: .zero, operation: .sourceOver, fraction: 1
|
||||
)
|
||||
NSGraphicsContext.restoreGraphicsState()
|
||||
|
||||
return rep.representation(using: .png, properties: [:])
|
||||
}
|
||||
|
||||
let fm = FileManager.default
|
||||
let root = URL(fileURLWithPath: fm.currentDirectoryPath)
|
||||
let iconset = root.appendingPathComponent("Resources/AppIcon.iconset")
|
||||
try? fm.removeItem(at: iconset)
|
||||
try fm.createDirectory(at: iconset, withIntermediateDirectories: true)
|
||||
|
||||
// (point size, scale) pairs iconutil expects.
|
||||
let variants: [(Int, Int)] = [
|
||||
(16, 1), (16, 2), (32, 1), (32, 2), (128, 1), (128, 2),
|
||||
(256, 1), (256, 2), (512, 1), (512, 2),
|
||||
]
|
||||
|
||||
for (points, scale) in variants {
|
||||
let pixels = points * scale
|
||||
guard let data = png(NSImage(), pixels: pixels) else {
|
||||
print("failed at \(pixels)px"); exit(1)
|
||||
}
|
||||
let suffix = scale == 2 ? "@2x" : ""
|
||||
let name = "icon_\(points)x\(points)\(suffix).png"
|
||||
try data.write(to: iconset.appendingPathComponent(name))
|
||||
}
|
||||
|
||||
print("wrote \(variants.count) PNGs to Resources/AppIcon.iconset")
|
||||
Reference in New Issue
Block a user