回顾

上一篇说到:ZRender源码分析3:Painter(View层)-上,接上篇,开始Shape对象

总体理解

先回到上次的Painter的render方法

/**
* 首次绘图,创建各种dom和context
* 核心方法,zr.render() --> painter.render
*
* render和refersh的区别:render是clear所有,refresh是清除已经改变的layer
*
* @param {Function=} callback 绘画结束后的回调函数
*/
Painter.prototype.render = function (callback) {
//省略
//升序遍历,shape上的zlevel指定绘画图层的z轴层叠
this.storage.iterShape(
this._brush({ all : true }),
{ normal: 'up' }
); //省略
return this;
};
/**
* 刷画图形
*
* @private
* @param {Object} changedZlevel 需要更新的zlevel索引
*/
Painter.prototype._brush = function (changedZlevel) {
var ctxList = this._ctxList;
var me = this;
function updatePainter(shapeList, callback) {
me.update(shapeList, callback);
} return function(shape) {
if ((changedZlevel.all || changedZlevel[shape.zlevel])
&& !shape.invisible
) {
var ctx = ctxList[shape.zlevel];
if (ctx) {
if (!shape.onbrush //没有onbrush
//有onbrush并且调用执行返回false或undefined则继续粉刷
|| (shape.onbrush && !shape.onbrush(ctx, false))
) {
if (config.catchBrushException) {
try {
shape.brush(ctx, false, updatePainter);
}
catch(error) {
log(
error,
'brush error of ' + shape.type,
shape
);
}
}
else {
shape.brush(ctx, false, updatePainter);
}
}
}
else {
log(
'can not find the specific zlevel canvas!'
);
}
}
};
};

可以看到,在最核心处,便是调用了storage的遍历shape对象方法,传入的回调便是Painter._brush方法, 逻辑转入到_brush方法,这里返回一个回调,在回调中,直接调用了shape对象的brush方法,可见,最后还是要到shape对象中去了。

Shape对象

打开zrender的shape文件夹,可以看到,有很多个JS,其中,Base类是一个基类,而其他的文件都各自是一个图形类,都继承自Base类。 很明确的是,这里用的是一个模板方法,接下来,用最简单的Circle类来分析源码。先看Circle的结构。

function Circle(options) {
Base.call(this, options);
}
Circle.prototype = {
type: 'circle',
/**
* 创建圆形路径
* @param {Context2D} ctx Canvas 2D上下文
* @param {Object} style 样式
*/
buildPath : function (ctx, style) { //省略实现
}, /**
* 返回矩形区域,用于局部刷新和文字定位
* @param {Object} style
*/
getRect : function (style) { //省略实现
}
}; require('../tool/util').inherits(Circle, Base);

最后一行比较重要,继承了Base类,而Base类实现了brush方法,看见Circle实现的buildPath和getRect方法和type属性,应该就是覆盖了Base类的同名方法吧。 来看Base类,依旧是function Base() {} Base.prototype.baba = funciton () {},构造中先设置了一些默认值,然后用用户自定义的option进行覆盖。

 function Base( options ) {
this.id = options.id || guid();
this.zlevel = 0;
this.draggable = false;
this.clickable = false;
this.hoverable = true;
this.position = [0, 0];
this.rotation = [0, 0, 0];
this.scale = [1, 1, 0, 0]; for ( var key in options ) {
this[ key ] = options[ key ];
} this.style = this.style || {};
}

再来看核心方法brush

