首页 > 代码库 > ZRender源码分析5:Shape绘图详解

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

回顾

上一篇说到:ZRender源码分析4:Painter(View层)-中,这次,来补充一下具体的shape

关于热区的边框

以圆形为例:

document.addEventListener(‘DOMContentLoaded‘, function () {	var canvasDom = document.getElementById(‘canvasId‘),	        context = canvasDom.getContext(‘2d‘);		context.lineWidth = 50;	context.arc(100, 100, 50, 0, Math.PI * 2);	context.stroke();		context.lineWidth = 1;	context.moveTo(0,100);	context.lineTo(200,100);		context.stroke();});

得到的图形如下:


arc方法中,参数分别为x,y,r,startAngle,endAngle,但是经过测量,这个圆形的总宽度不是2r(100),而是150。迷惑了很久,才明白r是圆心到边框中央的长度,而lineWidth比较小的时候,是看不出这种差别的。 如果要获得热区的宽度,那就是2 * r+ lineWidth/2 + lineWidth / 2,也就是 2 * r + lineWidth。而热区的最左端就是 x-r-lineWidth / 2,最上端就是 y-r-lineWidth / 2。这就解释了在zrender.shape.Circle类中的getRect方法。

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;}

先判断传入的style中是否有__rect这个属性,如果有直接返回,缓存,免得进行多次计算。如果brushType为stroke或者fill,确保有lineWidth,默认为1。最后根据上述算法计算出热点区域。其他图形关于lineWidth的计算都跟这个很相似,以后就不再赘述了。

关于矩形

主要看下圆角矩形的画法:

