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
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:
11
TODO.md
11
TODO.md
@@ -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 kick–bass–arpeggio line (the `TUNE` label), which
|
1985 did on one channel: an interleaved kick–bass–arpeggio 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
327
examples/megademo/vaporwave.py
Normal file
327
examples/megademo/vaporwave.py
Normal 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()
|
||||||
Reference in New Issue
Block a user