/**
* 画刷
*
* @param ctx 画布句柄
* @param isHighlight 是否为高亮状态
* @param updateCallback 需要异步加载资源的shape可以通过这个callback(e)
* 让painter更新视图,base.brush没用,需要的话重载brush
*/
Base.prototype.brush = function (ctx, isHighlight) {
var style = this.style; //比如LineShape,配置的有brushTypeOnly
if (this.brushTypeOnly) {
style.brushType = this.brushTypeOnly;
} if (isHighlight) {
// 根据style扩展默认高亮样式
style = this.getHighlightStyle(
style,
this.highlightStyle || {},
this.brushTypeOnly
);
} if (this.brushTypeOnly == 'stroke') {
style.strokeColor = style.strokeColor || style.color;
} ctx.save(); //根据style设置content对象
this.setContext(ctx, style); // 设置transform
this.updateTransform(ctx); ctx.beginPath();
this.buildPath(ctx, style);
if (this.brushTypeOnly != 'stroke') {
ctx.closePath();
} switch (style.brushType) {
case 'both':
ctx.fill();
case 'stroke':
style.lineWidth > 0 && ctx.stroke();
break;
default:
ctx.fill();
} if (style.text) {
this.drawText(ctx, style, this.style);
} ctx.restore();
};
  • 1.设置brushTypeOnly,brushType有三种形式:both,stroke,fill。比如在LineShape对象中,划线是不可能fill的,只能是stroke,所以由此特殊处理
  • 2.根据当前shape的style来获取适合的highlightStyle,转入到getHighlightStyle。
    /**
    * 根据默认样式扩展高亮样式
    *
    * @param ctx Canvas 2D上下文
    * @param {Object} style 默认样式
    * @param {Object} highlightStyle 高亮样式
    */
    Base.prototype.getHighlightStyle = function (style, highlightStyle, brushTypeOnly) {
    var newStyle = {};
    for (var k in style) {
    newStyle[k] = style[k];
    } var color = require('../tool/color');
    var highlightColor = color.getHighlightColor(); // rgba(255,255.0.0.5) 半透明黄色
    // 根据highlightStyle扩展
    if (style.brushType != 'stroke') {
    // 带填充则用高亮色加粗边线
    newStyle.strokeColor = highlightColor;
    newStyle.lineWidth = (style.lineWidth || 1)
    + this.getHighlightZoom(); //如果是文字,就是6,如果不是文字,是2
    newStyle.brushType = 'both'; //如果高亮层并且brushType为both或者fill,强制其为both
    }
    else {
    if (brushTypeOnly != 'stroke') {
    // 描边型的则用原色加工高亮
    newStyle.strokeColor = highlightColor;
    newStyle.lineWidth = (style.lineWidth || 1)
    + this.getHighlightZoom();
    }
    else {
    // 线型的则用原色加工高亮
    newStyle.strokeColor = highlightStyle.strokeColor
    || color.mix(
    style.strokeColor,
    color.toRGB(highlightColor)
    );
    }
    } // 可自定义覆盖默认值
    for (var k in highlightStyle) {
    if (typeof highlightStyle[k] != 'undefined') {
    newStyle[k] = highlightStyle[k];
    }
    } return newStyle;
    };
    • 先将默认的样式拷贝到newStyle变量中,在方法末尾,返回newStyle
    • 根据默认的样式计算出高亮的样式,如果brushType为both或者fill,将strokeColor变成半透明的黄色,根据图形类型算出lineWidth,将brushType赋值为both
    • 如果brushType为stroke,再如果brushOnly没有被设置为stroke,将strokeCOlor设置为半透明黄色,设置lineWidth
    • 如果brushType为stroke,没有设置brushOnly为stroke,就用color.mix计算出一个颜色值
    • 最后将用户自定义的highlightStyle覆盖到newStyle,返回newStyle
  • 如果brushTypeOnly为stroke,处理color的多个出处,然后就是ctx.save()与ctx.restore()之间的真正绘图了。
  • 转到setContext方法
    var STYLE_CTX_MAP = [
    ['color', 'fillStyle'],
    ['strokeColor', 'strokeStyle'],
    ['opacity', 'globalAlpha'],
    ['lineCap'],
    ['lineJoin'],
    ['miterLimit'],
    ['lineWidth'],
    ['shadowBlur'],
    ['shadowColor'],
    ['shadowOffsetX'],
    ['shadowOffsetY']
    ]; /**
    * 画布通用设置
    *
    * @param ctx 画布句柄
    * @param style 通用样式
    */
    Base.prototype.setContext = function (ctx, style) {
    for (var i = 0, len = STYLE_CTX_MAP.length; i < len; i++) {
    var styleProp = STYLE_CTX_MAP[i][0];
    var styleValue = style[styleProp];
    var ctxProp = STYLE_CTX_MAP[i][1] || styleProp; if (typeof styleValue != 'undefined') {
    ctx[ctxProp] = styleValue;
    }
    }
    };

    在原生的context赋值样式时,都是context.fillStyle = '#aaa'; 但是经过zrender的抽象变得更加的易用,setContext就是负责原生canvasAPI与zrender.shape.style的转换, 其实有变化的就只有fillStyle,strokeStyle,globalAlpha。分别用style.color,style.strokeColor,opacity进行替换,不过这些原生API的属性名确实不那么平易近人。

  • 关于变形,暂时跳过
  • 开始beginPath,然后调用Base.buildPath,发现Base中没有buildPath的实现,上面说了嘛,在子类实现了,模板方法。下面举例 进行buildPath的分析
    // shape/Circle.js
    
    /**
    * 创建圆形路径
    * @param {Context2D} ctx Canvas 2D上下文
    * @param {Object} style 样式
    */
    buildPath : function (ctx, style) {
    ctx.arc(style.x, style.y, style.r, 0, Math.PI * 2, true);
    return;
    }, //shape/Rectangle
    /**
    * 创建矩形路径
    * @param {Context2D} ctx Canvas 2D上下文
    * @param {Object} style 样式
    */
    buildPath : function(ctx, style) {
    if(!style.radius) {
    ctx.moveTo(style.x, style.y);
    ctx.lineTo(style.x + style.width, style.y);
    ctx.lineTo(style.x + style.width, style.y + style.height);
    ctx.lineTo(style.x, style.y + style.height);
    ctx.lineTo(style.x, style.y);
    //ctx.rect(style.x, style.y, style.width, style.height);
    } else {
    this._buildRadiusPath(ctx, style);
    }
    return;
    },

    可以看到,在Circle类的buildPath中,只有一句话,那就是真正的Canvas画图的API调用,而在Rectangle中,用moveTo和lineTo画出了一个路径出来。

  • 如果是只能划线的shape,没有必要closePath,否则colsePath,以避免图形的乱线出现,然后根据brushType的类型,进行fill和stroke,注意,第一个case没有break,所以fill和stroke可以同时进行
  • 最后,处理图形上附属的文字。
    Base.prototype.drawText = function (ctx, style, normalStyle) {
    // 字体颜色策略
    var textColor = style.textColor || style.color || style.strokeColor;
    ctx.fillStyle = textColor; /*
    if (style.textPosition == 'inside') {
    ctx.shadowColor = 'rgba(0,0,0,0)'; // 内部文字不带shadowColor
    }
    */ // 文本与图形间空白间隙
    var dd = 10;
    var al; // 文本水平对齐
    var bl; // 文本垂直对齐
    var tx; // 文本横坐标
    var ty; // 文本纵坐标 var textPosition = style.textPosition // 用户定义
    || this.textPosition // shape默认
    || 'top'; // 全局默认 switch (textPosition) {
    case 'inside':
    case 'top':
    case 'bottom':
    case 'left':
    case 'right':
    if (this.getRect) {
    var rect = (normalStyle || style).__rect
    || this.getRect(normalStyle || style); switch (textPosition) {
    case 'inside':
    tx = rect.x + rect.width / 2;
    ty = rect.y + rect.height / 2;
    al = 'center';
    bl = 'middle';
    // 如果brushType为both或者fill,那么就会有fill动作,这时,如果文字颜色跟填充颜色相同,文字就看不见了,所以把它变成白色
    // 但是,如果文字颜色是白色呢,哎,不想了,太变态
    if (style.brushType != 'stroke'
    && textColor == style.color
    ) {
    ctx.fillStyle = '#fff';
    }
    break;
    case 'left':
    tx = rect.x - dd; //间隙
    ty = rect.y + rect.height / 2;
    al = 'end';
    bl = 'middle';
    break;
    case 'right':
    tx = rect.x + rect.width + dd;
    ty = rect.y + rect.height / 2;
    al = 'start';
    bl = 'middle';
    break;
    case 'top':
    tx = rect.x + rect.width / 2;
    ty = rect.y - dd;
    al = 'center';
    bl = 'bottom';
    break;
    case 'bottom':
    tx = rect.x + rect.width / 2;
    ty = rect.y + rect.height + dd;
    al = 'center';
    bl = 'top';
    break;
    }
    }
    break;
    case 'start':
    case 'end':
    var xStart;
    var xEnd;
    var yStart;
    var yEnd;
    if (typeof style.pointList != 'undefined') {
    var pointList = style.pointList;
    if (pointList.length < 2) {
    // 少于2个点就不画了~
    return;
    }
    var length = pointList.length;
    switch (textPosition) {
    case 'start':
    xStart = pointList[0][0];
    xEnd = pointList[1][0];
    yStart = pointList[0][1];
    yEnd = pointList[1][1];
    break;
    case 'end':
    xStart = pointList[length - 2][0];
    xEnd = pointList[length - 1][0];
    yStart = pointList[length - 2][1];
    yEnd = pointList[length - 1][1];
    break;
    }
    }
    else {
    xStart = style.xStart || 0;
    xEnd = style.xEnd || 0;
    yStart = style.yStart || 0;
    yEnd = style.yEnd || 0;
    } switch (textPosition) {
    case 'start':
    al = xStart < xEnd ? 'end' : 'start';
    bl = yStart < yEnd ? 'bottom' : 'top';
    tx = xStart;
    ty = yStart;
    break;
    case 'end':
    al = xStart < xEnd ? 'start' : 'end';
    bl = yStart < yEnd ? 'top' : 'bottom';
    tx = xEnd;
    ty = yEnd;
    break;
    }
    dd -= 4;
    if (xStart != xEnd) {
    tx -= (al == 'end' ? dd : -dd);
    }
    else {
    al = 'center';
    } if (yStart != yEnd) {
    ty -= (bl == 'bottom' ? dd : -dd);
    }
    else {
    bl = 'middle';
    }
    break;
    case 'specific':
    tx = style.textX || 0;
    ty = style.textY || 0;
    al = 'start';
    bl = 'middle';
    break;
    } if (tx != null && ty != null) {
    _fillText(
    ctx,
    style.text,
    tx, ty,
    style.textFont,
    style.textAlign || al,
    style.textBaseline || bl
    );
    }
    }; // Circle.js 的getRect
    /**
    * 返回矩形区域,用于局部刷新和文字定位
    * @param {Object} style
    */
    getRect : function (style) {
    if (style.__rect) {
    return style.__rect;
    } var lineWidth;
    if (style.brushType == 'stroke' || style.brushType == 'fill') {
    lineWidth = style.lineWidth || 1;
    }
    else {
    lineWidth = 0;
    }
    style.__rect = {
    x : Math.round(style.x - style.r - lineWidth / 2),
    y : Math.round(style.y - style.r - lineWidth / 2),
    width : style.r * 2 + lineWidth,
    height : style.r * 2 + lineWidth
    }; return style.__rect;
    }
    };
    • 关于textPosition的具体设置,请移步API
    • getRect还是一个模板方法,用来获取图形所在的矩形区域。用Circle说明,通过一系列的计算,得到圆形左上角的xy坐标,获得原型的矩形宽高,返回。其中,__rect是缓存作用
    • 其中,al表示的是canvasAPI中的context.textAlign,bl指的是textBaseLine,tx,ty是文字的基准坐标,请看 http://www.w3school.com.cn/tags/canvas_textalign.asp 和 http://www.w3school.com.cn/tags/canvas_textbaseline.asp
    • 如果textPosition为inside,left,right,top,bottom(分别表示在图形的中央,左边,右边,上边,下边),根据rect的信息进行tx/ty/al/bl的赋值
    • 如果是start或者end,只有直线和折线配置这两个,同理,根据rect的信息分情况进行tx/ty/al/bl的设置
    • 最后,拿到了tx/ty/al/bl/font/text,调用真正的画图方法_fillText
      function _fillText(ctx, text, x, y, textFont, textAlign, textBaseline) {
      if (textFont) {
      ctx.font = textFont;
      }
      ctx.textAlign = textAlign;
      ctx.textBaseline = textBaseline;
      var rect = _getTextRect(
      text, x, y, textFont, textAlign, textBaseline
      ); text = (text + '').split('\n');
      var lineHeight = require('../tool/area').getTextHeight('国', textFont); switch (textBaseline) {
      case 'top':
      y = rect.y;
      break;
      case 'bottom':
      y = rect.y + lineHeight;
      break;
      default:
      y = rect.y + lineHeight / 2;
      } for (var i = 0, l = text.length; i < l; i++) {
      ctx.fillText(text[i], x, y);
      y += lineHeight;
      }
      }
      /**
      * 返回矩形区域,用于局部刷新和文字定位
      *
      * @inner
      * @param {Object} style
      */
      function _getTextRect(text, x, y, textFont, textAlign, textBaseline) {
      var area = require('../tool/area');
      var width = area.getTextWidth(text, textFont);
      var lineHeight = area.getTextHeight('国', textFont); text = (text + '').split('\n'); switch (textAlign) {
      case 'end':
      case 'right':
      x -= width;
      break;
      case 'center':
      x -= (width / 2);
      break;
      } switch (textBaseline) {
      case 'top':
      break;
      case 'bottom':
      y -= lineHeight * text.length;
      break;
      default:
      y -= lineHeight * text.length / 2;
      } return {
      x : x,
      y : y,
      width : width,
      height : lineHeight * text.length
      };
      } //以下是tool/area.js中方法 /**
      * 测算多行文本高度
      * @param {Object} text
      * @param {Object} textFont
      */
      function getTextHeight(text, textFont) {
      var key = text+':'+textFont;
      if (_textHeightCache[key]) {
      return _textHeightCache[key];
      } _ctx = _ctx || util.getContext(); _ctx.save();
      if (textFont) {
      _ctx.font = textFont;
      } text = (text + '').split('\n');
      //比较粗暴
      var height = (_ctx.measureText('国').width + 2) * text.length; _ctx.restore(); _textHeightCache[key] = height;
      if (++_textHeightCacheCounter > TEXT_CACHE_MAX) {
      // 内存释放
      _textHeightCacheCounter = 0;
      _textHeightCache = {};
      }
      return height;
      }
      /**
      * 测算多行文本宽度
      * @param {Object} text
      * @param {Object} textFont
      */
      function getTextWidth(text, textFont) {
      var key = text+':'+textFont;
      if (_textWidthCache[key]) {
      return _textWidthCache[key];
      }
      _ctx = _ctx || util.getContext();
      _ctx.save(); if (textFont) {
      _ctx.font = textFont;
      } text = (text + '').split('\n');
      var width = 0;
      for (var i = 0, l = text.length; i < l; i++) {
      width = Math.max(
      _ctx.measureText(text[i]).width,
      width
      );
      }
      _ctx.restore(); _textWidthCache[key] = width;
      if (++_textWidthCacheCounter > TEXT_CACHE_MAX) {
      // 内存释放
      _textWidthCacheCounter = 0;
      _textWidthCache = {};
      } return width;
      }
      • 先设置context的textAlign和textBaseLine
      • 关于area.getTextHeight和area.getTextWidth,主要是用了canvas的原生measureText方法,还有一个缓存技巧。关于measureText,请看 http://www.w3school.com.cn/tags/canvas_measuretext.asp
      • _getTextRect获取了需要画的问题的热点区域,仍旧返回的是x/y/width/height
      • 在_fillText,获取到热点区域后,对行高做一些特殊处理之后,调用fillText进行真真正的绘制了。
  • 至此,brush方法分析完毕。

