npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

three.js-rtt

v0.1.9

Published

Render live web apps to textures on Three.js meshes—interactive DOM UI for curved and flat in-scene screens.

Downloads

40

Readme

Three.js-RTT

Render live web apps to textures on Three.js meshes — ideal for curved, in-scene screens.

Put a real web app — DOM, CSS animations, <video>, nested <canvas> — onto any mesh in a Three.js scene, curved or flat, with no <iframe>, no camera-facing overlay, and full click/keyboard interactivity. The content is a real, ordinary texture: it rides the mesh's own UVs, gets normal lighting/occlusion/perspective from the WebGL pipeline, and never repositions itself to face the camera.

npm version license

Why this exists

The usual way to put "a screen" in a Three.js scene is one of:

  • An <iframe>, positioned in 3D via @react-three/drei's <Html> or similar. Works for simple cases, but it's cross-origin by nature (storage/session headaches), and it's fixed to a flat CSS-transformed plane — it can never conform to a curved screen mesh.
  • <Html transform> with in-process content (no iframe, real DOM mounted directly). Better for interactivity, but it's still an affine CSS 3D transform on a flat quad composited on top of the WebGL canvas — not a real, depth-tested, lit part of the scene. It cannot curve, and it needs manual z-index/occlusion hacks to look attached to the model at all.
  • A frozen snapshot texture. Solves curvature, but it's a single static image — no animation, no interactivity, no live content.

Three.js-RTT is a fourth option: a genuine render-to-texture pipeline. It continuously rasterizes a live, off-screen DOM subtree into a <canvas>, wires that canvas up as a THREE.CanvasTexture on the target mesh's material, and bridges pointer/keyboard input back into the real DOM via raycasting — so the only thing touching the WebGL scene is an ordinary texture, and the only thing handling text input is a real, focused, native <input>.

Install

npm install three.js-rtt

three (>=0.160.0) is a peer dependency — bring your own version.

Quick start

import { ScreenSurfaceRegistry } from "three.js-rtt";
import { Mesh, PerspectiveCamera, WebGLRenderer } from "three";

// Any live, connected DOM subtree — a real app, not a snapshot.
const root = document.createElement("div");
root.style.cssText = "position: fixed; inset: 0; z-index: -1; width: 1024px; height: 768px;";
root.innerHTML = `<button style="font-size: 32px;">Click me</button>`;
document.body.appendChild(root);

const registry = new ScreenSurfaceRegistry();

registry.register({
  id: "monitor-1",
  mesh: screenMesh, // any THREE.Mesh — flat or curved, doesn't matter
  root,
  resolution: { width: 1024, height: 768 },
  input: { camera, domElement: renderer.domElement },
});

That's it — screenMesh now shows root's live content, textured along its real geometry (curved meshes warp correctly, with zero special-casing), and clicking/typing on the mesh in the 3D scene reaches the real DOM inside root.

See examples/vite-demo for a complete, runnable scene: two monitors (one curved, one flat), a live @keyframes animation, a <video>, a nested animated <canvas>, and a real <input> — all interactive, all textured directly onto FBX-authored meshes.

How it works

  1. Compositor (DomCompositor): rAF-throttled, dirty-gated DOM rasterizer. The base layer (everything except <video>/<canvas>) is serialized via SVG foreignObject and only re-rasterized when a MutationObserver reports a structural change — a static UI costs effectively nothing per frame once painted once. <video>/<canvas> descendants are excluded from that serialization (unreliable across browsers) and instead drawImage()'d directly onto the output canvas every tick, since neither fires DOM mutations for their own playback — this gives them perfect, live fidelity.
  2. Texture (ScreenTexture): wraps the compositor's canvas in a THREE.CanvasTexture and assigns it to the target mesh's material. It never touches geometry or UVs — curvature support is free, because the GPU already interpolates the mesh's own UV attribute per-triangle, exactly as it would for any authored texture.
  3. Input (InputBridge): raycasts the pointer against the mesh, converts the hit's UV to pixel coordinates in the DOM root, and does a geometry-based hit-test (not document.elementFromPoint — the root isn't necessarily where the OS cursor visually is) to find the real element underneath. Dispatches a synthetic PointerEvent/WheelEvent there, and on a focusable/editable target, calls a real element.focus(). Because the DOM root is genuinely connected and laid out, that real focus call means native keyboard, IME composition, and text selection all keep working with zero further synthesis.
  4. Registry (ScreenSurfaceRegistry): tracks N independent screens and shares one rAF loop across all of their compositors, instead of each running its own.

