jjzjj

javascript - 如何在 JavaScript canvas 中为透明 PNG 图像添加描边/轮廓

coder 2024-07-17 原文

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

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

原图

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

结果图像

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

搜索继续...

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

最佳答案

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

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

首先将您的原始图像绘制到主 Canvas 上。

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

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

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

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

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

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

创建“贴纸效果”

您可以通过在每个元素周围放置一个带有描边的白色轮廓来创建贴纸效果。通过抚摸上面计算的轮廓路径来执行此操作。您可以选择向描边添加阴影。

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

重新构图包括贴纸效果在内的最终图像

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

这是带注释的示例代码:

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

这是一个行进方 block 边缘检测算法(来自优秀的开源 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 函数中应用贴纸笔画。

关于javascript - 如何在 JavaScript canvas 中为透明 PNG 图像添加描边/轮廓,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24039599/

有关javascript - 如何在 JavaScript canvas 中为透明 PNG 图像添加描边/轮廓的更多相关文章

  1. ruby - 如何在 Ruby 中顺序创建 PI - 2

    出于纯粹的兴趣,我很好奇如何按顺序创建PI,而不是在过程结果之后生成数字,而是让数字在过程本身生成时显示。如果是这种情况,那么数字可以自行产生,我可以对以前看到的数字实现垃圾收集,从而创建一个无限系列。结果只是在Pi系列之后每秒生成一个数字。这是我通过互联网筛选的结果:这是流行的计算机友好算法,类机器算法:defarccot(x,unity)xpow=unity/xn=1sign=1sum=0loopdoterm=xpow/nbreakifterm==0sum+=sign*(xpow/n)xpow/=x*xn+=2sign=-signendsumenddefcalc_pi(digits

  2. ruby - 我需要将 Bundler 本身添加到 Gemfile 中吗? - 2

    当我使用Bundler时,是否需要在我的Gemfile中将其列为依赖项?毕竟,我的代码中有些地方需要它。例如,当我进行Bundler设置时:require"bundler/setup" 最佳答案 没有。您可以尝试,但首先您必须用鞋带将自己抬离地面。 关于ruby-我需要将Bundler本身添加到Gemfile中吗?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/4758609/

  3. ruby - 如何在 buildr 项目中使用 Ruby 代码? - 2

    如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby​​

  4. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

    我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

  5. ruby - 将 Bootstrap Less 添加到 Sinatra - 2

    我有一个ModularSinatra应用程序,我正在尝试将Bootstrap添加到应用程序中。get'/bootstrap/application.css'doless:"bootstrap/bootstrap"end我在views/bootstrap中有所有less文件,包括bootstrap.less。我收到这个错误:Less::ParseErrorat/bootstrap/application.css'reset.less'wasn'tfound.Bootstrap.less的第一行是://CSSReset@import"reset.less";我尝试了所有不同的路径格式,但它

  6. ruby-on-rails - 如何在 ruby​​ 中使用两个参数异步运行 exe? - 2

    exe应该在我打开页面时运行。异步进程需要运行。有什么方法可以在ruby​​中使用两个参数异步运行exe吗?我已经尝试过ruby​​命令-system()、exec()但它正在等待过程完成。我需要用参数启动exe,无需等待进程完成是否有任何ruby​​gems会支持我的问题? 最佳答案 您可以使用Process.spawn和Process.wait2:pid=Process.spawn'your.exe','--option'#Later...pid,status=Process.wait2pid您的程序将作为解释器的子进程执行。除

  7. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  8. ruby - 续集在添加关联时访问many_to_many连接表 - 2

    我正在使用Sequel构建一个愿望list系统。我有一个wishlists和itemstable和一个items_wishlists连接表(该名称是续集选择的名称)。items_wishlists表还有一个用于facebookid的额外列(因此我可以存储opengraph操作),这是一个NOTNULL列。我还有Wishlist和Item具有续集many_to_many关联的模型已建立。Wishlist类也有:selectmany_to_many关联的选项设置为select:[:items.*,:items_wishlists__facebook_action_id].有没有一种方法可以

  9. ruby - 如何在 Ruby 中拆分参数字符串 Bash 样式? - 2

    我正在为一个项目制作一个简单的shell,我希望像在Bash中一样解析参数字符串。foobar"helloworld"fooz应该变成:["foo","bar","helloworld","fooz"]等等。到目前为止,我一直在使用CSV::parse_line,将列分隔符设置为""和.compact输出。问题是我现在必须选择是要支持单引号还是双引号。CSV不支持超过一个分隔符。Python有一个名为shlex的模块:>>>shlex.split("Test'helloworld'foo")['Test','helloworld','foo']>>>shlex.split('Test"

  10. ruby - 如何在 Lion 上安装 Xcode 4.6,需要用 RVM 升级 ruby - 2

    我实际上是在尝试使用RVM在我的OSX10.7.5上更新ruby,并在输入以下命令后:rvminstallruby我得到了以下回复:Searchingforbinaryrubies,thismighttakesometime.Checkingrequirementsforosx.Installingrequirementsforosx.Updatingsystem.......Errorrunning'requirements_osx_brew_update_systemruby-2.0.0-p247',pleaseread/Users/username/.rvm/log/138121

随机推荐