mirror of
https://github.com/aculix/knotchy.git
synced 2026-10-27 07:15:53 +00:00
123 lines
4.5 KiB
Python
Executable File
123 lines
4.5 KiB
Python
Executable File
#!/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")
|