The engine now runs on a plain Web Worker (no SDL, no PROXY_TO_PTHREAD: in a pthread Emscripten proxies every filesystem syscall to the main browser thread, where synchronous XHR - and thus FS.createLazyFile - is forbidden). A page/worker pair transfers an OffscreenCanvas and forwards DOM input; assets load lazily from an embedded manifest, so entering a map fetches only that map and its tiles. Presentation: ra3.webgpu (WebGPU via Emscripten's emdawnwebgpu port, WGSL shaders, the default on wasm) and ra3.wasmgl (WebGL2, GLSL ES). The legacy SDL ra3.webgl backend is removed.
102 lines
4.7 KiB
HTML
102 lines
4.7 KiB
HTML
<!doctype html>
|
|
<!-- OpenRA3 web page. The engine runs inside openra3.worker.js: the page owns the
|
|
DOM (input, resize) and transfers #canvas to the worker as an OffscreenCanvas,
|
|
so the engine never blocks the main thread. -->
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
|
<title>OpenRA3</title>
|
|
<style>
|
|
html, body { margin: 0; height: 100%; background: #1a0a08; overflow: hidden; }
|
|
#canvas { display: block; width: 100vw; height: 100vh; outline: none; cursor: default; }
|
|
#loading { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
|
|
color: #ecbe50; font: 16px/1.4 monospace; pointer-events: none; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<canvas id="canvas" tabindex="-1" oncontextmenu="event.preventDefault()"></canvas>
|
|
<div id="loading">Loading OpenRA3…</div>
|
|
<script>
|
|
(function () {
|
|
var canvas = document.getElementById('canvas');
|
|
var loading = document.getElementById('loading');
|
|
// `?nocache` busts the worker, module and wasm fetches (development),
|
|
// so a rebuild is picked up without clearing the browser cache.
|
|
var bust = location.search.indexOf('nocache') >= 0 ? Date.now() : 0;
|
|
var worker = new Worker('openra3.worker.js' + (bust ? '?v=' + bust : ''));
|
|
|
|
var ready = false;
|
|
var pending = []; // input queued until the runtime is up
|
|
|
|
function post(message) {
|
|
if (ready) {
|
|
worker.postMessage(message);
|
|
} else {
|
|
pending.push(message);
|
|
}
|
|
}
|
|
|
|
function backingSize() {
|
|
var dpr = window.devicePixelRatio || 1;
|
|
return {
|
|
width: Math.max(1, Math.round(window.innerWidth * dpr)),
|
|
height: Math.max(1, Math.round(window.innerHeight * dpr)),
|
|
};
|
|
}
|
|
|
|
worker.onmessage = function (event) {
|
|
var data = event.data || {};
|
|
if (data.log) { console.log(data.log); return; }
|
|
if (data.ready) {
|
|
ready = true;
|
|
if (loading) loading.remove();
|
|
for (var i = 0; i < pending.length; i++) worker.postMessage(pending[i]);
|
|
pending = [];
|
|
return;
|
|
}
|
|
if (typeof data.openra3Frames === 'number') { window.openra3Frames = data.openra3Frames; }
|
|
};
|
|
|
|
// Hand the canvas to the worker as an OffscreenCanvas. After this the
|
|
// page can no longer size or draw it; the worker owns it.
|
|
var size = backingSize();
|
|
var offscreen = canvas.transferControlToOffscreen();
|
|
worker.postMessage({ canvas: offscreen, width: size.width, height: size.height, bust: bust }, [offscreen]);
|
|
|
|
// ---- input: the page owns the DOM, the worker owns the game --------
|
|
window.addEventListener('keydown', function (event) {
|
|
post({ type: 'key', code: event.code, down: true });
|
|
if (event.code === 'Tab' || event.code === 'Space' || event.code.indexOf('Arrow') === 0) event.preventDefault();
|
|
}, false);
|
|
window.addEventListener('keyup', function (event) {
|
|
post({ type: 'key', code: event.code, down: false });
|
|
}, false);
|
|
window.addEventListener('keypress', function (event) {
|
|
if (event.charCode) post({ type: 'text', char: event.charCode });
|
|
}, false);
|
|
|
|
canvas.addEventListener('mousedown', function (event) {
|
|
post({ type: 'mousebutton', x: event.clientX, y: event.clientY, left: event.button === 0 });
|
|
}, false);
|
|
window.addEventListener('mousemove', function (event) {
|
|
post({ type: 'mousemove', x: event.clientX, y: event.clientY, dx: event.movementX, dy: event.movementY,
|
|
left: (event.buttons & 1) !== 0 });
|
|
}, false);
|
|
window.addEventListener('wheel', function (event) {
|
|
post({ type: 'wheel', deltaY: event.deltaY });
|
|
}, { passive: true });
|
|
window.addEventListener('contextmenu', function (event) { event.preventDefault(); });
|
|
|
|
window.addEventListener('resize', function () {
|
|
var next = backingSize();
|
|
post({ type: 'resize', width: next.width, height: next.height });
|
|
});
|
|
window.addEventListener('beforeunload', function () {
|
|
if (ready) worker.postMessage({ type: 'quit' });
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|