如何在JavaScript画布中为透明PNG图像添加描边/轮廓

11

如何使用JavaScript canvas最简单地为透明PNG图像添加轮廓/描边效果?

我发现大多数流行的图像特效库(例如glfx.jsPixastic)都没有描边效果。我在StackOverflow上找到的最接近的解决方案是使用模糊效果来产生发光效果,而不是描边效果。

原始图片

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

enter image description here

生成的图片

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

enter image description here

继续搜索...

我将更新此列表,以寻找实现描边效果的最简单方法。相关问题:


使用您找到的相同答案,但应用实心白线而不是发光线。 - markE
给定的算法(他们称之为“行进蚂蚁”,但它让人想起摩尔邻域)在您拥有具有分离区域的图像时无法工作:http://jsfiddle.net/dfkFF/ - Domas
你可以使用“Marching Squares”边缘检测算法来分离你的区域,并勾勒出这些区域的轮廓。请查看我的编辑答案。祝你的项目好运! - markE
我想到了一个更快的解决方法,使用阴影:https://dev59.com/HF4c5IYBdhLWcg3wMn31#63958475 - Josh
1个回答

24

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

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

插入图片描述

首先,在主画布上绘制原始图像。

插入图片描述

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

离散元素由相互连接而不与其他元素连接的像素组成。例如,精灵表中的每个单独的精灵都是一个离散元素。

您可以使用"Marching Squares"等边缘检测算法找到离散像素组。

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

插入图片描述

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

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

创建“贴纸效果”

你可以通过在每个元素周围绘制白色描边来创建贴纸效果。 通过描绘上述计算出的轮廓路径来实现此目的。您可以选择为描边添加阴影。

注意:画布描边总是在路径的内部和外部各画一半。这意味着贴纸描边会侵入元素内部。为了修复此问题:在绘制了贴纸描边后,您应该将元素重新绘制到上面。这将覆盖掉贴纸描边的侵入部分。

插入图片描述

重新合成包括贴纸效果的最终图像

通过将每个元素的画布层叠到主画布上来重新合成最终图像

插入图片描述

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

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

这是一个 marching squares 边缘检测算法(来自优秀的开源 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函数内应用贴纸描边。


1
优秀的演示和解释。 - Francesco Frapporti
1
Marching squares 仅检测外部边缘,这本身是不够的。让我们看看……你可以(1)隔离子图像,(2)用唯一颜色填充孔洞,(3)去除除了该唯一颜色之外的所有像素(只留下孔洞),(4)使用Marching squares获取孔洞的边缘,(5)从孔洞的边缘创建剪辑路径,(6)擦除孔洞,(7)用白色描边重新绘制剪裁路径。描边将仅出现在孔洞的边缘内,因为外部将被剪切掉。然后只需将描边的孔洞绘制回子图像中。TADAA!你有一个贴纸描边的孔洞了! - markE
太棒了!还有一个问题:你能否将代码发布到 gist.github.com 并附上 MIT 或 BSD 许可证?我想在我的项目中使用其中的一部分,但是我已经花费了很多时间合并许可证,如果只有一个文件就会感到很遗憾。谢谢 :) - m93a
由于时间不允许,我将把编写额外部分的工作留给您来完成。这样您就不必担心授权问题了。 - markE
1
你节省了我大量的时间 - 谢谢! - mgibas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接