_buildRadiusPath: function(ctx, style) {    //左上、右上、右下、左下角的半径依次为r1、r2、r3、r4    //r缩写为1         相当于 [1, 1, 1, 1]    //r缩写为[1]       相当于 [1, 1, 1, 1]    //r缩写为[1, 2]    相当于 [1, 2, 1, 2]    //r缩写为[1, 2, 3] 相当于 [1, 2, 3, 2]    var x = style.x;    var y = style.y;    var width = style.width;    var height = style.height;    var r = style.radius;    var r1;     var r2;     var r3;     var r4;          if(typeof r === ‘number‘) {        r1 = r2 = r3 = r4 = r;    }    else if(r instanceof Array) {        if (r.length === 1) {            r1 = r2 = r3 = r4 = r[0];        }        else if(r.length === 2) {            r1 = r3 = r[0];            r2 = r4 = r[1];        }        else if(r.length === 3) {            r1 = r[0];            r2 = r4 = r[1];            r3 = r[2];        } else {            r1 = r[0];            r2 = r[1];            r3 = r[2];            r4 = r[3];        }    } else {        r1 = r2 = r3 = r4 = 0;    }    ctx.moveTo(x + r1, y);    ctx.lineTo(x + width - r2, y);    r2 !== 0 && ctx.quadraticCurveTo(        x + width, y, x + width, y + r2    );    ctx.lineTo(x + width, y + height - r3);    r3 !== 0 && ctx.quadraticCurveTo(        x + width, y + height, x + width - r3, y + height    );    ctx.lineTo(x + r4, y + height);    r4 !== 0 && ctx.quadraticCurveTo(        x, y + height, x, y + height - r4    );    ctx.lineTo(x, y + r1);    r1 !== 0 && ctx.quadraticCurveTo(x, y, x + r1, y);},
  • zrender中圆角矩形是用二次贝塞尔曲线画的,关于二次贝塞尔请看 HTML5 canvas quadraticCurveTo() 方法
  • 还有一种是可以用arcTo方法,请看 html5 Canvas画图10:圆角矩形
  • 确定各个边角上的圆角半径,顺序为左上,右上,右下,坐下,这样兼容比较灵活。
  • 这里只举例说明前三句,其他都是同理。a.将当前点移动到左上角的右边(加上r1)。b.画出顶部的线 c.用二次贝塞尔曲线画出圆角,如下图所示
  • 在API中,没有公布圆角矩形的功能(为什么呢)。但是我们可以这样用:
    // 矩形var RectangleShape = require(‘zrender/shape/Rectangle‘);zr.addShape(new RectangleShape({    style : {        x : 100,        y : 100,        width : 100,        height : 50,        color : ‘rgba(135, 206, 250, 0.8)‘,        text:‘rectangle‘,        textPosition:‘inside‘,        radius: [1,2,3,4]    },    draggable : true}));zr.render();

关于椭圆

椭圆的画法有多种,请看这里 在HTML5的Canvas上绘制椭圆的几种方法,zrender用的是三次贝塞尔曲线法二

关于虚线

如果是实线(solid),直接moveTo,lineTo就搞定了,那虚线怎么画呢?看这里: HTML5 Canvas自定义圆角矩形与虚线(Rounded Rectangle and Dash Line)。zrender中将线的类型分为3种,solid(默认),dashed(虚线),dotted(点线)。 其实虚线和点线性质是一样的,只是线长不一样罢了。

// zrender.shape.LinebuildPath : function(ctx, style) {    if (!style.lineType || style.lineType == ‘solid‘) {        //默认为实线        ctx.moveTo(style.xStart, style.yStart);        ctx.lineTo(style.xEnd, style.yEnd);    }    else if (style.lineType == ‘dashed‘            || style.lineType == ‘dotted‘    ) {        var dashLength =(style.lineWidth || 1)                           * (style.lineType == ‘dashed‘ ? 5 : 1);        dashedLineTo(            ctx,            style.xStart, style.yStart,            style.xEnd, style.yEnd,            dashLength        );    }}// zrender.util.dashedLineTo/** * 虚线lineTo  */return function (ctx, x1, y1, x2, y2, dashLength) {    dashLength = typeof dashLength != ‘number‘                    ? 5                     : dashLength;    var deltaX = x2 - x1;    var deltaY = y2 - y1;    var numDashes = Math.floor(        Math.sqrt(deltaX * deltaX + deltaY * deltaY) / dashLength    );    for (var i = 0; i < numDashes; ++i) {        ctx[i % 2 ? ‘lineTo‘ : ‘moveTo‘](            x1 + (deltaX / numDashes) * i,            y1 + (deltaY / numDashes) * i        );    }    ctx.lineTo(x2, y2);};

可以看到,dashed和dotted的区别就只有一个dashLength(5或者1,不太灵活吧,不能自定义哦),实现思路也很明确:先计算出线的长度(勾股定理),然后计算一共分为多少段,最后用moveTo和lineTo一直画,就行了。

关于图片

brush : function(ctx, isHighlight, refresh) {    var style = this.style || {};    if (isHighlight) {        // 根据style扩展默认高亮样式        style = this.getHighlightStyle(            style, this.highlightStyle || {}        );    }    var image = style.image;    var me = this;    if (typeof(image) === ‘string‘) {        var src = http://www.mamicode.com/image;>
  • ImageShape覆盖了父类的buildPath和brush方法,其中buildPath用于判断hover时调用isPointInPath,由于Image特殊,所以覆盖了brush方法
  • ImageShape的style.image 可以配置一个string或者ImageElement对象,这就要分情况处理了,其中,_cache为缓存,提高效率
  • 如果Image是个字符串,则new Image()注册onload事件,在onload回调中执行refresh方法(其实是执行了painter.update方法),update又会执行brush动作,再次进去该方法
  • 判断图片是否已经加载完成,如果没完成,说明image传入的是字符串,并且为第一次进入方法,如果image是字符串第二次进入或者image传入的是DOM对象,继续向下执行
  • 其他代码同Base.js,不同的是调用了drawImage的多重重载,如果没有设置图片的宽高,直接取真实的宽高。

关于文字

先看getRect:

/** * 返回矩形区域,用于局部刷新和文字定位 * @param {Object} style */getRect : function(style) {    if (style.__rect) {        return style.__rect;    }        var width = area.getTextWidth(style.text, style.textFont);    var height = area.getTextHeight(style.text, style.textFont);        var textX = style.x;                 //默认start == left    if (style.textAlign == ‘end‘ || style.textAlign == ‘right‘) {        textX -= width;    }    else if (style.textAlign == ‘center‘) {        textX -= (width / 2);    }    var textY;    if (style.textBaseline == ‘top‘) {        textY = style.y;    }    else if (style.textBaseline == ‘bottom‘) {        textY = style.y - height;    }    else {        // middle        textY = style.y - height / 2;    }    style.__rect = {        x : textX,        y : textY,        width : width,        height : height    };        return style.__rect;}

为了更好地理解,进行如下测试

zr.addShape(new LineShape({	style:	{		xStart: 0,		yStart: 100,		xEnd: 300,		yEnd: 100,		strokeColor: ‘black‘,		lineWidth: 1	}}));zr.addShape(new LineShape({	style:	{		xStart: 100,		yStart: 0,		xEnd: 100,		yEnd: 300,		strokeColor: ‘black‘,		lineWidth: 1	}}));zr.addShape(new TextShape({	style:	{		x: 100,		y: 100,		color: ‘red‘,		text: ‘Align:right;\nBaseline:bottom‘,		textAlign: ‘right‘,		textBaseline: ‘bottom‘	},	hoverable: true,	zlevel: 2}));zr.addShape(new TextShape({	style:	{		x: 100,		y: 100,		color: ‘red‘,		text: ‘Align:right;\nBaseline:top‘,		textAlign: ‘right‘,		textBaseline: ‘top‘	},	hoverable: true,	zlevel: 2}));zr.addShape(new TextShape({	style:	{		x: 100,		y: 100,		color: ‘red‘,		text: ‘Align:left;\nBaseline:bottom‘,		textAlign: ‘left‘,		textBaseline: ‘bottom‘	},	hoverable: true,	zlevel: 2}));zr.addShape(new TextShape({	style:	{		x: 100,		y: 100,		color: ‘red‘,		text: ‘Align:left;\nBaseline:top‘,		textAlign: ‘left‘,		textBaseline: ‘top‘	},	hoverable: true,	zlevel: 2}));zr.render();

效果如下:
可见,x,y只是一个基准点,并不是左上角的点。所以在getRect中需要重新计算热区。

  • 通过area.getTextWidth和area.getTextHeight得到文字所占的宽高,这两个方法在以前有讲解。
  • 通过textAlign和textBaseline计算出文字左上角的x和y
  • 返回x/y/width/height

TextShape依旧覆盖了Base类的brush方法,如下:

brush : function(ctx, isHighlight) {    var style = this.style;    if (isHighlight) {        // 根据style扩展默认高亮样式        style = this.getHighlightStyle(            style, this.highlightStyle || {}        );    }        if (typeof style.text == ‘undefined‘) {        return;    }    ctx.save();    this.setContext(ctx, style);    // 设置transform    this.updateTransform(ctx);    if (style.textFont) {        ctx.font = style.textFont;    }    ctx.textAlign = style.textAlign || ‘start‘;    ctx.textBaseline = style.textBaseline || ‘middle‘;    var text = (style.text + ‘‘).split(‘\n‘);    var lineHeight = area.getTextHeight(‘国‘, style.textFont);    var rect = this.getRect(style);    var x = style.x;    var y;    if (style.textBaseline == ‘top‘) {        y = rect.y;    }    else if (style.textBaseline == ‘bottom‘) {        y = rect.y + lineHeight;    }    else {        y = rect.y + lineHeight / 2;    }        for (var i = 0, l = text.length; i < l; i++) {        if (style.maxWidth) {            switch (style.brushType) {                case ‘fill‘:                    ctx.fillText(                        text[i],                        x, y, style.maxWidth                    );                    break;                case ‘stroke‘:                    ctx.strokeText(                        text[i],                        x, y, style.maxWidth                    );                    break;                case ‘both‘:                    ctx.fillText(                        text[i],                        x, y, style.maxWidth                    );                    ctx.strokeText(                        text[i],                        x, y, style.maxWidth                    );                    break;                default:                    ctx.fillText(                        text[i],                        x, y, style.maxWidth                    );            }        }        else{            switch (style.brushType) {                case ‘fill‘:                    ctx.fillText(text[i], x, y);                    break;                case ‘stroke‘:                    ctx.strokeText(text[i], x, y);                    break;                case ‘both‘:                    ctx.fillText(text[i], x, y);                    ctx.strokeText(text[i], x, y);                    break;                default:                    ctx.fillText(text[i], x, y);            }        }        y += lineHeight;    }    ctx.restore();    return;},
  • brush方法与Base.brush方法大致相同,这里只说不同的。如果textAlign和textBaseline没有赋值,给予默认值
  • 关于fillText和strokeText,请看 HTML5 canvas fillText() 方法 和 HTML5 canvas strokeText() 方法,注意:这两个方法是可以传入maxWidth的
  • fillText或者strokeText时的x取得是style.x,因为text可能有多行,所以传入fillText中的y需要进行重新计算(根据textBaseline和rect.y和行高)
  • 将text根据\n(换行)分隔成数组,遍历进行绘图,每个遍历最后是将y加上lineHeight,以实现多行。

关于圆环

buildPath : function(ctx, style) {    // 非零环绕填充优化    ctx.arc(style.x, style.y, style.r, 0, Math.PI * 2, false);    ctx.moveTo(style.x + style.r0, style.y);    ctx.arc(style.x, style.y, style.r0, 0, Math.PI * 2, true);    return;},
  • 关于非零环绕规则,请看 非零环绕数规则和奇-偶规则
  • 想要画出圆环,两次arc的调用的最后一个参数一定不能相同,关于arc,请看 HTML5 canvas arc() 方法

关于贝塞尔曲线、心形、水滴

  • 分为二次贝塞尔曲线和三次贝塞尔曲线 请看:HTML5 canvas quadraticCurveTo() 方法 和 HTML5 canvas bezierCurveTo() 方法
  • zrender只是将二次和三次统一到一个图形里面做了封装,getRect也很简单,只是取这些个点的最大值与最小值进行计算,其他没什么特别之处,不贴代码了。
  • 心形(Heart)和水滴(Droplet)都是贝塞尔曲线绘制而成,不分析了就。

关于玫瑰线

请参考如下3个链接,太不常用了,不细细分析了。

  • http://xuxzmail.blog.163.com/blog/static/251319162009739563225/
  • http://en.wikipedia.org/wiki/Rose_(mathematics)
  • https://github.com/shimobayashi/rose-curve-canvas/blob/master/index.html

总结

剩余折线,多边形,正多边形,路径,扇形,五角星,内外旋轮曲线,下次再说。

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