You type a ZX Spectrum BASIC program. The page compiles it, in JavaScript, straight to an uncompressed .swf: the
Spectrum ROM font as sprite glyphs, a bank of square-wave DefineSounds for BEEP, and your program as AVM1
(ActionScript 1) bytecode with a small runtime. Ruffle, the open-source Flash player, runs it. Nothing is interpreted in the browser;
the BASIC really is running inside the Flash movie. Every edit rebuilds the movie and reloads it.
Lines may start with a number, as on the Spectrum. Lines without a number are numbered automatically (previous + 10).
Several statements go on one line separated by :. Keywords are case-insensitive. LET is optional.
PRINT items | items separated by ; (no gap), , (next 16-column zone), ' (newline). Modifiers: AT row,col; TAB n; INK n; PAPER n; BRIGHT n; |
INPUT "prompt"; a$ | reads from the lower screen; Enter finishes, Backspace edits. Several variables allowed. |
LET a = 1, LET a$ = "x" | string variables end in $ |
IF c THEN ... ELSE ... | ELSE is an extension; IF x THEN 100 jumps |
FOR i = a TO b STEP s / NEXT i | |
REPEAT / UNTIL c, WHILE c / WEND | extensions |
GO TO n, GO SUB n, RETURN | GOTO/GOSUB also accepted; computed targets work (GO TO 100+k*10) |
CLS, INK n, PAPER n, BORDER n, BRIGHT n | colours 0 black 1 blue 2 red 3 magenta 4 green 5 cyan 6 yellow 7 white; 8–15 are the bright set; extension: any #RRGGBB |
PLOT x,y, DRAW dx,dy, CIRCLE x,y,r | 256×176 pixel grid, origin bottom-left, colour items allowed first (PLOT INK 2; x,y) |
RECT x,y,w,h | extension: filled rectangle in the current INK (w, h in pixels) |
PAUSE n | n frames at 50 per second; PAUSE 0 waits for a key; any key ends a pause early |
KEEP "name", n | extension: remembers the number n under that name between visits (a Flash SharedObject, stored by the browser); the function KEPT "name" gives it back, or 0 if nothing was kept. Names are shared by every program, so make them specific, like "popparty.best" |
HALT | extension: wait for the next frame (a 50th of a second), like the Z80 instruction. A game loop ending in HALT runs 50 times a second; one ending in PAUSE 1 skips a frame and runs 25 times |
BEEP seconds, pitch | pitch in semitones from middle C, −24 to 36 (Spectrum square wave) |
LOADER, LOADER "name" | extension: stops the program and goes back to the loader menu, or opens the example with that id ("monster-maze", "tvp", "jump"...; the same ids as the # in the page address). It follows a #name link on this page, so a downloaded .swf just stops there |
TONE pitch [, seconds] | extension: a soft sine chime with a quick decay, −12 to 24; does not pause unless seconds is given |
DIM a(10), DIM b$(3,4) | arrays, elements 1..n |
DATA 1,2,"x", READ a, b$, RESTORE [n] | as on the Spectrum |
WRAP 1 / WRAP 0 | extension: with WRAP 1, PRINT moves a word that would not fit onto the next line instead of splitting it (handy for text adventures) |
SPRITE "name", "row", "row"... | defines a bitmap sprite baked into the movie: . transparent, # black, + grey, W white, 0-7 colours, a-h bright colours; repeating the name appends rows |
SPRITE n$, x, y [, xscale [, yscale [, rotation [, alpha]]]] | places a copy (top-left at x,y in screen pixels, y downwards; scale in percent, rotation in degrees, alpha in percent); SPRITE CLEAR removes them all, CLS too |
EMOJI "🙂", "name" [, size] | the browser rasterises the emoji at compile time (default 48 px) and bakes it into the movie as a true-colour alpha bitmap; place it with SPRITE "name", x, y, ..., centred on x,y so rotation spins about the middle. Plain text with no emoji in it, such as EMOJI "+10", "plus10", comes out as chunky outlined cartoon lettering, shrunk to fit: handy for titles and score pop-ups |
FX "method", args... | escape hatch to Flash: calls that MovieClip drawing-API method on the graphics layer, e.g. FX "lineStyle", 3, #ff00ff, 60, FX "moveTo", 10, 10, FX "curveTo", 128, 0, 246, 180, FX "beginFill", #0000ff, 40, FX "endFill", FX "chequer" (a ZX81 chequerboard fill). Coordinates are Flash's: pixels, origin top-left. |
FXP "property", value | sets a property of the graphics layer: FXP "_alpha", 50, FXP "_rotation", 30, FXP "_xscale", 200 |
STOP, REM | FLASH, INVERSE, OVER, POKE, RANDOMIZE, CLEAR are accepted and ignored |
Spectrum style, no brackets needed: INT x, ABS, SGN, SQR, SIN, COS, TAN, ATN, EXP, LN, RND, PI,
LEN a$, STR$ n, VAL a$, CHR$ n, CODE a$, INKEY$ (cursor keys give CHR$ 8 left, 9 right, 10 down, 11 up). String slicing: a$(2 TO 4), a$(3).
Extensions: MOUSEX, MOUSEY (Spectrum pixel coordinates), MOUSEB (1 while pressed; brief clicks and taps are reported for one executing BASIC frame), TICKS (frames since start), KEY "p" (1 while that key is held; several keys can be held at once, unlike INKEY$; KEY CHR$ 9 is cursor right).
Characters 128–143 are the block graphics; you can also paste ▘▝▀▖▌▞▛▗▚▐▜▄▙▟█ into strings. © and £ work too.
OPEN FILE takes a .bas listing or a Spectrum .tap tape image. BASIC blocks are detokenised into the
editor; CODE blocks are listed in a REM at the top. Tape, printer and stream commands (LOAD, SAVE,
LPRINT...) are accepted and skipped, PEEK/IN/ATTR/POINT return dummies, and
USR stops the program with a report, because machine code cannot run here.
DOWNLOAD .SWF saves the current movie. It is a plain Flash 8 file using only Flash 6–8 era APIs, so it should play in Adobe's original player, but it has only been tested in Ruffle. Adobe ended Flash Player at the end of 2020 and removed the downloads; the last standalone projector (32.0.0.465) survives in archives such as BlueMaxima's Flashpoint. The practical options today are the Ruffle desktop app or any page that embeds Ruffle.
Everything the BASIC can do is a compiler case in js/basic.js that emits a call into the AVM1 runtime in
js/runtime.js. RECT is twelve lines across the two. Three ways to go further, from cheap to deep:
SPRITE n, x, y backed by attachMovie),
gradients, alpha, rotation, bitmap fills, sound bank entries, a second graphics layer.js/swf.js can add tags (DefineBitsLossless for images, DefineFont for text fields,
longer DefineSounds) that BASIC statements then reference by export name.REM lines are deliberately inert; directives would be better as real keywords so the editor and the error messages know about them.
The whole program becomes one AVM1 function that is re-entered every frame (a program over about 40 KB of bytecode is split across several functions automatically; a function may hold 64 KB). Backward jumps spend a per-frame budget, so a
10 GO TO 10 loop never hangs the player: it just yields and carries on next frame. PAUSE, BEEP and
INPUT yield properly. RETURN and computed GO TO re-enter through that resume chain, so
they cost a few hundred operations each: keep GO SUBs out of per-vertex inner loops. Errors appear Spectrum-style in the lower two rows of the screen.