Decompress a full-screen picture and six frames of video from strings
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m23s
akbasic CI Build / sanitizers (push) Failing after 4m33s
akbasic CI Build / coverage (push) Failing after 3m45s
akbasic CI Build / akgl_build (push) Failing after 24s
akbasic CI Build / mutation_test (push) Failing after 3m30s

The megademo gains a scene: an 800x600 vaporwave sunset carried inside
the listing, then six looping delta frames of full motion video -- the
floor grid rolling forward and the sun's slices crawling, about 220
bytes a frame. DATA cannot carry an image (512 items, ~350 owned by the
stroke font), so the picture rides in RLE-encoded string literals
decoded with INSTR: 2 KB of base frame, 1.3 KB of video. Row records
jump the decoder between changed rows, an unreachable colour is the
skip, black draws so a delta can erase, and the rays are struck live
over every frame rather than encoded. The 63-byte bird gliding over it
is the one image DATA does have room for, via SPRSAV's type-in form.

examples/megademo/vaporwave.py composes the image in palette space,
dithers by row so the gradients band instead of shattering the RLE,
proves base-plus-deltas reproduces every frame against a simulation of
the decoder, and owns the generated block between the PICTURE markers.
TODO.md section 4 records the DATA ceiling and the wanted verb.

Co-Authored-By: Claude Code (Fable 5) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
This commit is contained in:
2026-08-02 16:26:56 -04:00
parent 6536658c92
commit 0d79a3f52c
4 changed files with 578 additions and 12 deletions

11
TODO.md
View File

@@ -778,6 +778,17 @@ and that is the argument for the verb. Ordered by how much BASIC each one would
the note rather than read from shared state at release. The demo works around it the way the note rather than read from shared state at release. The demo works around it the way
1985 did on one channel: an interleaved kickbassarpeggio line (the `TUNE` label), which 1985 did on one channel: an interleaved kickbassarpeggio line (the `TUNE` label), which
is authentic and should not be mandatory. is authentic and should not be mandatory.
- **`DATA` is too small a pipe for assets, and `SPRSAV`'s type-in form is 24 by 21.** The
pool is 512 items and the demo's stroke font already holds ~350, so a full-screen picture
can never arrive by `DATA` — and the one sprite form that reads from the program is fixed
at the VIC-II's 24x21 monochrome. The demo's scene 5 shows both the ceiling and the way
around it: a 160x120 sixteen-colour image rides in **string literals** instead — RLE, two
characters a run, ~4 KB in eighteen `IM$()` assignments, decoded with `INSTR` and painted
in 5x5 blocks (`examples/megademo/vaporwave.py` generates the block) — while the 63-byte
bird beside it is all `DATA` can actually carry. Strings are the workaround, not the
answer: every program that wants a picture must also carry a decoder. What is wanted is
either a bigger `DATA` pool (it is one constant) or, better, a `SPRSAV`/`BLOAD` form that
accepts packed image data from program text at a stated width and height.
- **The only vsync is spinning on `TI#`, and the spin is worse than it looks.** Chapter 13 - **The only vsync is spinning on `TI#`, and the spin is worse than it looks.** Chapter 13
documents the polling; the demo's first cut had the loop — read `TI#`, branch to yourself documents the polling; the demo's first cut had the loop — read `TI#`, branch to yourself
until it moves — and it did not just waste CPU, it **starved the `PLAY` queue dry**. The until it moves — and it did not just waste CPU, it **starved the `PLAY` queue dry**. The

View File

