mirror of
https://github.com/aculix/knotchy.git
synced 2026-10-27 07:15:53 +00:00
Initial release
This commit is contained in:
commit
4b67728cce
108 files changed
+13310
No files matched your search
@@ -0,0 +1,28 @@
|
||||
// swift scripts/icons/frame.swift <screenshot> <out.png>: fits a screenshot into the 1280×800 frame the
|
||||
// Chrome Web Store requires, on the icon's navy, as a 24-bit PNG. Window captures keep their own shadow.
|
||||
import AppKit
|
||||
import ImageIO
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
let args = CommandLine.arguments
|
||||
guard args.count == 3, let shot = NSImage(contentsOfFile: args[1]),
|
||||
let image = shot.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
|
||||
print("usage: swift scripts/icons/frame.swift <screenshot> <out.png>")
|
||||
exit(1)
|
||||
}
|
||||
let width = 1280, height = 800, margin: CGFloat = 40
|
||||
let ctx = CGContext(data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: 0,
|
||||
space: CGColorSpace(name: CGColorSpace.sRGB)!, bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue)!
|
||||
let colors = [CGColor(srgbRed: 0x1C / 255, green: 0x35 / 255, blue: 0x63 / 255, alpha: 1),
|
||||
CGColor(srgbRed: 0x0D / 255, green: 0x1A / 255, blue: 0x33 / 255, alpha: 1)] as CFArray
|
||||
let gradient = CGGradient(colorsSpace: CGColorSpace(name: CGColorSpace.sRGB), colors: colors, locations: [0, 1])!
|
||||
ctx.drawLinearGradient(gradient, start: CGPoint(x: 0, y: CGFloat(height)), end: .zero, options: [])
|
||||
let scale = min((CGFloat(width) - 2 * margin) / CGFloat(image.width), (CGFloat(height) - 2 * margin) / CGFloat(image.height))
|
||||
let size = CGSize(width: CGFloat(image.width) * scale, height: CGFloat(image.height) * scale)
|
||||
ctx.interpolationQuality = .high
|
||||
ctx.draw(image, in: CGRect(x: (CGFloat(width) - size.width) / 2, y: (CGFloat(height) - size.height) / 2,
|
||||
width: size.width, height: size.height))
|
||||
let dest = CGImageDestinationCreateWithURL(URL(fileURLWithPath: args[2]) as CFURL, UTType.png.identifier as CFString, 1, nil)!
|
||||
CGImageDestinationAddImage(dest, ctx.makeImage()!, nil)
|
||||
CGImageDestinationFinalize(dest)
|
||||
print("wrote \(args[2])")
|
||||
@@ -0,0 +1 @@
|
||||
{"icon": [[[43.147358872228004, 35.597261122187355], [42.61532507762965, 36.304681642555074], [42.09814885584686, 37.02497342074467], [41.59628295059067, 37.75754407139881], [41.11016314262925, 38.501788171305485], [40.64020783440553, 39.25708782787771], [40.186817652259094, 40.02281326017288], [39.7503750666409, 40.798323391885674], [39.331244030691536, 41.5829664557369], [38.92976963753638, 42.37608060866765], [38.54627779663111, 43.17699455723982], [38.18107492947324, 43.985028192631795], [37.83444768497557, 44.79949323460966], [37.506662674778056, 45.61969388384524], [37.19796622875481, 46.444927481943765], [36.90858417095287, 47.274485178536395], [36.638721616179424, 48.107652604785535], [36.38856278743295, 48.94371055264535], [36.158270854354186, 49.78193565921238], [35.94798779285065, 50.6216010954986], [35.757834266028794, 51.46197725895178], [35.58790952654568, 52.30233246904732], [35.43829134047141, 53.14193366527007], [35.3090359327321, 53.980047106803596], [35.20017795418167, 54.81593907324261], [35.11173047032945, 55.64887656564209], [35.04368497172906, 56.47812800721761], [34.99601140601252, 57.30296394301044], [34.96865823153224, 58.12265773783302], [34.961552492551895, 58.93648627181062], [34.974599915906126, 59.743730632839], [35.00768502902749, 60.54367680527923], [35.06067129921806, 61.335616354215425], [35.13340129402195, 62.11884710460527], [35.225696862534214, 62.89267381465754], [35.337359337460796, 63.65640884277764], [35.46816975772352, 64.40937280742695], [35.617889111384144, 65.15089523925003], [35.786258598640785, 65.88031522483004], [35.97299991463079, 66.59698204144215], [36.17781555175401, 67.30025578218226], [36.40038912121131, 67.98950797085851], [36.64038569343427, 68.66412216604321], [36.89745215706282, 69.32349455369211], [37.17121759611, 69.96703452775148], [37.46129368493396, 70.5941652581821], [37.767275100620964, 71.20432424584489], [38.08873995236454, 71.79696386370284], [38.425250227410274, 72.37155188380925], [38.776352253118695, 72.92757198956546], [39.14157717468284, 73.46452427274542], [39.52044144802173, 73.98192571480013], [39.91244734735576, 74.47931065196943], [40.31708348695592, 74.95623122374559], [40.73382535654387, 75.41225780424828], [41.1621358698074, 75.84697941608812], [41.60146592548216, 76.26000412631285], [42.051254980438095, 76.65095942404778], [42.510931634197796, 77.01949257945994], [42.97991422430148, 77.36527098369383], [43.4576114319241, 77.68798246944569], [43.94342289713845, 77.98733561186113], [44.43673984320961, 78.26306000946163], [44.9369457092966, 78.5149065448237], [45.443416790929255, 78.74264762475569], [45.95552288761989, 78.94607739973573], [46.47262795696309, 79.1250119623963], [46.99409077457018, 79.27928952486037], [47.51926559917878, 79.408770574755], [48.04750284227361, 79.5133380097496], [48.578149741549126, 79.59289725048669], [49.1105510375421, 79.64737633179448], [49.644049652757644, 79.67672597209153], [50.177987372610914, 79.68091962091582], [50.711705527504435, 79.65995348453107], [51.24454567535968, 79.61384652958573], [51.77585028392153, 79.54264046482056], [52.304963412154024, 79.44639970084314], [52.83123139004737, 79.32521128800849], [53.354003496156544, 79.1791848324669], [53.87263263219571, 79.00845239046137], [54.38647599401408, 78.81316834097822], [54.89489573828321, 78.59350923687589], [55.3972596442302, 78.34967363463801], [55.89294176975501, 78.0818819029179], [56.38132310127728, 77.79037601006189], [56.86179219666286, 77.47541929082043], [57.33374582058782, 77.13729619247562], [57.79658957170505, 76.7763120006342], [58.24973850098674, 76.39279254495501], [58.69261772062432, 75.98708388509974], [59.12466300287774, 75.55955197721482], [59.545321368274685, 75.11058232127185], [59.954051662571985, 74.64057958961236], [60.3503251219019, 74.14996723706122], [60.733625925537865, 73.63918709299146], [61.10345173572674, 73.10869893573992], [61.459314224046835, 72.55898004979225], [61.800739583765235, 71.99052476617032], [62.127269027680796, 71.40384398647382], [62.438459270954354, 70.79946469104226], [62.7338829984421, 70.17792943171975], [63.013129316063434, 69.53979580972054], [63.27580418575093, 68.88563593910635], [63.521530843545385, 68.21603589640245], [63.74995020041658, 67.53159515689163], [63.96072122540671, 66.83292601813906], [64.15352131071153, 66.1206530113129], [64.32804661833163, 65.39541230087809], [64.48401240794503, 64.65785107325169], [64.57754692112395, 64.1596231815178]], [[62.165552315024435, 44.799493234609656], [61.818925070526774, 43.98502819263181], [61.4537222033689, 43.17699455723984], [61.07023036246362, 42.376080608667664], [60.66875596930847, 41.582966455736916], [60.24962493335912, 40.79832339188571], [59.81318234774089, 40.022813260172846], [59.35979216559447, 39.257087827877704], [58.88983685737078, 38.50178817130552], [58.40371704940935, 37.75754407139884], [57.901851144153156, 37.024973420744686], [57.38467492237035, 36.30468164255507], [56.85264112777201, 35.59726112218736], [Line truncated
|
||||
Executable
+122
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generates the knot icons' geometry; run via scripts/make-icons.sh."""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
NAVY = "#132441"
|
||||
CORAL = "#FF8A5B"
|
||||
|
||||
|
||||
def trefoil(cx, cy, size, gap):
|
||||
n = 1200
|
||||
pts = []
|
||||
for i in range(n):
|
||||
t = 2 * math.pi * i / n
|
||||
pts.append((math.sin(t) + 2 * math.sin(2 * t), -(math.cos(t) - 2 * math.cos(2 * t))))
|
||||
xs = [p[0] for p in pts]
|
||||
ys = [p[1] for p in pts]
|
||||
mx, my = (max(xs) + min(xs)) / 2, (max(ys) + min(ys)) / 2
|
||||
span = max(max(xs) - min(xs), max(ys) - min(ys))
|
||||
p = [(cx + (x - mx) / span * size, cy + (y - my) / span * size) for x, y in pts]
|
||||
|
||||
# Self-crossings: points far apart along the curve that land on each other.
|
||||
cands = []
|
||||
for i in range(n):
|
||||
for j in range(i + n // 6, n):
|
||||
if j - i > n - n // 6:
|
||||
continue
|
||||
d = (p[i][0] - p[j][0]) ** 2 + (p[i][1] - p[j][1]) ** 2
|
||||
if d < (size * 0.004) ** 2:
|
||||
cands.append((d, i, j))
|
||||
cands.sort()
|
||||
crossings = []
|
||||
for _, i, j in cands:
|
||||
far = lambda a, b: min(abs(a - b) % n, n - abs(a - b) % n) > 40
|
||||
if all(far(i, a) and far(j, b) for a, b in crossings):
|
||||
crossings.append((i, j))
|
||||
if len(crossings) == 3:
|
||||
break
|
||||
assert len(crossings) == 3, crossings
|
||||
|
||||
# Alternate crossings go under; hide points within `gap` (straight-line) of each.
|
||||
visits = sorted([i for i, _ in crossings] + [j for _, j in crossings])
|
||||
under = visits[1::2]
|
||||
hidden = set()
|
||||
for u in under:
|
||||
for d in range(-n // 10, n // 10 + 1):
|
||||
k = (u + d) % n
|
||||
if math.dist(p[k], p[u]) < gap:
|
||||
hidden.add(k)
|
||||
|
||||
strands, cur = [], []
|
||||
start = next(i for i in range(n) if i in hidden)
|
||||
for k in range(1, n + 1):
|
||||
i = (start + k) % n
|
||||
if i in hidden:
|
||||
if len(cur) > 1:
|
||||
strands.append(cur)
|
||||
cur = []
|
||||
else:
|
||||
cur.append(p[i])
|
||||
if len(cur) > 1:
|
||||
strands.append(cur)
|
||||
assert len(strands) == 3
|
||||
return [s[::3] + [s[-1]] for s in strands]
|
||||
|
||||
|
||||
def path(strand):
|
||||
return "M" + " L".join(f"{x:.2f},{y:.2f}" for x, y in strand)
|
||||
|
||||
|
||||
# Each gap is half the crossing strand's width, plus the round cap, plus clear space.
|
||||
ICON_STROKE, GLYPH_STROKE = 8.5, 1.9
|
||||
icon = trefoil(50, 51, 62, ICON_STROKE + 1.3)
|
||||
glyph = trefoil(9, 9.3, 14.2, GLYPH_STROKE + 0.6)
|
||||
|
||||
os.makedirs(os.path.join(ROOT, "assets"), exist_ok=True)
|
||||
json.dump({"icon": icon, "iconStroke": ICON_STROKE, "glyph": glyph, "glyphStroke": GLYPH_STROKE,
|
||||
"navy": NAVY, "coral": CORAL},
|
||||
open(os.path.join(ROOT, "scripts/icons/knot.json"), "w"))
|
||||
|
||||
strokes = "".join(f'<path d="{path(s)}"/>' for s in icon)
|
||||
open(os.path.join(ROOT, "assets/knotchy-icon.svg"), "w").write(
|
||||
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">'
|
||||
f'<rect width="100" height="100" rx="22.5" fill="{NAVY}"/>'
|
||||
f'<g fill="none" stroke="{CORAL}" stroke-width="8.5" stroke-linecap="round" stroke-linejoin="round">{strokes}</g></svg>\n')
|
||||
open(os.path.join(ROOT, "assets/knotchy-mark.svg"), "w").write(
|
||||
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="15 18 70 66">'
|
||||
f'<g fill="none" stroke="{CORAL}" stroke-width="8.5" stroke-linecap="round" stroke-linejoin="round">{strokes}</g></svg>\n')
|
||||
|
||||
points = ",\n".join(
|
||||
" [" + ", ".join(f"CGPoint(x: {x:.2f}, y: {y:.2f})" for x, y in s) + "]" for s in glyph)
|
||||
open(os.path.join(ROOT, "Sources/Knotchy/KnotGlyph.swift"), "w").write(f'''// Generated by scripts/icons/knot.py; run scripts/make-icons.sh to change it.
|
||||
import AppKit
|
||||
|
||||
enum KnotGlyph {{
|
||||
static let strands: [[CGPoint]] = [
|
||||
{points},
|
||||
]
|
||||
|
||||
static let menuBarImage: NSImage = {{
|
||||
let image = NSImage(size: NSSize(width: 18, height: 18), flipped: true) {{ _ in
|
||||
NSColor.black.setStroke()
|
||||
for strand in strands {{
|
||||
let path = NSBezierPath()
|
||||
path.move(to: strand[0])
|
||||
for point in strand.dropFirst() {{ path.line(to: point) }}
|
||||
path.lineWidth = 1.9
|
||||
path.lineCapStyle = .round
|
||||
path.lineJoinStyle = .round
|
||||
path.stroke()
|
||||
}}
|
||||
return true
|
||||
}}
|
||||
image.isTemplate = true
|
||||
image.accessibilityDescription = "Knotchy"
|
||||
return image
|
||||
}}()
|
||||
}}
|
||||
''')
|
||||
print("knot: 3 strands; wrote knot.json, assets/*.svg, Sources/Knotchy/KnotGlyph.swift")
|
||||
@@ -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)")
|
||||
Reference in new issue
Block a user