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 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(`⚠️ Canvas format 'rgba16float' not supported: ${e.message}`);
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>
@@ -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(`⚠️ Canvas format 'rgba16float' not supported: ${e.message}`);
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 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};
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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bright-canvas",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Brighten any canvas with proper HDR support. No flags required.",
5
5
  "license": "MIT",
6
6
  "author": "Inclushe",