Walldye

Debian swirl

after Software in the Public Interest, Inc. and Raul Silva, Debian Open Use Logo, 19991

The curve of the Debian swirl follows ten circular arcs that meet without a kink.

Made with Claude Opus 5.5

Free to use, with credit as given above; a changed version must be shared on the same terms.

Technique
technical drawing
Shape
Any screen
Added
28 September 2026

Versions

Export

Shape
cropped from 16:9
Across
Down
Scale
Crop
Size
Format

Colors

  • #1C1B1Abackground
  • #DAD8CEforeground
  • #CF6A4Caccent

Notes

The outline is the official artwork’s, untouched apart from the cuts between arcs. Ten arcs that meet without a kink follow its center line. After the first, each turns through 60 degrees, and every radius is a whole number of units in the original file, so the centers form a small spiral whose sides turn 60 degrees too. Each circle touches the next where the swirl is cut, and the cuts on the outer turn run on as guide lines.

Sources

  1. Software in the Public Interest, Inc. and Raul Silva, Debian Open Use Logo, 1999. ↑

Source code

wallpapers/debian-swirl/design.py, 180 lines

"""The Debian swirl as a construction drawing of ten compass arcs."""

from itertools import pairwise

from shapely import LineString, Point, Polygon, make_valid
from shapely.geometry.base import BaseGeometry
from shapely.ops import unary_union

from walldye import (
    ACCENT,
    BG,
    BG_ALT,
    UI,
    UI_ALT,
    Canvas,
    P,
    Params,
    Rect,
    Vec,
    design,
    knob,
    polar,
)
from walldye.geom import Affine, bezier_points, parts

S = 5.4  # canvas units per point of the official artwork
FADE = 640  # construction lines dissolve into the background by this radius
DASHDOT = (24, 6, 3, 6)
ARROW, ARROW_W = 13, 4.2
GAP = 4  # the seam between two arcs of the swirl
TICK = 5  # half the cross marking a center
LIT = 9  # the arc picked out: the tail
OUTER = 5  # the first join whose seam runs on outwards; the ones before face the curl
# The swirl's center line as tangent arcs, fitted to the official openlogo-nd.svg: every arc
# after the first turns 60 degrees, and the radii are whole points, so the centers are the
# corners of a polygon whose sides turn 60 degrees.
TIP = Vec(61.3, 35.87)  # the inner end, in points
TIP_DEG = -43.7  # its direction from the first center
SWEEPS = (32.9, 60, 60, 60, 60, 60, 60, 60, 60, 60)
RADII = (15, 15, 18, 20, 23, 32, 32, 41, 50, 60)


def chain() -> list[tuple[Vec, float, float]]:
    """Each arc's center, radius and starting direction (degrees from the center), in points,
    from the inner end outwards; each arc runs `SWEEPS[k]` degrees anticlockwise from there."""
    out: list[tuple[Vec, float, float]] = []
    p, deg = TIP, TIP_DEG
    for sweep, r in zip(SWEEPS, RADII, strict=True):
        c = p - polar((0, 0), r, deg=deg)
        out.append((c, r, deg))
        deg -= sweep
        p = polar(c, r, deg=deg)
    return out


ARCS = chain()
BOX = (0.536, 0.0, 87.043, 108.449)  # the artwork's bounds, in points


def outline(ctrl: list[float], m: Affine) -> BaseGeometry:
    """One path of the artwork, a start point then cubic segments, as a valid shapely area."""
    pts = [(ctrl[0], ctrl[1])]
    for i in range(2, len(ctrl), 6):
        seg = [pts[-1], ctrl[i : i + 2], ctrl[i + 2 : i + 4], ctrl[i + 4 : i + 6]]
        pts.extend(map(tuple, bezier_points(seg, 16)[1:].tolist()))
    return make_valid(Polygon(m.apply(pts)))


def sector(c: Vec, r0: float, r1: float, a0: float, a1: float) -> Polygon:
    """The annular sector around `c` between radii r0 and r1, from a0 to a1 degrees."""
    n = max(8, int(abs(a1 - a0) / 2))
    angs = [a0 + (a1 - a0) * t / n for t in range(n + 1)]
    ring = [polar(c, r1, deg=a) for a in angs] + [polar(c, r0, deg=a) for a in reversed(angs)]
    return Polygon(ring)


def solid(g: BaseGeometry) -> BaseGeometry:
    """The areas of `g` over 12 square units; smaller ones are slivers left by the cuts."""
    return unary_union([q for q in parts(g) if isinstance(q, Polygon) and q.area > 12])


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]


class Drawing(Params):
    dimensions: bool = knob(default=True, doc="the picked-out arc's radius and angle")


