在2d图形可视化开发中,经常要绘制对象的选中效果。 一般来说,表达对象选中可以使用边框,轮廓或者发光的效果。  发光的效果,可以使用canvas的阴影功能,比较容易实现,此处不在赘述。

绘制边框

绘制边框是最容易实现的效果,比如下面的图片

要绘制边框,只需要使用strokeRect的方式即可。效果如下图所示:

这个代码也很简单,如下所示:

     ctx1.strokeStyle = "red";
ctx1.lineWidth = 2;
ctx1.drawImage(img, 1, 1,img.width ,img.height)
ctx1.strokeRect(1,1,img.width,img.height);

绘制轮廓

问题是,简单粗暴的加一个边框,并不能满足需求。很多时候,人们需要的是轮廓的效果,也就是图片的有像素和无像素的边缘处。如下图的效果所示:

要实现上述效果,最容易想到的思路就是通过像素的计算来判断边缘,并对边缘进行特定颜色的像素填充。但是像素的计算算法并不容易,简单的算法又很难达到预期的效果,而且由于逐像素操作,效率不高。

考虑到在三维webgl中,计算轮廓的算法思路是这样的:

  1. 先绘制三维模型自身,并在绘制的时候启动模板测试,把三维图像保存到模板缓冲中。
  2. 把模型适当放大,用纯属绘制模型,并在绘制的时候启用模板测试,和之前的模板缓冲区中的像素进行比较,如果对应的坐标处在之前模板缓冲区中有像素,就不绘制纯色。

依据上述的原理,就可以绘制处三维对象的轮廓了。下面是一个示例效果,(参考https://stemkoski.github.io/Three.js/Outline.html

在2d canvas里面有类似的原理可以实现轮廓效果,就是使用globalCompositeOperation了。 大体思路是这样的:

  1. 首先绘制放大一些的图片。
  2. 然后开启globalCompositeOperation = 'source-in', 并用纯色填充整个canvas区域,由于source-in的效果,纯色会填充放大图片有像素的区域。
  3. 使用默认的globalCompositeOperation(source-over),用原始尺寸绘制图片。

绘制放大一些的图片

通过drawImage的参数可以控制绘制图片的大小,如下所示,drawImage有几个形式:

1  void ctx.drawImage(image, dx, dy);
2 void ctx.drawImage(image, dx, dy, dWidth, dHeight);
3 void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);

其中dx,dy 代表绘制的起始位置,一般绘制的时候使用第一个方法,代表绘制的大小就是原本图片的大小。而使用第二个方法,我们可以指定绘制的尺寸,我们可以使用第二个方法绘制放大的图片,代码如所示:

ctx.drawImage(img, p - s, p  - s, w + 2 * s, h+ 2 * s);

其中p代表图片本身的绘制位置,s代表向左,向上的偏移量,同时图片的宽和高都增加 2 * s

用纯色填充放大图片的区域

在上一步绘制的基础上,开启globalCompositeOperation = 'source-in', 并用纯色填充整个canvas区域。 代码如下所示:

 // fill with color
ctx.globalCompositeOperation = "source-in";
ctx.fillStyle = "#FF0000";
ctx.fillRect(0, 0, cw, ch);

最终的效果如下图所示:

为什么会出现这种效果是因为使用了globalCompositeOperation = 'source-in',具体原理可以参考本人的其他文章。

绘制原始图片

最后一步就是绘制原始图片,代码如下所示:

  ctx.globalCompositeOperation = "source-over";
ctx.drawImage(img, p, p, w, h);

首先恢复globalCompositeOperation为默认值 "source-over",然后按照原本的大小绘制图片。

经过以上步骤,最终的效果如下图所示:

可以看出最终获得了我们要的效果。

只显示轮廓

如果我们只想得到图片的轮廓,则可以在最后绘制的时候,globalCompositeOperation 设置为“destination-out”,代码如下:

        ctx.globalCompositeOperation = "destination-out";
ctx.drawImage(img, p, p, w, h);

效果图如下:

轮廓粗细不一致的问题

上面的算法实现,是在图片的有像素值区域中心和图片本身的几何中心基本一直,如果图片的有像素值的中心和图片本身的几何中心相差比较大,则会出现轮廓粗细不一致的情况,比如下面这张图:

上半部分是透明的,下半部分是非透明的,像素的中心在3/4出,而几何中心在1/2处。使用上面的算法,该图片的轮廓如下:

