用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中的方法画。
- 用Canvas打造高强度渲染SVG
- 用Canvas打造高强度渲染SVG
- canvas把SVG渲染成图片
- 浏览器端图表渲染技术SVG, VML HTML Canvas
- ECharts官方教程(十四)【使用 Canvas 或者 SVG 渲染】
- Seen.js – 使用 SVG 或者 Canvas 渲染 3D 场景
- SVG的渲染顺序
- VML、SVG、Canvas简介
- svg 与canvas对比
- HTML5----Canvas VS SVG
- Canvas 与 SVG
- SVG特效&canvas特效
- Canvas和SVG
- canvas和svg基础
- SVG vs Canvas
- Canvas和SVG区别
- 浅谈Canvas与SVG
- Canvas与SVG
- 23种设计模式之——工厂方法模式
- KVC-键值编码是一种间接地访问实例变量的方式
- 【JAVA应用】多线程断点下载
- 给eclipse添加字体
- JDK中密钥和证书管理工具Keytool常用命令
- 用Canvas打造高强度渲染SVG
- 关于导入的工程没有权限修改的问题
- javaweb-jsp中session和application域的区别
- 程序员跳槽全攻略
- Unity3D技术之Advanced流媒体资源浅析
- 两个坐标系间的转换
- 解释:《深度探索C++对象模型》对NRV优化的讨论
- android textview改变部分文字的颜色和string.xml中文字的替换
- iOS_Apprentice_2_Checklists学习总结(6)输入/输出存储数据