bright-canvas 0.1.1 → 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 +7 -4
- 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'
|
|
@@ -56,6 +53,12 @@ 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.
|
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};
|