可以发现上边缘的轮廓宽度变成了0。

在比如下图,

绘制后上边缘的轮廓比其他边缘的细。

怎么处理这种情况呢?可以在绘制放大图片的时候,不直接使用缩放,而是在上下左右,上左,上右,下左,下右几个方向进行偏移绘制,多次绘制,代码如下:

  var dArr = [-1, -1, 0, -1, 1, -1, -1, 0, 1, 0, -1, 1, 0, 1, 1, 1], // offset array
// draw images at offsets from the array scaled by s
for (var i = 0; i < dArr.length; i += 2) {
ctx.drawImage(img, p + dArr[i] * s, p + dArr[i + 1] * s, w, h);
}

再看上面图片的轮廓效果,如下所示:

半透明的情况

我在其他文章中说过,globalCompositeOperation为"source-in"的时候,source图形的透明度,会影响到目标绘制图形的透明度。所以会导致轮廓的像素值会乘以透明度。比如,我们在绘制放大图的时候,设置globalAlpha = 0.5进行模拟。

最后的绘制效果如下:

可以看到轮廓的颜色变浅了,解决办法就是多绘制几次放大图。比如:

ctx.globalAlpha = 0.5;
ctx.drawImage(img, p - s, p - s, w + 2 * s, h+ 2 * s);
ctx.drawImage(img, p - s, p - s, w + 2 * s, h+ 2 * s);

而上面通过偏移的方式绘制的时候,本身都绘制了好多遍,所以不存在这个问题。如下:

  ctx.globalAlpha = 0.5;
for (var i = 0; i < dArr.length; i += 2) {
ctx.drawImage(img, p + dArr[i] * s, p + dArr[i + 1] * s, w, h);
}

如下图所示:

当然,在透明度很低的情况下,使用绘制很多遍的方式,不是很好的解决方案。

使用算法(marching-squares-algorithm)

上面的方法对于有些图片效果就很不好,比如这张图片:

由于其有很多中空的效果,所以其最终效果如下图所示:

但是想要的只是外部的轮廓,而不需要中空部分也绘制上轮廓效果。此时需要使用其他的算法。 直接使用marching squares algorithm 可以获取图片的边缘。这一块的算法具体实现本文不再讲解,后续有机会单独一篇文章进行讲解。 此处直接使用开源的实现。比如可以使用  https://github.com/sakri/MarchingSquaresJS,代码如下:

 function drawOuttline2(){
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
var w = img.width;
var h = img.height;
canvas.width = w;
canvas.height = h;
ctx.drawImage(img, 0, 0, w, h);
var pathPoints = MarchingSquares.getBlobOutlinePoints(canvas);
var points = []; for(var i = 0;i < pathPoints.length;i += 2){
points.push({
x:pathPoints[i],
y:pathPoints[i + 1],
})
} // ctx.clearRect(0, 0, w, h);
ctx.beginPath();
ctx.lineWidth = 2;
ctx.strokeStyle = '#00CCFF';
ctx.moveTo(points[0].x, points[0].y);
for (var i = 1; i < points.length; i += 1) {
var point = points[i];
ctx.lineTo(point.x,point.y);
}
ctx.closePath();
ctx.stroke(); ctx1.drawImage(canvas,0,0);
}

首先使用调用MarchingSquaresJS的方法获取img图像的轮廓点的集合,然后把所有的点连接起来。形成轮廓图,最终效果如下:

不过可以看出,MarchingSquares 算法获得的轮廓效果锯齿相对较多的。有光这块算法的优化,本文不讲解。

总结

对于没有中空效果的图片,我们一般不采用MarchingSquares算法,而采用前面的一种方式来实现,效率高,而且效果相对更好。 而对于有中空,就会使用MarchingSquares算法,效果相对差,效率也相对低一些,实际应用中,可以通过缓存来降低性能的损耗。

本文的起源来资源一个2.5D项目,上一张项目图吧:

参考文档

https://www.emanueleferonato.com/2013/03/01/using-marching-squares-algorithm-to-trace-the-contour-of-an-image/

https://github.com/sakri/MarchingSquaresJS

https://github.com/OSUblake/msqr

http://users.polytech.unice.fr/~lingrand/MarchingCubes/algo.html#squar

如果对可视化感兴趣, 关注公号“ITMan彪叔” 可以及时收到更多有价值的文章。也可以加微信541002349进行交流。

