Skip to content

NX.js compatibility: run melonJS without a DOM (experimental) - #1854

Merged
obiot merged 7 commits into
masterfrom
claude/serene-noether-1lvk7v
Nov 12, 2026
Merged

obiot merged 7 commits into
masterfrom
claude/serene-noether-1lvk7v

Conversation

@obiot

@obiot obiot commented Nov 12, 2026

Copy link
Copy Markdown
Member

Description

melonJS can now run where there is no document: on nx.js (JavaScript homebrew for the Nintendo Switch) and, for rendering and loading, in a Web Worker. The game hands the Application the canvas to draw to:

const app = new Application(1280, 720, {
    canvas: screen, // nx.js's display; an OffscreenCanvas in a worker
    renderer: video.AUTO,
    scale: 1,
});
await app.init();

Browsers are unchanged. Every new path is gated on document being missing (an internal hasDOM() in src/system/dom.ts), or on an API being missing. The few lines a page still runs give the same result as before, such as the OffscreenCanvas check, now done with a 1×1 canvas instead of 0×0.

Engine changes (packages/melonjs/src)

  • Application: with no DOM, there is no parent element to mount the canvas in, measure or observe. app.parentElement is undefined there and keeps its HTMLElement type, so existing TypeScript games don't break. scale: "auto" draws at the given size.
  • Canvas style: a canvas with no style (nx.js's screen) is accepted wherever the engine sets CSS on it: display, CSS size and touch-action.
  • WebGL check: with no DOM, it reports whether WebGL 2 exists rather than probing a <canvas> it cannot create. Creating the renderer decides, and video.AUTO still falls back to Canvas.
  • Spare canvases: the canvas factory returns an OffscreenCanvas with no DOM, and gives a clear error when there is neither a DOM nor an OffscreenCanvas. The OffscreenCanvas check now uses a 1×1 canvas, because nx.js rejects 0×0.
  • Touch: with no DOM, a global TouchEvent counts as touch support, and touches use client coordinates. nx.js touches have no pageX/Y.
  • Audio: unlocks at once with no DOM. When Audio has no canPlayType (nx.js), every format is assumed playable and decoding reports the ones that fail.
  • Screen orientation: screen.orientation is read inside a try/catch; nx.js's getter throws, which broke import "melonjs".
  • Tiled: an XML map where there is no DOMParser fails with an error saying to export it as JSON (.tmj/.tsj). The loader keeps the parser's error as the cause of its "Failed loading resource" error, in both preload() and reload().

Tests

  • tests/nxjs-compatibility.spec.js: runs a game in a Web Worker made to look like nx.js, using nx.js's own source as the reference: a style-less screen whose orientation throws, OffscreenCanvas that is 2D-only and rejects 0×0, touches without pageX, a Web Audio stand-in, an Audio without canPlayType, and no XHR, DOMParser or WebGPU. With the Canvas, WebGL and AUTO renderers it checks:
    • startup;
    • loading images and a JSON map, and the drawn pixels: tiles, a tile cut from a larger image, and text;
    • touch;
    • sound;
    • the XML-map error;
    • the DOM helpers with no DOM;
    • the error when there is no OffscreenCanvas.
  • tests/nxjs-browser-parity.spec.js: in a real page, checks each capability touched against the formula it was computed with before (touchEvent, pointerEvent, touch, maxTouchPoints, screenOrientation, offscreenCanvas, WebGL support, audio formats). It also checks the DOM helpers, and that an Application still mounts in the page with the same canvas styling, coordinate mapping and XML map parsing.
  • Mutation checks: reverting each fix, or removing a browser-side guard, makes its test fail.

Docs

  • Skills: a new "Runtimes with no DOM: nx.js, Web Workers" section in melonjs-deployment, with setup, what differs and symptom rows. Notes in melonjs-tilemaps (use JSON maps), melonjs-audio (no gesture needed; nodes nx.js lacks) and melonjs-input, plus the router entry.
  • README: a short "Running without a DOM (experimental)" section.
  • Changelog: entries under 20.14.0.

Limits

  • Experimental: tested against a simulation of nx.js, not yet on Switch hardware. Text over WebGL with no DOM now goes through OffscreenCanvas, which melonJS avoids in browsers (textImage2D does not accept OffscreenCanvas anymore as a pixel source for texture #1180); it renders correctly in the Chromium worker but should be checked on a Switch.
  • Things that need nx.js changes:
    • spatial audio, tone()/noise() and the audio effects, whose nodes nx.js doesn't implement;
    • fontface assets, because nx.js's FontFace takes bytes, not a URL. The skill shows how to load the bytes and add the face by hand.
  • Web Worker: gets no input events or AudioContext of its own, so those have to come from the page.
  • Node.js: import "melonjs" works there, but it can't run a game.

Type of change

  • Bug fix
  • New feature
  • Documentation update
  • Performance improvement
  • Refactoring (no functional changes)

Checklist

  • I have read the Contributing Guide
  • My code follows the existing code style (pnpm lint passes): run for the melonjs package (ESLint over src and tests, Biome, tsc), with no new warnings on changed lines; no other package changed
  • I have tested my changes locally (pnpm test passes): the full engine suite (452 files, 10,810 tests) passed before the parity spec was added; the parity and nx.js specs (31 tests) pass together
  • I have added tests that cover my changes (if applicable)
  • The build succeeds (pnpm build): the melonjs package build

Related issues

None.


Generated by Claude Code

melonJS can now run where there is no `document`: on nx.js (JavaScript
homebrew for the Nintendo Switch) and in a Web Worker. The game hands the
Application the canvas to draw to (`settings.canvas`, `screen` on nx.js).
Browsers behave as before: every new path is taken only when
`globalThis.document` is missing.

- Application: no parent element to mount the canvas in, measure or
  observe; `parentElement` is undefined there, and `scale: "auto"` draws
  at the given size
- a canvas with no `style` (nx.js's `screen`) is accepted wherever the
  engine set CSS on it (display, size, touch-action)
- the WebGL support check reports whether WebGL 2 exists instead of
  probing a `<canvas>` it cannot create; creating the renderer decides,
  and `video.AUTO` still falls back to Canvas
- scratch canvases fall back to OffscreenCanvas, and the Tiled tileset
  and Mesh code now go through the canvas factory
- touch: a global `TouchEvent` counts as touch support without a DOM, so
  touches dispatched on the canvas reach pointer events
- audio: unlocks at once instead of listening on `document`
- element bounds without a DOM come from the canvas itself
- Tiled: an XML map where there is no DOMParser fails saying to export it
  as JSON, and the loader keeps a parser's error as the `cause` of the
  "Failed loading resource" error

tests/nxjs-compatibility.spec.js runs a game in a Web Worker made to look
like nx.js, with the Canvas, WebGL and AUTO renderers: boot, image and
JSON map loading, drawn pixels, touch, and the XML map error.

Skills (deployment, tilemaps, audio, input) and the changelog describe
running without a DOM and what nx.js lacks.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JnzwA4ysaW5ocy8xevRB4k
nx.js throws on `new OffscreenCanvas(0, 0)`, so the 0x0 capability probe
read as "no OffscreenCanvas" there, and every scratch canvas (text, tiles,
render targets) fell through to `document.createElement` and crashed.
Probe with a 1x1 canvas instead, and where neither a document nor an
OffscreenCanvas is available, fail with an error saying so rather than
a TypeError on `document`.

The nx.js test worker now refuses empty canvases as nx.js does, and its
scene draws a Text, which needs a scratch canvas: without the fix the
test fails with the crash above. Two browser-side tests pin that touch
and OffscreenCanvas detection are unchanged where there is a DOM
(desktop Chromium defines `TouchEvent` with no touchscreen, which the
no-DOM touch fallback must not count).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JnzwA4ysaW5ocy8xevRB4k
Checked against nx.js's own runtime and native code rather than Chromium:

- `import "melonjs"` threw on nx.js: `device.screenOrientation` read
  `screen.orientation` at import, which nx.js implements as a getter that
  throws "Method not implemented". Read it inside a try/catch.
- every touch landed at (0, 0): nx.js's touches have `clientX/Y` but no
  `pageX/Y`. Without a DOM, page and client coordinates are the same,
  so use the client ones there.
- no audio clip could load: nx.js's `Audio` has no `canPlayType`, so no
  format was supported and every clip was refused before decoding. With
  no DOM and no `canPlayType`, assume each format and let decoding fail.
- Tiled image-collection tiles and `Mesh.toCanvas()` create a `<canvas>`
  again where there is a page, so a 0-sized one behaves as before; the
  canvas factory is only used without a DOM.
- `loader.reload()` also keeps the parser's error as `cause`.

The test worker now reproduces each of these (a throwing
`screen.orientation`, touches without `pageX`, an nx.js-like Web Audio
stand-in and an `Audio` without `canPlayType`), and covers text pixels,
an image-collection tile cut through a scratch canvas, the no-DOM
`enableSwipe` / `getElement` / `getParentElement`, and the error when
there is neither a DOM nor an OffscreenCanvas. Reverting any one fix
fails its test.

Deployment skill: the font snippet no longer calls `FontFace.load()`,
which throws on nx.js; notes on audio formats and the profile picker
that reading `localStorage` opens on nx.js.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JnzwA4ysaW5ocy8xevRB4k
tests/nxjs-browser-parity.spec.js holds a real page to what it did before
the no-DOM work: each capability it touched (touchEvent, pointerEvent,
touch, maxTouchPoints, screenOrientation, offscreenCanvas, WebGL support,
audio formats) against the formula it was computed with before, written
out in the test; the DOM helpers (getElement, getParentElement,
getElementBounds, enableSwipe, createCanvas) against what they returned;
and an Application mounting in the page: its parent element, the canvas
in it with display block, CSS size and touch-action, page coordinates
mapped through the canvas's client rect, and XML Tiled maps still
parsed. Removing the `hasDOM()` gate from touch detection or the WebGL
check, or inverting the one on appendChild, fails it.

The two browser-side tests of nxjs-compatibility.spec.js move here.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JnzwA4ysaW5ocy8xevRB4k
A short section under Getting Started: melonJS on nx.js or rendering in a
Web Worker, given the canvas as `settings.canvas`, marked experimental as
it is tested against a simulation of nx.js and not yet on Switch
hardware, with a link to the deployment skill for what differs. The
changelog entry says the same.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JnzwA4ysaW5ocy8xevRB4k
Copilot AI balanced review requested due to automatic review settings November 12, 2026 00:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

Every remaining `typeof document` test in the engine goes through
`hasDOM()`: the audio engine and context, the font face loader and its
unload, the glTF base URL, getUriFragment, the wheel, pointer lock, video
and fullscreen probes, and DOMContentLoaded. `hasDOM` moves above
DOMContentLoaded in dom.ts, which now uses it.

The changelog entry is shortened and names nx.js with its link, and the
README calls it the Nintendo Switch homebrew framework.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SeWCkETjW35ZbXDRswV1hx
Copilot AI balanced review requested due to automatic review settings November 12, 2026 01:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@obiot
obiot merged commit 1561cf5 into master Nov 12, 2026
6 checks passed
@obiot
obiot deleted the claude/serene-noether-1lvk7v branch November 12, 2026 03:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants