#!/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'' for s in icon) open(os.path.join(ROOT, "assets/knotchy-icon.svg"), "w").write( f'' f'' f'{strokes}\n') open(os.path.join(ROOT, "assets/knotchy-mark.svg"), "w").write( f'' f'{strokes}\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")