STASIS
Samples / WebAssembly

Play the public builds.

Activate one release Wasm package at a time. Each primary card shows the source shape, touch/pointer input, and links to the public sample plus platform packaging docs.

Primary sample / pointer + keyboard

Pointer Pong

Drag the left paddle with mouse or touch. Arrow keys are the keyboard fallback; the right paddle is deterministic.

Wasm · 1.6 KiB gzip

Not loaded.
Full Stasis source
import "/vendor/stasis/stdlib/graphics.stasis";

const SCREEN_W: f32 = 640.0;
const SCREEN_H: f32 = 360.0;
const PADDLE_W: f32 = 14.0;
const PADDLE_H: f32 = 72.0;
const BALL_SIZE: f32 = 12.0;

struct PongState {
    left_y: f32;
    right_y: f32;
    ball_x: f32;
    ball_y: f32;
    ball_dx: f32;
    ball_dy: f32;
    left_score: i32;
    right_score: i32;
}

global state: PongState;

function clamp_paddle(y: f32): f32 {
    if (y < 0.0) {
        return 0.0;
    }
    if (y > SCREEN_H - PADDLE_H) {
        return SCREEN_H - PADDLE_H;
    }
    return y;
}

function paddle_from_pointer(y: f32): f32 {
    return clamp_paddle(y - PADDLE_H / 2.0);
}

function reset_ball(direction: f32): void {
    state.ball_x = SCREEN_W / 2.0 - BALL_SIZE / 2.0;
    state.ball_y = SCREEN_H / 2.0 - BALL_SIZE / 2.0;
    state.ball_dx = direction * 4.0;
    state.ball_dy = 3.0;
}

function update_player(): void {
    if (input_pointer_count() > 0 && input_pointer_is_down(0)) {
        state.left_y = paddle_from_pointer(input_pointer_y_logical(0));
        return;
    }

    if (is_key_down(82)) {
        state.left_y = state.left_y - 5.0;
    }
    if (is_key_down(81)) {
        state.left_y = state.left_y + 5.0;
    }
    state.left_y = clamp_paddle(state.left_y);
}

function update_cpu(): void {
    let target: f32 = state.ball_y - PADDLE_H / 2.0;
    if (target > state.right_y) {
        state.right_y = state.right_y + 3.0;
    }
    if (target < state.right_y) {
        state.right_y = state.right_y - 3.0;
    }
    state.right_y = clamp_paddle(state.right_y);
}

function update_ball(): void {
    state.ball_x = state.ball_x + state.ball_dx;
    state.ball_y = state.ball_y + state.ball_dy;

    if (state.ball_y <= 0.0 || state.ball_y >= SCREEN_H - BALL_SIZE) {
        state.ball_dy = 0.0 - state.ball_dy;
    }

    if (state.ball_x <= 32.0 && state.ball_x >= 20.0 && state.ball_y + BALL_SIZE >= state.left_y && state.ball_y <= state.left_y + PADDLE_H) {
        state.ball_dx = 4.0;
    }
    if (
        state.ball_x + BALL_SIZE >= 608.0
        && state.ball_x <= 620.0
        && state.ball_y + BALL_SIZE >= state.right_y
        && state.ball_y <= state.right_y + PADDLE_H
    ) {
        state.ball_dx = -4.0;
    }

    if (state.ball_x < 0.0) {
        state.right_score = state.right_score + 1;
        reset_ball(1.0);
    }
    if (state.ball_x > SCREEN_W) {
        state.left_score = state.left_score + 1;
        reset_ball(-1.0);
    }
}

function main(): i32 {
    init_window(640, 360, "Stasis Pointer Pong");
    state.left_y = 144.0;
    state.right_y = 144.0;
    state.left_score = 0;
    state.right_score = 0;
    reset_ball(1.0);
    return 0;
}

function tick(): i32 {
    if (should_quit()) {
        return 1;
    }
    update_player();
    update_cpu();
    update_ball();
    return 0;
}

