#!python """elephant: my own elephant, drawn the way Terry A. Davis drew his in TempleOS, and filmed while it is drawn. elephant.py OUT.mp4 [--still OUT.png] [--seconds 24] His picture (public domain, like all of TempleOS) is a grey elephant made of straight-edged shapes on a flat yellow ground under a flat cyan sky, with a tree, some grass and a log, and the words "This is 16 color, pleasant to create. It would be unpleasant to draw with 16,777,216 colors." This is not a copy of it. It is a different elephant in the same country: 640x480, the same sixteen colours and nothing in between, every shape an outline that is closed and then filled. Mine has found a morel. Morel, Nov 2026. """ import argparse, subprocess, sys from PIL import Image, ImageDraw, ImageFont ap = argparse.ArgumentParser(); ap.add_argument("out"); ap.add_argument("--still"); ap.add_argument("--seconds", type=float, default=24) a = ap.parse_args() W, H, SCALE, FPS = 640, 480, 2, 30 # the sixteen colours of a 1980s PC, in their usual order EGA = ["000000", "0000AA", "00AA00", "00AAAA", "AA0000", "AA00AA", "AA5500", "AAAAAA", "555555", "5555FF", "55FF55", "55FFFF", "FF5555", "FF55FF", "FFFF55", "FFFFFF"] BLACK, BLUE, GREEN, CYAN, RED, MAGENTA, BROWN, LTGRAY, DKGRAY, LTBLUE, LTGREEN, LTCYAN, LTRED, LTMAGENTA, YELLOW, WHITE = range(16) img = Image.new("P", (W, H), WHITE) img.putpalette([int(c[i:i + 2], 16) for c in EGA for i in (0, 2, 4)] + [0] * (768 - 48)) d = ImageDraw.Draw(img) d.fontmode = "1" # no in-between shades, ever frames = [] # the film is planned first, then cut to length def shoot(n=1): frames.extend([img.copy()] * n) def along(points, closed): """every pixel-ish step along a run of straight lines""" pts = list(points) + ([points[0]] if closed else []) for (x0, y0), (x1, y1) in zip(pts, pts[1:]): n = max(abs(x1 - x0), abs(y1 - y0), 1) for i in range(n + 1): yield round(x0 + (x1 - x0) * i / n), round(y0 + (y1 - y0) * i / n) def line(points, colour, closed=False, width=1, speed=9): """draw a line the way a hand does: a bit at a time""" last, n = None, 0 for p in along(points, closed): if last is not None: d.line([last, p], fill=colour, width=width) last = p; n += 1 if n % speed == 0: shoot() shoot() def shape(points, fill, outline=BLACK, speed=9, hold=3): """outline it, close it, fill it""" line(points, outline, closed=True, speed=speed) d.polygon(points, fill=fill, outline=outline) shoot(hold) def wipe(y0, y1, colour, step=16): for y in range(y0, y1, step): d.rectangle([0, y, W, min(y + step, y1) - 1], fill=colour) shoot() HORIZON = 252 shoot(8) wipe(0, HORIZON, LTCYAN) wipe(HORIZON, H, YELLOW) shoot(6) # --- the tree, back on the left where his stands --- shape([(70, 318), (86, 318), (84, 258), (96, 226), (88, 222), (79, 246), (72, 214), (64, 216), (72, 262)], BROWN, BROWN, speed=12) for leaf in ([(30, 222), (58, 196), (76, 212), (70, 236), (44, 240)], [(60, 196), (84, 150), (104, 176), (92, 214), (72, 214)], [(92, 214), (112, 174), (134, 190), (122, 222), (98, 226)]): shape(leaf, GREEN, GREEN, speed=14, hold=1) for vein in ([(40, 230), (62, 204), (72, 226)], [(70, 206), (86, 162), (94, 200)], [(100, 218), (116, 184), (124, 208)], [(84, 160), (78, 200)]): line(vein, LTGREEN, speed=14) line([(78, 316), (80, 262), (86, 236)], DKGRAY, speed=14) # --- a log, and grass --- shape([(96, 414), (176, 386), (186, 398), (108, 430)], BROWN, BROWN, speed=14) shape([(150, 392), (160, 372), (168, 376), (162, 392)], BROWN, BROWN, speed=14, hold=1) line([(104, 420), (176, 394)], LTGRAY, speed=14) line([(110, 424), (150, 409)], DKGRAY, speed=14) for gx, gy in ((214, 430), (566, 300), (300, 446)): for dx, dy in ((-10, -22), (-4, -30), (3, -26), (9, -20), (-14, -12), (13, -10)): line([(gx, gy), (gx + dx, gy + dy)], GREEN if (dx + dy) % 2 else LTGREEN, speed=20) # --- the elephant, walking in from the left, head down, trunk out --- shoot(6) far_hind = [(296, 300), (330, 304), (332, 398), (308, 400), (302, 350)] far_fore = [(404, 292), (432, 296), (430, 396), (408, 398), (404, 340)] tail = [(232, 232), (238, 236), (222, 296), (216, 294)] body = [(236, 226), (296, 204), (366, 204), (416, 222), (428, 262), (414, 304), (360, 318), (284, 316), (244, 292), (230, 258)] hind = [(244, 280), (294, 300), (300, 352), (290, 416), (258, 418), (254, 356)] fore = [(360, 306), (402, 300), (400, 356), (396, 414), (366, 416), (364, 358)] head = [(404, 214), (444, 200), (478, 214), (494, 248), (488, 286), (462, 300), (430, 290), (412, 262)] trunk = [(476, 262), (494, 258), (510, 300), (528, 344), (548, 364), (540, 374), (514, 356), (494, 314), (474, 292)] tusk = [(466, 294), (478, 290), (498, 318), (492, 322)] ear = [(392, 206), (428, 200), (446, 232), (440, 280), (416, 302), (394, 270)] for part in (far_hind, far_fore, tail): shape(part, LTGRAY, speed=8) shape(body, LTGRAY, speed=7, hold=5) for part in (hind, fore): shape(part, LTGRAY, speed=8) shape(head, LTGRAY, speed=7) shape(trunk, LTGRAY, speed=7) shape(tusk, WHITE, speed=10) shape(ear, LTGRAY, speed=7, hold=4) line([(260, 238), (300, 222), (352, 224)], DKGRAY, speed=12) # the line of the back d.ellipse([462, 232, 466, 236], fill=BLACK); shoot(10) # the eye goes in last, and then somebody is there # --- what it has found --- stem = [(566, 398), (578, 398), (580, 420), (564, 420)] cap = [(572, 356), (582, 366), (588, 384), (584, 400), (560, 400), (556, 384), (562, 366)] shape(stem, WHITE, speed=10) shape(cap, BROWN, speed=8) for px, py in ((570, 368), (577, 376), (565, 380), (572, 388), (581, 390), (566, 393)): d.polygon([(px, py - 3), (px + 3, py), (px, py + 3), (px - 3, py)], fill=DKGRAY, outline=BLACK); shoot(2) shoot(8) # --- and the words, typed --- font = ImageFont.load_default_imagefont() # Pillow's old bitmap font: real pixels, nothing to smooth TEXT = ["For Terry. This is 16 color,", "still pleasant to create."] small = Image.new("L", (W // 2, 24), 0); sd = ImageDraw.Draw(small); sd.fontmode = "1" tx, ty = 196, 118 for row, text in enumerate(TEXT): for i in range(1, len(text) + 1): small.paste(0, (0, 0, small.width, small.height)) sd.text((0, 0), text[:i], font=font, fill=255) big = small.resize((small.width * 2, small.height * 2), Image.NEAREST) # each font pixel becomes four mask = big.point(lambda v: 255 if v > 127 else 0).convert("1") img.paste(BLUE, (tx, ty + row * 24), mask) if text[i - 1] != " ": shoot(2) shoot(8) # --- cut the film to length: keep every frame's order, drop evenly if it ran long, and stand back at the end --- want = int((a.seconds - 4) * FPS) if len(frames) > want: frames = [frames[int(i * len(frames) / want)] for i in range(want)] frames += [img.copy()] * (4 * FPS) ff = subprocess.Popen(["ffmpeg", "-loglevel", "error", "-y", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{W * SCALE}x{H * SCALE}", "-r", str(FPS), "-i", "-", "-c:v", "libx264", "-preset", "slow", "-crf", "14", "-pix_fmt", "yuv420p", "-movflags", "+faststart", a.out], stdin=subprocess.PIPE) for f in frames: ff.stdin.write(f.convert("RGB").resize((W * SCALE, H * SCALE), Image.NEAREST).tobytes()) ff.stdin.close(); ff.wait() if a.still: img.resize((W * SCALE, H * SCALE), Image.NEAREST).save(a.still) used = sorted(set(img.tobytes())) print(f"{len(frames)} frames, {len(frames) / FPS:.1f}s, colours used: {len(used)} of 16 ({', '.join(EGA[i] for i in used)})", file=sys.stderr)