本课题是我今年毕业设计的课题,现在我边做边跟大家分享,希望能通过“canvas矢量图形渲染器”让大家对canvas元素和其中的性能优化有更深的理解。
canvas元素封装了很对对图形绘制的接口,但是他跟flex相比最大的区别是我们通过fill() 或是 stroke()方法绘制的图形是一张像素图片,当放大或是缩小的时候会出现模糊等各种状况。所以直接调用canvasAPI来绘制矢量图形非常不合适。
这样我们就需要设计一渲染器,里面封装了各种图形的绘制接口,并通过调用渲染器的绘制方法按照我们想发来绘制每一个矢量元素。
每一个矢量元素都是一个继承自矢量图形基类的对象,比如可以使点,线,面等。每一个图形有自己的大小和位置信息,我们依次把每个图形送入渲染器,渲染器结合自己当前的属性(缩放百分比,中心点等)就可以计算出每一个图形需要绘制的真实像素位置,之后再调用canvas的底层API实现图形的绘制。
基本的类图结构如下(在设计的过程当中可能会增加一些新的功能类):
一些点、线、面的基本矢量元素继承自Geometry类,Geometry定义了矢量图形的形状信息。同时也是Vector类的一个属性,Vector类里面还包括矢量元素的其他信息(如id、添加时间等)。
Layer类表示了当前图层的一些基本信息,例如声明了一个图层(大小是400px * 400px),同时我在坐标为(0,0)的点放置了一个半径为50px的圆,当前的缩放为100%,视图中心点也是(0,0)则我们会得到下面的这样一张图片:
注:Layer类所表示的属性:外侧的方框代表当前的视图范围(viewBounds),坐标(0, 0)点则代表视图的中心点(center),zoom的值代表当前的缩放百分比。
Vector类所表示的属性: 拥有一个Geometry属性表示半径为70像素的圆,且拥有一个Style属性表示填充的颜色为橙色。
下面说说Layer类,Vector类和Canvas类是如何协调工作的:
Canvas类也就是渲染器类,是本课题的核心,其中定义了各种绘制Geometry图形的方法。但是我们该如何调用他进行绘制呢?
1.我们声明一个Vector类的实例V1(他表示一个矢量图形,其形状信息保存在Geometry属性当中)。
2.我们的Layer类必然有一个接口,用于接收由Vector声明的实例V1(比如addVectors方法),当我们把所有的矢量图形都接收到Layer当中后,我们就想要在浏览器当中看到我们所创建的的矢量图形。
3.之后我们就应该请渲染器类出场了,他接收了Layer的所有矢量图形的引用、当前的缩放级别、当前的视图范围和当前的中心点,之后渲染器通过一大堆的计算就神奇的把Geometry中的属性变成了CanvasAPI中可以调用的数据。
注:就拿上个例子来说圆的中心点是(0,0),半径是70像素,通过渲染器的一系列计算最后我们会调用“context.arc(200, 200, 70, Math.PI * 2, true); context.fill()”这两句代码。(200,200)这两个位置便是渲染器所要计算的绘制中心点,而70这个半径参数是通过70 * zoom(当前为100%)得到的。当然我们不能单单考虑这么简单的一种情况,后面会跟大家介绍这些Geometry对应的点到底是如何计算的。
这个类是一个几何图形的基类。
function Geometry(){ this.id = CanvasSketch.getId("geomtry_"); } //bounds属性定义了当前Geometry外接矩形范围。 Geometry.prototype.bounds = null; //定义Geometry的id属性。 Geometry.prototype.id = null; //定义对bounds基类克隆的方法 Geometry.prototype.clone = function () { return new Geometry(); } //销毁当前的Geometry Geometry.prototype.destroy = function () { this.bounds = null; this.id = null; }
其中的SketchCanvas.getId方法会返回一个唯一的id值。这个方法其实很简单就是用一个全局变量不断加1(后面的下载中有提供)。
point类作为继承自Geometry最简单的类,我们首先来介绍他。
function Point(x, y) { Geometry.apply(this, arguments); this.x = x; this.y = y; } Point.prototype = new Geometry(); //point类的横坐标。 Point.prototype.x = null; //point类的纵坐标。 Point.prototype.y = null; //得到点的范围。 Point.prototype.getBounds = function () { if(!this.bounds) { var x = this.x; var y = this.y; this.bounds = new CanvasSketch.Bounds(x, y, x, y); return this.bounds; } else { return this.bounds; } } //clone方法。 Point.prototype.clone = function () { return new Point(this.x, this.y); }
这个Point类定义了几个比较基本,简单的方法:getBounds、clone,使用prototype进行对Geometry类的继承。最重要的是Point会接受两个参数(x、y)也就是其位置信息。
这样我们就可以用以下的语句声明一个点了:
var point = new Point(0, 0);
这样的声明让我们的程序看起来更加简洁,扩展性会更强。
注:点在图形当中所充当的是一个无大小、只表示位置的几何对象。但是,通常我们需要为点设置一个半径,以便让大家可以看到。所以说点在任何缩放级别下的半径大小都是一个固定的像素值。同样,线的宽度也可以这样理解。
上面我们已经讲了如何创建一个矢量的点,但是我们该如何把这个点显示到我们的浏览器当中呢?
一个方法就是把这些矢量图形先存放到一个图层当中,这个图层中的一些属性(中心点、缩放百分比、视图范围)共同影响着矢量图形的显示结果。当然我们可以将绘制的核心代码写到这个图层类当中,但是更好的做法就是用一个渲染器类控制关于显示的方法,使以后可以更好的维护、扩展。
注:这个课题的第一节,我们的图层类只有几个必须的方法,以控制添加、显示。在以后的随笔中会更深入的增加图层的功能。(同样渲染器类也是如此)
//图层类 function Layer(div) { var style = div.style; var size = new CanvasSketch.Size(parseInt(style.width), parseInt(style.height)); this.size = size; this.div = div; this.maxBounds = new CanvasSketch.Bounds(-size.w / 2, -size.h / 2, size.w / 2, size.h / 2); this.bounds = new CanvasSketch.Bounds(-size.w / 2, -size.h / 2, size.w / 2, size.h / 2); this.zoom = 100; this.vectors = {}; //加入矢量图形的总个数。 this.vectorsCount = 0; //创建一个渲染器。 this.renderer = new Canvas(this); } Layer.prototype.addVectors = function (vectors) { this.renderer.lock = true; for(var i = 0, len = vectors.length; i < len; i++) { if(i == len-1) {this.renderer.lock = false;} this.vectors[vectors[i].id] = vectors[i]; this.drawVector(vectors[i]); } this.vectorsCount += vectors.length; } Layer.prototype.drawVector = function (vector) { if(!vector.style) { style = new CanvasSketch.defaultStyle(); } this.renderer.drawGeometry(vector.geometry, style); }
定义过图层类后,我们就可以为一个div创建图层。
首先我们得在DOM树中创建一个div,作为图层的容器,如:
<body onload="init()"> <div style="width:400px; height:300px;" id="renderer"></div> </body>
这样我们就可以在init函数中为这个id为“renderer”的div创建图层了:
var div = document.getElementById("renderer"); var layer = new Layer(div);
之后我们所要做的就是声明一个矢量图形数组,调用layer.addVectors来为我们的图层添加矢量元素。
for(var i = 0; i<1000; i++) { var point = new Point((Math.random()*400-200), (Math.random()*300-150)); vectors.push(new Vector(point)); } layer.addVectors(vectors);
注:创建1000个x坐标随机在(-200,200)和y坐标随机在(-150,150)之间的点,并添加到图层当中。
上面的代码中,我们通过new Vector()创建一个矢量图形,其几何信息就是之前声明的point变量。
Vector类的声明如下(在目前看来Vector类,就是将geometry和attributes整合到一起的一个类,但是以后会为这个Vector增加更多的方法和属性的)
function Vector(geometry, attributes) { this.id = CanvasSketch.getId("vector"); this.geometry = geometry; if(attributes) { this.attributes = attributes; } }
下一篇随笔会介绍渲染器类的构建方法,在下面提供的源码当中有今天所有讲过的类、和没有涉及到的类(一些工具类,和渲染器类)。
这一节我们只是构建了一个大体的渲染器使用框架,在后面的随笔中会完善他的功能~
下面这个demo展示了我们这个渲染器的基本用法。
尝试一下:改变for循环的个数可以控制产生多少个点,可以自定义x,y值来精确定位点。
下次随笔预告:1.重点介绍渲染器类的构建。
2.大家发现没有上面的demo是以左上角为中心点缩放的,下次添加缩放中心点。
3.增加更多的几何类型(圆和矩形)。
请关注~
本次随笔的所有源码+Demo,点击下载。