使用JavaScript画布向透明PNG图像添加轮廓/笔触效果的最简单方法是什么?
我发现的最受欢迎的image effect个库没有中风效果。我发现StackOverflow上最接近的解决方案是using blur to give it a glow effect,而不是轮廓笔划。
原始图片
透明PNG图像,可以有多个分离的形状:
产生的图片
应用了轮廓描边和阴影的透明图像。
搜索继续......
当我搜索完成笔画效果的最简单方法时,我会更新此列表。相关问题:
答案 0 :(得分:19)
这是在图片上添加“贴纸效果”的一种方法......
演示:http://jsfiddle.net/m1erickson/Q2j3L/
首先将原始图片绘制到主画布。
将图像分解为“离散元素”。
离散元素由彼此连接但未连接到其他元素的像素组组成。例如,spritesheet上的每个sprite都是一个离散元素。
您可以使用边缘检测算法(如“行进方块”)找到离散像素组。
将每个离散元素放在自己的画布上以进行进一步处理。还要从主画布中删除该离散元素(因此不会再次处理)。
检测每个离散元素的轮廓路径。
您可以再次使用“行进方块”算法进行边缘检测。行进方块的结果是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
函数中应用标签笔划。