【实现自己的可视化框架引擎02】抽象图像元素

1,013 阅读5分钟

写在前面

由上一章可以看出,当我们在绘制一个复杂的图形时,我们的代码将膨胀,无论是阅读还是理解都将变得复杂。将复杂的图形分解成多个简单的基本图形组合,并是我们的目标。
一个复杂的页面由不同组件组合形成,我们将组件化的思想引入到我们引擎中来。

图元

与组件对应,下面我们将其命名为图元。下面我们设计一下图元的类结构。

export default class Node {
  constructor(canvas, style = {}) {
    this.canvas = canvas || null;                         // 画布
    this.position = style.position || new Point(0, 0);    // 图元坐标
    this.visible = style.visible || true;                 // 是否显示
    this.rotation = style.rotation || 0;                  // 旋转角度
    this.scaleX = style.scaleX || 1;                      // X方向旋转
    this.scaleY = style.scaleY || 1;                      // Y方向
    this.alpha = style.alpha || 1;                        // 透明度
    this.color = style.color || '#000000';                // 颜色
    this.shadowOffsetX = style.shadowOffsetX || '';       // X方向阴影
    this.shadowOffsetY = style.shadowOffsetY || '';       // Y方向阴影
    this.shadowBlur = style.shadowBlur || '';             // 模糊程度
    this.shadowColor = style.shadowColor || '';           // 阴影颜色
  }
  
  draw(painter) {
    // Node 绘制函数
  }

  paint() {
    if (this.visible) {
      const { painter } = this.canvas;
      painter.save();
      painter.globalAlpha = this.alpha;
      painter.translate(0, this.scaleY * (this.canvas.height - this.canvas.ratio * this.position.y));
      painter.rotate(this.rotation * Math.PI / 180);
      painter.scale(this.scaleX, this.scaleY);
      this.draw(painter);
      painter.restore();
      config.after && config.after(this, painter);
    }
  }
}

从上面的代码可以看出,Node类通过传入画布canvas与样式配置style来描述图元。在元素中定义了paint函数,该函数在对画笔进行设置属性后,通过自身的draw函数进行绘制当前图元。Node类提供了一个抽象函数draw用于子类的继承,这里我们可以类比React组件的render方法,该方法获得画笔参数painter进行绘制。

坐标变换

我们知道Canvas的坐标原点为左上角,如图所示:

我们图元Node坐标position的设计与我们习惯的左下角,这里就需要进行坐标变换,使得我们的坐标系与Canvas坐标系适配。

如图所示,position转换到Canvas坐标系的坐标为(position.x, height - position.y)。所以在绘制函数painter中我们通过代码进行了坐标转换。

painter.translate(0, this.scaleY * (this.canvas.height - this.canvas.ratio * this.position.y));

保存与恢复

我们设置画笔样式时,通过save函数和restore函数进行画笔样式的保存与恢复,这样当我绘制完图元时,我们修改的画笔样式将不会影响其他图元的绘制,避免“交叉感染”的风险。

将图元绘制到画布上

我们已经设计好了图元,那么如何将图元绘制到画布上呢? 这里,我们修改一下上一章封装的画布类Canvas,使用画布类管理图元,并将图元绘制到实际的Canvas画布上。

class Canvas {
	constructor(config) {
		if (config.ele === undefined) {
		  throw new Error('Not found config of canvas element');
		}
		// canvas 标签的容器标签
		this.container = config.ele;
		// 设置canvas width属性与样式width 的比率
		this.ratio = config.ratio || 2;
		// 创建 canvas 标签
		this.canvas = document.createElement('canvas');
		// 图元数组
		this.childs = [];
		this.init();
	}
	
	/**
	* 重新定义Canvas的大小
	*/
	repaint() {
		this.container.innerHTML = '';
		this.canvas = document.createElement('canvas');
		this.init();
	}
	
	/**
	* 初始化Canvas系数
	*/
	init() {
		// 获取容器的样式
		const styles = getComputedStyle(this.container, null);
		// 容器的宽
		const width = parseInt(styles.width);
		// 容器的高
		const height = parseInt(styles.height);
		// 设置canvas的样式宽
		this.canvas.style.width = `${width}px`;
		// 设置canvas的样式高
		this.canvas.style.height = `${height}px`;
		// 根据比率设置相应的属性宽高
		this.canvas.width = this.ratio * width; //设置缩放比
		this.canvas.height = this.ratio * height;
		// 去除点击选中样式
		this.canvas.style.outline = 'none';
		this.canvas.onclick = (e) => { this.canvas.focus(); };
		this.container.appendChild(this.canvas);
		// 设置画笔属性
		this.painter = this.canvas.getContext('2d');
	}
	
