bright-canvas 0.1.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 +73 -0
- package/bright-canvas.js +241 -0
- package/bun.lock +106 -0
- package/demo/example.html +177 -0
- package/demo/minimal-float16.html +58 -0
- package/demo/minimal.html +45 -0
- package/demo/test_chart.png +0 -0
- package/dist/bright-canvas.js +243 -0
- package/dist/bright-canvas.min.js +1 -0
- package/package.json +22 -0
- package/rollup.config.mjs +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# bright-canvas
|
|
6
|
+
|
|
7
|
+
**Brighten any canvas with proper HDR support. No flags required.**
|
|
8
|
+
|
|
9
|
+
**[View the demo here!](https://bright-canvas.netlify.app/demo/example.html)**
|
|
10
|
+
|
|
11
|
+
Sends a source canvas through an WebGPU canvas with extended tone mapping, enabling HDR brightness with minimal setup and no fighting with WebGPU.
|
|
12
|
+
Falls back to a plain 2D context when WebGPU and/or HDR is not available in the browser.
|
|
13
|
+
|
|
14
|
+
Supported since [Chrome 129](https://developer.chrome.com/blog/new-in-webgpu-129#hdr_support_with_canvas_tone_mapping_mode) and [iOS/Safari 26](https://developer.apple.com/documentation/safari-release-notes/safari-26-release-notes#WebGPU).
|
|
15
|
+
|
|
16
|
+
Firefox does not have HDR support for WebGPU canvases yet.
|
|
17
|
+
|
|
18
|
+
WebGPU will only work in a secure context or on localhost.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install bright-canvas
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
import { initHdrCanvas } from 'bright-canvas';
|
|
28
|
+
|
|
29
|
+
// minimal usage
|
|
30
|
+
const hdr = await initHdrCanvas(srcCanvas, outCanvas);
|
|
31
|
+
|
|
32
|
+
// advanced usage
|
|
33
|
+
const hdr = await initHdrCanvas(srcCanvas, outCanvas, {
|
|
34
|
+
debug: true, // log messages to the console
|
|
35
|
+
brightness: 2, // initial brightness factor
|
|
36
|
+
sourceFloat16: false, // set true if srcCanvas has float16 backing
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
hdr.mode; // 'hdr' | 'sdr'
|
|
40
|
+
hdr.setBrightness(v); // 2 is relatively safe
|
|
41
|
+
hdr.render(); // call in frame function after drawing to srcCanvas
|
|
42
|
+
hdr.destroy(); // release WebGPU canvas resources
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
See [demo/minimal.html](https://github.com/ethan-ou/bright-canvas/blob/main/demo/minimal.html) for a minimal example. [(web)](https://bright-canvas.netlify.app/demo/minimal.html)
|
|
46
|
+
|
|
47
|
+
## Demo
|
|
48
|
+
|
|
49
|
+
WebGPU + ES modules in this demo require a secure context. When running this repo locally, run `local-web-server` to serve.
|
|
50
|
+
|
|
51
|
+
`local-web-server` serves a self-signed cert by default and will show a warning in browser.
|
|
52
|
+
Bypass by clicking Show Details/Advanced then Proceed/Visit ...
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
npx local-web-server --https
|
|
56
|
+
# then open to /demo/hdr-2d-canvas-module.html
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Notes
|
|
60
|
+
|
|
61
|
+
- Brightness multiplies the color values from the source canvas, but does not increase bit depth with default canvas/bright-canvas options, and may result in muted colors.
|
|
62
|
+
- When using canvases with higher bit depths (ex. `colorType: 'float16'` as 2d canvas option), set `sourceFloat16` to true to properly map these values.
|
|
63
|
+
- `colorType` is only supported in Chrome at the moment.
|
|
64
|
+
- Using in combination with `colorSpace: 'display-p3'` is recommended.
|
|
65
|
+
- Vibrant colors may only show up on Chrome in MacOS. See [demo/minimal-float16.html](https://github.com/ethan-ou/bright-canvas/blob/main/demo/minimal-float16.html) for a minimal example with float16 and display-p3 colors. [(web)](https://bright-canvas.netlify.app/demo/minimal-float16.html)
|
|
66
|
+
- Call `hdr.render()` in the same frame you draw to the source canvas.
|
|
67
|
+
- Source dimensions are captured at init; resizing the source canvas afterward is not handled yet.
|
|
68
|
+
|
|
69
|
+
## Disclosure
|
|
70
|
+
|
|
71
|
+
Claude Fable 5 was used to develop bright-canvas.js and demo/example.html with minimal editing.
|
|
72
|
+
Minimal examples and this README.md were written by me.
|
|
73
|
+
WebGPU code was adapted from this webgpu-samples particles demo: https://webgpu.github.io/webgpu-samples/?sample=particles
|
package/bright-canvas.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// bright-canvas.js
|
|
2
|
+
//
|
|
3
|
+
// Draws the contents of a source canvas onto an output canvas using a
|
|
4
|
+
// WebGPU rgba16float context with extended tone mapping (HDR) when the
|
|
5
|
+
// browser and display support it, or a plain 2D drawImage fallback (SDR).
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// const hdr = await initHdrCanvas(srcCanvas, outCanvas, options);
|
|
9
|
+
// hdr.mode // 'hdr' | 'sdr'
|
|
10
|
+
// hdr.setBrightness(2.5) // HDR only; no-op in SDR mode
|
|
11
|
+
// hdr.render() // call each frame after drawing to srcCanvas
|
|
12
|
+
// hdr.destroy() // release all GPU resources and references;
|
|
13
|
+
// // render/setBrightness become no-ops
|
|
14
|
+
//
|
|
15
|
+
// Options:
|
|
16
|
+
// debug (default false) - log messages to the console
|
|
17
|
+
// brightness (default 2) - initial brightness factor; can be
|
|
18
|
+
// changed later with setBrightness()
|
|
19
|
+
// sourceFloat16 (default false) - set true when srcCanvas has float16
|
|
20
|
+
// backing (e.g. colorType: 'float16')
|
|
21
|
+
// so extended-range (>1.0) values
|
|
22
|
+
// survive the copy. Not auto-detected:
|
|
23
|
+
// probing would call getContext() on
|
|
24
|
+
// the source canvas, which locks a
|
|
25
|
+
// fresh canvas to that context type.
|
|
26
|
+
|
|
27
|
+
export async function initHdrCanvas(
|
|
28
|
+
srcCanvas,
|
|
29
|
+
outCanvas,
|
|
30
|
+
{ debug = false, brightness = 2, sourceFloat16 = false } = {},
|
|
31
|
+
) {
|
|
32
|
+
const log = debug ? (msg) => console.log("[bright-canvas]", msg) : () => {};
|
|
33
|
+
|
|
34
|
+
const W = srcCanvas.width;
|
|
35
|
+
const H = srcCanvas.height;
|
|
36
|
+
|
|
37
|
+
const srcTextureFormat = sourceFloat16 ? "rgba16float" : "rgba8unorm";
|
|
38
|
+
|
|
39
|
+
// Capability check: WebGPU + HDR display
|
|
40
|
+
const hdrMediaQuery = window.matchMedia("(dynamic-range: high)");
|
|
41
|
+
let device = null;
|
|
42
|
+
if (!navigator.gpu) {
|
|
43
|
+
log("⚠️ WebGPU is not available - falling back to a 2D canvas (SDR).");
|
|
44
|
+
} else if (!hdrMediaQuery.matches) {
|
|
45
|
+
log("⚠️ Display isn't HDR-compatible - falling back to a 2D canvas (SDR).");
|
|
46
|
+
} else {
|
|
47
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
48
|
+
device = (await adapter?.requestDevice()) ?? null;
|
|
49
|
+
if (!device) {
|
|
50
|
+
log(
|
|
51
|
+
"⚠️ Failed to get a WebGPU device - falling back to a 2D canvas (SDR).",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Probe rgba16float + extended tone mapping (Firefox supports WebGPU
|
|
57
|
+
// but not rgba16float canvases). Probing uses a throwaway canvas so
|
|
58
|
+
// out-canvas can still get a '2d' context if we end up falling back.
|
|
59
|
+
let presentationFormat = null;
|
|
60
|
+
if (device) {
|
|
61
|
+
const probeContext = document.createElement("canvas").getContext("webgpu");
|
|
62
|
+
if (probeContext) {
|
|
63
|
+
try {
|
|
64
|
+
probeContext.configure({
|
|
65
|
+
device,
|
|
66
|
+
format: "rgba16float",
|
|
67
|
+
toneMapping: { mode: "extended" },
|
|
68
|
+
});
|
|
69
|
+
presentationFormat = "rgba16float";
|
|
70
|
+
} catch (e) {
|
|
71
|
+
log(`⚠️ Canvas format 'rgba16float' not supported: ${e.message}`);
|
|
72
|
+
}
|
|
73
|
+
probeContext.unconfigure();
|
|
74
|
+
}
|
|
75
|
+
if (!presentationFormat) {
|
|
76
|
+
log(
|
|
77
|
+
"⚠️ No usable WebGPU canvas format - falling back to a 2D canvas (SDR).",
|
|
78
|
+
);
|
|
79
|
+
device = null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Fallback path: plain 2D canvas, drawImage from the source canvas.
|
|
84
|
+
if (!device) {
|
|
85
|
+
let outCtx = outCanvas.getContext("2d");
|
|
86
|
+
return {
|
|
87
|
+
mode: "sdr",
|
|
88
|
+
setBrightness() {}, // no-op in SDR mode
|
|
89
|
+
render() {
|
|
90
|
+
if (!outCtx) return;
|
|
91
|
+
outCtx.drawImage(srcCanvas, 0, 0, outCanvas.width, outCanvas.height);
|
|
92
|
+
},
|
|
93
|
+
destroy() {
|
|
94
|
+
outCtx = null;
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// WebGPU path: rgba16float + extended tone mapping (HDR)
|
|
100
|
+
let context = outCanvas.getContext("webgpu");
|
|
101
|
+
context.configure({
|
|
102
|
+
device,
|
|
103
|
+
format: presentationFormat,
|
|
104
|
+
toneMapping: { mode: "extended" },
|
|
105
|
+
});
|
|
106
|
+
log(`ℹ️ WebGPU canvas format: ${presentationFormat}`);
|
|
107
|
+
log(`ℹ️ Source texture format: ${srcTextureFormat}`);
|
|
108
|
+
|
|
109
|
+
if ("getConfiguration" in GPUCanvasContext.prototype) {
|
|
110
|
+
const mode = context.getConfiguration()?.toneMapping?.mode;
|
|
111
|
+
if (mode !== "extended") {
|
|
112
|
+
log(
|
|
113
|
+
"⚠️ Browser doesn't support HDR canvas, fell back to '" + mode + "').",
|
|
114
|
+
);
|
|
115
|
+
} else {
|
|
116
|
+
log("✅ HDR canvas enabled.");
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
log("ℹ️ getConfiguration() unavailable.");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Bridge: copy the 2D canvas into a texture each frame, then render
|
|
123
|
+
// it fullscreen multiplied by the brightness factor. An 8-bit 2D
|
|
124
|
+
// canvas only holds 0–1 values, so the multiply is what pushes
|
|
125
|
+
// highlights into HDR range.
|
|
126
|
+
let srcTexture = device.createTexture({
|
|
127
|
+
size: [W, H],
|
|
128
|
+
format: srcTextureFormat,
|
|
129
|
+
usage:
|
|
130
|
+
GPUTextureUsage.TEXTURE_BINDING |
|
|
131
|
+
GPUTextureUsage.COPY_DST |
|
|
132
|
+
GPUTextureUsage.RENDER_ATTACHMENT,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const shaderModule = device.createShaderModule({
|
|
136
|
+
code: /* wgsl */ `
|
|
137
|
+
@group(0) @binding(0) var samp: sampler;
|
|
138
|
+
@group(0) @binding(1) var tex: texture_2d<f32>;
|
|
139
|
+
@group(0) @binding(2) var<uniform> brightness: f32;
|
|
140
|
+
|
|
141
|
+
struct VSOut {
|
|
142
|
+
@builtin(position) pos: vec4f,
|
|
143
|
+
@location(0) uv: vec2f,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
@vertex
|
|
147
|
+
fn vs(@builtin(vertex_index) i: u32) -> VSOut {
|
|
148
|
+
// Fullscreen triangle
|
|
149
|
+
let pos = array(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
|
|
150
|
+
var out: VSOut;
|
|
151
|
+
out.pos = vec4f(pos[i], 0, 1);
|
|
152
|
+
out.uv = pos[i] * vec2f(0.5, -0.5) + 0.5;
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@fragment
|
|
157
|
+
fn fs(in: VSOut) -> @location(0) vec4f {
|
|
158
|
+
let c = textureSample(tex, samp, in.uv);
|
|
159
|
+
return vec4f(c.rgb * brightness, 1.0);
|
|
160
|
+
}
|
|
161
|
+
`,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
let pipeline = device.createRenderPipeline({
|
|
165
|
+
layout: "auto",
|
|
166
|
+
vertex: { module: shaderModule, entryPoint: "vs" },
|
|
167
|
+
fragment: {
|
|
168
|
+
module: shaderModule,
|
|
169
|
+
entryPoint: "fs",
|
|
170
|
+
targets: [{ format: presentationFormat }],
|
|
171
|
+
},
|
|
172
|
+
primitive: { topology: "triangle-list" },
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
let sampler = device.createSampler({
|
|
176
|
+
magFilter: "linear",
|
|
177
|
+
minFilter: "linear",
|
|
178
|
+
});
|
|
179
|
+
let brightnessBuffer = device.createBuffer({
|
|
180
|
+
size: 4,
|
|
181
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
182
|
+
});
|
|
183
|
+
device.queue.writeBuffer(brightnessBuffer, 0, new Float32Array([brightness]));
|
|
184
|
+
|
|
185
|
+
let bindGroup = device.createBindGroup({
|
|
186
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
187
|
+
entries: [
|
|
188
|
+
{ binding: 0, resource: sampler },
|
|
189
|
+
{ binding: 1, resource: srcTexture.createView() },
|
|
190
|
+
{ binding: 2, resource: { buffer: brightnessBuffer } },
|
|
191
|
+
],
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
mode: "hdr",
|
|
196
|
+
setBrightness(value) {
|
|
197
|
+
if (!device) return;
|
|
198
|
+
device.queue.writeBuffer(brightnessBuffer, 0, new Float32Array([value]));
|
|
199
|
+
},
|
|
200
|
+
render() {
|
|
201
|
+
if (!device) return;
|
|
202
|
+
device.queue.copyExternalImageToTexture(
|
|
203
|
+
{ source: srcCanvas },
|
|
204
|
+
{ texture: srcTexture },
|
|
205
|
+
[W, H],
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const commandEncoder = device.createCommandEncoder();
|
|
209
|
+
const passEncoder = commandEncoder.beginRenderPass({
|
|
210
|
+
colorAttachments: [
|
|
211
|
+
{
|
|
212
|
+
view: context.getCurrentTexture().createView(),
|
|
213
|
+
clearValue: [0, 0, 0, 1],
|
|
214
|
+
loadOp: "clear",
|
|
215
|
+
storeOp: "store",
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
});
|
|
219
|
+
passEncoder.setPipeline(pipeline);
|
|
220
|
+
passEncoder.setBindGroup(0, bindGroup);
|
|
221
|
+
passEncoder.draw(3);
|
|
222
|
+
passEncoder.end();
|
|
223
|
+
device.queue.submit([commandEncoder.finish()]);
|
|
224
|
+
},
|
|
225
|
+
destroy() {
|
|
226
|
+
if (!device) return;
|
|
227
|
+
srcTexture.destroy();
|
|
228
|
+
brightnessBuffer.destroy();
|
|
229
|
+
context.unconfigure();
|
|
230
|
+
device.destroy();
|
|
231
|
+
srcTexture = null;
|
|
232
|
+
brightnessBuffer = null;
|
|
233
|
+
bindGroup = null;
|
|
234
|
+
pipeline = null;
|
|
235
|
+
sampler = null;
|
|
236
|
+
context = null;
|
|
237
|
+
device = null;
|
|
238
|
+
log("ℹ️ HDR canvas destroyed.");
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
package/bun.lock
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"@rollup/plugin-json": "^6.1.0",
|
|
8
|
+
"@rollup/plugin-terser": "^1.0.0",
|
|
9
|
+
"rollup": "^4.62.2",
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
"packages": {
|
|
14
|
+
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
|
15
|
+
|
|
16
|
+
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
|
17
|
+
|
|
18
|
+
"@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="],
|
|
19
|
+
|
|
20
|
+
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
|
21
|
+
|
|
22
|
+
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
|
23
|
+
|
|
24
|
+
"@rollup/plugin-json": ["@rollup/plugin-json@6.1.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA=="],
|
|
25
|
+
|
|
26
|
+
"@rollup/plugin-terser": ["@rollup/plugin-terser@1.0.0", "", { "dependencies": { "serialize-javascript": "^7.0.3", "smob": "^1.0.0", "terser": "^5.17.4" }, "peerDependencies": { "rollup": "^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ=="],
|
|
27
|
+
|
|
28
|
+
"@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="],
|
|
29
|
+
|
|
30
|
+
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.2", "", { "os": "android", "cpu": "arm" }, "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg=="],
|
|
31
|
+
|
|
32
|
+
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw=="],
|
|
33
|
+
|
|
34
|
+
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A=="],
|
|
35
|
+
|
|
36
|
+
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA=="],
|
|
37
|
+
|
|
38
|
+
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw=="],
|
|
39
|
+
|
|
40
|
+
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg=="],
|
|
41
|
+
|
|
42
|
+
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg=="],
|
|
43
|
+
|
|
44
|
+
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA=="],
|
|
45
|
+
|
|
46
|
+
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA=="],
|
|
47
|
+
|
|
48
|
+
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ=="],
|
|
49
|
+
|
|
50
|
+
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg=="],
|
|
51
|
+
|
|
52
|
+
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ=="],
|
|
53
|
+
|
|
54
|
+
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A=="],
|
|
55
|
+
|
|
56
|
+
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w=="],
|
|
57
|
+
|
|
58
|
+
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg=="],
|
|
59
|
+
|
|
60
|
+
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q=="],
|
|
61
|
+
|
|
62
|
+
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg=="],
|
|
63
|
+
|
|
64
|
+
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A=="],
|
|
65
|
+
|
|
66
|
+
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg=="],
|
|
67
|
+
|
|
68
|
+
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg=="],
|
|
69
|
+
|
|
70
|
+
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.2", "", { "os": "none", "cpu": "arm64" }, "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA=="],
|
|
71
|
+
|
|
72
|
+
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg=="],
|
|
73
|
+
|
|
74
|
+
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q=="],
|
|
75
|
+
|
|
76
|
+
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg=="],
|
|
77
|
+
|
|
78
|
+
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA=="],
|
|
79
|
+
|
|
80
|
+
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
|
81
|
+
|
|
82
|
+
"acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
|
|
83
|
+
|
|
84
|
+
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
|
85
|
+
|
|
86
|
+
"commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
|
87
|
+
|
|
88
|
+
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
|
89
|
+
|
|
90
|
+
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
|
91
|
+
|
|
92
|
+
"picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
|
|
93
|
+
|
|
94
|
+
"rollup": ["rollup@4.62.2", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="],
|
|
95
|
+
|
|
96
|
+
"serialize-javascript": ["serialize-javascript@7.0.7", "", {}, "sha512-YAy8Od6KV+uuwUuU50np8fGB/Aues6Y0nAhA9y/hId74PlKUcme4pXcBD46NWKr1Q4osN/iseZ17YqO1XfmI8g=="],
|
|
97
|
+
|
|
98
|
+
"smob": ["smob@1.6.2", "", {}, "sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw=="],
|
|
99
|
+
|
|
100
|
+
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
|
101
|
+
|
|
102
|
+
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
|
103
|
+
|
|
104
|
+
"terser": ["terser@5.49.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA=="],
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<title>2D canvas animation to WebGPU HDR canvas (module)</title>
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
color-scheme: light dark;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
html,
|
|
14
|
+
body {
|
|
15
|
+
margin: 0;
|
|
16
|
+
height: 100%;
|
|
17
|
+
display: flex;
|
|
18
|
+
place-content: center center;
|
|
19
|
+
align-items: center;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
gap: 12px;
|
|
22
|
+
font-family: system-ui, sans-serif;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.row {
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-wrap: wrap;
|
|
28
|
+
gap: 12px;
|
|
29
|
+
align-items: flex-end;
|
|
30
|
+
max-width: 100%;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
canvas {
|
|
34
|
+
display: block;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#out-canvas {
|
|
38
|
+
width: 600px;
|
|
39
|
+
height: 600px;
|
|
40
|
+
max-width: 100%;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#src-canvas {
|
|
44
|
+
width: 200px;
|
|
45
|
+
height: 200px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.label {
|
|
49
|
+
font-size: 12px;
|
|
50
|
+
text-align: center;
|
|
51
|
+
margin-top: 4px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#mode-summary {
|
|
55
|
+
font-size: 14px;
|
|
56
|
+
text-align: center;
|
|
57
|
+
}
|
|
58
|
+
</style>
|
|
59
|
+
</head>
|
|
60
|
+
|
|
61
|
+
<body>
|
|
62
|
+
<div class="row">
|
|
63
|
+
<div>
|
|
64
|
+
<canvas id="out-canvas"></canvas>
|
|
65
|
+
<div class="label" id="out-label">WebGPU canvas (HDR)</div>
|
|
66
|
+
</div>
|
|
67
|
+
<div>
|
|
68
|
+
<canvas id="src-canvas" width="600" height="600"></canvas>
|
|
69
|
+
<div class="label">Source 2D canvas (SDR)</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
<div>
|
|
73
|
+
<label><input id="show-chart" type="checkbox"> Show test_chart.png</label>
|
|
74
|
+
</div>
|
|
75
|
+
<div id="brightness-control" hidden>
|
|
76
|
+
<label for="brightness">Brightness factor: <span id="brightness-value">2.0</span></label>
|
|
77
|
+
<input id="brightness" type="range" min="1" max="4" step="0.1" value="2"
|
|
78
|
+
style="width: 300px; vertical-align: middle;">
|
|
79
|
+
</div>
|
|
80
|
+
<div id="mode-summary">Initializing…</div>
|
|
81
|
+
<script type="module">
|
|
82
|
+
import { initHdrCanvas } from '/bright-canvas.js';
|
|
83
|
+
// To use NPM package, move file to builder like Vite, comment out the above line and uncomment the below line
|
|
84
|
+
// import { initHdrCanvas } from 'bright-canvas';
|
|
85
|
+
|
|
86
|
+
// Source: plain 2D canvas with a basic animation (SDR, 0–1 values)
|
|
87
|
+
const srcCanvas = document.getElementById('src-canvas');
|
|
88
|
+
const ctx2d = srcCanvas.getContext('2d');
|
|
89
|
+
const W = srcCanvas.width;
|
|
90
|
+
const H = srcCanvas.height;
|
|
91
|
+
|
|
92
|
+
const ball = { x: W / 2, y: H / 3, vx: 260, vy: 200, r: 46 };
|
|
93
|
+
|
|
94
|
+
function draw2d(dt, t) {
|
|
95
|
+
// Solid black background
|
|
96
|
+
ctx2d.fillStyle = 'black';
|
|
97
|
+
ctx2d.fillRect(0, 0, W, H);
|
|
98
|
+
|
|
99
|
+
// Bounce
|
|
100
|
+
ball.x += ball.vx * dt;
|
|
101
|
+
ball.y += ball.vy * dt;
|
|
102
|
+
if (ball.x < ball.r || ball.x > W - ball.r) { ball.vx *= -1; ball.x = Math.max(ball.r, Math.min(W - ball.r, ball.x)); }
|
|
103
|
+
if (ball.y < ball.r || ball.y > H - ball.r) { ball.vy *= -1; ball.y = Math.max(ball.r, Math.min(H - ball.r, ball.y)); }
|
|
104
|
+
|
|
105
|
+
// Glowing ball - white core, colored halo
|
|
106
|
+
const halo = ctx2d.createRadialGradient(ball.x, ball.y, 0, ball.x, ball.y, ball.r * 2.2);
|
|
107
|
+
halo.addColorStop(0, 'white');
|
|
108
|
+
halo.addColorStop(0.35, `hsl(${(t * 40) % 360} 100% 60%)`);
|
|
109
|
+
halo.addColorStop(1, 'transparent');
|
|
110
|
+
ctx2d.fillStyle = halo;
|
|
111
|
+
ctx2d.beginPath();
|
|
112
|
+
ctx2d.arc(ball.x, ball.y, ball.r * 2.2, 0, Math.PI * 2);
|
|
113
|
+
ctx2d.fill();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Optional static image source (aspect-fit, centered)
|
|
117
|
+
const showChart = document.getElementById('show-chart');
|
|
118
|
+
const chartImg = new Image();
|
|
119
|
+
chartImg.src = './test_chart.png';
|
|
120
|
+
|
|
121
|
+
function drawChart() {
|
|
122
|
+
ctx2d.fillStyle = 'black';
|
|
123
|
+
ctx2d.fillRect(0, 0, W, H);
|
|
124
|
+
const scale = Math.min(W / chartImg.width, H / chartImg.height);
|
|
125
|
+
const w = chartImg.width * scale;
|
|
126
|
+
const h = chartImg.height * scale;
|
|
127
|
+
ctx2d.drawImage(chartImg, (W - w) / 2, (H - h) / 2, w, h);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Output canvas: HDR via the module, or its built-in SDR fallback
|
|
131
|
+
const outCanvas = document.getElementById('out-canvas');
|
|
132
|
+
outCanvas.width = outCanvas.clientWidth * window.devicePixelRatio;
|
|
133
|
+
outCanvas.height = outCanvas.clientHeight * window.devicePixelRatio;
|
|
134
|
+
|
|
135
|
+
const hdr = await initHdrCanvas(srcCanvas, outCanvas, { debug: true, brightness: 2 });
|
|
136
|
+
|
|
137
|
+
const modeSummary = document.getElementById('mode-summary');
|
|
138
|
+
if (hdr.mode === 'hdr') {
|
|
139
|
+
modeSummary.textContent = '✅ HDR mode (WebGPU)';
|
|
140
|
+
document.getElementById('brightness-control').hidden = false;
|
|
141
|
+
|
|
142
|
+
// Brightness slider (handled here in the HTML)
|
|
143
|
+
const brightnessSlider = document.getElementById('brightness');
|
|
144
|
+
const brightnessValue = document.getElementById('brightness-value');
|
|
145
|
+
const updateBrightness = () => {
|
|
146
|
+
const brightness = parseFloat(brightnessSlider.value);
|
|
147
|
+
brightnessValue.textContent = brightness.toFixed(1);
|
|
148
|
+
hdr.setBrightness(brightness);
|
|
149
|
+
};
|
|
150
|
+
brightnessSlider.addEventListener('input', updateBrightness);
|
|
151
|
+
updateBrightness();
|
|
152
|
+
} else {
|
|
153
|
+
document.getElementById('out-label').textContent = 'Fallback 2D canvas (SDR)';
|
|
154
|
+
modeSummary.textContent = '❌ SDR mode (2D canvas fallback)';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Frame loop
|
|
158
|
+
let lastT = 0;
|
|
159
|
+
function frame(timeMs) {
|
|
160
|
+
const t = timeMs / 1000;
|
|
161
|
+
const dt = Math.min(t - lastT, 0.05);
|
|
162
|
+
lastT = t;
|
|
163
|
+
|
|
164
|
+
if (showChart.checked && chartImg.complete && chartImg.naturalWidth > 0) {
|
|
165
|
+
drawChart();
|
|
166
|
+
} else {
|
|
167
|
+
draw2d(dt, t);
|
|
168
|
+
}
|
|
169
|
+
hdr.render();
|
|
170
|
+
|
|
171
|
+
requestAnimationFrame(frame);
|
|
172
|
+
}
|
|
173
|
+
requestAnimationFrame(frame);
|
|
174
|
+
</script>
|
|
175
|
+
</body>
|
|
176
|
+
|
|
177
|
+
</html>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>bright-canvas</title>
|
|
8
|
+
</head>
|
|
9
|
+
|
|
10
|
+
<body>
|
|
11
|
+
<canvas id="out-canvas" width="400" height="400"></canvas>
|
|
12
|
+
<canvas id="src-canvas" width="400" height="400"></canvas>
|
|
13
|
+
<script type="module">
|
|
14
|
+
import { initHdrCanvas } from '/bright-canvas.js';
|
|
15
|
+
// To use NPM package, move file to builder like Vite, comment out the above line and uncomment the below line
|
|
16
|
+
// import { initHdrCanvas } from 'bright-canvas';
|
|
17
|
+
|
|
18
|
+
const canvas = document.getElementById('src-canvas');
|
|
19
|
+
const ctx = canvas.getContext('2d', { colorSpace: 'display-p3', colorType: 'float16' });
|
|
20
|
+
const outCanvas = document.getElementById('out-canvas');
|
|
21
|
+
const hdr = await initHdrCanvas(canvas, outCanvas, {
|
|
22
|
+
debug: true,
|
|
23
|
+
brightness: 2,
|
|
24
|
+
sourceFloat16: true
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function frame(timeMs) {
|
|
28
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
29
|
+
ctx.fillStyle = 'black';
|
|
30
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
31
|
+
ctx.fillStyle = 'white';
|
|
32
|
+
ctx.font = 'bold 32px sans-serif';
|
|
33
|
+
ctx.fillText(`Wow, this is bright!`, 20, 50);
|
|
34
|
+
|
|
35
|
+
// Red
|
|
36
|
+
ctx.fillStyle = 'color(display-p3 1 0 0)';
|
|
37
|
+
ctx.fillRect(100, 100, 66, 100);
|
|
38
|
+
ctx.fillStyle = '#FF0000';
|
|
39
|
+
ctx.fillRect(100, 200, 66, 100);
|
|
40
|
+
// Green
|
|
41
|
+
ctx.fillStyle = 'color(display-p3 0 1 0)';
|
|
42
|
+
ctx.fillRect(166, 100, 66, 100);
|
|
43
|
+
ctx.fillStyle = '#00FF00';
|
|
44
|
+
ctx.fillRect(166, 200, 66, 100);
|
|
45
|
+
// Blue
|
|
46
|
+
ctx.fillStyle = 'color(display-p3 0 0 1)';
|
|
47
|
+
ctx.fillRect(232, 100, 66, 100);
|
|
48
|
+
ctx.fillStyle = '#0000FF';
|
|
49
|
+
ctx.fillRect(232, 200, 66, 100);
|
|
50
|
+
|
|
51
|
+
hdr.render();
|
|
52
|
+
requestAnimationFrame(frame);
|
|
53
|
+
}
|
|
54
|
+
requestAnimationFrame(frame);
|
|
55
|
+
</script>
|
|
56
|
+
</body>
|
|
57
|
+
|
|
58
|
+
</html>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>bright-canvas</title>
|
|
8
|
+
</head>
|
|
9
|
+
|
|
10
|
+
<body>
|
|
11
|
+
<canvas id="out-canvas" width="400" height="400"></canvas>
|
|
12
|
+
<canvas id="src-canvas" width="400" height="400"></canvas>
|
|
13
|
+
<script type="module">
|
|
14
|
+
import { initHdrCanvas } from '/bright-canvas.js';
|
|
15
|
+
// To use NPM package, move file to builder like Vite, comment out the above line and uncomment the below line
|
|
16
|
+
// import { initHdrCanvas } from 'bright-canvas';
|
|
17
|
+
|
|
18
|
+
const canvas = document.getElementById('src-canvas');
|
|
19
|
+
const ctx = canvas.getContext('2d');
|
|
20
|
+
const outCanvas = document.getElementById('out-canvas');
|
|
21
|
+
const hdr = await initHdrCanvas(canvas, outCanvas, { debug: true });
|
|
22
|
+
|
|
23
|
+
function draw(timeMs) {
|
|
24
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
25
|
+
ctx.fillStyle = 'black';
|
|
26
|
+
|
|
27
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
28
|
+
ctx.fillStyle = 'white';
|
|
29
|
+
ctx.font = 'bold 32px sans-serif';
|
|
30
|
+
ctx.fillText(`Wow, this is bright!`, 20, 50);
|
|
31
|
+
|
|
32
|
+
ctx.fillStyle = `hsl(${timeMs / 5000 * 360} 100% 50%)`;
|
|
33
|
+
ctx.fillRect(100, 100, 200, 200);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function frame(timeMs) {
|
|
37
|
+
draw(timeMs);
|
|
38
|
+
hdr.render();
|
|
39
|
+
requestAnimationFrame(frame);
|
|
40
|
+
}
|
|
41
|
+
requestAnimationFrame(frame);
|
|
42
|
+
</script>
|
|
43
|
+
</body>
|
|
44
|
+
|
|
45
|
+
</html>
|
|
Binary file
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// bright-canvas.js
|
|
2
|
+
//
|
|
3
|
+
// Draws the contents of a source canvas onto an output canvas using a
|
|
4
|
+
// WebGPU rgba16float context with extended tone mapping (HDR) when the
|
|
5
|
+
// browser and display support it, or a plain 2D drawImage fallback (SDR).
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// const hdr = await initHdrCanvas(srcCanvas, outCanvas, options);
|
|
9
|
+
// hdr.mode // 'hdr' | 'sdr'
|
|
10
|
+
// hdr.setBrightness(2.5) // HDR only; no-op in SDR mode
|
|
11
|
+
// hdr.render() // call each frame after drawing to srcCanvas
|
|
12
|
+
// hdr.destroy() // release all GPU resources and references;
|
|
13
|
+
// // render/setBrightness become no-ops
|
|
14
|
+
//
|
|
15
|
+
// Options:
|
|
16
|
+
// debug (default false) - log messages to the console
|
|
17
|
+
// brightness (default 2) - initial brightness factor; can be
|
|
18
|
+
// changed later with setBrightness()
|
|
19
|
+
// sourceFloat16 (default false) - set true when srcCanvas has float16
|
|
20
|
+
// backing (e.g. colorType: 'float16')
|
|
21
|
+
// so extended-range (>1.0) values
|
|
22
|
+
// survive the copy. Not auto-detected:
|
|
23
|
+
// probing would call getContext() on
|
|
24
|
+
// the source canvas, which locks a
|
|
25
|
+
// fresh canvas to that context type.
|
|
26
|
+
|
|
27
|
+
async function initHdrCanvas(
|
|
28
|
+
srcCanvas,
|
|
29
|
+
outCanvas,
|
|
30
|
+
{ debug = false, brightness = 2, sourceFloat16 = false } = {},
|
|
31
|
+
) {
|
|
32
|
+
const log = debug ? (msg) => console.log("[bright-canvas]", msg) : () => {};
|
|
33
|
+
|
|
34
|
+
const W = srcCanvas.width;
|
|
35
|
+
const H = srcCanvas.height;
|
|
36
|
+
|
|
37
|
+
const srcTextureFormat = sourceFloat16 ? "rgba16float" : "rgba8unorm";
|
|
38
|
+
|
|
39
|
+
// Capability check: WebGPU + HDR display
|
|
40
|
+
const hdrMediaQuery = window.matchMedia("(dynamic-range: high)");
|
|
41
|
+
let device = null;
|
|
42
|
+
if (!navigator.gpu) {
|
|
43
|
+
log("⚠️ WebGPU is not available - falling back to a 2D canvas (SDR).");
|
|
44
|
+
} else if (!hdrMediaQuery.matches) {
|
|
45
|
+
log("⚠️ Display isn't HDR-compatible - falling back to a 2D canvas (SDR).");
|
|
46
|
+
} else {
|
|
47
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
48
|
+
device = (await adapter?.requestDevice()) ?? null;
|
|
49
|
+
if (!device) {
|
|
50
|
+
log(
|
|
51
|
+
"⚠️ Failed to get a WebGPU device - falling back to a 2D canvas (SDR).",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Probe rgba16float + extended tone mapping (Firefox supports WebGPU
|
|
57
|
+
// but not rgba16float canvases). Probing uses a throwaway canvas so
|
|
58
|
+
// out-canvas can still get a '2d' context if we end up falling back.
|
|
59
|
+
let presentationFormat = null;
|
|
60
|
+
if (device) {
|
|
61
|
+
const probeContext = document.createElement("canvas").getContext("webgpu");
|
|
62
|
+
if (probeContext) {
|
|
63
|
+
try {
|
|
64
|
+
probeContext.configure({
|
|
65
|
+
device,
|
|
66
|
+
format: "rgba16float",
|
|
67
|
+
toneMapping: { mode: "extended" },
|
|
68
|
+
});
|
|
69
|
+
presentationFormat = "rgba16float";
|
|
70
|
+
} catch (e) {
|
|
71
|
+
log(`⚠️ Canvas format 'rgba16float' not supported: ${e.message}`);
|
|
72
|
+
}
|
|
73
|
+
probeContext.unconfigure();
|
|
74
|
+
}
|
|
75
|
+
if (!presentationFormat) {
|
|
76
|
+
log(
|
|
77
|
+
"⚠️ No usable WebGPU canvas format - falling back to a 2D canvas (SDR).",
|
|
78
|
+
);
|
|
79
|
+
device = null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Fallback path: plain 2D canvas, drawImage from the source canvas.
|
|
84
|
+
if (!device) {
|
|
85
|
+
let outCtx = outCanvas.getContext("2d");
|
|
86
|
+
return {
|
|
87
|
+
mode: "sdr",
|
|
88
|
+
setBrightness() {}, // no-op in SDR mode
|
|
89
|
+
render() {
|
|
90
|
+
if (!outCtx) return;
|
|
91
|
+
outCtx.drawImage(srcCanvas, 0, 0, outCanvas.width, outCanvas.height);
|
|
92
|
+
},
|
|
93
|
+
destroy() {
|
|
94
|
+
outCtx = null;
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// WebGPU path: rgba16float + extended tone mapping (HDR)
|
|
100
|
+
let context = outCanvas.getContext("webgpu");
|
|
101
|
+
context.configure({
|
|
102
|
+
device,
|
|
103
|
+
format: presentationFormat,
|
|
104
|
+
toneMapping: { mode: "extended" },
|
|
105
|
+
});
|
|
106
|
+
log(`ℹ️ WebGPU canvas format: ${presentationFormat}`);
|
|
107
|
+
log(`ℹ️ Source texture format: ${srcTextureFormat}`);
|
|
108
|
+
|
|
109
|
+
if ("getConfiguration" in GPUCanvasContext.prototype) {
|
|
110
|
+
const mode = context.getConfiguration()?.toneMapping?.mode;
|
|
111
|
+
if (mode !== "extended") {
|
|
112
|
+
log(
|
|
113
|
+
"⚠️ Browser doesn't support HDR canvas, fell back to '" + mode + "').",
|
|
114
|
+
);
|
|
115
|
+
} else {
|
|
116
|
+
log("✅ HDR canvas enabled.");
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
log("ℹ️ getConfiguration() unavailable.");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Bridge: copy the 2D canvas into a texture each frame, then render
|
|
123
|
+
// it fullscreen multiplied by the brightness factor. An 8-bit 2D
|
|
124
|
+
// canvas only holds 0–1 values, so the multiply is what pushes
|
|
125
|
+
// highlights into HDR range.
|
|
126
|
+
let srcTexture = device.createTexture({
|
|
127
|
+
size: [W, H],
|
|
128
|
+
format: srcTextureFormat,
|
|
129
|
+
usage:
|
|
130
|
+
GPUTextureUsage.TEXTURE_BINDING |
|
|
131
|
+
GPUTextureUsage.COPY_DST |
|
|
132
|
+
GPUTextureUsage.RENDER_ATTACHMENT,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const shaderModule = device.createShaderModule({
|
|
136
|
+
code: /* wgsl */ `
|
|
137
|
+
@group(0) @binding(0) var samp: sampler;
|
|
138
|
+
@group(0) @binding(1) var tex: texture_2d<f32>;
|
|
139
|
+
@group(0) @binding(2) var<uniform> brightness: f32;
|
|
140
|
+
|
|
141
|
+
struct VSOut {
|
|
142
|
+
@builtin(position) pos: vec4f,
|
|
143
|
+
@location(0) uv: vec2f,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
@vertex
|
|
147
|
+
fn vs(@builtin(vertex_index) i: u32) -> VSOut {
|
|
148
|
+
// Fullscreen triangle
|
|
149
|
+
let pos = array(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
|
|
150
|
+
var out: VSOut;
|
|
151
|
+
out.pos = vec4f(pos[i], 0, 1);
|
|
152
|
+
out.uv = pos[i] * vec2f(0.5, -0.5) + 0.5;
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@fragment
|
|
157
|
+
fn fs(in: VSOut) -> @location(0) vec4f {
|
|
158
|
+
let c = textureSample(tex, samp, in.uv);
|
|
159
|
+
return vec4f(c.rgb * brightness, 1.0);
|
|
160
|
+
}
|
|
161
|
+
`,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
let pipeline = device.createRenderPipeline({
|
|
165
|
+
layout: "auto",
|
|
166
|
+
vertex: { module: shaderModule, entryPoint: "vs" },
|
|
167
|
+
fragment: {
|
|
168
|
+
module: shaderModule,
|
|
169
|
+
entryPoint: "fs",
|
|
170
|
+
targets: [{ format: presentationFormat }],
|
|
171
|
+
},
|
|
172
|
+
primitive: { topology: "triangle-list" },
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
let sampler = device.createSampler({
|
|
176
|
+
magFilter: "linear",
|
|
177
|
+
minFilter: "linear",
|
|
178
|
+
});
|
|
179
|
+
let brightnessBuffer = device.createBuffer({
|
|
180
|
+
size: 4,
|
|
181
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
182
|
+
});
|
|
183
|
+
device.queue.writeBuffer(brightnessBuffer, 0, new Float32Array([brightness]));
|
|
184
|
+
|
|
185
|
+
let bindGroup = device.createBindGroup({
|
|
186
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
187
|
+
entries: [
|
|
188
|
+
{ binding: 0, resource: sampler },
|
|
189
|
+
{ binding: 1, resource: srcTexture.createView() },
|
|
190
|
+
{ binding: 2, resource: { buffer: brightnessBuffer } },
|
|
191
|
+
],
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
mode: "hdr",
|
|
196
|
+
setBrightness(value) {
|
|
197
|
+
if (!device) return;
|
|
198
|
+
device.queue.writeBuffer(brightnessBuffer, 0, new Float32Array([value]));
|
|
199
|
+
},
|
|
200
|
+
render() {
|
|
201
|
+
if (!device) return;
|
|
202
|
+
device.queue.copyExternalImageToTexture(
|
|
203
|
+
{ source: srcCanvas },
|
|
204
|
+
{ texture: srcTexture },
|
|
205
|
+
[W, H],
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const commandEncoder = device.createCommandEncoder();
|
|
209
|
+
const passEncoder = commandEncoder.beginRenderPass({
|
|
210
|
+
colorAttachments: [
|
|
211
|
+
{
|
|
212
|
+
view: context.getCurrentTexture().createView(),
|
|
213
|
+
clearValue: [0, 0, 0, 1],
|
|
214
|
+
loadOp: "clear",
|
|
215
|
+
storeOp: "store",
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
});
|
|
219
|
+
passEncoder.setPipeline(pipeline);
|
|
220
|
+
passEncoder.setBindGroup(0, bindGroup);
|
|
221
|
+
passEncoder.draw(3);
|
|
222
|
+
passEncoder.end();
|
|
223
|
+
device.queue.submit([commandEncoder.finish()]);
|
|
224
|
+
},
|
|
225
|
+
destroy() {
|
|
226
|
+
if (!device) return;
|
|
227
|
+
srcTexture.destroy();
|
|
228
|
+
brightnessBuffer.destroy();
|
|
229
|
+
context.unconfigure();
|
|
230
|
+
device.destroy();
|
|
231
|
+
srcTexture = null;
|
|
232
|
+
brightnessBuffer = null;
|
|
233
|
+
bindGroup = null;
|
|
234
|
+
pipeline = null;
|
|
235
|
+
sampler = null;
|
|
236
|
+
context = null;
|
|
237
|
+
device = null;
|
|
238
|
+
log("ℹ️ HDR canvas destroyed.");
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export { initHdrCanvas };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
async function e(e,t,{debug:n=!1,brightness:a=2,sourceFloat16:r=!1}={}){const o=n?e=>console.log("[bright-canvas]",e):()=>{},i=e.width,u=e.height,s=r?"rgba16float":"rgba8unorm",l=window.matchMedia("(dynamic-range: high)");let c=null;if(navigator.gpu)if(l.matches){const e=await navigator.gpu.requestAdapter();c=await(e?.requestDevice())??null,c||o("⚠️ Failed to get a WebGPU device - falling back to a 2D canvas (SDR).")}else o("⚠️ Display isn't HDR-compatible - falling back to a 2D canvas (SDR).");else o("⚠️ WebGPU is not available - falling back to a 2D canvas (SDR).");let g=null;if(c){const e=document.createElement("canvas").getContext("webgpu");if(e){try{e.configure({device:c,format:"rgba16float",toneMapping:{mode:"extended"}}),g="rgba16float"}catch(e){o(`⚠️ Canvas format 'rgba16float' not supported: ${e.message}`)}e.unconfigure()}g||(o("⚠️ No usable WebGPU canvas format - falling back to a 2D canvas (SDR)."),c=null)}if(!c){let n=t.getContext("2d");return{mode:"sdr",setBrightness(){},render(){n&&n.drawImage(e,0,0,t.width,t.height)},destroy(){n=null}}}let f=t.getContext("webgpu");if(f.configure({device:c,format:g,toneMapping:{mode:"extended"}}),o(`ℹ️ WebGPU canvas format: ${g}`),o(`ℹ️ Source texture format: ${s}`),"getConfiguration"in GPUCanvasContext.prototype){const e=f.getConfiguration()?.toneMapping?.mode;o("extended"!==e?"⚠️ Browser doesn't support HDR canvas, fell back to '"+e+"').":"✅ HDR canvas enabled.")}else o("ℹ️ getConfiguration() unavailable.");let d=c.createTexture({size:[i,u],format:s,usage:GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.COPY_DST|GPUTextureUsage.RENDER_ATTACHMENT});const p=c.createShaderModule({code:"\n @group(0) @binding(0) var samp: sampler;\n @group(0) @binding(1) var tex: texture_2d<f32>;\n @group(0) @binding(2) var<uniform> brightness: f32;\n\n struct VSOut {\n @builtin(position) pos: vec4f,\n @location(0) uv: vec2f,\n };\n\n @vertex\n fn vs(@builtin(vertex_index) i: u32) -> VSOut {\n // Fullscreen triangle\n let pos = array(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));\n var out: VSOut;\n out.pos = vec4f(pos[i], 0, 1);\n out.uv = pos[i] * vec2f(0.5, -0.5) + 0.5;\n return out;\n }\n\n @fragment\n fn fs(in: VSOut) -> @location(0) vec4f {\n let c = textureSample(tex, samp, in.uv);\n return vec4f(c.rgb * brightness, 1.0);\n }\n "});let v=c.createRenderPipeline({layout:"auto",vertex:{module:p,entryPoint:"vs"},fragment:{module:p,entryPoint:"fs",targets:[{format:g}]},primitive:{topology:"triangle-list"}}),m=c.createSampler({magFilter:"linear",minFilter:"linear"}),b=c.createBuffer({size:4,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST});c.queue.writeBuffer(b,0,new Float32Array([a]));let x=c.createBindGroup({layout:v.getBindGroupLayout(0),entries:[{binding:0,resource:m},{binding:1,resource:d.createView()},{binding:2,resource:{buffer:b}}]});return{mode:"hdr",setBrightness(e){c&&c.queue.writeBuffer(b,0,new Float32Array([e]))},render(){if(!c)return;c.queue.copyExternalImageToTexture({source:e},{texture:d},[i,u]);const t=c.createCommandEncoder(),n=t.beginRenderPass({colorAttachments:[{view:f.getCurrentTexture().createView(),clearValue:[0,0,0,1],loadOp:"clear",storeOp:"store"}]});n.setPipeline(v),n.setBindGroup(0,x),n.draw(3),n.end(),c.queue.submit([t.finish()])},destroy(){c&&(d.destroy(),b.destroy(),f.unconfigure(),c.destroy(),d=null,b=null,x=null,v=null,m=null,f=null,c=null,o("ℹ️ HDR canvas destroyed."))}}}export{e as initHdrCanvas};
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bright-canvas",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Brighten any canvas with proper HDR support. No flags required.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Inclushe",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "dist/bright-canvas.min.js",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "rollup -c"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"canvas",
|
|
14
|
+
"webgpu",
|
|
15
|
+
"hdr"
|
|
16
|
+
],
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@rollup/plugin-json": "^6.1.0",
|
|
19
|
+
"@rollup/plugin-terser": "^1.0.0",
|
|
20
|
+
"rollup": "^4.62.2"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// rollup.config.mjs
|
|
2
|
+
import json from "@rollup/plugin-json";
|
|
3
|
+
import terser from "@rollup/plugin-terser";
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
input: "bright-canvas.js",
|
|
7
|
+
output: [
|
|
8
|
+
{
|
|
9
|
+
file: "dist/bright-canvas.js",
|
|
10
|
+
format: "es",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
file: "dist/bright-canvas.min.js",
|
|
14
|
+
format: "es",
|
|
15
|
+
name: "version",
|
|
16
|
+
plugins: [terser()],
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
plugins: [json()],
|
|
20
|
+
};
|