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

@projektemacher/hdr-canvas

v0.4.0

Published

HDR capable HTML canvas

Readme

hdr-canvas

This module contains a collection of functions and classes to work with the HDR and WCG (Wide Color Gamut) support for HTML canvas elements. WCG (display-p3) works in Safari and Firefox on macOS, while full HDR (rec2100-* with float16) needs a Chromium based browser (like Chrome, Edge, Opera and Brave).

Package scope: the package has moved into an org and is published as @projektemacher/hdr-canvas on npm (and as @cmahnke/hdr-canvas on GitHub Packages):

npm i @projektemacher/hdr-canvas
import { checkHDRCanvas } from "@projektemacher/hdr-canvas";

Examples below using bare from "hdr-canvas" refer to the same package.

All changes and a bit of context are part of the release notes for 0.1.0.

This should only be considered as proof of concept or alpha code, don't use it in production environments!

Even if the display of HDR images works, the HDR support for the canvas element needs the browser flag enable-experimental-web-platform-features to be enabled. For example, open chrome://flags#enable-experimental-web-platform-features in Chrome to activate it.

Especially operations on the ImageData arrays are not optimized, e.g. quite slow.

Feature detection

WCG (Wide Color Gamut, e.g. display-p3) and HDR (High Dynamic Range, e.g. rec2100-hlg / rec2100-pq with brightness above SDR white) are detected separately: Safari and Firefox on macOS support WCG canvases without supporting HDR. HDR usually includes WCG, but WCG-only and HDR-only configurations exist.

| Capability | Media query / probe | Function | | ------------------------------------ | ---------------------------------------------------- | ------------------------- | | Wide gamut (p3 or rec2020) | (color-gamut: p3) / (color-gamut: rec2020) | checkWCG() | | High dynamic range | (dynamic-range: high) | checkDynamicRangeHigh() | | Both (HDR usually includes WCG) | both of the above | checkHDR() | | WCG canvas (display-p3) | getContext("2d", {colorSpace: "display-p3"}) | checkWCGCanvas() | | HDR canvas (rec2100-* + float16) | getContext("2d", getHdrOptions()) + Float16Array | checkHDRCanvas() |

Import the required function(s):

import {
  checkHDR,
  checkWCG,
  checkDynamicRangeHigh,
  checkColorGamutP3,
  checkColorGamutRec2020,
  checkHDRCanvas,
  checkWCGCanvas,
  checkHDRVideo,
  checkFloat16Array,
  checkHDRBitDepth,
  getDisplayCapabilities,
  onDisplayChange
} from "hdr-canvas";

Enabling HDR on a canvas

The functions return true if HDR is supported, example:

const canvas = document.getElementById("canvas");
if (checkHDRCanvas()) {
  canvas.configureHighDynamicRange({ mode: "extended" });
} else {
  console.debug("hdr not supported");
  return;
}

This can be useful to add a warning (using the fillText() method) to the canvas if it doesn't support HDR content.

Example checkHDRCanvas()

const hdrCanvasStatus = document.getElementById("hdr-canvas-check")! as HTMLDivElement;
if (checkHDRCanvas()) {
  hdrCanvasStatus.innerText = "HDR Canvas are supported";
  hdrCanvasStatus.style.color = "green";
} else {
  hdrCanvasStatus.innerText = "HDR Canvas are not supported";
  hdrCanvasStatus.style.color = "red";
}

Example checkWCG() and checkWCGCanvas()

Safari and Firefox on macOS support WCG (display-p3) canvases without HDR support:

const wcgStatus = document.getElementById("wcg-check")! as HTMLDivElement;
if (checkWCG()) {
  wcgStatus.innerText = "WCG is supported";
  wcgStatus.style.color = "green";
} else {
  wcgStatus.innerText = "WCG is not supported";
  wcgStatus.style.color = "red";
}

const wcgCanvasStatus = document.getElementById("wcg-canvas-check")! as HTMLDivElement;
if (checkWCGCanvas()) {
  wcgCanvasStatus.innerText = "WCG Canvas is supported";
  wcgCanvasStatus.style.color = "green";
} else {
  wcgCanvasStatus.innerText = "WCG Canvas is not supported";
  wcgCanvasStatus.style.color = "red";
}

Distinguish the four states explicitly:

if (checkHDRCanvas()) {
  console.log("HDR with WCG");
} else if (checkWCGCanvas()) {
  console.log("WCG only (e.g. Safari / Firefox on macOS)");
} else if (checkDynamicRangeHigh()) {
  console.log("HDR dynamic range without WCG canvas");
} else {
  console.log("SDR");
}

Example checkHDRVideo()

const hdrCanvasStatus = document.getElementById("hdr-video-check")! as HTMLDivElement;
if (checkHDRVideo()) {
  hdrCanvasStatus.innerText = "HDR Video is supported";
  hdrCanvasStatus.style.color = "green";
} else {
  hdrCanvasStatus.innerText = "HDR Video is not supported";
  hdrCanvasStatus.style.color = "red";
}

