【实现自己的可视化引擎03】构建基础图元库

728 阅读5分钟

上一章,我们已经介绍了图元基类Node,正如React构建组件库一样,我们也可以构建我们的基础图元库,以备我们后续开发使用。下面我们将简单介绍几个基本图元类。

直线

export default class Line extends Node {
  constructor(canvas, style) {
    super(canvas, style);
    this.lineWidth = style.lineWidth || 3;
    this.lineCap = style.lineCap || Line.LINE_CAP.BUTT;
    this.lineDash = style.lineDash || [];
    this.lineDashOffset = style.lineDashOffset || 0;
    this.to = style.to || new Point(0, 0);
  }

  static LINE_CAP = {
    BUTT: 'butt',
    ROUND: 'round',
    SQUARE: 'square'
  }

  setTo(to, y = 0) {
    if (to instanceof Point) {
      this.to = to;
    } else {
      this.to = new Point(to, y);
    }
  }

  draw(painter) {
    painter.beginPath();
    painter.strokeStyle = this.color;
    // 设置线框
    painter.lineWidth = this.lineWidth;
    // 设置线条末端样式
    painter.lineCap = this.lineCap;
    // 设置虚线样式
    if (this.lineDash.length > 0) {
      painter.setLineDash(this.lineDash);
      painter.lineDashOffset = this.lineDashOffset;
    }
    // 设置渐变色
    if (this.linearGradient.length > 0) {
      const lingrad = painter.createLinearGradient(this.position.x, this.position.y, this.to.x, this.to.y);
      for (let i = 0; i < this.linearGradient.length; i++) {
        lingrad.addColorStop(this.linearGradient[i][0], this.linearGradient[i][1]);
      }
      painter.strokeStyle = lingrad;
    }
    painter.moveTo(this.position.x, this.position.y);
    painter.translate(0, 2 * this.position.y);
    painter.lineTo(this.to.x, - this.to.y);
    painter.closePath();
    painter.stroke();
  }
}

下面我们给出React 下的使用例子

class AxisTest extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }
  componentDidMount() {
    this.canvas = new Canvas({
      ele: this.myRef.current,
      canAction: false
    });
    let line = new Line(this.canvas, {
      color: '#89DE45',
      lineDash: [12, 8],
      linearGradient: [
        [0, '#DEDEDE'],
        [0.3, '#999999'],
        [0.5, '#777777'],
        [0.7, '#444444'],
        [1, '#000000'],
      ],
      lineWidth: 15,
      position: new Point(0, 0),
      to: new Point(this.canvas.width, this.canvas.height),
    });
    this.canvas.addChild(line);
    this.canvas.paint();
  }

  render() {
    return <div className="axias-chart" ref={this.myRef}/>
  }
}

ReactDOM.render(
  <AxisTest />,
  document.getElementById('root')
)

长方形

export default class Rectangle extends Node {
  static TYPE = {
    STROKE: 1,
    FILL: 2,
  }

  constructor(canvas, style) {
    super(canvas, style);
    this.type = style.type || Rectangle.TYPE.STROKE;
    this.width = style.width || 0;
    this.height = style.height || 0;
    this.lineWidth = style.lineWidth || 3;
    this.lineDash = style.lineDash || [];
  }

  setSize(width, height) {
    this.width = width;
    this.height = height || this.height;
  }

