Canvas splitting image into rgba components - javascript

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.

Related

HTML Canvas - Adjust opacity of multiple elements as a layer [duplicate]

This question already has an answer here:
Is there a way to disable color mixing/overlapping in html canvas
(1 answer)
Closed 7 months ago.
I'm trying to find a way to draw multiple elements onto an HTML canvas, then adjust all of their opacities at once. For example, this codepen example draws two overlapping rectangles with globalAlpha set to 0.5, so they're semi-transparent.
Here's what I see:
Here's what I want to see:
In other words, I want to draw some set of elements, then adjust their alpha/opacity all at once. In the example above, I want the overlapping section of blue & red to appear as just blue, since the blue rectangle was drawn 2nd.
I want this solution to apply to images, shapes, any canvas drawings really.
Does anyone know how to accomplish this using HTML canvas?
you must decompose the process
1- create de canvas with all draw ( alpha 100%)
2 set aside the flattened drawings
3 clear the canvas
4 fetch the picture and add it to the canvas
5 set alpha to 50%
6 add the tmp flattened drawings with alpha
const cnv = document.createElement("canvas");
cnv.width = 300;
cnv.height = 300;
const ctx = cnv.getContext("2d");
document.body.appendChild(cnv);
// Draw red rectangle
ctx.fillStyle = "#f00";
ctx.fillRect(20, 20, cnv.width - 140, cnv.height - 60);
// Draw blue rectangle
ctx.fillStyle = "#00f";
ctx.fillRect(100, 40, cnv.width - 140, cnv.height - 60);
//aside the draw in flatten layer
let tmp = new Image();
tmp.src = cnv.toDataURL();
// clear the canvas
ctx.clearRect(0, 0, cnv.width, cnv.height);
// apply bg
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, cnv.width, cnv.height);
const image = new Image(); // Using optional size for image
image.src = "https://img.photographyblog.com/reviews/kodak_pixpro_fz201/photos/kodak_pixpro_fz201_01.jpg";
image.onload = () => {
// Draw background image
ctx.drawImage(image, 0, 0);
ctx.drawImage(image, 0, 0, cnv.width, cnv.width);
// Set alpha to 0.5
ctx.globalAlpha = 0.5;
//overlay with the tmp flatten img with 50%
ctx.drawImage(tmp, 0, 0, cnv.width, cnv.width);
}

Multiple masked images on top of each other using HTML canvas

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.

How to select particular area in canvas?

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>

In a 2D canvas, is there a way to give a sprite an outline?

I'd like to give a sprite an outline when the character gets healed/damaged/whatever but I can't think of a way to code this using the 2d canvas. If it were possible, I'd think it would be a global composite operation, but I can't think of a way to achieve it with one of them.
I did find this stackoverflow answer that recommends creating a fatter, solid color version of the original and put the original on top of it. That would give it an outline, but it seems like a lot of extra work especially considering I'm using placeholder art. Is there an easier way?
This question is different from the one linked because this is specifically about the HTML5 2D canvas. It may have a solution not available to the other question.
For what it's worth, I don't mind if the outline creates a wider border or keeps the sprite the same size, I just want the outline look.
Just draw your original image in 8 position around the original image
Change composite mode to source-in and fill with the outline color
Change composite mode back to source-over and draw in the original image at correct location
This will create a clean sharp outline with equal border thickness on every side. It is not so suited for thick outlines however. Image drawing is fast, especially when image is not scaled so performance is not an issues unless you need to draw a bunch (which in that case you would cache the drawings or use a sprite-sheet anyways).
Example:
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);
}
<canvas id=canvas width=500 height=500></canvas>
Maybe it would be worth trying this :
• build a canvas 1.1 time bigger than the original sprite
• fill it with the outline color
• draw the sprite scaled by 1.1 on the canvas using destination-in globalCompositeOperation.
Then you have a bigger 'shadow' of your sprite in the outline color.
When you want to draw the outline :
• draw the 'shadow' (centered)
• draw your sprite within the shadow.
Depending on the convexity of your sprite, this will work more or less nicely, but i think it's worth trying since it avoids you doubling the number of input graphic files.
I just did a short try as proof-of-concept and it quite works :
http://jsbin.com/dogoroxelupo/1/edit?js,output
Before :
After :
html
<html>
<body>
<image src='http://www.gifwave.com/media/463554/cartoons-comics-video-games-sprites-scott-pilgrim-paul-robertson_200s.gif' id='spr'></image>
<canvas id='cv' width = 500 height= 500 ></canvas>
</body>
</html>
code
window.onload=function() {
var spr = document.getElementById('spr');
var margin = 4;
var gh = createGhost(spr, '#F80', margin);
var cv = document.getElementById('cv');
var ctx = cv.getContext('2d');
var outlined = true;
setInterval(function() {
ctx.clearRect(0,0,cv.width, cv.height);
if (outlined)
ctx.drawImage(gh, 0, 0)
ctx.drawImage(spr, 0, 0)
outlined = !outlined;
}, 400);
}
function createGhost (img, color, margin) {
var cv= document.createElement('canvas');
cv.width = img.width+2*margin;
cv.height = img.height + 2*margin;
var ctx = cv.getContext('2d');
ctx.fillStyle = color;
ctx.fillRect(0,0, cv.width, cv.height);
ctx.save();
ctx.globalCompositeOperation = 'destination-in';
var scale = cv.width/spr.width;
ctx.scale(cv.width/spr.width, cv.height/spr.height);
ctx.drawImage(img, -margin, -margin);
ctx.restore();
return cv;
}
You could use strokeRect method to outline the sprite after drawing it. It should be asy if you know your sprite's dimensions...

Canvas: mask an image and preserve its alpha channel?

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

Categories

Resources