aboutsummaryrefslogtreecommitdiff
path: root/xM/gen-icon.swift
blob: 712e9e6b0a0a6145bd79cae10ca0c534e1c0f160 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// gen-icon.swift: generate a program icon for xM in the Apple icon format
//
// Copyright (c) 2023, Přemysl Eric Janouch <p@janouch.name>
// SPDX-License-Identifier: 0BSD
//
// NSGraphicsContext mostly just weirdly wraps over Quartz,
// so we do it all in Quartz directly.
import CoreGraphics
import Foundation
import ImageIO
import UniformTypeIdentifiers

// Apple uses something that's close to a "quintic superellipse" in their icons,
// but doesn't quite match. Either way, it looks better than rounded rectangles.
func addSquircle(context: CGContext, bounds: CGRect) {
	context.move(to: CGPoint(x: bounds.maxX, y: bounds.midY))
	for theta in stride(from: 0.0, to: .pi * 2, by: .pi / 1e4) {
		let x = pow(abs(cos(theta)), 2 / 5.0) * bounds.width / 2
			* CGFloat(signOf: cos(theta), magnitudeOf: 1) + bounds.midX
		let y = pow(abs(sin(theta)), 2 / 5.0) * bounds.height / 2
			* CGFloat(signOf: sin(theta), magnitudeOf: 1) + bounds.midY
		context.addLine(to: CGPoint(x: x, y: y))
	}
	context.closePath()
}

func drawIcon(scale: CGFloat) -> CGImage? {
	let size = CGSizeMake(1024, 1024)

	let colorspace = CGColorSpaceCreateDeviceRGB()
	let context = CGContext(data: nil,
		width: Int(size.width * scale), height: Int(size.height * scale),
		bitsPerComponent: 8, bytesPerRow: 0, space: colorspace,
		bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!
	context.scaleBy(x: scale, y: scale)

	let bounds = CGRectMake(100, 100, size.width - 200, size.height - 200)
	addSquircle(context: context, bounds: bounds)
	let squircle = context.path!

	// Gradients don't draw shadows, so draw it separately.
	context.saveGState()
	context.setShadow(offset: CGSizeMake(0, -12).applying(context.ctm),
		blur: 28 * scale, color: CGColor(gray: 0, alpha: 0.5))
	context.setFillColor(CGColor(red: 1, green: 0x55p-8, blue: 0, alpha: 1))
	context.fillPath()
	context.restoreGState()

	context.saveGState()
	context.addPath(squircle)
	context.clip()
	context.drawLinearGradient(
		CGGradient(colorsSpace: colorspace, colors: [
			CGColor(red: 1, green: 0x00p-8, blue: 0, alpha: 1),
			CGColor(red: 1, green: 0xaap-8, blue: 0, alpha: 1)
		] as CFArray, locations: [0, 1])!,
		start: CGPointMake(0, 100), end: CGPointMake(0, size.height - 100),
		options: CGGradientDrawingOptions(rawValue: 0))
	context.restoreGState()

	context.move(to: CGPoint(x: size.width * 0.30, y: size.height * 0.30))
	context.addLine(to: CGPoint(x: size.width * 0.30, y: size.height * 0.70))
	context.addLine(to: CGPoint(x: size.width * 0.575, y: size.height * 0.425))
	context.move(to: CGPoint(x: size.width * 0.70, y: size.height * 0.30))
	context.addLine(to: CGPoint(x: size.width * 0.70, y: size.height * 0.70))
	context.addLine(to: CGPoint(x: size.width * 0.425, y: size.height * 0.425))
	context.setLineWidth(80)
	context.setLineCap(.round)
	context.setLineJoin(.round)
	context.setStrokeColor(CGColor.white)
	context.strokePath()
	return context.makeImage()
}

if CommandLine.arguments.count != 2 {
	print("Usage: \(CommandLine.arguments.first!) OUTPUT.icns")
	exit(EXIT_FAILURE)
}

let filename = CommandLine.arguments[1]

let macOSSizes: Array<CGFloat> = [16, 32, 128, 256, 512]
let icns = CGImageDestinationCreateWithURL(
	URL(fileURLWithPath: filename) as CFURL,
	UTType.icns.identifier as CFString, macOSSizes.count * 2, nil)!
for size in macOSSizes {
	CGImageDestinationAddImage(icns, drawIcon(scale: size / 1024.0)!, nil)
	CGImageDestinationAddImage(icns, drawIcon(scale: size / 1024.0 * 2)!, [
			kCGImagePropertyDPIWidth: 144,
			kCGImagePropertyDPIHeight: 144,
		] as CFDictionary)
}
if !CGImageDestinationFinalize(icns) {
	print("ICNS finalization failed.")
	exit(EXIT_FAILURE)
}