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 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](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)
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](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)
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(`⚠️ 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.0",
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",