Add Konverter: a local-only menu bar file converter
Drag files in Finder, hold a modifier, and drop on a wheel of outputs. Conversions run entirely on-device through ImageIO, PDFKit, AVFoundation, the bundled CLI tools, and optional ffmpeg/LibreOffice. Includes a --self-test suite (41 checks) and a --convert CLI mode so the conversion matrix can be verified without the UI.
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import AppKit
|
||||
|
||||
// Draws the Konverter icon: a warm tangerine squircle with a radial conversion
|
||||
// wheel on it. Everything is vector-drawn so the script has no dependencies.
|
||||
|
||||
let canvas: CGFloat = 1024
|
||||
let size = NSSize(width: canvas, height: canvas)
|
||||
guard let rep = NSBitmapImageRep(
|
||||
bitmapDataPlanes: nil, pixelsWide: Int(canvas), pixelsHigh: Int(canvas),
|
||||
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false,
|
||||
colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0
|
||||
) else { exit(1) }
|
||||
|
||||
NSGraphicsContext.saveGraphicsState()
|
||||
NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep)
|
||||
NSGraphicsContext.current?.imageInterpolation = .high
|
||||
|
||||
let rect = NSRect(origin: .zero, size: size)
|
||||
|
||||
// macOS icons live inside a ~10% margin; the squircle fills the rest.
|
||||
let body = rect.insetBy(dx: 96, dy: 96)
|
||||
let squircle = NSBezierPath(roundedRect: body, xRadius: 200, yRadius: 200)
|
||||
|
||||
let gradient = NSGradient(colors: [
|
||||
NSColor(calibratedRed: 1.00, green: 0.66, blue: 0.20, alpha: 1),
|
||||
NSColor(calibratedRed: 0.94, green: 0.33, blue: 0.11, alpha: 1),
|
||||
])!
|
||||
gradient.draw(in: squircle, angle: -60)
|
||||
|
||||
// Soft top highlight.
|
||||
NSGraphicsContext.saveGraphicsState()
|
||||
squircle.addClip()
|
||||
NSGradient(colors: [
|
||||
NSColor.white.withAlphaComponent(0.28),
|
||||
NSColor.white.withAlphaComponent(0.00),
|
||||
])!.draw(in: NSRect(x: body.minX, y: body.midY, width: body.width, height: body.height / 2), angle: -90)
|
||||
NSGraphicsContext.restoreGraphicsState()
|
||||
|
||||
// Radial wheel: eight segments, one of them highlighted as the selection.
|
||||
let center = NSPoint(x: rect.midX, y: rect.midY + 24)
|
||||
let outer: CGFloat = 296
|
||||
let inner: CGFloat = 150
|
||||
let segmentCount = 8
|
||||
let gap: CGFloat = 3.2
|
||||
|
||||
func polar(_ radius: CGFloat, _ degrees: CGFloat) -> NSPoint {
|
||||
let radians = degrees * .pi / 180
|
||||
return NSPoint(x: center.x + radius * cos(radians), y: center.y + radius * sin(radians))
|
||||
}
|
||||
|
||||
func segment(_ index: Int, from start: CGFloat, to end: CGFloat) -> NSBezierPath {
|
||||
let path = NSBezierPath()
|
||||
path.move(to: polar(inner, start))
|
||||
path.line(to: polar(outer, start))
|
||||
path.appendArc(withCenter: center, radius: outer, startAngle: start, endAngle: end)
|
||||
path.line(to: polar(inner, end))
|
||||
path.appendArc(withCenter: center, radius: inner, startAngle: end, endAngle: start, clockwise: true)
|
||||
path.close()
|
||||
return path
|
||||
}
|
||||
|
||||
for index in 0..<segmentCount {
|
||||
let centre = 90 - CGFloat(index) * (360 / CGFloat(segmentCount))
|
||||
let start = centre - (360 / CGFloat(segmentCount)) / 2 + gap
|
||||
let end = centre + (360 / CGFloat(segmentCount)) / 2 - gap
|
||||
let path = segment(index, from: start, to: end)
|
||||
// Index 4 sits at -90°, i.e. straight down — where the arrow points.
|
||||
let selected = index == 4
|
||||
(selected ? NSColor.white : NSColor.white.withAlphaComponent(0.34)).setFill()
|
||||
path.fill()
|
||||
}
|
||||
|
||||
// Hub with a downward arrow: the drop that performs the conversion.
|
||||
let hubRadius: CGFloat = 118
|
||||
let hubRect = NSRect(
|
||||
x: center.x - hubRadius, y: center.y - hubRadius,
|
||||
width: hubRadius * 2, height: hubRadius * 2
|
||||
)
|
||||
NSColor(calibratedRed: 0.16, green: 0.10, blue: 0.10, alpha: 0.92).setFill()
|
||||
NSBezierPath(ovalIn: hubRect).fill()
|
||||
|
||||
NSColor.white.setFill()
|
||||
NSBezierPath(ovalIn: hubRect.insetBy(dx: 14, dy: 14)).fill()
|
||||
|
||||
// Arrow glyph, pointing at the selected segment.
|
||||
let arrow = NSBezierPath()
|
||||
let up = center.y + 58
|
||||
let down = center.y - 58
|
||||
let shaftWidth: CGFloat = 26
|
||||
arrow.move(to: NSPoint(x: center.x - shaftWidth / 2, y: up))
|
||||
arrow.line(to: NSPoint(x: center.x + shaftWidth / 2, y: up))
|
||||
arrow.line(to: NSPoint(x: center.x + shaftWidth / 2, y: down + 30))
|
||||
arrow.line(to: NSPoint(x: center.x + 62, y: down + 30))
|
||||
arrow.line(to: NSPoint(x: center.x, y: down - 32))
|
||||
arrow.line(to: NSPoint(x: center.x - 62, y: down + 30))
|
||||
arrow.line(to: NSPoint(x: center.x - shaftWidth / 2, y: down + 30))
|
||||
arrow.close()
|
||||
arrow.lineJoinStyle = .round
|
||||
arrow.lineWidth = 6
|
||||
NSColor(calibratedRed: 0.86, green: 0.28, blue: 0.09, alpha: 1).setFill()
|
||||
NSColor(calibratedRed: 0.86, green: 0.28, blue: 0.09, alpha: 1).setStroke()
|
||||
arrow.fill()
|
||||
arrow.stroke()
|
||||
|
||||
NSGraphicsContext.restoreGraphicsState()
|
||||
|
||||
guard CommandLine.arguments.count >= 2 else { exit(1) }
|
||||
guard let data = rep.representation(using: .png, properties: [:]) else { exit(1) }
|
||||
do {
|
||||
try data.write(to: URL(fileURLWithPath: CommandLine.arguments[1]))
|
||||
} catch {
|
||||
exit(1)
|
||||
}
|
||||
Reference in New Issue
Block a user