What is the easiest way to add an outline/stroke effect to a transparent PNG image using JavaScript canvas?
Most popular image effect libraries I found does not have stroke effect. The closest solution on StackOverflow I found is using blur to give it a glow effect instead of outline stroke.
Original picture
Transparent PNG image that can have multiple separated shapes:
Resulting image
Transparent image with outline stroke and shadow applied to it.
The search continues...
I'll update this list as I search for the easiest way to accomplish the stroke effect. Related questions:
Here's one way to add a "sticker effect" on your image...
A Demo: http://jsfiddle.net/m1erickson/Q2j3L/
Start by drawing your original image to the main canvas.
Decompose the image into “discrete elements”.
Discrete elements consist of groups of pixels that are connected to each other but not connected to other elementss. For example, each individual sprite on a spritesheet would be a discrete element.
You can find discrete pixel groups using an edge detection algorithm like "marching squares".
Put each discrete element on its own canvas for further processing. Also erase that discrete element from the main canvas (so it's not processed again).
Detect the outline-path of each discrete element.
You can again use the “marching squares” algorithm to do edge detection. The result of marching squares is an array of x/y coordinates that form the outside outline of the element
Create the “sticker effect”
You can create a sticker effect by putting a stroked white outline around each element. Do this by stroking the outline path which you calculated above. You can optionally add a shadow to the stroke.
Note: canvas strokes are always drawn half-inside & half-outside the path. This means that the sticker-stroke will intrude inside the element. To fix this: After you have drawn the sticker-stroke you should redraw the element back on top. This overwrites the intruding part of the sticker-stroke.
Recompose the final image including the sticker-effect
Recompose the final image by layering each element's canvas onto the main canvas
Here is annotated example code:
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="marching squares.js"></script>
<style>
body{ background-color:silver; }
canvas{border:1px solid red;}
</style>
<script>
$(function(){
// canvas related variables
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
// variables used in pixel manipulation
var canvases=[];
var imageData,data,imageData1,data1;
// size of sticker outline
var strokeWeight=8;
// true/false function used by the edge detection method
var defineNonTransparent=function(x,y){
return(data1[(y*cw+x)*4+3]>0);
}
// the image receiving the sticker effect
var img=new Image();
img.crossOrigin="anonymous";
img.onload=start;
img.src="https://dl.dropboxusercontent.com/u/139992952/multple/makeIndividual.png";
//img.src="https://dl.dropboxusercontent.com/u/139992952/stackoverflow/angryBirds.png";
function start(){
// resize the main canvas to the image size
canvas.width=cw=img.width;
canvas.height=ch=img.height;
// draw the image on the main canvas
ctx.drawImage(img,0,0);
// Move every discrete element from the main canvas to a separate canvas
// The sticker effect is applied individually to each discrete element and
// is done on a separate canvas for each discrete element
while(moveDiscreteElementToNewCanvas()){}
// add the sticker effect to all discrete elements (each canvas)
for(var i=0;i<canvases.length;i++){
addStickerEffect(canvases[i],strokeWeight);
ctx.drawImage(canvases[i],0,0);
}
// redraw the original image
// (necessary because the sticker effect
// slightly intrudes on the discrete elements)
ctx.drawImage(img,0,0);
}
//
function addStickerEffect(canvas,strokeWeight){
var url=canvas.toDataURL();
var ctx1=canvas.getContext("2d");
var pts=canvas.outlinePoints;
addStickerLayer(ctx1,pts,strokeWeight);
var imgx=new Image();
imgx.onload=function(){
ctx1.drawImage(imgx,0,0);
}
imgx.src=url;
}
function addStickerLayer(context,points,weight){
imageData=context.getImageData(0,0,canvas.width,canvas.height);
data1=imageData.data;
var points=geom.contour(defineNonTransparent);
defineGeomPath(context,points)
context.lineJoin="round";
context.lineCap="round";
context.strokeStyle="white";
context.lineWidth=weight;
context.stroke();
}
// This function finds discrete elements on the image
// (discrete elements == a group of pixels not touching
// another groups of pixels--e.g. each individual sprite on
// a spritesheet is a discreet element)
function moveDiscreteElementToNewCanvas(){
// get the imageData of the main canvas
imageData=ctx.getImageData(0,0,canvas.width,canvas.height);
data1=imageData.data;
// test & return if the main canvas is empty
// Note: do this b/ geom.contour will fatal-error if canvas is empty
var hit=false;
for(var i=0;i<data1.length;i+=4){
if(data1[i+3]>0){hit=true;break;}
}
if(!hit){return;}
// get the point-path that outlines a discrete element
var points=geom.contour(defineNonTransparent);
// create a new canvas and append it to page
var newCanvas=document.createElement('canvas');
newCanvas.width=canvas.width;
newCanvas.height=canvas.height;
document.body.appendChild(newCanvas);
canvases.push(newCanvas);
var newCtx=newCanvas.getContext('2d');
// attach the outline points to the new canvas (needed later)
newCanvas.outlinePoints=points;
// draw just that element to the new canvas
defineGeomPath(newCtx,points);
newCtx.save();
newCtx.clip();
newCtx.drawImage(canvas,0,0);
newCtx.restore();
// remove the element from the main canvas
defineGeomPath(ctx,points);
ctx.save();
ctx.clip();
ctx.globalCompositeOperation="destination-out";
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.restore();
return(true);
}
// utility function
// Defines a path on the canvas without stroking or filling that path
function defineGeomPath(context,points){
context.beginPath();
context.moveTo(points[0][0],points[0][1]);
for(var i=1;i<points.length;i++){
context.lineTo(points[i][0],points[i][1]);
}
context.lineTo(points[0][0],points[0][1]);
context.closePath();
}
}); // end $(function(){});
</script>
</head>
<body>
<canvas id="canvas" width=300 height=300></canvas><br>
</body>
</html>
This is a marching squares edge detection algorithm (from the excellent open-source d3 library):
/**
* Computes a contour for a given input grid function using the <a
* href="http://en.wikipedia.org/wiki/Marching_squares">marching
* squares</a> algorithm. Returns the contour polygon as an array of points.
*
* @param grid a two-input function(x, y) that returns true for values
* inside the contour and false for values outside the contour.
* @param start an optional starting point [x, y] on the grid.
* @returns polygon [[x1, y1], [x2, y2], ...]
*/
(function(){
geom = {};
geom.contour = function(grid, start) {
var s = start || d3_geom_contourStart(grid), // starting point
c = [], // contour polygon
x = s[0], // current x position
y = s[1], // current y position
dx = 0, // next x direction
dy = 0, // next y direction
pdx = NaN, // previous x direction
pdy = NaN, // previous y direction
i = 0;
do {
// determine marching squares index
i = 0;
if (grid(x-1, y-1)) i += 1;
if (grid(x, y-1)) i += 2;
if (grid(x-1, y )) i += 4;
if (grid(x, y )) i += 8;
// determine next direction
if (i === 6) {
dx = pdy === -1 ? -1 : 1;
dy = 0;
} else if (i === 9) {
dx = 0;
dy = pdx === 1 ? -1 : 1;
} else {
dx = d3_geom_contourDx[i];
dy = d3_geom_contourDy[i];
}
// update contour polygon
if (dx != pdx && dy != pdy) {
c.push([x, y]);
pdx = dx;
pdy = dy;
}
x += dx;
y += dy;
} while (s[0] != x || s[1] != y);
return c;
};
// lookup tables for marching directions
var d3_geom_contourDx = [1, 0, 1, 1,-1, 0,-1, 1,0, 0,0,0,-1, 0,-1,NaN],
d3_geom_contourDy = [0,-1, 0, 0, 0,-1, 0, 0,1,-1,1,1, 0,-1, 0,NaN];
function d3_geom_contourStart(grid) {
var x = 0,
y = 0;
// search for a starting point; begin at origin
// and proceed along outward-expanding diagonals
while (true) {
if (grid(x,y)) {
return [x,y];
}
if (x === 0) {
x = y + 1;
y = 0;
} else {
x = x - 1;
y = y + 1;
}
}
}
})();
Note: This code separates the process of applying the sticker outline into a separate function. That's done in case you want to have multiple layers around your discrete element. For example, you might want a second gray border on the outside of the sticker-stroke. If you don't need to apply "layers" then you could apply the sticker-stroke within the moveDiscreteElementToNewCanvas
function.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With