  draw(painter) {
    if (this.type === Rectangle.TYPE.STROKE) {
      painter.strokeStyle = this.color;
      painter.lineWidth = this.lineWidth;
      if (this.lineDash.length > 0) {
        painter.setLineDash(this.lineDash);
      }
    } else {
      painter.fillStyle = this.color;
    }
    // 设置渐变色
    if (this.linearGradient.length > 0) {
      const lingrad = painter.createLinearGradient(
        this.position.x - this.width / 2,
        this.position.y,
        this.position.x + this.width / 2,
        this.position.y,
      );
      for (let i = 0; i < this.linearGradient.length; i++) {
        lingrad.addColorStop(this.linearGradient[i][0], this.linearGradient[i][1]);
      }
      if (this.type === Rectangle.TYPE.STROKE) {
        painter.strokeStyle = lingrad;
      } else {
        painter.fillStyle = lingrad;
      }
    }
    // 设置阴影
    if (!!this.shadowOffsetX) {
      painter.shadowOffsetX = this.shadowOffsetX;
    }
    if (!!this.shadowOffsetY) {
      painter.shadowOffsetY = this.shadowOffsetY;
    }
    if (!!this.shadowBlur) {
      painter.shadowBlur = this.shadowBlur;
    }
    if (!!this.shadowColor) {
      painter.shadowColor = this.shadowColor;
    }
    if (this.type === Rectangle.TYPE.STROKE) {
      painter.strokeRect(
        this.position.x - this.width / 2,
        this.position.y - this.height / 2,
        this.width,
        this.height
      );
    } else {
      painter.fillRect(
        this.position.x - this.width / 2,
        this.position.y - this.height / 2,
        this.width,
        this.height
      );
    }

  }
}

下面我们给出一个带边框的长方形的使用例子

let rectangle = new Rectangle(this.canvas, {
  position: new Point(this.canvas.width / 2, this.canvas.height / 2),
  width: 500,
  height: 100,
  type: Rectangle.TYPE.FILL,
  linearGradient: [
    [0, '#DEDEDE'],
    [0.3, '#999999'],
    [0.5, '#777777'],
    [0.7, '#444444'],
    [1, '#000000'],
  ],
});
let rectBorder = new Rectangle(this.canvas, {
  position: new Point(this.canvas.width / 2, this.canvas.height / 2),
  width: 500,
  height: 100,
  type: Rectangle.TYPE.STROKE,
  color: '#2584BE',
  lineDash: [20, 12],
  lineWidth: 12,
});
this.canvas.addChild(rectangle, rectBorder);
this.canvas.paint();

圆形

export default class Circle extends Node {
  static TYPE = {
    STROKE: 1,
    FILL: 2,
  }

  constructor(canvas, style) {
    super(canvas, style);
    this.type = style.type || Circle.TYPE.STROKE;
    this.radius = style.radius || 30;
    this.lineWidth = style.lineWidth || 3;
    this.lineDash = style.lineDash || [];
  }

  draw(painter) {
    if (this.type === Circle.TYPE.STROKE) {
      painter.strokeStyle = this.color;
      painter.lineWidth = this.lineWidth;
      if (this.lineDash.length > 0) {
        painter.setLineDash(this.lineDash);
      }
    } else {
      painter.fillStyle = this.color;
    }
    // 设置渐变色
    if (this.linearGradient.length > 0) {
      const lingrad = painter.createRadialGradient(
        this.position.x,
        this.position.y,
        0,
        this.position.x,
        this.position.y,
        this.radius,
      );
      for (let i = 0; i < this.linearGradient.length; i++) {
        lingrad.addColorStop(this.linearGradient[i][0], this.linearGradient[i][1]);
      }
      if (this.type === Circle.TYPE.STROKE) {
        painter.strokeStyle = lingrad;
      } else {
        painter.fillStyle = lingrad;
      }
    }
    // 设置阴影
    if (!!this.shadowOffsetX) {
      painter.shadowOffsetX = this.shadowOffsetX;
    }
    if (!!this.shadowOffsetY) {
      painter.shadowOffsetY = this.shadowOffsetY;
    }
    if (!!this.shadowBlur) {
      painter.shadowBlur = this.shadowBlur;
    }
    if (!!this.shadowColor) {
      painter.shadowColor = this.shadowColor;
    }
    painter.beginPath();
    painter.arc(
      this.position.x / this.scaleX,
      this.position.y / this.scaleY,
      this.radius,
      0,
      2 * Math.PI
    );
    painter.closePath();
    if (this.type === Circle.TYPE.STROKE) {
      painter.stroke();
    } else {
      painter.fill();
    }
  }
}