function render(): i32 {
    begin_frame();
    clear(0.02, 0.05, 0.09, 1.0);
    fill_rect(20.0, state.left_y, PADDLE_W, PADDLE_H, 0.33, 0.85, 0.98, 1.0);
    fill_rect(606.0, state.right_y, PADDLE_W, PADDLE_H, 0.98, 0.70, 0.32, 1.0);
    fill_rect(state.ball_x, state.ball_y, BALL_SIZE, BALL_SIZE, 0.96, 0.98, 1.0, 1.0);
    fill_rect(318.0, 0.0, 4.0, SCREEN_H, 0.16, 0.30, 0.40, 1.0);
    end_frame();
    return 0;
}
Primary sample / touch

Tap Target

Tap the target. A fixed sequence moves it after each hit, showing pointer edges and bounded state.

Wasm · 1.3 KiB gzip

Not loaded.
Full Stasis source
import "/vendor/stasis/stdlib/graphics.stasis";

const SCREEN_W: f32 = 360.0;
const SCREEN_H: f32 = 640.0;
const TARGET_SIZE: f32 = 72.0;

struct TargetState {
    x: f32;
    y: f32;
    taps: i32;
}

global state: TargetState;

function target_contains(x: f32, y: f32): bool {
    return x >= state.x && x <= state.x + TARGET_SIZE && y >= state.y && y <= state.y + TARGET_SIZE;
}

function move_target(): void {
    // A fixed sequence keeps every platform and test deterministic.
    if (state.taps % 3 == 0) {
        state.x = 48.0;
        state.y = 180.0;
    } else if (state.taps % 3 == 1) {
        state.x = 240.0;
        state.y = 320.0;
    } else {
        state.x = 112.0;
        state.y = 456.0;
    }
}

function main(): i32 {
    init_window(360, 640, "Stasis Tap Target");
    state.taps = 0;
    move_target();
    return 0;
}

function tick(): i32 {
    if (should_quit()) {
        return 1;
    }
    if (input_pointer_count() > 0 && input_pointer_went_down(0)) {
        if (target_contains(input_pointer_x_logical(0), input_pointer_y_logical(0))) {
            state.taps = state.taps + 1;
            move_target();
        }
    }
    return 0;
}

function render(): i32 {
    begin_frame();
    clear(0.04, 0.06, 0.10, 1.0);
    fill_rect(0.0, 0.0, SCREEN_W, SCREEN_H, 0.04, 0.06, 0.10, 1.0);
    fill_rect(state.x, state.y, TARGET_SIZE, TARGET_SIZE, 0.95, 0.25, 0.48, 1.0);
    fill_rect(state.x + 12.0, state.y + 12.0, TARGET_SIZE - 24.0, TARGET_SIZE - 24.0, 1.0, 0.82, 0.30, 1.0);
    end_frame();
    return 0;
}
Primary sample / touch

Color Switch

Tap anywhere to cycle three fixed colors. The whole game is a small explicit state transition.

Wasm · 1.2 KiB gzip

Not loaded.
Full Stasis source
import "/vendor/stasis/stdlib/graphics.stasis";

const SCREEN_W: f32 = 360.0;
const SCREEN_H: f32 = 640.0;

global color_index: i32;

function next_color(index: i32): i32 {
    if (index >= 2) {
        return 0;
    }
    return index + 1;
}

function main(): i32 {
    init_window(360, 640, "Stasis Color Switch");
    color_index = 0;
    return 0;
}

function tick(): i32 {
    if (should_quit()) {
        return 1;
    }
    if (input_pointer_count() > 0 && input_pointer_went_down(0)) {
        color_index = next_color(color_index);
    }
    return 0;
}

function render(): i32 {
    begin_frame();
    if (color_index == 0) {
        clear(0.12, 0.32, 0.88, 1.0);
    } else if (color_index == 1) {
        clear(0.10, 0.72, 0.48, 1.0);
    } else {
        clear(0.92, 0.28, 0.22, 1.0);
    }
    fill_rect(64.0, 176.0, SCREEN_W - 128.0, SCREEN_H - 352.0, 0.04, 0.05, 0.08, 0.86);
    fill_rect(116.0, 228.0, SCREEN_W - 232.0, SCREEN_H - 456.0, 0.98, 0.98, 0.92, 1.0);
    end_frame();
    return 0;
}
Stress sample / fixed state + hot swap

