构建Canvas矢量图形渲染器(一)—— 基础架构、矢量点的绘制

时间:2022-08-23 00:05:52

 本课题是我今年毕业设计的课题,现在我边做边跟大家分享,希望能通过“canvas矢量图形渲染器”让大家对canvas元素和其中的性能优化有更深的理解。

1.首先说说这个矢量渲染器是什么。

    canvas元素封装了很对对图形绘制的接口,但是他跟flex相比最大的区别是我们通过fill() 或是 stroke()方法绘制的图形是一张像素图片,当放大或是缩小的时候会出现模糊等各种状况。所以直接调用canvasAPI来绘制矢量图形非常不合适。

    这样我们就需要设计一渲染器,里面封装了各种图形的绘制接口,并通过调用渲染器的绘制方法按照我们想发来绘制每一个矢量元素。

    每一个矢量元素都是一个继承自矢量图形基类的对象,比如可以使点,线,面等。每一个图形有自己的大小和位置信息,我们依次把每个图形送入渲染器,渲染器结合自己当前的属性(缩放百分比,中心点等)就可以计算出每一个图形需要绘制的真实像素位置,之后再调用canvas的底层API实现图形的绘制。

    基本的类图结构如下(在设计的过程当中可能会增加一些新的功能类):

 构建Canvas矢量图形渲染器(一)—— 基础架构、矢量点的绘制

    一些点、线、面的基本矢量元素继承自Geometry类,Geometry定义了矢量图形的形状信息。同时也是Vector类的一个属性,Vector类里面还包括矢量元素的其他信息(如id、添加时间等)。

   Layer类表示了当前图层的一些基本信息,例如声明了一个图层(大小是400px * 400px),同时我在坐标为(0,0)的点放置了一个半径为50px的圆,当前的缩放为100%,视图中心点也是(0,0)则我们会得到下面的这样一张图片:

构建Canvas矢量图形渲染器(一)—— 基础架构、矢量点的绘制

    注: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对应的点到底是如何计算的。

2.从构建基本的几何基类、点开始。

1.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(后面的下载中有提供)。

2.Point类,继承自Geometry。

    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);

    这样的声明让我们的程序看起来更加简洁,扩展性会更强。

    注:点在图形当中所充当的是一个无大小、只表示位置的几何对象。但是,通常我们需要为点设置一个半径,以便让大家可以看到。所以说点在任何缩放级别下的半径大小都是一个固定的像素值。同样,线的宽度也可以这样理解。

3.添加、显示矢量图形

    上面我们已经讲了如何创建一个矢量的点,但是我们该如何把这个点显示到我们的浏览器当中呢?

    一个方法就是把这些矢量图形先存放到一个图层当中,这个图层中的一些属性(中心点、缩放百分比、视图范围)共同影响着矢量图形的显示结果。当然我们可以将绘制的核心代码写到这个图层类当中,但是更好的做法就是用一个渲染器类控制关于显示的方法,使以后可以更好的维护、扩展。

1.图层类

    注:这个课题的第一节,我们的图层类只有几个必须的方法,以控制添加、显示。在以后的随笔中会更深入的增加图层的功能。(同样渲染器类也是如此)

//图层类
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,点击下载