Files
akbasic/examples/megademo/vaporwave.py

328 lines
11 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Generate the megademo's vaporwave picture, and its six frames of video.
The picture is composed directly in akbasic's 16-colour palette space at
160x120 -- one cell is a 5x5 pixel block on the 800x600 window -- then
run-length encoded into string literals the demo decodes with INSTR and
draws with three WIDTH-2 lines per run.
The video is delta frames. Six phases of a perfect loop -- the floor
grid advancing on a geometric progression parameterised so phase 6 lands
exactly on phase 0, and the sun's slice pattern crawling on a period-six
cycle -- and each frame encodes only the rows that differ from the one
before it: an 'R' record carrying the row in two base-36 digits, then
ordinary runs, with unreachable colour 'Q' as a skip. The rays are not
in the encoding at all; the demo overdraws them live, which is what
turns a thirty-run grid row back into a five-run one.
Why strings and not DATA: the interpreter's DATA pool is 512 items and
the stroke font already holds ~350 of them. A string literal carries one
RLE run in two characters. TODO.md section 4 records the gap.
Why the dithering is by row and not by pixel: per-pixel ordered dither
shatters every gradient into one-cell runs, which is death for RLE.
Row-phase dither keeps the runs long, and horizontal banding is what a
CRT did to a gradient anyway.
Usage:
python3 vaporwave.py --preview OUT.png write a x5 preview of
phase 0, rays included
python3 vaporwave.py --splice MEGADEMO.BAS rewrite the generated
block between the
PICTURE-BEGIN/END markers
"""
import argparse
import struct
import sys
import zlib
W, H = 160, 120
PHASES = 6
# src/graphics_tables.c, Pepto's PAL measurement. Index 0 unused.
PALETTE = [
(0x00, 0x00, 0x00), (0x00, 0x00, 0x00), (0xff, 0xff, 0xff),
(0x88, 0x39, 0x32), (0x67, 0xb6, 0xbd), (0x8b, 0x3f, 0x96),
(0x55, 0xa0, 0x49), (0x40, 0x31, 0x8d), (0xbf, 0xce, 0x72),
(0x8b, 0x54, 0x29), (0x57, 0x42, 0x00), (0xb8, 0x69, 0x62),
(0x50, 0x50, 0x50), (0x78, 0x78, 0x78), (0x94, 0xe0, 0x89),
(0x78, 0x69, 0xc4), (0x9f, 0x9f, 0x9f),
]
# Run encoding alphabets. Position decides meaning, so overlap is fine.
# 'Q' is a skip (INSTR misses the colour table and the decoder's guard
# draws nothing) and 'R' opens a row record.
COLORCH = "ABCDEFGHIJKLMNOP"
SKIP = "Q"
ROWREC = "R"
LENCH = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
MAXRUN = len(LENCH)
PAYLOAD = 240
SUN_CX, SUN_CY, SUN_R = 80, 50, 30
HORIZON = 74
GRID_R = 1.55
RAY_SPREAD = 26
def band_mix(y, x, frac, a, b):
threshold = (((y * 5) + ((x // 8) * 3)) % 8) / 8.0
return b if frac > threshold else a
def sky_color(y, x):
bands = [(7, 5), (5, 11), (11, 9)]
seg = HORIZON / len(bands)
i = min(int(y / seg), len(bands) - 1)
frac = (y - (i * seg)) / seg
a, b = bands[i]
return band_mix(y, x, frac, a, b)
def sun_color(y):
frac = (y - (SUN_CY - SUN_R)) / (2.0 * SUN_R)
if frac < 0.4:
return 8
if frac < 0.7:
return 9
return 3
def sun_sliced(y, phase):
"""Period-six cuts below the sun's midline, crawling with the phase."""
if y < SUN_CY:
return False