Here's what I'm trying to do:
Get image A, and image B. Image B is a black and white mask image.
Replace image A's alpha channel with image B's red channel.
Draw image C on the canvas.
Draw image A on top of image C.
Everything seems ok until step 4. Image C isn't visible at all and where image A should be transparent there's white color.
cx.putImageData(imageA, 0, 0);
var resultData = cx.getImageData(0, 0, view.width, view.height);
for (var h=0; h<resultData.data.length; h+=4) {
resultData.data[h+3] = imageB.data[h];
}
cx.putImageData(imageC, 0, 0);
cx.putImageData(resultData, 0, 0);
Simon is right: the putImageData method does not pay any attention to compositing; it merely copies pixel values. In order to get compositing, we need to use drawing operations.
We need to mess with the channels (turn red into alpha) with the pixel data, put that changed pixel data into an image, and then use a composite operation to get the desired masking.
//copy from one channel to another
var assignChannel = function(imageData, channelTo, channelFrom) {
if(channelTo < 0 || channelTo > 3 || channelFrom < 0 || channelFrom > 3) {
throw new Error("bad channel number");
}
if(channelTo == channelFrom)
return;
var px = imageData.data;
for(var i = 0; i < px.length; i += 4) {
px[i + channelTo] = px[i + channelFrom];
}
};
/**============================================================================
* this function uses 3 or 4 canvases for clarity / pedagogical reasons:
* redCanvas has our mask image;
* maskCanvas will be used to store the alpha channel conversion of redCanvas' image;
* imageCanvas contains the image to be masked;
* ctx is the context of the canvas to which the masked image will be drawn.
============================================================================**/
var drawOnTopOfRed = function(redCanvas, maskCanvas, imageCanvas, ctx) {
var redImageData = redCanvas.getContext("2d").getImageData(0, 0, w, h);
//assign the alpha channel
assignChannel(redImageData, 3, 0);
//write the mask image
maskCanvas.getContext("2d").putImageData(redImageData, 0, 0);
ctx.save();
//draw the mask
ctx.globalCompositeOperation = "copy";
ctx.drawImage(maskCanvas, 0, 0);
//draw the image to be masked, but only where both it
//and the mask are opaque; see http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#compositing for details.
ctx.globalCompositeOperation = "source-in";
ctx.drawImage(imageCanvas, 0, 0);
ctx.restore();
};
jsfiddle example
A doodle with the example:
Because in step 4 you are using putImageData which perfectly replaces pixels. You want to draw image A on top of image C, so you can't do this. Instead you will want to use drawImage()
So do:
cx.putImageData(imageC, 0, 0); // step 3
// create a new canvas and new context,
// call that new context ctx2 and canvas can2:
var can2 = document.createElement('canvas');
// set can2's width and height, get the context etc...
ctx2.putImageData(resultData, 0, 0);
cx.drawImage(can2, 0, 0); // step 4 using drawImage instead of putting image data
Related
I'm pretty new to canvas and haven't worked with it before but I thought it would be a good fit for the following task. While working on it I got doubts and I still don't know if the task is even possible to implement using canvas.
Exemplary graphic of the masks and images and the result that I want to achieve (and the actual results that I got).
The outlines are just there to better illustrate the images
dimensions.
The masks are SVG images which are preloaded using promises before
they are drawn and they change per iteration. So on the first
iteration it's mask A for image 1 and on the second iteration mask
B for image 2.
Simplified pseudo code example:
const items = [1, 2];
for (let i = 0; i < items.length; i++) {
ctx.drawImage(preloadedMask[i], x, y, canvasWidth, canvasHeight);
ctx.globalCompositeOperation = 'source-in';
img[i] = new Image();
img[i].onload = () => {
ctx.drawImage(img[i], 0, 0, canvasWidth, canvasHeight);
ctx.globalCompositeOperation = 'source-over';
//ctx.globalCompositeOperation = 'source-out';
};
img[i].src = `images/${i+1}.jpg`;
}
When I remove the globalCompositeOperation and the images the masks are perfectly drawn next to each other like I expected.
But as soon as I add a globalCompositeOperation it gets complicated and I am super confused to be honest.
I tried every possible globalCompositeOperation value in the onload callback - but it doesn't change much. I think I have to change the globalCompositeOperation after the mask is drawn for each iteration to a different value - but I am out of ideas.
Is there any way to achieve my desired output as described in the graphic or should I ditch canvas for this task?
What you're trying to achieve isn't that easy unfortunately - at least if you're using SVGs which are treated as images and directly drawn to the canvas.
Suppose we have the following svg masks and images
If we take the first mask and the first image and use the following code:
context.drawImage(maskA,0,0,width,height);
context.globalCompositeOperation = "source-in";
context.drawImage(imageA,0,0,width,height);
we get the desired output:
If we repeat the process and do the same for the second mask:
context.drawImage(maskB,0,0,width,height);
context.globalCompositeOperation = "source-in";
context.drawImage(imageB,0,0,width,height);
we'll just see an empty canvas. Why? We set globalCompositeOperation to 'source-in' and the previous canvas and the second mask (maskB) don't have any overlapping regions. That means we're effectively erasing the canvas.
If we try to compensate and either save/restore the context or reset globalCompositeOperation to it's initial state
context.save();
context.drawImage(maskA,0,0,width,height);
context.globalCompositeOperation = "source-in";
context.drawImage(imageA,0,0,width,height);
context.restore();
context.drawImage(maskB,0,0,width,height);
context.globalCompositeOperation = "source-in";
context.drawImage(imageB,0,0,width,height);
we still don't succeed:
So the trick here is this:
make sure both the svgs and images to be masked are fully loaded
create a new empty canvas the size of your target canvas
draw the first mask onto the new canvas
set it's globalCompositeOperation to 'source-in'
draw the first image onto the new canvas
draw the new canvas to the target canvas
erase the new canvas and repeat the previous steps to compose your final image
Here's an example (just click 'Run code snippet'):
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
let imagesLoaded = 0;
let imageA = document.getElementById("imageA");
let imageB = document.getElementById("imageB");
let width = canvas.width;
let height = canvas.height;
function loaded() {
imagesLoaded++;
if (imagesLoaded == 4) {
let tempCanvas = document.createElement("canvas");
let tempContext = tempCanvas.getContext("2d");
tempCanvas.width = width;
tempCanvas.height = height;
tempContext.save();
tempContext.drawImage(document.getElementById("semiCircleA"), 0, 0, width, height);
tempContext.globalCompositeOperation = "source-in";
tempContext.drawImage(imageA, 0, 0, width, 160);
ctx.drawImage(tempCanvas, 0, 0, width, height);
tempContext.restore();
tempContext.clearRect(0, 0, width, height);
tempContext.drawImage(document.getElementById("semiCircleB"), 0, 0, width, height);
tempContext.globalCompositeOperation = "source-in";
tempContext.drawImage(imageB, 0, 0, width, height);
ctx.drawImage(tempCanvas, 0, 0, width, height);
}
}
document.getElementById("semiCircleA").onload = loaded;
document.getElementById("semiCircleB").onload = loaded;
imageA.onload = loaded;
imageA.src = "https://picsum.photos/id/237/160/160";
imageB.onload = loaded;
imageB.src = "https://picsum.photos/id/137/160/160";
<h1>Final Canvas</h1>
<canvas id="canvas" width=160 height=160>
</canvas>
<br>
<h1>Sources</h1>
<img id="semiCircleA" src='data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="160px" height="160px">
<path d="M80,0 A80,80 0 0,0 80,160"/>
</svg>'>
<img id="semiCircleB" src='data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="160px" height="160px">
<path d="M80,0 A80,80 0 0,1 80,160"/>
</svg>'>
<img id="imageA">
<img id="imageB">
A canvas can be a layer
The canvas like any element is easy to create and can be treated like an image, or if you are familiar with photoshop, a canvas can be a layer.
To create a blank canvas
// Returns the renderable image (canvas)
function CreateImage(width, height) {
return Object.assign(document.createElement("canvas"), {width, height});
}
To copy a canvas or image like object
// Image can be any image like element including canvas. Returns the renderable image
function CopyImage(img, width = img.width, height = img.height, smooth = true) {
const can = createImage(width, height});
can.ctx = can.getContext("2d");
can.ctx.imageSmoothingEnabled = smooth;
can.ctx.drawImage(img, 0, 0, width, height);
return can;
}
Loading
Never load images in a render loop. The image onload event will not respect the order you assign the src. Thus the rendering of images in onload will not always be in the order you wish.
Load all images and wait before rendering.
An example of loading a set of images. The function loadImages returns a promise that will resolve when all images have loaded.
const images = {
maskA: "imageUrl",
maskB: "imageUrl",
imgA: "imageUrl",
imgB: "imageUrl",
};
function loadImages(imgList, data) {
return new Promise((done, loadingError) => {
var count = 0;
const imgs = Object.entries();
for (const [name, src] of imgs) {
imgList[name] = new Image;
imgList[name].src = src;
count ++;
imgList[name].addEventListener("load", () => {
count--;
if (count === 0) { done({imgs: imgList, data}) }
}, {once, true)
);
imgList[name].addEventListener("error", () => {
for (const [name, src] of imgs) { imgList[name] = src }
loadingError(new Error("Could not load all images"));
}, {once, true)
);
}
});
}
Rendering
It is best to create functions to do repeated tasks. One task you are repeating is masking, the following function uses a canvas as a destination, an image, and a mask
function maskImage(ctx, img, mask, x = 0, y = 0, w = ctx.canvas.height, h = ctx.canvas.width, clear = true) {
ctx.globalCompositeOperation = "source-over";
clear && ctx.clearRect(0, 0, ctx.canvas.height, ctx.canvas.width);
ctx.drawImage(img, x, y, w, h);
ctx.globalCompositeOperation = "destination-in";
ctx.drawImage(mask, 0, 0, w, h);
return ctx.canvas; // return the renderable image
}
Once you have some utilities set up to help coordinate the loading and rendering you can composite your finial result
// assumes ctx is the context to render to
loadImages(images, {ctx}).then(({imgs, {ctx}} => {
const w = ctx.canvas.width, h = ctx.canvas.height;
ctx.clearRect(0, 0, w, h);
const layer = copyImage(ctx.canvas);
ctx.drawImage(maskImage(layer.ctx, imgs.imgA, imgs.maskA), 0, 0, w, h);
ctx.drawImage(maskImage(layer.ctx, imgs.imgB, imgs.maskB), 0, 0, w, h);
// if you no longer need the images always remove them from memory to avoid hogging
// client's resources.
imgs = {}; // de-reference images so that GC can clean up.
}
You can now layer as many masked images as needed. Because functions where created for each sub task it is easy to create more complicated rendering without needing to write verbose and repeated code, in both this project and future projects.
I'm trying to split a black and white image into its RGB components and offset each layer setting it to overlay. The idea is to have a hero image slightly out of register and randomly move it slightly.
I am wondering if this is in fact the correct way to approach this.
I tend to crash the browser if I dare add a console log in the function to see where I am going wrong.
Has anyone done this kind of manipulation in the browser and is it viable?
https://github.com/Julieslv/image-shift/blob/master/index.js
Splitting image into RGBA channels
First note that you can not separate the channels from an image. You can only set the unwanted channels to zero. However setting the alpha channel to zero will automatically zero all channels. Thus you must keep the alpha channel.
Copy image
To copy an image create a second canvas and draw the original image onto it.
The following function will do that
function copyToCanvas(image) {
const can = document.createElement("canvas");
can.width = image.naturalWidth || image.width;
can.height = image.naturalHeight || image.height;
can.ctx = can.getContext("2d");
can.ctx.drawImage(image, 0, 0);
return can;
}
Removing channel data
Two remove the unwanted channel data from a image copied by the previous method requires two steps.
Use composite operation "multiply" to remove unwanted channel data.
The above step will set the alpha channel to 255. To put the alpha back you use the composite operation "destination-in" and draw the original image over the new image.
The following function will copy the image, remove unwanted channel data and keep the alpha channel intact.
const channels = {
red: "#F00",
green: "#0F0",
blue: "#00F",
};
function getChannel(channelName, image) {
const copy = copyToCanvas(image);
const ctx = copy.ctx;
ctx.fillStyle = channels[channelName];
ctx.globalCompositeOperation = "multiply";
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.globalCompositeOperation = "destination-in";
ctx.drawImage(image, 0, 0);
ctx.globalCompositeOperation = "source-over";
return copy;
}
Getting RGB channels
As the alpha channel is maintained you need only separate out red green and blue channels.
Using the previous function the following will create an object the holds the original image and the 3 channels
function seperateRGB(image) {
return {
red: getChannel("red", image),
green: getChannel("green", image),
blue: getChannel("blue", image),
};
}
Recombining channels
Now that the channels have been separated you can recombine channels by first making a new canvas the same size as the original image plus any offsets you are adding as you recombine the image (if you are not adding back alpha).
function createCanvas(w, h) {
const can = document.createElement("canvas");
can.width = w;
can.height = h;
can.ctx = can.getContext("2d");
return can;
}
Then draw the first channel (can be any of the 3) onto the new canvas. Then draw the other two channels using the composite operation "lighter". Then restore alpha using composite operation "destination-in"
const RGB = seperateRGB(image);
const recombined = createCanvas(RGB.red.width, RGB.red.height);
const ctx = recombined.ctx;
ctx.drawImage(RGB.red, -2, -2);
ctx.globalCompositeOperation = "lighter";
ctx.drawImage(RGB.green, 0, 0);
ctx.drawImage(RGB.blue, 2, 2);
ctx.globalCompositeOperation = "destination-in";
ctx.drawImage(image, 0, 0);
ctx.globalCompositeOperation = "source-over";
All done
The canvas element recombined holds the recombined image with the green and blue channels offset by 2 pixels and the original alpha restored.
Note that you do not need to restore the alpha. Restoring the alpha if some channels are offset will remove some of the non overlapping pixels.
I am new to canvas, I have an image myimg.jpg, I have converted this image into canvas and i am trying to apply some pattern image for heel.
I am not able to do it. Here is my screenshot:
How can I get it done.
<div id="myId">
<canvas id="canvaswrapper" width="660" height="540"></canvas>
</div>
function drawImage(){
var ctx = $("canvas")[0].getContext("2d"),
img = new Image();
img.onload = function(){
ctx.drawImage(img, 0, 0, 500, 500);
ctx.beginPath();
var img2= new Image();
var w;
var h;
img2.src = "http://www.gravatar.com/avatar/e555bd971bc2f4910893cd5b785c30ff?s=128&d=identicon&r=PG";
var pattern = ctx.createPattern(img2, "repeat");
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, w, h);
ctx.arc(300,305,50,0,2*Math.PI);
ctx.fill();
ctx.stroke();
};
img.src = "myimg.jpg";
}
drawImage();
You can define the area you want to fill using an image mask that fits on top of your image - this step is something for Photoshop/GIMP.
For example, having your shoe as-is:
Create a mask for it leaving the heal in the original position (it makes it easier to draw it back in - you can always crop it and draw it using an offset instead). Important: background must be transparent:
Then super-impose the pattern using these steps:
Load the pattern and define is as a fill-pattern
Draw the mask into the empty canvas
Optional step: Adjust transformations if needed (translate, scale)
Choose composite mode "source-atop"
Fill the canvas
Choose composite mode "destination-atop"
Draw the main image on top (which will show behind the mask/pattern)
Optional step: draw in original mask image using blending mode "multiply" to add shadow and highlights (does not work in IE). This will help creating an illusion of depth. For IE, drawing it on top using a reduced alpha or a separate image only containing shadows etc. can be an option
Result
Example
var iShoe = new Image, iMask = new Image, iPatt = new Image, count = 3;
iShoe.onload = iMask.onload = iPatt.onload = loader;
iShoe.src = "http://i.stack.imgur.com/hqL1C.png";
iMask.src = "http://i.stack.imgur.com/k5XWN.png";
iPatt.src = "http://i.stack.imgur.com/CEQ10.png";
function loader() {
if (--count) return; // wait until all images has loaded
var ctx = document.querySelector("canvas").getContext("2d"),
pattern = ctx.createPattern(iPatt, "repeat");
// draw in mask
ctx.drawImage(iMask, 0, 0);
// change comp mode
ctx.globalCompositeOperation = "source-atop";
// fill mask
ctx.scale(0.5, 0.5); // scale: 0.5
ctx.fillStyle = pattern; // remember to double the area to fill:
ctx.fillRect(0, 0, ctx.canvas.width*2, ctx.canvas.height*2);
ctx.setTransform(1,0,0,1,0,0); // reset transform
// draw shoe behind mask
ctx.globalCompositeOperation = "destination-atop";
ctx.drawImage(iShoe, 0, 0);
// to make it more realistic, add mask in blend mode (does not work in IE):
ctx.globalCompositeOperation = "multiply";
if (ctx.globalCompositeOperation === "multiply") {
ctx.drawImage(iMask, 0, 0);
}
}
<canvas width=281 height=340></canvas>
Is it possible to use a image with a shape as a mask for a whole canvas or images within the canvas?
I want to place images in a canvas with a mask over the images, and then save it as a new image.
You can use a black and white image as a mask using 'source-in' globalCompositeOperation. First you draw your mask image to the canvas, then you change the globalCompositeOperation to 'source-in', finally you draw your final image.
Your final image will only be draw where it overlay the mask.
var ctx = document.getElementById('c').getContext('2d');
ctx.drawImage(YOUR_MASK, 0, 0);
ctx.globalCompositeOperation = 'source-in';
ctx.drawImage(YOUR_IMAGE, 0 , 0);
More info on global composite operations
In addition to Pierre's answer you can also use a black and white image as a mask source for your image by copying its data into a CanvasPixelArray like:
var
dimensions = {width: XXX, height: XXX}, //your dimensions
imageObj = document.getElementById('#image'), //select image for RGB
maskObj = document.getElementById('#mask'), //select B/W-mask
image = imageObj.getImageData(0, 0, dimensions.width, dimensions.height),
alphaData = maskObj.getImageData(0, 0, dimensions.width, dimensions.height).data; //this is a canvas pixel array
for (var i = 3, len = image.data.length; i < len; i = i + 4) {
image.data[i] = alphaData[i-1]; //copies blue channel of BW mask into A channel of the image
}
//displayCtx is the 2d drawing context of your canvas
displayCtx.putImageData(image, 0, 0, 0, 0, dimensions.width, dimensions.height);
Is there any way to check if a selected (x,y) point of a PNG image is transparent?
Building on Jeff's answer, your first step would be to create a canvas representation of your PNG. The following creates an off-screen canvas that is the same width and height as your image and has the image drawn on it.
var img = document.getElementById('my-image');
var canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height);
After that, when a user clicks, use event.offsetX and event.offsetY to get the position. This can then be used to acquire the pixel:
var pixelData = canvas.getContext('2d').getImageData(event.offsetX, event.offsetY, 1, 1).data;
Because you are only grabbing one pixel, pixelData is a four entry array containing the pixel's R, G, B, and A values. For alpha, anything less than 255 represents some level of transparency with 0 being fully transparent.
Here is a jsFiddle example: http://jsfiddle.net/thirtydot/9SEMf/869/ I used jQuery for convenience in all of this, but it is by no means required.
Note: getImageData falls under the browser's same-origin policy to prevent data leaks, meaning this technique will fail if you dirty the canvas with an image from another domain or (I believe, but some browsers may have solved this) SVG from any domain. This protects against cases where a site serves up a custom image asset for a logged in user and an attacker wants to read the image to get information. You can solve the problem by either serving the image from the same server or implementing Cross-origin resource sharing.
Canvas would be a great way to do this, as #pst said above. Check out this answer for a good example:
getPixel from HTML Canvas?
Some code that would serve you specifically as well:
var imgd = context.getImageData(x, y, width, height);
var pix = imgd.data;
for (var i = 0, n = pix.length; i < n; i += 4) {
console.log pix[i+3]
}
This will go row by row, so you'd need to convert that into an x,y and either convert the for loop to a direct check or run a conditional inside.
Reading your question again, it looks like you want to be able to get the point that the person clicks on. This can be done pretty easily with jquery's click event. Just run the above code inside a click handler as such:
$('el').click(function(e){
console.log(e.clientX, e.clientY)
}
Those should grab your x and y values.
The two previous answers demonstrate how to use Canvas and ImageData. I would like to propose an answer with runnable example and using an image processing framework, so you don't need to handle the pixel data manually.
MarvinJ provides the method image.getAlphaComponent(x,y) which simply returns the transparency value for the pixel in x,y coordinate. If this value is 0, pixel is totally transparent, values between 1 and 254 are transparency levels, finally 255 is opaque.
For demonstrating I've used the image below (300x300) with transparent background and two pixels at coordinates (0,0) and (150,150).
Console output:
(0,0): TRANSPARENT
(150,150): NOT_TRANSPARENT
image = new MarvinImage();
image.load("https://i.imgur.com/eLZVbQG.png", imageLoaded);
function imageLoaded(){
console.log("(0,0): "+(image.getAlphaComponent(0,0) > 0 ? "NOT_TRANSPARENT" : "TRANSPARENT"));
console.log("(150,150): "+(image.getAlphaComponent(150,150) > 0 ? "NOT_TRANSPARENT" : "TRANSPARENT"));
}
<script src="https://www.marvinj.org/releases/marvinj-0.7.js"></script>
Building on Brian Nickel's answer, only the wanted single pixel of the source image is drawn onto a 1*1 pixel canvas, which is more efficient than drawing the entire image just to get a single pixel:
function getPixel(img, x, y) {
let canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
canvas.getContext('2d').drawImage(img, x, y, 1, 1, 0, 0, 1, 1);;
let pixelData = canvas.getContext('2d').getImageData(0, 0, 1, 1).data;
return pixelData;
}
With : i << 2
const pixels = context.getImageData(x, y, width, height).data;
for (let i = 0, dx = 0; dx < data.length; i++, dx = i << 2)
{
if (pixels[dx+3] <= 8) { console.log("transparent x= " + i); }
}
Here's a consolidation of a few answers into a runnable snippet that lets you upload a file, hover to preview the RGB value of each pixel, then click to put the RGB in a div.
Pertinent to the original question, the last value (alpha) is the transparency. 0 is fully transparent and 255 is fully opaque.
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const input = document
.querySelector('input[type="file"]');
input.addEventListener("change", e => {
const image = new Image();
image.addEventListener("load", e => {
const {width, height} = image;
canvas.width = width;
canvas.height = height;
ctx.drawImage(image, 0, 0);
const {data} = ctx.getImageData(
0, 0, width, height
);
const rgb = (x, y) => {
const i = (x + y * width) * 4;
return data.slice(i, i + 4).join(", ");
};
canvas.addEventListener("mousemove", event => {
const {offsetX: x, offsetY: y} = event;
console.log(rgb(x, y));
});
canvas.addEventListener("click", event => {
const {offsetX: x, offsetY: y} = event;
document.querySelector("div")
.textContent = rgb(x, y);
});
});
image.addEventListener("error", () =>
console.error("failed")
);
image.src = URL
.createObjectURL(event.target.files[0]);
});
.as-console-wrapper {
height: 21px !important;
}
<div>
Upload image and mouseover to preview RGB. Click to select a value.
</div>
<form>
<input type="file">
</form>
<canvas></canvas>
References:
HTML5 Canvas - How to get adjacent pixels position from the linearized imagedata Uint8ClampedArray?
How to upload image into HTML5 canvas