"saturation" globalCompositeOperation without changing transparency? - javascript

I have a canvas containing art on a transparent background. I desaturate it like this:
boardCtx.fillStyle = "rgba(0, 0, 0, 1.0)";
boardCtx.globalCompositeOperation = 'saturation';
boardCtx.fillRect(0, 0, boardCanvas.width, boardCanvas.height);
and find that the transparent background has turned opaque black. I wouldn't expect the saturation blend mode to change the alpha channel... am I doing something wrong? My current solution is to copy the canvas before desaturation and use it to mask the black background away from the desaturated copy, but that involves another canvas and a big draw... not ideal.

You can use ctx.filter
The 2D context filter can be used to apply various filters to the canvas.
ctx.filter = "saturate(0%)";
ctx.drawImage(ctx.canvas,0,0);
But this will add to the alpha if there is anti-aliasing / transparency, reducing quality.
Fix Alpha
To fix you need to use the ctx.globalCompositeOperation = "copy" operation.
ctx.filter = "saturate(0%)";
ctx.globalCompositeOperation = "copy";
ctx.drawImage(ctx.canvas,0,0);
// restore defaults;
ctx.filter = "";
ctx.globalCompositeOperation = "source-over";
This will stop the alpha channel from being modified.
Check support.
Warning. Check browser support at bottom of filter page. If no support you will have to use a copy of the canvas to restore the alpha if you use ctx.globalCompositeOperation = "saturation"

Blending modes will work only on the foreground (source) layer without respect to the alpha channel, while the regular composite operations only use alpha channels - this is why you see the opaque result.
To solve simply add a "clipping call" to the existing content after de-saturation process using composition mode "destination-out", then redraw the image:
// draw image 1. time
boardCtx.fillStyle = "#000";
boardCtx.globalCompositeOperation = 'saturation';
boardCtx.fillRect(0, 0, boardCanvas.width, boardCanvas.height);
boardCtx.globalCompositeOperation = 'destination-out';
// draw image again 2. time
This will also restore the original alpha channel.
If the art is not an image source then you can take a snapshot by drawing the canvas to a temporary canvas, then use that temporary canvas as image source when drawing back using the same steps as above.
You can also use filters as in the other answer (there is also a filter "grayscale" which is slightly more efficient than "saturate") but currently only Chrome (from v52) and Firefox (from v49) supports filter, as well as Webview on Android (from v52).
/*
CanvasRenderingContext2D.filter (EXPERIMENTAL, On Standard Track)
https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/filter
DESKTOP:
Chrome | Edge | Firefox | IE | Opera | Safari
----------+-----------+-----------+-----------+-----------+-----------
52 | ? | 49° | - | - | -
°) 35-48: Behind flag canvas.filters.enabled set to true.
MOBILE:
Chrome/A | Edge/mob | Firefox/A | Opera/A |Safari/iOS | Webview/A
----------+-----------+-----------+-----------+-----------+-----------
52 | ? | 49° | - | - | 52
°) 35-48: Behind flag canvas.filters.enabled set to true.
*/
A third approach is to iterate over the pixels and do the desaturation. This would only be necessary if you intend to support older browsers which do not support the blending modes.
var ctx = c.getContext("2d"), i = new Image;
i.onload = function() {
ctx.drawImage(this, 0, 0); // draw image normally
ctx.globalCompositeOperation = "saturation"; // desaturate (blending removes alpha)
ctx.fillRect(0, 0, c.width, c.height);
ctx.globalCompositeOperation = "destination-in"; // knock out the alpha channel
ctx.drawImage(this, 0, 0); // by redrawing image using this mode
};
i.src = "//i.stack.imgur.com/F4ukA.png";
<canvas id=c></canvas>

Related

Is it possible to not render an object in canvas outside of a given region?

For example context.fillText("foobar",30,0); would render the full text "foobar" 30 pixels down, but how could I keep the rightmost 20 pixels, to throw out a random number, from rendering? One solution for this is to render a white box immediately after to hide the rest of foobar. But this solution isn't compatible with other features I want to incorporate. I need a way to really keep the rest of foobar from rendering in the first place. Is this possible in canvas or would I need to use another graphics API?
.clip() allows you to use paths to form a mask. This, combined with the various path methods, would allow you to draw a clipped version of your text.
An example, from the MDN page, uses a circle to mask a rectangle:
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Create circular clipping region
ctx.beginPath();
ctx.arc(100, 75, 50, 0, Math.PI * 2);
ctx.clip();
// Draw stuff that gets clipped
ctx.fillStyle = 'blue';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'orange';
ctx.fillRect(0, 0, 100, 100);
<canvas id="canvas"></canvas>

Three.js sets Texture RGB values to zero when ALPHA is zero on IOS

I am working on a WebGL project using javascript and the three.js framework. For that I am writing a custom shader with GLSL in which I have to load several lookup tables. Meaning I need to use some textures' individual RGBA values for some calculations rather than displaying them.
This works fine on all devices that I've tested. However, on iOS devices (like an iPad) the RGB values of a texture are automatically set to 0 when its alpha channel is 0. I do not think that this is due to GLSL's texture2D function but rather has something to do with how three.js loads textures on iOS. I am using the built-in TextureLoader for that:
var textureLoader = new THREE.TextureLoader();
var lutMap = textureLoader.load('path/to/lookup/table/img.png');
lutMap.minFilter = THREE.NearestFilter;
lutMap.magFilter = THREE.NearestFilter;
lutMap.generateMipmaps = false;
lutMap.type = THREE.UnsignedByteType;
lutMap.format = THREE.RGBAFormat;
For testing purposes I've created a test image with constant RGB values (255,0,0) and with a constantly decreasing alpha value from the top-right corner to the bottom-left one with some pixels' alpha values being 0:
After the texture was loaded, I checked the zero-alpha pixels and their R values were indeed set to 0. I used the following code to read the image's data:
function getImageData( image ) {
var canvas = document.createElement( 'canvas' );
canvas.width = image.width;
canvas.height = image.height;
var context = canvas.getContext( '2d' );
context.drawImage( image, 0, 0 );
return context.getImageData( 0, 0, image.width, image.height );
}
The strange thing was that this was also true on my Windows PC, but the shader works just fine. So maybe this is only due to the canvas and has nothing to do with the actual problem. On the iOS device however, the texture2D(...) lookup in the GLSL code indeed returned (0,0,0,0) for exactly those pixels. (Please note that I come from Java/C++ and I am not very familiar with javascript yet! :) )
I've also tried to set the premultipliedAlpha flag to 0 in the WebGLRenderer instance, but also in the THREE.ShaderMaterial object itself. Sadly, It did not fix the problem.
Did anyone experience similar problems and knows how to fix this unwanted behaviour?
The low level PNG reading code on iOS will go through CoreGraphics and premultiply each RGB value by the A component for each pixel, so if A = 0 then each RGB value will come out as zero. What you can do is load a 24 BPP image, so that the alpha is always 0xFF (aka 255), but you cannot disable this premultiply step under iOS when dealing with a 32 BPP image.

HTML Canvas draw image from database [duplicate]