下面我们给出一个带边框的圆形的使用例子

let circle = new Circle(this.canvas, {
  radius: 100,
  position: new Point(this.canvas.width / 2, 100),
  type: Circle.TYPE.FILL,
  linearGradient: [
    [0, '#DEDEDE'],
    [0.3, '#999999'],
    [0.5, '#777777'],
    [0.7, '#444444'],
    [1, '#000000'],
  ],
});
let circleBorder = new Circle(this.canvas, {
  radius: 100,
  position: new Point(this.canvas.width / 2, 100),
  type: Circle.TYPE.STROKE,
  color: '#2584BE',
  lineDash: [20, 12],
  lineWidth: 12,
});
this.canvas.addChild(circle, circleBorder);
this.canvas.paint();

扇形

export default class Sector extends Node {
  static TYPE = {
    STROKE: 1,
    FILL: 2,
  }

  constructor(canvas, style) {
    super(canvas, style);
    this.start = style.start || 0;
    this.stop = style.stop || 2 * Math.PI;
    this.radius = style.radius || 3;
    this.type = style.type || Sector.TYPE.STROKE;
    this.lineWidth = style.lineWidth || 3;
    this.lineDash = style.lineDash || [];
  }

  setStart(_start) {
    this.start = _start;
  }

  setStop(_stop) {
    this.stop = _stop;
  }

  setRadius(radius) {
    this.radius = radius;
  }

  draw(painter) {
    if (this.type === Sector.TYPE.STROKE) {
      painter.strokeStyle = this.color;
      painter.lineWidth = this.lineWidth;
      if (this.lineDash.length > 0) {
        painter.setLineDash(this.lineDash);
      }
    } else {
      painter.fillStyle = this.color;
    }
    // 设置渐变色
    if (this.linearGradient.length > 0) {
      const lingrad = painter.createRadialGradient(
        this.position.x,
        this.position.y,
        0,
        this.position.x,
        this.position.y,
        this.radius,
      );
      for (let i = 0; i < this.linearGradient.length; i++) {
        lingrad.addColorStop(this.linearGradient[i][0], this.linearGradient[i][1]);
      }
      if (this.type === Sector.TYPE.STROKE) {
        painter.strokeStyle = lingrad;
      } else {
        painter.fillStyle = lingrad;
      }
    }
    // 设置阴影
    if (!!this.shadowOffsetX) {
      painter.shadowOffsetX = this.shadowOffsetX;
    }
    if (!!this.shadowOffsetY) {
      painter.shadowOffsetY = this.shadowOffsetY;
    }
    if (!!this.shadowBlur) {
      painter.shadowBlur = this.shadowBlur;
    }
    if (!!this.shadowColor) {
      painter.shadowColor = this.shadowColor;
    }
    painter.beginPath();
    painter.arc(
      this.position.x / this.scaleX,
      this.position.y / this.scaleY,
      this.radius,
      this.start,
      this.stop
    );
    painter.lineTo(
      this.position.x / this.scaleX,
      this.position.y / this.scaleY
    );
    painter.closePath();
    if (this.type === Sector.TYPE.STROKE) {
      painter.stroke();
    } else {
      painter.fill();
    }
  }
}

下面是扇形的使用例子

let sector = new Sector(this.canvas, {
  radius: 100,
  position: new Point(this.canvas.width / 2, 100),
  type: Sector.TYPE.FILL,
  linearGradient: [
    [0, '#DEDEDE'],
    [0.3, '#999999'],
    [0.5, '#777777'],
    [0.7, '#444444'],
    [1, '#000000'],
  ],
  start: 0,
  stop: Math.PI / 3
});
let sectorBorder = new Sector(this.canvas, {
  radius: 100,
  position: new Point(this.canvas.width / 2, 100),
  type: Sector.TYPE.STROKE,
  lineWidth: 10,
  start: 0,
  stop: Math.PI / 3
});
sector.rotation = 90;
this.canvas.addChild(sector, sectorBorder);
this.canvas.paint();

