用Canvas打造高强度渲染SVG

来源:互联网 发布:男士购物软件 编辑:程序博客网 时间:2024/04/28 19:21

前言

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

一、基本概念

         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:

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

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

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

<span style="font-size:14px;"><!DOCTYPE html><html><head><meta charset="UTF-8"/><title>canvas2SVG</title><style>#container {   border: 1px solid #000;   position: 0px;   left: 0px;   top: 0px;}</style></head><bodyonload="init()"><canvas width="800px"height="400" id="container"></canvas><scripttype="text/javascript">function init() {   var ctx = document.getElementById('container').getContext('2d');   var point = [300, 100];   //路径1   ctx.beginPath();   ctx.moveTo(0, 0);   ctx.lineTo(0, 100);   ctx.lineTo(100, 100);   ctx.lineTo(100, 0);   ctx.lineTo(0, 0);   if (ctx.isPointInPath(point[0], point[1])) {        ctx.fillStyle = '#f00';   } else {        ctx.fillStyle = '#000';   }   ctx.fill();   //路径2   ctx.beginPath();   ctx.moveTo(400, 0);   ctx.lineTo(400, 200);   ctx.lineTo(200, 200);   ctx.lineTo(400, 0);   if (ctx.isPointInPath(point[0], point[1])) {        ctx.fillStyle = '#f00';   } else {        ctx.fillStyle = '#000';   }   ctx.fill();}</script></body></html></span>


五、鼠标响应

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

<span style="font-size:14px;">functioninit() {    var width = 800;    var height = 400;    var container =document.getElementById('container');    var ctx = container.getContext('2d');    var func = [line1, line2];    container.width = width;    container.height = height;    document.getElementById('container').addEventListener('mousemove',render);    render({clientX: -1, clientY: -1});     function render(event) {        ctx.clearRect(0, 0, width, height);        for (var key in func) {            ctx.beginPath();            ctx.fillStyle = func[key](event.clientX,event.clientY);            ctx.fill();        }    }     function line1(x, y) {        ctx.moveTo(0, 0);        ctx.lineTo(0, 100);        ctx.lineTo(100, 100);        ctx.lineTo(100, 0);        ctx.lineTo(0, 0);        return ctx.isPointInPath(x, y) ? '#f00': '#000';    }     function line2(x, y) {        ctx.moveTo(400, 0);        ctx.lineTo(400, 200);        ctx.lineTo(200, 200);        ctx.lineTo(400, 0);        return ctx.isPointInPath(x, y) ? '#f00': '#000';    }}</span>

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

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


六、物体重叠

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

<span style="font-size:14px;">function init() {   var width = 800;   var height = 400;   var container = document.getElementById('container');   var ctx = container.getContext('2d');   var geometry = [        [            [[10, 10], [10, 110], [110, 110],[10, 10]],            [[110, 10], [110, 110], [210, 110],[110, 10]],            [[210, 10], [210, 110], [310, 110],[210, 10]]        ],[            [[0, 50], [0, 150], [100, 150], [0,50]],            [[100, 50], [100, 150], [200, 150],[100, 50]],            [[200, 50], [200, 150], [300, 150],[200, 50]]        ],[            [[0, 100], [0, 200], [100, 200],[0, 100]],            [[100, 100], [100, 200], [200,200], [100, 100]],            [[200, 100], [200, 200], [300, 200],[200,100]]        ]   ];   container.width = width;   container.height = height;   document.getElementById('container').addEventListener('mousemove',render);   render({clientX: -1, clientY: -1});    function render(event) {        ctx.clearRect(0, 0, width, height);        for (var n = 0; n < geometry.length;n++) {            ctx.beginPath();            ctx.strokeStyle =draw(event.clientX, event.clientY, geometry[n]);            ctx.stroke();       }   }    function draw(x, y, geo) {      for (var i = 0; i < geo.length; i++) {          var face = geo[i];          for (var p = 0; p < face.length;p++) {              if (p == 0) {                  ctx.moveTo(face[p][0],face[p][1]);              } else {                  ctx.lineTo(face[p][0],face[p][1]);              }          }      }      return ctx.isPointInPath(x, y) ? '#f00' :'#000';   }}</span>

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


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

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

