如何在JavaScript画布中将笔画/轮廓添加到透明PNG图像

时间:2014-06-04 14:03:50

标签: javascript image-processing canvas

使用JavaScript画布向透明PNG图像添加轮廓/笔触效果的最简单方法是什么?

我发现的最受欢迎的image effect个库没有中风效果。我发现StackOverflow上最接近的解决方案是using blur to give it a glow effect,而不是轮廓笔划。

原始图片

透明PNG图像,可以有多个分离的形状:

enter image description here

产生的图片

应用了轮廓描边和阴影的透明图像。

enter image description here

搜索继续......

当我搜索完成笔画效果的最简单方法时,我会更新此列表。相关问题:

1 个答案:

答案 0 :(得分:19)

这是在图片上添加“贴纸效果”的一种方法......

演示:http://jsfiddle.net/m1erickson/Q2j3L/

enter image description here

首先将原始图片绘制到主画布。

enter image description here

将图像分解为“离散元素”。

离散元素由彼此连接但未连接到其他元素的像素组组成。例如,spritesheet上的每个sprite都是一个离散元素。

您可以使用边缘检测算法(如“行进方块”)找到离散像素组。

将每个离散元素放在自己的画布上以进行进一步处理。还要从主画布中删除该离散元素(因此不会再次处理)。

enter image description here

检测每个离散元素的轮廓路径。

您可以再次使用“行进方块”算法进行边缘检测。行进方块的结果是x / y坐标数组,形成元素的外部轮廓

创建“贴纸效果”

您可以通过在每个元素周围添加描边白色轮廓来创建贴纸效果。通过抚摸上面计算的轮廓路径来完成此操作。您可以选择为笔划添加阴影。

注意:画布笔划始终是半内部和外部绘制的。半路外。这意味着贴纸笔划将侵入元素内部。要解决此问题:绘制标签笔划后,应将元素重新绘制在顶部。这会覆盖贴纸笔划的侵入部分。

enter image description here

重新制作最终影像,包括贴纸效果

通过将每个元素的画布分层到主画布上来重构最终图像

enter image description here

以下是带注释的示例代码:

<!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>

这是一个行进方块边缘检测算法(来自优秀的开源d3库):

/** 
 * 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; 
    } 
  } 
} 

})();

注意:此代码将贴纸轮廓应用的过程分为单独的功能。如果你想在你的离散元素周围有多个图层,那就完成了。例如,您可能希望在贴纸笔划的外侧有第二个灰色边框。如果您不需要应用“图层”,则可以在moveDiscreteElementToNewCanvas函数中应用标签笔划。