Swarm Field

8,192 agents update and render every tick. Drag the field with mouse or touch; tap toggles attract/repel.

Wasm · 1.9 KiB gzip · 5.2 KiB raw

The fixed Agent[8192] collection lives in bounded SoA-backed Wasm memory. Ordered deterministic ticks use no per-frame allocation; the same source runs on Web, Windows, and Android. Compatible hot swaps call on_code_swap without resetting agent state.

Not loaded.
Full Stasis source
import "/vendor/stasis/stdlib/graphics.stasis";

const SCREEN_W: f32 = 640.0;
const SCREEN_H: f32 = 360.0;
const AGENT_COUNT: i32 = 8192;
const AGENT_SIZE: f32 = 1.8;
const MAX_SPEED: f32 = 2.8;

struct Agent {
    x: f32;
    y: f32;
    vx: f32;
    vy: f32;
    band: i32;
}

// Fixed-size state: no entity allocation or iteration-count changes at runtime.
global agents: Agent[8192];
global field_x: f32;
global field_y: f32;
global field_mode: i32;
global presentation_variant: i32;

function abs_f32(value: f32): f32 {
    if (value < 0.0) {
        return 0.0 - value;
    }
    return value;
}

function clamp_speed(value: f32): f32 {
    if (value < 0.0 - MAX_SPEED) {
        return 0.0 - MAX_SPEED;
    }
    if (value > MAX_SPEED) {
        return MAX_SPEED;
    }
    return value;
}

function wrap_x(value: f32): f32 {
    if (value < 0.0) {
        return value + SCREEN_W;
    }
    if (value >= SCREEN_W) {
        return value - SCREEN_W;
    }
    return value;
}

function wrap_y(value: f32): f32 {
    if (value < 0.0) {
        return value + SCREEN_H;
    }
    if (value >= SCREEN_H) {
        return value - SCREEN_H;
    }
    return value;
}

function initialize_agents(): void {
    // 128 columns x 64 rows gives a stable, dense starting lattice.
    for (let i: i32 = 0; i < AGENT_COUNT; i += 1) {
        let column: i32 = i % 128;
        let row: i32 = i / 128;
        agents[i].x = i32_to_f32(column) * 5.0 + 2.0;
        agents[i].y = i32_to_f32(row) * 5.0 + 2.0;
        agents[i].vx = i32_to_f32((i % 5) - 2) * 0.22;
        agents[i].vy = i32_to_f32(((i / 5) % 5) - 2) * 0.18;
        agents[i].band = i % 6;
    }
}

function toggle_field_mode(): void {
    if (field_mode == 0) {
        field_mode = 1;
    } else {
        field_mode = 0;
    }
}

function update_field_from_pointer(): void {
    if (input_pointer_count() <= 0) {
        return;
    }
    field_x = input_pointer_x_logical(0);
    field_y = input_pointer_y_logical(0);
    if (input_pointer_went_down(0)) {
        toggle_field_mode();
    }
}

