(, SPy — Canvas Demo

SPy Demo

Tiny SPy/WASM demo.

Parameters

255
255
0

The SPy source code below is what drives the animation above. It is compiled to WebAssembly by the SPy compiler and runs directly in your browser — no Python runtime, no PyScript, no Pyodide.

demo_image_data.spySPy
"""
Note that closures are not yet supported in red function so we have to use few global
variables.

However, module level code is quite particular in SPy:

- It is evaluated at import time (i.e. during compilation).
- Global variables are by default constant and a keyword `var` is used to indicate that they
  are real variables.
- Moreover, currently `var` global variables can be only simple types and NULL pointers.

jsffi is also currently very limited. In particular, there is no equivalent to the
`new` JS keyword, so we need to rely on function helpers (like js_u8array_from_ptr
and js_new_ImageData).
"""

from time import time

import jsffi
from jsffi import JsRef, js_new_ImageData, js_u8array_from_ptr
from unsafe import gc_alloc, gc_ptr

WIDTH: i32 = 800
HEIGHT: i32 = 600
SIZE: i32 = WIDTH * HEIGHT * 4


@struct
class Params:
    red: i32
    green: i32
    blue: i32


var counter: i32 = 0
var inc: i32 = 5
var BUF: gc_ptr[u8] = gc_ptr[u8].NULL
var PARAMS:  gc_ptr[Params] = gc_ptr[Params].NULL
var n_jsrefs: i32 = 0

var JS_REFS: gc_ptr[JsRef] = gc_ptr[JsRef].NULL
CTX_IDX: i32 = 0
IMAGE_DATA_IDX: i32 = 1


def get_ctx() -> JsRef:
    return JS_REFS[CTX_IDX]


def get_image_data() -> JsRef:
    return JS_REFS[IMAGE_DATA_IDX]


def sync_slider_callback[id]() -> None:
    document = jsffi.get_Document()
    span = document.getElementById(id + "Val")
    elem = document.getElementById(id)
    value = elem.value
    span.textContent = value
    setattr(PARAMS[0], id, value)

    # SPy has currently no destructor
    jsffi.drop_ref(span)
    jsffi.drop_ref(elem)
    jsffi.drop_ref(value)


def simulate_step() -> None:
    if inc > 0 and counter + inc > 255:
        inc *= -1
    elif inc < 0 and counter + inc < 0:
        inc *= -1
    counter = counter + inc

    params = PARAMS[0]

    # improve perf when compiling without --release
    _red: u8 = (params.red * counter) // 255
    _green: u8 = (params.green * counter) // 255
    _blue: u8 = (params.blue * counter) // 255

    for idx in range(WIDTH * HEIGHT):
        BUF[idx * 4 + 0] = _red
        BUF[idx * 4 + 1] = _green
        BUF[idx * 4 + 2] = _blue
        BUF[idx * 4 + 3] = counter


def frame(timestamp: f64) -> None:
    console = jsffi.get_Console()

    t_start = time()
    simulate_step()
    console.log("simulate_step done in", time() - t_start, "s")

    get_ctx().putImageData(get_image_data(), 0, 0)

    n_tmp = jsffi._debug_n_jsrefs()
    n_per_frame = n_tmp - n_jsrefs
    n_jsrefs = n_tmp

    console.log("n_per_frame:", n_per_frame)

    jsffi.request_animation_frame(frame)
    # this is equivalent to
    # jsffi.drop_ref(jsffi.get_GlobalThis().requestAnimationFrame(frame))


def main() -> None:
    jsffi.init()
    BUF = gc_alloc[u8](SIZE)
    for idx in range(SIZE):
        BUF[idx] = 255

    PARAMS = gc_alloc[Params](1)

    document = jsffi.get_Document()

    # will use: for color in unroll(["red", "green", "blue"]):
    elem = document.getElementById("red")
    setattr(PARAMS[0], "red", elem.value)
    elem.addEventListener("input", sync_slider_callback["red"])

    elem = document.getElementById("green")
    setattr(PARAMS[0], "green", elem.value)
    elem.addEventListener("input", sync_slider_callback["green"])

    elem = document.getElementById("blue")
    setattr(PARAMS[0], "blue", elem.value)
    elem.addEventListener("input", sync_slider_callback["blue"])

    u8arr = js_u8array_from_ptr(BUF, SIZE)
    image_data = js_new_ImageData(u8arr, WIDTH, HEIGHT)

    canvas = document.getElementById("canvas")
    ctx = canvas.getContext("2d")

    JS_REFS = gc_alloc[JsRef](2)
    JS_REFS[CTX_IDX] = ctx
    JS_REFS[IMAGE_DATA_IDX] = image_data

    jsffi.request_animation_frame(frame)
)