PyTamaro 2026-01-09. Filed in public, twoville.
I recently watched an episode of the Computing Education Things podcast in which host Daniel Prol interviewed professor Matthias Hauswirth about teaching computing through the lens of graphics programming. Hauswirth built the learning platform PyTamaro . I'm afraid I didn't listen for the big ideas because I'm already a member of the choir. Rather I was interested in the graphics. These four images were begging to be recreated in Twoville:
let s = 10
let ss = s * 0.5
let sss = ss * 0.5
// Body
figure
color = [0, 0.7, 0]
turtle
position = -[s, s]
heading = 90
repeat 2
walk.distance = s
curl
radius = s
degrees = -90
walk.distance = s
turn.degrees = -90
// Wing
figure
color = [0, 0.4, 0]
turtle
position = -[s, s]
heading = 90
repeat 2
walk.distance = ss
curl
radius = ss
degrees = -90
walk.distance = ss
turn.degrees = -90
// Eye
circle
center = [ss, ~]
radius = 3.0 / 5 * ss
color = :white
circle
center = [ss, ~]
radius = 1.5 / 5 * ss
color = [0, 0.3, 0]
// Top beak
figure
color = [1, 0.7, 0]
turtle
position = [s, s]
heading = 0
curl
degrees = -90
radius = ss
line.rposition = [-ss, 0]
// Bottom beak
figure
color = [0.9, 0.5, 0]
turtle
position = [s, ss * 0.5]
heading = 0
curl
degrees = 90
radius = ss * 0.5
line.rposition = [-ss * 0.5, 0]
circle
center = :zero
radius = 10
figure
color = :red
turtle
heading = -5
position = [-10, 0]
walk.distance = 20
bump
position = [-10, 0]
degrees = -180
turtle
heading = 5
position = [-10, 0]
walk.distance = 20
bump
position = [-10, 0]
degrees = 180
// Spots
circle
center = [0.42, 7.81]
radius = 1.7
circle
center = [-2.34, 3.67]
radius = 1.7
circle
center = [4.17, 4.36]
radius = 1.7
circle
center = [0.5, -7.74]
radius = 1.7
circle
center = [-2.42, -3.91]
radius = 1.7
circle
center = [4.4, -4.6]
radius = 1.7
// Head
circle
center = [-10, 0]
radius = 6
circle
color = :white
center = [-12, 2]
radius = 2
circle
color = :white
center = [-12, -2]
radius = 2
circle
color = :black
center = [-12.7, -2]
radius = 1.3
circle
color = :black
center = [-12.7, 2]
radius = 1.3
circle
center = :zero
radius = 10
figure
color = :red
turtle
heading = -5
position = [-10, 0]
walk.distance = 20
bump
position = [-10, 0]
degrees = -180
turtle
heading = 5
position = [-10, 0]
walk.distance = 20
bump
position = [-10, 0]
degrees = 180
// Spots
circle
center = [0.42, 7.81]
radius = 1.7
circle
center = [-2.34, 3.67]
radius = 1.7
circle
center = [4.17, 4.36]
radius = 1.7
circle
center = [0.5, -7.74]
radius = 1.7
circle
center = [-2.42, -3.91]
radius = 1.7
circle
center = [4.4, -4.6]
radius = 1.7
// Head
circle
center = [-10, 0]
radius = 6
circle
color = :white
center = [-12, 2]
radius = 2
circle
color = :white
center = [-12, -2]
radius = 2
circle
color = :black
center = [-12.7, -2]
radius = 1.3
circle
color = :black
center = [-12.7, 2]
radius = 1.3
let topWidth = 1
let slopeWidth = 8
let baseWidth = 5
let stripHeight = 5
let coneHeight = stripHeight * (5)
let baseHeight = 2
to coneX(y: float): float
y * slopeWidth / coneHeight
polygon
color = [1, 0.55, 0]
vertex.position = [-(topWidth + slopeWidth + baseWidth), 0]
vertex.rposition = [0, baseHeight]
vertex.rposition = [baseWidth, 0]
vertex.rposition = [slopeWidth, coneHeight]
mirror
pivot = :zero
axis = :up
polygon
color = [0.95, ~, ~]
vertex.position = [-coneX(4 * stripHeight) - topWidth, baseHeight + stripHeight]
vertex.rposition = [coneX(stripHeight), stripHeight]
mirror
pivot = :zero
axis = :up
polygon
color = [0.95, ~, ~]
vertex.position = [-coneX(2 * stripHeight) - topWidth, baseHeight + stripHeight * 3]
vertex.rposition = [coneX(stripHeight), stripHeight]
mirror
pivot = :zero
axis = :up
view
autofit = true
margin = 1
let topWidth = 1
let slopeWidth = 8
let baseWidth = 5
let stripHeight = 5
let coneHeight = stripHeight * (5)
let baseHeight = 2
to coneX(y: float): float
y * slopeWidth / coneHeight
polygon
color = [1, 0.55, 0]
vertex.position = [-(topWidth + slopeWidth + baseWidth), 0]
vertex.rposition = [0, baseHeight]
vertex.rposition = [baseWidth, 0]
vertex.rposition = [slopeWidth, coneHeight]
mirror
pivot = :zero
axis = :up
polygon
color = [0.95, ~, ~]
vertex.position = [-coneX(4 * stripHeight) - topWidth, baseHeight + stripHeight]
vertex.rposition = [coneX(stripHeight), stripHeight]
mirror
pivot = :zero
axis = :up
polygon
color = [0.95, ~, ~]
vertex.position = [-coneX(2 * stripHeight) - topWidth, baseHeight + stripHeight * 3]
vertex.rposition = [coneX(stripHeight), stripHeight]
mirror
pivot = :zero
axis = :up
view
autofit = true
margin = 1
let radius = 5
let eraserWidth = 6
let ferruleWidth = 4
let bodyWidth = 40
let tipWidth = 10
let zigzagWidth = 3
let leadWidth = 3
let tipHeight = leadWidth * 1.0 / tipWidth * radius
// Eraser
polygon
color = [0.8, 0.5, 0.5]
vertex.position = [eraserWidth, radius]
vertex.rposition = [0, -2 * radius]
quadratic.distance = 3
vertex.rposition = [-eraserWidth, 0]
vertex.rposition = [0, 2 * radius]
// Ferrule
polygon
color = [0.9, ~, ~]
vertex.position = [eraserWidth + ferruleWidth, radius]
vertex.rposition = [0, -2 * radius]
vertex.rposition = [-ferruleWidth, 0]
vertex.rposition = [0, 2 * radius]
// Body
polygon
color = [0.9, 0.8, 0.2]
vertex.position = [eraserWidth + ferruleWidth + bodyWidth, radius]
vertex.rposition = [0, -2 * radius]
vertex.rposition = [-bodyWidth, 0]
vertex.rposition = [0, 2 * radius]
// Tip
polygon
color = [0.95, 0.9, 0.7]
vertex.position = [eraserWidth + ferruleWidth + bodyWidth, radius]
vertex.rposition = [tipWidth, -radius]
vertex.rposition = [-tipWidth, -radius]
repeat 3
vertex.rposition = [-zigzagWidth, radius / 3.0]
around
vertex.rposition = [zigzagWidth, radius / 3.0]
// Lead
polygon
color = :black
vertex.position = [eraserWidth + ferruleWidth + bodyWidth + tipWidth, 0]
vertex.rposition = [-leadWidth, -tipHeight]
vertex.rposition = [0, 2 * tipHeight]
view
autofit = true
margin = 1
let radius = 5
let eraserWidth = 6
let ferruleWidth = 4
let bodyWidth = 40
let tipWidth = 10
let zigzagWidth = 3
let leadWidth = 3
let tipHeight = leadWidth * 1.0 / tipWidth * radius
// Eraser
polygon
color = [0.8, 0.5, 0.5]
vertex.position = [eraserWidth, radius]
vertex.rposition = [0, -2 * radius]
quadratic.distance = 3
vertex.rposition = [-eraserWidth, 0]
vertex.rposition = [0, 2 * radius]
// Ferrule
polygon
color = [0.9, ~, ~]
vertex.position = [eraserWidth + ferruleWidth, radius]
vertex.rposition = [0, -2 * radius]
vertex.rposition = [-ferruleWidth, 0]
vertex.rposition = [0, 2 * radius]
// Body
polygon
color = [0.9, 0.8, 0.2]
vertex.position = [eraserWidth + ferruleWidth + bodyWidth, radius]
vertex.rposition = [0, -2 * radius]
vertex.rposition = [-bodyWidth, 0]
vertex.rposition = [0, 2 * radius]
// Tip
polygon
color = [0.95, 0.9, 0.7]
vertex.position = [eraserWidth + ferruleWidth + bodyWidth, radius]
vertex.rposition = [tipWidth, -radius]
vertex.rposition = [-tipWidth, -radius]
repeat 3
vertex.rposition = [-zigzagWidth, radius / 3.0]
around
vertex.rposition = [zigzagWidth, radius / 3.0]
// Lead
polygon
color = :black
vertex.position = [eraserWidth + ferruleWidth + bodyWidth + tipWidth, 0]
vertex.rposition = [-leadWidth, -tipHeight]
vertex.rposition = [0, 2 * tipHeight]
view
autofit = true
margin = 1
Twoville code is not shorter than PyTamaro code. There are a couple of reasons for this. First, Twoville uses a declarative approach in which properties are explicitly named across multiple lines. By design, it doesn't have a compact one-line function for drawing a circle. Second, I expressed these shapes parametrically. For the most part, you can modify the variables at the top and see the whole image adapt. This is especially fun to see on the pencil. Put your cursor on the first vertex of the eraser and then drag the handle.