acrylic-web 0.3.5

compile acrylic apps to web-assembly
Documentation
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <!-- <meta name="viewport" content="width=device-width, initial-scale=1.0" /> -->
        <link rel="icon" href="https://emojipedia-us.s3.amazonaws.com/source/skype/289/woman-cartwheeling_1f938-200d-2640-fe0f.png" type="image/x-icon">
        <title>[acrylic demo]</title>
        <style>
html, body {
    position: absolute;
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    border: 0;
    left: 0;
    top: 0;
    overflow: hidden;
    background-color: #acc;
}

canvas {
    position: absolute;
}
        </style>
    </head>
    <body onload="onLoad();">
        <canvas id="fb-canvas" style="z-index: 0;"></canvas>
        <script>
let wasm;
let app;
let textInputBuffer;
let w, h;
let pendingRequest = null;
let pendingRequestUrl;
let urlPrefix = null;
let timeout = null;
let textEncoder = new TextEncoder();
let previousFrame = performance.now();
const targetFPS = 20;
const targetFrameTime = parseInt(1000 / targetFPS);
let lastFrameTime;
let avgRustTime = 0;
let appDateOfBirth;
let assets = {};
let fb_address = 0;
let canvas;

function getString(ptr, len) {
    let mem = wasm.exports.memory;
    let slice = new Uint8Array(mem.buffer, ptr, len);
    return String.fromCharCode.apply(null, slice);
}

function raw_set_buffer_address(framebuffer) {
    fb_address = framebuffer;
}

function raw_is_request_pending() {
    return pendingRequest === null ? 0 : 1;
}

function onRequestLoad(event) {
    let len = pendingRequest.response.byteLength;
    let addr = wasm.exports.alloc_response_bytes(len);
    let mem = wasm.exports.memory;
    let dst = new Uint8Array(mem.buffer, addr, len);
    let src = new Uint8Array(pendingRequest.response);
    for (let i = 0; i < len; i++) dst[i] = src[i];
    wasm.exports.process_response(app);
    // wasm.exports.drop_response_bytes();
    pendingRequest = null;
    needsSliceRefresh = true;
    assets[pendingRequestUrl] = src;
    // todo: check for next requests
}

function raw_set_request_url_prefix(s, l) {
    urlPrefix = getString(s, l);
}

function raw_set_request_url(s, l) {
    let url = getString(s, l);
    if (url in assets) {
        console.log("using cached "  + url);
        let src = assets[url];
        let len = src.length;
        let addr = wasm.exports.alloc_response_bytes(len);
        let mem = wasm.exports.memory;
        let dst = new Uint8Array(mem.buffer, addr, len);
        for (let i = 0; i < len; i++) dst[i] = src[i];
        wasm.exports.process_response(app);
        wasm.exports.drop_response_bytes();
        pendingRequest = null;
        needsSliceRefresh = true;
        // todo: check for next requests
    } else {
        pendingRequestUrl = url;
        pendingRequest = new XMLHttpRequest();
        pendingRequest.responseType = "arraybuffer";
        pendingRequest.addEventListener("load", onRequestLoad);
        // todo: call discard_request on error
        pendingRequest.open("GET", urlPrefix + url);
        pendingRequest.send();
    }
}

function frame() {
    let printLastFrameTime = false;
    if (w != window.innerWidth || h != window.innerHeight) {
        w = window.innerWidth;
        h = window.innerHeight;
        wasm.exports.set_output_size(w, h);
        canvas.width = w;
        canvas.height = h;
        printLastFrameTime = true;
    }

    let age = performance.now() - appDateOfBirth;
    let frameStart = performance.now();
    wasm.exports.frame(app, age);
    let rustTime = (performance.now() - frameStart).toString();
    if ((rustTime * 0.9) > avgRustTime && rustTime > 3) {
        console.log("Big rust time", rustTime);
    }
    avgRustTime = (avgRustTime * 0.9) + (rustTime * 0.1);

    let len = w * h * 4;
    let mem = wasm.exports.memory;
    let slice = new Uint8ClampedArray(mem.buffer, fb_address, len);
    let ctx = canvas.getContext('2d');
    ctx.putImageData(new ImageData(slice, w, h), 0, 0);

    now = performance.now();
    lastFrameTime = now - previousFrame;
    if (printLastFrameTime) console.log(lastFrameTime);
    previousFrame = now;
    let delay = (lastFrameTime > targetFrameTime) ? 0 : (targetFrameTime - lastFrameTime);
    timeout = setTimeout(frame, delay);
}