canvas绘制图像轮廓效果的更多相关文章

  1. canvas 绘制图像

    结果: 代码: <!DOCTYPE html> <html> <head lang="en"> <meta charset="U ...

  2. html5新特性canvas绘制图像

    在前端页面开发过程中偶尔会有需要对数据进行相应的数学模型展示,或者地理位置的动态展示,可能就会想到用canvas,网上有很多已经集成好的,比如说类似echarts,确实功能非常强大,而且用到了canv ...

  3. 数字雨(Javascript使用canvas绘制Matrix,效果很赞哦)

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  4. HTML5 Canvas绘制的下雪效果

    在HTML页面的HEAD区域直接引入snow.js即可,如下:<script type="text/javascript" src="js/snow.js" ...

  5. 使用html5 canvas绘制图片

    注意:本文属于<html5 Canvas绘制图形入门详解>系列文章中的一部分.如果你是html5初学者,仅仅阅读本文,可能无法较深入的理解canvas,甚至无法顺畅地通读本文.请点击上述链 ...

  6. html5 canvas绘制圆形印章,以及与页面交互

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

  7. 使用canvas绘制渐变色矩形和使用按键控制人物移动

    使用canvas绘制渐变色矩形和使用按键控制人物移动 1.使用canvas绘制渐变色矩形 效果演示 相关代码: <!DOCTYPE html> <html lang="en ...

  8. 测试canvas绘制旋转文字的性能

    canvas 绘制各种动画效果时,我们经常会使用画布旋转,使绘制上去的元素有旋转的效果. 最近在项目中碰到了很严重的性能问题,经常排查发现是因为绘制批量文字时使用了画布旋转,且每行文字的旋转角度是不一 ...

  9. canvas绘制视频封面--摘抄

    一.需求:上传视频,同时截取视频某一帧作为视频的封面. 二.实现思路:利用canvas绘制图像的功能,绘制图像某一帧,这里绘制了第一帧,很简单就实现了. 三.代码: <!DOCTYPE html ...

随机推荐

  1. js 运算符的执行顺序

    js 运算符的执行顺序 js 运算符优先级 Operator Precedence 下表从最高(21)到最低(1)优先顺序排列 left-to-right 从左到右 / 先左后右 right-to-l ...

  2. 微信公众号 webfullstack

    微信公众号 webfullstack weixin refs https://mp.weixin.qq.com/cgi-bin/loginpage?t=wxm2-login&lang=zh_C ...

  3. Nestjs 上传文件到七牛云

    $ npm install qiniu import * as url from 'url'; import * as qiniu from 'qiniu'; @Post('upload') @Use ...

  4. uniapp 发起网络请求

    推荐下我写的uni-http 创建http-config.js import Vue from 'vue' const BASE_URL = 'http://xxx.com'; if (process ...

  5. 比起USDT,我为什么建议你选择USDN

    2018年1月16日,USDT(泰达币)进入了很多人的视野.因为在这一天,在全球价值排名前50的加密货币中,包括比特币.莱特币以及以太坊在内的大多数的数字虚拟加密货币都遭遇了价格大幅下跌,只有泰达币价 ...

  6. 利用 Java 操作 Jenkins API 实现对 Jenkins 的控制详解

    本文转载自利用 Java 操作 Jenkins API 实现对 Jenkins 的控制详解 导语 由于最近工作需要利用 Jenkins 远程 API 操作 Jenkins 来完成一些列操作,就抽空研究 ...

  7. 安装vue脚手架

    npm install -g @vue/cli 创建项目 vue create freemall

  8. JS输出为[object object]

    问题描述:在控制台打印时应输出对象,但是却输出[object object] 解决办法:先将数据转换为json格式,然后再转换为json对象 JSON.parse(JSON.stringify(use ...

  9. 必知必会之 Java

    必知必会之 Java 目录 不定期更新中-- 基础知识 数据计量单位 面向对象三大特性 基础数据类型 注释格式 访问修饰符 运算符 算数运算符 关系运算符 位运算符 逻辑运算符 赋值运算符 三目表达式 ...

  10. Redis缓存中的常见问题

    缓存穿透:是指查询一个Redis和数据库中都不存在的数据. 问题:查询一个Redis和数据库中都不存在的数据,大量请求去访问数据库,导致数据库宕机. 解决办法: 1.根据id查询,如果id是自增的,将 ...