Example checkFloat16Array()

const float16ArrayStatus = document.getElementById("float16array-check")! as HTMLDivElement;
if (checkFloat16Array()) {
  float16ArrayStatus.innerText = "Float16Array in Image constructor is supported";
  float16ArrayStatus.style.color = "green";
} else {
  float16ArrayStatus.innerText = "Float16Array in Image constructor is not supported";
  float16ArrayStatus.style.color = "red";
}

Example checkHDRBitDepth()

This is only a heuristic for the current screen: browsers commonly report a colorDepth of 24 even on HDR displays, and WCG-only screens (display-p3, still 8 bit per channel) can't be distinguished from SDR. Don't rely on it alone, combine it with checkHDR() / checkWCG().

setInterval(() => {
  console.log(checkHDRBitDepth());
}, 500);

Reacting to screen changes (e.g. external screens)

All checks describe the screen currently showing the page. On a multi-screen setup — e.g. an HDR-capable machine with a non-HDR external screen attached (see #107) — a one-shot check at page load goes stale once the window moves to another display. Re-evaluate via onDisplayChange(), which uses media query change events where available and falls back to polling (automatic when no change listener can be attached, covering screen.colorDepth, which has no change event):

import { onDisplayChange, getDisplayCapabilities } from "hdr-canvas";

console.log(getDisplayCapabilities());
// => { hdr, wcg, dynamicRangeHigh, colorGamutP3, colorGamutRec2020, bitDepthHDR }

const unsubscribe = onDisplayChange((capabilities) => {
  console.log("Display changed", capabilities);
});
// Poll `screen.colorDepth` as well (media queries have no bit-depth event):
// onDisplayChange(cb, { pollIntervalMs: 2000 });
// Disable polling entirely:
// onDisplayChange(cb, { pollIntervalMs: 0 });

// Later:
unsubscribe();

Canvas

Note: Currently the Chrome flag enable-experimental-web-platform-features needs to be enabled to have HDR support for the canvas element. You need to tell your visitors about that.

The HDR canvas support is activated by initializing a canvas context using the following snippet:

const colorSpace = "rec2100-hlg";
canvas.configureHighDynamicRange({ mode: "extended" });
const ctx = canvas.getContext("2d", {
  colorSpace: colorSpace,
  colorType: "float16"
  // Use this for Chrome < 133
  //pixelFormat: "float16"
});

Canvas setup

The snippet above is also available as function:

import { initHDRCanvas } from "hdr-canvas";

WCG canvas setup

For WCG-only environments (Safari, Firefox on macOS) a display-p3 canvas without HDR is enough. The options are available as function:

import { getWcgOptions } from "hdr-canvas";

const ctx = canvas.getContext("2d", getWcgOptions());
// => { colorSpace: "display-p3" }

Use checkWCGCanvas() to guard this, and checkHDRCanvas() for the full HDR variant.

Implicit Canvas setup

It's now also possible to use a HDR enabled Canvas by wrapping the browser internal getContext() function, by calling defaultGetContextHDR().

import { defaultGetContextHDR, checkHDR, checkHDRCanvas } from "hdr-canvas";

if (checkHDR() && checkHDRCanvas()) {
  defaultGetContextHDR();
  console.log("Enabled HDR Canvas");
}

Note: This example wraps the call to defaultGetContextHDR() into a check (checkHDR() && checkHDRCanvas()), because calling the function in a browser that isn't HDR-capable will break every subsequent call to getContext().

Resetting default HDR canvas

Use the method resetGetContext() to undo the changes by defaultGetContextHDR().

import { resetGetContext } from "hdr-canvas";

resetGetContext();

Importing Float16Image

Afterwards one can use ImageData with a float16 array, first the Float16Image needs to be imported:

import { Float16Image } from "hdr-canvas";

Example: Loading an image

This example assumes image to be a HTMLImageElement including an existing image.

const offscreen = new OffscreenCanvas(image.width, image.height);
const loadCtx = offscreen.getContext("2d");
loadCtx.drawImage(image, 0, 0);
const imData = loadCtx.getImageData(0, 0, image.width, image.height);
console.log(imData);

var hdrCanvas = document.createElement("canvas");
hdrCanvas.width = image.width;
hdrCanvas.height = image.height;

const rec210hglImage = Float16Image.fromImageData(imData);

const ctx = initHDRCanvas(hdrCanvas);
ctx.putImageData(rec210hglImage.getImageData(), 0, 0);

Experimental WASM acceleration

Float16Image.applyGain() / scaled() can optionally run in a small WASM kernel (AssemblyScript, inlined as base64 — no .wasm file handling, no bundler plugins needed). It is disabled by default and hidden behind a flag:

import { setWasmEnabled } from "@projektemacher/hdr-canvas";

setWasmEnabled(true); // experimental, default: false

With the flag off nothing changes: no WebAssembly use, no extra fetch. With it on, large arrays (≥ 1 M values) take the WASM path with transparent fallback to JS — the WASM path never throws, and per-call applyGain(gain, { useWasm: false }) forces the JS path. Note that WebAssembly.compile still requires script-src: wasm-unsafe-eval under a strict CSP; denial silently falls back to JS.

Honest benchmark (Node 24, applyGain over 12 M values): JS ~12 ms, WASM ~51 ms, of which only ~3 ms is copying. Engines with native Float16Array (which this library requires anyway) convert via hardware F16C instructions that scalar WASM can't beat, so leave the flag off for gain — it exists as tested infrastructure for future compute-heavy kernels (e.g. color-space conversion), where the arithmetic intensity can amortize the copy cost.

Full size sweep (ms per applyGain call, Node 24, single machine — indicative, JS varied run to run):

| Values (pixels) | JS | WASM | | --------------- | --------- | ------- | | 1 M (262 kpx) | ~1–3 ms | ~4.5 ms | | 4 M (1 Mpx) | ~4–10 ms | ~18 ms | | 12 M (3 Mpx) | ~12–41 ms | ~51 ms |

Three.js WebGPU

Note: Make sure to have Three.js added as a dependency.

This is just a drop-in-replacement for the regular WebGPURenderer of Three.js.

import HDRWebGPURenderer from "hdr-canvas/three/HDRWebGPURenderer.js";

Note: Starting Three.js 167 the WebGPU renderer is the new default renderer. This has several consequences for the required imports. Use this import instead of the official one and if your using Vite don't provide an import map of resolver alias configuration.

import * as THREE from "three/src/Three.js";

Use it as you'll do with a WebGPURenderer.

renderer = new HDRWebGPURenderer({ canvas: canvas, antialias: true });

Three.js WebGPU

Starting with R180 the WebGPU supports the toneMapping which is the succesor of colorMetadata. But currently this is still limited in colorspaces.

Updating textures

Starting from Three.js version 167 you need to fix imported UHDR Textures, otherwise they will appear black:

model = gltf.scene;
model.traverse((element) => {
  if (element?.material?.type != undefined) {
    let targetMaterial = new THREE.MeshBasicMaterial();
    THREE.MeshBasicMaterial.prototype.copy.call(targetMaterial, element.material);
    element.material = targetMaterial;
  }
});
scene.add(model);

Compatibility

Full HDR canvases currently don't work with Firefox and Safari, due to missing support for HDR (rec2100-* with float16) and only partial WebGPU support. WCG (display-p3) canvases do work in Safari and in Firefox on macOS — use checkWCGCanvas() to detect that case separately. One can import WebGPU and use also a HDR check to guard from errors:

import WebGPU from "hdr-canvas/three/WebGPU.js";

Only use the provided renderer if the browser supports WebGPU and HDR:

if (WebGPU.isAvailable() && checkHDRCanvas()) {
  renderer = new HDRWebGPURenderer({ canvas: canvas, antialias: true });
} else {
  renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
}

Examples

Bundled examples

Some of the examples above are also part of this repository.

npm i
npm run dev

Open This URL in your browser: http://localhost:5173/, you can also access them directly from GitHub.

Old examples on my blog:

All examples requires a Chromium based browser (like Chrome, Edge, Opera and Brave) and a HDR-enable monitor.


TODO

The following things might be improved:

  • [x] Try to detect change of screen for HDR detection - #107 - see onDisplayChange() (change events with polling fallback), screen.colorDepth remains a heuristic
  • [ ] Improve speed
  • [ ] Documentation
    • [ ] Link to browser HDR support
    • [ ] Give a overview on current status

References

Browser HDR

Older

Sources

This section contains different definitions, which can be helpful to impkement HDR related things

Workflow on related changes to web APIs

This is considered to be experimental, currently we're waiting for TypeSript to pick up the changes made to the web APIs. See microsoft/TypeScript-DOM-lib-generator#2107

Along the spec

  1. Change to the WhatsWG spec - Issue Tracker
  2. Change apporoved
  3. MDN picks up spec change - Create an issue
  4. Changes are popagated to the TypeScript generated DOM library

By custom TypeScript types

  1. Extend src/types, try to extend existing interfaces

Generating updated TypeScript types

Starting with one of the next monor versions (maybe 0.2.0) the prefered way is to update the browser / DOM types instead of adding our own HDR types. The existing types will continue to exist for now. This change reflects improvements in browser support and should make transitioning to standard base types later on.

The basic workflow is as follows:

To make this repaetable add patches to this repository:

Edit the types, for example to change existing definitions, see Update the definitions

cd node_modules/@typescript/dom-lib-generator/
vi inputfiles/overridingTypes.jsonc

To generate a patch pmake sure, that the Git submmodule is remove, otherwise patch-package will fail.

node scripts/git-submodules.js -c -d node_modules/@typescript/dom-lib-generator/
npx patch-package @typescript/dom-lib-generator