构建Canvas矢量图形渲染器(二)—— 渲染器、定点缩放、漫游

时间:2022-08-23 00:06:40

上一次随笔大概的讲了下构建一个矢量绘图渲染器的基本架构。下面我们来继续深入的完善我们的渲染器。

本次随笔目标:实现定点的放大缩小功能、漫游,先上Demo。(大家可以添加多个点和圆,在放大、缩小的时候两者有什么不同?—— 点的大小貌似没有变化,而圆的呢。。想想为什么)

 

1.渲染器类的实现

   上一节我们已经实现了点的绘制,但是并没有给大家介绍渲染器类。如果有关注的同学,请打开上一节最后一部分看看我们在图层类中调用渲染器的地方。

this.renderer.drawGeometry(vector.geometry, style);

 

   没错我们通过drawGeometry方法来绘制,这样我们降低了渲染器类与图层类的耦合。渲染器只关心图形和样式两个参数,其他的信息对于渲染器来说是无用的。

1.首先看构造函数

//渲染器类的构造函数。
function Canvas (layer) {
    this.canvas = document.createElement("canvas");
    this.context = this.canvas.getContext("2d");
    //只有当lock为false的时候才会执行绘制。
    this.lock = true;
    this.layer = layer;
    this.setSize(layer.size);
    this.geometrys = {};
    layer.div.appendChild(this.canvas);
}

   我们在构造函数中创建了一个canvas元素,并添加为layer.div的子元素。

2.设置渲染器宽高的方法——setSize

//设置canvas元素的大小。
Canvas.prototype.setSize = function(size){
    this.canvas.width = size.w;
    this.canvas.height = size.h;
    this.canvas.style.width = size.w + "px";
    this.canvas.style.height = size.h + "px";
}

3.目前唯一对外的接口——drawGeometry

//这个方法用于收集所有需要绘制的矢量元素。    
Canvas.prototype.drawGeometry = function(geometry, style){
    this.geometrys[geometry.id] = [geometry, style];
    //如果渲染器没有被锁定则可以进行重绘。
    if(!this.lock){
        this.redraw();
    }
}

    在渲染器类中有一个geometrys的对象,里面存储着所有需要被绘制几何形状和其对应的样式。

    每次我们判断lock这个属性,他代表了当前是否锁定这个渲染器,如果不锁定,就把刚刚存在geometrys对象里面的所有矢量图形绘制一遍。(因为渲染器本身并不知道有多少个矢量元素需要绘制,所以我们要通过lock这个属性来控制)。

4.全部重绘——redraw方法。

//每次绘制都是全部清除,全部重绘。
//todo加入快照后可以大大提高性能。    
Canvas.prototype.redraw = function(){
    this.context.clearRect(0, 0, this.layer.size.w, this.layer.size.h);
    var geometry;
    if(!this.lock){
        for(var id in this.geometrys){
            if(this.geometrys.hasOwnProperty(id)){
                geometry = this.geometrys[id][0];
                style = this.geometrys[id][1];
                this.draw(geometry, style, geometry.id);
            }            
        }
    }    
}

    我们在redraw这个方法中执行了对geometrys的遍历绘制,但是不要忘记首先需要调用clearRect做一次全部清除。

5.每一个矢量图形的绘制——draw方法。

//每一个矢量元素的绘制,这里我们在以后会添加更多的矢量图形。
Canvas.prototype.draw = function(geometry, style, id){
    if(geometry instanceof Point){
        this.drawPoint(geometry, style, id);
    }
    //{todo} 我们在这里判断各种矢量要素的绘制。        
}

    draw方法里面首先需要判断我们当前的这个矢量图形到底是什么,在通过对应的方法进行绘制。

6.绘制点和设置样式 —— drawPoint和setCanvasStyle方法。

//针对点的绘制方法。
Canvas.prototype.drawPoint = function(geometry, style, id){
    var radius = style.pointRadius;
    var twoPi = Math.PI*2;
    var pt = this.getLocalXY(geometry);
    //填充
    if(style.fill) {
        this.setCanvasStyle("fill", style)
        this.context.beginPath();
        this.context.arc(pt.x, pt.y, radius, 0, twoPi, true);
        this.context.fill();
    }
    //描边
    if(style.stroke) {
        this.setCanvasStyle("stroke", style)
        this.context.beginPath();
        this.context.arc(pt.x, pt.y, radius, 0, twoPi, true);
        this.context.stroke();
    }
    this.setCanvasStyle("reset");
}

//设置canvas的样式。
Canvas.prototype.setCanvasStyle = function(type, style) {
    if (type === "fill") {     
        this.context.globalAlpha = style['fillOpacity'];
        this.context.fillStyle = style['fillColor'];
    } else if (type === "stroke") {  
        this.context.globalAlpha = style['strokeOpacity'];
        this.context.strokeStyle = style['strokeColor'];
        this.context.lineWidth = style['strokeWidth'];
    } else {
        this.context.globalAlpha = 0;
        this.context.lineWidth = 1;
    }
}

    我们的点的位置信息是基于单位坐标的。比如说我们的canvas大小是400 * 400,则世界坐标系的原点位置(0,0)对应在屏幕上则应该是(200,200)。

    所以一个定义在原点位置的点,则需要一个函数对我们的世界坐标系进行转换,变成一个屏幕可显示的坐标。这样我们需要一个函数getLocalXY。

7.世界坐标与屏幕坐标的转换——getLocalXY。

