(,
Tiny SPy/WASM demo.
Parameters
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.
"""
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)