@design(aspects="any", variants={"undimensioned": Drawing(dimensions=False)})
def draw(s: Canvas[Drawing]) -> None:
    c = s.pick(landscape=(0.64, 0.47), portrait=(0.44, 0.42))
    mid = Vec((BOX[0] + BOX[2]) / 2, (BOX[1] + BOX[3]) / 2)
    m = Affine.scale(S) @ Affine.translate(-mid.x, -mid.y)  # centers the artwork on c
    arcs = [(m(ac), r * S, deg) for ac, r, deg in ARCS]
    pole = Vec(sum(a[0].x for a in arcs) / len(arcs), sum(a[0].y for a in arcs) / len(arcs))
    lc, lr, ldeg = arcs[LIT]
    reach = Rect(pole.x - FADE, pole.y - FADE, 2 * FADE, 2 * FADE)
    loop = Rect(lc.x - lr, lc.y - lr, 2 * lr, 2 * lr)
    with s.motif(c, reach, handle=loop):  # grabbed by the picked-out arc's full circle
        paths: list[list[float]] = s.data("swirl.json")
        mark = unary_union([outline(p, m) for p in paths])
        # every join as (center, radius, direction): the inner end, the nine joins, the tail
        joins = [arcs[0]] + [(a[0], a[1], b[2]) for a, b in pairwise(arcs)]
        joins.append((arcs[-1][0], arcs[-1][1], arcs[-1][2] - SWEEPS[-1]))

        # the swirl cut into its arcs along the radii where they meet; each arc's sector reaches
        # past the band to take in the flecks beside it, and the end sectors run on past the tips
        seams = unary_union(
            [
                LineString([polar(ac, r - 40, deg=deg), polar(ac, r + 70, deg=deg)])
                for ac, r, deg in joins[1:-1]
            ]
        ).buffer(GAP / 2, cap_style="flat")
        sectors: list[BaseGeometry] = []
        for k, ((ac, r, deg), sweep) in enumerate(zip(arcs, SWEEPS, strict=True)):
            lo = deg - sweep - (40 if k == len(arcs) - 1 else 0)
            hi = deg + (40 if k == 0 else 0)
            sectors.append(sector(ac, r - 40, r + 70, lo, hi))
        segs = [solid(mark.intersection(q).difference(seams)) for q in sectors]
        rest = solid(mark.difference(unary_union(sectors)))

        # construction: every arc run on into its full circle, and the outer seams run on
        # outwards, all fading with distance and kept off the swirl
        ban = mark.buffer(4)
        field = Point(pole.x, pole.y).buffer(FADE, quad_segs=128)
        lines = P()
        drawn: list[tuple[Vec, float]] = [(lc, lr)]  # arcs sharing a center share a circle
        for ac, r, _ in arcs:
            if any(abs(r - r2) < 1 and abs(ac - c2) < 1 for c2, r2 in drawn):
                continue
            drawn.append((ac, r))
            ring = LineString(Point(ac.x, ac.y).buffer(r, quad_segs=96).exterior.coords)
            for q in strands(ring.intersection(field).difference(ban)):
                lines.poly(q.coords)
        for k, (ac, r, deg) in enumerate(joins):
            if k < OUTER or k in (LIT, LIT + 1):
                continue
            ray = LineString([polar(ac, r, deg=deg), polar(ac, 2 * FADE, deg=deg)])
            for q in strands(ray.intersection(field).difference(ban)):
                lines.poly(q.coords)
        fade = s.radial_gradient([(0, BG_ALT), (0.55, BG_ALT), (1, BG)], pole, FADE)
        s.stroke(lines, fade, 1.2)

        # the polygon of centers, each center ticked
        centers = P().poly([a[0] for a in arcs])
        for ac, _, _ in arcs:
            centers.M(ac.x - TICK, ac.y).H(ac.x + TICK).M(ac.x, ac.y - TICK).V(ac.y + TICK)
        s.stroke(centers, UI, 1.2)

        # the picked-out arc: the radii to its ends, and its full circle
        a0, a1 = ldeg - SWEEPS[LIT], ldeg
        cl = P()
        for deg in (a0, a1):
            cl.M(lc).L(polar(lc, lr + 70, deg=deg))
        s.stroke(cl.circle(lc, lr), UI, 1.2, dash=DASHDOT)

        if s.params.dimensions:
            # dimensions: the picked-out arc's radius, to the band's inner edge, and the angle it
            # turns through, outside it
            dl, heads = P(), P()
            amid = (a0 + a1) / 2
            leader = LineString([lc, polar(lc, lr, deg=amid)]).intersection(segs[LIT])
            hits = [Vec(*q) for g in parts(leader) for q in g.coords]
            tip = min(hits, key=lambda q: abs(q - lc))
            dl.M(lc).L(tip)
            heads.arrowhead(tip, ARROW, deg=amid, width=ARROW_W)
            rd = lr + 45
            dl.arc(lc, rd, deg=(a0, a1))
            heads.arrowhead(polar(lc, rd, deg=a1), ARROW, deg=a1 + 90, width=ARROW_W)
            heads.arrowhead(polar(lc, rd, deg=a0), ARROW, deg=a0 - 90, width=ARROW_W)
            s.stroke(dl, UI_ALT, 1.2)
            s.fill(heads, UI_ALT)

        # the picked-out arc carries the accent; the rest alternate UI and UI_ALT
        with s.buckets((ACCENT, UI, UI_ALT), "fill") as b:
            for k, g in enumerate(segs):
                b[0 if k == LIT else 1 + k % 2].shape(g)
            b[1].shape(rest)

Run it yourself

$ git clone https://github.com/nickolaj-jepsen/walldye && cd walldye$ uv run walldye render debian-swirl --theme fireproof -o debian-swirl-fireproof-16x9.svg