@@ -8,14 +8,15 @@ megagreetz to starfort.
./build-akgl/basic examples/megademo/megademo.bas ./build-akgl/basic examples/megademo/megademo.bas
``` ```
Q or ESC quits. Six scenes: a title card, a Lissajous braid, a spinning wireframe Q or ESC quits. Seven scenes: a title card, a Lissajous braid, a spinning wireframe
cube and octahedron — which drift together, ignite a pulsing origin point strobing cube and octahedron — which drift together, ignite a pulsing origin point strobing
the next scene's colours, swallow the camera as the perspective scale outgrows the the next scene's colours, swallow the camera as the perspective scale outgrows the
window, and split that origin into the first two raster bars — then the raster window, and split that origin into the first two raster bars — the raster bars
bars proper, the sprite bank going supernova — the letters fall into the middle, a proper, a full-screen vaporwave sunset decompressed out of the listing itself, the
fireball throws them at the walls, and a black hole drags everything back into the sprite bank going supernova — the letters fall into the middle, a fireball throws
centre as the transition out — and a finale that composes all of it. A sawtooth them at the walls, and a black hole drags everything back into the centre as the
chiptune in A minor throughout, with a noise drum break, if a SID answers. transition out — and a finale that composes all of it. A sawtooth chiptune in A
minor throughout, with a noise drum break, if a SID answers.
## What it leans on ## What it leans on
@@ -44,6 +45,29 @@ This is a tour of the interpreter's edges, on purpose:
the window, which the silent clipping permits. The pulsing origin point strobes the window, which the silent clipping permits. The pulsing origin point strobes
through the bar ramp table because it *is* the next scene: it splits, rides the through the bar ramp table because it *is* the next scene: it splits, rides the
Y axis to the band edges, and stretches into the first two raster bars in place. Y axis to the band edges, and stretches into the first two raster bars in place.
- **The picture is strings, because `DATA` is a straw** — the pool is 512 items
and the font owns ~350, so the full-screen sunset rides in string literals:
160x120 cells, sixteen colours, run-length encoded two characters a run and
decoded with `INSTR` into 5x5 blocks — about 2 KB for the whole base frame.
`vaporwave.py` composes the image in palette space — dithered by *row*, not
pixel, so the gradients band like a CRT instead of shattering the RLE — and
owns the generated block between the `PICTURE-BEGIN`/`PICTURE-END` markers. Do
not hand-edit it; rerun the script. The bird gliding over it is the
counterpoint: a genuine 24x21 `SPRSAV` type-in sprite, 63 bytes, which is
everything `DATA` has room to say. The paint is the reveal, and the exit is a
stride-63 column dissolve — 63 is coprime to 160, so the walk hits every
column once and looks random while carrying no state.
- **And then the picture is video.** Six delta frames loop the floor grid toward
you and crawl the sun's slice pattern — full motion video, 1.3 KB total, about
220 bytes a frame. A delta re-encodes only the rows that changed: an `R`
record jumps the decoder's cursor (two base-36 digits of row), `Q` skips what
stayed, and black *draws*, which is how a moved grid line is erased. The rays
are not in the encoding at all; they are struck live over every frame, which
is the difference between a thirty-run grid row and a five-run one. The loop
is mathematically exact: line k sits at `B*(r^(k+t)-1)` so six frames advance
t by one and land the set on itself, and the slice period divides the frame
count. The generator simulates the decoder and asserts base-plus-deltas
reproduces every frame before it will emit a byte.
- **3D with no matrices** — the wireframe scene rotates fourteen pooled vertices - **3D with no matrices** — the wireframe scene rotates fourteen pooled vertices
through two composed axis rotations and a perspective divide, all of it `SIN`, through two composed axis rotations and a perspective divide, all of it `SIN`,
`COS` and one division, with the float kept on the left of every product because `COS` and one division, with the float kept on the left of every product because

View File

