import turtle, math
screen = turtle.Screen()
screen.bgcolor("#1a1a2e")
screen.title("Soccer Ball")
screen.setup(700, 700)
screen.tracer(0)
t = turtle.Turtle()
t.hideturtle(); t.speed(0)
CX, CY, R = 0, 0, 220 # ball centre & radius in pixels
# ── Build 60 correct vertices of a truncated icosahedron ──────────
phi = (1 + math.sqrt(5)) / 2
def norm3(v):
x, y, z = v
d = math.sqrt(x*x+y*y+z*z)
return (x/d,y/d,z/d)
def dot3(a,b):
return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]
def cross3(a,b):
return(a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0])
def sub3(a,b):
return(a[0]-b[0],a[1]-b[1],a[2]-b[2])
raw = set()
for s1 in (-1,1):
for s2 in (-1,1):
raw.add((0, s1, s2*3*phi))
raw.add((s2*3*phi, 0, s1))
raw.add((s1, s2*3*phi, 0))
for s3 in (-1,1):
raw.add((s1, s2*(2+phi), s3*2*phi))
raw.add((s3*2*phi, s1, s2*(2+phi)))
raw.add((s2*(2+phi),s3*2*phi, s1))
raw.add((s1*2, s2*(1+2*phi), s3*phi))
raw.add((s3*phi, s1*2, s2*(1+2*phi)))
raw.add((s2*(1+2*phi), s3*phi, s1*2))
verts = [norm3(v) for v in raw] # 60 unit-sphere vertices
N = len(verts)
# ── Adjacency (shortest edge)
def d3(a,b): return math.sqrt(sum((a[k]-b[k])**2 for k in range(3)))
min_d = min(d3(verts[i],verts[j]) for i in range(N) for j in range(i+1,N))
ETOL = 0.02
adj = [[] for _ in range(N)]
for i in range(N):
for j in range(i+1,N):
if abs(d3(verts[i],verts[j])-min_d) < ETOL:
adj[i].append(j); adj[j].append(i)
# ── Face enumeration via half-edge walk
def next_vert(i, j):
"""Return k: the next vertex CCW around the face containing directed edge i→j."""
others = [k for k in adj[j] if k != i]
if len(others) == 1:
return others[0]
nj = verts[j]
raw_in = sub3(verts[i], nj)
proj_in = dot3(raw_in, nj)
tang_in = (raw_in[0]-proj_in*nj[0], raw_in[1]-proj_in*nj[1], raw_in[2]-proj_in*nj[2])
mag = math.sqrt(dot3(tang_in,tang_in))
if mag > 1e-10: tang_in = tuple(c/mag for c in tang_in)
best, best_k = None, None
for k in others:
raw_out = sub3(verts[k], nj)
proj_out = dot3(raw_out, nj)
tang_out = (raw_out[0]-proj_out*nj[0], raw_out[1]-proj_out*nj[1], raw_out[2]-proj_out*nj[2])
cr = cross3(tang_in, tang_out)
sine = dot3(cr, nj)
cosine = dot3(tang_in, tang_out)
angle = math.atan2(sine, cosine)
if best is None or angle < best:
best, best_k = angle, k
return best_k
face_set, face_list = set(), []
for i in range(N):
for j in adj[i]:
face = [i, j]
ci, cj = i, j
for _ in range(7):
k = next_vert(ci, cj)
if k == i: break
face.append(k); ci, cj = cj, k
key = frozenset(face)
if key not in face_set and len(face) in (5, 6):
face_set.add(key); face_list.append(face)
# ── Rotation & projection
AY, AX = 0.52, -0.28
def rotate(v, ay, ax):
x,y,z = v
x2 = x*math.cos(ay)+z*math.sin(ay); z2 = -x*math.sin(ay)+z*math.cos(ay); y2=y
y3 = y2*math.cos(ax)-z2*math.sin(ax); z3 = y2*math.sin(ax)+z2*math.cos(ax)
return (x2, y3, z3)
def project(v):
x,y,z = rotate(v, AY, AX)
fov = 3.8
s = R * fov / (fov + z + 1)
return (CX + x*s, CY + y*s, z)
# ── Lighting / shading
LIGHT = norm3((0.5, 0.7, 1.0))
def shade(face_idxs, is_pent):
fv = [verts[i] for i in face_idxs]
cn = norm3(tuple(sum(v[k] for v in fv)/len(fv) for k in range(3)))
rcn = rotate(cn, AY, AX)
diff = max(0.0, dot3(rcn, LIGHT))
spec = max(0.0, diff)**12 * 0.5
if is_pent:
bright = 0.15 + 0.65*diff + spec
base = (18, 18, 18)
else:
bright = 0.38 + 0.55*diff + spec
base = (248, 248, 248)
r = int(min(255, base[0]*bright + 255*spec))
g = int(min(255, base[1]*bright + 255*spec))
b = int(min(255, base[2]*bright + 255*spec))
return f"#{r:02x}{g:02x}{b:02x}"
# ── Drawing helpers
def draw_disk(x, y, r, fill, pen, lw=2):
t.penup()
t.goto(x, y-r)
t.pendown()
t.pencolor(pen)
t.pensize(lw)
t.fillcolor(fill)
t.begin_fill()
t.end_fill()
def draw_poly(pts, fill, pen, lw):
t.penup(); t.goto(pts[0]); t.pendown()
t.pencolor(pen); t.pensize(lw); t.fillcolor(fill)
t.begin_fill()
for p in pts[1:]: t.goto(p)
t.goto(pts[0]); t.end_fill()
def in_ball(px, py):
return (px-CX)**2+(py-CY)**2 <= R**2
# ── RENDER
# Ground shadow
t.penup(); t.pencolor("#0c0c1a"); t.fillcolor("#0c0c1a")
sw, sh = 170, 22
t.goto(CX-sw, CY-R-14); t.pendown(); t.begin_fill()
for s in range(61):
a = math.pi + s*math.pi/60
t.goto(CX+sw*math.cos(a), CY-R-14+sh*math.sin(a))
t.end_fill()
# White ball base
draw_disk(CX, CY, R, "white", "#1a1a1a", lw=3)
# Sort faces back→front and draw
sorted_faces = sorted(face_list, key=lambda f: sum(project(verts[i])[2] for i in f)/len(f))
for face in sorted_faces:
projs = [project(verts[i]) for i in face]
avg_z = sum(p[2] for p in projs)/len(projs)
if avg_z < -0.10:
continue # back-face cull
pts2d = [(p[0],p[1]) for p in projs]
if not any(in_ball(px,py) for px,py in pts2d):
continue
is_pent = len(face) == 5
col = shade(face, is_pent)
pen = "#000000" if is_pent else "#555555"
lw = 2.5 if is_pent else 1.2
draw_poly(pts2d, col, pen, lw)
# Clean ball outline
t.penup(); t.goto(CX, CY-R); t.pendown()
t.pencolor("#111111"); t.pensize(4); t.fillcolor("")
# Specular highlights
for hx,hy,hr,col in [(CX-65,CY+80,40,"#ffffff"),(CX-55,CY+68,20,"#ffffff")]:
t.penup(); t.goto(hx,hy-hr); t.pendown()
t.pencolor(col); t.pensize(1); t.fillcolor(col)
t.begin_fill()
t.end_fill()
screen.update()
turtle.done()