hdr-canvas 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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;