由于之前略忙,主要也还是因为自己太懒,拖了好久才把这篇博客写完。这篇博客是关于图像边缘羽化(柔化)的,也是 JavaScript 图像处理这一系列的第二篇。上一篇是关于Gamma校正的,有兴趣的朋友可以看看: HTML5/JavaScript 图像自动Gamma校正 — 打造图像处理类库第一步
按照习惯,我们还是先来看看,今天这篇文章最后所要达到的图像边缘羽化效果:
由于gif图画质不是太好,所以看起来效果有点欠缺。要想看到真实的效果,可以去文末下载源码运行,还可以将 Get Data 之后得到的Base64数据转存成图片,看看羽化之后图片发生了什么变化。
1. 什么是图片边缘羽化
羽化又可以称为柔化,大致的定义是使图片边缘达到朦胧的效果,羽化值越大,则朦胧范围越宽。我最早是在 PhotoShop 接触到羽化这个名词的,用过 PhotoShop 的朋友应该大都接触过羽化,让我们一起来看看PS中羽化到底是一种什么样的效果:
① 设定好羽化值;② 选择选框工具,并框选好图像羽化区域;③ 点击 Ctrl+Shift+i 反选后,点击 Delete 键,羽化也就完成了。
最后完成的图像效果如下:
2. 实现图片羽化功能的思考
接下来,我想先和大家分享一下之前考虑实现这个功能的一些思考过程:
① 一开始想到的方案是直接利用 CSS 来实现羽化效果,就是利用 CSS 中 box-shadow 实现边缘羽化效果,下图是实现的代码和效果图(利用在线调试平台JS Bin运行的代码):
② 第二种想到的方案是将要做羽化的图片以及透明度渐变的边框图片一起放置 <svg> 元素标签下,边框图片大小和要做羽化的图片大小保持一致,并覆盖在该图片之上。具体代码就不贴了,具体过程也就是将如下的左右两张图片,叠加到一起之后实现羽化效果(下方最后一张图片)。
③ 第三种方案是将通过调用 drawImage() 方法将图片绘制到 canvas 上,再通过一系列的处理,达到边缘羽化的效果,这个也是最终选定的方案,后面再详细的介绍一下。
仔细看前两个方案,你会发现其实本质上这两个方案是没有区别的,因为都只是在图片上覆盖一个边框,乍一看效果似乎和羽化差不多,但是实际上覆盖了边框之后,图片四周依然还是不透明的,所以还是没有真正实现羽化。
之前没有考虑清楚就开始实践,首先通过 CSS 实现了看起来和羽化差不过的效果后,我就开始想这个效果只是在 <div> 上的,如何才能得到图片数据,把这个效果保存下来,我就往 html to canvas 方向考虑了,先把DOM元素绘制到 canvas 上,绘制完成后再利用 toDataURL() 方法得到图片数据,整套流程完成之后,也得到了图片数据,发现得到数据只是 <img> 数据,<div> 的 box-shadow 效果并没有体现出来,这时我觉得应该是 CSS 的效果没办法绘制到 canvas 上,所以就放弃了这个方案。
其实这个时候我并没有意识到第①个方案就算实现了,也没有达到真正意义上的羽化,因为没有发现这个问题,所以就有了第二个方案。方案②完全就是朝着 CSS 效果没有成功绘制到 canvas 去的,我当时想着只要解决了这个问题,边缘羽化就大功告成了。然后朝着这个方向做了一系列调查,最终得到了图片数据,这个时候才发现这种方案实现的不是真正的羽化效果,图片四周是不透明的。方案②的具体实现步骤就是先将要做羽化的图片添加到 <svg> 中,在将边框图片添加到 <svg> ,两张图片大小保持一致;然后将 svg 绘制到 canvas 上,利用 toDataURL() 方法得到图片数据。这里稍微贴一下将 <svg> 转成 base64 数据的代码吧:
var svg_data = (new XMLSerializer()).serializeToString(document.getElementById("mySvg"));
var src = "data:image/svg+xml;base64," + btoa(svg_data);
var tempImg = new Image();
tempImg.onload = function(evt) {
var ctx = document.createElement('canvas').getContext('2d');
ctx.canvas.width = this.width;
ctx.canvas.height = this.height;
ctx.drawImage(tempImg, 0, 0);
var dataURL = ctx.canvas.toDataURL("image/png");
}
tempImg.src = src;
写这段也是想告诉大家,在实现某一个功能之前,应该把可行的方案考虑好定下来再去实施,不要急于实现,导致花费了一些不必要的时间,影响工作进度;当然如果你是在学习过程中,鼓励大家多去尝试,考虑各种各样的方式实现,像我为了完成图片边缘羽化这个效果,有很多自己的思考在里面,虽然前两个方案对于最后的功能没什么用,但是在实现自己的想法的过程中我做了很多调查研究,通过这两个方案也学习了不少新知识,对自己来说也是一个积累的过程。
3. 相关知识点
3.1 CanvasRenderingContext2D对象
CanvasRenderingContext2D 接口提供了一系列的函数用来在 <canvas> 上绘制的图形。我们今天所讲的图像处理操作都是基于这个接口对象所提供的方法来完成的,要获取这个接口的对象,需要通过调用 <canvas> 元素的 getContext() 方法:
var canvas = document.getElementById('id');
// var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
3.2 绘制矩形
① CanvasRenderingContext2D.clearRect()
可清除指定范围的内容的方法。以点 (x, y) 为起点,并根据设定的 width 和 height 所围成的矩形区域,在这个范围内的所有像素变成透明,并擦除之前绘制的所有内容。
② CanvasRenderingContext2D.fillRect()
这是一个绘制填充矩形的方法。矩形的起点在 (x, y) 位置,矩形的尺寸是 width 和 height ,可利用fillStyle 属性来设定矩形的样式,下一小节会详细介绍和这个属性相关的方法。
上面两个函数所传的4个参数如下表所示:
函数参数 | 描述 |
x | 矩形起点的 x 轴坐标 |
y | 矩形起点的 y 轴坐标 |
width | 矩形的宽度 |
height | 矩形的高度 |
3.3 渐变效果
① CanvasRenderingContext2D.fillStyle
fillStyle 属性设置或返回用于填充绘图的颜色,渐变或图案。要使用渐变来填充区域,只需要把②或③方法返回的CanvasGradient对象赋给fillStyle属性即可。
② CanvasRenderingContext2D.createLinearGradient()
根据传入的坐标参数创建出线性渐变,这个方法会返回一个线性 CanvasGradient 对象。
函数参数 | 描述 |
x0 | 渐变起始点的 x 轴坐标 |
y0 | 渐变起始点的 y 轴坐标 |
x1 | 渐变终点的 x 轴坐标 |
y1 | 渐变终点的 y 轴坐标 |
③ CanvasRenderingContext2D.createRadialGradient()
根据传入的参数确定两个圆的坐标,绘制放射性渐变的方法。这个方法同样会返回 CanvasGradient 对象。
函数参数 | 描述 |
x0 | 开始圆的 x 轴坐标 |
y0 | 开始圆的 y 轴坐标 |
r0 | 开始圆的半径 |
x1 | 结束圆的 x 轴坐标 |
y1 | 结束圆的 y 轴坐标 |
r1 | 结束圆的半径 |
④ CanvasGradient.addColorStop()
addColorStop() 方法用于设定渐变中的某一点的颜色变化。
函数参数 | 描述 |
offset | 这是一个范围在 0 到 1 之间的浮点值,表示渐变的开始点和结束点之间的一部分。offset 为 0 对应开始点,offset 为 1 对应结束点。若不在0到1之间,将抛出INDEX_SIZE_ERR错误 |
color | 以一个 CSS 颜色字符串的方式,表示在指定 offset 显示的颜色。沿着渐变某一点的颜色是根据这个值以及任何其他的颜色色标来插值的。如果颜色值不能被解析为有效的CSS颜色值,将抛出SYNTAX_ERR错误 |
(1)线性渐变
var canvas = document.getElementById("id");
var ctx = canvas.getContext("2d");
var gradient = ctx.createLinearGradient(0,0,200,0);
gradient.addColorStop(0, "green");
gradient.addColorStop(1, "yellow");
ctx.fillStyle = gradient;
ctx.fillRect(10,10,200,100);
(2)放射性渐变
var canvas = document.getElementById("id");
var ctx = canvas.getContext("2d");
var gradient = ctx.createRadialGradient(100,100,100,100,100,0);
gradient.addColorStop(0,"white");
gradient.addColorStop(1,"green");
ctx.fillStyle = gradient;
ctx.fillRect(0,0,200,200);
3.4 CanvasRenderingContext2D.globalCompositeOperation
设置globalCompositeOperation属性值,可以改变源(新的)图像绘制到目标(已有)的图像上的方式,下面以表格的形式,直观的了解一下各个属性值的含义及效果:
假设目标(已有)的图像【左】、源(新的)图像【右】,如下所示(这里使用了MDN的图像来演示各属性效果):
属性值 | 描述 | 效果 |
source-over | 这是默认值,新图形会覆盖在原有内容之上。 | |
source-in | 新图形会仅仅出现与原有内容重叠的部分。其它区域都变成透明的。 | |
source-out | 只有新图形中与原有内容不重叠的部分会被绘制出来。 | |
source-atop | 新图形中与原有内容重叠的部分会被绘制,并覆盖于原有内容之上。 | |
destination-over | 会在原有内容之下绘制新图形。 | |
destination-in | 原有内容中与新图形重叠的部分会被保留,其它区域都变成透明的。 | |
destination-out | 原有内容中与新图形不重叠的部分会被保留。 | |
destination-atop | 原有内容中与新内容重叠的部分会被保留,并会在原有内容之下绘制新图形。 |
4. 具体实现
下面来看看怎么利用之前的api实现羽化功能。
4.1 HTML代码
<!DOCTYPE html>
<html>
<head>
<title>Feather image(Soft image edge) by canvas</title>
<link type="text/css" rel="stylesheet" href="css/soft_edge.css">
<script type="text/javascript" src="lib/jquery.js" οnlοad="window.$ = window.jQuery"></script>
<script type="text/javascript" src="js/soft_edge.js"></script>
</head>
<body>
<div class="btn_area">
<button id="feather" class="btn_feather" οnclick="featherImage()">Feather</button>
<button id="get_data" class="btn_feather" οnclick="getImgData()">Get Data</button>
</div>
<img id="original_image" class="img_feather" src="original_image.png">
<img id="new_image" class="img_feather">
</body>
</html>
4.2 CSS代码
body {
text-align: center;
}
.img_feather {
max-width: 500px;
max-height: 500px;
margin-top: 30px;
float: left;
}
.btn_feather {
width: 100px;
height: 60px;
}
4.3 JavaScript代码
var image_id = "original_image";
var imageWidth, imageWidth;
function featherImage() {
var image = document.getElementById(image_id);
//imageWidth = image.width;
//imageHeight = image.height;
imageWidth = image.naturalWidth;
imageHeight = image.naturalHeight;
init($("#" + image_id)[0], function() {
console.log("feather successful");
});
}
function getImgData() {
var canvas = document.getElementById(image_id);
var dataURL = canvas.toDataURL('image/png');
$('#new_image').attr("src", dataURL);
}
function init(image, callback) {
var image_parentNode = image.parentNode;
var canvas = document.createElement('canvas');
if (canvas.getContext("2d")) {
canvas.id = image.id;
canvas.source = image.src;
canvas.className = image.className;
canvas.height = imageHeight;
canvas.width = imageWidth;
image_parentNode.replaceChild(canvas, image);
featherOperation(canvas, callback);
}
}
function featherOperation(canvas, callback) {
var ih = canvas.height;
var iw = canvas.width;
// var value = iw * ih * 0.157 / 2 / (iw + ih);
var value =(iw + ih) * 0.04 / 2;
var isize = parseInt(value);
if (canvas.tagName.toUpperCase() == "CANVAS" && canvas.getContext("2d")) {
var context = canvas.getContext("2d");
var img = new Image();
img.onload = function() {
context.clearRect(0, 0, iw, ih);
context.globalCompositeOperation = "source-over";
context.drawImage(img, 0, 0, iw, ih);
context.save();
context.globalCompositeOperation = "destination-out";
addFrameMask(context, 0, 0, iw, ih, isize, 255);
context.restore();
if (typeof callback === 'function') {
callback();
}
};
img.src = canvas.source;
}
}
function addFrameMask(ctx, x, y, w, h, size, color) {
var style;
style = setLinearStyle(ctx, x + size, y + size, x + size, y, color);
ctx.fillStyle = style;
ctx.fillRect(x + size, y, w - (size * 2), size);
style = setRadialStyle(ctx, x + size, y + size, 0, x + size, y + size, size, color);
ctx.fillStyle = style;
ctx.fillRect(x, y, size, size);
style = setLinearStyle(ctx, x + size, y + size, x, y + size, color);
ctx.fillStyle = style;
ctx.fillRect(x, y + size, size, h - (size * 2));
style = setRadialStyle(ctx, x + size, y + h - size, 0, x + size, y + h - size, size, color);
ctx.fillStyle = style;
ctx.fillRect(x, y + h - size, size, size);
style = setLinearStyle(ctx, x + size, y + h - size, x + size, y + h, color);
ctx.fillStyle = style;
ctx.fillRect(x + size, y + h - size, w - (size * 2), size);
style = setRadialStyle(ctx, x + w - size, y + h - size, 0, x + w - size, y + h - size, size, color);
ctx.fillStyle = style;
ctx.fillRect(x + w - size, y + h - size, size, size);
style = setLinearStyle(ctx, x + w - size, y + size, x + w, y + size, color);
ctx.fillStyle = style;
ctx.fillRect(x + w - size, y + size, size, h - (size * 2));
style = setRadialStyle(ctx, x + w - size, y + size, 0, x + w - size, y + size, size, color);
ctx.fillStyle = style;
ctx.fillRect(x + w - size, y, size, size);
}
function setRadialStyle(ctx, x1, y1, r1, x2, y2, r2, color) {
var tmp = ctx.createRadialGradient(x1, y1, r1, x2, y2, r2);
tmp.addColorStop(0, 'rgba(' + color + ',' + color + ',' + color + ', 0)');
tmp.addColorStop(1, 'rgba(' + color + ',' + color + ',' + color + ', 1)');
return tmp;
}
function setLinearStyle(ctx, x1, y1, x2, y2, color) {
var tmp = ctx.createLinearGradient(x1, y1, x2, y2);
tmp.addColorStop(0, 'rgba(' + color + ',' + color + ',' + color + ', 0)');
tmp.addColorStop(1, 'rgba(' + color + ',' + color + ',' + color + ', 1)');
return tmp;
}
注意:Chrome浏览器运行这个程序,或者说操作canvas时会报错,因为Chrome的安全策略问题导致,解决方案可以参考我的上一篇博客:HTML5/JavaScript 图像自动Gamma校正 — 打造图像处理类库第一步 文末的内容。
下面简单说一下js代码,实现羽化效果,主要是通过 addFrameMask()方法在图像的上、下、左、右四个方向添加线性渐变,在左上、右上、左下及右下四角添加放射性渐变,之前把 globalCompositeOperation属性值为 destination-out ,这步是实现羽化的关键。还有就是羽化的size是我自己定的比例,大家可以自行修改代码达到想要的效果。其他内容就不再赘述了,大家有问题或有更好的想法,都欢迎大家随时和我交流,如果文中有不足或错误的地方,也希望大家能指出来。代码链接就贴在文末了。
程序源代码: