hue-shift given rgba value in javascript? - javascript

I'm trying to use a canvas to draw an image with a colored filter over it. As css has the hue-shift filter, I'm using that to apply on the canvas. The catch is that I have a palette and I want to shift the color via the color the user presses on the palette. I've already gotten the RGBA value from clicking on the palette - I'm just not sure how to use this to create a color filter for the image itself.
function changeColor2(e) {
x2 = e.offsetX;
y2 = e.offsetY;
var imageData2 = firstContext2.getImageData(x2, y2, 1, 1).data;
rgbaColor2 = 'rgba(' + imageData2[0] + ',' + imageData2[1] + ',' + imageData2[2] + ',1)';
colorLabel2.style.backgroundColor = rgbaColor2;
console.log(rgbaColor2);
context.filter= 'grayscale(100%)' + 'sepia(100%)';
//context.strokeStyle = 'rgba(' + imageData[0] + ',' + imageData[1] + ',' + imageData[2] + ',1)';
}
Where would I go from here to get the color shift?

Maybe the "hue" blending mode will do for you:
You draw the image where you want to apply the color shift.
You set the context's globalCompositeOperation to "hue".
You fill a rectangle of the target color over the whole image.
const inp = document.querySelector("input");
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const img = new Image();
img.src = "https://picsum.photos/300/300";
img.decode().then(() => {
canvas.width = img.width;
canvas.height = img.height;
inp.oninput = e => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
ctx.globalCompositeOperation = "hue";
ctx.fillStyle = inp.value;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.globalCompositeOperation = "source-over";
};
inp.oninput();
});
<input type=color value=#cca633><br>
<canvas></canvas>

Related

Intersected semi-transparent stroke text

I have an issue with the painting context.stokeText that style contains an alpha. A big value of line width makes some effect of intersected strokes, as a result, the color is darker.
How I can avoid this?
ctx.strokeStyle ="rgba(0,0,0,0.3)";
ctx.lineWidth = 15;
ctx.lineJoin="round";
ctx.strokeText(text, x, y);
Image
That's a bit of an inconsistency in the specs since usually overlapping sub-pathes are painted only once.
However strokeText() does create one shape per glyph, and thus this method will indeed paint each glyphs on their own, creating this visible overlapping.
To overcome this, you'll to be a bit creative:
first draw your text fully opaque,
then redraw the produced pixels with the desired alpha level (many ways to do so).
draw that on your scene (or draw the background behind).
Here are a few ways (there are many others):
Probably the easiest, but which costs more memory: use a second disconnected canvas:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
// create a new canvas just for the text
const canvas2 = canvas.cloneNode();
const ctx2 = canvas2.getContext("2d");
ctx2.font = "60px sans-serif";
const text = "MY TEXT";
const x = canvas.width - ctx2.measureText(text).width - 20;
const y = canvas.height - 20;
// draw it fully opaque
ctx2.lineWidth = 15;
ctx2.lineJoin="round";
ctx2.strokeText(text, x, y);
// draw the background on the visible canvas
ctx.fillStyle = "#ffe97f";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// now draw our text canvas onto the visible one
// with the desired opacity
ctx.globalAlpha = 0.3;
ctx.drawImage(canvas2, 0, 0);
<canvas width="465" height="234"></canvas>
More memory friendly, but which requires you to rewrite your drawing logic in a different direction, use compositing:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.font = "60px sans-serif";
const text = "MY TEXT";
const x = canvas.width - ctx.measureText(text).width - 20;
const y = canvas.height - 20;
// first draw the text fully opaque
ctx.lineWidth = 15;
ctx.lineJoin="round";
ctx.strokeText(text, x, y);
// now apply the opacity
ctx.fillStyle ="rgba(0,0,0,0.3)";
ctx.globalCompositeOperation = "source-in";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// and the background
ctx.fillStyle = "#ffe97f";
ctx.globalCompositeOperation = "destination-over";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// if you want to keep drawing "normaly"
ctx.globalCompositeOperation = "source-over";
<canvas width="465" height="234"></canvas>
A mix of both, with different compositing rules:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.font = "60px sans-serif";
const text = "MY TEXT";
const x = canvas.width - ctx.measureText(text).width - 20;
const y = canvas.height - 20;
// first draw the text fully opaque
ctx.lineWidth = 15;
ctx.lineJoin="round";
ctx.strokeText(text, x, y);
// now redraw over itself with the desired opacity
ctx.globalAlpha = 0.3;
ctx.globalCompositeOperation = "copy";
ctx.drawImage(canvas, 0, 0);
ctx.globalAlpha = 1;
// and the background
ctx.fillStyle = "#ffe97f";
ctx.globalCompositeOperation = "destination-over";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// if you want to keep drawing "normaly"
ctx.globalCompositeOperation = "source-over";
<canvas width="465" height="234"></canvas>

Multi color image into single color image in canvas JavaScript [duplicate]

I want to change the background color of this image while keeping the form, the effects and the contour of
the image.
<canvas id="canvas01" width="1200" height="800"></canvas>
<script>
function drawImage(imageObj,x, y, width, height){
var canvas = document.getElementById('canvas01');
var context = canvas.getContext('2d');
context.drawImage(imageObj, x, y, width, height);
}
var image = new Image();
image.onload = function(){
drawImage(this, 400, 100, 320, 450);
};
image.src ="images/658FFBC6.png";
</script>
Luma preservation
At the risk of looking similar to the existing answer, I would like to point out a small but important difference using a slightly different approach.
The key is to preserve the luma component in an image (ie. shadow details, wrinkles etc. in this case) so two steps are needed to control the look using blending modes via globalCompositeOperation (or alternatively, a manual approach using conversion between RGB and the HSL color-space if older browsers must be supported):
"saturation": will alter the chroma (intensity, saturation) from the next drawn element and apply it to the existing content on the canvas, but preserve luma and hue.
"hue": will grab the chroma and luma from the source but alter the hue, or color if you will, based on the next drawn element.
As these are blending modes (ignoring the alpha channel) we will also need to clip the result using composition as a last step.
The color blending mode can be used too but it will alter luma which may or may not be desirable. The difference can be subtle in many cases, but also very obvious depending on target chroma and hue where luma/shadow definition is lost.
So, to achieve a good quality result preserving both luma and chroma, these are more or less the main steps (assumes an empty canvas):
// step 1: draw in original image
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(img, 0, 0);
// step 2: adjust saturation (chroma, intensity)
ctx.globalCompositeOperation = "saturation";
ctx.fillStyle = "hsl(0," + sat + "%, 50%)"; // hue doesn't matter here
ctx.fillRect(0, 0);
// step 3: adjust hue, preserve luma and chroma
ctx.globalCompositeOperation = "hue";
ctx.fillStyle = "hsl(" + hue + ",1%, 50%)"; // sat must be > 0, otherwise won't matter
ctx.fillRect(0, 0, c.width, c.height);
// step 4: in our case, we need to clip as we filled the entire area
ctx.globalCompositeOperation = "destination-in";
ctx.drawImage(img, 0, 0);
// step 5: reset comp mode to default
ctx.globalCompositeOperation = "source-over";
50% lightness (L) will keep the original luma value.
Live Example
Click the checkbox to see the effect on the result. Then test with different chroma and hue settings.
var ctx = c.getContext("2d");
var img = new Image(); img.onload = demo; img.src = "//i.stack.imgur.com/Kk1qd.png";
function demo() {c.width = this.width>>1; c.height = this.height>>1; render()}
function render() {
var hue = +rHue.value, sat = +rSat.value, l = +rL.value;
ctx.clearRect(0, 0, c.width, c.height);
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(img, 0, 0, c.width, c.height);
if (!!cColor.checked) {
// use color blending mode
ctx.globalCompositeOperation = "color";
ctx.fillStyle = "hsl(" + hue + "," + sat + "%, 50%)";
ctx.fillRect(0, 0, c.width, c.height);
}
else {
// adjust "lightness"
ctx.globalCompositeOperation = l < 100 ? "color-burn" : "color-dodge";
// for common slider, to produce a valid value for both directions
l = l >= 100 ? l - 100 : 100 - (100 - l);
ctx.fillStyle = "hsl(0, 50%, " + l + "%)";
ctx.fillRect(0, 0, c.width, c.height);
// adjust saturation
ctx.globalCompositeOperation = "saturation";
ctx.fillStyle = "hsl(0," + sat + "%, 50%)";
ctx.fillRect(0, 0, c.width, c.height);
// adjust hue
ctx.globalCompositeOperation = "hue";
ctx.fillStyle = "hsl(" + hue + ",1%, 50%)";
ctx.fillRect(0, 0, c.width, c.height);
}
// clip
ctx.globalCompositeOperation = "destination-in";
ctx.drawImage(img, 0, 0, c.width, c.height);
// reset comp. mode to default
ctx.globalCompositeOperation = "source-over";
}
rHue.oninput = rSat.oninput = rL.oninput = cColor.onchange = render;
body {font:16px sans-serif}
<div>
<label>Hue: <input type=range id=rHue max=359 value=0></label>
<label>Saturation: <input type=range id=rSat value=100></label>
<label>Lightness: <input type=range id=rL max=200 value=100></label>
<label>Use "color" instead: <input type=checkbox id=cColor></label>
</div>
<canvas id=c></canvas>
Global composite operations
The 2D context property ctx.globalCompositeOperation is very useful for a wide range of image processing tasks. For more on globalCompositeOperation at MDN
You can convert the image into a canvas, that way you can edit it.
function imageToCanvas(image){
const c = document.createElement("canvas");
c.width = image.width;
c.height = image.height;
c.ctx = c.getContext("2d"); // attach context to the canvas for eaasy reference
c.ctx.drawImage(image,0,0);
return c;
}
You can use the globalCompositeOperation = "color" to colour the image
function colorImage(image,color){ // image is a canvas image
image.ctx.fillStyle = color;
image.ctx.globalCompositeOperation = "color";
image.ctx.fillRect(0,0,image.width,image.height);
image.ctx.globalCompositeOperation = "source-over";
return image;
}
Unfortunately this also overwrites the alpha pixels so you need to use the original image as a mask to restore the alpha pixels.
function maskImage(dest,source){
dest.ctx.globalCompositeOperation = "destination-in";
dest.ctx.drawImage(source,0,0);
dest.ctx.globalCompositeOperation = "source-over";
return dest;
}
And then you have a coloured image
Example.
In he example I colour the image in a range of colours and added a function to restore the canvas copy of the image back to the original. If you get the image from the page as an element then use naturalWidth and naturalHeight as the width and height properties may not match the image resolution.
const ctx = canvas.getContext("2d");
const image = new Image;
var colCopy;
image.src = "https://i.stack.imgur.com/Kk1qd.png";
image.onload = () => {
colCopy = imageToCanvas(image);
const scale = canvas.height / image.naturalHeight;
ctx.scale(scale, scale);
ctx.drawImage(colCopy, 0, 0);
for (var i = 32; i < 360; i += 32) {
restoreImage(colCopy, image);
colorImage(colCopy, "hsl(" + i + ",100%,50%)");
maskImage(colCopy, image);
ctx.drawImage(colCopy, 150 * i / 16, 0);
}
}
function imageToCanvas(image) {
const c = document.createElement("canvas");
c.width = image.naturalWidth;
c.height = image.naturalHeight;
c.ctx = c.getContext("2d"); // attach context to the canvas for easy reference
c.ctx.drawImage(image, 0, 0);
return c;
}
function restoreImage(dest, source) {
dest.ctx.clearRect(0, 0, dest.width, dest.height);
dest.ctx.drawImage(source, 0, 0);
return dest;
}
function colorImage(dest, color) { // image is a canvas image
dest.ctx.fillStyle = color;
dest.ctx.globalCompositeOperation = "color";
dest.ctx.fillRect(0, 0, dest.width, dest.height);
dest.ctx.globalCompositeOperation = "source-over";
return dest;
}
function maskImage(dest, source) {
dest.ctx.globalCompositeOperation = "destination-in";
dest.ctx.drawImage(source, 0, 0);
dest.ctx.globalCompositeOperation = "source-over";
return dest;
}
canvas {
border: 2px solid black;
}
<canvas id="canvas" width=600></canvas>
The image can get a little washed out in some situations, you can convert the image to a higher contrast black and white image using composite operations similar to shown above, and use the high contrast image as the template to colour.
Using Filters
Most of the common browsers now support canvas filters which has a hue shift filter. You can use that to shift the hue to the value you want, though first you will need to know what the image original hue is. (see below example on how to find HUE)
See Canvas filters at MDN for compatibility and how to use canvas filters.
The following function will preserve the saturation and just shift the hue.
// dest canvas to hold the resulting image
// source the original image
// hue The hue to set the dest image to
// sourceHue the hue reference point of the original image.
function colorImage(dest,source, hue , sourceHue) { // image is a canvas image
dest.ctx.clearRect(0,0,dest.width, dest.height);
dest.ctx.filter="hue-rotate("+((hue - sourceHue) | 0)+"deg)";
dest.ctx.drawImage(source,0, 0, dest.width, dest.height);
return dest;
}
Filters example.
The following uses ctx.filter = "hue-rotate(30deg)" to rotate the hue. I have not included any code to find the image original hue so manually set it by eye to 120.
const ctx = canvas.getContext("2d");
const image = new Image;
var colCopy;
const sourceHue = 120;
image.src = "https://i.stack.imgur.com/Kk1qd.png";
image.onload = () => {
colCopy = imageToCanvas(image);
const scale = canvas.height / image.naturalHeight;
ctx.scale(scale, scale);
ctx.drawImage(colCopy, 0, 0);
for (var i = 32; i < 360; i += 32) {
colorImage(colCopy,image,i,sourceHue);
ctx.drawImage(colCopy, 150 * i / 16, 0);
}
}
function imageToCanvas(image) {
const c = document.createElement("canvas");
c.width = image.naturalWidth;
c.height = image.naturalHeight;
c.ctx = c.getContext("2d"); // attach context to the canvas for easy reference
c.ctx.drawImage(image, 0, 0);
return c;
}
function colorImage(dest,source, hueRotate , sourceHue) { // image is a canvas image
dest.ctx.clearRect(0,0,dest.width, dest.height);
dest.ctx.filter="hue-rotate("+((hueRotate - sourceHue) | 0)+"deg)";
dest.ctx.drawImage(source,0, 0, dest.width, dest.height);
return dest;
}
canvas {
border: 2px solid black;
}
<canvas id="canvas" width=600></canvas>
RGB to Hue
There are plenty of answers to help find the hue of a pixel here on SO. Here is a particularly detailed one RGB to HSL conversion.
Filters example White.
The following uses ctx.filter = "grayscale(100%)" to remove saturation and then ctx.filter = "brightness(amount%)" to change the brightness. This gives a range of gray colours from black to white. You can also do the same with the colour, by reducing the grayscale amount.
const ctx = canvas.getContext("2d");
const image = new Image;
var colCopy;
const sourceHue = 120;
image.src = "https://i.stack.imgur.com/Kk1qd.png";
image.onload = () => {
colCopy = imageToCanvas(image);
const scale = canvas.height / image.naturalHeight;
ctx.scale(scale, scale);
ctx.drawImage(colCopy, 0, 0);
for (var i = 40; i < 240; i += 20) {
grayImage(colCopy,image,i);
ctx.drawImage(colCopy, 150 * ((i-40) / 12), 0);
}
}
function imageToCanvas(image) {
const c = document.createElement("canvas");
c.width = image.naturalWidth;
c.height = image.naturalHeight;
c.ctx = c.getContext("2d"); // attach context to the canvas for easy reference
c.ctx.drawImage(image, 0, 0);
return c;
}
function grayImage(dest,source, brightness) { // image is a canvas image
dest.ctx.clearRect(0,0,dest.width, dest.height);
dest.ctx.filter = "grayscale(100%)";
dest.ctx.drawImage(source,0, 0, dest.width, dest.height);
dest.ctx.filter = "brightness(" + brightness +"%)";
dest.ctx.drawImage(dest,0, 0, dest.width, dest.height);
return dest;
}
canvas {
border: 2px solid black;
}
<canvas id="canvas" width=600></canvas>
You can combine filters on a single line of code before performing your draw operation, like this:
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const image = document.getElementById('source');
ctx.filter = 'hue-rotate(120deg) grayscale(10%) brightness(150%)';
ctx.drawImage(image, 10, 10, 180, 120);
<canvas id="canvas"></canvas>
<div style="display:none;">
<img id="source"
src="https://interactive-examples.mdn.mozilla.net/media/examples/gecko-320-213.jpg">
</div>

How can I grayscale a canvas image in JavaScript?

I have an image inside of an HTML5 canvas with the size of 28x28 pixels. I get the imageData of the canvas as an array of RGBA (red, green, blue, alpha) values using this code:
canvas = document.getElementById('canvas');
ctx = canvas.getContext("2d");
imgData = ctx.getImageData(0, 0, 28, 28);
Now I want to grayscale the image, so that I get an array of 784 values (28x28 pixels) where each pixel has one value (instead of four).
I've found a lot of different formulas for grayscaling, some are multiplying the rgb values, some are just calculating the average - I really don't know which of them to use...
I'm also stuck at getting 784 values - it's always 3136 (because of the 4 channels)...
Thanks in advance!
The main idea is to have the same value for the red green and blue component of the color. For this you need to calculate the lightness of every pixel. There are several ways to calculate the lightness. This is one of them.
window.onload = function() {
let canvas = document.getElementById("c");
let ctx = canvas.getContext("2d");
canvas.width = 50;
canvas.height = 50;
let srcImg = document.getElementById("sof");
ctx.drawImage(srcImg, 0, 0, ctx.canvas.width, ctx.canvas.height);
let imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
let pixels = imgData.data;
for (var i = 0; i < pixels.length; i += 4) {
let lightness = parseInt((pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3);
pixels[i] = lightness;
pixels[i + 1] = lightness;
pixels[i + 2] = lightness;
}
ctx.putImageData(imgData, 0, 0);
}
<canvas id="c"></canvas>
<img src="data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAAZAAD/4QMtaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjMtYzAxMSA2Ni4xNDU2NjEsIDIwMTIvMDIvMDYtMTQ6NTY6MjcgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDUzYgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTJDRTJFNzlFMzkzMTFFODlEQ0FEQ0NCN0JCMjUxRTEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTJDRTJFN0FFMzkzMTFFODlEQ0FEQ0NCN0JCMjUxRTEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoxMkNFMkU3N0UzOTMxMUU4OURDQURDQ0I3QkIyNTFFMSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoxMkNFMkU3OEUzOTMxMUU4OURDQURDQ0I3QkIyNTFFMSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv/uAA5BZG9iZQBkwAAAAAH/2wCEABENDQ0ODRIODhIaEQ8RGh8XEhIXHyIXFxcXFyIjGx4dHR4bIyMpKi0qKSM2Njs7NjZBQUFBQUFBQUFBQUFBQUEBEhERFBYUGBUVGBcTFxMXHRcZGRcdLB0dIB0dLDgoIyMjIyg4MjUtLS01Mj09ODg9PUFBQUFBQUFBQUFBQUFBQf/AABEIADQAMgMBIgACEQEDEQH/xACCAAEAAgMBAAAAAAAAAAAAAAAAAwQBAgUGAQEAAwEAAAAAAAAAAAAAAAAAAgMEARAAAQMCAgYHBwUAAAAAAAAAAQACAxEEIRIxQYHREwXwUWGRoSIVcbHBMkJSY2JyIxQ1EQACAgEEAgMBAAAAAAAAAAAAARECITFRYQMSQkGBE3H/2gAMAwEAAhEDEQA/APcIiIAijnnit4zLK7Kwd9TqAVC/fMwxcwtpOJCweZgPkynXt8FC91VN6xlpaxuTp1uzS0nCb0nY6aKOGaOeJssZq1w2g6wfYpFJNNSvki004eGgiIunAiLUvBa4xkPc0HAGvm6kBRn5haiZ9pdRkR6MzhVrvjtVUsksKzWxFzYSfOyuagPXv71vJfyFmS/sSW66YjZXeqrDbcSthcPglcQOFICWuJ1VFfFYr3lzKb9fS64h6m6lIUQ0vZT50fMrNS3y0PZOf638ljKMxJwMbh9J/V7wuso4WRRNEMeUFuJa2gxOk0Ui1ddPCsffC/hl7b+dp+uXy+QiIplZkaVzrQS8G64FBLmOSujN2roa1S9OFSRM8VNcMNKPKOpw5IcvPfuj8NyR23MzPG+dsLmtcC4hozAdhopvT/zv6bU9P/O/ptVX4rE27HG9i39nmKdanapmL/Rm/buVxVoLQQSGTiF5IpirKtKQiIgCIiAIiIAiIgCIiA//2Q=="
id="sof" />
UPDATE:
Alternative lightness Calculations:
Wikipedia (Luma):
let lightness = parseInt(pixels[i]*.299 + pixels[i + 1]*.587 + pixels[i + 2]*.114);
elsewhere (source unknown):
let lightness = parseInt(3*pixels[i] + 4*pixels[i + 1] + pixels[i + 2] >>> 3);
Wikipedia (Linear Luminance):
let lightness = 0.2126 * pixels[i] + 0.715 * pixels[i+1] + 0.0722 * pixels[i+2];
You can directly set a filter to the rendering context. It will be simpler than calculate every pixel color.
For this you should use ctx.filter = 'grayscale(1)';
const img = document.querySelector('img');
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
img.onload = function() {
canvas.width = img.width;
canvas.height = img.height;
ctx.filter = 'grayscale(1)';
ctx.drawImage(img, 0, 0, img.width, img.height);
}
<canvas></canvas>
<img src="data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAAZAAD/4QMtaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjMtYzAxMSA2Ni4xNDU2NjEsIDIwMTIvMDIvMDYtMTQ6NTY6MjcgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDUzYgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTJDRTJFNzlFMzkzMTFFODlEQ0FEQ0NCN0JCMjUxRTEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTJDRTJFN0FFMzkzMTFFODlEQ0FEQ0NCN0JCMjUxRTEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoxMkNFMkU3N0UzOTMxMUU4OURDQURDQ0I3QkIyNTFFMSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoxMkNFMkU3OEUzOTMxMUU4OURDQURDQ0I3QkIyNTFFMSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv/uAA5BZG9iZQBkwAAAAAH/2wCEABENDQ0ODRIODhIaEQ8RGh8XEhIXHyIXFxcXFyIjGx4dHR4bIyMpKi0qKSM2Njs7NjZBQUFBQUFBQUFBQUFBQUEBEhERFBYUGBUVGBcTFxMXHRcZGRcdLB0dIB0dLDgoIyMjIyg4MjUtLS01Mj09ODg9PUFBQUFBQUFBQUFBQUFBQf/AABEIADQAMgMBIgACEQEDEQH/xACCAAEAAgMBAAAAAAAAAAAAAAAAAwQBAgUGAQEAAwEAAAAAAAAAAAAAAAAAAgMEARAAAQMCAgYHBwUAAAAAAAAAAQACAxEEIRIxQYHREwXwUWGRoSIVcbHBMkJSY2JyIxQ1EQACAgEEAgMBAAAAAAAAAAAAARECITFRYQMSQkGBE3H/2gAMAwEAAhEDEQA/APcIiIAijnnit4zLK7Kwd9TqAVC/fMwxcwtpOJCweZgPkynXt8FC91VN6xlpaxuTp1uzS0nCb0nY6aKOGaOeJssZq1w2g6wfYpFJNNSvki004eGgiIunAiLUvBa4xkPc0HAGvm6kBRn5haiZ9pdRkR6MzhVrvjtVUsksKzWxFzYSfOyuagPXv71vJfyFmS/sSW66YjZXeqrDbcSthcPglcQOFICWuJ1VFfFYr3lzKb9fS64h6m6lIUQ0vZT50fMrNS3y0PZOf638ljKMxJwMbh9J/V7wuso4WRRNEMeUFuJa2gxOk0Ui1ddPCsffC/hl7b+dp+uXy+QiIplZkaVzrQS8G64FBLmOSujN2roa1S9OFSRM8VNcMNKPKOpw5IcvPfuj8NyR23MzPG+dsLmtcC4hozAdhopvT/zv6bU9P/O/ptVX4rE27HG9i39nmKdanapmL/Rm/buVxVoLQQSGTiF5IpirKtKQiIgCIiAIiIAiIgCIiA//2Q=="
/>

Blur behind transparent box in JavaScript canvas

How can I achieve a blur behind a transparent box (fillStyle = 'rgba(255, 255, 255, 0.2)') in JavaScript canvas? Here's what I've got so far:
var canvas = document.getElementById('draw');
var c = canvas.getContext('2d');
function main() {
c.fillStyle = '#222';
c.fillRect(0, 0, canvas.width, canvas.height);
c.fillStyle = '#000';
c.fillRect(32, 32, 64, 64);
c.fillStyle = 'rgba(255, 255, 255, 0.2)';
c.filter = 'blur(5px)';
c.fillRect(16, 16, 128, 24);
}
But what happens, is instead of blurring the background behind the rectangle, is the rectangle itself is blurred, kind of obviously.
In the final script, I will probably use paths instead of rects.
Context2D filters will be applied only on your new drawings, so to also blur the background, you would actually have to redraw the part of the background you want to be blurred.
Fortunately, canvas can drawImage itself.
var blurredRect = {
x: 80,
y: 80,
height: 200,
width: 200,
spread: 10
};
var ctx = canvas.getContext('2d');
var img = new Image();
img.onload = draw;
img.src = 'https://upload.wikimedia.org/wikipedia/commons/5/55/John_William_Waterhouse_A_Mermaid.jpg';
function draw() {
canvas.width = img.width / 2;
canvas.height = img.height / 2;
// first pass draw everything
ctx.drawImage(img, 0,0, canvas.width, canvas.height);
// next drawings will be blurred
ctx.filter = 'blur('+ blurredRect.spread +'px)';
// draw the canvas over itself, cropping to our required rect
ctx.drawImage(canvas,
blurredRect.x, blurredRect.y, blurredRect.width, blurredRect.height,
blurredRect.x, blurredRect.y, blurredRect.width, blurredRect.height
);
// draw the coloring (white-ish) layer, without blur
ctx.filter = 'none'; // remove filter
ctx.fillStyle = 'rgba(255,255,255,0.2)';
ctx.fillRect(blurredRect.x, blurredRect.y, blurredRect.width, blurredRect.height);
}
<canvas id="canvas"></canvas>
But, canvas blur filter is a bit different than CSS one in that it will make the spreading stay inside the drawn area. This means that in our case, we have a 5px border around our rectangle that is less blurred than the center.
To workaround, we can take the whole thing in a different order and play with globalCompositeOperation property*:
var blurredRect = {
x: 80,
y: 80,
height: 200,
width: 200,
spread: 10
};
var ctx = canvas.getContext('2d');
var img = new Image();
img.onload = draw;
img.src = 'https://upload.wikimedia.org/wikipedia/commons/5/55/John_William_Waterhouse_A_Mermaid.jpg';
function draw() {
var spread = blurredRect.spread,
ratio = 0.5,
// make our blurred rect spreads
x = blurredRect.x - spread,
y = blurredRect.y - spread,
w = blurredRect.width + (spread * 2),
h = blurredRect.height + (spread * 2);
canvas.width = img.width * ratio;
canvas.height = img.height * ratio;
// this time we will first draw the blurred rect
ctx.filter = 'blur('+ spread +'px)';
// this time we draw from the img directly
ctx.drawImage(img,
x / ratio, y / ratio, w / ratio, h / ratio,
x, y, w, h
);
// now we will want to crop the resulting blurred image to the required one, so we get a clear-cut
ctx.filter = 'none'; // remove filter
// with this mode, previous drawings will be kept where new drawings are made
ctx.globalCompositeOperation = 'destination-in';
ctx.fillStyle = '#000'; // make it opaque
ctx.rect(blurredRect.x, blurredRect.y, blurredRect.width, blurredRect.height);
ctx.fill(); // clear-cut done
// reuse our rect to make the white-ish overlay
ctx.fillStyle = 'rgba(255,255,255,0.2)';
// reset gCO to its default
ctx.globalCompositeOperation = 'source-over';
ctx.fill();
// now we will draw behind the our blurred rect
ctx.globalCompositeOperation = 'destination-over';
ctx.drawImage(img, 0,0, canvas.width, canvas.height);
// reset to defaults
ctx.globalCompositeOperation = 'source-over';
}
<canvas id="canvas"></canvas>
But this approach requires that we keep access to the whole background as a drawable thing, in the example above that was just an image, but in real life, this might mean you'd have to do this operation on a second offscreen canvas.
var blurredRect = {
x: 80,
y: 80,
height: 200,
width: 200,
spread: 2
};
var ctx = canvas.getContext('2d');
// create an off-screen canvas
var bCanvas = canvas.cloneNode();
var bCtx = bCanvas.getContext('2d');
var img = new Image();
img.onload = draw;
img.src = 'https://upload.wikimedia.org/wikipedia/commons/5/55/John_William_Waterhouse_A_Mermaid.jpg';
function draw() {
var spread = blurredRect.spread;
canvas.width = bCanvas.width = img.width / 2;
canvas.height = bCanvas.height = img.height / 2;
// now we have a composed background
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
ctx.font = '40px Impact';
ctx.fillStyle = 'white';
ctx.fillText('..SO BLUR ME..', 120, 282);
// make our clear-cut on the offscreen canvas
bCtx.filter = 'blur(' + spread +'px)';
bCtx.drawImage(canvas,
blurredRect.x - spread, blurredRect.y - spread, blurredRect.width + spread * 2, blurredRect.height + spread * 2,
blurredRect.x - spread, blurredRect.y - spread, blurredRect.width + spread * 2, blurredRect.height + spread * 2
);
// clear-cut
bCtx.filter = 'none';
bCtx.globalCompositeOperation = 'destination-in';
bCtx.beginPath();
bCtx.rect(blurredRect.x, blurredRect.y, blurredRect.width, blurredRect.height);
bCtx.fillStyle = '#000';
bCtx.fill();
// white-ish layer
bCtx.globalCompositeOperation = 'source-over';
bCtx.fillStyle = 'rgba(255,255,255,0.2)';
bCtx.fillRect(blurredRect.x, blurredRect.y, blurredRect.width, blurredRect.height);
// now just redraw on the visible canvas
ctx.drawImage(bCanvas, 0,0);
}
<canvas id="canvas"></canvas>
*One may say that instead of an offscreen canvas and gCO we could have used ctx.clip(), but since you said it might a more complex Path than a rect, I will not advise to do so. Indeed, while it would require less code, and maybe use less memory, clipping is just bad with antialiasing, and since you are doing blurring, that will just look plain ugly.

get area of outline of 2D canvas

I am very nerd in this topic. But still I got some solution from this answer. I need covered area by non-transparent part of image on canvas. Its possible to draw outline for sprite image using globalCompositeOperation .
Now Is it possible to get that area covered by outline for that non-transparent part of image? Is it possible to get covered area in x,y position that I can save it for further use ? OR Is there any way to restrict touch event on source-out area from globalCompositeOperation ?
Currently using code from this answer to draw outline :
var ctx = canvas.getContext('2d'),
img = new Image;
img.onload = draw;
img.src = "http://i.stack.imgur.com/UFBxY.png";
function draw() {
var dArr = [-1,-1, 0,-1, 1,-1, -1,0, 1,0, -1,1, 0,1, 1,1], // offset array
s = 2, // scale
i = 0, // iterator
x = 5, // final position
y = 5;
// draw images at offsets from the array scaled by s
for(; i < dArr.length; i += 2)
ctx.drawImage(img, x + dArr[i]*s, y + dArr[i+1]*s);
// fill with color
ctx.globalCompositeOperation = "source-in";
ctx.fillStyle = "red";
ctx.fillRect(0,0,canvas.width, canvas.height);
// draw original image in normal mode
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(img, x, y);
}
EDIT:
Using #Kaiido solution.Its taking pixels from non-transparent + outline. I need only non transparent area.
var ctx = canvas.getContext('2d'),
img = new Image;
img.onload = draw;
img.crossOrigin = 'anonymous';
img.src = "drawing/templates/drawing-pic4.png";
var outline, origOutline,
outlineCtx;
function draw(color) {
ctx.clearRect(0,0,canvas.width,canvas.height);
// onload
if(typeof color !== 'string') color = 'white';
var dArr = [-1,-1, 0,-1, 1,-1, -1,0, 1,0, -1,1, 0,1, 1,1], // offset array
s = 5, // scale
i = 0, // iterator
x = 5, // final position
y = 5;
// draw images at offsets from the array scaled by s
for(; i < dArr.length; i += 2)
ctx.drawImage(img, x + dArr[i]*s, y + dArr[i+1]*s);
// fill with color
ctx.globalCompositeOperation = "source-in";
ctx.fillStyle = color;
ctx.fillRect(0,0,canvas.width, canvas.height);
// keep only the outline
ctx.globalCompositeOperation = "destination-out";
ctx.drawImage(img, x, y);
origOutline = ctx.getImageData(0,0,canvas.width, canvas.height).data;
// store the imageData in a new Canvas
outline = canvas.cloneNode(true);
outlineCtx = outline.getContext('2d')
outlineCtx.drawImage(canvas,0,0);
// draw image in original mode
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(img, x, y);
}
var w= 10;
canvas.onclick = function(e){
var rect = canvas.getBoundingClientRect();
var x = e.clientX-rect.left,
y = e.clientY-rect.top;
var pixels = ((y*canvas.width)+x)*4;
showLog("pixels: "+pixels);
if(origOutline[pixels+3]!==0)
{
showLog("in out line: "+origOutline[pixels+3]);
}
// not transparent ?
if(outlineCtx.getImageData(x,y,1,1).data[3]!==0){
ctx.strokeStyle = "#0000ff";
ctx.lineWidth = w;
ctx.lineJoin = ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(x,y);
ctx.lineTo(x,y);
ctx.stroke();
ctx.closePath();
}
else{
showLog("else");
}
}
Here is one way :
Use Ken's method to draw the shape, but for the last drawing, instead of setting the globalCompositeOperation property to 'source-out', set it to 'destination-out'. This way you will have only the outline :
var ctx = canvas.getContext('2d'),
img = new Image;
img.onload = draw;
img.src = "https://dl.dropboxusercontent.com/s/1alt1303g9zpemd/UFBxY.png";
function draw() {
var dArr = [-1,-1, 0,-1, 1,-1, -1,0, 1,0, -1,1, 0,1, 1,1], // offset array
s = 2, // scale
i = 0, // iterator
x = 5, // final position
y = 5;
// draw images at offsets from the array scaled by s
for(; i < dArr.length; i += 2)
ctx.drawImage(img, x + dArr[i]*s, y + dArr[i+1]*s);
// fill with color
ctx.globalCompositeOperation = "source-in";
ctx.fillStyle = "red";
ctx.fillRect(0,0,canvas.width, canvas.height);
// draw original image in dest-out mode to keep only the outline
ctx.globalCompositeOperation = "destination-out";
ctx.drawImage(img, x, y);
}
<canvas id=canvas width=500 height=500></canvas>
Now, you can store this outline in a new canvas, and every time you click on your canvas, compare the click event's position to the pixel at same position in your stored canvas :
var ctx = canvas.getContext('2d'),
img = new Image;
img.onload = draw;
img.crossOrigin = 'anonymous';
img.src = "https://dl.dropboxusercontent.com/s/1alt1303g9zpemd/UFBxY.png";
var outline,
outlineCtx;
function draw(color) {
ctx.clearRect(0,0,canvas.width,canvas.height);
// onload
if(typeof color !== 'string') color = 'red';
var dArr = [-1,-1, 0,-1, 1,-1, -1,0, 1,0, -1,1, 0,1, 1,1], // offset array
s = 5, // scale
i = 0, // iterator
x = 5, // final position
y = 5;
// draw images at offsets from the array scaled by s
for(; i < dArr.length; i += 2)
ctx.drawImage(img, x + dArr[i]*s, y + dArr[i+1]*s);
// fill with color
ctx.globalCompositeOperation = "source-in";
ctx.fillStyle = color;
ctx.fillRect(0,0,canvas.width, canvas.height);
// keep only the outline
ctx.globalCompositeOperation = "destination-out";
ctx.drawImage(img, x, y);
// store the imageData in a new Canvas
outline = canvas.cloneNode(true);
outlineCtx = outline.getContext('2d')
outlineCtx.drawImage(canvas,0,0);
// draw image in original mode
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(img, x, y);
}
canvas.onclick = function(e){
var rect = canvas.getBoundingClientRect();
var x = e.clientX-rect.left,
y = e.clientY-rect.top;
// not transparent ?
if(outlineCtx.getImageData(x,y,1,1).data[3]===255){
draw('green');
}
else
draw('red');
}
<canvas id=canvas width=500 height=500></canvas>
If your outline is unlikely to change very often, it may be interesting to store the imageData instead of calling getImageData every click.
// in the draw function
ctx.globalCompositeOperation = "destination-out";
ctx.drawImage(img, x, y);
outline = ctx.getImageData(0,0,canvas.width, canvas.height).data;
// in the click event
var pixels = ((y*canvas.width)+x)*4;
if(outline[pixels+3]===255)
// in the outline
else
// out

Categories