Merge canvas image and canvas alpha mask into dataurl generated png - javascript

given two canvas with the same pixel size, where canvas1 contains an arbitrary image (jpg, png or such) and canvas2 contains black and non-black pixels.
what i want to achive: using a third canvas3 i want to clone canvas1 and have every black canvas2 pixel (may including a threshold of blackness) be transparent in canvas3
i already have a working solution like this:
canvas3context.drawImage(canvas1,0,0);
var c3img = canvas3context.getImageData(0,0,canvas3.width,canvas3.height);
var c2img = canvas2context.getImageData(0,0,canvas2.width,canvas2.height);
loop(){
if(c2img i-th,i+1-th,i+2-th pixel is lower than threshold)
set c3img.data[i]=c3img.data[i+1]=c3img.data[i+2]=c3img.data[i+3]=0
}
the problem with above (pseudo) code is, that it is slow
so my question is: anyone can share an idea how to speed this up significantly?
i thought about webgl but i never worked with it - so i have no idea about shaders or the tools or terms needed for this. another idea was that maybe i could convert canvas2 to black&white somehow very fast (not just modifieng every pixel in a loop like above) and work with blend modes to generate the transparent pixels
any help is highly appreciated

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

Related

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

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

HTML Canvas draw image from database [duplicate]

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

ctx.clearRect canvas sprite

I am wondering how I could alter my Javascript to only clear the falling sprites, and not the entire canvas (as it does currently).
I hope to place multiple other (animated) sprites on the canvas, which do not appear with the way my function animate is structured.
Is there a way so that if there was another image/sprite was on the canvas, it would not be affected by the function animate.
I'm thinking that this line needs to change:
ctx.clearRect(0, 0, canvas.width, canvas.height);
Though I have no idea what parameters I would need to place inside.
The falling sprites draw at a size of 60x60, but as they fall downwards this is where I am a bit stuck with clearing the only the sprite path.
Any help would be appreciated :)
var canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d");
canvas.width = 1408;
canvas.height = 640;
canvasWidth = canvas.width;
canvasHeight = canvas.height;
var orangeEnemy = new Image();
orangeEnemy.src = "http://www.catholicsun.org/wp-content/uploads/2016/09/cropped-sun-favicon-512x512-270x270.png";
var yellowEnemy = new Image();
yellowEnemy.src = "http://www.clker.com/cliparts/o/S/R/S/h/9/transparent-red-circle-hi.png";
var srcX;
var srcY;
var enemySpeed = 2.75;
var images = [orangeEnemy, yellowEnemy];
var spawnLineY=-50;
var spawnRate=2500;
var spawnRateOfDescent=1.50;
var lastSpawn=-1;
var objects=[];
var startTime=Date.now();
animate();
function spawnRandomObject() {
var object = {
x: Math.random() * (canvas.width - 15),
y: spawnLineY,
image: images[Math.floor(Math.random() * images.length)]
}
objects.push(object);
}
function animate(){
var time=Date.now();
if(time>(lastSpawn+spawnRate)){
lastSpawn=time;
spawnRandomObject();
}
requestAnimationFrame(animate);
ctx.clearRect(0, 0, canvas.width, canvas.height);
// move each object down the canvas
for(var i=0;i<objects.length;i++){
var object=objects[i];
object.y += enemySpeed;
ctx.drawImage(object.image, object.x, object.y, 60, 60);
}
}
<html>
<canvas id="canvas" style="border:3px solid"></canvas>
</html>
The easiest and quickest way would be to overlay another canvas, specifically for your sprites, atop your current canvas (requires a bit of CSS). Put all your sprites in one, everything else in the other. The clearRect() in your animate() function will then only apply to your sprite canvas, and not the other.
Otherwise, you will have to keep track of the positions of the sprites, and clear each programatically with 60x60 rectangles using clearRect(offsetX, offsetY, 60, 60).
P.S. excuse the non-formatted answer... still figuring SO out
Clear once for performance.
You are much better off clearing the whole canvas and redrawing the sprites. Using the previous position, checking for overlap and then clearing each sprite in turn, making sure you don't clear an existing sprite will take many more CPU cycles than clearing the screen once.
The clear screen function is very fast and is done in hardware, the following is the results of a performance test on Firefox (currently the quickest renderer) of clearing 65K pixels using just one call for whole area then 4 calls each a quarter, then 16 calls each clearing a 16th. (µs is 1/1,000,000th second)
Each test clears 256*256 pixels Each sample is 100 tests
'Clear 1/1' Mean time: 213µs ±4µs 1396 samples
'Clear 4/4' Mean time: 1235µs ±14µs 1390 samples
'Clear 16/16' Mean time: 4507µs ±42µs 1405 samples
As you can see clearing 65K pixels is best done in one call with the actual javascript call adding about 100µs to do.
On Firefox the number of pixels to clear does not affect the execution time of the call to clearRect with a call clearRect(0,0,256,256) and clearRect(0,0,16,16) both taking ~2µs
Apart from the speed, trying to clear overlapping animated sprites per sprite becomes extremely complicated and can result in 100s of clear calls with only a dozen sprites. This is due to the overlapping.

HTML5 Canvas blendmode

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

drawImage() implementation

I have two canvases that are different sizes. My goal is copy the user's drawing from the main canvas to a second canvas as a scaled down version. So far the drawImage() and scale appear to be working, but the second canvas keeps the old version of the main drawing along with the new copy. I tried clearing it each time before calling drawImage(), but that doesn't appear to do anything. How can I copy just the current image to my secondary canvas each time the function runs?
$('#hand').dblclick(function(){
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
//var imageData = context.getImageData(0, 0, 100, 100);
var newCanvas = document.getElementById('scaledCanvas');
var destCtx = newCanvas.getContext('2d');
destCtx.clearRect(0, 0, newCanvas.width, newCanvas.height);
destCtx.scale(.5,.5);
destCtx.drawImage(canvas, 0, 0);
});
I can include more code if necessary. I also just realized that scale keeps getting called; this explains why the new copied image would get smaller each time as well, so that might be another problem.
It's quite simple actually, you're using what's called a transform (translate, rotate, or scale).
In order to use them "freshly" each time you must save and restore the canvas state each time.
$('#hand').dblclick(function(){
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
//var imageData = context.getImageData(0, 0, 100, 100);
var newCanvas = document.getElementById('scaledCanvas');
var destCtx = newCanvas.getContext('2d');
destCtx.clearRect(0, 0, newCanvas.width, newCanvas.height);
//save the current state of this canvas' drawing mode
destCtx.save();
destCtx.scale(.5,.5);
destCtx.drawImage(canvas, 0, 0);
//restore destCtx to a 1,1 scale (and also 0,0 origin and 0 rotation)
destCtx.restore();
});
It's also important to note you can push several times before calling restore, in order to perform many cool geometric tricks using recursive functions etc...
Take a look at this explanation of states and transformations:
https://developer.mozilla.org/en-US/docs/HTML/Canvas/Tutorial/Transformations
Hope this helps you understand canvas transforms a bit better.

Categories

Resources