ntk
v8.23.3
Published
Desktop UI toolkit for X11 with canvas-like 2d and OpenGL rendering
Maintainers
Readme
ntk
node.js desktop UI toolkit for X11
A set of wrappers around the low level node-x11 module to simplify X Window UI programming — window creation, DOM-style event handling, 2d/3d graphics — using API concepts you already know from the web.
Everything, including font rasterization, is pure JavaScript: npm install
never compiles anything.
Docs & live playground: https://sidorares.github.io/ntk/ — the playground runs ordinary ntk code in your browser against node-x11's in-browser pure-JS X server (XRender included) with bundled fonts. The same server also works headless in node — see docs/xserver.md — so ntk apps and tests can run with no real X server at all.
Installation
npm install ntkRequires Node.js >= 20.19 and an X server. Full documentation lives in docs/.
Basic usage
import { createClient } from 'ntk';
const app = await createClient();
const wnd = app.createWindow({ width: 500, height: 300, title: 'Hello' });
wnd.on('mousedown', (ev) => wnd.setTitle(`click: ${ev.x},${ev.y}`));
wnd.map();2d graphics
Each window (or pixmap) can create a 2d canvas implementing the HTML
context2d api
via the
XRender extension —
paths (arcs, beziers, Path2D with SVG path data, non-zero/even-odd fill
rules), transforms with save()/restore(), clipping, globalAlpha and
Porter-Duff composite ops (docs/context-2d.md).
Most operations are performed on the X server side (image composition,
scaling, blur, text composition, gradients etc). Text is fully shaped in
pure JS — OpenType kerning/ligatures and complex scripts (fontkit), bidi
(bidi-js), automatic font fallback — rasterized by a built-in scanline
rasterizer and cached server-side as XRender glyphs, so drawing a line of
text costs about a byte per glyph on the wire. Font names resolve through
fontconfig (fc-match). Very large and continuously animated sizes render
as server-side trapezoids instead of cached bitmaps. A TextLayout engine
wraps styled text to a target width — see docs/text.md.
PNG/JPEG images decode client-side (loadImage) and composite server-side
via ctx.drawImage (docs/images.md). An SvgView widget
renders static SVG (shapes, gradients, transforms, use) through the same
2d pipeline (docs/svg.md).
Rendering documents — markdown, formulas, rich text — is not ntk's job:
it draws, and a document is a tree of layout decisions on top of that.
@react-x11/components
is where those live, over the react-x11 renderer.
import { createClient } from 'ntk';
const app = await createClient();
const wnd = app.createWindow({ width: 800, height: 600 });
const ctx = wnd.getContext('2d');
wnd.on('mousemove', (ev) => {
const gradient = ctx.createRadialGradient(0, 0, 10, ev.x, ev.y, 500);
gradient.addColorStop(0, 'red');
gradient.addColorStop(0.5, 'green');
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, ctx.width, ctx.height);
});
wnd.map();ctx.drawImage() also accepts a node-canvas
canvas as a source — for images with lots of drawing calls it might be more
efficient to draw locally and transfer pixels to the server when ready.
Frame pacing & networked displays
Noisy events (resize, mousemove, expose) are coalesced into paced
frames — the latest state wins, nothing queues up — and each frame is
fenced with a server round-trip, so rendering automatically slows to the
connection's real throughput instead of drawing a trail of stale updates
over ssh-forwarded displays. Animation uses the DOM-style
requestAnimationFrame:
function frame(now) {
// ... draw ...
wnd.requestAnimationFrame(frame); // ~60fps locally, RTT-paced remotely
}
wnd.requestAnimationFrame(frame);See docs/window.md for the knobs (frameInterval,
frameSync, maxFramesInFlight, coalesceEvents) and the raw uncoalesced
event stream.
Resource management
Server-side resources support using / await using (Node 24+):
{
await using app = await createClient();
using pixmap = app.createPixmap({ width: 256, height: 256, depth: 24 });
// ... draw ...
} // pixmap freed, connection closedImporting less
ntk is the whole package: the X11 client, the font engine, the SVG parser
and the image decoders all load with it. A program that needs one piece —
a renderer drawing through something other than X11 that wants the colour
parser, say — can import just that piece, and loads only what it needs:
| Import | What | Loads with it |
| ------------------- | ------------------------------------------------------- | --------------------------- |
| ntk/color | cssColor, cssColorStraight, premultiply | parse-color |
| ntk/imagedata | ImageData, pixelLayout, toStraightRgba, … | — |
| ntk/path | Path2D, parseSvgPath | — |
| ntk/gl | GLError, GL_MODES, DEFAULT_GL_POLICY, … | — |
| ntk/shadow-math | shadowSigma, shadowReach, blurScale, … | — |
| ntk/shadow-tiles | planShadowTiles, shadowTileAlpha, … | — |
| ntk/image | Image, decodeImage, loadImage, exifOrientation | jpeg-js, pngjs |
| ntk/svg | SvgView (default export) | htmlparser2, domutils |
| ntk/font | Font (default export) | fontkit |
| ntk/xembed | the XEmbed protocol (docs/xembed.md) | x11 |
Each is the same module the root re-exports, so importing a name both ways gives the same function or class: nothing is loaded, or instantiated, twice.
ntk/package.json is exported too, for the tools that read a dependency's
version or metadata that way — bundlers, React Native's resolver, version
reporters. It loads nothing.
3d graphics
Two backends, chosen by glPolicy
(docs/context-gles.md):
- direct (opt-in) — shader GL on the real GPU with no pixels on the
socket: OpenGL ES 2 over DRI3 + Present on Linux, CGL over the Apple-DRI
extension on macOS/XQuartz. Needs the optional
x11-driaddon. - indirect GLX (default) — most of the OpenGL 1.4 api, serialized into the X connection. Note that on many systems indirect GLX is disabled by default — you'll need to enable it for gl to work.
import { createClient } from 'ntk';
const app = await createClient();
// GLX drawables need a GLX-capable visual, chosen before the window exists
const glx = await app.chooseGLXConfig({ DEPTH_SIZE: 24 });
const wnd = app.createWindow({ width: 300, height: 300, visual: glx.visual, depth: glx.depth });
wnd.map();
const gl = wnd.getContext('opengl', glx);
gl.ClearColor(0.3, 0.3, 0.3, 0.0);
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.Begin(gl.TRIANGLES);
gl.Color3f(1, 0, 0);
gl.Vertex3f(-1, -1, 0);
gl.Vertex3f(1, -1, 0);
gl.Vertex3f(0, 1, 0);
gl.End();
gl.SwapBuffers();See examples/ for more (teapot, GL clock, textures, text rendering).
High level widgets / layout management etc
Likely to be implemented outside as part of a react renderer (react-x11).
