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

headless-three

v1.8.0

Published

Headless Three.js rendering for Node.js, made simple.

Readme

headless-three

Headless Three.js rendering for Node.js, made simple. Render 3D scenes to images on the server with no browser required. Runs Three.js r162 (the last version with WebGL 1 support) without polluting the global scope.

npm version License: MIT

Features

  • Three.js r162 running in an isolated VM context
  • No global scope pollution
  • Works with any canvas library (skia-canvas, @napi-rs/canvas, canvas)
  • Headless WebGL rendering via gl
  • Built-in render function with multi-format output (PNG, JPEG, WebP, etc.) via sharp
  • Texture loading utility
  • Extensible via runInContext for custom loaders

Install

npm install headless-three

You also need a canvas library:

npm install skia-canvas
# or
npm install @napi-rs/canvas
# or
npm install canvas

Quick Start

import { Canvas, Image, ImageData } from "skia-canvas"
import getTHREE from "headless-three"

const { THREE, render, loadTexture } = await getTHREE({ Canvas, Image, ImageData })

// Create scene
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100)
camera.position.set(0, 0, 5)

// Add a lit cube
scene.add(new THREE.AmbientLight(0x404040))
const light = new THREE.DirectionalLight(0xffffff, 1)
light.position.set(-1, 2, 3)
scene.add(light)

// Load a texture
const texture = await loadTexture("path/to/texture.png")

const cube = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1),
  new THREE.MeshStandardMaterial({ map: texture })
)
scene.add(cube)

// Render to file
await render({
  scene,
  camera,
  width: 512,
  height: 512,
  path: "output.png"
})

// Or get a PNG buffer
const buffer = await render({ scene, camera })

API

getTHREE(options)

Returns a promise that resolves to { THREE, render, loadTexture, runInContext, parseColor }.

Options

| Option | Description | |---|---| | Canvas | Canvas class from your canvas library | | Image | Image class from your canvas library | | ImageData | ImageData class from your canvas library |

Returns

| Property | Description | |---|---| | THREE | The Three.js library object | | render(options) | Renders a scene to an image buffer or file | | loadTexture(input) | Creates a THREE.CanvasTexture from a Canvas, Image, ImageData, string path, or Buffer | | runInContext(code) | Executes JavaScript code inside the VM context | | parseColor(input) | Parses a color into { color, alpha } |

All helpers (render, loadTexture, runInContext, parseColor) are also attached to the returned THREE object under THREE.headless, so you can destructure just THREE and access them as THREE.headless.render(...) etc.

render(options)

Renders a scene to an image buffer or file. When saving to a file, the format is inferred from the extension unless format is specified. Buffer output defaults to PNG.

| Option | Default | Description | |---|---|---| | scene | | The Three.js scene to render | | camera | | The camera to render from | | width | 1024 | Output width in pixels | | height | 1024 | Output height in pixels | | path | | If provided, saves to this file path. Format is inferred from the extension | | format | | Output format ("png", "jpeg", "webp", "avif", "tiff", etc.). Overrides extension inference. See sharp's output docs for the full list of supported formats | | output | | Options passed directly to the sharp format encoder (e.g. { quality: 85, mozjpeg: true } for JPEG). See sharp's output docs for all available options per format | | colorSpace | THREE.SRGBColorSpace | Renderer output color space | | background | transparent | Background color. Accepts any format supported by parseColor | | premultiplyAlpha | false | Keeps alpha premultiplied in the output image. WebGL's alpha blending produces premultiplied pixels in the framebuffer, which makes semi-transparent colors appear darker than expected when saved as PNG. The default (false) un-premultiplies them so they render correctly in image viewers. Set to true only if you need the raw premultiplied output |

// Save to file (format inferred from extension)
await render({
  scene,
  camera,
  width: 512,
  height: 512,
  path: "output.png"
})

// Save as JPEG
await render({ scene, camera, path: "output.jpg" })

// Force format regardless of extension
await render({ scene, camera, path: "output.img", format: "webp" })

// Get PNG buffer (default)
const buffer = await render({ scene, camera })

// Get JPEG buffer
const buffer = await render({ scene, camera, format: "jpeg" })

// Smaller JPEG via mozjpeg encoder
await render({
  scene,
  camera,
  path: "output.jpg",
  output: { quality: 85, mozjpeg: true }
})

// Maximum PNG compression
await render({
  scene,
  camera,
  path: "output.png",
  output: { compressionLevel: 9, effort: 10 }
})

loadTexture(input)

Creates a THREE.CanvasTexture from various input types:

// From a file path
const texture = await loadTexture("path/to/image.png")

// From an Image
const img = new Image()
img.src = "path/to/image.png"
const texture = await loadTexture(img)

// From a Canvas
const texture = await loadTexture(canvas)

runInContext(code)

Executes code inside the same VM context as Three.js. This is useful for loading bundled Three.js addons:

import fs from "node:fs"

const { THREE, runInContext } = await getTHREE({ ... })

// Load a pre-bundled addon
runInContext(fs.readFileSync("GLTFLoader.bundle.js", "utf-8"))

parseColor(input)

render()'s background option accepts many color formats (hex, arrays, objects, CSS strings, etc.). parseColor exposes the same parser so you can accept those formats in your own code without duplicating the logic.

Returns { color, alpha }, where color is a THREE.Color and alpha is a number in [0, 1].

const { THREE, parseColor } = await getTHREE({ ... })

const { color, alpha } = parseColor("rgba(255, 0, 0, 0.5)")
renderer.setClearColor(color, alpha)

Supported formats

Alpha defaults to fully opaque when not specified.

| Form | Example | Notes | |---|---|---| | Hex number | 0xff0000 | Treated as sRGB | | Array | [1, 0, 0], [1, 0, 0, 0.5] | 0–1 floats, sRGB | | Object | { r: 1, g: 0, b: 0 }, { r: 1, g: 0, b: 0, a: 0.5 } | 0–1 floats, sRGB | | Hex string | "#rgb", "#rgba", "#rrggbb", "#rrggbbaa" | Shorthand forms expand per channel | | rgb() / rgba() string | "rgb(255, 0, 0)", "rgba(255, 0, 0, 0.5)" | CSS 0–255 for r/g/b, 0–1 for alpha | | hsl() / hsla() string | "hsl(0, 100%, 50%)", "hsla(0, 100%, 50%, 0.5)" | CSS HSL | | Named color | "red", "lime", "transparent" | Any CSS named color | | THREE.Color instance | new THREE.Color(1, 0, 0) | Used verbatim (not re-decoded) |

All non-THREE.Color inputs are treated as sRGB values and decoded to linear via THREE.Color().setRGB(..., SRGBColorSpace) so the output matches the color you'd pick in a color picker.

How It Works

headless-three uses Node.js's vm module to create an isolated V8 context with polyfilled browser APIs (document, window, URL, etc.). Three.js's CJS build runs inside this sandbox, thinking it's in a browser. The canvas library you provide handles the actual drawing surface and image operations. Rendering uses gl for headless WebGL and sharp for image encoding.

License

MIT © Ewan Howell