<span style="font-size:14px;">    function render(event) {        ctx.clearRect(0, 0, width, height);        var hover = [];        for (var n = 0; n < geometry.length;n++) {            ctx.beginPath();            ctx.strokeStyle =draw(event.clientX, event.clientY, geometry[n]);            if (ctx.strokeStyle == '#ff0000') {                hover.push(n);            }            ctx.stroke();        }        hover.reverse();        if (hover.length > 0) {            for (var n = 0; n <geometry.length; n++) {                ctx.beginPath();                draw(event.clientX,event.clientY, geometry[n]);                ctx.strokeStyle = n == hover[0]? '#f00' : '#000'                ctx.stroke();            }        }    }</span>

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


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

七、物体重叠的解决

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

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


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

<span style="font-size:14px;">  d = Math.sqrt((x0 - x1) * (x0 - x1) + (y0 -y1) * (y0 - y1));  r = 1;//圆的半径  sina = (x1 - x0) / d;  cosa = (y1 - y0) / d;  ax = x0 + r * cosa;  ay = y0 - r * sina;  bx = x0 + r * cosa + x1 - x0;  by = y0 - r * sina + y1 - y0;  ex = x0 - r * cosa + x1 - x0;  ey = y0 + r * sina + y1 - y0;  fx = x0 - r * cosa;  fy = y0 + r * sina;  dx = x0 + r * cosa;  dy = y0 - r * sina;</span>

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

<span style="font-size:14px;"><!DOCTYPEhtml><html><head><metacharset="UTF-8" /><title>canvas2SVG</title><style>#container{    border: 1px solid #000;    position: absolute;    left: 0px;    top: 0px;}</style></head><bodyonload="init()"><canvasid="container"></canvas><scripttype="text/javascript">functioninit() {     var width = 800;    var height = 400;    var container =document.getElementById('container');    var ctx = container.getContext('2d');    var geometry = [        [            [[10, 10], [10, 110], [110, 110],[10, 10]],            [[110, 10], [110, 110], [210, 110],[110, 10]],            [[210, 10], [210, 110], [310, 110],[210, 10]]        ],[            [[0, 50], [0, 150], [100, 150], [0,50]],            [[100, 50], [100, 150], [200, 150],[100, 50]],            [[200, 50], [200, 150], [300, 150],[200, 50]]        ],[            [[0, 100], [0, 200], [100, 200],[0, 100]],            [[100, 100], [100, 200], [200,200], [100, 100]],            [[200, 100], [200, 200], [300,200], [200,100]]        ]    ];      container.width = width;    container.height = height;   document.getElementById('container').addEventListener('mousemove',render);    render({clientX: -1, clientY: -1});      function render(event) {        ctx.clearRect(0, 0, width, height);        var hover = [];        for (var n = 0; n < geometry.length;n++) {            ctx.beginPath();            ctx.strokeStyle = draw(event.clientX,event.clientY, geometry[n]);            if (ctx.strokeStyle == '#ff0000') {                hover.push(n);            }            ctx.stroke();        }        hover.reverse();        if (hover.length > 0) {            for (var n = 0; n <geometry.length; n++) {                ctx.beginPath();                draw(event.clientX,event.clientY, geometry[n]);                ctx.strokeStyle = n == hover[0]? '#f00' : '#000'                ctx.stroke();            }        }    }     function draw(x, y, geo) {      for (var i = 0; i < geo.length; i++) {          var face = geo[i];          var oldx = null;          var oldy = null;          for (var p = 0; p < face.length;p++) {              if (p == 0) {                  oldx = face[p][0];                  oldy = face[p][1];              } else {                  line(oldx, oldy, face[p][0],face[p][1]);                  oldx = face[p][0];                  oldy = face[p][1];              }          }      }      return ctx.isPointInPath(x, y) ? '#f00' :'#000';    }     function line(x0, y0, x1, y1) {        var d = Math.sqrt((x0 - x1) * (x0 - x1)+ (y0 - y1) * (y0 - y1));        var r = 0.5;        var sina = (x1 - x0) / d;        var cosa = (y1 - y0) / d;        ctx.moveTo(x0 + r * cosa, y0 - r *sina);        ctx.lineTo(x0 + r * cosa + x1 - x0, y0- r * sina + y1 - y0);        ctx.lineTo(x0 - r * cosa + x1 - x0, y0+ r * sina + y1 - y0);        ctx.lineTo(x0 - r * cosa, y0 + r * sina);        ctx.lineTo(x0 + r * cosa, y0 - r *sina);    }}</script></body></html></span>

效果图如下:


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

 

0 0
原创粉丝点击