Skip to main content

API

Alpha Version: This is the v2 engine documentation. Features and APIs may change.

Most functions in Floaty closely resemble Pico-8 functions. The main difference is that you access them through the scope() method instead of having them globally available. This works better for JavaScript running in a web browser.

Core

scope(callback)

Provides access to all engine functions through a closure. This is how you start using the engine:

engine.scope(({ start, cls, spr }) => {
    start({
        target: document.querySelector('.game'),
        draw() {
            cls();
            spr('player', 10, 10);
        },
    });
});

start({ ... })

Starts the game loop. Pass an object with the following properties:

  • target - The DOM element to render into
  • sprites - Object containing sprite definitions
  • sounds - Object containing sound definitions
  • init - Function called once when game starts
  • update(time, frame) - Function called each game tick with the current timestamp and frame count
  • draw(time, frame) - Function called each frame to render with the current timestamp and frame count
  • shutdown - Function called when the engine stops (optional)
  • background - CSS color for area around canvas (optional)

Here's a complete example showing the game lifecycle:

engine.scope(({ start, cls, circfill }) => {
    let x = 20;
    let y = 20;
    let dx = 1;
    let dy = 1;

    const sprites = {};
    const sounds = {};

    function init() {
        x = 20;
        y = 20;
    }

    function update() {
        x += dx;
        y += dy;

        if (x <= 4 || x >= 124) {
            dx *= -1;
        }

        if (y <= 4 || y >= 124) {
            dy *= -1;
        }
    }

    function draw() {
        cls(1);
        circfill(x, y, 4, 8);
    }

    start({
        sprites,
        sounds,
        init,
        update,
        draw,
        background: '#1D2B53',
    });
});

fps(callback)

Registers a callback that receives the current frames per second each frame. Use this to build an FPS counter or performance monitor.

engine.scope(({ start, cls, text, fps }) => {
    let currentFps = 0;

    fps((value) => {
        currentFps = value;
    });

    start({
        target: document.querySelector('.game'),
        sprites: {},
        sounds: {},
        draw() {
            cls();
            text(`${currentFps} fps`, 1, 1, 7);
        },
    });
});

time + frame

Both update and draw receive (time, frame) as arguments. time is the elapsed timestamp in milliseconds. frame is the total number of game ticks since the game started. Use frame for deterministic timing (e.g. animation cycles) and time for smooth real-time effects.

engine.scope(({ start, cls, circfill }) => {
    const colors = [8, 9, 10, 11, 12];

    start({
        target: document.querySelector('.game'),
        sprites: {},
        sounds: {},
        draw(time, frame) {
            cls();

            // cycle through colors every 15 frames
            const colorIndex = Math.floor(frame / 15) % colors.length;
            circfill(64, 64, 16, colors[colorIndex]);
        },
    });
});

Drawing

cls(color = 0)

Clear the screen with the specified color (0-15). Most commonly used at the beginning of the draw function.

pset(x, y, color) + pget(x, y)

pset sets a pixel at coordinates (x, y) to the specified color (0-15). pget reads the color back. Both are adjusted by the current camera offset (see Camera section), so pget(x, y) reads back what pset(x, y) wrote.

// inside draw()
for (let x = 0; x < 128; x++) {
    for (let y = 0; y < 64; y++) {
        pset(x, y, Math.floor(x / 8) % 16);
    }
}

// read back a pixel color
const color = pget(8, 8);
text(`color: ${color}`, 2, 80, 7);

spr(key, x, y, w = 1, h = 1, flipX = false, flipY = false)

Draw a sprite at the given position. Use w and h to draw larger sprites (multiples of 8x8). Use flipX and flipY to mirror the sprite.

const sprites = {
    // prettier-ignore
    arrow: [
        -1, -1, -1,  7,  7, -1, -1, -1,
        -1, -1,  7,  7,  7, -1, -1, -1,
        -1,  7, -1,  7,  7, -1, -1, -1,
         7, -1, -1,  7,  7,  7,  7,  7,
         7, -1, -1,  7,  7,  7,  7,  7,
        -1,  7, -1,  7,  7, -1, -1, -1,
        -1, -1,  7,  7,  7, -1, -1, -1,
        -1, -1, -1,  7,  7, -1, -1, -1,
    ],
};

// inside draw()
spr('arrow', 20, 60);
spr('arrow', 40, 60, 1, 1, true); // flipped horizontally

line(x1, y1, x2, y2, color = 7)

Draw a line from (x1, y1) to (x2, y2) in the specified color.

rect(x1, y1, x2, y2, color = 7)

Draw an unfilled rectangle from (x1, y1) to (x2, y2) in the specified color.

rectfill(x1, y1, x2, y2, color = 7)

Draw a filled rectangle from (x1, y1) to (x2, y2) in the specified color.

