用Canvas打造高强度渲染SVG

前言

         早就想写这篇博客。一个原因是其中某些内容在某度的面试中遇到,比如第二部分;二是一些需求在实际工程中遇到了。

一、基本概念

         Canvas:HTML5新增的DOM,提供了像素级图形编程接口。支持path,不支持鼠标响应,不能绘制矢量图,依赖分辨率,文本渲染能力弱,刷新能力强。适用于图像密集型编程,如游戏。

         SVG:一种使用XML描述2D图像的语言。因此SVG中所有元素都是DOM。支持path,支持鼠标事件,能绘制矢量图,不依赖分辨率,文本渲染能力强,刷新能力弱。适用于大区域渲染程序,比如地图。

二、性能对比及应用场景

         详情见这篇文章:http://msdn.microsoft.com/zh-cn/library/gg193983,这里给出一些摘要。


         选择SVG:

         l  用于查看和打印的高保真文档,如建筑图、工程图、电子图、航空图、地理图、组织图、生物图等。

         l  静态图像,比如带有某些特效的图片,用SVG会节省图片的存储空间。

         选择Canvas:

         l  高性能图形,如光线跟踪器、3D引擎渲染器。

         l  复杂场景,实时动画等。

         l  图像处理,涉及到像素操作。

三、应用中的问题

        既然Canvas和SVG各有特点,我们只需要在特定场景选择特定技术就行了。但其中有一些问题。比如我想做一个CAD online,就是在Web上做一个工程制图工具,能编辑CAD图纸,用哪种技术好?首先想到的肯定是SVG,因为编辑器肯定要在工作区选择某一条线、某一个元素,这需要鼠标事件。并且,SVG适合高保真图纸的显示,又能保存成矢量图。

        但实际做起来,就不太容易了。因为SVG是DOM,如果一张图纸里有几万条线,恐怕打开图纸,成功渲染出来都要几分钟,更别说编辑、来回拖动图纸了。可用Canvas绘制图纸,先不说怎样高保真绘制,这些可以通过引入图形算法解决,单响应鼠标事件这一项就不好实现,似乎不可能从像素中识别出一条完整的线。

真的不行么?接下来我们就来解决这个问题,并做一个简单的DEMO,让Canvas看起来像极了SVG,能响应鼠标事件。

四、Canvas的鼠标接口

        Canvas其实提供了一个鼠标接口,仔细翻API文档,你就会发现isPointInPath()方法,这是我们唯一可以利用的。该方法的作用是判断Canvas中的某个点,在不在某个路径中。输入参数两个,x值和y值,笛卡尔坐标,返回值boolean:

[javascript] view plain copy
  1. <span style="font-size:14px;">    var ctx =document.getElementById(‘container’).getContext(‘2d’);  
  2.     var isin = ctx.isPointPath(0, 0);</span>  

        需要强调的是,前面提到“在不在某个路径中”,指的是哪个路径?Canvas在绘制时,都是以路径为基本单位。路径以ctx.beginPath();开始,那么以什么结束呢?很多人说closePath()结束,这是大错特错的说法。closePath() 的意思是闭合路径,而不是结束路径,如果是endPath 就对了。其实,路径,在绘制时宣布结束,即ctx.strock()或ctx.fill()。只要碰到这两个方法的任何一个,都算一个路径绘制结束。接下来剩下的问题就是,什么时候使用isPointInPath(),就是在路径的开始后,绘制前。在这个阶段可以做很多事:设置线条样式、设置填充样式、在路径中绘画、判断某个点是否在路径中。

        比如下面这个例子,我们画两条折线,这两条折线在不同路径上,并判断某个点是否在某个路径上,如果是,讲那个路径绘制成红色,否则绘制成黑色。代码版本1:

  1. <span style="font-size:14px;"><!DOCTYPE html>  
  2. <html>  
  3. <head>  
  4. <meta charset="UTF-8"/>  
  5. <title>canvas2SVG</title>  
  6. <style>  
  7. #container {  
  8.    border: 1px solid #000;  
  9.    position: 0px;  
  10.    left: 0px;  
  11.    top: 0px;  
  12. }  
  13. </style>  
  14. </head>  
  15. <bodyonloadbodyonload="init()">  
  16. <canvas width="800px"height="400" id="container"></canvas>  
  17. <scripttypescripttype="text/javascript">  
  18. function init() {  
  19.    var ctx = document.getElementById('container').getContext('2d');  
  20.    var point = [300, 100];  
  21.    //路径1  
  22.    ctx.beginPath();  
  23.    ctx.moveTo(0, 0);  
  24.    ctx.lineTo(0, 100);  
  25.    ctx.lineTo(100, 100);  
  26.    ctx.lineTo(100, 0);  
  27.    ctx.lineTo(0, 0);  
  28.    if (ctx.isPointInPath(point[0], point[1])) {  
  29.         ctx.fillStyle = '#f00';  
  30.    } else {  
  31.         ctx.fillStyle = '#000';  
  32.    }  
  33.    ctx.fill();  
  34.    //路径2  
  35.    ctx.beginPath();  
  36.    ctx.moveTo(400, 0);  
  37.    ctx.lineTo(400, 200);  
  38.    ctx.lineTo(200, 200);  
  39.    ctx.lineTo(400, 0);  
  40.    if (ctx.isPointInPath(point[0], point[1])) {  
  41.         ctx.fillStyle = '#f00';  
  42.    } else {  
  43.         ctx.fillStyle = '#000';  
  44.    }  
  45.    ctx.fill();}  
  46. </script>  
  47. </body>  
  48. </html></span>  


