bright-canvas 0.1.0 → 0.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 +10 -6
- package/bright-canvas.js +23 -5
- package/demo/minimal-transparent.html +48 -0
- package/dist/bright-canvas.js +23 -5
- package/dist/bright-canvas.min.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
1
|
# bright-canvas
|
|
6
2
|
|
|
7
3
|
**Brighten any canvas with proper HDR support. No flags required.**
|
|
@@ -34,6 +30,7 @@ const hdr = await initHdrCanvas(srcCanvas, outCanvas, {
|
|
|
34
30
|
debug: true, // log messages to the console
|
|
35
31
|
brightness: 2, // initial brightness factor
|
|
36
32
|
sourceFloat16: false, // set true if srcCanvas has float16 backing
|
|
33
|
+
alpha: true // true preserves source transparency
|
|
37
34
|
});
|
|
38
35
|
|
|
39
36
|
hdr.mode; // 'hdr' | 'sdr'
|
|
@@ -42,7 +39,7 @@ hdr.render(); // call in frame function after drawing to srcCanvas
|
|
|
42
39
|
hdr.destroy(); // release WebGPU canvas resources
|
|
43
40
|
```
|
|
44
41
|
|
|
45
|
-
See [demo/minimal.html](
|
|
42
|
+
See [demo/minimal.html](demo/minimal.html) for a minimal example. [(web)](https://bright-canvas.netlify.app/demo/minimal.html)
|
|
46
43
|
|
|
47
44
|
## Demo
|
|
48
45
|
|
|
@@ -56,13 +53,19 @@ npx local-web-server --https
|
|
|
56
53
|
# then open to /demo/hdr-2d-canvas-module.html
|
|
57
54
|
```
|
|
58
55
|
|
|
56
|
+
## Development
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
npm run build
|
|
60
|
+
```
|
|
61
|
+
|
|
59
62
|
## Notes
|
|
60
63
|
|
|
61
64
|
- 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
65
|
- When using canvases with higher bit depths (ex. `colorType: 'float16'` as 2d canvas option), set `sourceFloat16` to true to properly map these values.
|
|
63
66
|
- `colorType` is only supported in Chrome at the moment.
|
|
64
67
|
- 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](
|
|
68
|
+
- Vibrant colors may only show up on Chrome in MacOS. See [demo/minimal-float16.html](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
69
|
- Call `hdr.render()` in the same frame you draw to the source canvas.
|
|
67
70
|
- Source dimensions are captured at init; resizing the source canvas afterward is not handled yet.
|
|
68
71
|
|
|
@@ -70,4 +73,5 @@ npx local-web-server --https
|
|
|
70
73
|
|
|
71
74
|
Claude Fable 5 was used to develop bright-canvas.js and demo/example.html with minimal editing.
|
|
72
75
|
Minimal examples and this README.md were written by me.
|
|
76
|
+
Supermaven autocomplete was used to help me write README.md and minimal examples faster.
|
|
73
77
|
WebGPU code was adapted from this webgpu-samples particles demo: https://webgpu.github.io/webgpu-samples/?sample=particles
|
package/bright-canvas.js
CHANGED
|
@@ -23,14 +23,26 @@
|
|
|
23
23
|
// probing would call getContext() on
|
|
24
24
|
// the source canvas, which locks a
|
|
25
25
|
// fresh canvas to that context type.
|
|
26
|
+
// alpha (default true) - true preserves source transparency
|
|
27
|
+
// ('premultiplied' alpha mode), false
|
|
28
|
+
// composites opaque ('opaque'). A string
|
|
29
|
+
// is passed through as the alphaMode
|
|
30
|
+
// verbatim, for any future modes.
|
|
31
|
+
// Note: brightening semi-transparent
|
|
32
|
+
// pixels can push color above alpha
|
|
33
|
+
// (undefined per spec; Chrome composites
|
|
34
|
+
// it additively, like a glow).
|
|
26
35
|
|
|
27
36
|
export async function initHdrCanvas(
|
|
28
37
|
srcCanvas,
|
|
29
38
|
outCanvas,
|
|
30
|
-
{ debug = false, brightness = 2, sourceFloat16 = false } = {},
|
|
39
|
+
{ debug = false, brightness = 2, sourceFloat16 = false, alpha = true } = {},
|
|
31
40
|
) {
|
|
32
41
|
const log = debug ? (msg) => console.log("[bright-canvas]", msg) : () => {};
|
|
33
42
|
|
|
43
|
+
const alphaMode = typeof alpha === "string" ? alpha : alpha ? "premultiplied" : "opaque";
|
|
44
|
+
const transparent = alphaMode !== "opaque";
|
|
45
|
+
|
|
34
46
|
const W = srcCanvas.width;
|
|
35
47
|
const H = srcCanvas.height;
|
|
36
48
|
|
|
@@ -65,10 +77,11 @@ export async function initHdrCanvas(
|
|
|
65
77
|
device,
|
|
66
78
|
format: "rgba16float",
|
|
67
79
|
toneMapping: { mode: "extended" },
|
|
80
|
+
alphaMode,
|
|
68
81
|
});
|
|
69
82
|
presentationFormat = "rgba16float";
|
|
70
83
|
} catch (e) {
|
|
71
|
-
log(`⚠️
|
|
84
|
+
log(`⚠️ Configuration not supported: ${e.message}`);
|
|
72
85
|
}
|
|
73
86
|
probeContext.unconfigure();
|
|
74
87
|
}
|
|
@@ -88,6 +101,9 @@ export async function initHdrCanvas(
|
|
|
88
101
|
setBrightness() {}, // no-op in SDR mode
|
|
89
102
|
render() {
|
|
90
103
|
if (!outCtx) return;
|
|
104
|
+
if (transparent) {
|
|
105
|
+
outCtx.clearRect(0, 0, outCanvas.width, outCanvas.height);
|
|
106
|
+
}
|
|
91
107
|
outCtx.drawImage(srcCanvas, 0, 0, outCanvas.width, outCanvas.height);
|
|
92
108
|
},
|
|
93
109
|
destroy() {
|
|
@@ -102,9 +118,11 @@ export async function initHdrCanvas(
|
|
|
102
118
|
device,
|
|
103
119
|
format: presentationFormat,
|
|
104
120
|
toneMapping: { mode: "extended" },
|
|
121
|
+
alphaMode,
|
|
105
122
|
});
|
|
106
123
|
log(`ℹ️ WebGPU canvas format: ${presentationFormat}`);
|
|
107
124
|
log(`ℹ️ Source texture format: ${srcTextureFormat}`);
|
|
125
|
+
log(`ℹ️ Alpha mode: ${alphaMode}`);
|
|
108
126
|
|
|
109
127
|
if ("getConfiguration" in GPUCanvasContext.prototype) {
|
|
110
128
|
const mode = context.getConfiguration()?.toneMapping?.mode;
|
|
@@ -156,7 +174,7 @@ export async function initHdrCanvas(
|
|
|
156
174
|
@fragment
|
|
157
175
|
fn fs(in: VSOut) -> @location(0) vec4f {
|
|
158
176
|
let c = textureSample(tex, samp, in.uv);
|
|
159
|
-
return vec4f(c.rgb * brightness, 1.0);
|
|
177
|
+
return vec4f(c.rgb * brightness, ${transparent ? "c.a" : "1.0"});
|
|
160
178
|
}
|
|
161
179
|
`,
|
|
162
180
|
});
|
|
@@ -201,7 +219,7 @@ export async function initHdrCanvas(
|
|
|
201
219
|
if (!device) return;
|
|
202
220
|
device.queue.copyExternalImageToTexture(
|
|
203
221
|
{ source: srcCanvas },
|
|
204
|
-
{ texture: srcTexture },
|
|
222
|
+
{ texture: srcTexture, premultipliedAlpha: transparent },
|
|
205
223
|
[W, H],
|
|
206
224
|
);
|
|
207
225
|
|
|
@@ -210,7 +228,7 @@ export async function initHdrCanvas(
|
|
|
210
228
|
colorAttachments: [
|
|
211
229
|
{
|
|
212
230
|
view: context.getCurrentTexture().createView(),
|
|
213
|
-
clearValue: [0, 0, 0, 1],
|
|
231
|
+
clearValue: [0, 0, 0, transparent ? 0 : 1],
|
|
214
232
|
loadOp: "clear",
|
|
215
233
|
storeOp: "store",
|
|
216
234
|
},
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
// linear gradient from black to transparent
|
|
26
|
+
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
|
|
27
|
+
gradient.addColorStop(0, 'color(display-p3 0 0 0 / 0%)');
|
|
28
|
+
gradient.addColorStop(1, 'color(display-p3 0 0 0 / 100%)');
|
|
29
|
+
ctx.fillStyle = gradient;
|
|
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
|
+
ctx.fillStyle = `hsl(${timeMs / 5000 * 360} 100% 50%)`;
|
|
36
|
+
ctx.fillRect(100, 100, 200, 200);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function frame(timeMs) {
|
|
40
|
+
draw(timeMs);
|
|
41
|
+
hdr.render();
|
|
42
|
+
requestAnimationFrame(frame);
|
|
43
|
+
}
|
|
44
|
+
requestAnimationFrame(frame);
|
|
45
|
+
</script>
|
|
46
|
+
</body>
|
|
47
|
+
|
|
48
|
+
</html>
|
package/dist/bright-canvas.js
CHANGED
|
@@ -23,14 +23,26 @@
|
|
|
23
23
|
// probing would call getContext() on
|
|
24
24
|
// the source canvas, which locks a
|
|
25
25
|
// fresh canvas to that context type.
|
|
26
|
+
// alpha (default true) - true preserves source transparency
|
|
27
|
+
// ('premultiplied' alpha mode), false
|
|
28
|
+
// composites opaque ('opaque'). A string
|
|
29
|
+
// is passed through as the alphaMode
|
|
30
|
+
// verbatim, for any future modes.
|
|
31
|
+
// Note: brightening semi-transparent
|
|
32
|
+
// pixels can push color above alpha
|
|
33
|
+
// (undefined per spec; Chrome composites
|
|
34
|
+
// it additively, like a glow).
|
|
26
35
|
|
|
27
36
|
async function initHdrCanvas(
|
|
28
37
|
srcCanvas,
|
|
29
38
|
outCanvas,
|
|
30
|
-
{ debug = false, brightness = 2, sourceFloat16 = false } = {},
|
|
39
|
+
{ debug = false, brightness = 2, sourceFloat16 = false, alpha = true } = {},
|
|
31
40
|
) {
|
|
32
41
|
const log = debug ? (msg) => console.log("[bright-canvas]", msg) : () => {};
|
|
33
42
|
|
|
43
|
+
const alphaMode = typeof alpha === "string" ? alpha : alpha ? "premultiplied" : "opaque";
|
|
44
|
+
const transparent = alphaMode !== "opaque";
|
|
45
|
+
|
|
34
46
|
const W = srcCanvas.width;
|
|
35
47
|
const H = srcCanvas.height;
|
|
36
48
|
|
|
@@ -65,10 +77,11 @@ async function initHdrCanvas(
|
|
|
65
77
|
device,
|
|
66
78
|
format: "rgba16float",
|
|
67
79
|
toneMapping: { mode: "extended" },
|
|
80
|
+
alphaMode,
|
|
68
81
|
});
|
|
69
82
|
presentationFormat = "rgba16float";
|
|
70
83
|
} catch (e) {
|
|
71
|
-
log(`⚠️
|
|
84
|
+
log(`⚠️ Configuration not supported: ${e.message}`);
|
|
72
85
|
}
|
|
73
86
|
probeContext.unconfigure();
|
|
74
87
|
}
|
|
@@ -88,6 +101,9 @@ async function initHdrCanvas(
|
|
|
88
101
|
setBrightness() {}, // no-op in SDR mode
|
|
89
102
|
render() {
|
|
90
103
|
if (!outCtx) return;
|
|
104
|
+
if (transparent) {
|
|
105
|
+
outCtx.clearRect(0, 0, outCanvas.width, outCanvas.height);
|
|
106
|
+
}
|
|
91
107
|
outCtx.drawImage(srcCanvas, 0, 0, outCanvas.width, outCanvas.height);
|
|
92
108
|
},
|
|
93
109
|
destroy() {
|
|
@@ -102,9 +118,11 @@ async function initHdrCanvas(
|
|
|
102
118
|
device,
|
|
103
119
|
format: presentationFormat,
|
|
104
120
|
toneMapping: { mode: "extended" },
|
|
121
|
+
alphaMode,
|
|
105
122
|
});
|
|
106
123
|
log(`ℹ️ WebGPU canvas format: ${presentationFormat}`);
|
|
107
124
|
log(`ℹ️ Source texture format: ${srcTextureFormat}`);
|
|
125
|
+
log(`ℹ️ Alpha mode: ${alphaMode}`);
|
|
108
126
|
|
|
109
127
|
if ("getConfiguration" in GPUCanvasContext.prototype) {
|
|
110
128
|
const mode = context.getConfiguration()?.toneMapping?.mode;
|
|
@@ -156,7 +174,7 @@ async function initHdrCanvas(
|
|
|
156
174
|
@fragment
|
|
157
175
|
fn fs(in: VSOut) -> @location(0) vec4f {
|
|
158
176
|
let c = textureSample(tex, samp, in.uv);
|
|
159
|
-
return vec4f(c.rgb * brightness, 1.0);
|
|
177
|
+
return vec4f(c.rgb * brightness, ${transparent ? "c.a" : "1.0"});
|
|
160
178
|
}
|
|
161
179
|
`,
|
|
162
180
|
});
|
|
@@ -201,7 +219,7 @@ async function initHdrCanvas(
|
|
|
201
219
|
if (!device) return;
|
|
202
220
|
device.queue.copyExternalImageToTexture(
|
|
203
221
|
{ source: srcCanvas },
|
|
204
|
-
{ texture: srcTexture },
|
|
222
|
+
{ texture: srcTexture, premultipliedAlpha: transparent },
|
|
205
223
|
[W, H],
|
|
206
224
|
);
|
|
207
225
|
|
|
@@ -210,7 +228,7 @@ async function initHdrCanvas(
|
|
|
210
228
|
colorAttachments: [
|
|
211
229
|
{
|
|
212
230
|
view: context.getCurrentTexture().createView(),
|
|
213
|
-
clearValue: [0, 0, 0, 1],
|
|
231
|
+
clearValue: [0, 0, 0, transparent ? 0 : 1],
|
|
214
232
|
loadOp: "clear",
|
|
215
233
|
storeOp: "store",
|
|
216
234
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
async function e(e,t,{debug:n=!1,brightness:a=2,sourceFloat16:r=!1}={}){const
|
|
1
|
+
async function e(e,t,{debug:n=!1,brightness:a=2,sourceFloat16:r=!1,alpha:o=!0}={}){const i=n?e=>console.log("[bright-canvas]",e):()=>{},u="string"==typeof o?o:o?"premultiplied":"opaque",l="opaque"!==u,s=e.width,c=e.height,g=r?"rgba16float":"rgba8unorm",d=window.matchMedia("(dynamic-range: high)");let f=null;if(navigator.gpu)if(d.matches){const e=await navigator.gpu.requestAdapter();f=await(e?.requestDevice())??null,f||i("⚠️ Failed to get a WebGPU device - falling back to a 2D canvas (SDR).")}else i("⚠️ Display isn't HDR-compatible - falling back to a 2D canvas (SDR).");else i("⚠️ WebGPU is not available - falling back to a 2D canvas (SDR).");let p=null;if(f){const e=document.createElement("canvas").getContext("webgpu");if(e){try{e.configure({device:f,format:"rgba16float",toneMapping:{mode:"extended"},alphaMode:u}),p="rgba16float"}catch(e){i(`⚠️ Configuration not supported: ${e.message}`)}e.unconfigure()}p||(i("⚠️ No usable WebGPU canvas format - falling back to a 2D canvas (SDR)."),f=null)}if(!f){let n=t.getContext("2d");return{mode:"sdr",setBrightness(){},render(){n&&(l&&n.clearRect(0,0,t.width,t.height),n.drawImage(e,0,0,t.width,t.height))},destroy(){n=null}}}let v=t.getContext("webgpu");if(v.configure({device:f,format:p,toneMapping:{mode:"extended"},alphaMode:u}),i(`ℹ️ WebGPU canvas format: ${p}`),i(`ℹ️ Source texture format: ${g}`),i(`ℹ️ Alpha mode: ${u}`),"getConfiguration"in GPUCanvasContext.prototype){const e=v.getConfiguration()?.toneMapping?.mode;i("extended"!==e?"⚠️ Browser doesn't support HDR canvas, fell back to '"+e+"').":"✅ HDR canvas enabled.")}else i("ℹ️ getConfiguration() unavailable.");let m=f.createTexture({size:[s,c],format:g,usage:GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.COPY_DST|GPUTextureUsage.RENDER_ATTACHMENT});const b=f.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, ${l?"c.a":"1.0"});\n }\n `});let h=f.createRenderPipeline({layout:"auto",vertex:{module:b,entryPoint:"vs"},fragment:{module:b,entryPoint:"fs",targets:[{format:p}]},primitive:{topology:"triangle-list"}}),x=f.createSampler({magFilter:"linear",minFilter:"linear"}),y=f.createBuffer({size:4,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST});f.queue.writeBuffer(y,0,new Float32Array([a]));let w=f.createBindGroup({layout:h.getBindGroupLayout(0),entries:[{binding:0,resource:x},{binding:1,resource:m.createView()},{binding:2,resource:{buffer:y}}]});return{mode:"hdr",setBrightness(e){f&&f.queue.writeBuffer(y,0,new Float32Array([e]))},render(){if(!f)return;f.queue.copyExternalImageToTexture({source:e},{texture:m,premultipliedAlpha:l},[s,c]);const t=f.createCommandEncoder(),n=t.beginRenderPass({colorAttachments:[{view:v.getCurrentTexture().createView(),clearValue:[0,0,0,l?0:1],loadOp:"clear",storeOp:"store"}]});n.setPipeline(h),n.setBindGroup(0,w),n.draw(3),n.end(),f.queue.submit([t.finish()])},destroy(){f&&(m.destroy(),y.destroy(),v.unconfigure(),f.destroy(),m=null,y=null,w=null,h=null,x=null,v=null,f=null,i("ℹ️ HDR canvas destroyed."))}}}export{e as initHdrCanvas};
|