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 am trying to create the puzzle pieces with the effect in the image but i am unable to get it through.
Can anyone help me how to create the pieces with canvas and html5.
thanks
You can draw a beveled edge puzzle piece this way.
Draw the part of the image you want to use for the piece positioned correctly
Build a path for the puzzle piece
Punch out the piece using composite mode destination-in
Add rect/bounding box to path
Define dark shadow and fill using composite mode source-atop
Move shadow and change color to bright, fill again
What you need to do in addition is to calculate the offset for the image you're using for each piece. You could also draw each piece into a separate canvas and extract the canvas as image which you later use to draw/show the pieces.
Example
var ctx = c.getContext("2d"), img = new Image;
img.onload = demo;
img.src = "https://pixeloceanblog.files.wordpress.com/2016/05/pres_frontcorner.jpg";
function demo() {
ctx.translate(-20,-90); // just to compensate for demo puzzle piece position
// 1) Draw puzzle pieze graphics
ctx.drawImage(this, -220, -110);
// 2) build path for puzzle piece
puzzlePath(ctx);
// 3) Punch out piece
ctx.globalCompositeOperation = "destination-in";
ctx.fill();
// 4) Add rect to make stencil
ctx.rect(0, 0, c.width, c.height);
// 5) Build dark shadow
ctx.shadowBlur = 7;
ctx.shadowOffsetX = -7;
ctx.shadowOffsetY = -7;
ctx.shadowColor = "rgba(0,0,0,0.8)";
// 6) Draw stencil with shadow but only on non-transparent pixels
ctx.globalCompositeOperation = "source-atop";
ctx.fill();
// 7) move shadow and change color to white transparent
ctx.shadowOffsetX = 7;
ctx.shadowOffsetY = 7;
ctx.shadowColor = "rgba(255,255,255,0.8)";
ctx.fill();
// DONE!
// Puzzle path for demo
function puzzlePath(ctx) {
ctx.beginPath();
ctx.moveTo(36.421871,256.82809);
ctx.bezierCurveTo(36.981411,272.02753,42.075181,289.07672,52.984371,298.17184);
ctx.bezierCurveTo(81.172101,311.4011,68.157281,279.26413,87.671871,275.48434);
ctx.bezierCurveTo(107.18646,271.70455,108.26562,294.39059,108.26562,294.39059);
ctx.lineTo(108.26562,294.39059);
ctx.lineTo(108.26562,392.14059);
ctx.bezierCurveTo(108.26562,392.14059,205.20313,392.01559,205.20312,392.01559);
ctx.bezierCurveTo(205.20312,392.01559,227.88915,390.93642,224.10937,371.42184);
ctx.bezierCurveTo(221.03829,355.56624,199.23133,361.18908,198.26562,348.73434);
ctx.bezierCurveTo(198.04276,345.86017,198.94138,342.01954,201.42187,336.73434);
ctx.bezierCurveTo(210.51699,325.82514,227.56618,320.73138,242.76562,320.17184);
ctx.bezierCurveTo(248.37657,319.96528,253.65356,320.45514,258.26562,321.42184);
ctx.bezierCurveTo(258.47031,321.44665,258.68579,321.48913,258.89062,321.51559);
ctx.bezierCurveTo(272.00503,323.2095,285.30842,328.27739,292.98437,337.48434);
ctx.bezierCurveTo(306.21364,365.67208,274.07665,352.65725,270.29687,372.17184);
ctx.bezierCurveTo(267.54294,386.39004,278.77521,390.78229,285.10937,392.14059);
ctx.lineTo(388.01562,392.14059);
ctx.lineTo(388.01562,290.54684);
ctx.bezierCurveTo(389.23388,284.34562,393.50469,272.30575,408.14062,275.14059);
ctx.bezierCurveTo(427.65522,278.92038,414.64038,311.02611,442.82812,297.79684);
ctx.bezierCurveTo(452.03508,290.1209,457.10296,276.8175,458.79687,263.70309);
ctx.bezierCurveTo(458.82334,263.49827,458.86581,263.28278,458.89062,263.07809);
ctx.bezierCurveTo(459.85733,258.46605,460.34718,253.18904,460.14062,247.57809);
ctx.bezierCurveTo(459.58109,232.37865,454.48732,215.32946,443.57812,206.23434);
ctx.bezierCurveTo(415.3904,193.00508,428.40521,225.14205,408.89062,228.92184);
ctx.bezierCurveTo(389.37605,232.70164,388.29687,210.01559,388.29687,210.01559);
ctx.lineTo(388.01562,112.82809);
ctx.bezierCurveTo(388.01562,112.82809,289.76561,113.4531,289.76562,113.45309);
ctx.bezierCurveTo(289.76562,113.45309,267.07957,114.53227,270.85937,134.04684);
ctx.bezierCurveTo(274.63916,153.56143,306.77612,140.54662,293.54687,168.73434);
ctx.bezierCurveTo(284.45175,179.64354,267.40256,184.73731,252.20312,185.29684);
ctx.bezierCurveTo(246.59217,185.5034,241.31517,185.01355,236.70312,184.04684);
ctx.bezierCurveTo(236.49843,184.02203,236.28293,183.97956,236.07812,183.95309);
ctx.bezierCurveTo(222.96371,182.25918,209.6603,177.1913,201.98437,167.98434);
ctx.bezierCurveTo(199.50388,162.69914,198.60526,158.85851,198.82812,155.98434);
ctx.bezierCurveTo(199.79383,143.5296,221.56954,149.15245,224.64062,133.29684);
ctx.bezierCurveTo(228.16732,115.08894,209.3009,113.03555,206.67187,112.82809);
ctx.lineTo(108.26562,112.82809);
ctx.lineTo(108.26562,214.98434);
ctx.bezierCurveTo(106.78368,221.38993,102.27374,231.94857,88.421871,229.26559);
ctx.bezierCurveTo(68.907281,225.48581,81.922111,193.38007,53.734371,206.60934);
ctx.bezierCurveTo(44.527431,214.28529,39.459541,227.58868,37.765621,240.70309);
ctx.bezierCurveTo(37.739161,240.90792,37.696681,241.1234,37.671871,241.32809);
ctx.bezierCurveTo(36.705171,245.94014,36.215311,251.21714,36.421871,256.82809);
}
}
body {background:#777}
<canvas id=c width=500 height=500></canvas>
Using masking to Apply a inner bevel FX
A very quick answer. I am sure someone can improve on this and are welcome to.
Function takes an image and returns a new image with a bevel applied.
Function arguments
image. The image to apply the bevel to
amount. The strength of the bevel 0 is none and 1 is full
offsetX
offsetY. The offset of the bevel.
blur. The blur. Must be greater or equal to 0
type. The type of bevel as string, ethier, 'shadow', 'light' or as a colour eg
'black'
Returns an image (as canvas)
Example usage
var image = new Image();
image.src = "imageURL.png";
image.onload = function(){
var bevImage = innerBevel(this,0.5,-4,-4,4,"shadow"); // bevel shadow
bevImage = innerBevel(bevImage,0.5,4,4,4,"light"); // bevel highlight
if(typeof ctx !== 'undefined'){
ctx.drawImage(bevImage,0,0); // draw the image to the current context
}
}
The function
function innerBevel(image, amount, offsetX, offsetY, blur, type){
var c = document.createElement("canvas");
c.width = image.width + (Math.abs(offsetX) + blur) * 2; // cludge could be a better fit
c.height = image.height + (Math.abs(offsetY) + blur) * 2; // cludge could be a better fit
var ctx1 = c.getContext("2d");
ctx1.fillRect(0,0,c.width,c.height); // fill pixels
// create the shadow mask
ctx1.globalCompositeOperation = "destination-out";
ctx1.drawImage(image, Math.abs(offsetX)+blur, Math.abs(offsetY)+blur); // create inverse mask
// create second image.
var c1 = document.createElement("canvas");
c1.width = image.width;
c1.height = image.height;
var ctx2 = c1.getContext("2d");
// copy the image
ctx2.drawImage(image,0,0);
// create the shadow and draw it as a shadow from the mask
ctx2.save();
if(type === "shadow"){
ctx2.shadowColor = "black";
ctx2.globalCompositeOperation = "multiply";
}else
if(type === "light"){
ctx2.shadowColor = "White";
ctx2.globalCompositeOperation = "lighter";
}else{
ctx2.shadowColor = type;
}
ctx2.globalAlpha = amount;
ctx2.shadowOffsetX = offsetX;
ctx2.shadowOffsetY = offsetY;
ctx2.shadowBlur = blur;
ctx2.drawImage(c,-(Math.abs(offsetX) + blur), -(Math.abs(offsetY) + blur)); // create inverse mask
ctx2.restore(); // remove the shadow settings
// mask out the unwanted pixels
ctx2.globalCompositeOperation = "destination-out";
ctx2.drawImage(c,-(Math.abs(offsetX) + blur), -(Math.abs(offsetY) + blur));
ctx2.globalCompositeOperation = "source-over";
return c1; // return the new beveled image;
}
I don't understand how swiffy succefully antialias clipping mask when it exports a flash animation that contains mask.
Here is my exemple :
Full canvas, with mask. Super dirty in Chrome : http://goo.gl/n8yB5h
And a swiffy export where there is a picture that move inside a mask. Super clean on Chrome :
http://www.creaktif.com/lab/test4.html
I tried a lot of things, including draw a 200% canvas then scale it down, adding more points when I draw my mask, but no way to get a clean mask in my canvas.
How does swiffy ?
It just turn me crazy.
Thanks. :)
Instead of using clip() you can draw the clipping path into an off-screen canvas and use that as a "matte", or rather, an alpha mask.
Now you can use the mask with various composite modes to get anti-aliased edges. Draw in the background you want to clip, set a composite mode (operator) and draw in mask. Depending on operator you can cut out center or the surroundings (destination-in is typical equivalent to using clip() though).
Note: demo below is only useful in Chrome/Opera as Firefox/IE already apply anti-aliasing to the clip mask - here's a snapshot showing the difference:
var ctxC = document.getElementById("clip").getContext("2d");
var ctxM = document.getElementById("mask").getContext("2d");
var w = ctxC.canvas.width, h = ctxC.canvas.height;
var img = new Image();
img.onload = demo;
img.src = "http://i.imgur.com/s9ksOb1.jpg";
function demo() {
// define a clip path
ctxC.save();
createPath(ctxC);
ctxC.clip();
ctxC.drawImage(this, 0, 0, w, h);
ctxC.restore();
// create a "matte" / alpha mask
var matte = document.createElement("canvas"),
ctx = matte.getContext("2d");
matte.width = w;
matte.height = h;
// fill the path instead:
createPath(ctx);
ctx.fill(); // color doesn't matter, alpha does
// now use composition to "clip"
ctxM.drawImage(this, 0, 0, w, h); // draw image
ctxM.globalCompositeOperation = "destination-in"; // will keep bg where mask cover
ctxM.drawImage(matte, 0, 0);
ctxM.globalCompositeOperation = "source-over"; // default mode
// zoom some details:
zoom(ctxC);
zoom(ctxM);
}
function createPath(ctx) {
var r = 88;
ctx.beginPath();
ctx.moveTo(100 + r, 90);
ctx.arc(100,90,r, 0, 6.28);
}
function zoom(ctx) {
ctx.imageSmoothingEnabled =
ctx.webkitImageSmoothingEnabled =
ctx.mozImageSmoothingEnabled = false;
ctx.drawImage(ctx.canvas, 10, 10, 100, 100, 70,0, 400, 400);
}
html, body {margin:4px 0 0 4px;overflow:hidden}
canvas{background:#000;border:1px solid #000;margin:0 1px 0 0}
<canvas id="clip" height=180></canvas>
<canvas id="mask" height=180></canvas>
I'm looking for a solution to change the texture/pattern for a product.
At this moment i have:
A .png picture of a couch with a transparent background
A .png picture of a texture
With the following code:
<canvas id="a" width="800" height="500">Canvas not supported on your browser</canvas>
var width = $(window).width();
var height = $(window).height();
var c = document.getElementById("a");
var ctx = c.getContext("2d");
var can2 = document.createElement('canvas');
document.body.appendChild(can2)
can2.width = c.width;
can2.height = c.height;
var ctx2 = can2.getContext("2d");
var test = new Image();
test.src = "Images/newBank.png";
test.onload = function () {
ctx2.drawImage(test, 0, 0);
};
var img = new Image();
img.src = "Images/texturetrans.png";
img.onload = function () {
ctx2.globalCompositeOperation = 'source-in';
var ptrn = ctx2.createPattern(img, 'repeat');
ctx2.fillStyle = ptrn;
ctx2.fillRect(0, 0, can2.width, can2.height);
}
`
I get this result:
As you can see, the whole object is filled with my texture. No definitions of the pillows etc. are visible anymore. Is it possible to let my texture be a sort of transparent mask?
I'm already able to change the color of the couch:
But I'd like to be able to also add a pattern to my couch!
Any help will be appreciated and I'm already very sorry for my bad English.
If you're just after an illustrative approximation you can use a combination of blending and composition modes.
First thing is to make sure your main image has transparency - this is important for composition to work (I made a rough cut-off in the following demo).
Main steps:
Draw the pattern
Draw the main image on top with blending mode multiply
Draw the main image on top with compositing mode destination-in - this will make a cut-out
If you want to reduce the size of the pattern you can either do this by using a smaller version of the image, draw to a temporary canvas at a smaller size and use that as pattern, or use the new transform methods on the pattern itself.
Demo
var img1 = new Image, img2 = new Image, cnt = 2,
canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d");
// image loading for demo (ignore)
img1.onload = img2.onload = function() {if (!--cnt) go()};
img1.src = "//i.imgur.com/8WqH9v4.png"; // sofa
img2.src = "//i.stack.imgur.com/sQlu8.png"; // pattern
// MAIN CODE ---
function go() {
// create a pattern
ctx.fillStyle = ctx.createPattern(img2, "repeat");
// fill canvas with pattern
ctx.fillRect(0, 0, canvas.width, canvas.height);
// use blending mode multiply
ctx.globalCompositeOperation = "multiply";
// draw sofa on top
ctx.drawImage(img1, 0, 0, img1.width*.5, img1.height*.5);
// change composition mode
ctx.globalCompositeOperation = "destination-in";
// draw to cut-out sofa
ctx.drawImage(img1, 0, 0, img1.width*.5, img1.height*.5);
}
<canvas id="canvas" width=600 height=400></canvas>
You can also reverse the order of which image is drawn etc., if you prefer. This is just an example of one way.
If you need accurate texture then there is no way around to either take photos or use a 3D software, or hand-drawn the textures.
NOTE: IE does not support multiply - For this you need to manually iterate through the pixels and multiply each component with each other.
You can test for support this way:
ctx.globalCompositeOperation = "multiply";
if (ctx.globalCompositeOperation === "multiply") {
// blend as above
}
else {
// iterate and blend manually
}
Blending mode luminosity is mentioned in comments and this can be used too of course. I just want to point a couple of things to consider. The first is that this is a non-separable blending mode meaning it depends on all components as it goes through the HSL color model. This makes it a bit more compute intensive.
The second is that if you end up having to do this manually (in for example IE) the code is a bit more complex to emulate, and will be noticeably slower.
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...