五、鼠标响应

       有了这个神器,我们就可以做鼠标响应了。每绘制一个路径,只要判断鼠标是不是在它上面,从而获取鼠标下的路径。那么,绘制结束了怎么办?我们把绘制方法挂到mousemove事件上,只要移动就重绘。Canvas绘制效率很高,在1080p分辨率下,根本不用考虑绘制效率问题。代码版本2如下:

[javascript] view plain copy
  1. <span style="font-size:14px;">functioninit() {  
  2.     var width = 800;  
  3.     var height = 400;  
  4.     var container =document.getElementById('container');  
  5.     var ctx = container.getContext('2d');  
  6.     var func = [line1, line2];  
  7.     container.width = width;  
  8.     container.height = height;  
  9.     document.getElementById('container').addEventListener('mousemove',render);  
  10.     render({clientX: -1, clientY: -1});  
  11.    
  12.     function render(event) {  
  13.         ctx.clearRect(0, 0, width, height);  
  14.         for (var key in func) {  
  15.             ctx.beginPath();  
  16.             ctx.fillStyle = func[key](event.clientX,event.clientY);  
  17.             ctx.fill();  
  18.         }  
  19.     }  
  20.    
  21.     function line1(x, y) {  
  22.         ctx.moveTo(0, 0);  
  23.         ctx.lineTo(0, 100);  
  24.         ctx.lineTo(100, 100);  
  25.         ctx.lineTo(100, 0);  
  26.         ctx.lineTo(0, 0);  
  27.         return ctx.isPointInPath(x, y) ? '#f00''#000';  
  28.     }  
  29.    
  30.     function line2(x, y) {  
  31.         ctx.moveTo(400, 0);  
  32.         ctx.lineTo(400, 200);  
  33.         ctx.lineTo(200, 200);  
  34.         ctx.lineTo(400, 0);  
  35.         return ctx.isPointInPath(x, y) ? '#f00''#000';  
  36.     }  
  37. }</span>  

这段代码的效果是鼠标移动到哪个物体上,哪个物体变成红色,其他物体是黑色。

        注意,在Canvas里面,路径都是实心的,哪怕不填充颜色,也是实心的。比如在上面代码中把的fillStyle改成strokeStyle,把fill()改成strock(),当鼠标移动到物体内部时,虽然鼠标不在边线上,物体也会变颜色。更确切的说法,Canvas绘制path时,如果path的最后一个点和第一个点不重合,路径不闭合,Canvas会自动闭合上。所以closePath()这个方法基本没用。


六、物体重叠

        上面的例子物体不重叠,那么重叠了会是什么效果?在看下面的代码版本3:

