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 using HTML5 canvas in a project and occasionally need to draw drop shadows on SVGs within a canvas. I've noticed that, compared to Chrome, Safari does two things incorrectly when doing this:
Safari draws a shadow on each individual shape within an SVG
Safari crops off parts of the shadow that go beyond the SVG's bounds
These issues can be illustrated by the following code:
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.shadowOffsetX = 10;
context.shadowOffsetY = 10;
context.shadowColor = 'red'
var image = new Image();
image.src = 'https://storage.googleapis.com/card-conjurer/img/manaSymbols/0.svg';
image.onload = function() {
context.drawImage(image, 10, 10, 100, 100);
}
<canvas id='canvas'></canvas>
I can't embed images yet, but here are some links to images that illustrate the problem:
SVG Shadows with Google Chrome
SVG Shadows with Safari
(they are screenshots of the code above)
The results from Safari are... quite ugly, as you can see. Is there a way make Safari to render SVGs with shadows on HTML5 canvas like Chrome does?
Any help would be greatly appreciated.
Thanks so much for your time!
That's a bug, you should report it to webkit's bug-tracker.
Though you can workaround it by first drawing the image on a second canvas just to rasterize that svg image and use that canvas as source for the shadowing:
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
var image = new Image();
image.src = 'https://storage.googleapis.com/card-conjurer/img/manaSymbols/0.svg';
image.onload = function() {
const off = canvas.cloneNode();
off.getContext('2d').drawImage(image, 10, 10, 100, 100);
context.shadowOffsetX = 10;
context.shadowOffsetY = 10;
context.shadowColor = 'red';
context.drawImage(off, 0, 0);
}
<canvas id='canvas'></canvas>
In order to use a single canvas, we need to use an offset trick, but it's not always easy to do since it requires knowing clearly the position of our drawing:
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
var image = new Image();
image.src = 'https://storage.googleapis.com/card-conjurer/img/manaSymbols/0.svg';
image.onload = function() {
// first pass without shadow
context.drawImage(image, 10, 10, 100, 100);
// set shadow offsets to the position in page of bottom-right corner
context.shadowOffsetX = 10 + 110;
context.shadowOffsetY = 10 + 110;
context.shadowColor = 'red';
// draw behind
context.globalCompositeOperation = "destination-over";
// draw with inverse offset, so that the image is not visible
// but the shadow is in-screen
context.drawImage(canvas, -110, -110);
}
<canvas id='canvas'></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>
How can I save fragment in memory?
var img = document.getElementById('img1');
ctx.save();
ctx.rotate(Math.PI / 180 * 45);
ctx.drawImage(img, 100, 100, img.width, img.height);
ctx.restore();
And now I want save this fragment (already rotated) to some array, and then just call drawImage without new rotating.
Is it real in JS and canvas or I should rotate image every time, when I want draw it?
var url = canvas.toDataURL();
may assist you with storing the canvas data in memory. It can then be re-drawn from this url later. e.g.
img.src = url;
ctx.drawImage(img,0,0);
I assume what you want is to store a rotated image.
To do it, most easy way is to create a canvas, then draw rotated into this canvas : you now have your rotated image inside a canvas, that you might want to transform into an image.
However, the security policy prevents you from creating an image out of a canvas if you used an image coming from another domain. So be sure to use canvas only in such a case.
http://jsbin.com/gemijekewewu/1/edit?js,output
(thx #markE for careful reading).
function getRotatedImage(sourceImage, provideImage) {
var cv = document.createElement('canvas');
cv.width = sourceImage.height;
cv.height = sourceImage.width;
var ctx = cv.getContext('2d');
ctx.translate(cv.width/2, cv.height/2);
ctx.rotate(Math.PI / 2);
ctx.drawImage(sourceImage, -cv.width/2, -cv.height/2);
if (provideImage) {
var img=new Image();
img.src= cv.toDataURL();
return img;
} else
return cv;
}
use with :
// during the init ...
var rotatedImage=getRotatedImage(myImage);
// ... later ...
ctx.drawImage(rotatedImage, ... , ...);
I want to (I have accomplished 1-3):
grab an image from the page
add it to a canvas element
clip the image to a mask
move (animate) the mask around the image
I have accomplished the first 3, but cannot figure out how to move the mask.. Please help!
// get the canvas
var canvas = document.getElementById('c');
var context = canvas.getContext('2d');
// get a div from the page
var stuff = document.getElementsByName('testElem')[0];
// add a class to the div
stuff.setAttribute('class', 'pleaseWork');
var newImage = new Image();
// get the background image of the div
var bgClass = document.getElementsByClassName('pleaseWork')[0].style.backgroundImage;
var x = canvas.width / 2;
var y = canvas.height / 2;
var radius = 75;
var offset = 50;
// clip the context to create a circular clipping mask
context.save();
context.beginPath();
context.arc(x, y, radius, 0, 2 * Math.PI, false);
context.clip();
// on load put the image into the clipping mask
newImage.onload = function () {
context.drawImage(newImage,0,0);
}
// put the background image from the div into the canvas (without the url())
newImage.src = bgClass.replace(/^url|[\(\)]/g, '');
How can I move (animate) the clipping mask from the canvas to show different parts of the clipped image?
Thanks for any ideas!
You can put your clip+drawing code in a function and call that function inside an animation loop:
Example code and a Demo: http://jsfiddle.net/m1erickson/07mzbat9/
Drawing Function:
function draw(){
// clear the canvas
ctx.clearRect(0,0,cw,ch);
// save the unclipped context
ctx.save();
// draw a circular path
ctx.beginPath();
ctx.arc(cx,cy,radius,0,PI2);
ctx.closePath();
ctx.stroke();
// create a clipping path from the circular path
// use the clipping path to restrict drawing
ctx.clip();
ctx.drawImage(img,0,0);
// restore the unclipped context (to remove clip)
ctx.restore();
}
Animation loop:
var cw=canvas.width;
var ch=canvas.height;
var cx=50;
var cy=50;
var radius=35;
var PI2=Math.PI*2;
// animate the mask across the canvas
function animate(time){
if(cx-radius>cw || cy-radius>ch){return;}
requestAnimationFrame(animate);
cx+=0.2;
cy+=0.2;
draw();
}