hdr-canvas 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "hdr-canvas",
3
+ "version": "0.0.1",
4
+ "description": "HDR capable HTML canvas",
5
+ "main": "dist/hdr-canvas.js",
6
+ "files": [
7
+ "dist/hdr-canvas.d.ts",
8
+ "dist/hdr-canvas.js",
9
+ "dist/hdr-canvas.js.map",
10
+ "dist/hdr-canvas.min.js",
11
+ "dist/hdr-canvas.min.js.map",
12
+ "dist/hdr-canvas.cjs",
13
+ "dist/hdr-canvas.cjs.map",
14
+ "three",
15
+ "src"
16
+ ],
17
+ "scripts": {
18
+ "test": "echo \"Error: no test specified\" && exit 1",
19
+ "lint": "eslint . -c eslint.config.mjs --report-unused-disable-directives",
20
+ "tsc": "tsc",
21
+ "build": "rimraf ./dist ./build && tsc && rollup --config",
22
+ "clean": "rimraf ./dist ./build",
23
+ "format": "prettier . --check"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/cmahnke/hdr-canvas.git"
28
+ },
29
+ "keywords": [
30
+ "HDR",
31
+ "canvas",
32
+ "3D",
33
+ "Three.js"
34
+ ],
35
+ "author": "Christian Mahnke",
36
+ "license": "MIT",
37
+ "bugs": {
38
+ "url": "https://github.com/cmahnke/hdr-canvas/issues"
39
+ },
40
+ "homepage": "https://github.com/cmahnke/hdr-canvas#readme",
41
+ "devDependencies": {
42
+ "@eslint/js": "^9.6.0",
43
+ "@rollup/plugin-node-resolve": "^15.2.3",
44
+ "@rollup/plugin-terser": "^0.4.4",
45
+ "@rollup/plugin-typescript": "^11.1.6",
46
+ "@types/eslint__js": "^8.42.3",
47
+ "eslint": "^9.6.0",
48
+ "prettier": "^3.3.2",
49
+ "rimraf": "^5.0.7",
50
+ "rollup": "^4.18.0",
51
+ "rollup-plugin-dts": "^6.1.1",
52
+ "three": "^0.166.1",
53
+ "tslib": "^2.6.3",
54
+ "typescript": "^5.5.3",
55
+ "typescript-eslint": "^8.0.0-alpha.10"
56
+ },
57
+ "dependencies": {
58
+ "colorjs.io": "^0.5.2"
59
+ }
60
+ }
@@ -0,0 +1,25 @@
1
+ // See https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts
2
+
3
+ type HDRHTMLCanvasOptionsType = "mode";
4
+ type HDRHTMLCanvasOptions = { [key in HDRHTMLCanvasOptionsType]?: string };
5
+
6
+ interface HDRHTMLCanvasElement extends HTMLCanvasElement {
7
+ configureHighDynamicRange(options: HDRHTMLCanvasOptions): void;
8
+ }
9
+
10
+ interface HDRImageData {
11
+ readonly colorSpace: PredefinedColorSpace;
12
+ readonly data: Uint8ClampedArray | Uint16Array;
13
+ readonly height: number;
14
+ readonly width: number;
15
+ }
16
+
17
+ // See https://github.com/w3c/ColorWeb-CG/blob/main/hdr_html_canvas_element.md
18
+ // "rec2100-display-linear" is left out beacause of mapping issues
19
+ type HDRPredefinedColorSpace =
20
+ | "display-p3"
21
+ | "srgb"
22
+ | "rec2100-hlg"
23
+ | "rec2100-pq";
24
+
25
+ //enum HDRPredefinedColorSpace {"display-p3", "srgb", "rec2100-hlg", "rec2100-pq", "rec2100-display-linear"};
@@ -0,0 +1,156 @@
1
+ import Color from "colorjs.io";
2
+ import type { Coords, ColorTypes } from "colorjs.io";
3
+
4
+ type Uint16ImagePixelCallback = (
5
+ red: number,
6
+ green: number,
7
+ blue: number,
8
+ alpha: number,
9
+ ) => Uint16Array;
10
+
11
+ /*
12
+ interface ColorSpaceMapping {
13
+ [key: HDRPredefinedColorSpace]: string
14
+ }
15
+ */
16
+
17
+ export class Uint16Image {
18
+ height: number;
19
+ width: number;
20
+ data: Uint16Array;
21
+ static DEFAULT_COLORSPACE: HDRPredefinedColorSpace = "rec2100-hlg";
22
+ static SDR_MULTIPLIER = 2 ** 16 - 1; //(2**16 - 1)
23
+ static COLORSPACES: Record<HDRPredefinedColorSpace, ColorTypes> = {
24
+ "rec2100-hlg": "rec2100hlg",
25
+ "display-p3": "p3",
26
+ srgb: "sRGB",
27
+ "rec2100-pq": "rec2100pq",
28
+ };
29
+ colorSpace: HDRPredefinedColorSpace;
30
+
31
+ constructor(width: number, height: number, colorspace?: string) {
32
+ if (colorspace === undefined || colorspace === null) {
33
+ this.colorSpace = Uint16Image.DEFAULT_COLORSPACE;
34
+ } else {
35
+ this.colorSpace = colorspace as HDRPredefinedColorSpace;
36
+ }
37
+
38
+ this.height = height;
39
+ this.width = width;
40
+ this.data = new Uint16Array(height * width * 4);
41
+ }
42
+
43
+ fill(color: number[]): Uint16Image | undefined {
44
+ if (color.length != 4) {
45
+ return;
46
+ }
47
+ for (let i = 0; i < this.data.length; i += 4) {
48
+ this.data[i] = color[0];
49
+ this.data[i + 1] = color[1];
50
+ this.data[i + 2] = color[2];
51
+ this.data[i + 3] = color[3];
52
+ }
53
+ return this;
54
+ }
55
+
56
+ getPixel(w: number, h: number): Uint16Array {
57
+ const pos = (h * this.width + w) * 4;
58
+
59
+ return this.data.slice(pos, pos + 4);
60
+ }
61
+
62
+ setPixel(w: number, h: number, px: number[]): void {
63
+ const pos = (h * this.width + w) * 4;
64
+ this.data[pos + 0] = px[0];
65
+ this.data[pos + 1] = px[1];
66
+ this.data[pos + 2] = px[2];
67
+ this.data[pos + 3] = px[3];
68
+ }
69
+
70
+ // Only use this for aplha, since it doesn't to color space conversions
71
+ static scaleUint8ToUint16(val: number): number {
72
+ return (val << 8) | val;
73
+ }
74
+
75
+ getImageData(): ImageData | null {
76
+ if (this.data === undefined || this.data === null) {
77
+ return null;
78
+ }
79
+ return new ImageData(
80
+ this.data as unknown as Uint8ClampedArray,
81
+ this.width,
82
+ this.height,
83
+ { colorSpace: this.colorSpace as PredefinedColorSpace },
84
+ );
85
+ }
86
+
87
+ static convertPixelToRec2100_hlg(pixel: Uint8ClampedArray): Uint16Array {
88
+ const colorJScolorSpace = <string>(
89
+ Uint16Image.COLORSPACES["rec2100-hlg" as HDRPredefinedColorSpace]
90
+ );
91
+
92
+ const srgbColor = new Color(
93
+ "srgb",
94
+ Array.from(pixel.slice(0, 3)).map((band: number) => {
95
+ return band / 255;
96
+ }) as Coords,
97
+ pixel[3] / 255,
98
+ );
99
+ const rec2100hlgColor = srgbColor.to(colorJScolorSpace);
100
+ const hlg: Array<number> = rec2100hlgColor.coords.map((band: number) => {
101
+ return Math.round(band * Uint16Image.SDR_MULTIPLIER);
102
+ });
103
+ // Readd alpha
104
+ hlg.push(rec2100hlgColor.alpha * Uint16Image.SDR_MULTIPLIER);
105
+
106
+ return Uint16Array.from(hlg);
107
+ }
108
+
109
+ static convertArrayToRec2100_hlg(data: Uint8ClampedArray): Uint16Array {
110
+ const uint16Data = new Uint16Array(data.length);
111
+ for (let i = 0; i < data.length; i += 4) {
112
+ const rgbPixel: Uint8ClampedArray = data.slice(i, i + 4);
113
+ const pixel = Uint16Image.convertPixelToRec2100_hlg(rgbPixel);
114
+ uint16Data.set(pixel, i);
115
+ }
116
+ return uint16Data;
117
+ }
118
+
119
+ pixelCallback(fn: Uint16ImagePixelCallback) {
120
+ for (let i = 0; i < this.data.length; i += 4) {
121
+ this.data.set(
122
+ fn(this.data[i], this.data[i + 1], this.data[i + 2], this.data[i + 3]),
123
+ i,
124
+ );
125
+ }
126
+ }
127
+
128
+ static fromImageData(imageData: HDRImageData): Uint16Image {
129
+ const i = new Uint16Image(imageData.width, imageData.height);
130
+ if (imageData.colorSpace == "srgb") {
131
+ i.data = Uint16Image.convertArrayToRec2100_hlg(
132
+ <Uint8ClampedArray>imageData.data,
133
+ );
134
+ } else if (imageData.colorSpace == Uint16Image.DEFAULT_COLORSPACE) {
135
+ i.data = <Uint16Array>imageData.data;
136
+ } else {
137
+ throw new Error(`ColorSpace ${imageData.colorSpace} isn't supported!`);
138
+ }
139
+ return i;
140
+ }
141
+
142
+ setImageData(imageData: HDRImageData): void {
143
+ this.width = imageData.width;
144
+ this.height = imageData.height;
145
+ if (imageData.colorSpace == "srgb") {
146
+ this.data = Uint16Image.convertArrayToRec2100_hlg(
147
+ <Uint8ClampedArray>imageData.data,
148
+ );
149
+ } else if (imageData.colorSpace == Uint16Image.DEFAULT_COLORSPACE) {
150
+ this.data = <Uint16Array>imageData.data;
151
+ } else {
152
+ throw new Error(`ColorSpace ${imageData.colorSpace} isn't supported!`);
153
+ }
154
+ this.colorSpace = Uint16Image.DEFAULT_COLORSPACE;
155
+ }
156
+ }
@@ -0,0 +1,7 @@
1
+ import {Uint16Image} from './Uint16Image'
2
+
3
+ export function initHDRCanvas(canvas : HDRHTMLCanvasElement) : RenderingContext | null {
4
+ canvas.configureHighDynamicRange({mode: 'extended'});
5
+ const ctx = canvas.getContext("2d", {colorSpace: Uint16Image.DEFAULT_COLORSPACE, pixelFormat: 'float16'});
6
+ return ctx;
7
+ }
@@ -0,0 +1,58 @@
1
+ export function checkHDR(): boolean {
2
+ try {
3
+ const bitsPerChannel: number = screen.colorDepth / 3;
4
+ const hdrSupported: boolean = bitsPerChannel > 8;
5
+
6
+ //TODO: Test if this works as expected
7
+ const dynamicRangeHighMQ: boolean = window.matchMedia(
8
+ "(dynamic-range: high)",
9
+ ).matches;
10
+ const colorGamutMQ: boolean =
11
+ window.matchMedia("(color-gamut: rec2020)").matches ||
12
+ window.matchMedia("(color-gamut: p3)").matches;
13
+ if (colorGamutMQ && dynamicRangeHighMQ) {
14
+ if (bitsPerChannel !== Math.round(bitsPerChannel)) {
15
+ // iOS bug
16
+ return false;
17
+ } else if (hdrSupported) {
18
+ return true;
19
+ } else {
20
+ return false;
21
+ }
22
+ }
23
+ return false;
24
+ } catch (e) {
25
+ /* eslint-disable no-console */
26
+ console.error("Bad window.screen test", e);
27
+ /* eslint-enable */
28
+ return false;
29
+ }
30
+ }
31
+
32
+ export function checkHDRCanvas(): boolean {
33
+ const colorSpace: string = "rec2100-pq";
34
+
35
+ try {
36
+ const canvas: HTMLCanvasElement = document.createElement("canvas");
37
+ if (!canvas.getContext) {
38
+ return false;
39
+ }
40
+ const ctx: CanvasRenderingContext2D | null = <CanvasRenderingContext2D>(
41
+ canvas.getContext("2d", {
42
+ colorSpace: colorSpace,
43
+ pixelFormat: "float16",
44
+ })
45
+ );
46
+ //canvas.drawingBufferColorSpace = colorSpace;
47
+ //canvas.unpackColorSpace = colorSpace;
48
+ if (ctx === null) {
49
+ return false;
50
+ }
51
+ return true;
52
+ } catch (e) {
53
+ /* eslint-disable no-console */
54
+ console.error("Bad canvas ColorSpace test", e);
55
+ /* eslint-enable */
56
+ return false;
57
+ }
58
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { Uint16Image } from "./Uint16Image";
2
+ export { checkHDR, checkHDRCanvas } from "./hdr-check";
3
+ export { initHDRCanvas } from "./hdr-canvas";
@@ -0,0 +1,87 @@
1
+ import WebGPUBackend from 'three/addons/renderers/webgpu/WebGPUBackend.js';
2
+ import { GPUFeatureName, GPUTextureFormat, GPUTextureUsage } from 'three/addons/renderers/webgpu/utils/WebGPUConstants.js';
3
+
4
+ class HDRWebGPUBackend extends WebGPUBackend {
5
+
6
+ // See https://github.com/mrdoob/three.js/blob/master/examples/jsm/renderers/webgpu/WebGPUBackend.js#L123
7
+ async init( renderer ) {
8
+
9
+ await super.init( renderer );
10
+
11
+ //
12
+
13
+ const parameters = this.parameters;
14
+
15
+ // create the device if it is not passed with parameters
16
+
17
+ let device;
18
+
19
+ if ( parameters.device === undefined ) {
20
+
21
+ const adapterOptions = {
22
+ powerPreference: parameters.powerPreference
23
+ };
24
+
25
+ const adapter = await navigator.gpu.requestAdapter( adapterOptions );
26
+
27
+ if ( adapter === null ) {
28
+
29
+ throw new Error( 'WebGPUBackend: Unable to create WebGPU adapter.' );
30
+
31
+ }
32
+
33
+ // feature support
34
+
35
+ const features = Object.values( GPUFeatureName );
36
+
37
+ const supportedFeatures = [];
38
+
39
+ for ( const name of features ) {
40
+
41
+ if ( adapter.features.has( name ) ) {
42
+
43
+ supportedFeatures.push( name );
44
+
45
+ }
46
+
47
+ }
48
+
49
+ const deviceDescriptor = {
50
+ requiredFeatures: supportedFeatures,
51
+ requiredLimits: parameters.requiredLimits
52
+ };
53
+
54
+ device = await adapter.requestDevice( deviceDescriptor );
55
+
56
+ } else {
57
+
58
+ device = parameters.device;
59
+
60
+ }
61
+
62
+ const context = ( parameters.context !== undefined ) ? parameters.context : renderer.domElement.getContext( 'webgpu' );
63
+
64
+ this.device = device;
65
+ this.context = context;
66
+
67
+ const alphaMode = parameters.alpha ? 'premultiplied' : 'opaque';
68
+
69
+ // See https://github.com/ccameron-chromium/webgpu-hdr/blob/main/EXPLAINER.md#example-use
70
+ this.context.configure( {
71
+ device: this.device,
72
+ format: GPUTextureFormat.BGRA8Unorm,
73
+ //format: GPUTextureFormat.RGBA16Float,
74
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
75
+ alphaMode: alphaMode,
76
+ //colorSpace: "display-p3",
77
+ colorSpace: "rec2100-hlg",
78
+ colorMetadata: { mode:"extended" }
79
+ } );
80
+
81
+ this.updateSize();
82
+
83
+ }
84
+
85
+ }
86
+
87
+ export default HDRWebGPUBackend;
@@ -0,0 +1,40 @@
1
+ import WebGPU from 'three/addons/capabilities/WebGPU.js';
2
+
3
+ import Renderer from 'three/addons/renderers/common/Renderer.js';
4
+ import WebGLBackend from 'three/addons/renderers/webgl/WebGLBackend.js';
5
+ import HDRWebGPUBackend from './HDRWebGPUBackend.js';
6
+
7
+ class HDRWebGPURenderer extends Renderer {
8
+ constructor( parameters = {} ) {
9
+
10
+ let BackendClass;
11
+
12
+ if ( parameters.forceWebGL ) {
13
+
14
+ BackendClass = WebGLBackend;
15
+
16
+ } else if ( WebGPU.isAvailable() ) {
17
+
18
+ BackendClass = HDRWebGPUBackend;
19
+
20
+ } else {
21
+
22
+ BackendClass = WebGLBackend;
23
+
24
+ /* eslint-disable no-console */
25
+ console.warn( 'THREE.WebGPURenderer: WebGPU is not available, running under WebGL2 backend.' );
26
+ /* eslint-enable no-console */
27
+
28
+ }
29
+
30
+ const backend = new BackendClass( parameters );
31
+
32
+ super( backend, parameters );
33
+
34
+ this.isWebGPURenderer = true;
35
+
36
+ }
37
+
38
+ }
39
+
40
+ export default HDRWebGPURenderer;