2026-03-20--Umfeld-x-WebAssembly

while listening to an interesting interview about The Future of Processing – with Raphaël and Stef by tim rodenbröker, i heard raphaël ask “we are looking at ways that Processing can get back into the browser … in different ways …”

well, here i present one way of doing it with WebAssembly ( Wasm ). i remembered that i read somewhere that SDL3 ( <- which a lot of Umfeld is based upon ) support this via Emscripten ( a toolchain that compiles C/C++ to Wasm ).

click to run an example♥ right here the browser.

since i had already implemented an OpenGL ES 3.0 renderer it was VERY easy to get the workflow up and running. i plan to test it a bit before merging it into the main branch.

if you are interested in testing right away check out the development branch dev-web-assembly,; only prerequisit is the installation of Emscripten SDK ( on macOS with brew install emscripten or manually from https://emscripten.org/docs/getting_started/downloads.html ).

compilation is rather easy, but here is a convenient shell script and a base HTML container; both should live next to the CMake script:

.
├── application.cpp
├── build-wasm.sh
├── CMakeLists.txt
├── data
│   └── ...
└── shell.html

shell script build-wasm.sh:

#!/usr/bin/env bash
#
# build script for Umfeld WebAssembly application
#
# usage:
#   ./build-wasm.sh          # configure + build
#   ./build-wasm.sh clean    # remove build directory
#   ./build-wasm.sh serve    # start a local web server on port 8000
#
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUILD_DIR="${SCRIPT_DIR}/build-wasm"

# -------------------------------------------------------------------------
# Locate Emscripten toolchain
# -------------------------------------------------------------------------

# Option 1: emsdk-based (EMSDK env var set by `source emsdk_env.sh`)
# Option 2: homebrew-installed emscripten
if command -v emcmake &>/dev/null; then
    echo "✓ Found emcmake: $(which emcmake)"
elif [ -n "${EMSDK:-}" ]; then
    echo "Sourcing emsdk_env.sh from EMSDK=$EMSDK"
    source "$EMSDK/emsdk_env.sh"
else
    echo "ERROR: Emscripten not found."
    echo "install via homebrew:  brew install emscripten"
    echo "or install emsdk:      https://emscripten.org/docs/getting_started/downloads.html"
    exit 1
fi

# -------------------------------------------------------------------------
# sub-commands
# -------------------------------------------------------------------------

case "${1:-build}" in
    clean)
        echo "Removing $BUILD_DIR ..."
        rm -rf "$BUILD_DIR"
        echo "Done."
        exit 0
        ;;
    serve)
        if [ ! -d "$BUILD_DIR" ]; then
            echo "ERROR: build directory not found. run './build-wasm.sh' first."
            exit 1
        fi
        echo "starting web server at http://localhost:8000"
        echo "open http://localhost:8000/web-assembly.html in your browser."
        cd "$BUILD_DIR"
        python3 -m http.server 8000
        exit 0
        ;;
    build)
        ;;
    *)
        echo "Usage: $0 [build|clean|serve]"
        exit 1
        ;;
esac

# -------------------------------------------------------------------------
# Configure + Build
# -------------------------------------------------------------------------

echo "configuring WebAssembly build ..."
emcmake cmake -B "$BUILD_DIR" -S "$SCRIPT_DIR" \
    -DCMAKE_BUILD_TYPE=Release

echo ""
echo "building ..."
cmake --build "$BUILD_DIR" --parallel

echo ""
echo "============================================="
echo "  build complete!"
echo "  output: $BUILD_DIR/web-assembly.html"
echo ""
echo "  test locally with:"
echo "    ./build-wasm.sh serve"
echo "    open http://localhost:8000/web-assembly.html"
echo "============================================="

HTML file shell.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Umfeld</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            background: #202020;
            color: #e0e0e0;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
        }
        h1 { margin-bottom: 0.5em; font-size: 1.2em; color: #a0a0a0; }
        #canvas {
            border: 1px solid #333;
            cursor: crosshair;
        }
        #status {
            margin-top: 0.5em;
            font-size: 0.85em;
            color: #888;
        }
        #toggle-console {
            margin-top: 0.5em;
            padding: 0.25em 1em;
            background: #222;
            color: #888;
            border: 1px solid #444;
            cursor: pointer;
            font-size: 0.75em;
            font-family: monospace;
            border-radius: 3px;
        }
        #toggle-console:hover { color: #ccc; border-color: #666; }
        #output {
            width: 1024px;
            max-height: 200px;
            overflow-y: auto;
            margin-top: 0.5em;
            padding: 0.5em;
            background: #111;
            font-family: "SF Mono", "Menlo", "Monaco", "Consolas", monospace;
            font-size: 0.75em;
            line-height: 1.4;
            border: 1px solid #333;
            border-radius: 3px;
            display: none;
        }
        #output .msg-error   { color: #ff6b6b; }
        #output .msg-warning { color: #ffa726; }
        #output .msg-console { color: #66bb6a; }
        #output .msg-info    { color: #aaa; }
    </style>
</head>
<body>
    <h1>Umfeld / WebAssembly</h1>
    <canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas>
    <div id="status">Loading…</div>
    <button id="toggle-console">Show Console</button>
    <pre id="output"></pre>
    <script>
        var outputEl = document.getElementById('output');
        var toggleBtn = document.getElementById('toggle-console');

        function appendMessage(text, className) {
            var span = document.createElement('span');
            span.className = className;
            span.textContent = text + '\n';
            outputEl.appendChild(span);
            outputEl.scrollTop = outputEl.scrollHeight;
        }

        function classifyMessage(text) {
            if (text.indexOf('ERROR') !== -1) return 'msg-error';
            if (text.indexOf('WARNING') !== -1) return 'msg-warning';
            if (text.indexOf('CONSOLE') !== -1) return 'msg-console';
            return 'msg-info';
        }

        toggleBtn.addEventListener('click', function() {
            var hidden = outputEl.style.display === 'none';
            outputEl.style.display = hidden ? 'block' : 'none';
            toggleBtn.textContent = hidden ? 'Hide Console' : 'Show Console';
        });

        var Module = {
            canvas: (function() { return document.getElementById('canvas'); })(),
            setStatus: function(text) {
                document.getElementById('status').textContent = text;
            },
            print: function(text) {
                console.log(text);
                appendMessage(text, classifyMessage(text));
            },
            printErr: function(text) {
                console.warn(text);
                appendMessage(text, classifyMessage(text));
            },
        };
    </script>
    {{{ SCRIPT }}}
</body>
</html>