API

| Export | What it's for | |---|---| | ScreenSurface | Facade tying one mesh to one live DOM root: compositor + texture + input, one object. Prefer this (or the registry) over the pieces below unless you need finer control. | | ScreenSurfaceRegistry | Register/unregister N screens; shares one rAF loop; worldTransformsSnapshot() gives an app raw material for multi-monitor layout policy (ordering, a spanned desktop, focus-follows-click) — no such policy is baked in here. | | DomCompositor | The rasterizer on its own, if you want to drive the canvas yourself. | | ScreenTexture | The CanvasTexture ↔ mesh material wiring on its own. | | InputBridge | The raycast → hit-test → dispatch bridge on its own. | | ForeignObjectRasterStrategy | The default rasterization strategy; implement RasterStrategy to swap in another (e.g. html2canvas, or a native offscreen-webview capture for an Electron/Tauri desktop shell). |

Full signatures are in src/index.ts and the shipped .d.ts.

ScreenSurface / ScreenSurfaceRegistry.register() options

interface ScreenSurfaceOptions {
  id: string;
  mesh: THREE.Mesh;
  root: HTMLElement;                 // live, connected, laid-out DOM subtree
  resolution: { width: number; height: number };
  compositor?: {
    fps?: number;                    // default 24; the focused/active screen can go up to 60
    dirty?: "always" | "mutation-observer" | (() => boolean);
    strategy?: RasterStrategy;
  };
  texture?: {
    applyAsEmissive?: boolean;       // self-lit "glowing screen" look
    materialFactory?: (original: THREE.Material) => THREE.Material;
    colorSpace?: THREE.ColorSpace;
    generateMipmaps?: boolean;       // default false: crisp text over blurred mipmaps
  };
  input?: {
    camera: THREE.Camera;
    domElement: HTMLElement;         // usually renderer.domElement
    keyboardMode?: "focus-transfer" | "synthetic"; // default "focus-transfer"
  } | false;                         // false: render-only, no input forwarding
}

Performance

The two costs involved are very different in kind, and the design keeps them separate on purpose:

  • Cheap, every live-media tick: drawImage() for live <video>/<canvas> descendants, followed by texture.needsUpdate = true. Fully static screens skip both the canvas copy and the GPU upload after their initial paint.
  • Comparatively expensive, only when actually dirty: the SVG-foreignObject base-layer serialize → decode → draw. Gated behind MutationObserver by default, so a static UI with an idle window costs nothing once painted.

Recommended tiering (all configurable, none hardcoded): the focused/actively-used screen up to 60fps; background-but-visible screens 15–24fps; off-frustum screens frozen entirely via ScreenSurface.setDetail("frozen"). If a capture hasn't resolved before the next tick is due, that tick is skipped rather than queued, so a loaded machine degrades to a lower effective fps smoothly instead of backing up.

Known limitations

  • Native CSS :hover needs genuine cursor position, not dispatchEvent() — hover-only styling won't trigger by default. The input bridge tags the current geometric hit target with a data-hover attribute whenever it changes, so a consuming app can add companion [data-hover] CSS rules if it cares.
  • Hit-testing is a geometry-based approximation, not a full CSS stacking-context resolver: explicit z-index wins ties, then smallest-area (most specific) element, then document order. Correct for the overwhelming majority of real UIs; may misfire on deliberately exotic stacking.
  • <video>/<canvas> clipping uses rectangular intersections for every scrolling/clipping ancestor — exact clip-path/border-radius clipping isn't applied.
  • Base-layer rasterization is SVG-foreignObject-based, which has known cross-browser quirks for very exotic CSS. The RasterStrategy interface exists specifically so a different backend can be swapped in without touching the texture or input layers.

Demo

git clone https://github.com/AnotherJulia/Three.js-RTT.git
cd Three.js-RTT
npm install
npm run build
cd examples/vite-demo
npm run dev      # or, if your npm has workspace-related quirks: ../../node_modules/.bin/vite

Open the printed local URL. Orbit to confirm no billboarding and a specular sweep across the curved monitor; click a screen to type/click into it; press U to toggle a UV-checker pattern on both screens.

The two FBX models under examples/vite-demo/public/models/ are included solely to demonstrate the library against real curved/flat authored geometry — they aren't covered by the MIT license below.

License

MIT for the library source. See the note above regarding the demo's bundled 3D models.