[javascript] view plain copy
  1. <span style="font-size:14px;">function init() {  
  2.    var width = 800;  
  3.    var height = 400;  
  4.    var container = document.getElementById('container');  
  5.    var ctx = container.getContext('2d');  
  6.    var geometry = [  
  7.         [  
  8.             [[10, 10], [10, 110], [110, 110],[10, 10]],  
  9.             [[110, 10], [110, 110], [210, 110],[110, 10]],  
  10.             [[210, 10], [210, 110], [310, 110],[210, 10]]  
  11.         ],[  
  12.             [[0, 50], [0, 150], [100, 150], [0,50]],  
  13.             [[100, 50], [100, 150], [200, 150],[100, 50]],  
  14.             [[200, 50], [200, 150], [300, 150],[200, 50]]  
  15.         ],[  
  16.             [[0, 100], [0, 200], [100, 200],[0, 100]],  
  17.             [[100, 100], [100, 200], [200,200], [100, 100]],  
  18.             [[200, 100], [200, 200], [300, 200],[200,100]]  
  19.         ]  
  20.    ];  
  21.    container.width = width;  
  22.    container.height = height;  
  23.    document.getElementById('container').addEventListener('mousemove',render);  
  24.    render({clientX: -1, clientY: -1});  
  25.    
  26.    function render(event) {  
  27.         ctx.clearRect(0, 0, width, height);  
  28.         for (var n = 0; n < geometry.length;n++) {  
  29.             ctx.beginPath();  
  30.             ctx.strokeStyle =draw(event.clientX, event.clientY, geometry[n]);  
  31.             ctx.stroke();  
  32.        }  
  33.    }  
  34.    
  35.    function draw(x, y, geo) {  
  36.       for (var i = 0; i < geo.length; i++) {  
  37.           var face = geo[i];  
  38.           for (var p = 0; p < face.length;p++) {  
  39.               if (p == 0) {  
  40.                   ctx.moveTo(face[p][0],face[p][1]);  
  41.               } else {  
  42.                   ctx.lineTo(face[p][0],face[p][1]);  
  43.               }  
  44.           }  
  45.       }  
  46.       return ctx.isPointInPath(x, y) ? '#f00' :'#000';  
  47.    }  
  48. }</span>  

版本3中,我们绘制了三个路径,每个路径由三个三角形组成。除了绘图方法,其他代码同版本2相同,于是出现了这种情况:


当鼠标移动到黑色箭头所指的区域,三个物体全部变红,说明这个位置,处在三个物体中。从代码里就能看出来,这个问题是我们实现的逻辑造成的。

        如果想单独一个物体,比如最后绘制,只要增加一个数组就行了。用这个数组保存所有处在鼠标下的物体,然后逆序,数组第一个元素就是我们想得到的物体,最后再重绘所有物体就成了。代码版本4:

[javascript] view plain copy
  1. <span style="font-size:14px;">    function render(event) {  
  2.         ctx.clearRect(0, 0, width, height);  
  3.         var hover = [];  
  4.         for (var n = 0; n < geometry.length;n++) {  
  5.             ctx.beginPath();  
  6.             ctx.strokeStyle =draw(event.clientX, event.clientY, geometry[n]);  
  7.             if (ctx.strokeStyle == '#ff0000') {  
  8.                 hover.push(n);  
  9.             }  
  10.             ctx.stroke();  
  11.         }  
  12.         hover.reverse();  
  13.         if (hover.length > 0) {  
  14.             for (var n = 0; n <geometry.length; n++) {  
  15.                 ctx.beginPath();  
  16.                 draw(event.clientX,event.clientY, geometry[n]);  
  17.                 ctx.strokeStyle = n == hover[0]? '#f00' : '#000'  
  18.                 ctx.stroke();  
  19.             }  
  20.         }  
  21.     }</span>  

这里我们重写了render方法,效果如下。


是不是看着很眼熟?没错,这就是浏览器冒泡机制。我们一步一步做到这里,原理一点儿都不复杂。

七、物体重叠的解决

        做到这里,虽然看到了曙光,但我们的DEMO还是没有实用价值。因为物体还是实心的,我们真正想要的,是空心物体。如果我们把lineTo方法自己实现,用一个path模拟一条线,这个问题也就迎刃而解了。

        lineTo涉及到起点、终点,我们可以用四边形把这两个点圈起来,如果想要好看一些,四边形可以做出圆角、阴影、虚线等等效果。这些都是计算机图形学里的知识,我们只给出下面图中的四边形定点如何计算。


        当前画笔位置(x0,y0),原lineTo的参数(x1, y1),则A、B、E、F 坐标计算过程如下(理论证明略,初中几何):

[javascript] view plain copy
  1. <span style="font-size:14px;">  d = Math.sqrt((x0 - x1) * (x0 - x1) + (y0 -y1) * (y0 - y1));  
  2.   r = 1;//圆的半径  
  3.   sina = (x1 - x0) / d;  
  4.   cosa = (y1 - y0) / d;  
  5.   ax = x0 + r * cosa;  
  6.   ay = y0 - r * sina;  
  7.   bx = x0 + r * cosa + x1 - x0;  
  8.   by = y0 - r * sina + y1 - y0;  
  9.   ex = x0 - r * cosa + x1 - x0;  
  10.   ey = y0 + r * sina + y1 - y0;  
  11.   fx = x0 - r * cosa;  
  12.   fy = y0 + r * sina;  
  13.   dx = x0 + r * cosa;  
  14.   dy = y0 - r * sina;</span>  