/*setTimeout(() => {
    clearTimeout(timeout);
}, 10000);*/

const env = {
    raw_error: (s, l) => console.error(getString(s, l)),
    raw_warn: (s, l) => console.warn(getString(s, l)),
    raw_info: (s, l) => console.info(getString(s, l)),
    raw_debug: (s, l) => console.debug(getString(s, l)),
    raw_trace: (s, l) => console.trace(getString(s, l)),
    raw_set_buffer_address,
    raw_is_request_pending,
    raw_set_request_url_prefix,
    raw_set_request_url,
    now: Date.now,
};

function startWasm(file, debug) {
    let path = 'target/wasm32-unknown-unknown/';
    fetch( path + (debug ? 'debug/' : 'release/') + file + '.wasm').then(response => {
        response.arrayBuffer().then(buffer => {
            WebAssembly.compile(buffer).then(module => {
                WebAssembly.instantiate(module, { env }).then(mod => {
                    wasm = mod;
                    app = wasm.exports.init();
                    textInputBuffer = wasm.exports.get_text_input_buffer();
                    appDateOfBirth = performance.now();
                    frame();
                });
            });
        });
    });
}

function findWasm(debug) {
    fetch('Cargo.toml').then(response => {
        response.text().then(text => {
            text.split('\n').map(line => {
                let tokens = line.split('"');
                if (tokens[0] == 'name = ') {
                    let file = tokens[1].replaceAll('-', '_');
                    startWasm(file, debug);
                }
            });
        });
    });
}

function onClick(event) {
    if (wasm) {
        let x = event.clientX;
        let y = event.clientY;
        wasm.exports.quick_action(app, 1, x, y);
    }
}

function onWheel(event) {
    // wheelDelta = parseInt(event.wheelDelta / 2);
}

function onMouseMove(event) {
    if (wasm) {
        let x = event.clientX;
        let y = event.clientY;
        wasm.exports.mouse_move(app, x, y);
    }
}

function onKeyDown(event) {
    if (wasm) {
        if (event.key.length == 1) {
            let bytes = textEncoder.encode(event.key);
            if (bytes.length < 16) {
                let mem = wasm.exports.memory;
                let slice = new Uint8Array(mem.buffer, textInputBuffer, 16);
                for (let i = 0; i < bytes.length; i++) {
                    slice[i] = bytes[i];
                }
                wasm.exports.send_text_input(app, bytes.length, false);
            }
        } else if (event.key == 'Backspace') {
            wasm.exports.send_text_delete(app, -1);
        } else if (event.key == 'Delete') {
            wasm.exports.send_text_delete(app, 1);
        }/* else if (event.key.startsWith('ArrowUp')) {
            wasm.exports.send_dir_input(app, 0);
        } else if (event.key.startsWith('ArrowLeft')) {
            wasm.exports.send_dir_input(app, 1);
        } else if (event.key.startsWith('ArrowDown')) {
            wasm.exports.send_dir_input(app, 2);
        } else if (event.key.startsWith('ArrowRight')) {
            wasm.exports.send_dir_input(app, 3);
        }*/
    }
}

function onLoad() {
    document.body.addEventListener('click', onClick);
    document.body.addEventListener('wheel', onWheel);
    document.body.addEventListener('mousemove', onMouseMove);
    document.body.addEventListener('keydown', onKeyDown);
    canvas = document.getElementById('fb-canvas');
    let hash = document.location.hash;
    let debug;
    if (hash == '#debug') {
        debug = true;
    } else if (hash == '#release') {
        debug = false;
    } else {
        if (confirm('Use release build?')) {
            document.location = '#release';
            debug = false;
        } else if (confirm('Use debug build?')) {
            document.location = '#debug';
            debug = true;
        } else {
            document.location = '';
        }
    }
    findWasm(debug);
}

// setTimeout(() => clearTimeout(timeout), 10000);
        </script>
    </body>
</html>