hdrify-react 1.4.0 → 2.2.0
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/README.md +12 -16
- package/dist/HdrifyCanvas.d.ts +31 -4
- package/dist/HdrifyCanvas.d.ts.map +1 -1
- package/dist/HdrifyCanvas.js +198 -14
- package/dist/HdrifyCanvas.js.map +1 -1
- package/dist/ImageViewer.d.ts +16 -5
- package/dist/ImageViewer.d.ts.map +1 -1
- package/dist/ImageViewer.js +53 -11
- package/dist/ImageViewer.js.map +1 -1
- package/dist/capture.d.ts +4 -3
- package/dist/capture.d.ts.map +1 -1
- package/dist/capture.js +14 -2
- package/dist/capture.js.map +1 -1
- package/dist/decode.d.ts +1 -1
- package/dist/decode.d.ts.map +1 -1
- package/dist/decode.js +5 -3
- package/dist/decode.js.map +1 -1
- package/dist/display.d.ts +17 -0
- package/dist/display.d.ts.map +1 -0
- package/dist/display.js +37 -0
- package/dist/display.js.map +1 -0
- package/dist/element.d.ts +2 -2
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +9 -6
- package/dist/element.js.map +1 -1
- package/dist/image-viewer.js +27 -12
- package/dist/image-viewer.js.map +4 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/webgpuDisplay.d.ts +4 -0
- package/dist/webgpuDisplay.d.ts.map +1 -0
- package/dist/webgpuDisplay.js +208 -0
- package/dist/webgpuDisplay.js.map +1 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -24,13 +24,11 @@
|
|
|
24
24
|
- 🖼️ Works With hdrify Readers
|
|
25
25
|
- 🔷 Typed Props
|
|
26
26
|
|
|
27
|
-
React component for displaying HDR images from [hdrify](https://www.npmjs.com/package/hdrify). Accepts `HdrifyImage` (returned by `readHdr`, `readExr`, `readJpegGainMap`) and renders with configurable tone mapping and exposure.
|
|
27
|
+
React component for displaying HDR images from [hdrify](https://www.npmjs.com/package/hdrify). Accepts `HdrifyImage` (returned by `readHdr`, `readExr`, `readJxl`, `readJpegGainMap`) and renders with configurable tone mapping and exposure.
|
|
28
28
|
|
|
29
29
|
**Related packages:** [hdrify](https://www.npmjs.com/package/hdrify) (core library) · [hdrify-cli](https://www.npmjs.com/package/hdrify-cli) (CLI tool)
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
[Read the Docs to Learn More](https://hdrify.ben3d.ca/).
|
|
31
|
+
Try the **[online demo](https://hdrify.ben3d.ca/demo/)** — HDR, EXR, and Ultra HDR viewer and converter built with this component; try it in your browser.
|
|
34
32
|
|
|
35
33
|
## Installation
|
|
36
34
|
|
|
@@ -46,12 +44,14 @@ import { readExr } from 'hdrify';
|
|
|
46
44
|
|
|
47
45
|
const hdrifyImage: HdrifyImage = readExr(buffer);
|
|
48
46
|
|
|
49
|
-
<HdrifyCanvas hdrifyImage={hdrifyImage}
|
|
47
|
+
<HdrifyCanvas hdrifyImage={hdrifyImage} exposure={1.0} className="max-w-full" />;
|
|
50
48
|
```
|
|
51
49
|
|
|
50
|
+
See [`HdrifyCanvas`](https://hdrify.ben3d.ca/docs/api/react/functions/HdrifyCanvas/) and [`HdrifyCanvasProps`](https://hdrify.ben3d.ca/docs/api/react/interfaces/HdrifyCanvasProps/) for rendering behavior and props.
|
|
51
|
+
|
|
52
52
|
### Image viewer
|
|
53
53
|
|
|
54
|
-
`ImageViewer` loads an EXR, HDR or gain map JPEG (detected from its content, gzip allowed) and adds an options panel and an optional vertical exposure slider:
|
|
54
|
+
`ImageViewer` loads an EXR, HDR, JPEG XL or gain map JPEG (detected from its content, gzip allowed) and adds an options panel and an optional vertical exposure slider:
|
|
55
55
|
|
|
56
56
|
```tsx
|
|
57
57
|
import { ImageViewer } from 'hdrify-react';
|
|
@@ -66,19 +66,13 @@ The same viewer is available as a web component for plain HTML and other framewo
|
|
|
66
66
|
<image-viewer src="/images/memorial.exr" exposure-control></image-viewer>
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
API reference: [`ImageViewer`](https://hdrify.ben3d.ca/docs/api/react/functions/ImageViewer/) · [`ImageViewerProps`](https://hdrify.ben3d.ca/docs/api/react/interfaces/ImageViewerProps/) · [`ImageViewerHandle`](https://hdrify.ben3d.ca/docs/api/react/interfaces/ImageViewerHandle/).
|
|
70
70
|
|
|
71
|
-
With a bundler, `import 'hdrify-react/element'`
|
|
71
|
+
With a bundler, `import 'hdrify-react/element'` to register the web component.
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
## Documentation
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
| -------------- | -------------------------------------------- | ----------------------------------------------------- |
|
|
77
|
-
| `hdrifyImage` | `HdrifyImage` | Image from `readHdr`, `readExr`, or `readJpegGainMap` |
|
|
78
|
-
| `toneMapping` | `'aces' \| 'reinhard' \| 'neutral' \| 'agx'` | Tone mapper to use |
|
|
79
|
-
| `exposure` | `number` | Exposure value (1.0 = nominal) |
|
|
80
|
-
| `className` | `string` | Optional CSS class for the canvas |
|
|
81
|
-
| `forwardedRef` | `RefObject<HTMLCanvasElement \| null>` | Optional ref for canvas access (e.g. `toBlob`) |
|
|
75
|
+
Full documentation for the React components is available [on the HDRify documentation website](https://hdrify.ben3d.ca/docs/react/). See the [React guide](https://hdrify.ben3d.ca/docs/react/) and [`<image-viewer>` documentation](https://hdrify.ben3d.ca/docs/image-viewer/) for loading, capture, attributes, events, and theming.
|
|
82
76
|
|
|
83
77
|
## Contribution
|
|
84
78
|
|
|
@@ -91,3 +85,5 @@ MIT
|
|
|
91
85
|
## Author
|
|
92
86
|
|
|
93
87
|
[Ben Houston](https://ben3d.ca), Sponsored by [Land of Assets](https://landofassets.com).
|
|
88
|
+
|
|
89
|
+
Compare browser behavior at [hdr-capabilities.ben3d.ca](https://hdr-capabilities.ben3d.ca), maintained in the separate [hdr-capabilities repository](https://github.com/bhouston/hdr-capabilities).
|
package/dist/HdrifyCanvas.d.ts
CHANGED
|
@@ -1,12 +1,39 @@
|
|
|
1
|
-
import type { HdrifyImage
|
|
1
|
+
import type { HdrifyImage } from 'hdrify';
|
|
2
2
|
import type * as React from 'react';
|
|
3
|
+
import { type DisplayMode, type ViewerToneMapping } from './display.js';
|
|
3
4
|
export interface HdrifyCanvasProps {
|
|
5
|
+
/** Decoded floating-point RGBA image from a hdrify reader. */
|
|
4
6
|
hdrifyImage: HdrifyImage;
|
|
5
|
-
|
|
7
|
+
/** Omit to use None on HDR displays and Neutral on SDR displays. */
|
|
8
|
+
toneMapping?: ViewerToneMapping;
|
|
9
|
+
/** Linear exposure multiplier: 1 is nominal, 2 adds one stop. */
|
|
6
10
|
exposure: number;
|
|
11
|
+
/** CSS class applied to the canvas wrapper. */
|
|
7
12
|
className?: string;
|
|
8
|
-
/**
|
|
13
|
+
/** Ref to the active canvas, updated when HDR/SDR fallback replaces it. */
|
|
9
14
|
forwardedRef?: React.RefObject<HTMLCanvasElement | null>;
|
|
15
|
+
/** Called after the current image has finished rendering. */
|
|
16
|
+
onDraw?: (image: HdrifyImage, mode: DisplayMode) => void;
|
|
17
|
+
onError?: (error: Error) => void;
|
|
10
18
|
}
|
|
11
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Display a decoded HDR image with exposure and tone mapping.
|
|
21
|
+
* Uses native WebGPU only when an HDR-capable display and extended `rgba16float`
|
|
22
|
+
* presentation succeed. The capability probe caches success or failure per page;
|
|
23
|
+
* each mounted canvas owns its rendering resources. Otherwise a 2D SDR canvas is used.
|
|
24
|
+
* GPU resources are disposed on unmount; device loss falls back to SDR.
|
|
25
|
+
*
|
|
26
|
+
* Omitting `toneMapping` selects None for HDR and Neutral for SDR. Explicit `none`
|
|
27
|
+
* also falls back to Neutral on SDR; other explicit mappers are preserved.
|
|
28
|
+
* Display size follows the viewport and device pixel ratio. Pass a new image object
|
|
29
|
+
* after editing pixels to trigger a redraw. `onDraw` reports a completed render.
|
|
30
|
+
* Use {@link captureImage} for portable SDR export rather than serializing the GPU canvas.
|
|
31
|
+
*
|
|
32
|
+
* @param props - Image, display settings, and rendering callbacks.
|
|
33
|
+
* @example
|
|
34
|
+
* ```tsx
|
|
35
|
+
* <HdrifyCanvas hdrifyImage={image} exposure={1} toneMapping="agx" />
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function HdrifyCanvas(props: HdrifyCanvasProps): React.ReactElement;
|
|
12
39
|
//# sourceMappingURL=HdrifyCanvas.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HdrifyCanvas.d.ts","sources":["../src/HdrifyCanvas.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,
|
|
1
|
+
{"version":3,"file":"HdrifyCanvas.d.ts","sources":["../src/HdrifyCanvas.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAC1C,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AAGpC,OAAO,EAAsB,KAAK,WAAW,EAAqB,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAE/G,MAAM,WAAW,iBAAiB;IAChC,8DAA8D;IAC9D,WAAW,EAAE,WAAW,CAAC;IACzB,oEAAoE;IACpE,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,iEAAiE;IACjE,QAAQ,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IACzD,6DAA6D;IAC7D,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC;IACzD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAKD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,KAAK,CAAC,YAAY,CAiLzE"}
|
package/dist/HdrifyCanvas.js
CHANGED
|
@@ -1,19 +1,203 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
3
|
import { drawToneMapped } from './capture.js';
|
|
4
|
-
|
|
4
|
+
import { resolveToneMapping } from './display.js';
|
|
5
|
+
const CSS = '.hdrify-canvas{display:grid;place-items:center;width:var(--hdrify-image-width);height:auto;aspect-ratio:var(--hdrify-image-aspect);min-height:0;flex:none;margin:auto;min-width:0}.hdrify-canvas>canvas{display:block;dynamic-range-limit:no-limit}';
|
|
6
|
+
/**
|
|
7
|
+
* Display a decoded HDR image with exposure and tone mapping.
|
|
8
|
+
* Uses native WebGPU only when an HDR-capable display and extended `rgba16float`
|
|
9
|
+
* presentation succeed. The capability probe caches success or failure per page;
|
|
10
|
+
* each mounted canvas owns its rendering resources. Otherwise a 2D SDR canvas is used.
|
|
11
|
+
* GPU resources are disposed on unmount; device loss falls back to SDR.
|
|
12
|
+
*
|
|
13
|
+
* Omitting `toneMapping` selects None for HDR and Neutral for SDR. Explicit `none`
|
|
14
|
+
* also falls back to Neutral on SDR; other explicit mappers are preserved.
|
|
15
|
+
* Display size follows the viewport and device pixel ratio. Pass a new image object
|
|
16
|
+
* after editing pixels to trigger a redraw. `onDraw` reports a completed render.
|
|
17
|
+
* Use {@link captureImage} for portable SDR export rather than serializing the GPU canvas.
|
|
18
|
+
*
|
|
19
|
+
* @param props - Image, display settings, and rendering callbacks.
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* <HdrifyCanvas hdrifyImage={image} exposure={1} toneMapping="agx" />
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function HdrifyCanvas(props) {
|
|
26
|
+
const hostRef = useRef(null);
|
|
5
27
|
const canvasRef = useRef(null);
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
28
|
+
const latest = useRef(props);
|
|
29
|
+
const redraw = useRef(() => { });
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
latest.current = props;
|
|
32
|
+
});
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
redraw.current();
|
|
35
|
+
}, [props.hdrifyImage, props.toneMapping, props.exposure]);
|
|
13
36
|
useEffect(() => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
37
|
+
const forwardedRef = props.forwardedRef;
|
|
38
|
+
if (!forwardedRef)
|
|
39
|
+
return undefined;
|
|
40
|
+
// oxlint-disable-next-line react/immutability -- forward the active canvas into the caller-owned ref
|
|
41
|
+
forwardedRef.current = canvasRef.current;
|
|
42
|
+
return () => {
|
|
43
|
+
// oxlint-disable-next-line react/immutability -- clear the caller-owned ref on replacement or unmount
|
|
44
|
+
forwardedRef.current = null;
|
|
45
|
+
};
|
|
46
|
+
}, [props.forwardedRef]);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const host = hostRef.current;
|
|
49
|
+
const view = host.ownerDocument.defaultView;
|
|
50
|
+
const media = view.matchMedia('(dynamic-range: high)');
|
|
51
|
+
let active;
|
|
52
|
+
let canvas;
|
|
53
|
+
let disposed = false;
|
|
54
|
+
let generation = 0;
|
|
55
|
+
let drawRevision = 0;
|
|
56
|
+
let fallbackDrawing = '';
|
|
57
|
+
let fallbackImage;
|
|
58
|
+
const mount = (next) => {
|
|
59
|
+
canvas = next;
|
|
60
|
+
canvasRef.current = canvas;
|
|
61
|
+
// oxlint-disable-next-line react/immutability -- forwarding the internally created canvas
|
|
62
|
+
if (latest.current.forwardedRef)
|
|
63
|
+
latest.current.forwardedRef.current = canvas;
|
|
64
|
+
host.replaceChildren(canvas);
|
|
65
|
+
};
|
|
66
|
+
const draw = () => {
|
|
67
|
+
if (disposed || !active)
|
|
68
|
+
return;
|
|
69
|
+
const display = active;
|
|
70
|
+
const { hdrifyImage: image, toneMapping, exposure, onDraw, onError } = latest.current;
|
|
71
|
+
const mapper = resolveToneMapping(toneMapping, display.mode);
|
|
72
|
+
const revision = ++drawRevision;
|
|
73
|
+
try {
|
|
74
|
+
const scale = Math.min((host.clientWidth || image.width) / image.width, (host.clientHeight || image.height) / image.height);
|
|
75
|
+
const width = Math.max(1, image.width * scale);
|
|
76
|
+
const height = Math.max(1, image.height * scale);
|
|
77
|
+
canvas.style.width = `${width}px`;
|
|
78
|
+
canvas.style.height = `${height}px`;
|
|
79
|
+
display.resize(width, height, view.devicePixelRatio || 1);
|
|
80
|
+
display.draw(image, mapper, exposure);
|
|
81
|
+
canvas.dataset.displayMode = display.mode;
|
|
82
|
+
canvas.dataset.toneMapping = mapper;
|
|
83
|
+
void display
|
|
84
|
+
.complete()
|
|
85
|
+
.then(() => {
|
|
86
|
+
if (!disposed && revision === drawRevision && active === display)
|
|
87
|
+
onDraw?.(image, display.mode);
|
|
88
|
+
})
|
|
89
|
+
.catch((error) => {
|
|
90
|
+
if (!disposed && active === display) {
|
|
91
|
+
if (display.mode === 'hdr') {
|
|
92
|
+
fallback();
|
|
93
|
+
}
|
|
94
|
+
else
|
|
95
|
+
onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (display.mode === 'hdr') {
|
|
101
|
+
fallback();
|
|
102
|
+
}
|
|
103
|
+
else
|
|
104
|
+
onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const fallback = () => {
|
|
108
|
+
if (disposed)
|
|
109
|
+
return;
|
|
110
|
+
++generation;
|
|
111
|
+
active?.dispose();
|
|
112
|
+
mount(host.ownerDocument.createElement('canvas'));
|
|
113
|
+
fallbackImage = undefined;
|
|
114
|
+
fallbackDrawing = '';
|
|
115
|
+
active = {
|
|
116
|
+
mode: 'sdr',
|
|
117
|
+
draw(image, mapper, exposure) {
|
|
118
|
+
const key = `${mapper}:${exposure}`;
|
|
119
|
+
if (fallbackImage === image && fallbackDrawing === key)
|
|
120
|
+
return;
|
|
121
|
+
drawToneMapped(canvas, image, mapper, exposure);
|
|
122
|
+
fallbackImage = image;
|
|
123
|
+
fallbackDrawing = key;
|
|
124
|
+
},
|
|
125
|
+
resize() { },
|
|
126
|
+
complete: () => Promise.resolve(),
|
|
127
|
+
dispose() { },
|
|
128
|
+
};
|
|
129
|
+
draw();
|
|
130
|
+
};
|
|
131
|
+
const select = async () => {
|
|
132
|
+
const request = ++generation;
|
|
133
|
+
if (!media.matches || !view.navigator.gpu) {
|
|
134
|
+
fallback();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
let next;
|
|
138
|
+
try {
|
|
139
|
+
const { createWebGPUDisplay } = await import('./webgpuDisplay.js');
|
|
140
|
+
if (disposed || request !== generation)
|
|
141
|
+
return;
|
|
142
|
+
const candidate = host.ownerDocument.createElement('canvas');
|
|
143
|
+
next = await createWebGPUDisplay(candidate, () => {
|
|
144
|
+
if (!disposed && request === generation)
|
|
145
|
+
fallback();
|
|
146
|
+
});
|
|
147
|
+
if (disposed || request !== generation) {
|
|
148
|
+
next.dispose();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
active?.dispose();
|
|
152
|
+
active = next;
|
|
153
|
+
mount(candidate);
|
|
154
|
+
draw();
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
next?.dispose();
|
|
158
|
+
if (!disposed && request === generation)
|
|
159
|
+
fallback();
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
redraw.current = draw;
|
|
163
|
+
const observer = new ResizeObserver(draw);
|
|
164
|
+
try {
|
|
165
|
+
observer.observe(host, { box: 'device-pixel-content-box' });
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
observer.observe(host);
|
|
169
|
+
}
|
|
170
|
+
const onMediaChange = () => {
|
|
171
|
+
void select();
|
|
172
|
+
};
|
|
173
|
+
media.addEventListener('change', onMediaChange);
|
|
174
|
+
view.addEventListener('resize', draw);
|
|
175
|
+
let resolution = view.matchMedia(`(resolution: ${view.devicePixelRatio}dppx)`);
|
|
176
|
+
const onResolutionChange = () => {
|
|
177
|
+
resolution.removeEventListener('change', onResolutionChange);
|
|
178
|
+
resolution = view.matchMedia(`(resolution: ${view.devicePixelRatio}dppx)`);
|
|
179
|
+
resolution.addEventListener('change', onResolutionChange);
|
|
180
|
+
draw();
|
|
181
|
+
};
|
|
182
|
+
resolution.addEventListener('change', onResolutionChange);
|
|
183
|
+
void select();
|
|
184
|
+
return () => {
|
|
185
|
+
disposed = true;
|
|
186
|
+
++generation;
|
|
187
|
+
++drawRevision;
|
|
188
|
+
observer.disconnect();
|
|
189
|
+
media.removeEventListener('change', onMediaChange);
|
|
190
|
+
view.removeEventListener('resize', draw);
|
|
191
|
+
resolution.removeEventListener('change', onResolutionChange);
|
|
192
|
+
active?.dispose();
|
|
193
|
+
canvasRef.current = null;
|
|
194
|
+
redraw.current = () => { };
|
|
195
|
+
host.replaceChildren();
|
|
196
|
+
};
|
|
197
|
+
}, []);
|
|
198
|
+
return (_jsxs(_Fragment, { children: [_jsx("style", { children: CSS }), _jsx("div", { ref: hostRef, className: `hdrify-canvas${props.className ? ` ${props.className}` : ''}`, style: {
|
|
199
|
+
'--hdrify-image-width': `${props.hdrifyImage.width}px`,
|
|
200
|
+
'--hdrify-image-aspect': `${props.hdrifyImage.width}/${props.hdrifyImage.height}`,
|
|
201
|
+
} })] }));
|
|
18
202
|
}
|
|
19
203
|
//# sourceMappingURL=HdrifyCanvas.js.map
|
package/dist/HdrifyCanvas.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HdrifyCanvas.js","sources":["../src/HdrifyCanvas.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"HdrifyCanvas.js","sources":["../src/HdrifyCanvas.tsx"],"names":[],"mappings":"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"}
|
package/dist/ImageViewer.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { HdrifyImage
|
|
1
|
+
import type { HdrifyImage } from 'hdrify';
|
|
2
|
+
import { type ViewerToneMapping } from './display.js';
|
|
2
3
|
import { type CSSProperties, type Ref } from 'react';
|
|
3
4
|
import { type ImageViewerCaptureOptions } from './capture.js';
|
|
4
5
|
import { type ImageViewerProgress } from './progress.js';
|
|
@@ -8,14 +9,15 @@ export type SettingsPanelMode = 'open' | 'closed' | 'hidden';
|
|
|
8
9
|
export type ImageFit = 'contain' | 'actual';
|
|
9
10
|
/** User-adjustable display settings. */
|
|
10
11
|
export interface ImageViewerSettings {
|
|
11
|
-
|
|
12
|
+
/** Omit for None on HDR displays and Neutral on SDR; explicit None falls back to Neutral on SDR. */
|
|
13
|
+
toneMapping?: ViewerToneMapping;
|
|
12
14
|
/** Exposure in stops (EV): `0` is unchanged, `+1` doubles the brightness. */
|
|
13
15
|
exposure: number;
|
|
14
16
|
fit: ImageFit;
|
|
15
17
|
}
|
|
16
18
|
export declare const DEFAULT_IMAGE_VIEWER_SETTINGS: Readonly<ImageViewerSettings>;
|
|
17
19
|
export declare const TONE_MAPPING_OPTIONS: readonly {
|
|
18
|
-
value:
|
|
20
|
+
value: ViewerToneMapping;
|
|
19
21
|
label: string;
|
|
20
22
|
}[];
|
|
21
23
|
/** Range of the exposure slider, in stops. The options panel accepts any value. */
|
|
@@ -30,12 +32,16 @@ export interface ImageViewerHandle {
|
|
|
30
32
|
* it waits for the new source instead. Rejects with the load's error, or if the component unmounts.
|
|
31
33
|
*/
|
|
32
34
|
whenReady(): Promise<void>;
|
|
33
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Wait for {@link whenReady}, including the first completed render, then export
|
|
37
|
+
* an SDR PNG/JPEG/WebP without viewer controls. Capture is independent of display
|
|
38
|
+
* output: an omitted mapper uses Neutral; explicit `none` clips above-white values.
|
|
39
|
+
*/
|
|
34
40
|
toBlob(options?: ImageViewerCaptureOptions): Promise<Blob>;
|
|
35
41
|
}
|
|
36
42
|
/** Props for {@link ImageViewer}. Settings props override the user's choice whenever they change. */
|
|
37
43
|
export interface ImageViewerProps extends Partial<ImageViewerSettings> {
|
|
38
|
-
/** URL of an EXR, HDR or gain map JPEG (optionally gzipped). Fetching is cancelled when it changes. */
|
|
44
|
+
/** URL of an EXR, HDR, JPEG XL or gain map JPEG (optionally gzipped). Fetching is cancelled when it changes. */
|
|
39
45
|
src?: string;
|
|
40
46
|
/** Encoded file bytes; takes precedence over `src`. */
|
|
41
47
|
data?: ArrayBuffer | Uint8Array;
|
|
@@ -65,6 +71,11 @@ export interface ImageViewerProps extends Partial<ImageViewerSettings> {
|
|
|
65
71
|
/**
|
|
66
72
|
* Loads and displays an HDR image with tone mapping, a loading bar, an optional options panel and an optional
|
|
67
73
|
* exposure slider. Accepts every format hdrify reads; see {@link decodeHdrifyImage}.
|
|
74
|
+
* Uses {@link HdrifyCanvas} for HDR presentation and SDR fallback. None is offered
|
|
75
|
+
* and selected by default only after extended FP16 presentation succeeds; otherwise
|
|
76
|
+
* Neutral is the default and None is hidden. There is no Automatic menu item.
|
|
77
|
+
* `fit="actual"` shows one source pixel per CSS pixel. Loading progress is reported
|
|
78
|
+
* through `onProgress`; the imperative handle exposes readiness and SDR capture.
|
|
68
79
|
*
|
|
69
80
|
* @example
|
|
70
81
|
* ```tsx
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImageViewer.d.ts","sources":["../src/ImageViewer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,
|
|
1
|
+
{"version":3,"file":"ImageViewer.d.ts","sources":["../src/ImageViewer.tsx"],"names":[],"mappings":"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"}
|
package/dist/ImageViewer.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { resolveToneMapping } from './display.js';
|
|
2
3
|
import { useEffect, useId, useImperativeHandle, useRef, useState } from 'react';
|
|
3
4
|
import { captureImage } from './capture.js';
|
|
4
5
|
import { decodeHdrifyImage } from './decode.js';
|
|
@@ -7,11 +8,12 @@ import { readResponse } from './progress.js';
|
|
|
7
8
|
import { ViewerReadiness } from './readiness.js';
|
|
8
9
|
import { IMAGE_VIEWER_CSS } from './styles.js';
|
|
9
10
|
export const DEFAULT_IMAGE_VIEWER_SETTINGS = {
|
|
10
|
-
toneMapping:
|
|
11
|
+
toneMapping: undefined,
|
|
11
12
|
exposure: 0,
|
|
12
13
|
fit: 'contain',
|
|
13
14
|
};
|
|
14
15
|
export const TONE_MAPPING_OPTIONS = [
|
|
16
|
+
{ value: 'none', label: 'None' },
|
|
15
17
|
{ value: 'neutral', label: 'Neutral' },
|
|
16
18
|
{ value: 'agx', label: 'AgX' },
|
|
17
19
|
{ value: 'aces', label: 'ACES' },
|
|
@@ -34,6 +36,11 @@ let loadCount = 0;
|
|
|
34
36
|
/**
|
|
35
37
|
* Loads and displays an HDR image with tone mapping, a loading bar, an optional options panel and an optional
|
|
36
38
|
* exposure slider. Accepts every format hdrify reads; see {@link decodeHdrifyImage}.
|
|
39
|
+
* Uses {@link HdrifyCanvas} for HDR presentation and SDR fallback. None is offered
|
|
40
|
+
* and selected by default only after extended FP16 presentation succeeds; otherwise
|
|
41
|
+
* Neutral is the default and None is hidden. There is no Automatic menu item.
|
|
42
|
+
* `fit="actual"` shows one source pixel per CSS pixel. Loading progress is reported
|
|
43
|
+
* through `onProgress`; the imperative handle exposes readiness and SDR capture.
|
|
37
44
|
*
|
|
38
45
|
* @example
|
|
39
46
|
* ```tsx
|
|
@@ -57,7 +64,11 @@ export function ImageViewer({ src, data, image, toneMapping, exposure, fit, sett
|
|
|
57
64
|
provided.fit !== fit ||
|
|
58
65
|
provided.settingsPanel !== settingsPanel) {
|
|
59
66
|
setProvided({ toneMapping, exposure, fit, settingsPanel });
|
|
60
|
-
setSettings((current) => ({
|
|
67
|
+
setSettings((current) => ({
|
|
68
|
+
...current,
|
|
69
|
+
...definedSettings({ exposure, fit }),
|
|
70
|
+
...(provided.toneMapping !== toneMapping ? { toneMapping } : {}),
|
|
71
|
+
}));
|
|
61
72
|
if (provided.settingsPanel !== settingsPanel)
|
|
62
73
|
setPanelMode(settingsPanel);
|
|
63
74
|
}
|
|
@@ -71,6 +82,9 @@ export function ImageViewer({ src, data, image, toneMapping, exposure, fit, sett
|
|
|
71
82
|
callbacks.current.onSettingsPanelChange?.(mode);
|
|
72
83
|
};
|
|
73
84
|
const [readiness] = useState(() => new ViewerReadiness());
|
|
85
|
+
const [rendered, setRendered] = useState();
|
|
86
|
+
const [renderError, setRenderError] = useState();
|
|
87
|
+
const announced = useRef(undefined);
|
|
74
88
|
useEffect(() => () => readiness.disconnect(), [readiness]);
|
|
75
89
|
// The latest fetch/decode result, tagged with the source it came from; the rest is derived during render.
|
|
76
90
|
const source = image ? undefined : (data ?? src);
|
|
@@ -80,6 +94,7 @@ export function ImageViewer({ src, data, image, toneMapping, exposure, fit, sett
|
|
|
80
94
|
const download = useRef(NO_DOWNLOAD);
|
|
81
95
|
useEffect(() => {
|
|
82
96
|
readiness.pending();
|
|
97
|
+
announced.current = undefined;
|
|
83
98
|
download.current = NO_DOWNLOAD;
|
|
84
99
|
if (image)
|
|
85
100
|
return; // Drawn below.
|
|
@@ -131,20 +146,41 @@ export function ImageViewer({ src, data, image, toneMapping, exposure, fit, sett
|
|
|
131
146
|
// While a new source loads, keep showing the previous image.
|
|
132
147
|
const shownImage = image ?? (source === undefined ? undefined : loaded.image);
|
|
133
148
|
const error = !loading && source !== undefined ? loaded.error : undefined;
|
|
134
|
-
|
|
135
|
-
// HdrifyCanvas (a child) draws in its effect, which runs before this one: the new image is on screen here.
|
|
149
|
+
// Readiness follows the asynchronous renderer, including its first completed GPU draw.
|
|
136
150
|
const drawn = loading ? undefined : shownImage;
|
|
151
|
+
const displayMode = rendered?.mode ?? 'sdr';
|
|
152
|
+
const renderReady = !!drawn &&
|
|
153
|
+
rendered?.image === drawn &&
|
|
154
|
+
rendered.exposure === settings.exposure &&
|
|
155
|
+
rendered.toneMapping === settings.toneMapping;
|
|
156
|
+
const barValue = bar && !loading && renderReady ? 1 : bar?.value;
|
|
157
|
+
if (!shownImage && rendered)
|
|
158
|
+
setRendered(undefined);
|
|
137
159
|
const latest = useRef({ image: drawn, settings });
|
|
138
160
|
useEffect(() => {
|
|
139
161
|
latest.current = { image: drawn, settings };
|
|
140
162
|
});
|
|
163
|
+
const handleDraw = (current, mode) => {
|
|
164
|
+
setRenderError(undefined);
|
|
165
|
+
if (mode === 'sdr' && settings.toneMapping === 'none')
|
|
166
|
+
change({ toneMapping: 'neutral' });
|
|
167
|
+
if (current === drawn)
|
|
168
|
+
setBar((currentBar) => (currentBar && currentBar.value < 1 ? { ...currentBar, value: 1 } : currentBar));
|
|
169
|
+
setRendered((previous) => previous?.image === current &&
|
|
170
|
+
previous.mode === mode &&
|
|
171
|
+
previous.exposure === settings.exposure &&
|
|
172
|
+
previous.toneMapping === settings.toneMapping
|
|
173
|
+
? previous
|
|
174
|
+
: { image: current, mode, exposure: settings.exposure, toneMapping: settings.toneMapping });
|
|
175
|
+
};
|
|
141
176
|
useEffect(() => {
|
|
142
|
-
if (!drawn)
|
|
177
|
+
if (!drawn || !renderReady || announced.current === drawn)
|
|
143
178
|
return;
|
|
179
|
+
announced.current = drawn;
|
|
144
180
|
readiness.finish();
|
|
145
181
|
callbacks.current.onProgress?.({ ...download.current, totalProgress: 1, stage: 'render' });
|
|
146
182
|
callbacks.current.onLoad?.(drawn);
|
|
147
|
-
}, [drawn, readiness]);
|
|
183
|
+
}, [drawn, renderReady, readiness]);
|
|
148
184
|
useImperativeHandle(ref, () => ({
|
|
149
185
|
whenReady: () => readiness.whenReady(),
|
|
150
186
|
toBlob: async (options) => {
|
|
@@ -152,14 +188,18 @@ export function ImageViewer({ src, data, image, toneMapping, exposure, fit, sett
|
|
|
152
188
|
const { image: current, settings: currentSettings } = latest.current;
|
|
153
189
|
if (!current)
|
|
154
190
|
throw new Error('No image is loaded.');
|
|
155
|
-
return captureImage(current, currentSettings.toneMapping, 2 ** currentSettings.exposure, options);
|
|
191
|
+
return captureImage(current, currentSettings.toneMapping ?? 'neutral', 2 ** currentSettings.exposure, options);
|
|
156
192
|
},
|
|
157
193
|
}), [readiness]);
|
|
158
|
-
return (_jsxs("div", { className: className ? `hdrify-viewer ${className}` : 'hdrify-viewer', style: { height: 480, ...style }, "data-fit": settings.fit, "aria-busy": loading, children: [_jsx("style", { children: IMAGE_VIEWER_CSS }), _jsx("div", { className: "hdrify-viewer-viewport", children: shownImage && (_jsx(HdrifyCanvas, { hdrifyImage: shownImage, toneMapping: settings.toneMapping, exposure: 2 ** settings.exposure, className: "hdrify-viewer-canvas"
|
|
194
|
+
return (_jsxs("div", { className: className ? `hdrify-viewer ${className}` : 'hdrify-viewer', style: { height: 480, ...style }, "data-fit": settings.fit, "data-display-mode": displayMode, "aria-busy": loading || (!!drawn && !renderReady), children: [_jsx("style", { children: IMAGE_VIEWER_CSS }), _jsx("div", { className: "hdrify-viewer-viewport", children: shownImage && (_jsx(HdrifyCanvas, { hdrifyImage: shownImage, toneMapping: settings.toneMapping, exposure: 2 ** settings.exposure, className: "hdrify-viewer-canvas", onDraw: handleDraw, onError: (failure) => {
|
|
195
|
+
readiness.finish(failure);
|
|
196
|
+
setRenderError(failure.message);
|
|
197
|
+
callbacks.current.onError?.(failure);
|
|
198
|
+
} })) }), _jsx("div", { className: "hdrify-viewer-progress", hidden: barValue === undefined, children: _jsx("slot", { name: "progress-bar", children: bar && barValue !== undefined && (_jsx("div", { className: barValue === 1 ? 'hdrify-viewer-progress-fill complete' : 'hdrify-viewer-progress-fill', part: "default-progress-bar",
|
|
159
199
|
// oxlint-disable-next-line jsx-a11y/prefer-tag-over-role -- a scaled fill, styleable via ::part like <material-viewer>'s
|
|
160
|
-
role: "progressbar", "aria-label": "Loading image", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(barValue * 100), "aria-hidden": barValue === 1, style: { transform: `scaleX(${Math.max(0.02, barValue)})` } }, bar.id)) }) }), error && (_jsx("p", { role: "alert", className: "hdrify-viewer-error", children: error })), exposureControl && (_jsxs("div", { className: "hdrify-viewer-exposure hdrify-viewer-surface", title: "Exposure (double-click to reset)", children: [_jsx("output", { children: formatStops(settings.exposure) }), _jsx("input", { type: "range", "aria-label": "Exposure in stops", min: EXPOSURE_SLIDER_RANGE.min, max: EXPOSURE_SLIDER_RANGE.max, step: 0.1, value: settings.exposure, onChange: (event) => change({ exposure: event.currentTarget.valueAsNumber }), onDoubleClick: () => change({ exposure: 0 }) })] })), panelMode !== 'hidden' && (_jsx(OptionsPanel, { settings: settings, image: shownImage, open: panelMode === 'open', onChange: change, onOpenChange: (open) => changePanel(open ? 'open' : 'closed') }))] }));
|
|
200
|
+
role: "progressbar", "aria-label": "Loading image", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(barValue * 100), "aria-hidden": barValue === 1, style: { transform: `scaleX(${Math.max(0.02, barValue)})` } }, bar.id)) }) }), (error || renderError) && (_jsx("p", { role: "alert", className: "hdrify-viewer-error", children: error || renderError })), exposureControl && (_jsxs("div", { className: "hdrify-viewer-exposure hdrify-viewer-surface", title: "Exposure (double-click to reset)", children: [_jsx("output", { children: formatStops(settings.exposure) }), _jsx("input", { type: "range", "aria-label": "Exposure in stops", min: EXPOSURE_SLIDER_RANGE.min, max: EXPOSURE_SLIDER_RANGE.max, step: 0.1, value: settings.exposure, onChange: (event) => change({ exposure: event.currentTarget.valueAsNumber }), onDoubleClick: () => change({ exposure: 0 }) })] })), panelMode !== 'hidden' && (_jsx(OptionsPanel, { settings: settings, image: shownImage, displayMode: displayMode, open: panelMode === 'open', onChange: change, onOpenChange: (open) => changePanel(open ? 'open' : 'closed') }))] }));
|
|
161
201
|
}
|
|
162
|
-
function OptionsPanel({ settings, image, open, onChange, onOpenChange }) {
|
|
202
|
+
function OptionsPanel({ settings, image, displayMode, open, onChange, onOpenChange }) {
|
|
163
203
|
const id = useId();
|
|
164
204
|
const button = useRef(null);
|
|
165
205
|
const panel = useRef(null);
|
|
@@ -190,7 +230,9 @@ function OptionsPanel({ settings, image, open, onChange, onOpenChange }) {
|
|
|
190
230
|
document.removeEventListener('keydown', escape);
|
|
191
231
|
};
|
|
192
232
|
}, [open]);
|
|
193
|
-
return (_jsxs(_Fragment, { children: [_jsx("button", { ref: button, type: "button", className: "hdrify-viewer-toggle hdrify-viewer-surface", "aria-label": "Viewer options", title: "Viewer options", "aria-controls": id, "aria-expanded": open, onClick: () => onOpenChange(!open), children: _jsxs("svg", { viewBox: "0 0 24 24", width: "20", height: "20", fill: "none", stroke: "currentColor", strokeWidth: "1.6", "aria-hidden": "true", children: [_jsx("path", { d: "m9 3-.8 2.2-2 .9L4 5.5 2.5 8l1.6 1.7-.2 2.3L2 13.5 3.5 16l2.3-.4 1.8 1.4.4 2.4h3l.8-2.2 2-.9 2.2.6 1.5-2.5-1.6-1.7.2-2.3L18 8.5 16.5 6l-2.3.4-1.8-1.4L12 3Z", transform: "translate(2 1)" }), _jsx("circle", { cx: "12", cy: "12", r: "3" })] }) }), _jsxs("section", { ref: panel, id: id, className: "hdrify-viewer-panel hdrify-viewer-surface", "aria-label": "Viewer options", hidden: !open, children: [_jsxs("header", { children: [_jsx("strong", { children: "Viewer options" }), _jsx("button", { type: "button", "aria-label": "Close viewer options", onClick: () => onOpenChange(false), children: "\u00D7" })] }), _jsxs("label", { children: ["Tone mapping", _jsx("select", {
|
|
233
|
+
return (_jsxs(_Fragment, { children: [_jsx("button", { ref: button, type: "button", className: "hdrify-viewer-toggle hdrify-viewer-surface", "aria-label": "Viewer options", title: "Viewer options", "aria-controls": id, "aria-expanded": open, onClick: () => onOpenChange(!open), children: _jsxs("svg", { viewBox: "0 0 24 24", width: "20", height: "20", fill: "none", stroke: "currentColor", strokeWidth: "1.6", "aria-hidden": "true", children: [_jsx("path", { d: "m9 3-.8 2.2-2 .9L4 5.5 2.5 8l1.6 1.7-.2 2.3L2 13.5 3.5 16l2.3-.4 1.8 1.4.4 2.4h3l.8-2.2 2-.9 2.2.6 1.5-2.5-1.6-1.7.2-2.3L18 8.5 16.5 6l-2.3.4-1.8-1.4L12 3Z", transform: "translate(2 1)" }), _jsx("circle", { cx: "12", cy: "12", r: "3" })] }) }), _jsxs("section", { ref: panel, id: id, className: "hdrify-viewer-panel hdrify-viewer-surface", "aria-label": "Viewer options", hidden: !open, children: [_jsxs("header", { children: [_jsx("strong", { children: "Viewer options" }), _jsx("button", { type: "button", "aria-label": "Close viewer options", onClick: () => onOpenChange(false), children: "\u00D7" })] }), _jsxs("label", { children: ["Tone mapping", _jsx("select", { "aria-label": "Tone mapping", value: resolveToneMapping(settings.toneMapping, displayMode), onChange: (event) => onChange({
|
|
234
|
+
toneMapping: event.currentTarget.value ? event.currentTarget.value : undefined,
|
|
235
|
+
}), children: TONE_MAPPING_OPTIONS.filter((option) => option.value !== 'none' || displayMode === 'hdr').map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value))) })] }), _jsxs("label", { children: ["Exposure (EV)", _jsx("input", { type: "number", step: 0.1, value: Number(settings.exposure.toFixed(2)), onChange: (event) => {
|
|
194
236
|
const value = event.currentTarget.valueAsNumber;
|
|
195
237
|
if (Number.isFinite(value))
|
|
196
238
|
onChange({ exposure: value });
|