圆环

export default class Ring extends Node {
  static TYPE = {
    STROKE: 1,
    FILL: 2,
  }

  constructor(canvas, style) {
    super(canvas, style);
    this.startAngle = style.startAngle || 0;
    this.endAngle = style.endAngle || 360;
    this.longRadius = style.longRadius || 10;
    this.shortRadius = style.shortRadius || 5;
    this.type = style.type || Ring.TYPE.STROKE;
    this.lineWidth = style.lineWidth || 3;
    this.lineDash = style.lineDash || [];
  }

  draw (painter) {
    if (this.type === Ring.TYPE.STROKE) {
      painter.strokeStyle = this.color;
      painter.lineWidth = this.lineWidth;
      if (this.lineDash.length > 0) {
        painter.setLineDash(this.lineDash);
      }
    } else {
      painter.fillStyle = this.color;
    }
    // 设置渐变色
    if (this.linearGradient.length > 0) {
      const lingrad = painter.createRadialGradient(
        this.position.x,
        this.position.y,
        0,
        this.position.x,
        this.position.y,
        this.radius,
      );
      for (let i = 0; i < this.linearGradient.length; i++) {
        lingrad.addColorStop(this.linearGradient[i][0], this.linearGradient[i][1]);
      }
      if (this.type === Ring.TYPE.STROKE) {
        painter.strokeStyle = lingrad;
      } else {
        painter.fillStyle = lingrad;
      }
    }
    // 设置阴影
    if (!!this.shadowOffsetX) {
      painter.shadowOffsetX = this.shadowOffsetX;
    }
    if (!!this.shadowOffsetY) {
      painter.shadowOffsetY = this.shadowOffsetY;
    }
    if (!!this.shadowBlur) {
      painter.shadowBlur = this.shadowBlur;
    }
    if (!!this.shadowColor) {
      painter.shadowColor = this.shadowColor;
    }
    painter.beginPath();
    // 顺时针绘制长弧度
    painter.arc(
      this.position.x,
      this.position.y,
      this.longRadius,
      this.startAngle / 180 * Math.PI,
      this.endAngle / 180 * Math.PI
    );
    // 计算短弧线上的钟点
    const sx = this.position.x + this.shortRadius * Math.cos(this.endAngle / 180 * Math.PI);
    const sy = this.position.y + this.shortRadius * Math.sin(this.endAngle / 180 * Math.PI);
    // 移动到短弧线上的起始点
    painter.lineTo(sx, sy);
    // 顺时针针绘制短弧线
    painter.arc(
      this.position.x,
      this.position.y,
      this.shortRadius,
      this.endAngle / 180 * Math.PI,
      this.startAngle / 180 * Math.PI,
      true
    );
    // 计算长弧线的起始点
    const lx = this.position.x + this.longRadius * Math.cos(this.startAngle / 180 * Math.PI);
    const ly = this.position.y + this.longRadius * Math.sin( this.startAngle / 180 * Math.PI);
    console.log(lx, ly);
    painter.lineTo(lx, ly);
    painter.closePath();
    if (this.type === Ring.TYPE.STROKE) {
      painter.stroke();
    } else {
      painter.fill();
    }
  }
}

多边形

多边形为多个坐标一次组合而成的闭合区域,其源代码如下:

export default class Polygon extends Node {
  static TYPE = {
    STROKE: 1,
    FILL: 2,
  }

  constructor(canvas, style, points = []) {
    super(canvas, style);
    this.type = style.type || Polygon.TYPE.STROKE;
    this.points = points || [];
    this.lineWidth = style.lineWidth || 3;
    this.lineDash = style.lineDash || [];
  }

