Rubik’s cube
A Rubik’s cube mid-turn, whose six centers keep their places relative to each other.
Made with Claude Opus 5.5
- Technique
- technical drawing
- Inspired by
- patent drawings
- Shape
- Any screen
- Added
- 27 September 2026
Export
Shapecropped from 16:9
Across
Down
Scale
Crop
SizeThis browser can’t draw a file that large.
FormatThis browser can’t make WebP files.
Colors
- #1C1B1Abackground
- #DAD8CEforeground
- #CF6A4Caccent
Notes
The shading follows the rule for United States patent drawings, which has light fall from the upper left. Only the stickers facing away from it, on the right-hand side of the lower two layers, are ruled, with the lines closing up toward their shadowed edge.
Sources
- Ernő Rubik, Rubik’s Cube, 1974.
- Standards for drawings (37 CFR 1.84).
Source code
wallpapers/
"""A Rubik's cube mid-turn, drawn as a patent figure with ruled shading."""
import math
import numpy as np
import shapely
from numpy.typing import NDArray
from shapely import MultiPoint
from walldye import ACCENT, BG, UI, UI_ALT, UI_HI, Canvas, P, Path, Rect, Vec, design
from walldye.field import runs
from walldye.geom import Affine
S = 100 # cubie edge on screen
TURN = math.radians(-24) # how far the top layer has turned
YAW, PITCH = math.radians(40), math.radians(30)
FACES = ((0, 1), (0, -1), (1, 1), (1, -1), (2, 1), (2, -1)) # (axis, sign)
HOT = ((1, 1, 1), 1) # (cubie, axis) of the one lit sticker
ORBIT_R, ORBIT_N = 2.6, 720 # turn arrow: orbit radius in cubies, samples round it
def rot_y(a: float) -> NDArray[np.float64]:
c, s = math.cos(a), math.sin(a)
return np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]])
VIEW = np.array(
[[1, 0, 0], [0, math.cos(PITCH), -math.sin(PITCH)], [0, math.sin(PITCH), math.cos(PITCH)]]
) @ rot_y(YAW)
def project(p: NDArray[np.float64]) -> Vec:
"""Parallel projection of a cube-space point (y up) to the figure's screen space, cube center
at the origin."""
v = VIEW @ p
return Vec(S * v[0], -S * v[1])
def face_quad(
c: tuple[int, int, int], axis: int, sign: int, inset: float
) -> list[NDArray[np.float64]]:
"""Corners of cubie `c`'s face on `axis` at `sign`, shrunk about its center to `inset`."""
u, v = [k for k in range(3) if k != axis]
out = []
for du, dv in ((-1, -1), (1, -1), (1, 1), (-1, 1)):
p = np.array(c, float)
p[axis] += 0.5 * sign
p[u] += 0.5 * du * inset
p[v] += 0.5 * dv * inset
out.append(p)
return out
def rounded(d: Path, quad: list[Vec], r: float) -> None:
"""A quad with each corner cut back by fraction `r` of its edges and rounded with a
quadratic curve."""
n = len(quad)
for i in range(n):
p0, p1, p2 = quad[i - 1], quad[i], quad[(i + 1) % n]
a, b = p1 + (p0 - p1) * r, p1 + (p2 - p1) * r
(d.M if i == 0 else d.L)(a)
d.Q(p1, b)
d.Z()
def shade_lines(d: Path, quad: list[Vec], n: int = 4) -> None:
"""Patent shade lines across a sticker, crowding toward its shadowed edge."""
a, b, c, e = quad
for k in range(n):
t = 0.9 - 0.55 * (k / (n - 1)) ** 1.4
p, q = a + (b - a) * t, e + (c - e) * t
m = (q - p) * 0.14
d.M(p + m).L(q - m)
@design(aspects="any")
def draw(s: Canvas) -> None:
# landscape: where the 16:9 figure sat, right of center; portrait: larger and low, under
# the clock, nudged left because the arrow widens the right side
at = s.pick(landscape=(1240 / 1920, 520 / 1080), portrait=(0.48, 0.54))
k = 1.0 if s.landscape else 1.3
# The lower two layers, then the turned top layer; viewed from above, the top layer hides
# the block beneath wherever they overlap. Each block is convex, so its front-facing
# outer faces never overlap one another and need no depth sort.
blocks = (((-1, 0), np.eye(3)), ((1,), rot_y(TURN)))
sils = []
# the cube's outline and the turn arrow's orbit, with room for the arrowhead
ink = Rect(-278 * k, -238 * k, 556 * k, 476 * k)
with s.motif(at, ink), s.group(transform=Affine.scale(k)):
for layer, rot in blocks:
bodies, stickers, lit, hatch = P(), P(), P(), P()
corners: list[Vec] = []
for c in ((x, y, z) for x in (-1, 0, 1) for y in layer for z in (-1, 0, 1)):
for axis, sign in FACES:
edge = min(layer) - 0.5 if sign < 0 else max(layer) + 0.5
outer = c[axis] == sign if axis != 1 else c[1] + 0.5 * sign == edge
n = rot @ np.eye(3)[axis] * sign
facing = (VIEW @ n)[2]
if not outer or facing <= 0:
continue
body = [project(rot @ p) for p in face_quad(c, axis, sign, 1.0)]
bodies.poly(body, closed=True)
corners += body
# no stickers on the block's cut face (inside the cube) or on faces seen
# too edge-on to read
if c[axis] != sign or facing < 0.2:
continue
quad = [project(rot @ p) for p in face_quad(c, axis, sign, 0.78)]
if (c, axis) == HOT:
rounded(lit, quad, 0.2)
continue
rounded(stickers, quad, 0.2)
# shade lines on faces turned away from the light (screen right)
if (VIEW @ n)[0] > 0.3:
shade_lines(hatch, quad)
sil = MultiPoint(corners).convex_hull
sils.append(sil)
s.path(bodies, fill=BG, stroke=UI_HI, stroke_width=1.5, stroke_linejoin="round")
s.stroke(stickers, UI_ALT, 1.4)
s.path(lit, fill=ACCENT, stroke=ACCENT, stroke_width=1.4)
s.stroke(hatch, UI, 1.2)
s.stroke(P().shape(sil), UI_HI, 2.6, join="round")
# Turn arrow hugging the top layer: the longest run of its orbit that clears the cube,
# trimmed by an eighth at each end.
solid = shapely.unary_union(sils).buffer(14)
a = np.linspace(0, 2 * math.pi, ORBIT_N, endpoint=False)
ring = np.stack([ORBIT_R * np.cos(a), np.ones_like(a), ORBIT_R * np.sin(a)])
orbit = (S * (VIEW @ rot_y(TURN) @ ring)[:2] * [[1], [-1]]).T
free = ~shapely.contains_xy(solid, orbit[:, 0], orbit[:, 1])
# start the scan inside the cube so no run wraps past the end
k0 = int(np.argmin(free))
orbit, free = np.roll(orbit, -k0, axis=0), np.roll(free, -k0)
i0, i1 = max(runs(free), key=lambda r: r[1] - r[0])
trim = (i1 - i0) // 8
arc = orbit[i0 + trim : i1 - trim]
s.stroke(P().poly(arc), UI_HI, 1.6, cap="round")
end = Vec(*arc[-1])
heading = (end - Vec(*arc[-3])).unit()
tip = end + heading * 16
s.fill(P().arrowhead(tip, 16, rad=math.atan2(heading.y, heading.x), width=6), UI_HI)"""A Rubik's cube mid-turn, drawn as a patent figure with ruled shading."""
import math
import numpy as np
import shapely
from numpy.typing import NDArray
from shapely import MultiPoint
from walldye import ACCENT, BG, UI, UI_ALT, UI_HI, Canvas, P, Path, Rect, Vec, design
from walldye.field import runs
from walldye.geom import Affine
S = 100 # cubie edge on screen
TURN = math.radians(-24) # how far the top layer has turned
YAW, PITCH = math.radians(40), math.radians(30)
FACES = ((0, 1), (0, -1), (1, 1), (1, -1), (2, 1), (2, -1)) # (axis, sign)
HOT = ((1, 1, 1), 1) # (cubie, axis) of the one lit sticker
ORBIT_R, ORBIT_N = 2.6, 720 # turn arrow: orbit radius in cubies, samples round it
def rot_y(a: float) -> NDArray[np.float64]:
c, s = math.cos(a), math.sin(a)
return np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]])
VIEW = np.array(
[[1, 0, 0], [0, math.cos(PITCH), -math.sin(PITCH)], [0, math.sin(PITCH), math.cos(PITCH)]]
) @ rot_y(YAW)
def project(p: NDArray[np.float64]) -> Vec:
"""Parallel projection of a cube-space point (y up) to the figure's screen space, cube center
at the origin."""
v = VIEW @ p
return Vec(S * v[0], -S * v[1])
def face_quad(
c: tuple[int, int, int], axis: int, sign: int, inset: float
) -> list[NDArray[np.float64]]:
"""Corners of cubie `c`'s face on `axis` at `sign`, shrunk about its center to `inset`."""
u, v = [k for k in range(3) if k != axis]
out = []
for du, dv in ((-1, -1), (1, -1), (1, 1), (-1, 1)):
p = np.array(c, float)
p[axis] += 0.5 * sign
p[u] += 0.5 * du * inset
p[v] += 0.5 * dv * inset
out.append(p)
return out
def rounded(d: Path, quad: list[Vec], r: float) -> None:
"""A quad with each corner cut back by fraction `r` of its edges and rounded with a
quadratic curve."""
n = len(quad)
for i in range(n):
p0, p1, p2 = quad[i - 1], quad[i], quad[(i + 1) % n]
a, b = p1 + (p0 - p1) * r, p1 + (p2 - p1) * r
(d.M if i == 0 else d.L)(a)
d.Q(p1, b)
d.Z()
def shade_lines(d: Path, quad: list[Vec], n: int = 4) -> None:
"""Patent shade lines across a sticker, crowding toward its shadowed edge."""
a, b, c, e = quad
for k in range(n):
t = 0.9 - 0.55 * (k / (n - 1)) ** 1.4
p, q = a + (b - a) * t, e + (c - e) * t
m = (q - p) * 0.14
d.M(p + m).L(q - m)
@design(aspects="any")
def draw(s: Canvas) -> None:
# landscape: where the 16:9 figure sat, right of center; portrait: larger and low, under
# the clock, nudged left because the arrow widens the right side
at = s.pick(landscape=(1240 / 1920, 520 / 1080), portrait=(0.48, 0.54))
k = 1.0 if s.landscape else 1.3
# The lower two layers, then the turned top layer; viewed from above, the top layer hides
# the block beneath wherever they overlap. Each block is convex, so its front-facing
# outer faces never overlap one another and need no depth sort.
blocks = (((-1, 0), np.eye(3)), ((1,), rot_y(TURN)))
sils = []
# the cube's outline and the turn arrow's orbit, with room for the arrowhead
ink = Rect(-278 * k, -238 * k, 556 * k, 476 * k)
with s.motif(at, ink), s.group(transform=Affine.scale(k)):
for layer, rot in blocks:
bodies, stickers, lit, hatch = P(), P(), P(), P()
corners: list[Vec] = []
for c in ((x, y, z) for x in (-1, 0, 1) for y in layer for z in (-1, 0, 1)):
for axis, sign in FACES:
edge = min(layer) - 0.5 if sign < 0 else max(layer) + 0.5
outer = c[axis] == sign if axis != 1 else c[1] + 0.5 * sign == edge
n = rot @ np.eye(3)[axis] * sign
facing = (VIEW @ n)[2]
if not outer or facing <= 0:
continue
body = [project(rot @ p) for p in face_quad(c, axis, sign, 1.0)]
bodies.poly(body, closed=True)
corners += body
# no stickers on the block's cut face (inside the cube) or on faces seen
# too edge-on to read
if c[axis] != sign or facing < 0.2:
continue
quad = [project(rot @ p) for p in face_quad(c, axis, sign, 0.78)]
if (c, axis) == HOT:
rounded(lit, quad, 0.2)
continue
rounded(stickers, quad, 0.2)
# shade lines on faces turned away from the light (screen right)
if (VIEW @ n)[0] > 0.3:
shade_lines(hatch, quad)
sil = MultiPoint(corners).convex_hull
sils.append(sil)
s.path(bodies, fill=BG, stroke=UI_HI, stroke_width=1.5, stroke_linejoin="round")
s.stroke(stickers, UI_ALT, 1.4)
s.path(lit, fill=ACCENT, stroke=ACCENT, stroke_width=1.4)
s.stroke(hatch, UI, 1.2)
s.stroke(P().shape(sil), UI_HI, 2.6, join="round")
# Turn arrow hugging the top layer: the longest run of its orbit that clears the cube,
# trimmed by an eighth at each end.
solid = shapely.unary_union(sils).buffer(14)
a = np.linspace(0, 2 * math.pi, ORBIT_N, endpoint=False)
ring = np.stack([ORBIT_R * np.cos(a), np.ones_like(a), ORBIT_R * np.sin(a)])
orbit = (S * (VIEW @ rot_y(TURN) @ ring)[:2] * [[1], [-1]]).T
free = ~shapely.contains_xy(solid, orbit[:, 0], orbit[:, 1])
# start the scan inside the cube so no run wraps past the end
k0 = int(np.argmin(free))
orbit, free = np.roll(orbit, -k0, axis=0), np.roll(free, -k0)
i0, i1 = max(runs(free), key=lambda r: r[1] - r[0])
trim = (i1 - i0) // 8
arc = orbit[i0 + trim : i1 - trim]
s.stroke(P().poly(arc), UI_HI, 1.6, cap="round")
end = Vec(*arc[-1])
heading = (end - Vec(*arc[-3])).unit()
tip = end + heading * 16
s.fill(P().arrowhead(tip, 16, rad=math.atan2(heading.y, heading.x), width=6), UI_HI)
Run it yourself
$ git clone https://github.com/nickolaj-jepsen/walldye && cd walldye$ uv run walldye render rubik --theme fireproof -o rubik-fireproof-16x9.svg