2021SC@SDUSC山东大学软件学院软件工程应用与实践--quark renderer代码分析 第十二篇 绘画系统分析(3):连线(line)

这是绘画系统的第撒个大部分,连线,连线的内容包括最上层的链接线抽象类,和细分的直线,折线,贝塞尔曲线以及内外旋轮曲线五个部分。

首先是连接线抽象类。

连接线抽象类,需要成为连接线的类都可以混入此抽象类的实现。为了方便实现一些复杂的连接场景,特别注意:
1.连接线总是画在全局坐标系中。
2.连接线可以移动位置,但不能缩放、旋转、斜切。
3.连接线只有两个端点,即使是折线,也是两个端点,不会有更多。
4.连线不属于任何分组。
5.混入此实现的类默认假定已经混入了 Eventful ,因为我们需要事件系统。

function CableLike() {
  this.isCable = false;
  this.showLinkHooks = false;
  this.startBounding = null; // 起始形状的边界矩形

  this.endBounding = null; // 结束形状的边界矩形

  this.arrowType = 'both'; // 结束,开始,两者都有

  this.arrowAngel = Math.PI / 8;
  this.arrowLength = 10;
  this.fromId = ''; // 开始元素的ID。

  this.toId = ''; // 结束元素的ID。

  this.fromPosition = ''; // 起始元素的连接位置

  this.toPosition = ''; // 结束元素的连接位置

  this.startHook = new LinkHook({
    el: this,
    name: 'START'
  });
  this.endHook = new LinkHook({
    el: this,
    name: 'END'
  });
  this.on('afterRender', this.afterRenderHandler, this);
}

这里是对连线的基本属性的定义过程,其中加了注释,包括了起始和结束的边界形状,箭头式的情况下是起始还是结束还是两者都有箭头,起始和结束的元素以及两者的位置。

后面大段的定义了不同方式的调用以及渲染方式,这里就不多作展示了。

__renderArrow: function __renderArrow(twoPoints) {
    var _this$ctx;

    var p1 = twoPoints[0];
    var p2 = twoPoints[1]; 

    p2[0] = p2[0] - p1[0];
    p2[1] = p2[1] - p1[1]; 

    var cosp2 = matrixUtil.cosx.apply(matrixUtil, _toConsumableArray(p2));
    var sinp2 = matrixUtil.sinx.apply(matrixUtil, _toConsumableArray(p2));
    var cosArrow = mathCos(this.arrowAngel);
    var sinArrow = mathSin(this.arrowAngel);
    var x1 = this.arrowLength * (cosp2 * cosArrow - sinp2 * sinArrow);
    var y1 = this.arrowLength * (sinp2 * cosArrow + cosp2 * sinArrow);
    var x2 = this.arrowLength * (cosp2 * cosArrow + sinp2 * sinArrow);
    var y2 = this.arrowLength * (sinp2 * cosArrow - cosp2 * sinArrow); 

    x1 += p1[0];
    y1 += p1[1];
    x2 += p1[0];
    y2 += p1[1]; 

    this.ctx.save();
    this.ctx.strokeStyle = this.style.stroke;
    this.ctx.fillStyle = this.style.stroke;
    this.ctx.beginPath();

    (_this$ctx = this.ctx).moveTo.apply(_this$ctx, _toConsumableArray(p1));

    this.ctx.lineTo(x1, y1);
    this.ctx.lineTo(x2, y2);
    this.ctx.closePath();
    this.ctx.stroke();
    this.ctx.fill();
    this.ctx.restore();
  },

这是连线的过程,大体上可以分为四步:

1.将原点移动到终点

2.求出变化的角度

3.将原点移回(0,0)

4.利用刚才的数据画出路径,期间会利用返回参数的不同调用不同的函数。

接着就是最有代表性的直线绘制

直线。所有线条的特征是:

1.线条可以移动位置,但不能进行其它仿射变换,scale/rotate/skew 都不可以。所以计算线条的相关参数时可以做简化,只要计算 position 和 translate 就可以了。

2.线条总是画在全局空间中。

3.线条不能加到 Group 中。

首先是构造线条的函数

function Line(options) {
    var _this;

    _classCallCheck(this, Line);

    _this = _super.call(this, dataUtil.merge({
      shape: {
        x1: 0,
        y1: 0,
        x2: 0,
        y2: 0,
        percent: 1
      },
      style: {
        stroke: '#000',
        fill: null
      }
    }, options, true));
   
    _this.type = 'line';
    classUtil.inheritProperties(_assertThisInitialized(_this), CableLike, _this.options);
    classUtil.copyOwnProperties(_assertThisInitialized(_this), _this.options, ['style', 'shape']);
    _this.transformable = false;
    return _this;
  }

在获得来自前面抽象类的信息后会开始进行这一内容调用,通过规定线的开始结束百分比等数据来构造线段,百分比用于后期获取线上特定点。

_createClass(Line, [{
    key: "buildPath",
    value: function buildPath(ctx, shape) {
      var x1;
      var y1;
      var x2;
      var y2;

      if (this.subPixelOptimize) {
        var subPixelOptimizeOutputShape = {};
        subPixelOptimizeLine(subPixelOptimizeOutputShape, shape, this.style);
        x1 = subPixelOptimizeOutputShape.x1;
        y1 = subPixelOptimizeOutputShape.y1;
        x2 = subPixelOptimizeOutputShape.x2;
        y2 = subPixelOptimizeOutputShape.y2;
      } else {
        x1 = shape.x1;
        y1 = shape.y1;
        x2 = shape.x2;
        y2 = shape.y2;
      }

      var percent = shape.percent;

      if (percent === 0) {
        return;
      }

      ctx.moveTo(x1, y1);

      if (percent < 1) {
        x2 = x1 * (1 - percent) + x2 * percent;
        y2 = y1 * (1 - percent) + y2 * percent;
      }

      ctx.lineTo(x2, y2);
    }

这里就是构建当前线条的路径,数据结构类似前面讲过的SVG中的 path 属性,方法也大同小异

key: "renderTransformControls",
    value: function renderTransformControls(ctx, prevEl) {}

 这里是关键,也是比较特殊的地方,其作用为禁用 Elemnet 类上的 renderTransformControls 方法,因为我们不希望在线条上使用几何变换。

后面获取百分比位置的点的功能与前面几何中的线内容极为相似,就不多说了。

后面主要就是几种不同的线,构建的基本方法是一样的,思路也完全相同,只是因为线的特征而算法不同。

其中内外旋轮类曲线可分为:外切外摆线、渐开线、内切外摆线、内摆线和直滚摆线。该分类的规律是:

(1)从外切外摆线到渐开线是滚圆曲率半径由小变无限大;

(2)从渐开线到内切外摆线是滚圆曲率改变方向

(3)从内切外摆线到内摆线是滚圆曲率半径由大变到小于基圆半径;

(4)从内摆线到直滚摆线是基圆曲率半径由小变无限大。

从这个变化规律可以看出摆线和渐开线是性质上相同的曲线,可以把它们归入一族,将它们取名为摆线族曲线。

而贝塞尔曲线由线段与节点组成,节点是可拖动的支点,线段像可伸缩的皮筋,我们在绘图工具上看到的钢笔工具就是来做这种矢量曲线的。详细内容感兴趣的可以自己去查一下,我在这里三言两语也说不清楚。

总之连线部分的内容就这么多了。

上一篇:Solon Web 开发,二、开发知识准备


下一篇:68 - I. 二叉搜索树的最近公共祖先(迭代 / 递归,清晰图解)