Teletext sunset
inspired by BBC, Ceefax1
A teletext sunset in gapped blocks, two ridges to its left and three narrowing reflection bars on a dashed sea.
Made with Claude Opus 5.5
- Technique
- text characters, pixel art
- Inspired by
- vintage computers
- 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
A teletext page is 40 characters wide and 24 rows tall, and each graphics character is a 2 by 3 grid of blocks. In separated mode every block keeps a gap around it.
A character cell can show only one colour of block, so wherever the two ridges share a cell, the nearer ridge takes all of it. The BBC’s Ceefax service drew its pages under these rules from 1974 to 2012.
Sources
- BBC, Ceefax, 1974. ↑
- Teletext character set.
Source code
wallpapers/sixel-ceefax/design.py, 63 lines
"""A teletext page in separated-graphics sixels: two hill ridges, a dashed sea and a half-sunk sun."""
import numpy as np
from numpy.typing import NDArray
from walldye import ACCENT, ACCENT_3, BG_ALT, UI, UI_ALT, Canvas, design
from walldye.pixel import glyphs
SW, SH = 24, 15 # sixel pitch; 80x72 sixels = a 40x24 teletext page
GW, GH = 18, 11 # lit block inside the pitch, the rest is the separated-mode gutter
COLS, ROWS = 80, 72
HORIZON = 42 # first row of character row 14: the horizon gets a text row to itself
SUN = (52, 42, 4, 6.4) # centre col/row, radius in cols/rows
HEADER = "P101 TELETEXT 101 Sun 27 Sep 19:04"
# sixel kinds in rising priority: the highest one in a 2x3 character cell recolours the rest
SEA, LINE, FAR, NEAR, REFL, DISC = range(1, 7)
TONES = (BG_ALT, UI, ACCENT_3, ACCENT)
# kind -> TONES index: the far ridge shares the horizon line's step, the near one the sea's
INK = (0, 0, 1, 1, 0, 2, 3)
def bumps(c: NDArray[np.floating], *hs: tuple[float, float, float]) -> NDArray[np.floating]:
"""Smooth ridge height in rows: a sum of (height, centre col, width) Gaussians over `c`."""
return sum((h * np.exp(-(((c - m) / w) ** 2)) for h, m, w in hs), np.zeros_like(c))
@design()
def draw(s: Canvas) -> None:
g = np.zeros((ROWS, COLS), int)
rr, cc = np.mgrid[0:ROWS, 0:COLS]
c = np.arange(COLS) + 0.5
far = HORIZON - bumps(c, (16, 22, 12), (6, 36, 6))
near = HORIZON - bumps(c, (7, 2, 10), (5.5, 20, 9), (2.5, 32, 5))
g[(rr >= far) & (rr < HORIZON)] = FAR
g[(rr >= near) & (rr < HORIZON)] = NEAR
land_end = int(np.nonzero(HORIZON - far >= 1)[0].max())
g[HORIZON, land_end - 2 :] = LINE # tuck the horizon a little under the last slope
# sea: dash rows in perspective, closer together toward the horizon, evenly jittered
sc, sr, rx, ry = SUN
r = s.np_rng(3)
for k, row in enumerate([45, 47, 50, 54, 59, 65, 71]):
period, n = 6 + 2 * k, 2 + k // 2
for c0 in range(-int(r.integers(0, period)), COLS, period):
c0 += int(r.integers(0, period - n))
calm = row <= HORIZON + 12 and sc - rx - 3 < c0 + n and c0 < sc + rx + 3
if not calm: # a calm patch round the reflection
g[row, max(c0, 0) : max(c0 + n, 0)] = SEA
for row, half in [(45, 4), (48, 3), (51, 1)]: # reflection bars shrinking toward the viewer
g[row, sc - half : sc + half] = REFL
disc = ((cc + 0.5 - sc) / rx) ** 2 + ((rr + 0.5 - sr) / ry) ** 2 <= 1
g[disc & (rr < HORIZON)] = DISC
# teletext allows one foreground colour per 2x3 character cell
top = g.reshape(ROWS // 3, 3, COLS // 2, 2).max(axis=(1, 3))
g = np.where(g > 0, top.repeat(3, axis=0).repeat(2, axis=1), 0)
ink = np.array(INK)[g]
with s.buckets(TONES, "fill") as b:
for row, col in np.argwhere(g).tolist():
x, y = col * SW + (SW - GW) // 2, row * SH + (SH - GH) // 2
b[int(ink[row, col])].rect(x, y, GW, GH)
glyphs(s, [HEADER], UI_ALT, at=(48, 10), font="5x8", px=3, gap=1)"""A teletext page in separated-graphics sixels: two hill ridges, a dashed sea and a half-sunk sun."""
import numpy as np
from numpy.typing import NDArray
from walldye import ACCENT, ACCENT_3, BG_ALT, UI, UI_ALT, Canvas, design
from walldye.pixel import glyphs
SW, SH = 24, 15 # sixel pitch; 80x72 sixels = a 40x24 teletext page
GW, GH = 18, 11 # lit block inside the pitch, the rest is the separated-mode gutter
COLS, ROWS = 80, 72
HORIZON = 42 # first row of character row 14: the horizon gets a text row to itself
SUN = (52, 42, 4, 6.4) # centre col/row, radius in cols/rows
HEADER = "P101 TELETEXT 101 Sun 27 Sep 19:04"
# sixel kinds in rising priority: the highest one in a 2x3 character cell recolours the rest
SEA, LINE, FAR, NEAR, REFL, DISC = range(1, 7)
TONES = (BG_ALT, UI, ACCENT_3, ACCENT)
# kind -> TONES index: the far ridge shares the horizon line's step, the near one the sea's
INK = (0, 0, 1, 1, 0, 2, 3)
def bumps(c: NDArray[np.floating], *hs: tuple[float, float, float]) -> NDArray[np.floating]:
"""Smooth ridge height in rows: a sum of (height, centre col, width) Gaussians over `c`."""
return sum((h * np.exp(-(((c - m) / w) ** 2)) for h, m, w in hs), np.zeros_like(c))
@design()
def draw(s: Canvas) -> None:
g = np.zeros((ROWS, COLS), int)
rr, cc = np.mgrid[0:ROWS, 0:COLS]
c = np.arange(COLS) + 0.5
far = HORIZON - bumps(c, (16, 22, 12), (6, 36, 6))
near = HORIZON - bumps(c, (7, 2, 10), (5.5, 20, 9), (2.5, 32, 5))
g[(rr >= far) & (rr < HORIZON)] = FAR
g[(rr >= near) & (rr < HORIZON)] = NEAR
land_end = int(np.nonzero(HORIZON - far >= 1)[0].max())
g[HORIZON, land_end - 2 :] = LINE # tuck the horizon a little under the last slope
# sea: dash rows in perspective, closer together toward the horizon, evenly jittered
sc, sr, rx, ry = SUN
r = s.np_rng(3)
for k, row in enumerate([45, 47, 50, 54, 59, 65, 71]):
period, n = 6 + 2 * k, 2 + k // 2
for c0 in range(-int(r.integers(0, period)), COLS, period):
c0 += int(r.integers(0, period - n))
calm = row <= HORIZON + 12 and sc - rx - 3 < c0 + n and c0 < sc + rx + 3
if not calm: # a calm patch round the reflection
g[row, max(c0, 0) : max(c0 + n, 0)] = SEA
for row, half in [(45, 4), (48, 3), (51, 1)]: # reflection bars shrinking toward the viewer
g[row, sc - half : sc + half] = REFL
disc = ((cc + 0.5 - sc) / rx) ** 2 + ((rr + 0.5 - sr) / ry) ** 2 <= 1
g[disc & (rr < HORIZON)] = DISC
# teletext allows one foreground colour per 2x3 character cell
top = g.reshape(ROWS // 3, 3, COLS // 2, 2).max(axis=(1, 3))
g = np.where(g > 0, top.repeat(3, axis=0).repeat(2, axis=1), 0)
ink = np.array(INK)[g]
with s.buckets(TONES, "fill") as b:
for row, col in np.argwhere(g).tolist():
x, y = col * SW + (SW - GW) // 2, row * SH + (SH - GH) // 2
b[int(ink[row, col])].rect(x, y, GW, GH)
glyphs(s, [HEADER], UI_ALT, at=(48, 10), font="5x8", px=3, gap=1)
Run it yourself
$ git clone https://github.com/nickolaj-jepsen/walldye && cd walldye$ uv run walldye render sixel-ceefax --theme fireproof -o sixel-ceefax-fireproof-16x9.svg