  draw(painter) {
    if (this.points.length === 0) {
      return;
    }
    if (this.type === Polygon.TYPE.STROKE) {
      painter.strokeStyle = this.color;
      painter.lineWidth = this.lineWidth;
      if (this.lineDash.length > 0) {
        painter.setLineDash(this.lineDash);
      }
    } else {
      painter.fillStyle = this.color;
    }
    // 设置阴影
    if (!!this.shadowOffsetX) {
      painter.shadowOffsetX = this.shadowOffsetX;
    }
    if (!!this.shadowOffsetY) {
      painter.shadowOffsetY = this.shadowOffsetY;
    }
    if (!!this.shadowBlur) {
      painter.shadowBlur = this.shadowBlur;
    }
    if (!!this.shadowColor) {
      painter.shadowColor = this.shadowColor;
    }
    painter.beginPath();
    painter.moveTo(this.points[0].x, -this.points[0].y);
    // 遍历所有的点绘制多边形
    for (let i = 1; i < this.points.length; i++) {
      painter.lineTo(this.points[i].x, -this.points[i].y);
    }
    painter.closePath();
    if (this.type === Polygon.TYPE.STROKE) {
      painter.stroke();
    } else {
      painter.fill();
    }
  }
}

文本

export default class Text extends Node{
  constructor(canvas, style) {
    super(canvas, style);
    this.text = style.text || '';
    this.font = style.font;
    this.size = style.size;
    this.color = new Color(style.color || '#000000FF');
  }

  get width() {
    // 通过measureText获取该文本的长度
    this.canvas.painter.save();
    this.canvas.painter.font = `${this.size}px ${this.font}`;
    const width = this.canvas.painter.measureText(this.text).width;
    this.canvas.painter.restore();
    return width;
  }

  get height() {
    return this.size;
  }
  
  draw(painter) {
    painter.font = `${this.size}px ${this.font}`;
    painter.fillStyle = this.color.getColor();
    painter.fillText(this.text, this.position.x / this.scaleX, this.position.y / this.scaleY);
  }
}

折线

折线是由多个坐标点组成的曲线,不同于Line由两个点组成。源代码如下:

export default class MultiLine extends Node {
  static LINE_CAP = {
    BUTT: 'butt',
    ROUND: 'round',
    SQUARE: 'square'
  }
  constructor(canvas, style, points = []) {
    super(canvas, style)
    this.points = points;
    this.lineCap = style.lineCap || MultiLine.LINE_CAP.BUTT;
    this.lineDash = style.lineDash || [];
    this.lineDashOffset = style.lineDashOffset || 0;
  }

  draw(painter) {
    if (this.points.length === 0) {
      return;
    }
    painter.beginPath();
    // 设置线框
    painter.lineWidth = this.lineWidth;
    painter.strokeStyle = this.color;
    // 设置线条末端样式
    painter.lineCap = this.lineCap;
    // 设置虚线样式
    if (this.lineDash.length > 0) {
      painter.setLineDash(this.lineDash);
      painter.lineDashOffset = this.lineDashOffset;
    }
    // 设置渐变色
    if (this.linearGradient.length > 0) {
      const lingrad = painter.createLinearGradient(this.position.x, this.position.y, this.to.x, this.to.y);
      for (let i = 0; i < this.linearGradient.length; i++) {
        lingrad.addColorStop(this.linearGradient[i][0], this.linearGradient[i][1]);
      }
      painter.strokeStyle = lingrad;
    }
    painter.moveTo(this.position.x, this.position.y);
    // 遍历各点绘制线段
    painter.translate(0, 2 * this.position.y);
    for (let i = 0; i < this.points.length; i++) {
      painter.lineTo(this.points[i].x, - this.points[i].y);
    }
    painter.stroke();
  }
}

目录

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