总结

写这些东西,真是很费时间,关于变形的设置,和其他图形的详细实现,等机缘到了,再续吧。下篇将继续Painter的分析。

ZRender源码分析4:Painter(View层)-中的更多相关文章

  1. ZRender源码分析3:Painter(View层)-上

    回顾 上一篇说到:ZRender源码分析2:Storage(Model层),这次咱看来看看Painter-View层 总体理解 Painter这个类主要负责MVC中的V(View)层,负责将Stora ...

  2. ZRender源码分析2:Storage(Model层)

    回顾 上一篇请移步:zrender源码分析1:总体结构 本篇进行ZRender的MVC结构中的M进行分析 总体理解 上篇说到,Storage负责MVC层中的Model,也就是模型,对于zrender来 ...

  3. zrender源码分析3--初始化Painter绘图模块

    接上次分析到初始化ZRender的源码,这次关注绘图模块Painter的初始化 入口1:new Painter(dom, this.storage); // zrender.js /** * ZRen ...

  4. ZRender源码分析5:Shape绘图详解

    回顾 上一篇说到:ZRender源码分析4:Painter(View层)-中,这次,来补充一下具体的shape 关于热区的边框 以圆形为例: document.addEventListener('DO ...

  5. ABP源码分析四十七:ABP中的异常处理

    ABP 中异常处理的思路是很清晰的.一共五种类型的异常类. AbpInitializationException用于封装ABP初始化过程中出现的异常,只要抛出AbpInitializationExce ...

  6. ABP源码分析十五:ABP中的实用扩展方法

    类名 扩展的类型 方法名 参数 作用 XmlNodeExtensions XmlNode GetAttributeValueOrNull attributeName Gets an   attribu ...

  7. 8.源码分析---从设计模式中看SOFARPC中的EventBus?

    我们在前面分析客户端引用的时候会看到如下这段代码: // 产生开始调用事件 if (EventBus.isEnable(ClientStartInvokeEvent.class)) { EventBu ...

  8. zrender源码分析2--初始化Storage

    接上次分析到初始化ZRender的源码,这次关注内容仓库Storage的初始化 入口1:new Storage(); // zrender.js /** * ZRender接口类,对外可用的所有接口都 ...

  9. zrender源码分析1:总体结构

    开始 zrender(Zlevel Render) 是一个轻量级的Canvas类库,这里是GitHub的网址 点我, 类似的类库有Kinetic.JS.EaselJS. 但貌似都没有zrender好用 ...

随机推荐

  1. 通过一张简单的图,让你搞懂JS的==运算

    == 运算的规则: undefined == null,结果是true.且它俩与所有其他值比较的结果都是false. String == Boolean,需要两个操作数同时转为Number. Stri ...

  2. linux配置加载顺序

    linux加载配置项时通过下面方式 首先 加载/etc/profile配置 然后 加载/ect/profile.d/下面的所有脚本 然后 加载当前用户 .bash_profile 然后 加载.bash ...

  3. S - 骨牌铺方格(第二季水)

    Description          在2×n的一个长方形方格中,用一个1× 2的骨牌铺满方格,输入n ,输出铺放方案的总数.         例如n=3时,为2× 3方格,骨牌的铺放方案有三种, ...

  4. 测试工具:insure++

    CSDN资源:http://www.csdn.net/tag/insure%252B%252B 安装. 1,简介:http://baike.baidu.com/link?url=bCcoWd3xi07 ...

  5. 在vs.net c#中添加mysql模型

    http://weblogs.asp.net/gunnarpeipman/getting-mysql-work-with-entity-framework-4-0 http://dev.mysql.c ...

  6. select语句后面加上for update的作用

    Select…For Update语句的语法与select语句相同,只是在select语句的后面加FOR UPDATE [NOWAIT]子句. 该语句用来锁定特定的行(如果有where子句,就是满足w ...

  7. RMAN之一:快速入门

    1.数据导出基础 (1)创建datapump导出文件的目录对象并为相应用户授予权限. 出于安全考虑,不允许oracle用户直接在OS上进行文件的操作,而应通过directory对象指定. SQL> ...

  8. LayoutInflater的获取方式

    1.LayoutInflater是什么? 通俗而讲,就是将xml中定义的布局找出来. 2.获取LayoutInflater的三种方式   1. LayoutInflater inflater = ge ...

  9. web安全测试工具介绍---webscarab

    webscarab: 这主要是一款代理软件或许没有其它的工具能和OWASP的WebScarab如此丰富的功能相媲美了,如果非要列举一些有用的模块的话,那么他们包括HTTP代理,网络爬行.网络蜘蛛,会话 ...

  10. python操作redis-set

    #!/usr/bin/python #!coding: utf-8 import redis if __name__=="__main__": try: conn=redis.St ...