This question already has answers here:
Load image from url and draw to HTML5 Canvas
(3 answers)
Closed 4 years ago.
I want to get an image from my database, and show it in a canvas and draw shapes on it. Is there a way to draw image on canvas using image as byte array etc..
A <canvas> is like a regular <img> image with the difference that the Javascript code of the page can actually control the values of the pixels.
You can decide what is the content of the canvas by drawing primitives like lines, polygons, arcs or even other images. You can also set or alter the content by processing the actual red, green, blue and transparency values of every single pixel (using getImageData and putImageData). You can resize the canvas as you wish and drawing on a canvas is quite fast... fast enough to be able to do complex animations with just plain Javascript code.
For example the code to load an image and draw it in a canvas after changing it to black and white and adding a red grid could be:
function canvasTest(img) {
let canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
let ctx = canvas.getContext("2d");
// Copy image pixels to the canvas
ctx.drawImage(img, 0, 0);
// Now let's set R and B channels equal to G
let idata = ctx.getImageData(0, 0, canvas.width, canvas.height);
for (let i=0,n=img.width*img.height*4; i<n; i+=4) {
idata.data[i] = idata.data[i+2] = idata.data[i+1];
}
ctx.putImageData(idata, 0, 0);
// Add a red grid
ctx.beginPath();
for (let y=0; y<img.height; y+=50) {
ctx.moveTo(0, y+0.5); ctx.lineTo(img.width, y+0.5);
}
for (let x=0; x<img.width; x+=50) {
ctx.moveTo(x+0.5, 0); ctx.lineTo(x+0.5, img.height);
}
ctx.strokeStyle = "#F00";
ctx.lineWidth = 1;
ctx.stroke();
// add the final result to page
document.body.appendChild(canvas);
}
The only annoyance is that if you draw an image on a canvas and the image is coming from a different origin from where the page comes from, then the canvas becomes in the general case "tainted" and you cannot access the pixel values any more (getImageData is forbidden). The reason for this limit is security (and the sad fact that security in web application is on the client). In your case since the image database is yours there should be no problem.

HTML5 Canvas blendmode

I'm new to HTML5 canvas and I want to reproduce the result of BlendMode.ADD in ActionScript 3.
According to the documentation, here's what BlendMode.ADD does:
Adds the values of the constituent colors of the display object to the
colors of its background, applying a ceiling of 0xFF. This setting is
commonly used for animating a lightening dissolve between two objects.
For example, if the display object has a pixel with an RGB value of
0xAAA633, and the background pixel has an RGB value of 0xDD2200, the
resulting RGB value for the displayed pixel is 0xFFC833 (because 0xAA
+ 0xDD > 0xFF, 0xA6 + 0x22 = 0xC8, and 0x33 + 0x00 = 0x33).
Source: http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/display/BlendMode.html#ADD
How can I do the same thing to an image in HTML5 Canvas?
The specification of the 2D canvas has implemented the blending mode with the name "lighter" (not to be confused with "lighten" which is a different mode) that will do "add".
var ctx = c.getContext("2d");
ctx.fillStyle = "#037";
ctx.fillRect(0, 0, 130, 130);
ctx.globalCompositeOperation = "lighter"; // AKA add / linear-dodge
ctx.fillStyle = "#777";
ctx.fillRect(90, 20, 130, 130);
<canvas id=c></canvas>
(update: I was remembering (incorrectly) lighten as the name for it, so sorry for the extra manual step in the original version of the answer).

Merge canvas image and canvas alpha mask into dataurl generated png