circ(x, y, r, color = 7, thickness = 1)

Draw an unfilled circle centered at (x, y) with radius r. Use thickness to draw a thicker ring.

circfill(x, y, r, color = 7)

Draw a filled circle centered at (x, y) with radius r.

text(str, x, y, color = 7)

Draw text at position (x, y) in the specified color.

const message = 'game over';
const size = textSize(message);
const x = Math.floor((128 - size.x) / 2);
const y = Math.floor((128 - size.y) / 2);

// draw a background panel behind the text
rectfill(x - 4, y - 4, x + size.x + 4, y + size.y + 4, 2);
text(message, x, y, 7);

textSize(str)

Get the dimensions of text. Returns an object with x (width) and y (height) properties.

textPixels(str, x, y, color = 7)

Get the pixel data for text at position (x, y) in the specified color. Returns an array of [x, y, color] coordinates.

Input

btn(key)

Check if a button is currently being held down. Returns true/false. Accepts any KeyboardEvent.key value (e.g. 'ArrowUp', 'ArrowLeft', 'z', 'x'). Call without arguments to detect any key being held.

btnp(key)

Check if a button was just pressed (not held). Returns true only on the first frame the key is pressed. Use without arguments to detect any key press.

// inside update()

// btn() is true every frame while held
if (btn('ArrowUp')) {
    y -= 1;
}
if (btn('ArrowDown')) {
    y += 1;
}
if (btn('ArrowLeft')) {
    x -= 1;
}
if (btn('ArrowRight')) {
    x += 1;
}

// btnp() is true only on the first frame
if (btnp('z')) {
    dash();
}

mouse()

Get the current mouse position on the canvas. Returns an object with x and y properties (in canvas coordinates, not screen coordinates).

mdown()

Check if the mouse button is currently being held down. Returns true/false.

mup()

Check if the mouse button is currently released. Returns true/false.

click()

Check if the mouse was clicked this frame. Returns true/false. Note: this is consumed on read — call it once per frame and store the result if you need it in multiple places.

dragging()

Check if the mouse is being dragged (button held while moving). Returns true/false.

// inside update()
const pos = mouse();

if (click()) {
    pixels.push(pos);
}

if (dragging()) {
    pixels.push(pos);
}

cursor(type = 'default')

Control the browser cursor over the game canvas. Call cursor(false) to hide it, cursor() to restore the default, or pass any CSS cursor value (e.g. 'pointer', 'crosshair').

// inside init()
cursor(false);

// inside draw()
const pos = mouse();
circfill(pos.x, pos.y, 1, 7);

// to restore the browser cursor
cursor(); // default cursor
cursor('pointer'); // or any CSS cursor value

Sound

sfx(key, volume = 1.0)

Play a sound effect by its key in the sounds object. Optionally pass a volume multiplier (0-1). The sound data format determines how it plays:

  • Synthesized: [length, [freq, vol, wave], ...]
  • Audio file path: '/sounds/jump.wav'
  • Data URI: 'data:audio/wav;base64,...'

Here's an example using the synthesized format:

const sounds = {
    jump: [4, [40, 0.8, 0], [50, 0.6, 0], [60, 0.4, 0]],
    coin: [3, [55, 0.7, 1], [60, 0.7, 1], [64, 0.5, 1]],
};

// inside update()
if (btnp('z')) {
    sfx('jump');
}

music(source, options = {})

Play background music. source can be a key from sounds, a URL, or a data URI. Pass false to stop playback. Options: loop (default: true), volume (default: 0.5, range 0-1). Automatically stops any currently playing music.

const sounds = {
    bgm: '/audio/background.mp3',
};

// start looping background music
music('bgm', { loop: true, volume: 0.5 });

// stop it later
music(false);

Assets

queue(url)

Queue a URL for deferred asset loading. Returns a placeholder that load() will resolve later. Use in sprite and sound definitions instead of inline data. For JSON URLs, the fetched data replaces the placeholder. For audio URLs (wav/mp3/ogg), the URL string is stored so the engine can decode it at playback time.

load()

Fetch all queued assets in parallel and replace placeholders with resolved data. Call with await inside an async init() function. This lets you control the loading UX — show a loading screen, animate progress, then transition to gameplay.

const sprites = {
    player: queue('https://example.com/player.json'),
};

const sounds = {
    laser: queue('https://example.com/laser.json'),
    music: queue('https://example.com/track.mp3'),
};

async function init() {
    await load();
}

Sprites

fget(index, flag) + fset(index, flag, value)

fset marks a sprite with a flag (0-7). fget reads the flag back. Use flags to tag sprites with properties like "solid" or "dangerous". Sprites must include a flags array as the second element: [pixels, [f0, f1, ..., f7]].

