如何在 JavaScript 画布中向透明 PNG 图像添加笔触/轮廓 [英] How to add stroke/outline to transparent PNG image in JavaScript canvas

查看:26
本文介绍了如何在 JavaScript 画布中向透明 PNG 图像添加笔触/轮廓的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

最受欢迎的

  • 如何制作photoshop描边效果?
  • 如何制作画布轮廓用于悬停发光的透明 png
  • 解决方案

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

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

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

    将图像分解为离散元素".

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

    您可以使用行进方块"等边缘检测算法找到离散像素组.

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

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

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

    创建贴纸效果"

    您可以通过在每个元素周围放置一个白色的描边轮廓来创建贴纸效果.通过抚摸您在上面计算的轮廓路径来做到这一点.您可以选择为笔画添加阴影.

    注意:画布笔触总是画在一半的内侧 &一半在路径之外.这意味着贴纸笔画将侵入元素内部.要解决此问题:在绘制贴纸笔划后,您应该将元素重新绘制回顶部.这会覆盖贴纸笔划的侵入部分.

    重新构图包括贴纸效果

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

    这里是带注释的示例代码:

    <头><link rel="stylesheet" type="text/css" media="all" href="css/reset.css"/><!-- 重置 css --><script src="http://code.jquery.com/jquery.min.js"></script><script src="marching squares.js"></script><风格>身体{背景颜色:银色;}画布{边框:1px纯红色;}</风格><脚本>$(函数(){//画布相关变量var canvas=document.getElementById("canvas");var ctx=canvas.getContext("2d");//像素操作中使用的变量var canvases=[];var imageData,data,imageData1,data1;//贴纸轮廓的大小varstrokeWeight=8;//边缘检测方法使用的真/假函数vardefineNonTransparent=function(x,y){返回(数据1[(y*cw+x)*4+3]>0);}//接收贴纸效果的图片var img=new Image();img.crossOrigin="匿名";img.onload=开始;img.src="https://dl.dropboxusercontent.com/u/139992952/multple/makeIndividual.png";//img.src="https://dl.dropboxusercontent.com/u/139992952/stackoverflow/angryBirds.png";函数开始(){//将主画布调整为图像大小canvas.width=cw=img.width;canvas.height=ch=img.height;//在主画布上绘制图像ctx.drawImage(img,0,0);//将每个离散元素从主画布移动到单独的画布//贴纸效果单独应用于每个离散元素和//在单独的画布上为每个离散元素完成while(moveDiscreteElementToNewCanvas()){}//将贴纸效果添加到所有离散元素(每个画布)for(var i=0;i0){hit=true;break;}}如果(!命中){返回;}//获取勾勒出离散元素的点路径var points=geom.contour(defineNonTransparent);//创建一个新画布并将其附加到页面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');//将轮廓点附加到新画布(稍后需要)newCanvas.outlinePoints=点;//只将该元素绘制到新画布上定义GeomPath(newCtx,points);newCtx.save();newCtx.clip();newCtx.drawImage(canvas,0,0);newCtx.restore();//从主画布中删除元素定义几何路径(ctx,点);ctx.save();ctx.clip();ctx.globalCompositeOperation="destination-out";ctx.clearRect(0,0,canvas.width,canvas.height);ctx.restore();返回(真);}//实用功能//在画布上定义一条路径,但不抚摸或填充该路径函数defineGeomPath(上下文,点){context.beginPath();context.moveTo(points[0][0],points[0][1]);for(var i=1;i<身体><canvas id="canvas" width=300 height=300></canvas><br>

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

    /*** 使用 <a 计算给定输入网格函数的轮廓* href="http://en.wikipedia.org/wiki/Marching_squares">行军* 平方</a>算法.以点数组的形式返回轮廓多边形.** @param grid 一个双输入函数(x, y) 返回真值* 在轮廓内,对于轮廓外的值,则为 false.* @param 在网格上启动一个可选的起点 [x, y].* @returns 多边形 [[x1, y1], [x2, y2], ...]*/(功能(){几何 = {};geom.contour = 函数(网格,开始){var s = 开始 ||d3_geom_contourStart(grid),//起点c = [],//轮廓多边形x = s[0],//当前 x 位置y = s[1],//当前 y 位置dx = 0,//下一个 x 方向dy = 0,//下一个 y 方向pdx = NaN,//前一个 x 方向pdy = NaN,//前一个 y 方向我 = 0;做 {//确定行进方块索引我 = 0;如果(网格(x-1,y-1))我+ = 1;如果(网格(x,y-1))我+ = 2;if (grid(x-1, y )) i += 4;如果(网格(x,y))我+ = 8;//确定下一个方向如果(我 === 6){dx = pdy === -1 ?-1:1;dy = 0;} else if (i === 9) {dx = 0;dy = pdx === 1 ?-1:1;} 别的 {dx = d3_geom_contourDx[i];dy = d3_geom_contourDy[i];}//更新轮廓多边形如果 (dx != pdx && dy != pdy) {c.push([x, y]);pdx = dx;pdy = dy;}x += dx;y += dy;} while (s[0] != x || s[1] != y);返回 c;};//行进方向查找表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];功能 d3_geom_contourStart(网格){变量 x = 0,y = 0;//寻找起点;从原点开始//并沿着向外扩展的对角线前进而(真){如果(网格(x,y)){返回 [x,y];}如果(x === 0){x = y + 1;y = 0;} 别的 {x = x - 1;y = y + 1;}}}})();

    注意:此代码将应用贴纸轮廓的过程分离到一个单独的函数中.如果您希望在离散元素周围有多个图层,则可以这样做.例如,您可能希望在贴纸笔画的外侧有第二个灰色边框.如果您不需要应用图层",那么您可以在 moveDiscreteElementToNewCanvas 函数中应用贴纸笔画.

    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.

    这篇关于如何在 JavaScript 画布中向透明 PNG 图像添加笔触/轮廓的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

    查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