hdr-canvas 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/dist/hdr-canvas.cjs +5918 -0
- package/dist/hdr-canvas.cjs.map +1 -0
- package/dist/hdr-canvas.d.ts +30 -0
- package/dist/hdr-canvas.js +5913 -0
- package/dist/hdr-canvas.js.map +1 -0
- package/dist/hdr-canvas.min.js +2 -0
- package/dist/hdr-canvas.min.js.map +1 -0
- package/package.json +60 -0
- package/src/@types/HDRCanvas.d.ts +25 -0
- package/src/Uint16Image.ts +156 -0
- package/src/hdr-canvas.ts +7 -0
- package/src/hdr-check.ts +58 -0
- package/src/index.ts +3 -0
- package/three/HDRWebGPUBackend.js +87 -0
- package/three/HDRWebGPURenderer.js +40 -0
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
|
+
}
|
package/src/hdr-check.ts
ADDED
@@ -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,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;
|