// sprites with a flags array as the second element
const sprites = {
    // prettier-ignore
    brick: [[
        5, 5, 5, 5, 5, 5, 5, 5,
        5, 4, 4, 4, 4, 4, 4, 5,
        5, 4, 4, 4, 4, 4, 4, 5,
        5, 5, 5, 5, 5, 5, 5, 5,
        5, 5, 5, 5, 5, 5, 5, 5,
        4, 4, 4, 5, 5, 4, 4, 4,
        4, 4, 4, 5, 5, 4, 4, 4,
        5, 5, 5, 5, 5, 5, 5, 5,
    ], [0, 0, 0, 0, 0, 0, 0, 0]],
};

// inside init() — mark brick as solid
fset('brick', 0, true);

// inside update() — check if tile is solid
if (fget('brick', 0)) {
    // this tile is solid
}

pal(from, to)

Swap palette colors. Calling pal(0, 8) will replace all instances of color 0 with color 8. Call pal() with no arguments to reset the palette to default.

// draw with original colors
spr('character', 20, 60);

// red team: swap blue (12) for red (8)
pal(12, 8);
spr('character', 56, 60);
pal(); // reset palette

// green team: swap blue (12) for green (11)
pal(12, 11);
spr('character', 92, 60);
pal(); // reset palette

Collision

circlesCollide(a, b)

Check if two circles are overlapping. Each circle is an array: [x, y, radius]. Returns true/false.

// each circle is [x, y, radius]
const a = [playerX, playerY, 10];
const b = [enemyX, enemyY, 10];

if (circlesCollide(a, b)) {
    // handle collision
}

boxesCollide(a, b)

Check if two rectangles are overlapping. Each box is an array: [x, y, width, height]. Returns true/false.

// each box is [x, y, width, height]
const player = [playerX, playerY, 16, 16];
const wall = [80, 30, 8, 68];

if (boxesCollide(player, wall)) {
    // handle collision
}

Random

rnd([limit])

Generate a random number between 0 and limit. Like Pico-8's rnd() function.

randomFloat()

Generate a random float between 0 and 1.

randomIntegerBetween(min, max)

Generate a random integer between min and max (inclusive).

randomFloatBetween(min, max)

Generate a random float between min and max.

// inside init()
for (let i = 0; i < 80; i++) {
    stars.push({
        x: randomIntegerBetween(0, 127),
        y: randomIntegerBetween(0, 127),
        speed: randomFloatBetween(0.2, 1.5),
        brightness: rnd() > 0.5 ? 7 : 6,
    });
}

// inside draw()
for (const star of stars) {
    star.y = (star.y + star.speed) % 128;
    const color = randomFloat() > 0.9 ? 5 : star.brightness;
    pset(star.x, star.y, color);
}

Camera

camera(x = 0, y = 0)

Set the camera offset. All subsequent drawing operations will be shifted by (-x, -y). Use this to scroll the view so it follows a player or pans across a level.

cget()

Get the current camera position. Returns an object with x and y properties. Useful for positioning HUD elements relative to the camera.

creset()

Reset the camera to (0, 0). Call this before drawing screen-fixed UI like health bars or score text.

// inside update()
camera(playerX - 64, playerY - 64);

// inside draw()
// world draws offset by camera
spr('player', playerX, playerY);

// read camera to position HUD elements
const cam = cget();
text(`HP: ${hp}`, cam.x + 2, cam.y + 2, 7);

// reset for screen-fixed UI
creset();
text('PAUSED', 48, 60, 8);

Tilemap

mset(x, y, tile, layer = 0, mapId = 'default')

Set a tile at grid position (x, y) on the specified layer. tile is a sprite key matching a name in your sprites object. Use layer to separate background and foreground tiles. Use mapId to manage multiple maps.

mget(x, y, layer = 0, mapId = 'default')

Get the tile at grid position (x, y). Returns 0 if no tile is set.

map(layer = 0, cellX = 0, cellY = 0, cellW, cellH, screenX = 0, screenY = 0, mapId = 'default')

Render all tiles from the given layer by calling spr() for each non-zero tile. Optionally specify a region of the tilemap to draw with cellX, cellY, cellW, cellH, and where on screen to draw it with screenX, screenY.

mclear(layer, mapId)

Clear tiles. With no arguments, clears everything. With layer, clears that layer across all maps. With both layer and mapId, clears a specific layer of a specific map.

// inside init()
for (let x = 0; x < 16; x++) {
    mset(x, 15, 'brick');
    mset(x, 14, 'grass');
}

// inside update()
const tileBelow = mget(
    Math.floor(playerX / 8),
    Math.floor(playerY / 8) + 1,
);

if (tileBelow === 'brick') {
    // standing on solid ground
}

// inside draw()
map(0);

// reset a layer
mclear(0);