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 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
- An **online demo** is available at **[https://hdrify.ben3d.ca](https://hdrify.ben3d.ca)** — HDR, EXR, and Ultra HDR viewer and converter built with this component; try it in your browser.
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} toneMapping="neutral" exposure={1.0} className="max-w-full" />;
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
- While loading, a progress bar fills across the top (`onProgress` / the `progress` event report it). `whenReady()` and `toBlob()` (on the element, or on the component's `ref`) wait for the image and export the tone-mapped result.
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'` instead. See the [`<image-viewer>` documentation](https://hdrify.ben3d.ca/docs/image-viewer/) for all attributes, events and theming.
71
+ With a bundler, `import 'hdrify-react/element'` to register the web component.
72
72
 
73
- ### HdrifyCanvas props
73
+ ## Documentation
74
74
 
75
- | Prop | Type | Description |
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).
@@ -1,12 +1,39 @@
1
- import type { HdrifyImage, ToneMappingType } from 'hdrify';
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
- toneMapping: ToneMappingType;
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
- /** Optional ref so parent can access the canvas (e.g. for toBlob). */
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
- export declare function HdrifyCanvas({ hdrifyImage, toneMapping, exposure, className, forwardedRef, }: HdrifyCanvasProps): React.ReactElement;
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,eAAe,EAAE,MAAM,QAAQ,CAAC;AAC3D,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AAIpC,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,WAAW,CAAC;IACzB,WAAW,EAAE,eAAe,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sEAAsE;IACtE,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;CAC1D;AAED,wBAAgB,YAAY,CAAC,EAC3B,WAAW,EACX,WAAW,EACX,QAAQ,EACR,SAAS,EACT,YAAY,GACb,EAAE,iBAAiB,GAAG,KAAK,CAAC,YAAY,CAmBxC"}
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"}
@@ -1,19 +1,203 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useRef } from 'react';
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
- export function HdrifyCanvas({ hdrifyImage, toneMapping, exposure, className, forwardedRef, }) {
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 setRef = useCallback((el) => {
7
- canvasRef.current = el;
8
- if (forwardedRef) {
9
- // oxlint-disable-next-line react/immutability -- forwarding the element into the parent-owned ref
10
- forwardedRef.current = el;
11
- }
12
- }, [forwardedRef]);
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
- if (canvasRef.current)
15
- drawToneMapped(canvasRef.current, hdrifyImage, toneMapping, exposure);
16
- }, [hdrifyImage, exposure, toneMapping]);
17
- return _jsx("canvas", { className: className, ref: setRef });
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
@@ -1 +1 @@
1
- {"version":3,"file":"HdrifyCanvas.js","sources":["../src/HdrifyCanvas.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAW9C,MAAM,UAAU,YAAY,CAAC,EAC3B,WAAW,EACX,WAAW,EACX,QAAQ,EACR,SAAS,EACT,YAAY,GACM;IAClB,MAAM,SAAS,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAC;IAElD,MAAM,MAAM,GAAG,WAAW,CACxB,CAAC,EAA4B,EAAE,EAAE;QAC/B,SAAS,CAAC,OAAO,GAAG,EAAE,CAAC;QACvB,IAAI,YAAY,EAAE,CAAC;YACjB,kGAAkG;YAClG,YAAY,CAAC,OAAO,GAAG,EAAE,CAAC;QAC5B,CAAC;IACH,CAAC,EACD,CAAC,YAAY,CAAC,CACf,CAAC;IAEF,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,SAAS,CAAC,OAAO;YAAE,cAAc,CAAC,SAAS,CAAC,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC;IAC/F,CAAC,EAAE,CAAC,WAAW,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC;IAEzC,OAAO,iBAAQ,SAAS,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,GAAI,CAAC;AACvD,CAAC"}
1
+ {"version":3,"file":"HdrifyCanvas.js","sources":["../src/HdrifyCanvas.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAA+D,MAAM,cAAc,CAAC;AAkB/G,MAAM,GAAG,GACP,qPAAqP,CAAC;AAExP;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,YAAY,CAAC,KAAwB;IACnD,MAAM,OAAO,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAC;IAC7C,MAAM,SAAS,GAAG,MAAM,CAA2B,IAAI,CAAC,CAAC;IACzD,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC7B,MAAM,MAAM,GAAG,MAAM,CAAa,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAC5C,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,CAAC,OAAO,GAAG,KAAK,CAAC;IACzB,CAAC,CAAC,CAAC;IACH,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC3D,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC;QACxC,IAAI,CAAC,YAAY;YAAE,OAAO,SAAS,CAAC;QACpC,qGAAqG;QACrG,YAAY,CAAC,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC;QACzC,OAAO,GAAG,EAAE;YACV,sGAAsG;YACtG,YAAY,CAAC,OAAO,GAAG,IAAI,CAAC;QAC9B,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC;IAEzB,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,IAAI,GAAG,OAAO,CAAC,OAAQ,CAAC;QAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,aAAa,CAAC,WAAY,CAAC;QAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,uBAAuB,CAAC,CAAC;QACvD,IAAI,MAAgC,CAAC;QACrC,IAAI,MAAyB,CAAC;QAC9B,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,IAAI,UAAU,GAAG,CAAC,CAAC;QACnB,IAAI,YAAY,GAAG,CAAC,CAAC;QACrB,IAAI,eAAe,GAAG,EAAE,CAAC;QACzB,IAAI,aAAsC,CAAC;QAC3C,MAAM,KAAK,GAAG,CAAC,IAAuB,EAAE,EAAE;YACxC,MAAM,GAAG,IAAI,CAAC;YACd,SAAS,CAAC,OAAO,GAAG,MAAM,CAAC;YAC3B,0FAA0F;YAC1F,IAAI,MAAM,CAAC,OAAO,CAAC,YAAY;gBAAE,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,OAAO,GAAG,MAAM,CAAC;YAC9E,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;QAC/B,CAAC,CAAC;QACF,MAAM,IAAI,GAAG,GAAG,EAAE;YAChB,IAAI,QAAQ,IAAI,CAAC,MAAM;gBAAE,OAAO;YAChC,MAAM,OAAO,GAAG,MAAM,CAAC;YACvB,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC;YACtF,MAAM,MAAM,GAAG,kBAAkB,CAAC,WAAW,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;YAC7D,MAAM,QAAQ,GAAG,EAAE,YAAY,CAAC;YAChC,IAAI,CAAC;gBACH,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CACpB,CAAC,IAAI,CAAC,WAAW,IAAI,KAAK,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,KAAK,EAC/C,CAAC,IAAI,CAAC,YAAY,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CACnD,CAAC;gBACF,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC;gBAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC;gBACjD,MAAM,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,KAAK,IAAI,CAAC;gBAClC,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC;gBACpC,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,gBAAgB,IAAI,CAAC,CAAC,CAAC;gBAC1D,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;gBACtC,MAAM,CAAC,OAAO,CAAC,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;gBAC1C,MAAM,CAAC,OAAO,CAAC,WAAW,GAAG,MAAM,CAAC;gBACpC,KAAK,OAAO;qBACT,QAAQ,EAAE;qBACV,IAAI,CAAC,GAAG,EAAE;oBACT,IAAI,CAAC,QAAQ,IAAI,QAAQ,KAAK,YAAY,IAAI,MAAM,KAAK,OAAO;wBAAE,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;gBAClG,CAAC,CAAC;qBACD,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;oBACxB,IAAI,CAAC,QAAQ,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;wBACpC,IAAI,OAAO,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;4BAC3B,QAAQ,EAAE,CAAC;wBACb,CAAC;;4BAAM,OAAO,EAAE,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;oBAC9E,CAAC;gBACH,CAAC,CAAC,CAAC;YACP,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,OAAO,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;oBAC3B,QAAQ,EAAE,CAAC;gBACb,CAAC;;oBAAM,OAAO,EAAE,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAC9E,CAAC;QACH,CAAC,CAAC;QACF,MAAM,QAAQ,GAAG,GAAG,EAAE;YACpB,IAAI,QAAQ;gBAAE,OAAO;YACrB,EAAE,UAAU,CAAC;YACb,MAAM,EAAE,OAAO,EAAE,CAAC;YAClB,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAC;YAClD,aAAa,GAAG,SAAS,CAAC;YAC1B,eAAe,GAAG,EAAE,CAAC;YACrB,MAAM,GAAG;gBACP,IAAI,EAAE,KAAK;gBACX,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ;oBAC1B,MAAM,GAAG,GAAG,GAAG,MAAM,IAAI,QAAQ,EAAE,CAAC;oBACpC,IAAI,aAAa,KAAK,KAAK,IAAI,eAAe,KAAK,GAAG;wBAAE,OAAO;oBAC/D,cAAc,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;oBAChD,aAAa,GAAG,KAAK,CAAC;oBACtB,eAAe,GAAG,GAAG,CAAC;gBACxB,CAAC;gBACD,MAAM,KAAI,CAAC;gBACX,QAAQ,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE;gBACjC,OAAO,KAAI,CAAC;aACb,CAAC;YACF,IAAI,EAAE,CAAC;QACT,CAAC,CAAC;QACF,MAAM,MAAM,GAAG,KAAK,IAAI,EAAE;YACxB,MAAM,OAAO,GAAG,EAAE,UAAU,CAAC;YAC7B,IAAI,CAAC,KAAK,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC;gBAC1C,QAAQ,EAAE,CAAC;gBACX,OAAO;YACT,CAAC;YACD,IAAI,IAA8B,CAAC;YACnC,IAAI,CAAC;gBACH,MAAM,EAAE,mBAAmB,EAAE,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAC;gBACnE,IAAI,QAAQ,IAAI,OAAO,KAAK,UAAU;oBAAE,OAAO;gBAC/C,MAAM,SAAS,GAAG,IAAI,CAAC,aAAa,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;gBAC7D,IAAI,GAAG,MAAM,mBAAmB,CAAC,SAAS,EAAE,GAAG,EAAE;oBAC/C,IAAI,CAAC,QAAQ,IAAI,OAAO,KAAK,UAAU;wBAAE,QAAQ,EAAE,CAAC;gBACtD,CAAC,CAAC,CAAC;gBACH,IAAI,QAAQ,IAAI,OAAO,KAAK,UAAU,EAAE,CAAC;oBACvC,IAAI,CAAC,OAAO,EAAE,CAAC;oBACf,OAAO;gBACT,CAAC;gBACD,MAAM,EAAE,OAAO,EAAE,CAAC;gBAClB,MAAM,GAAG,IAAI,CAAC;gBACd,KAAK,CAAC,SAAS,CAAC,CAAC;gBACjB,IAAI,EAAE,CAAC;YACT,CAAC;YAAC,MAAM,CAAC;gBACP,IAAI,EAAE,OAAO,EAAE,CAAC;gBAChB,IAAI,CAAC,QAAQ,IAAI,OAAO,KAAK,UAAU;oBAAE,QAAQ,EAAE,CAAC;YACtD,CAAC;QACH,CAAC,CAAC;QACF,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;QACtB,MAAM,QAAQ,GAAG,IAAI,cAAc,CAAC,IAAI,CAAC,CAAC;QAC1C,IAAI,CAAC;YACH,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,0BAA0B,EAAE,CAAC,CAAC;QAC9D,CAAC;QAAC,MAAM,CAAC;YACP,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACzB,CAAC;QACD,MAAM,aAAa,GAAG,GAAG,EAAE;YACzB,KAAK,MAAM,EAAE,CAAC;QAChB,CAAC,CAAC;QACF,KAAK,CAAC,gBAAgB,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;QAChD,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QACtC,IAAI,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,gBAAgB,IAAI,CAAC,gBAAgB,OAAO,CAAC,CAAC;QAC/E,MAAM,kBAAkB,GAAG,GAAG,EAAE;YAC9B,UAAU,CAAC,mBAAmB,CAAC,QAAQ,EAAE,kBAAkB,CAAC,CAAC;YAC7D,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,gBAAgB,IAAI,CAAC,gBAAgB,OAAO,CAAC,CAAC;YAC3E,UAAU,CAAC,gBAAgB,CAAC,QAAQ,EAAE,kBAAkB,CAAC,CAAC;YAC1D,IAAI,EAAE,CAAC;QACT,CAAC,CAAC;QACF,UAAU,CAAC,gBAAgB,CAAC,QAAQ,EAAE,kBAAkB,CAAC,CAAC;QAC1D,KAAK,MAAM,EAAE,CAAC;QACd,OAAO,GAAG,EAAE;YACV,QAAQ,GAAG,IAAI,CAAC;YAChB,EAAE,UAAU,CAAC;YACb,EAAE,YAAY,CAAC;YACf,QAAQ,CAAC,UAAU,EAAE,CAAC;YACtB,KAAK,CAAC,mBAAmB,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;YACnD,IAAI,CAAC,mBAAmB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YACzC,UAAU,CAAC,mBAAmB,CAAC,QAAQ,EAAE,kBAAkB,CAAC,CAAC;YAC7D,MAAM,EAAE,OAAO,EAAE,CAAC;YAClB,SAAS,CAAC,OAAO,GAAG,IAAI,CAAC;YACzB,MAAM,CAAC,OAAO,GAAG,GAAG,EAAE,GAAE,CAAC,CAAC;YAC1B,IAAI,CAAC,eAAe,EAAE,CAAC;QACzB,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO,CACL,8BACE,0BAAQ,GAAG,GAAS,EACpB,cACE,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,gBAAgB,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EACzE,KAAK,EACH;oBACE,sBAAsB,EAAE,GAAG,KAAK,CAAC,WAAW,CAAC,KAAK,IAAI;oBACtD,uBAAuB,EAAE,GAAG,KAAK,CAAC,WAAW,CAAC,KAAK,IAAI,KAAK,CAAC,WAAW,CAAC,MAAM,EAAE;iBAC3D,GAE1B,IACD,CACJ,CAAC;AACJ,CAAC"}
@@ -1,4 +1,5 @@
1
- import type { HdrifyImage, ToneMappingType } from 'hdrify';
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
- toneMapping: ToneMappingType;
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: ToneMappingType;
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
- /** Wait for {@link whenReady}, then encode the tone-mapped image with the current settings; no viewer controls. */
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,eAAe,EAAE,MAAM,QAAQ,CAAC;AAC3D,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,GAAG,EAA2D,MAAM,OAAO,CAAC;AAC9G,OAAO,EAAgB,KAAK,yBAAyB,EAAE,MAAM,cAAc,CAAC;AAG5E,OAAO,EAAyB,KAAK,mBAAmB,EAAgB,MAAM,eAAe,CAAC;AAI9F,uGAAuG;AACvG,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAC7D,uHAAuH;AACvH,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE5C,wCAAwC;AACxC,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,eAAe,CAAC;IAC7B,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,QAAQ,CAAC;CACf;AAED,eAAO,MAAM,6BAA6B,EAAE,QAAQ,CAAC,mBAAmB,CAIvE,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,SAAS;IAAE,KAAK,EAAE,eAAe,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAKpF,CAAC;AAEF,mFAAmF;AACnF,eAAO,MAAM,qBAAqB;aAAK,GAAG,EAAE,CAAC,EAAE;aAAE,GAAG,EAAE,EAAE;CAAW,CAAC;AAEpE,wFAAwF;AACxF,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,mHAAmH;IACnH,MAAM,CAAC,OAAO,CAAC,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5D;AAED,qGAAqG;AACrG,MAAM,WAAW,gBAAiB,SAAQ,OAAO,CAAC,mBAAmB,CAAC;IACpE,uGAAuG;IACvG,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,IAAI,CAAC,EAAE,WAAW,GAAG,UAAU,CAAC;IAChC,wEAAwE;IACxE,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,gEAAgE;IAChE,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAClC,4DAA4D;IAC5D,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,0BAA0B;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,kDAAkD;IAClD,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IACtC,8EAA8E;IAC9E,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,2DAA2D;IAC3D,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACrD,mFAAmF;IACnF,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAC3D,8DAA8D;IAC9D,qBAAqB,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC1D,6EAA6E;IAC7E,GAAG,CAAC,EAAE,GAAG,CAAC,iBAAiB,CAAC,CAAC;CAC9B;AAwBD;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EAC1B,GAAG,EACH,IAAI,EACJ,KAAK,EACL,WAAW,EACX,QAAQ,EACR,GAAG,EACH,aAAwB,EACxB,eAAuB,EACvB,SAAS,EACT,KAAK,EACL,MAAM,EACN,OAAO,EACP,UAAU,EACV,gBAAgB,EAChB,qBAAqB,EACrB,GAAG,GACJ,EAAE,gBAAgB,+BA8LlB"}
1
+ {"version":3,"file":"ImageViewer.d.ts","sources":["../src/ImageViewer.tsx"],"names":[],"mappings":"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"}
@@ -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: 'neutral',
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) => ({ ...current, ...definedSettings({ toneMapping, exposure, fit }) }));
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
- const barValue = bar && !loading ? 1 : bar?.value;
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" })) }), _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",
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", { value: settings.toneMapping, onChange: (event) => onChange({ toneMapping: event.currentTarget.value }), children: TONE_MAPPING_OPTIONS.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) => {
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 });