【问题标题】:How to add stroke/outline to transparent PNG image in JavaScript canvas如何在 JavaScript 画布中向透明 PNG 图像添加描边/轮廓
【发布时间】:2014-07-25 05:32:43
【问题描述】:

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

我发现的最流行的imageeffect库没有描边效果。我在 StackOverflow 上找到的最接近的解决方案是 using blur to give it a glow effect,而不是轮廓描边。

原图

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

生成的图像

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

搜索继续...

我会在寻找最简单的方法来完成笔画效果时更新此列表。相关问题:

【问题讨论】:

  • 使用您找到的相同答案,但使用实心白线而不是发光线。
  • 给定算法(他们称其为“行军蚂蚁”,但它提醒 Moore neighborhood)在您拥有具有分离区域的图像时不起作用:jsfiddle.net/dfkFF
  • 您可以使用“marching squares”边缘检测算法来分离您的区域,然后勾勒出这些区域。请参阅我编辑的答案。祝你的项目好运!
  • 我想出了一个使用阴影更快的解决方法:stackoverflow.com/a/63958475/8451391

标签: javascript image-processing canvas


【解决方案1】:

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

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

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

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

离散元素由相互连接但不与其他元素连接的像素组组成。例如,spritesheet 上的每个单独的 sprite 都将是一个离散元素。

您可以使用“行进广场”等边缘检测算法找到离散像素组。

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

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

您可以再次使用“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>

这是一个行进方块边缘检测算法(来自优秀的开源 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) 隔离子图像,(2) 用唯一颜色填充孔,(3) 删除除唯一颜色之外的所有像素(仅留下孔),(4) 使用行进方块要获得孔的边缘,(5)从孔的边缘创建一个剪切路径,(6)擦除孔,(7)用白色笔划重新绘制剪切路径。笔划只会出现在孔的边缘内部,因为外部将被剪掉。然后将那个描边孔画回子图像。塔达!你有一个贴纸笔触的洞!
  • 酷!还有一个问题:您能否将代码发布到例如。 gist.github.com 具有某种类型的 MIT 或 BSD 许可证?我想在我的项目中使用它的一部分,而且我花了很多时间合并许可证,以至于我会因为用一个文件贬低我的努力而感到难过。谢谢:)
  • 由于时间不允许,我将留给你用孔编码额外的位。这样您就不必担心许可问题。
  • 您为我节省了大量时间 - 谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-04-23
  • 2011-03-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多