接下来就是重写我们的绘图方法,代码版本5:

  1. <span style="font-size:14px;"><!DOCTYPEhtml>  
  2. <html>  
  3. <head>  
  4. <metacharsetmetacharset="UTF-8" />  
  5. <title>canvas2SVG</title>  
  6. <style>  
  7. #container{  
  8.     border: 1px solid #000;  
  9.     position: absolute;  
  10.     left: 0px;  
  11.     top: 0px;  
  12. }  
  13. </style>  
  14. </head>  
  15. <bodyonloadbodyonload="init()">  
  16. <canvasidcanvasid="container"></canvas>  
  17. <scripttypescripttype="text/javascript">  
  18. functioninit() {  
  19.    
  20.     var width = 800;  
  21.     var height = 400;  
  22.     var container =document.getElementById('container');  
  23.     var ctx = container.getContext('2d');  
  24.     var geometry = [  
  25.         [  
  26.             [[10, 10], [10, 110], [110, 110],[10, 10]],  
  27.             [[110, 10], [110, 110], [210, 110],[110, 10]],  
  28.             [[210, 10], [210, 110], [310, 110],[210, 10]]  
  29.         ],[  
  30.             [[0, 50], [0, 150], [100, 150], [0,50]],  
  31.             [[100, 50], [100, 150], [200, 150],[100, 50]],  
  32.             [[200, 50], [200, 150], [300, 150],[200, 50]]  
  33.         ],[  
  34.             [[0, 100], [0, 200], [100, 200],[0, 100]],  
  35.             [[100, 100], [100, 200], [200,200], [100, 100]],  
  36.             [[200, 100], [200, 200], [300,200], [200,100]]  
  37.         ]  
  38.     ];  
  39.    
  40.    
  41.     container.width = width;  
  42.     container.height = height;  
  43.    document.getElementById('container').addEventListener('mousemove',render);  
  44.     render({clientX: -1, clientY: -1});  
  45.    
  46.    
  47.     function render(event) {  
  48.         ctx.clearRect(0, 0, width, height);  
  49.         var hover = [];  
  50.         for (var n = 0; n < geometry.length;n++) {  
  51.             ctx.beginPath();  
  52.             ctx.strokeStyle = draw(event.clientX,event.clientY, geometry[n]);  
  53.             if (ctx.strokeStyle == '#ff0000') {  
  54.                 hover.push(n);  
  55.             }  
  56.             ctx.stroke();  
  57.         }  
  58.         hover.reverse();  
  59.         if (hover.length > 0) {  
  60.             for (var n = 0; n <geometry.length; n++) {  
  61.                 ctx.beginPath();  
  62.                 draw(event.clientX,event.clientY, geometry[n]);  
  63.                 ctx.strokeStyle = n == hover[0]? '#f00' : '#000'  
  64.                 ctx.stroke();  
  65.             }  
  66.         }  
  67.     }  
  68.    
  69.     function draw(x, y, geo) {  
  70.       for (var i = 0; i < geo.length; i++) {  
  71.           var face = geo[i];  
  72.           var oldx = null;  
  73.           var oldy = null;  
  74.           for (var p = 0; p < face.length;p++) {  
  75.               if (p == 0) {  
  76.                   oldx = face[p][0];  
  77.                   oldy = face[p][1];  
  78.               } else {  
  79.                   line(oldx, oldy, face[p][0],face[p][1]);  
  80.                   oldx = face[p][0];  
  81.                   oldy = face[p][1];  
  82.               }  
  83.           }  
  84.       }  
  85.       return ctx.isPointInPath(x, y) ? '#f00' :'#000';  
  86.     }  
  87.    
  88.     function line(x0, y0, x1, y1) {  
  89.         var d = Math.sqrt((x0 - x1) * (x0 - x1)+ (y0 - y1) * (y0 - y1));  
  90.         var r = 0.5;  
  91.         var sina = (x1 - x0) / d;  
  92.         var cosa = (y1 - y0) / d;  
  93.         ctx.moveTo(x0 + r * cosa, y0 - r *sina);  
  94.         ctx.lineTo(x0 + r * cosa + x1 - x0, y0- r * sina + y1 - y0);  
  95.         ctx.lineTo(x0 - r * cosa + x1 - x0, y0+ r * sina + y1 - y0);  
  96.         ctx.lineTo(x0 - r * cosa, y0 + r * sina);  
  97.         ctx.lineTo(x0 + r * cosa, y0 - r *sina);  
  98.     }  
  99. }  
  100. </script>  
  101. </body>  
  102. </html></span>  

效果图如下:


至此,我们想要的功能都已经实现了。空心物体,响应鼠标事件。那实心物体怎么办?实心物体,使用版本1中的方法,空心物体使用版本6中的方法画。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值