	addChild() {
        for (const i in arguments) {
          if (arguments[i] instanceof Node) {
            if (this.childs.indexOf(arguments[i]) === -1) {
              arguments[i].canvas = this;
              this.childs.push(arguments[i]);
              if (arguments[i] instanceof Layer) {
                arguments[i].build();
              }
            }
          }
        }
    }

    removeChild() {
        for (const i in arguments) {
          if (arguments[i] instanceof Node) {
            if (this.childs.indexOf(arguments[i]) !== -1) {
              arguments[i].canvas = this;
              this.childs.splice(this.childs.indexOf(arguments[i]), 1);
            }
          }
        }
    }
	
	paint() {
        this.painter.clearRect(0, 0, this.width, this.height);
        for (let i = 0; i < this.childs.length; i++) {
          this.childs[i].paint();
        }
    }
}

上面代码,我们给Canvas类增加的paint函数,在paint函数中,我们遍历图元数组,调用图元类的paint() 方法绘制图元,从而达到整个页面的绘制。
下面,我们将通过绘制给出了一个长方形图元的例子进行图元的使用。

/**
* 矩形类 定义实现
*/
class Rectangle extends Node {
  constructor(canvas, style) {
	super(canvas, style);
	this.width = style.width || 0;
	this.height = style.height || 0;
  }

  setSize(width, height) {
	this.width = width;
	this.height = height || this.height;
  }
  /**
  * 主要实现的绘制函数
  */
  draw(painter) {
	painter.fillStyle = this.color;
	painter.fillRect(this.position.x, this.position.y - this.height, this.width, this.height);
  }
}

下面利用图元绘制两个矩形

let dv = document.getElementById('canvas');
let canvas = new Canvas({
	ele: dv
});
let rect1 = new Rectangle(canvas, {
	width: 80,
	height: 30,
	color: '#FF0000'
});
let rect2 = new Rectangle(canvas, {
	width: 80,
	height: 20,
	color: '#00FF00'
});
rect1.setPosition(0, 80);
rect2.setPosition(100, 200);
canvas.addChild(rect1, rect2);
canvas.paint();

效果图

图层

对于复杂的图表,如封面的雷达图,如果我们都让Canvas 类进行管理,那么Canvas类的管理将变得非常复杂。所以我们将在图元与画布之间引入图层,将图层一层一层的叠加在画布Canvas上。

class Layer extends Node {
    constructor(canvas) {
      super(canvas);
      this.childs = [];
    }
    
    addChild() {
      for (const i in arguments) {
        if (arguments[i] instanceof Node) {
          if (this.childs.indexOf(arguments[i]) === -1) {
            arguments[i].canvas = this.canvas;
            this.childs.push(arguments[i]);
            if (arguments[i] instanceof Layer) {
              arguments[i].build()
            }
          }
        }
      }
    }
    
    removeChild(child) {
      if (this.childs.indexOf(child) !== -1) {
        this.childs.splice(this.childs.indexOf(child), 1);
      }
    }
    
    paint() {
      if (this.visible) {
        for (const child of this.childs) {
          child.paint();
        }
      }
      super.paint();
    }
}

有代码可以看出,图层类是一个复杂的图元,它拥有一个childs 的图元数组属性。在这里我们可以增加一个zIndex属性用于图层的层次顺序,类比css的z-index,这里就不展开说明了。
目录

【实现自己的可视化引擎01】认识Canvas
【实现自己的可视化框架引擎02】抽象图像元素
【实现自己的可视化引擎03】构建基础图元库
【实现自己的可视化引擎04】图像元素动画
【实现自己的可视化引擎05】交互与事件
【实现自己的可视化引擎06】折线图
【实现自己的可视化引擎07】柱状图
【实现自己的可视化引擎08】条形图
【实现自己的可视化引擎09】饼图
【实现自己的可视化引擎10】散点图
【实现自己的可视化引擎11】雷达图
【实现自己的可视化引擎12】K线图
【实现自己的可视化引擎13】仪表盘
【实现自己的可视化引擎14】地图
【实现自己的可视化引擎15】关系图