Arch logo
after Thayer Williams, Arch Linux logo, 20071
Arch Linux’s A, drafted to fit a square. Its flanks and arcs run on past the corners as guide lines, and the peak above the notch is filled in.
Made with Claude Opus 5.5
Unofficial fan tribute, not affiliated with or endorsed by Arch Linux. Arch Linux and its characters are trademarks of their owners. Non-commercial; contact [email protected] for takedown.
- Technique
- technical drawing
- Shape
- Any screen
- Added
- 28 September 2026
Colours
- #1C1B1Abackground
- #DAD8CEforeground
- #CF6A4Caccent
Export
Notes
The letter is as tall as it is wide, and its apex and both feet lie on the dash-dot circle. Its corners and the centres of its arcs sit on a grid of 48 squares across the base, measured off the official artwork. The flanks curve in very slightly: each is an arc eight to ten times the letter’s width in radius. The base is a pointed arch of two arcs, each drawn from a centre on the far side of the axis, 61 squares below the letter. Each notch is cut between two arcs that meet at its tip. The upper arc of the left notch, continued across the letter, marks off the peak, and its radius is drawn from its centre. The opening is an ellipse 11 squares wide and 18 tall. Carried on through the letter, the left base arc comes out at the notch on the right.
Sources
- Thayer Williams, Arch Linux logo, 2007. ↑
Source code
wallpapers/arch-logo/design.py, 165 lines
"""The Arch Linux logo as a construction drawing: flanks and arcs snapped to a grid, run on as fading guide lines, the peak picked out."""
import math
from shapely import LineString, Point, Polygon, box
from shapely.affinity import scale
from shapely.geometry.base import BaseGeometry
from shapely.ops import linemerge, unary_union
from walldye import ACCENT, BG, BG_ALT, UI, UI_ALT, Canvas, P, Vec, design
from walldye.geom import parts
G = 10 # the grid unit; the mark is 48 units wide and 48 tall
FADE = 620 # construction lines dissolve into the background by this radius
DASHDOT = (24, 6, 3, 6)
ARROW, ARROW_W = 13, 4.2
# Grid points, y up from the middle of the base, snapped from the official archlinux-logo SVG
APEX, FOOT = (0, 48), (24, 0)
MID = 18 # height of the circumscribed circle's centre
FLANK_R = (384, 480) # left and right: both flanks are faintly hollow arcs
BASE_C = (14, -61) # centre of the left base arc, across the axis; mirrored for the right
CUT_C, CUT_R = (0, 11), (5.5, 9) # the inner cut, an ellipse: centre and semi-axes
NOTCH_L = ((0, 29), (22, 59), (10, 54)) # tip, then the centres of its upper and lower edges
NOTCH_R = ((12, 13), (-7, -23), (7, -10)) # tip, then the centres of its lower and upper edges
def disc(c: Vec, r: float) -> Polygon:
return Point(c.x, c.y).buffer(r, quad_segs=256)
def arc_centre(a: Vec, b: Vec, r: float, side: int) -> Vec:
"""Centre of the circle of radius `r` through `a` and `b`, on `side` (1 or -1) of a to b."""
m, d = (a + b) / 2, b - a
return m + d.perp().unit() * side * math.sqrt(r * r - abs(d) ** 2 / 4)
def outline(g: BaseGeometry) -> list[Vec]:
"""The vertices of the outer rings of `g`'s polygons."""
return [Vec(v[0], v[1]) for q in parts(g) if isinstance(q, Polygon) for v in q.exterior.coords]
def corners(g: BaseGeometry) -> list[Point]:
"""The vertices of `g`'s outline where it turns by more than 12 degrees."""
out = []
for q in parts(g):
if not isinstance(q, Polygon):
continue
pts = q.exterior.coords[:-1]
for a, b, c in zip(pts[-1:] + pts[:-1], pts, pts[1:] + pts[:1], strict=True):
t0 = math.atan2(b[1] - a[1], b[0] - a[0])
t1 = math.atan2(c[1] - b[1], c[0] - b[0])
if abs((t1 - t0 + math.pi) % (2 * math.pi) - math.pi) > math.radians(12):
out.append(Point(b))
return out
def strands(g: BaseGeometry) -> list[LineString]:
"""The line parts of `g` longer than 2 units; shorter ones are boolean slivers."""
return [q for q in parts(g) if isinstance(q, LineString) and q.length > 2]
@design(aspects="any")
def draw(s: Canvas) -> None:
c = s.pick(landscape=(31 / 48, 0.5), portrait=(0.5, 0.42))
def at(x: float, y: float) -> Vec:
return Vec(c.x + x * G, c.y - (y - MID) * G)
apex, fl, fr = at(*APEX), at(-FOOT[0], FOOT[1]), at(*FOOT)
R = abs(apex - c) # the circumscribed circle, through the apex and both feet
# the triangle in its square, both flanks hollowed by a long arc
flanks = [
(arc_centre(apex, fl, FLANK_R[0] * G, 1), FLANK_R[0] * G),
(arc_centre(apex, fr, FLANK_R[1] * G, -1), FLANK_R[1] * G),
]
hull = Polygon([apex, fl, fr]).difference(unary_union([disc(*o) for o in flanks]))
# the base: two arcs, each struck from across the axis, meeting in a pointed arch
bl, br = at(*BASE_C), at(-BASE_C[0], BASE_C[1])
rb = abs(fl - bl)
long_arcs = [*flanks, (bl, rb), (br, rb)]
under = disc(bl, rb).intersection(disc(br, rb))
# the inner cut: an ellipse standing on the axis
e, (ex, ey) = at(*CUT_C), (CUT_R[0] * G, CUT_R[1] * G)
cut = scale(disc(e, 1), ex, ey, origin=(e.x, e.y))
# two notches, each a sliver between two arcs through its tip
notches, notch_arcs = [], []
for tip, *centres in (NOTCH_L, NOTCH_R):
t = at(*tip)
(a, ra), (b, rb_) = [(at(*o), abs(t - at(*o))) for o in centres]
notch_arcs += [(a, ra), (b, rb_)]
side = box(0, 0, t.x, s.h) if tip[0] <= 0 else box(t.x, 0, s.w, s.h)
sliver = disc(a, ra).symmetric_difference(disc(b, rb_)).intersection(side)
notches.append(sliver.intersection(Point(t.x, t.y).buffer(12 * G)))
mark = hull.difference(unary_union([under, cut, *notches]))
# the peak: the left notch's upper arc, run on across the mark
a0 = at(*NOTCH_L[1])
peak = mark.intersection(disc(a0, abs(at(*NOTCH_L[0]) - a0)))
body = mark.difference(peak)
# construction: each edge's line or circle, run on from the mark's corners into the fading
# field, the notches' arcs only as far as the circumscribed circle; the inner cut, the
# notches and the pointed arch under the mark stay clear
field = Point(c.x, c.y).buffer(FADE, quad_segs=128)
inner = Point(c.x, c.y).buffer(R, quad_segs=128)
solid = mark.buffer(2.5, join_style="mitre")
arch = under.intersection(box(0, 0, s.w, fl.y))
clear = unary_union([cut, *notches, arch]).intersection(hull).buffer(2)
ends = corners(mark)
runs: list[tuple[BaseGeometry, Polygon]] = [
*[(disc(o, r).exterior, field) for o, r in long_arcs],
*[(disc(o, r).exterior, inner) for o, r in notch_arcs],
(LineString([(c.x - 2 * FADE, fl.y), (c.x + 2 * FADE, fl.y)]), field),
]
lines = P()
for run, bound in runs:
own = [v.buffer(5) for v in ends if v.distance(run) < 1] # the corners on this edge
pieces = run.intersection(bound).difference(solid).difference(clear)
for q in strands(linemerge(strands(pieces))): # rejoin the ring at its start point
if any(q.intersects(v) for v in own):
lines.poly(q.coords)
fade = s.radial_gradient([(0, BG_ALT), (R * 1.2 / FADE, BG_ALT), (1, BG)], c, FADE)
s.stroke(lines, fade, 1.2)
# the axis, the inner cut's cross axis and the circumscribed circle
cl = P().M(c.x, apex.y - 60).V(c.y + R + 60).M(e.x - ex - 30, e.y).H(e.x + ex + 30)
s.stroke(cl.circle(c, R), UI, 1.2, dash=DASHDOT)
# the peak's arc: its centre, and its radius to where it meets the right flank
ext, dl, heads = P(), P(), P()
ext.M(a0.x - 7, a0.y).H(a0.x + 7).M(a0.x, a0.y - 7).V(a0.y + 7)
end = max(outline(peak), key=lambda v: v.x)
u = (end - a0).unit()
dl.M(a0).L(end - u * ARROW)
heads.arrowhead(end, ARROW, rad=math.atan2(u.y, u.x), width=ARROW_W)
# dimensions: the overall width below, the inner cut's width above it, the height at left
yd = c.y + R + 80
for x in (fl.x, fr.x):
ext.M(x, fl.y + 14).V(yd + 12)
dl.M(fl.x, yd).H(fr.x)
heads.arrowhead((fr.x, yd), ARROW, deg=0, width=ARROW_W)
heads.arrowhead((fl.x, yd), ARROW, deg=180, width=ARROW_W)
x0, x1, yw = e.x - ex, e.x + ex, e.y
yc = yd - 36
for x in (x0, x1):
ext.M(x, yw + 12).V(yc + 12)
dl.M(x0 - 34, yc).H(x0).M(x1, yc).H(x1 + 34)
heads.arrowhead((x0, yc), ARROW, deg=0, width=ARROW_W)
heads.arrowhead((x1, yc), ARROW, deg=180, width=ARROW_W)
xh = fl.x - 80
ext.M(apex.x - 14, apex.y).H(xh - 12).M(fl.x - 14, fl.y).H(xh - 12)
dl.M(xh, apex.y).V(fl.y)
heads.arrowhead((xh, apex.y), ARROW, deg=-90, width=ARROW_W)
heads.arrowhead((xh, fl.y), ARROW, deg=90, width=ARROW_W)
s.stroke(ext, UI, 1.2)
s.stroke(dl, UI_ALT, 1.2)
s.fill(heads, UI_ALT)
s.fill(P().shape(body), UI_ALT)
s.fill(P().shape(peak), ACCENT)"""The Arch Linux logo as a construction drawing: flanks and arcs snapped to a grid, run on as fading guide lines, the peak picked out."""
import math
from shapely import LineString, Point, Polygon, box
from shapely.affinity import scale
from shapely.geometry.base import BaseGeometry
from shapely.ops import linemerge, unary_union
from walldye import ACCENT, BG, BG_ALT, UI, UI_ALT, Canvas, P, Vec, design
from walldye.geom import parts
G = 10 # the grid unit; the mark is 48 units wide and 48 tall
FADE = 620 # construction lines dissolve into the background by this radius
DASHDOT = (24, 6, 3, 6)
ARROW, ARROW_W = 13, 4.2
# Grid points, y up from the middle of the base, snapped from the official archlinux-logo SVG
APEX, FOOT = (0, 48), (24, 0)
MID = 18 # height of the circumscribed circle's centre
FLANK_R = (384, 480) # left and right: both flanks are faintly hollow arcs
BASE_C = (14, -61) # centre of the left base arc, across the axis; mirrored for the right
CUT_C, CUT_R = (0, 11), (5.5, 9) # the inner cut, an ellipse: centre and semi-axes
NOTCH_L = ((0, 29), (22, 59), (10, 54)) # tip, then the centres of its upper and lower edges
NOTCH_R = ((12, 13), (-7, -23), (7, -10)) # tip, then the centres of its lower and upper edges
def disc(c: Vec, r: float) -> Polygon:
return Point(c.x, c.y).buffer(r, quad_segs=256)
def arc_centre(a: Vec, b: Vec, r: float, side: int) -> Vec:
"""Centre of the circle of radius `r` through `a` and `b`, on `side` (1 or -1) of a to b."""
m, d = (a + b) / 2, b - a
return m + d.perp().unit() * side * math.sqrt(r * r - abs(d) ** 2 / 4)
def outline(g: BaseGeometry) -> list[Vec]:
"""The vertices of the outer rings of `g`'s polygons."""
return [Vec(v[0], v[1]) for q in parts(g) if isinstance(q, Polygon) for v in q.exterior.coords]
def corners(g: BaseGeometry) -> list[Point]:
"""The vertices of `g`'s outline where it turns by more than 12 degrees."""
out = []
for q in parts(g):
if not isinstance(q, Polygon):
continue
pts = q.exterior.coords[:-1]
for a, b, c in zip(pts[-1:] + pts[:-1], pts, pts[1:] + pts[:1], strict=True):
t0 = math.atan2(b[1] - a[1], b[0] - a[0])
t1 = math.atan2(c[1] - b[1], c[0] - b[0])
if abs((t1 - t0 + math.pi) % (2 * math.pi) - math.pi) > math.radians(12):
out.append(Point(b))
return out
def strands(g: BaseGeometry) -> list[LineString]:
"""The line parts of `g` longer than 2 units; shorter ones are boolean slivers."""
return [q for q in parts(g) if isinstance(q, LineString) and q.length > 2]
@design(aspects="any")
def draw(s: Canvas) -> None:
c = s.pick(landscape=(31 / 48, 0.5), portrait=(0.5, 0.42))
def at(x: float, y: float) -> Vec:
return Vec(c.x + x * G, c.y - (y - MID) * G)
apex, fl, fr = at(*APEX), at(-FOOT[0], FOOT[1]), at(*FOOT)
R = abs(apex - c) # the circumscribed circle, through the apex and both feet
# the triangle in its square, both flanks hollowed by a long arc
flanks = [
(arc_centre(apex, fl, FLANK_R[0] * G, 1), FLANK_R[0] * G),
(arc_centre(apex, fr, FLANK_R[1] * G, -1), FLANK_R[1] * G),
]
hull = Polygon([apex, fl, fr]).difference(unary_union([disc(*o) for o in flanks]))
# the base: two arcs, each struck from across the axis, meeting in a pointed arch
bl, br = at(*BASE_C), at(-BASE_C[0], BASE_C[1])
rb = abs(fl - bl)
long_arcs = [*flanks, (bl, rb), (br, rb)]
under = disc(bl, rb).intersection(disc(br, rb))
# the inner cut: an ellipse standing on the axis
e, (ex, ey) = at(*CUT_C), (CUT_R[0] * G, CUT_R[1] * G)
cut = scale(disc(e, 1), ex, ey, origin=(e.x, e.y))
# two notches, each a sliver between two arcs through its tip
notches, notch_arcs = [], []
for tip, *centres in (NOTCH_L, NOTCH_R):
t = at(*tip)
(a, ra), (b, rb_) = [(at(*o), abs(t - at(*o))) for o in centres]
notch_arcs += [(a, ra), (b, rb_)]
side = box(0, 0, t.x, s.h) if tip[0] <= 0 else box(t.x, 0, s.w, s.h)
sliver = disc(a, ra).symmetric_difference(disc(b, rb_)).intersection(side)
notches.append(sliver.intersection(Point(t.x, t.y).buffer(12 * G)))
mark = hull.difference(unary_union([under, cut, *notches]))
# the peak: the left notch's upper arc, run on across the mark
a0 = at(*NOTCH_L[1])
peak = mark.intersection(disc(a0, abs(at(*NOTCH_L[0]) - a0)))
body = mark.difference(peak)
# construction: each edge's line or circle, run on from the mark's corners into the fading
# field, the notches' arcs only as far as the circumscribed circle; the inner cut, the
# notches and the pointed arch under the mark stay clear
field = Point(c.x, c.y).buffer(FADE, quad_segs=128)
inner = Point(c.x, c.y).buffer(R, quad_segs=128)
solid = mark.buffer(2.5, join_style="mitre")
arch = under.intersection(box(0, 0, s.w, fl.y))
clear = unary_union([cut, *notches, arch]).intersection(hull).buffer(2)
ends = corners(mark)
runs: list[tuple[BaseGeometry, Polygon]] = [
*[(disc(o, r).exterior, field) for o, r in long_arcs],
*[(disc(o, r).exterior, inner) for o, r in notch_arcs],
(LineString([(c.x - 2 * FADE, fl.y), (c.x + 2 * FADE, fl.y)]), field),
]
lines = P()
for run, bound in runs:
own = [v.buffer(5) for v in ends if v.distance(run) < 1] # the corners on this edge
pieces = run.intersection(bound).difference(solid).difference(clear)
for q in strands(linemerge(strands(pieces))): # rejoin the ring at its start point
if any(q.intersects(v) for v in own):
lines.poly(q.coords)
fade = s.radial_gradient([(0, BG_ALT), (R * 1.2 / FADE, BG_ALT), (1, BG)], c, FADE)
s.stroke(lines, fade, 1.2)
# the axis, the inner cut's cross axis and the circumscribed circle
cl = P().M(c.x, apex.y - 60).V(c.y + R + 60).M(e.x - ex - 30, e.y).H(e.x + ex + 30)
s.stroke(cl.circle(c, R), UI, 1.2, dash=DASHDOT)
# the peak's arc: its centre, and its radius to where it meets the right flank
ext, dl, heads = P(), P(), P()
ext.M(a0.x - 7, a0.y).H(a0.x + 7).M(a0.x, a0.y - 7).V(a0.y + 7)
end = max(outline(peak), key=lambda v: v.x)
u = (end - a0).unit()
dl.M(a0).L(end - u * ARROW)
heads.arrowhead(end, ARROW, rad=math.atan2(u.y, u.x), width=ARROW_W)
# dimensions: the overall width below, the inner cut's width above it, the height at left
yd = c.y + R + 80
for x in (fl.x, fr.x):
ext.M(x, fl.y + 14).V(yd + 12)
dl.M(fl.x, yd).H(fr.x)
heads.arrowhead((fr.x, yd), ARROW, deg=0, width=ARROW_W)
heads.arrowhead((fl.x, yd), ARROW, deg=180, width=ARROW_W)
x0, x1, yw = e.x - ex, e.x + ex, e.y
yc = yd - 36
for x in (x0, x1):
ext.M(x, yw + 12).V(yc + 12)
dl.M(x0 - 34, yc).H(x0).M(x1, yc).H(x1 + 34)
heads.arrowhead((x0, yc), ARROW, deg=0, width=ARROW_W)
heads.arrowhead((x1, yc), ARROW, deg=180, width=ARROW_W)
xh = fl.x - 80
ext.M(apex.x - 14, apex.y).H(xh - 12).M(fl.x - 14, fl.y).H(xh - 12)
dl.M(xh, apex.y).V(fl.y)
heads.arrowhead((xh, apex.y), ARROW, deg=-90, width=ARROW_W)
heads.arrowhead((xh, fl.y), ARROW, deg=90, width=ARROW_W)
s.stroke(ext, UI, 1.2)
s.stroke(dl, UI_ALT, 1.2)
s.fill(heads, UI_ALT)
s.fill(P().shape(body), UI_ALT)
s.fill(P().shape(peak), ACCENT)
Run it yourself
$ git clone https://github.com/nickolaj-jepsen/walldye && cd walldye$ uv run walldye render arch-logo --theme fireproof -o arch-logo-fireproof-16x9.svg