@@ -57,9 +57,14 @@ DIM WQX#(14)
DIM WQY#(14) DIM WQY#(14)
DIM WE1#(24) DIM WE1#(24)
DIM WE2#(24) DIM WE2#(24)
DIM PBRD#(63)
ALPHA$ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 :-.!" ALPHA$ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 :-.!"
GC$ = "GAMECUBE" GC$ = "GAMECUBE"
REM The picture decoder's alphabets: a run is two characters, colour
REM then length, and INSTR turns each back into a number.
CPAL$ = "ABCDEFGHIJKLMNOP"
LTAB$ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
SEED# = 314159 SEED# = 314159
QF# = 0 QF# = 0
RB# = 0 RB# = 0
@@ -120,6 +125,7 @@ SEED# = SEED# + TI#
GOSUB LOADTABLES GOSUB LOADTABLES
GOSUB LOADWIRE GOSUB LOADWIRE
GOSUB LOADFONT GOSUB LOADFONT
GOSUB LOADBIRD
IF SND# = 1 THEN GOSUB SIDINIT IF SND# = 1 THEN GOSUB SIDINIT
GOSUB TUNE GOSUB TUNE
IF TXT# = 1 THEN CHAR 1, 0, 0, "AKLABS MEGADEMO 85 - PRESS Q TO EXIT" IF TXT# = 1 THEN CHAR 1, 0, 0, "AKLABS MEGADEMO 85 - PRESS Q TO EXIT"
@@ -429,7 +435,117 @@ LABEL BARDONE
IF QF# = 1 THEN GOTO BYE IF QF# = 1 THEN GOTO BYE
REM ===================================================================== REM =====================================================================
REM SCENE 5 : THE SPRITE BANK IS THE SCROLLTEXT, AND THEN IT EXPLODES. REM SCENE 5 : THE PICTURE, AND THEN IT MOVES. A full 800x600
REM vaporwave sunset carried inside the listing, then six delta
REM frames of full motion video in a loop. DATA could never hold any
REM of it -- the pool is 512 items and the font owns most of them
REM (TODO.md section 4) -- so it all rides in string literals:
REM run-length encoded, two characters a run, decoded with INSTR and
REM painted as 5x5 blocks. A video frame re-encodes only the rows
REM that changed ('R' records jump the cursor), 'Q' skips what stayed
REM and black draws over what moved, so a frame of grid-scroll and
REM sun-shimmer is about two hundred bytes and paints in a few
REM presents. The rays are not encoded at all: they are struck live
REM over every frame, which is what keeps the floor rows cheap. The
REM loop is exact -- the grid advances one geometric step over six
REM frames and the slice pattern's period is six. The paint is the
REM reveal; the exit is a stride-63 column dissolve. The bird is the
REM one thing DATA does hold: a real 24x21 SPRSAV sprite, 63 bytes,
REM gliding across its own sunset. vaporwave.py draws the picture,
REM proves base plus deltas reproduce every frame, and owns the
REM generated block.
REM =====================================================================
GRAPHIC 1, 1
WIDTH 2
BS% = W# / 160
IF BS% < 1 THEN BS% = 1
REM ---- PICTURE-BEGIN (generated by vaporwave.py; do not
REM ---- hand-edit -- rerun the script to change the picture)
DIM IM$(16)
DIM VA#(6)
DIM VB#(6)
NS# = 9
VA#(0) = 9
VB#(0) = 9
VA#(1) = 10
VB#(1) = 10
VA#(2) = 11
VB#(2) = 12
VA#(3) = 13
VB#(3) = 13
VA#(4) = 14
VB#(4) = 14
VA#(5) = 15
VB#(5) = 15
IM$(0) = "G9G9G9G9GPGHEHG9GTEHG9GTEHGPGPEHG9GTEHG9GTEHGHGXEHGTBAG8EHG9GTEHG5EHGPEHG5EHGPEHG5EHGAPAG3EHGPEHG5EHGPEHGXGHEHG5EHGPEHG5EHGFBAGIEHGPGPEHGPEHGHEHGPEHGPEHGHEHGPEHGHEHGPEHGPEHGHEHGBPAGMEHGPEHGHEHGPEHGHEHGPEHGPEHGHEHGPEHGPEHGHEHGPEHGHEPGHEHGPEH"
IM$(1) = "GHEPGHEHGPEHGHEPGHEHGHEGBAEHGHEHGPEHGHEPGHEHGPEHGHEHGPEHGHEPGHEHGPEHGHEPGHEHGPECPAEDGHEPGHEHGHEPGHEPGHEHGHEPGHEPGHEHGHEPGHEHGHEPGHEPGHECBAEDGHEPGHEPGHEHGHEPGHEHGHEPGHEPGHEHGHEPGHEPEPGHEPGHE5GHEPGHE5GHEHEXGHEPGHEEPAEZGHEPGHE5GHE5GHEPGHE5GHEP"
IM$(2) = "GHE5GHE9ETGHE9ETGHEXEHGHE9ETGHHAE9ESGHEPEPGBBAGEE9EMHOE9ETGHEHE9E7HUE9E6E9E4H0E9E3E9E3H2E9EHBAETEHKHE9ELH6E9ECKHEPEPKHE9EBH9HAE9EIKHEHEXKHE2H9HCE9EPKHE5KHEPKDH9HEKCEPKHE5KHE5KHEKH9HGEBKHEPKHEXEHKHE5KHEBH9HIEIKHEPKHEPEPKHEPKHEHKAH9HKKHEHKHEP"
IM$(3) = "KHEHKHEPKHEPKHH9HMEGKHEHKHEPKHEHKHEPKHEPH9HMEOKHEHKHEPKHEHKPEHKHEGH9HOKFEPKHEHKPEHKHEHKPEHKGH9HOEFKHEPKHEHKHEPKHEHKPEFH9HQKEEHKHEPKHEHKHEPKHEHKMH9HSKLEHKHEPKHEHKPEHKHEHKEH9HSEDKPEHKPEHKHEHKPEHKHEEH9HSKDEHKPEHKPKPEHKPEHKDH9HUECKHEHKPEHKHKXEH"
IM$(4) = "KPEDH9HUKCEHK5EHK5EHKLH9HUKKEHK5EHK5EHKCH9HWEBKPEHKXKHEHK8I9IWKZEHKPKPEHK0I9IWK7EHKHKXEHKSI9IWK9KFEHK9KOI9IWK9KNK9KOI9IWK9KNK9KOI9IWK9KNKPIHKZI9IYK6IHKHKXIHKSI9IWK9KFIHK5IHKKI9IWK9KNIHK5IHKCI9IYKPIHKXKHIHK5IHKPIHK5IHKPIHKPKPIHK0I9IWKJIHKPIH"
IM$(5) = "KHIHKPIHKPI9IZKBIHKHIHKPIHKHIHKPIHKKI9IWKJIHKHIHKPIHKHIHKPIHKDI9IXKPIHKHIHKHKHIHKHIPKHI9IYKCIHKPIHKHIHKPIHKHIPKHIHKPIHKHIPKHIHKPIHKHIHKPIHKHI9I9IHKHIHKPIHKHIPKHIHKHIEC9CSKDIPKHIPKHIHKHIPKHIHKEC9CSIDKHIPKHIPIPKHIPKHIFC9CQKEIHKHIPKHIHIXKHIPKG"
IM$(6) = "C9COIFKHI5KHI5KHIPKHI5KHIPKHI5KHI5KHIPKHI5KHIPKHIXIHKHI9IDC9CMI4KHIPIPKHI6C9CKI9IDKHIHIXKHIZC9CII9IMKHI9IWC9CGI9IVI9I9I9I9IPI9I9I9I9IPQ9Q9Q9Q9QPE9E9E9E9EPE9E9E9E9EPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9"
IM$(7) = "Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9"
IM$(8) = "Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QP"
IM$(9) = "RBRQ9QOKMQHK5RBSQ9QTI9IRRBXQ9QTKPQHKHQPKERBYQ9QPI9IHRB3Q9QSKAI5KHIJRB5Q9QTC9CMRB9Q9QWI9IGRCBQ9QYC9CCRCIA9A9A9A9APRCJE9E9E9E9EPRCNA9A9A9A9APRCOE9E9E9E9EPRCUA9A9A9A9APRCVE9E9E9E9EPRC5A9A9A9A9APRC7E9E9E9E9EP"
IM$(10) = "RBQQ9QOK9KIQHKFRBRQ9QOI9IQRBWQ9QPKLQHKHQHKPRBXQ9QTI9QLIERB2Q9QRIBKHIPKHIPKCRB4Q9QSC9CORB8Q9QVI3KHIGRCAQ9QXC9CERCJA9A9A9A9APRCKE9E9E9E9EPRCOA9A9A9A9APRCPE9E9E9E9EPRCVA9A9A9A9APRCXE9E9E9E9EPRC7A9A9A9A9APRDAE9E9E9E9EP"
IM$(11) = "RBPQ9QOK9KAQHKNRBQQ9QOI9IWRBVQ9QOKEQHKHQHKPQHKFRBWQ9QPI9IPRB1Q9QQKCIPKHIPKHIDRB3Q9QSC9CORB7Q9QUIWKHIPRB9Q9QWC9CGRCEA9A9A9A9APRCFE9E9E9E9EPRCGA9A9A9A9APRCHE9E9E9E9EPRCPA9A9A9A9APRCQE9E9E9E9EPRCXA9A9A9A9APRCZE9E9E9E9EPRDAA9A9A9A9APRDCE9E9E9E9"
IM$(12) = "EP"
IM$(13) = "RBOQ9QNK3QHKWRBPQ9QOI9IWRBUQ9QTKHQHKPQHKNRBVQ9QOI9IWRB0Q9QQIKKHIPKHIHKDRB1Q9QQC9CSRB2Q9QRC9CQRB6Q9QTIPKHIYRB8Q9QVC9CIRCKA9A9A9A9APRCLE9E9E9E9EPRCQA9A9A9A9APRCRE9E9E9E9EPRCZA9A9A9A9APRC1E9E9E9E9EPRDCA9A9A9A9APRDFE9E9E9E9EP"
IM$(14) = "RBOQ9QNI9IYRBTQ9QOKEQHKPQHKVRBUQ9QTI9IRRBZQ9QTKHQHKPQHKHRB0Q9QQC9CSRB5Q9QTIHKHI5KARB7Q9QUC9CKRCBQ9QYI9ICRCHA9A9A9A9APRCIE9E9E9E9EPRCLA9A9A9A9APRCME9E9E9E9EPRCRA9A9A9A9APRCTE9E9E9E9EPRC1A9A9A9A9APRC3E9E9E9E9EPRDFA9A9A9A9APRDIE9E9E9E9EP"
IM$(15) = "RBSQ9QTKPQHK3RBTQ9QOI9IWRBYQ9QPKDQHKPQHKHRBZQ9QTI9ILRB4Q9QSIAKHI5KHIBRB6Q9QTC9CMRCAQ9QXI9IERCEE9E9E9E9EPRCFA9A9A9A9APRCGE9E9E9E9EPRCMA9A9A9A9APRCNE9E9E9E9EPRCTA9A9A9A9APRCUE9E9E9E9EPRC3A9A9A9A9APRC5E9E9E9E9EPRDIA9A9A9A9AP"
REM ---- PICTURE-END
SS# = 0
SE# = NS# - 1
GOSUB DRAWSTREAM
GOSUB RAYS
SPRSAV PBRD#, 1
SPRITE 1, 1, 1, 0, 1, 1
MOVSPR 1, 0.16 * W#, 0.14 * H#
IF HL# = 0 THEN MOVSPR 1, 96 # 1
COLOR 1, 2
T$ = "THE PICTURE IS STRINGS - THE BIRD IS DATA"
SZ% = 0.0038 * W#
GOSUB CENTERX
TY% = 0.02 * H#
GOSUB DRAWTEXT
IF HL# = 1 THEN GOTO PICDONE
REM Six delta frames in a loop: the grid rolls toward you and the
REM sun's slices crawl. Each frame repaints only the rows that
REM changed, and the rays are struck over the top again.
VI# = 0
DL# = TI# + 480
DO WHILE TI# < DL#
SS# = VA#(VI#)
SE# = VB#(VI#)
GOSUB DRAWSTREAM
GOSUB RAYS
VI# = VI# + 1
IF VI# >= 6 THEN VI# = 0
IF TI# > MT# THEN GOSUB TUNE
GOSUB POLLKEY
IF QF# = 1 THEN EXIT
LOOP
REM The dissolve: black out the 160 columns in stride-63 order.
COLOR 1, 1
K2# = 0
DO WHILE K2# < 160
CO# = MOD(K2# * 63, 160)
X3% = CO# * BS%
DRAW 1, X3%, 0 TO X3%, H# - 1
X3% = X3% + 2
DRAW 1, X3%, 0 TO X3%, H# - 1
X3% = X3% + 2
DRAW 1, X3%, 0 TO X3%, H# - 1
IF MOD(K2#, 5) = 0 THEN GOSUB VSYNC
K2# = K2# + 1
LOOP
MOVSPR 1, 0 # 0
SPRITE 1, 0
LABEL PICDONE
IF QF# = 1 THEN GOTO BYE
REM =====================================================================
REM SCENE 6 : THE SPRITE BANK IS THE SCROLLTEXT, AND THEN IT EXPLODES.
REM Eight letters, eight sprites: draw each with the stroke font, REM Eight letters, eight sprites: draw each with the stroke font,
REM SSHAPE it, SPRSAV it, surf it on a sine. Then the letters fall REM SSHAPE it, SPRSAV it, surf it on a sine. Then the letters fall
REM into the middle, a supernova throws them at the walls -- SOLID REM into the middle, a supernova throws them at the walls -- SOLID
@@ -573,7 +689,7 @@ LABEL WAVEDONE
IF QF# = 1 THEN GOTO BYE IF QF# = 1 THEN GOTO BYE
REM ===================================================================== REM =====================================================================
REM SCENE 6 : THE FINALE CARD. Everything at once: stars, bars, the REM SCENE 7 : THE FINALE CARD. Everything at once: stars, bars, the
REM logo, the greetz, and the sprite bank surfing a sine. This is the REM logo, the greetz, and the sprite bank surfing a sine. This is the
REM frame a clockless host leaves on the screen. REM frame a clockless host leaves on the screen.
REM ===================================================================== REM =====================================================================
@@ -869,6 +985,7 @@ REM A ball of light: concentric circles three pixels apart, solid at
REM WIDTH 2. DCX%/DCY% centre it, DR% sizes it, DCI# colours it -- REM WIDTH 2. DCX%/DCY% centre it, DR% sizes it, DCI# colours it --
REM DCI# = 1 is the eraser. REM DCI# = 1 is the eraser.
LABEL DISC LABEL DISC
IF DR% < 1 THEN RETURN
COLOR 1, DCI# COLOR 1, DCI#
R3% = DR% R3% = DR%
DO WHILE R3% > 1 DO WHILE R3% > 1
@@ -881,13 +998,11 @@ REM The origin point: erase it where it was, redraw it pulsing on
REM SIN(BF#) and strobing through the bar ramp table -- the colours REM SIN(BF#) and strobing through the bar ramp table -- the colours
REM of the next scene, because this is where its bars come from. REM of the next scene, because this is where its bars come from.
LABEL BALLFX LABEL BALLFX
IF PB% <= 0 THEN GOTO BALLFX2
DCI# = 1 DCI# = 1
DR% = PB% DR% = PB%
DCX% = C1X% DCX% = C1X%
DCY% = CY% DCY% = CY%
GOSUB DISC GOSUB DISC
LABEL BALLFX2
DR% = BBR% + ((0.45 * BBR%) * SIN(1.1 * BF#)) DR% = BBR% + ((0.45 * BBR%) * SIN(1.1 * BF#))
DCI# = BARC#(MOD(BF#, 20)) DCI# = BARC#(MOD(BF#, 20))
DCX% = C1X% DCX% = C1X%
@@ -920,6 +1035,72 @@ IF SND# = 0 THEN RETURN
SOUND 3, 4000, 40, 1, 52000, 800 SOUND 3, 4000, 40, 1, 52000, 800
RETURN RETURN
REM Decode picture strings IM$(SS#) through IM$(SE#) and paint them
REM as 5x5 blocks. A run is colour-char then length-char. 'Q' misses
REM the colour table, so it advances without drawing -- the skip. 'A'
REM is black and DOES draw, which is how a video delta erases. 'R'
REM opens a row record: two base-36 digits of row, cursor to column
REM zero -- how a delta jumps between the rows that changed.
LABEL DRAWSTREAM
X# = 0
Y# = 0
S# = SS#
DO WHILE S# <= SE#
T2$ = IM$(S#)
TL2# = LEN(T2$)
P# = 0
DO WHILE P# < TL2#
C2$ = MID(T2$, P#, 1)
IF C2$ = "R" THEN GOSUB PICROW ELSE GOSUB PICRUN2
LOOP
GOSUB POLLKEY
S# = S# + 1
LOOP
RETURN
LABEL PICROW
Y# = INSTR(LTAB$, MID(T2$, P# + 1, 1)) * 36
Y# = Y# + INSTR(LTAB$, MID(T2$, P# + 2, 1))
X# = 0
P# = P# + 3
RETURN
LABEL PICRUN2
CI# = INSTR(CPAL$, C2$) + 1
LN# = INSTR(LTAB$, MID(T2$, P# + 1, 1)) + 1
IF CI# > 0 THEN GOSUB PICRUN
X# = X# + LN#
IF X# >= 160 THEN X# = 0
IF X# = 0 THEN Y# = Y# + 1
P# = P# + 2
RETURN
REM One run: LN# cells of colour CI# at cell (X#, Y#), scaled by BS%.
LABEL PICRUN
X3% = X# * BS%
X4% = ((X# + LN#) * BS%) - 1
Y3% = Y# * BS%
COLOR 1, CI#
DRAW 1, X3%, Y3% TO X4%, Y3%
Y3% = Y3% + 2
DRAW 1, X3%, Y3% TO X4%, Y3%
Y3% = Y3% + 2
DRAW 1, X3%, Y3% TO X4%, Y3%
RETURN
REM The rays live outside the encoding, redrawn over every video
REM frame: nineteen lines instead of thirty runs a grid row, and they
REM are what makes the encoded floor nearly free.
LABEL RAYS
COLOR 1, 4
RI# = 0 - 9
DO WHILE RI# < 10
X3% = (0.5 * W#) + (RI# * 26 * BS%)
DRAW 1, 0.5 * W#, (74 * BS%) + 2 TO X3%, H# + 99
RI# = RI# + 1
LOOP
RETURN
REM Park the eight letter sprites on a sine. REM Park the eight letter sprites on a sine.
LABEL WAVE LABEL WAVE
L# = 1 L# = 1
@@ -954,11 +1135,9 @@ RETURN
REM One frame of fireball: erase the previous radius, draw the new REM One frame of fireball: erase the previous radius, draw the new
REM one. PR% carries the radius to erase between calls. REM one. PR% carries the radius to erase between calls.
LABEL BOOMFX LABEL BOOMFX
IF PR% <= 0 THEN GOTO BOOMFX2
BCF# = 1 BCF# = 1
BR2% = PR% BR2% = PR%
GOSUB BOOMPASS GOSUB BOOMPASS
LABEL BOOMFX2
BCF# = 0 BCF# = 0
BR2% = ER% BR2% = ER%
GOSUB BOOMPASS GOSUB BOOMPASS
@@ -969,6 +1148,7 @@ REM The fireball at radius BR2%: three rings, white heart to orange
REM rim, and twelve red spokes thrown past the outer ring. BCF# = 1 REM rim, and twelve red spokes thrown past the outer ring. BCF# = 1
REM draws the whole thing in black, which is the eraser. REM draws the whole thing in black, which is the eraser.
LABEL BOOMPASS LABEL BOOMPASS
IF BR2% < 1 THEN RETURN
R3% = BR2% R3% = BR2%
IF BCF# = 1 THEN COLOR 1, 1 ELSE COLOR 1, 2 IF BCF# = 1 THEN COLOR 1, 1 ELSE COLOR 1, 2
CIRCLE 1, CX0%, CY0%, R3%, R3% CIRCLE 1, CX0%, CY0%, R3%, R3%
@@ -1220,6 +1400,15 @@ FOR K# = 1 TO N# * 2
NEXT K# NEXT K#
RETURN RETURN
REM The bird: SPRSAV's type-in form, sixty-three bytes, three per row,
REM most significant bit leftmost.
LABEL LOADBIRD
FOR I# = 0 TO 62
READ D#
PBRD#(I#) = D#
NEXT I#
RETURN
REM ===================================================================== REM =====================================================================
REM Trap handlers for the hardware probes. Each marks its device gone REM Trap handlers for the hardware probes. Each marks its device gone
REM and lets the probe fall through to the next line. REM and lets the probe fall through to the next line.
@@ -1367,3 +1556,18 @@ REM full stop
DATA 1, 15,16 DATA 1, 15,16
REM exclamation mark REM exclamation mark
DATA 2, 10,14, 15,16 DATA 2, 10,14, 15,16
REM The bird, 24 by 21, one bit a pixel:
REM ..XXX...........XXX.....
REM .X...XX.......XX...X....
REM X.....XXX...XXX.....X...
REM .......XXXXXX...........
REM .........XX.............
REM and sixteen empty rows of sky.
DATA 0, 0, 0, 56, 0, 224, 70, 3, 16
DATA 131, 142, 8, 1, 248, 0, 0, 96, 0
DATA 0, 0, 0, 0, 0, 0, 0, 0, 0
DATA 0, 0, 0, 0, 0, 0, 0, 0, 0
DATA 0, 0, 0, 0, 0, 0, 0, 0, 0
DATA 0, 0, 0, 0, 0, 0, 0, 0, 0
DATA 0, 0, 0, 0, 0, 0, 0, 0, 0

View File

@@ -0,0 +1,327 @@
#!/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
return ((y + phase) % 6) < (1 + min(1, (y - SUN_CY) // 14))
def grid_rows(t):
"""Horizontal grid lines at loop parameter t in [0,1): row k sits at
HORIZON+1 + B*(r^(k+t)-1), so t=1 reproduces t=0 shifted one line."""
rows, k = [], 0
while True:
y = HORIZON + 1 + 2.2 * (GRID_R ** (k + t) - 1.0)
if y >= H:
return rows
rows.append(int(y))
k += 1
def compose(phase, rays=False):
img = [[1] * W for _ in range(H)]
for y in range(HORIZON):
for x in range(W):
img[y][x] = sky_color(y, x)
for i, (sx, sy) in enumerate([(9, 5), (31, 11), (52, 3), (74, 8),
(99, 14), (126, 6), (147, 12), (18, 21),
(139, 24), (61, 17)]):
img[sy][sx] = 2 if i % 3 else 16
for y in range(SUN_CY - SUN_R, SUN_CY + SUN_R + 1):
if y < 0 or y >= HORIZON or sun_sliced(y, phase):
continue
dy = y - SUN_CY
half = int((SUN_R * SUN_R - dy * dy) ** 0.5)
for x in range(SUN_CX - half, SUN_CX + half + 1):
img[y][x] = sun_color(y)
for r in grid_rows(phase / float(PHASES)):
for x in range(W):
img[r][x] = 5
if rays:
for i in range(-9, 10):
x0, y0 = SUN_CX, HORIZON
x1, y1 = SUN_CX + (i * RAY_SPREAD), H + 20
steps = max(abs(x1 - x0), abs(y1 - y0))
for s in range(steps + 1):
x = x0 + ((x1 - x0) * s) // steps
y = y0 + ((y1 - y0) * s) // steps
if 0 <= x < W and HORIZON < y < H:
img[y][x] = 4
return img
def encode_run(color, n):
return COLORCH[color - 1] + LENCH[n - 1]
def encode_skip(n):
out = ""
while n > 0:
step = min(n, MAXRUN)
out += SKIP + LENCH[step - 1]
n -= step
return out
def encode_base(img):
"""The full raster, top to bottom; rows advance automatically.
Black spans become skips -- GRAPHIC's clear already painted them --
which is what keeps the floor nearly free. A skip draws nothing and
a black run draws black: the base wants the former, a delta erasing
a moved grid line needs the latter."""
out = ""
for y in range(H):
x = 0
while x < W:
c = img[y][x]
n = 1
while x + n < W and img[y][x + n] == c and n < MAXRUN:
n += 1
out += encode_skip(n) if c == 1 else encode_run(c, n)
x += n
return out
def encode_delta(prev, cur):
"""Row records for every row that differs, runs spanning the changed
extent. An unchanged span of eight or more becomes a skip; anything
shorter is simply repainted, which merges into its neighbours' runs.
Black runs cost nothing either way -- the decoder draws nothing for
colour 1 and just advances."""
out = ""
for y in range(H):
diffs = [x for x in range(W) if prev[y][x] != cur[y][x]]
if not diffs:
continue
lo, hi = diffs[0], diffs[-1]
out += ROWREC + LENCH[y // 36] + LENCH[y % 36]
if lo:
out += encode_skip(lo)
x = lo
while x <= hi:
n = 0
while x + n <= hi and prev[y][x + n] == cur[y][x + n]:
n += 1
if n >= 8:
out += encode_skip(n)
x += n
continue
c = cur[y][x]
n = 1
while x + n <= hi and cur[y][x + n] == c and n < MAXRUN:
n += 1
out += encode_run(c, n)
x += n
return out
def chop(blob):
return [blob[i:i + PAYLOAD] for i in range(0, len(blob), PAYLOAD)]
def simulate(raster, blob):
"""Apply one encoded stream to a raster exactly the way the BASIC
decoder does, skips-draw-nothing and all."""
x = y = p = 0
while p < len(blob):
c = blob[p]
if c == ROWREC:
y = (LENCH.index(blob[p + 1]) * 36) + LENCH.index(blob[p + 2])
x = 0
p += 3
continue
n = LENCH.index(blob[p + 1]) + 1
ci = COLORCH.find(c) + 1
if ci >= 1:
for i in range(n):
raster[y][x + i] = ci
x += n
if x >= W:
x = 0
y += 1
p += 2
return raster
def verify(frames, base_blob, delta_blobs):
"""The base must reproduce frame 0 exactly, and each delta must
carry the raster exactly to the next frame. A skip leaves the cell
the encoder promised was already right, so equality is total and
any difference at all is an encoder bug."""
raster = [[1] * W for _ in range(H)]
raster = simulate(raster, base_blob)
assert raster == frames[0], "base stream does not reproduce frame 0"
for i, blob in enumerate(delta_blobs):
want = frames[(i + 1) % PHASES]
raster = simulate(raster, blob)
assert raster == want, "delta %d does not reproduce its frame" % i
def emit_block(base_lines, delta_ranges, all_lines):
out = []
out.append("REM ---- PICTURE-BEGIN (generated by vaporwave.py; do not")
out.append("REM ---- hand-edit -- rerun the script to change the picture)")
out.append("DIM IM$(%d)" % len(all_lines))
out.append("DIM VA#(%d)" % PHASES)
out.append("DIM VB#(%d)" % PHASES)
out.append("NS# = %d" % len(base_lines))
for i, (a, b) in enumerate(delta_ranges):
out.append("VA#(%d) = %d" % (i, a))
out.append("VB#(%d) = %d" % (i, b))
for i, s in enumerate(all_lines):
out.append('IM$(%d) = "%s"' % (i, s))
out.append("REM ---- PICTURE-END")
return out
def write_png(path, img, scale=5):
w, h = W * scale, H * scale
raw = bytearray()
for y in range(h):
raw.append(0)
row = img[y // scale]
for x in range(w):
raw.extend(PALETTE[row[x // scale]])
def chunk(tag, data):
c = struct.pack(">I", len(data)) + tag + data
return c + struct.pack(">I", zlib.crc32(tag + data) & 0xffffffff)
with open(path, "wb") as f:
f.write(b"\x89PNG\r\n\x1a\n")
f.write(chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0)))
f.write(chunk(b"IDAT", zlib.compress(bytes(raw), 9)))
f.write(chunk(b"IEND", b""))
def splice(path, block):
with open(path) as f:
text = f.read().splitlines()
begin = next(i for i, l in enumerate(text) if "PICTURE-BEGIN" in l)
end = next(i for i, l in enumerate(text) if "PICTURE-END" in l)
text[begin:end + 1] = block
with open(path, "w") as f:
f.write("\n".join(text) + "\n")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--preview")
ap.add_argument("--splice")
args = ap.parse_args()
frames = [compose(p) for p in range(PHASES)]
base_blob = encode_base(frames[0])
base_lines = chop(base_blob)
all_lines = list(base_lines)
delta_ranges = []
delta_blobs = []
for i in range(PHASES):
blob = encode_delta(frames[i], frames[(i + 1) % PHASES])
delta_blobs.append(blob)
lines = chop(blob)
delta_ranges.append((len(all_lines), len(all_lines) + len(lines) - 1))
all_lines.extend(lines)
verify(frames, base_blob, delta_blobs)
dbytes = sum(len(b) for b in delta_blobs)
print("base %d bytes in %d strings; video %d bytes in %d strings; "
"%d strings total" %
(sum(len(s) for s in base_lines), len(base_lines), dbytes,
len(all_lines) - len(base_lines), len(all_lines)),
file=sys.stderr)
if args.preview:
write_png(args.preview, compose(0, rays=True))
if args.splice:
splice(args.splice, emit_block(base_lines, delta_ranges, all_lines))
if __name__ == "__main__":
main()