Initial release

This commit is contained in:
aculix committed 2026-09-28 18:28:32 +05:30
commit 4b67728cce
108 files changed
+13310

No files matched your search

+183
View File
@@ -0,0 +1,183 @@
// swift scripts/icons/render.swift <out-dir>; run via scripts/make-icons.sh.
import AppKit
import CoreGraphics
import ImageIO
import UniformTypeIdentifiers
struct Knot: Decodable {
let icon: [[[Double]]]
let iconStroke: Double
}
let args = CommandLine.arguments
let here = URL(fileURLWithPath: #filePath).deletingLastPathComponent()
let knot = try JSONDecoder().decode(Knot.self, from: Data(contentsOf: here.appendingPathComponent("knot.json")))
let out = URL(fileURLWithPath: args.count > 1 ? args[1] : ".")
func color(_ hex: UInt32, _ alpha: CGFloat = 1) -> CGColor {
CGColor(srgbRed: CGFloat(hex >> 16 & 0xFF) / 255, green: CGFloat(hex >> 8 & 0xFF) / 255,
blue: CGFloat(hex & 0xFF) / 255, alpha: alpha)
}
/// Apple's icon shape is close to a superellipse with exponent 5.
func squircle(_ r: CGRect) -> CGPath {
let path = CGMutablePath()
let steps = 720
for i in 0...steps {
let t = Double(i) / Double(steps) * 2 * .pi
let c = cos(t), s = sin(t)
let x = r.midX + r.width / 2 * CGFloat(copysign(pow(abs(c), 2.0 / 5), c))
let y = r.midY + r.height / 2 * CGFloat(copysign(pow(abs(s), 2.0 / 5), s))
if i == 0 { path.move(to: CGPoint(x: x, y: y)) } else { path.addLine(to: CGPoint(x: x, y: y)) }
}
path.closeSubpath()
return path
}
func verticalGradient(_ ctx: CGContext, in rect: CGRect, top: CGColor, bottom: CGColor) {
let gradient = CGGradient(colorsSpace: CGColorSpace(name: CGColorSpace.sRGB), colors: [top, bottom] as CFArray, locations: [0, 1])!
ctx.drawLinearGradient(gradient, start: CGPoint(x: rect.midX, y: rect.minY), end: CGPoint(x: rect.midX, y: rect.maxY), options: [])
}
/// The knot as drawn on the icon: `box` is the icon's body, and the knot keeps the icon's margins inside it.
func drawKnot(_ ctx: CGContext, in box: CGRect) {
let scale = box.width / 100
let width = knot.iconStroke * scale
for strand in knot.icon {
let line = CGMutablePath()
for (i, p) in strand.enumerated() {
let point = CGPoint(x: box.minX + p[0] * scale, y: box.minY + p[1] * scale)
if i == 0 { line.move(to: point) } else { line.addLine(to: point) }
}
let stroked = line.copy(strokingWithWidth: width, lineCap: .round, lineJoin: .round, miterLimit: 4)
ctx.saveGState()
ctx.addPath(stroked)
ctx.clip()
verticalGradient(ctx, in: box, top: color(0xFFA37A), bottom: color(0xFF7345))
ctx.restoreGState()
}
}
func render(size: Int, body: CGRect, shadow: Bool, to url: URL) {
let s = CGFloat(size)
let ctx = CGContext(data: nil, width: size, height: size, bitsPerComponent: 8, bytesPerRow: 0,
space: CGColorSpace(name: CGColorSpace.sRGB)!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!
ctx.translateBy(x: 0, y: s)
ctx.scaleBy(x: 1, y: -1)
ctx.setShouldAntialias(true)
ctx.interpolationQuality = .high
let shape = squircle(body)
if shadow {
ctx.saveGState()
ctx.setShadow(offset: CGSize(width: 0, height: s * 0.012), blur: s * 0.028, color: color(0x000000, 0.32))
ctx.addPath(shape)
ctx.setFillColor(color(0x132441))
ctx.fillPath()
ctx.restoreGState()
}
ctx.saveGState()
ctx.addPath(shape)
ctx.clip()
verticalGradient(ctx, in: body, top: color(0x1C3563), bottom: color(0x0D1A33))
drawKnot(ctx, in: body)
ctx.restoreGState()
let image = ctx.makeImage()!
let dest = CGImageDestinationCreateWithURL(url as CFURL, UTType.png.identifier as CFString, 1, nil)!
CGImageDestinationAddImage(dest, image, nil)
CGImageDestinationFinalize(dest)
}
// Apple's grid: an 824-point body on a 1024-point canvas.
let iconset = out.appendingPathComponent("AppIcon.iconset")
try FileManager.default.createDirectory(at: iconset, withIntermediateDirectories: true)
for (points, scale) in [(16, 1), (16, 2), (32, 1), (32, 2), (128, 1), (128, 2), (256, 1), (256, 2), (512, 1), (512, 2)] {
let size = points * scale
let inset = CGFloat(size) * 100 / 1024
let body = CGRect(x: inset, y: inset, width: CGFloat(size) - 2 * inset, height: CGFloat(size) - 2 * inset)
let name = scale == 1 ? "icon_\(points)x\(points).png" : "icon_\(points)x\(points)@2x.png"
render(size: size, body: body, shadow: size >= 64, to: iconset.appendingPathComponent(name))
}
// The Chrome Web Store wants 96 points of art with 16 of padding at 128.
for (size, pad) in [(128, 16.0), (48, 2.0), (32, 1.0), (16, 0.0)] {
let body = CGRect(x: pad, y: pad, width: Double(size) - 2 * pad, height: Double(size) - 2 * pad)
render(size: size, body: body, shadow: false, to: out.appendingPathComponent("chrome-\(size).png"))
}
// The disk image window: Finder puts Knotchy at (180, 175) and Applications at (460, 175).
// It's drawn taller than the 400-point window so a slightly taller window never shows an edge, and it's light
// because Finder always draws the file names in black over a background picture, even in dark mode.
func renderBackground(scale: Int, to url: URL) {
let width = 640, height = 440
let ctx = CGContext(data: nil, width: width * scale, height: height * scale, bitsPerComponent: 8, bytesPerRow: 0,
space: CGColorSpace(name: CGColorSpace.sRGB)!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!
ctx.scaleBy(x: CGFloat(scale), y: CGFloat(scale))
ctx.translateBy(x: 0, y: CGFloat(height))
ctx.scaleBy(x: 1, y: -1)
ctx.setFillColor(color(0xF5F5F7))
ctx.fill(CGRect(x: 0, y: 0, width: width, height: height))
let arrow = CGMutablePath()
arrow.move(to: CGPoint(x: 272, y: 175))
arrow.addLine(to: CGPoint(x: 366, y: 175))
arrow.move(to: CGPoint(x: 356, y: 165))
arrow.addLine(to: CGPoint(x: 368, y: 175))
arrow.addLine(to: CGPoint(x: 356, y: 185))
ctx.addPath(arrow)
ctx.setStrokeColor(color(0xFF8456))
ctx.setLineWidth(3)
ctx.setLineCap(.round)
ctx.setLineJoin(.round)
ctx.strokePath()
NSGraphicsContext.current = NSGraphicsContext(cgContext: ctx, flipped: true)
let caption = NSAttributedString(string: "Drag Knotchy to Applications", attributes: [
.font: NSFont.systemFont(ofSize: 12, weight: .medium),
.foregroundColor: NSColor(cgColor: color(0x86868B))!,
])
let size = caption.size()
caption.draw(at: CGPoint(x: (CGFloat(width) - size.width) / 2, y: 300))
NSGraphicsContext.current = nil
let dest = CGImageDestinationCreateWithURL(url as CFURL, UTType.png.identifier as CFString, 1, nil)!
CGImageDestinationAddImage(dest, ctx.makeImage()!, [kCGImagePropertyDPIWidth: 72 * scale, kCGImagePropertyDPIHeight: 72 * scale] as CFDictionary)
CGImageDestinationFinalize(dest)
}
renderBackground(scale: 1, to: out.appendingPathComponent("dmg-background.png"))
renderBackground(scale: 2, to: out.appendingPathComponent("dmg-background@2x.png"))
// Chrome Web Store promo images. The store takes 24-bit PNGs only, hence no alpha.
func renderPromo(width: Int, height: Int, to url: URL) {
let w = CGFloat(width), h = CGFloat(height), unit = h / 280
let ctx = CGContext(data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: 0,
space: CGColorSpace(name: CGColorSpace.sRGB)!, bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue)!
ctx.translateBy(x: 0, y: h)
ctx.scaleBy(x: 1, y: -1)
verticalGradient(ctx, in: CGRect(x: 0, y: 0, width: w, height: h), top: color(0x1C3563), bottom: color(0x0D1A33))
let title = NSAttributedString(string: "Open in Knotchy", attributes: [
.font: NSFont.systemFont(ofSize: 28 * unit, weight: .bold),
.foregroundColor: NSColor.white,
])
let subtitle = NSAttributedString(string: "Open suspicious links in a\ndisposable, isolated browser.", attributes: [
.font: NSFont.systemFont(ofSize: 14 * unit, weight: .medium),
.foregroundColor: NSColor(white: 1, alpha: 0.72),
])
let knotSide = 140 * unit, gap = 14 * unit, lineGap = 8 * unit
let left = (w - knotSide - gap - max(title.size().width, subtitle.size().width)) / 2
drawKnot(ctx, in: CGRect(x: left, y: (h - knotSide) / 2, width: knotSide, height: knotSide))
let top = (h - title.size().height - lineGap - subtitle.size().height) / 2
NSGraphicsContext.current = NSGraphicsContext(cgContext: ctx, flipped: true)
title.draw(at: CGPoint(x: left + knotSide + gap, y: top))
subtitle.draw(at: CGPoint(x: left + knotSide + gap, y: top + title.size().height + lineGap))
NSGraphicsContext.current = nil
let dest = CGImageDestinationCreateWithURL(url as CFURL, UTType.png.identifier as CFString, 1, nil)!
CGImageDestinationAddImage(dest, ctx.makeImage()!, nil)
CGImageDestinationFinalize(dest)
}
renderPromo(width: 440, height: 280, to: out.appendingPathComponent("promo-440x280.png"))
renderPromo(width: 1400, height: 560, to: out.appendingPathComponent("marquee-1400x560.png"))
print("rendered AppIcon.iconset, chrome-{16,32,48,128}.png, dmg-background{,@2x}.png and the store promos into \(out.path)")