I'm writing a "TV filter" (you know the kind, RGB bars as it zooms in), for a video file and I've been having a look at some ways of shrinking the image that retains as much detail as possible.
For testing I'm drawing the sampled image back to the screen to see the quality - in the actual filter, I'll just be sampling pixels and getting the RGB values of the resultant computed color.
I've tried three, and the Hermite filter looks good, but compared to the speed "hardware" nearest neighbour version, it's not going to be suitable for processing video.
Is there any "tricks" in JavaScript that can be used to get accelerated image shrinking like 2, but with a quality like 1 or 3?
1: Brute force: http://codepen.io/SarahC/pen/VpvWvb?editors=1010
2: Internal nearest neighbour: http://codepen.io/SarahC/pen/ryeQgN?editors=1010
3: Hermite filter: http://codepen.io/SarahC/pen/ryMNWZ?editors=1010
Here's the "hardware"? version:
function processResize(percent) {
var size = percent * 0.01;
var sw = canvas.width * size;
var sh = canvas.height * size;
ctx.drawImage(canvas2, 0, 0, sw, sh);
ctx.drawImage(canvas, 0, 0, sw, sh, 0, 0, w, h);
}
I am not entirely sure from the description what you try to achieve, but from the codepens it seems as you try to create a mosaic effect.
You can use the built-in interpolation setting of the canvas context to use nearest-neighbor by turning image smoothing off, then draw the image to a small size representing how many "blocks" you want. Then draw back that version to full size again:
// blocks = initial number of pixels (video aspect is usually 16:9 so you may want
// to calculate a separate values for height.
var blocks = 24;
// draw initial size representing "blocks"
ctx.drawImage(video, 0, 0, blocks, blocks);
// turn off image smoothing (see below for prefixing)
// This uses nearest neighbor
ctx.imageSmoothingEnabled = false;
// enlarge the mosaic back to full size
ctx.drawImage(c, 0, 0, blocks, blocks, 0, 0, c.width, c.height);
Video Example
(the video may take a few seconds to load...)
var ctx = null;
var blocks = 24;
var video = document.createElement("video");
video.preload = "auto"; video.muted = video.autoplay = video.loop = true;
video.oncanplay = function() { // initialize for demo
if (!ctx) {
c.width = this.videoWidth;
c.height = this.videoHeight;
ctx = c.getContext("2d");
document.querySelector("input").oninput = function() {blocks = +this.value};
requestAnimationFrame(loop);
}
}
video.src = "//media.w3.org/2010/05/sintel/trailer.mp4";
function smoothing(state) {
ctx.oImageSmoothingEnabled = ctx.msImageSmoothingEnabled =
ctx.mozImageSmoothingEnabled = ctx.webkitImageSmoothingEnabled =
ctx.imageSmoothingEnabled = state;
}
function loop() {
smoothing(true); // improve quality of first step
ctx.drawImage(video, 0, 0, blocks, blocks);
smoothing(false); // mosaic step
ctx.drawImage(c, 0, 0, blocks, blocks, 0, 0, c.width, c.height);
// loop and throttle to 30 fps
requestAnimationFrame(function() {requestAnimationFrame(loop)});
}
<label>Blocks: <input type=range min=8 max=128 value=24></label><br>
<canvas id=c></canvas>
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 want to do client side real-time interactive thresholding of large images using a slider. Is it possible to threshold an image in javascript to produce a binary image without using a for-loop over all pixels? And if so, is it faster?
This can be done using only globalCompositeOperations, in two stages.
Set all pixels below threshold to 0 (black).
'Divide' this image by itself, using an algorithm that defines 0/0 = 0
Define three canvases, one on screen, one to hold the greyscale image off screen, and another off screen 'working' canvas.
//--- on-screen canvas
var onScreenCanvas=document.getElementById("canvasTest");
var ctxOnScreen=onScreenCanvas.getContext("2d");
//--- off-screen working canvas
var drawingCanvas = document.createElement('canvas');
var ctx=drawingCanvas.getContext("2d");
//--- off-screen canvas to store the greyscale image
var greyscaleImageCanvas = document.createElement('canvas');
var ctxGreyscaleImage=greyscaleImageCanvas.getContext("2d");
Load the greyscale image onto the greyscaleImageCanvas, then the following two operations achieve step 1, where thresh_str is a hex string for the threshold value between 0-FF for each of RGB
//(1a) Threshold the image on the offscreen working canvas,
// reducing values above threshold to have threshold value
ctx.drawImage(greyscaleImageCanvas, 0, 0);
ctx.globalCompositeOperation='darken';
ctx.fillStyle=thresh_str;
ctx.fillRect(0,0, drawingCanvas.width, drawingCanvas.height);
//(1b) Set everything *below* threshold to 0 (black) since that part is unchanged
// from the original image. Pixels above threshold are all non-zero.
ctx.globalCompositeOperation='difference';
ctx.drawImage(greyscaleImageCanvas, 0, 0);
There is no straight 'divide' operation defined for HTML globalCompositeOperations, but there is a 'color-dodge', which divides the bottom layer by the inverted top layer. So the desired result is achieved by first making an inverted copy of the output of step 1, and then using the color-dodge operation (which does define 0/0=0) to 'un-invert' it before dividing. The result is that non-zero (above-threshold) pixels become 1, zero (sub-threshold) pixels stay zero.
//(2a) Copy the result of (1b) to the onscreen canvas
ctxOnScreen.globalCompositeOperation='copy';
ctxOnScreen.drawImage(drawingCanvas, 0, 0);
//(2b) Invert the result of step (1b) so that it can be 'un-inverted' by color dodge
ctx.globalCompositeOperation='difference';
ctx.fillStyle='white';
ctx.fillRect(0,0,onScreenCanvas.width,onScreenCanvas.height);
//(2c) 'color-dodge' the results of (1b) with it's own inverse (2b)
ctxOnScreen.globalCompositeOperation='color-dodge';
ctxOnScreen.drawImage(drawingCanvas, 0, 0);
This method appears to be 3-5 times faster than a for-loop, at least on Chrome 79 on a Mac and android (Huawei P10) JSPerf
function img2grey(canvasContext) {
canvasContext.globalCompositeOperation='color';
canvasContext.fillStyle='white';
canvasContext.fillRect(0,0,onScreenCanvas.width,onScreenCanvas.height);
}
//--- on-screen canvas
var onScreenCanvas=document.getElementById("canvasTest");
var ctxOnScreen=onScreenCanvas.getContext("2d");
//--- off-screen working canvas
var drawingCanvas = document.createElement('canvas');
var ctx=drawingCanvas.getContext("2d");
//--- off-screen canvas to store the greyscale image
var greyscaleImageCanvas = document.createElement('canvas');
var ctxGreyscaleImage=greyscaleImageCanvas.getContext("2d");
var image = new Image();
function thresholdImage(thresh_val) {
if(thresh_val.length == 1){
thresh_val = '0' + thresh_val;
}
thresh_str = '#'+thresh_val+thresh_val+thresh_val;
ctxOnScreen.clearRect(0, 0, onScreenCanvas.width, onScreenCanvas.height);
ctx.clearRect(0, 0, drawingCanvas.width, drawingCanvas.height);
//----- (1) Threshold the image on the offscreen working canvas,
// reducing values above threshold to have threshold value
ctx.drawImage(greyscaleImageCanvas, 0, 0);
ctx.globalCompositeOperation='darken';
ctx.fillStyle=thresh_str;
ctx.fillRect(0,0,onScreenCanvas.width,onScreenCanvas.height);
//----- (2) Set everything *below* threshold to 0 (black) since that part is unchanged
// from the original image
ctx.globalCompositeOperation='difference';
ctx.drawImage(greyscaleImageCanvas, 0, 0);
//----- (3) Copy the result to the onscreen canvas
ctxOnScreen.globalCompositeOperation='copy';
ctxOnScreen.drawImage(drawingCanvas, 0, 0);
//----- (4) Invert the result of step (2) so that it can be 'un-inverted' by color dodge
ctx.globalCompositeOperation='difference';
ctx.fillStyle='white';
ctx.fillRect(0,0,onScreenCanvas.width,onScreenCanvas.height);
//----- (5) 'color-dodge' the results of (2) with it's own inverse (4)
//----- This makes use of 0/0 defined as 0 in this globalCompositeOperation,
//----- so that non-zero (suprathreshold) pixels become 1, zero (sub-threshold) pixels stay zero
//~ ctxOnScreen.globalCompositeOperation='color-dodge';
ctxOnScreen.globalCompositeOperation='color-dodge';
ctxOnScreen.drawImage(drawingCanvas, 0, 0);
}
image.onload = function() {
onScreenCanvas.width = image.width;
onScreenCanvas.height = image.height;
drawingCanvas.width = image.width;
drawingCanvas.height = image.height;
greyscaleImageCanvas.width = image.width;
greyscaleImageCanvas.height = image.height;
//!!NB Doesn't work on chrome for local files, use firefox
// https://stackoverflow.com/questions/45444097/the-canvas-has-been-tainted-by-cross-origin-data-local-image
ctxGreyscaleImage.drawImage(image, 0, 0);
img2grey(ctxGreyscaleImage);
thresholdImage((Math.round(rng.value)).toString(16));
};
var rng = document.querySelector("input");
var listener = function() {
window.requestAnimationFrame(function() {
thresholdImage( (Math.round(rng.value)).toString(16) );
});
};
rng.addEventListener("mousedown", function() {
listener();
rng.addEventListener("mousemove", listener);
});
rng.addEventListener("mouseup", function() {
rng.removeEventListener("mousemove", listener);
});
image.src = "https://i.imgur.com/vN0NbVu.jpg";
.slider-width100 {
width: 255px;
}
<html>
<head>
</head>
<body>
<canvas id="canvasTest"></canvas>
<input class="slider-width100" type="range" min="0" max="254" value="122" />
</body>
</html>
I am new with canvas and I've been Googling for a couple of hours, but I am stuck.
What I would like to do is to render a video on a canvas element, divide it and animate the pieces. I am halfway there (see: http://jsbin.com/riduxadazi/edit?html,css,js,console,output ) but I have a couple of questions:
Am I doing things right, or is this extremly inefficient?
I would like to use the video fullscreen. Whatever I try, the canvas grid + video don't seem to match size.
I would like to animate the pieces of the video, but I have no clue how I should address them. Can I get some sort of array and animate the pieces one by one?
My JS looks like this. I tried to add comments to the most important parts. At least what I think were the most important parts ;)
var video = document.getElementById('video'); // Get the video
var ctx = canvas.getContext('2d'),
columns = 6,
rows = 4,
w, h, tileWidth, tileHeight;
// Start video and add it to canvas
video.addEventListener('play', function() {
var $this = this; //cache
(function loop() {
if (!$this.paused && !$this.ended) {
ctx.drawImage($this, 0, 0,window.innerWidth,window.innerHeight);
calcSize(); // Divide video
setTimeout(loop, 1000 / 30); // drawing at 30fps
}
})();
}, 0);
function calcSize() {
video.width = w = window.innerWidth;
video.height = h = window.innerHeight;
tileWidth = w / columns;
tileHeight = h / rows;
ctx.strokeStyle = '#000';
render();
}
function render() {
for(var x = 0; x < columns; x++) {
ctx.moveTo(x * tileWidth, 0);
ctx.lineTo(x * tileWidth, h);
}
for(var y = 0; y < rows; y++) {
ctx.moveTo(0, y * tileHeight);
ctx.lineTo(w, y * tileHeight);
}
ctx.stroke();
}
You would perhaps consider:
Using requestAnimationFrame to update the loop. This allows for perfect synchronization with the monitor update rate as well as being more efficient than setTimeout/setInterval You could throttle it so you only update per 1/30 frame to match video rate by using a simple boolean flag that alternates.
The video element does not need to be inserted into DOM. Also, the actual video bitmap size is read through the properties videoWidth and videoHeight, though, in the provided code you should use canvas' properties width and height as this determine the destination size. To draw proportional you can for example use this answer.
Using drawImage() using the clipping parameters would be the more efficient way to draw video onto canvas if you want to split the content.
You could split your video using a mathematical approach (see this answer) or using objects which allows you to define source regions and have individual properties on it such as position, rotation, scale and so forth. In case you would have to consider destination position to adopt to the current size of canvas.
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...
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