function update_agent(index: i32): void {
    let column: i32 = index % 128;
    let row: i32 = index / 128;
    let home_x: f32 = i32_to_f32(column) * 5.0 + 2.0;
    let home_y: f32 = i32_to_f32(row) * 5.0 + 2.0;
    let dx: f32 = field_x - agents[index].x;
    let dy: f32 = field_y - agents[index].y;
    let home_dx: f32 = home_x - agents[index].x;
    let home_dy: f32 = home_y - agents[index].y;

    // A spring keeps the lattice distributed while the pointer adds a local orbit.
    let in_core: bool = abs_f32(dx) < 70.0 && abs_f32(dy) < 70.0;
    let radial_sign: f32 = 1.0;
    if (field_mode == 1 || in_core) {
        radial_sign = -1.0;
    }
    let ax: f32 = home_dx * 0.004 + dx * 0.00055 * radial_sign - dy * 0.00035;
    let ay: f32 = home_dy * 0.004 + dy * 0.00055 * radial_sign + dx * 0.00035;
    if (ax > 0.18) {
        ax = 0.18;
    }
    if (ax < -0.18) {
        ax = -0.18;
    }
    if (ay > 0.18) {
        ay = 0.18;
    }
    if (ay < -0.18) {
        ay = -0.18;
    }
    agents[index].vx = clamp_speed(agents[index].vx * 0.992 + ax);
    agents[index].vy = clamp_speed(agents[index].vy * 0.992 + ay);
    agents[index].x = wrap_x(agents[index].x + agents[index].vx);
    agents[index].y = wrap_y(agents[index].y + agents[index].vy);
}

function update_agents(): void {
    for (let i: i32 = 0; i < AGENT_COUNT; i += 1) {
        update_agent(i);
    }
}

function main(): i32 {
    init_window(640, 360, "Stasis Swarm Field");
    field_x = SCREEN_W / 2.0;
    field_y = SCREEN_H / 2.0;
    field_mode = 0;
    presentation_variant = 0;
    initialize_agents();
    return 0;
}

function tick(): i32 {
    if (should_quit()) {
        return 1;
    }
    update_field_from_pointer();
    update_agents();
    return 0;
}

function render(): i32 {
    begin_frame();
    clear(0.015, 0.025, 0.055, 1.0);
    fill_rect(0.0, 0.0, SCREEN_W, 1.0, 0.16, 0.28, 0.42, 1.0);
    fill_rect(0.0, SCREEN_H - 1.0, SCREEN_W, 1.0, 0.16, 0.28, 0.42, 1.0);

    // One rectangle per agent: 8,192 commands, below the 10,000 command cap.
    for (let i: i32 = 0; i < AGENT_COUNT; i += 1) {
        let red: f32 = 0.22;
        let green: f32 = 0.78;
        let blue: f32 = 0.98;
        if (agents[i].band == 1) {
            red = 0.98;
            green = 0.72;
            blue = 0.28;
        } else if (agents[i].band == 2) {
            red = 0.42;
            green = 0.96;
            blue = 0.54;
        } else if (agents[i].band == 3) {
            red = 0.92;
            green = 0.38;
            blue = 0.76;
        } else if (agents[i].band == 4) {
            red = 0.98;
            green = 0.42;
            blue = 0.30;
        } else if (agents[i].band == 5) {
            red = 0.64;
            green = 0.50;
            blue = 0.98;
        }
        if (presentation_variant == 1) {
            let swap: f32 = red;
            red = blue;
            blue = swap;
        }
        fill_rect(agents[i].x, agents[i].y, AGENT_SIZE, AGENT_SIZE, red, green, blue, 0.94);
    }

    let field_red: f32 = 0.30;
    let field_blue: f32 = 0.96;
    if (field_mode == 1) {
        field_red = 0.98;
        field_blue = 0.32;
    }
    fill_rect(field_x - 8.0, field_y - 1.0, 16.0, 2.0, field_red, 0.92, field_blue, 1.0);
    fill_rect(field_x - 1.0, field_y - 8.0, 2.0, 16.0, field_red, 0.92, field_blue, 1.0);
    end_frame();
    return 0;
}

// Compatible swaps keep agents moving and only change the color presentation.
function on_code_swap(): void {
    if (presentation_variant == 0) {
        presentation_variant = 1;
    } else {
        presentation_variant = 0;
    }
}
Secondary / larger project

Rootbeer Maze 3

A larger Stasis project with keyboard navigation and visual/audio assets.

Wasm · 7.0 KiB gzip

Not loaded.

Gambit Guard

A featured external game. This site links to it without bundling its source or package.

Public sample boundary

These hosted builds include the small samples, the Swarm Field stress sample, and the secondary Rootbeer Maze build. Proprietary projects are not copied, embedded, or linked to source repositories.