//获得一个点的屏幕显示位置。
Canvas.prototype.getLocalXY = function(point) {
    var resolution = this.layer.getRes();
    var extent = this.layer.bounds;
    var x = (point.x / resolution + (-extent.left / resolution));
    var y = ((extent.top / resolution) - point.y / resolution);
    return {x: x, y: y};
}

    这里我们先不用管resolution这个函数到底是干什么的,在后面一点我会给大家解释的。

    这里我们只需要知道这样就可以把我们在世界坐标系中定义的点转换为屏幕坐标。

8.总结下渲染器

    可能大家看到这里觉得很郁闷,简简单单的画圆方法通过我们的渲染器一封装咋就实现的这么复杂哩?

    1.通过这样的一个结构我们可以很好的扩展所支持的矢量图形种类(虽然到目前为止只有点)。

    2.我们结合几何信息和样式两个方面来绘制一个矢量图形。几何信息只表示位置,大小等;样式控制线宽,点大小,透明度,颜色等。这样一来我们在设计的过程中可以更好的专注一方面的内容。

    3.使用这样的结构最最重要的是:我们需要实现矢量图形,而不是普通的像素图片。

2.关于zoom,resolution和当前视图范围的概念

1.zoom 和 resolution

    之前我们在Layer这个类里面定义了一个方法,叫做getRes:

//这个res代表当前zoom下每像素代表的单位长度。 
//比如当前缩放比率为 200% 则通过计算得到 res为0.5,说明当前zoom下每个像素只表示0.5个单位长度。
Layer.prototype.getRes = function() {
    this.res = 1 / (this.zoom / 100);
    return this.res;
}

    为了明白这个函数的意思,首先我们得明确一个几个事情:

    1.当我们为一个div创建图层,假设div的widht是400px、height是400px,其默认的zoom为100%。我们其实是为这个div创建了一个世界坐标系,坐标系的原点(0,0)在div的中心也就是(200,200)这个位置,整个世界坐标系的范围也就是:左下(-200,-200);右上(200,200)。

    2.在我们的世界坐标系中的长度单位和实际的像素长度单位有一个对应关系,当zoom为100%时,一个像素单位对应这一个世界坐标系中的单位。通过getRes方法我们在对应的zoom下面可以计算出当前的resolution。假设当前的zoom为200%。则通过计算:

“1 / (200 / 100)” 得到0.5这个值,也就是说在当前的zoom下,1个像素单位只能表示0.5个世界坐标系中的单位,通过这一关系我们就实现了矢量缩放。

 2.当前视图范围

   通过layer.moveto我们来寻找当前范围,并对当前范围的矢量图形进行显示。

Layer.prototype.moveTo = function (zoom, center) {
    this.zoom = zoom;
    if(!center) {
        center = this.center;
    }
    var res = this.getRes();
    var width = this.size.w * res;
    var height = this.size.h * res;
    //获取新的视图范围。
    var bounds = new CanvasSketch.Bounds(center.x - width/2, center.y - height/2, center.x + width/2, center.y + height/2);
    this.bounds = bounds;
    //记录已经绘制vector的个数
    var index = 0;
    this.renderer.lock = true;
    for(var id in this.vectors){
        index++;
        if(index == this.vectorsCount) {
            this.renderer.lock = false;
        }
        this.drawVector(this.vectors[id]);
    }
}

    这段代码当中,我们首先用width、height两个局部变量表示了在当前的缩放级别下,整个div所能表示的世界坐标系中的长度和宽度。并通过中心点确定了当前的视图范围,循环所有的矢量图形,在当前视图范围外的就一定不会被绘制。

    下面这个Demo便是通过此理论制作的矢量图形漫游。

 

3.创建新的图形——圆

    圆和点最大的区别在于圆有半径而点没有,圆用拥有一个世界坐标系下的长度半径,所以圆的大小会根据zoom的不同而改变,圆继承自点,所以代码非常easy

function Circle(x, y, radius) {
    Point.apply(this, arguments);
    this.radius = radius;
}

Circle.prototype = new Point();

Circle.prototype.getBounds = function () {
    if(!this.bounds) {
        this.bounds = new CanvasSketch.Bounds(this.x - this.radius, this.y - this.radius, this.x + this.radius, this.y + this.radius);
        return this.bounds;
    } else {
        return this.bounds;
    }
}

Circle.prototype.geoType = "Circle";

   有了圆我们就需要在渲染器类中加入渲染圆的方法:

//针对圆的绘制方法。
Canvas.prototype.drawCircle = function(geometry, style, id){
    var radius = geometry.radius
    var twoPi = Math.PI*2;
    var pt = this.getLocalXY(geometry);
    //填充
    if(style.fill) {
        this.setCanvasStyle("fill", style)
        this.context.beginPath();
        this.context.arc(pt.x, pt.y, radius / this.layer.res, 0, twoPi, true);
        this.context.fill();
    }
    //描边
    if(style.stroke) {
        this.setCanvasStyle("stroke", style)
        this.context.beginPath();
        this.context.arc(pt.x, pt.y, radius / this.layer.res, 0, twoPi, true);
        this.context.stroke();
    }
    this.setCanvasStyle("reset");
}

    这样一来我们的渲染器就新增了一个几何形状,慢慢的大家就会体会到这样架构的优势所在:每次增加新的几何元素都非常的方便,扩展性强~

    现在大家在看看本次随笔一开始的demo,添加点和圆并进行缩放,点的屏幕显示大小永远不变,而圆会随着zoom值得变换而发生改变。

    尝试一下:下载后面提供的本次随笔的源码,并修改两个demo的,可能会发现很有趣~

     下次随笔预告:1.用世界坐标系控制缩放中心点实在困难,下次我们来解决用屏幕坐标来控制缩放。

                        2.增加鼠标事件,鼠标可以滚轮可以缩放,拖动可以平移。

          3.增加更多的图形支持。

   本次随笔的所有源码+demo,请点击下载

   谢谢关注!