given two canvas with the same pixel size, where canvas1 contains an arbitrary image (jpg, png or such) and canvas2 contains black and non-black pixels.
what i want to achive: using a third canvas3 i want to clone canvas1 and have every black canvas2 pixel (may including a threshold of blackness) be transparent in canvas3
i already have a working solution like this:
canvas3context.drawImage(canvas1,0,0);
var c3img = canvas3context.getImageData(0,0,canvas3.width,canvas3.height);
var c2img = canvas2context.getImageData(0,0,canvas2.width,canvas2.height);
loop(){
if(c2img i-th,i+1-th,i+2-th pixel is lower than threshold)
set c3img.data[i]=c3img.data[i+1]=c3img.data[i+2]=c3img.data[i+3]=0
}
the problem with above (pseudo) code is, that it is slow
so my question is: anyone can share an idea how to speed this up significantly?
i thought about webgl but i never worked with it - so i have no idea about shaders or the tools or terms needed for this. another idea was that maybe i could convert canvas2 to black&white somehow very fast (not just modifieng every pixel in a loop like above) and work with blend modes to generate the transparent pixels
any help is highly appreciated
answering my own question, i provide a solution for merging an arbitrary image with a black&white image. what im still missing is how to set the alpha channel for just one color of a canvas.
I seperate the question in pieces and answer them each.
Question 1: How to convert a canvas into grayscale without iterating every pixel?
Answer: draw the image on to a white canvas with blend mode 'luminosity'
function convertCanvasToGrayscale(canvas){
var tmp = document.createElement('canvas');
tmp.width = canvas.width;
tmp.height = canvas.height;
var tmpctx = tmp.getContext('2d');
// conversion
tmpctx.globalCompositeOperation="source-over"; // default composite value
tmpctx.fillStyle="#FFFFFF";
tmpctx.fillRect(0,0,canvas.width,canvas.height);
tmpctx.globalCompositeOperation="luminosity";
tmpctx.drawImage(canvas,0,0);
// write converted back to canvas
ctx = canvas.getContext('2d');
ctx.globalCompositeOperation="source-over";
ctx.drawImage(tmp, 0, 0);
}
Question 2: How to convert a grayscale canvas into black&white without iterating every pixel?
Answer: two times color-dodge blend mode with color #FEFEFE will do the job
function convertGrayscaleCanvasToBlackNWhite(canvas){
var ctx = canvas.getContext('2d');
// in case the grayscale conversion is to bulky for ya
// darken the canvas bevore further black'nwhite conversion
//for(var i=0;i<3;i++){
// ctx.globalCompositeOperation = 'multiply';
// ctx.drawImage(canvas, 0, 0);
//}
ctx.globalCompositeOperation = 'color-dodge';
ctx.fillStyle = "rgba(253, 253, 253, 1)";
ctx.beginPath();
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fill();
ctx.globalCompositeOperation = 'color-dodge';
ctx.fillStyle = "rgba(253, 253, 253, 1)";
ctx.beginPath();
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fill();
}
Note: this function assumes that you want black areas left black and every non-black pixel become white! thus a grayscale image which has no black pixel will become completely white
the reason i choose this operation is that it worked better in my case and using only two blend operations means its pretty fast - if you want that more dark pixel be left black and more white pixel become white you can use the commented for loop to darken the image beforehand. thus dark pixel will become black and brighter pixel become darker. as you increase the amount of black pixel's using color-dodge will again do the rest of the job
Question 3: How to merge a Black&White canvas with another canvas without iterating every pixel?
Answer: use 'multiply' blend mode
function getBlendedImageWithBlackNWhite(canvasimage, canvasbw){
var tmp = document.createElement('canvas');
tmp.width = canvasimage.width;
tmp.height = canvasimage.height;
var tmpctx = tmp.getContext('2d');
tmpctx.globalCompositeOperation = 'source-over';
tmpctx.drawImage(canvasimage, 0, 0);
// multiply means, that every white pixel gets replaced by canvasimage pixel
// and every black pixel will be left black
tmpctx.globalCompositeOperation = 'multiply';
tmpctx.drawImage(canvasbw, 0, 0);
return tmp;
}
Question 4: How to invert a Black&White canvas without iterating every pixel?
Answer: use 'difference' blend mode
function invertCanvas(canvas){
var ctx = canvas.getContext("2d");
ctx.globalCompositeOperation = 'difference';
ctx.fillStyle = "rgba(255, 255, 255, 1)";
ctx.beginPath();
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fill();
}
now to 'merge' an canvasimage with a canvasmask one can do
convertCanvasToGrayscale(canvasmask);
convertGrayscaleCanvasToBlackNWhite(canvasmask);
result = getBlendedImageWithBlackNWhite(canvasimage, canvasmask);
regarding performance: obviously those blend modes are much faster than modifieng every pixel and to get a bit faster one can pack all functions together as needed into one function and recycle only one tmpcanvas - but thats left to the reader ^^
as a sidenote: i tested how the size of the resulting png differs when you compare above's getBlendedImageWithBlackNWhite result with the same image but the black areas are made transparent by iterating every pixel and setting the alpha channel
the difference in size is nearly nothing and thus if you dont really need the alpha-mask the information that every black pixel is meant to be transparent may be enough for futher processing
note: one can invert the meaning of black and white using the invertCanvas() function
if you want to know more of why i use those blend modes or how blend modes really work
you should check the math behind them - imho you're not able to develop such functions if ya dont know how they really work:
math behind blend modes
canvas composition standard including a bit math
need an example - spot the difference: http://jsfiddle.net/C3fp4/1/

Categories

Resources