Compact Cassette
A Compact Cassette in two scale views, one roller magnified in a circle. Its tape runs lit from spool to spool.
Made with Claude Opus 5.5
- Technique
- technical drawing
- Shape
- 16:9 (cropped for other screens)
- Added
- 27 September 2026
Colours
- #1C1B1Abackground
- #DAD8CEforeground
- #CF6A4Caccent
Export
FormatThis browser can’t make WebP files.
Shapecropped from 16:9
Crop
SizeThis browser can’t draw a file that large.
Notes
The front view is drawn as if the shell were clear, so the tape wound on both spools shows through the label area. Below it, the bottom edge shows the tape through its three openings, with dashed lines tracing it behind the shell.
Sources
- Philips, Compact Cassette, 1963.
Source code
wallpapers/cassette/design.py, 222 lines
"""A Compact Cassette drafted in front, bottom and magnified detail views; only its tape is picked out."""
import math
from itertools import pairwise
import numpy as np
from walldye import (
ACCENT,
ACCENT_4,
ACCENT_5,
ACCENT_6,
ACCENT_7,
BG,
BG_ALT,
UI,
UI_ALT,
UI_HI,
Canvas,
P,
Path,
Point,
Vec,
design,
polar,
)
from walldye.geom import Affine
# The cassette is measured in mm about the centre of its front face; S scales it onto the sheet.
S = 7.5 # px per mm
CX, CY = 1180, 486 # front view centre
BY = 852 # bottom view centre line
WD, HT, THK = 100.4, 63.8, 8.7 # shell width, height and thickness
HUB_X, HUB_Y, HUB_R = 21.3, -3.0, 8.5
PACK_L, PACK_R = 20.5, 12.0 # outer radii of the supply and take-up packs
TURN = 0.42 # spacing of the drawn turns in a pack
ROLLER, ROLLER_R = Vec(42.4, 25.2), 2.3
PIN, PIN_R = Vec(32.4, 28.4), 1.1
SCREWS = ((-46, -28), (46, -28), (-47, 18), (47, 18), (0, 17))
OPENINGS = ((-19.5, -12.5), (-6.0, 6.0), (12.5, 19.5)) # windows in the bottom edge
DETAIL_C, DETAIL_R, DETAIL_K = Vec(-42.0, 27.0), 6.6, 3.1 # magnified region and its scale
DETAIL_AT = Vec(560, CY + DETAIL_C.y * S) # level with the region, so the leader runs flat
TAPE_W = 3.81
DASHDOT = (22, 6, 3, 6)
ARROW, ARROW_W = 12, 3.8 # dimension arrowhead length and half-width
# The tape's route: supply pack, roller, pin, past the head, pin, roller, take-up pack. WANTS
# picks, for each span between two circles, the outer tangent whose normal points nearest it.
CHAIN = (
(Vec(-HUB_X, HUB_Y), PACK_L),
(Vec(-ROLLER.x, ROLLER.y), ROLLER_R),
(Vec(-PIN.x, PIN.y), PIN_R),
(PIN, PIN_R),
(ROLLER, ROLLER_R),
(Vec(HUB_X, HUB_Y), PACK_R),
)
WANTS = (math.pi, math.pi / 2, math.pi / 2, math.pi / 2, 0.0)
def tangent(a: Vec, ra: float, b: Vec, rb: float, want: float) -> float:
"""Normal angle (rad) of the outer tangent of circles `a` and `b` on the side nearest `want`."""
th = math.atan2(b.y - a.y, b.x - a.x)
k = math.acos((ra - rb) / abs(b - a))
return min((th + k, th - k), key=lambda p: abs(math.remainder(p - want, 2 * math.pi)))
def tape(t: Affine, k: float) -> Path:
"""The tape from a quarter turn round the supply pack to a quarter turn onto the take-up
pack, as tangent lines and arcs; `t` maps mm to the canvas at `k` px per mm."""
phis = [tangent(a, ra, b, rb, w) for ((a, ra), (b, rb)), w in zip(pairwise(CHAIN), WANTS)]
# the tape wraps every circle on the outside, so its normal angle only decreases
spans = [(phis[0] + math.pi / 2, phis[0])]
spans += [(p0, p0 - (p0 - p1) % (2 * math.pi)) for p0, p1 in pairwise(phis)]
spans.append((phis[-1], phis[-1] - math.pi / 2))
d = P()
for (c, r), (a0, a1) in zip(CHAIN, spans):
start = t(polar(c, r, rad=a0))
d.M(start) if d.empty else d.L(start)
d.A(r * k, r * k, 0, a0 - a1 > math.pi, False, t(polar(c, r, rad=a1)))
return d
def front(s: Canvas, t: Affine, k: float) -> None:
"""The front view; `t` maps mm to the canvas at `k` px per mm, so line weights stay put."""
def rbox(d: Path, a: Point, b: Point, r: float) -> None:
p, q = t(a), t(b)
d.rrect(p.x, p.y, q.x - p.x, q.y - p.y, r * k)
shell = P()
rbox(shell, (-WD / 2, -HT / 2), (WD / 2, HT / 2), 3)
# head-area bridge and the openings in the bottom edge
shell.M(t((-37.5, HT / 2))).L(t((-31, 21.5))).L(t((31, 21.5))).L(t((37.5, HT / 2)))
for a, b in OPENINGS:
shell.M(t((a, HT / 2))).L(t((a, HT / 2 - 3.6))).L(t((b, HT / 2 - 3.6))).L(t((b, HT / 2)))
s.stroke(shell, UI_HI, 1.6, join="round")
detail = P()
rbox(detail, (-45, -27.5), (45, 14.5), 2) # label recess
rbox(detail, (-30, -11.5), (30, 5.5), 2.2) # window
for sx in (-1, 1): # write-protect tabs
a, b = sorted((sx * 38, sx * 44))
detail.M(t((a, -HT / 2))).L(t((a, -HT / 2 + 3))).L(t((b, -HT / 2 + 3))).L(t((b, -HT / 2)))
for c in SCREWS:
detail.circle(t(c), 1.7 * k)
for sx in (-1, 1):
roller, pin = t((sx * ROLLER.x, ROLLER.y)), t((sx * PIN.x, PIN.y))
detail.circle(roller, ROLLER_R * k)
detail.circle(roller, 0.8 * k)
detail.circle(pin, PIN_R * k)
rbox(detail, (-3.2, HT / 2 - 5.6), (3.2, HT / 2 - 4.2), 0.3) # pressure pad
s.stroke(detail, UI_ALT, 1.3, join="round")
rules = P()
for i in range(4):
rules.M(t((-40, -23.5 + i * 3.4))).L(t((40, -23.5 + i * 3.4)))
s.stroke(rules, BG_ALT, 1.2)
# wound packs, drawn as if the shell were clear: they cover the label lines
discs, turns, edge, hub, teeth = P(), P(), P(), P(), P()
for sx, pack, spin in ((-1, PACK_L, 0.26), (1, PACK_R, 0.7)):
c = t((sx * HUB_X, HUB_Y))
discs.circle(c, pack * k)
edge.circle(c, pack * k)
for r in np.arange(HUB_R + 0.6, pack - 0.3, TURN):
turns.circle(c, r * k)
hub.circle(c, HUB_R * k)
hub.circle(c, 6.2 * k)
for i in range(6):
a = spin + i * math.pi / 3
teeth.M(polar(c, 6.2 * k, rad=a)).L(polar(c, 4.6 * k, rad=a))
s.fill(discs, BG)
s.stroke(turns, ACCENT_7, 1)
s.stroke(edge, ACCENT_5, 1.4)
s.stroke(hub, UI_ALT, 1.3)
s.stroke(teeth, UI_ALT, 2.4, cap="round")
s.stroke(tape(t, k), ACCENT, 2.4, cap="round", join="round")
def dim(lines: Path, heads: Path, a: Vec, b: Vec) -> None:
"""A dimension line from `a` to `b` with an arrowhead at each end, pointing outwards."""
lines.M(a).L(b)
ang = math.atan2(b.y - a.y, b.x - a.x)
heads.arrowhead(b, ARROW, rad=ang, width=ARROW_W)
heads.arrowhead(a, ARROW, rad=ang + math.pi, width=ARROW_W)
@design()
def draw(s: Canvas) -> None:
t = Affine.frame((CX, CY), deg=0, scale=S)
x0, y0 = t((-WD / 2, -HT / 2))
x1, y1 = t((WD / 2, HT / 2))
front(s, t, S)
# bottom view: the edge with the head openings, tape seen face-on inside them
bt, bb = BY - THK / 2 * S, BY + THK / 2 * S
bottom, band, edges = P(), P(), P()
bottom.rrect(x0, bt, x1 - x0, bb - bt, 1.2 * S)
for a, b in OPENINGS:
xa, xb = t((a, 0)).x, t((b, 0)).x
bottom.rect(xa, bt + 1.2 * S, xb - xa, bb - bt - 2.4 * S)
band.rect(xa, BY - TAPE_W / 2 * S, xb - xa, TAPE_W * S)
for e in (-1, 1):
edges.M(xa, BY + e * TAPE_W / 2 * S).H(xb)
for sx in (-1, 1):
bottom.M(t((sx * 37.5, 0)).x, bt).V(bb)
s.stroke(bottom, UI_HI, 1.5, join="round")
cl, proj = P(), P()
cl.M(x0 - 34, CY + HUB_Y * S).H(x1 + 34)
for sx in (-1, 1):
hx = t((sx * HUB_X, 0)).x
cl.M(hx, CY + (HUB_Y - PACK_L) * S - 18).V(CY + (HUB_Y + PACK_L) * S + 18)
cl.M(CX, y0 - 30).V(bb + 30)
cl.M(x0 - 34, BY).H(x1 + 34)
s.stroke(cl, UI, 1.2, dash=DASHDOT)
for a, b in OPENINGS:
for x in (a, b):
proj.M(t((x, 0)).x, y1 + 10).V(bt - 10)
s.stroke(proj, BG_ALT, 1.2)
s.fill(band, ACCENT_5)
# the ribbon's edges run on as hidden lines behind the shell, pin to pin
hidden = P()
for e in (-1, 1):
hidden.M(t((-PIN.x, 0)).x, BY + e * TAPE_W / 2 * S).H(t((PIN.x, 0)).x)
s.stroke(hidden, ACCENT_6, 1.2, dash=(8, 5))
s.stroke(edges, ACCENT_4, 1.3)
# dimensions: overall width above, height and thickness to the right
ext, dl, heads = P(), P(), P()
yd = y0 - 68
for x in (x0, x1):
ext.M(x, y0 - 10).V(yd - 12)
dim(dl, heads, Vec(x0, yd), Vec(x1, yd))
xd = x1 + 64
for y in (y0, y1, bt, bb):
ext.M(x1 + 10, y).H(xd + 12)
dim(dl, heads, Vec(xd, y0), Vec(xd, y1))
dl.M(xd, bt - 44).V(bt).M(xd, bb).V(bb + 44)
heads.arrowhead((xd, bt), ARROW, deg=90, width=ARROW_W)
heads.arrowhead((xd, bb), ARROW, deg=-90, width=ARROW_W)
# detail view: the tape turning the corner roller, magnified off to the left
dr = DETAIL_R * S * DETAIL_K
zoom = Affine.frame(DETAIL_AT, deg=0, scale=S * DETAIL_K) @ Affine.translate(
-DETAIL_C.x, -DETAIL_C.y
)
with s.clip() as lens:
lens.add(P().circle(DETAIL_AT, dr))
with s.group(clip_path=lens.ref):
front(s, zoom, S * DETAIL_K)
src = t(DETAIL_C)
rings = P().circle(src, DETAIL_R * S)
s.stroke(rings.circle(DETAIL_AT, dr), UI_ALT, 1.3, dash=(10, 6))
# leader between the two circles, rim to rim
u = (DETAIL_AT - src).unit()
dl.M(src + u * (DETAIL_R * S + 6)).L(DETAIL_AT - u * (dr + 6))
s.stroke(ext, UI, 1.2)
s.stroke(dl, UI_ALT, 1.2)
s.fill(heads, UI_ALT)"""A Compact Cassette drafted in front, bottom and magnified detail views; only its tape is picked out."""
import math
from itertools import pairwise
import numpy as np
from walldye import (
ACCENT,
ACCENT_4,
ACCENT_5,
ACCENT_6,
ACCENT_7,
BG,
BG_ALT,
UI,
UI_ALT,
UI_HI,
Canvas,
P,
Path,
Point,
Vec,
design,
polar,
)
from walldye.geom import Affine
# The cassette is measured in mm about the centre of its front face; S scales it onto the sheet.
S = 7.5 # px per mm
CX, CY = 1180, 486 # front view centre
BY = 852 # bottom view centre line
WD, HT, THK = 100.4, 63.8, 8.7 # shell width, height and thickness
HUB_X, HUB_Y, HUB_R = 21.3, -3.0, 8.5
PACK_L, PACK_R = 20.5, 12.0 # outer radii of the supply and take-up packs
TURN = 0.42 # spacing of the drawn turns in a pack
ROLLER, ROLLER_R = Vec(42.4, 25.2), 2.3
PIN, PIN_R = Vec(32.4, 28.4), 1.1
SCREWS = ((-46, -28), (46, -28), (-47, 18), (47, 18), (0, 17))
OPENINGS = ((-19.5, -12.5), (-6.0, 6.0), (12.5, 19.5)) # windows in the bottom edge
DETAIL_C, DETAIL_R, DETAIL_K = Vec(-42.0, 27.0), 6.6, 3.1 # magnified region and its scale
DETAIL_AT = Vec(560, CY + DETAIL_C.y * S) # level with the region, so the leader runs flat
TAPE_W = 3.81
DASHDOT = (22, 6, 3, 6)
ARROW, ARROW_W = 12, 3.8 # dimension arrowhead length and half-width
# The tape's route: supply pack, roller, pin, past the head, pin, roller, take-up pack. WANTS
# picks, for each span between two circles, the outer tangent whose normal points nearest it.
CHAIN = (
(Vec(-HUB_X, HUB_Y), PACK_L),
(Vec(-ROLLER.x, ROLLER.y), ROLLER_R),
(Vec(-PIN.x, PIN.y), PIN_R),
(PIN, PIN_R),
(ROLLER, ROLLER_R),
(Vec(HUB_X, HUB_Y), PACK_R),
)
WANTS = (math.pi, math.pi / 2, math.pi / 2, math.pi / 2, 0.0)
def tangent(a: Vec, ra: float, b: Vec, rb: float, want: float) -> float:
"""Normal angle (rad) of the outer tangent of circles `a` and `b` on the side nearest `want`."""
th = math.atan2(b.y - a.y, b.x - a.x)
k = math.acos((ra - rb) / abs(b - a))
return min((th + k, th - k), key=lambda p: abs(math.remainder(p - want, 2 * math.pi)))
def tape(t: Affine, k: float) -> Path:
"""The tape from a quarter turn round the supply pack to a quarter turn onto the take-up
pack, as tangent lines and arcs; `t` maps mm to the canvas at `k` px per mm."""
phis = [tangent(a, ra, b, rb, w) for ((a, ra), (b, rb)), w in zip(pairwise(CHAIN), WANTS)]
# the tape wraps every circle on the outside, so its normal angle only decreases
spans = [(phis[0] + math.pi / 2, phis[0])]
spans += [(p0, p0 - (p0 - p1) % (2 * math.pi)) for p0, p1 in pairwise(phis)]
spans.append((phis[-1], phis[-1] - math.pi / 2))
d = P()
for (c, r), (a0, a1) in zip(CHAIN, spans):
start = t(polar(c, r, rad=a0))
d.M(start) if d.empty else d.L(start)
d.A(r * k, r * k, 0, a0 - a1 > math.pi, False, t(polar(c, r, rad=a1)))
return d
def front(s: Canvas, t: Affine, k: float) -> None:
"""The front view; `t` maps mm to the canvas at `k` px per mm, so line weights stay put."""
def rbox(d: Path, a: Point, b: Point, r: float) -> None:
p, q = t(a), t(b)
d.rrect(p.x, p.y, q.x - p.x, q.y - p.y, r * k)
shell = P()
rbox(shell, (-WD / 2, -HT / 2), (WD / 2, HT / 2), 3)
# head-area bridge and the openings in the bottom edge
shell.M(t((-37.5, HT / 2))).L(t((-31, 21.5))).L(t((31, 21.5))).L(t((37.5, HT / 2)))
for a, b in OPENINGS:
shell.M(t((a, HT / 2))).L(t((a, HT / 2 - 3.6))).L(t((b, HT / 2 - 3.6))).L(t((b, HT / 2)))
s.stroke(shell, UI_HI, 1.6, join="round")
detail = P()
rbox(detail, (-45, -27.5), (45, 14.5), 2) # label recess
rbox(detail, (-30, -11.5), (30, 5.5), 2.2) # window
for sx in (-1, 1): # write-protect tabs
a, b = sorted((sx * 38, sx * 44))
detail.M(t((a, -HT / 2))).L(t((a, -HT / 2 + 3))).L(t((b, -HT / 2 + 3))).L(t((b, -HT / 2)))
for c in SCREWS:
detail.circle(t(c), 1.7 * k)
for sx in (-1, 1):
roller, pin = t((sx * ROLLER.x, ROLLER.y)), t((sx * PIN.x, PIN.y))
detail.circle(roller, ROLLER_R * k)
detail.circle(roller, 0.8 * k)
detail.circle(pin, PIN_R * k)
rbox(detail, (-3.2, HT / 2 - 5.6), (3.2, HT / 2 - 4.2), 0.3) # pressure pad
s.stroke(detail, UI_ALT, 1.3, join="round")
rules = P()
for i in range(4):
rules.M(t((-40, -23.5 + i * 3.4))).L(t((40, -23.5 + i * 3.4)))
s.stroke(rules, BG_ALT, 1.2)
# wound packs, drawn as if the shell were clear: they cover the label lines
discs, turns, edge, hub, teeth = P(), P(), P(), P(), P()
for sx, pack, spin in ((-1, PACK_L, 0.26), (1, PACK_R, 0.7)):
c = t((sx * HUB_X, HUB_Y))
discs.circle(c, pack * k)
edge.circle(c, pack * k)
for r in np.arange(HUB_R + 0.6, pack - 0.3, TURN):
turns.circle(c, r * k)
hub.circle(c, HUB_R * k)
hub.circle(c, 6.2 * k)
for i in range(6):
a = spin + i * math.pi / 3
teeth.M(polar(c, 6.2 * k, rad=a)).L(polar(c, 4.6 * k, rad=a))
s.fill(discs, BG)
s.stroke(turns, ACCENT_7, 1)
s.stroke(edge, ACCENT_5, 1.4)
s.stroke(hub, UI_ALT, 1.3)
s.stroke(teeth, UI_ALT, 2.4, cap="round")
s.stroke(tape(t, k), ACCENT, 2.4, cap="round", join="round")
def dim(lines: Path, heads: Path, a: Vec, b: Vec) -> None:
"""A dimension line from `a` to `b` with an arrowhead at each end, pointing outwards."""
lines.M(a).L(b)
ang = math.atan2(b.y - a.y, b.x - a.x)
heads.arrowhead(b, ARROW, rad=ang, width=ARROW_W)
heads.arrowhead(a, ARROW, rad=ang + math.pi, width=ARROW_W)
@design()
def draw(s: Canvas) -> None:
t = Affine.frame((CX, CY), deg=0, scale=S)
x0, y0 = t((-WD / 2, -HT / 2))
x1, y1 = t((WD / 2, HT / 2))
front(s, t, S)
# bottom view: the edge with the head openings, tape seen face-on inside them
bt, bb = BY - THK / 2 * S, BY + THK / 2 * S
bottom, band, edges = P(), P(), P()
bottom.rrect(x0, bt, x1 - x0, bb - bt, 1.2 * S)
for a, b in OPENINGS:
xa, xb = t((a, 0)).x, t((b, 0)).x
bottom.rect(xa, bt + 1.2 * S, xb - xa, bb - bt - 2.4 * S)
band.rect(xa, BY - TAPE_W / 2 * S, xb - xa, TAPE_W * S)
for e in (-1, 1):
edges.M(xa, BY + e * TAPE_W / 2 * S).H(xb)
for sx in (-1, 1):
bottom.M(t((sx * 37.5, 0)).x, bt).V(bb)
s.stroke(bottom, UI_HI, 1.5, join="round")
cl, proj = P(), P()
cl.M(x0 - 34, CY + HUB_Y * S).H(x1 + 34)
for sx in (-1, 1):
hx = t((sx * HUB_X, 0)).x
cl.M(hx, CY + (HUB_Y - PACK_L) * S - 18).V(CY + (HUB_Y + PACK_L) * S + 18)
cl.M(CX, y0 - 30).V(bb + 30)
cl.M(x0 - 34, BY).H(x1 + 34)
s.stroke(cl, UI, 1.2, dash=DASHDOT)
for a, b in OPENINGS:
for x in (a, b):
proj.M(t((x, 0)).x, y1 + 10).V(bt - 10)
s.stroke(proj, BG_ALT, 1.2)
s.fill(band, ACCENT_5)
# the ribbon's edges run on as hidden lines behind the shell, pin to pin
hidden = P()
for e in (-1, 1):
hidden.M(t((-PIN.x, 0)).x, BY + e * TAPE_W / 2 * S).H(t((PIN.x, 0)).x)
s.stroke(hidden, ACCENT_6, 1.2, dash=(8, 5))
s.stroke(edges, ACCENT_4, 1.3)
# dimensions: overall width above, height and thickness to the right
ext, dl, heads = P(), P(), P()
yd = y0 - 68
for x in (x0, x1):
ext.M(x, y0 - 10).V(yd - 12)
dim(dl, heads, Vec(x0, yd), Vec(x1, yd))
xd = x1 + 64
for y in (y0, y1, bt, bb):
ext.M(x1 + 10, y).H(xd + 12)
dim(dl, heads, Vec(xd, y0), Vec(xd, y1))
dl.M(xd, bt - 44).V(bt).M(xd, bb).V(bb + 44)
heads.arrowhead((xd, bt), ARROW, deg=90, width=ARROW_W)
heads.arrowhead((xd, bb), ARROW, deg=-90, width=ARROW_W)
# detail view: the tape turning the corner roller, magnified off to the left
dr = DETAIL_R * S * DETAIL_K
zoom = Affine.frame(DETAIL_AT, deg=0, scale=S * DETAIL_K) @ Affine.translate(
-DETAIL_C.x, -DETAIL_C.y
)
with s.clip() as lens:
lens.add(P().circle(DETAIL_AT, dr))
with s.group(clip_path=lens.ref):
front(s, zoom, S * DETAIL_K)
src = t(DETAIL_C)
rings = P().circle(src, DETAIL_R * S)
s.stroke(rings.circle(DETAIL_AT, dr), UI_ALT, 1.3, dash=(10, 6))
# leader between the two circles, rim to rim
u = (DETAIL_AT - src).unit()
dl.M(src + u * (DETAIL_R * S + 6)).L(DETAIL_AT - u * (dr + 6))
s.stroke(ext, UI, 1.2)
s.stroke(dl, UI_ALT, 1.2)
s.fill(heads, UI_ALT)
Run it yourself
$ git clone https://github.com/nickolaj-jepsen/walldye && cd walldye$ uv run walldye render cassette --theme fireproof -o cassette-fireproof-16x9.svg