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.Line
buildPath : 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 = image;
        if (_cache[src]) {
            image = _cache[src];
        }
        else {
            image = new Image();//document.createElement(‘image‘);
            image.onload = function(){
                image.onload = null;
                clearTimeout( _refreshTimeout );
                _needsRefresh.push( me );
                // 防止因为缓存短时间内触发多次onload事件
                _refreshTimeout = setTimeout(function(){
                    refresh && refresh( _needsRefresh );
                    // 清空needsRefresh
                    _needsRefresh = [];
                }, 10);
            };
            _cache[ src ] = image;

            image.src = src;
        }
    }
    if (image) {
        //图片已经加载完成
        if (window.ActiveXObject) {
            if (image.readyState != ‘complete‘) {
                return;
            }
        }
        else {
            if (!image.complete) {
                return;
            }
        }

        ctx.save();
        this.setContext(ctx, style);

        // 设置transform
        this.updateTransform(ctx);

        var width = style.width || image.width;
        var height = style.height || image.height;
        var x = style.x;
        var y = style.y;
        if (style.sWidth && style.sHeight) {
            var sx = style.sx || 0;
            var sy = style.sy || 0;
            ctx.drawImage(
                image,
                sx, sy, style.sWidth, style.sHeight,
                x, y, width, height
            );
        }
        else if (style.sx && style.sy) {
            var sx = style.sx;
            var sy = style.sy;
            var sWidth = width - sx;
            var sHeight = height - sy;
            ctx.drawImage(
                image,
                sx, sy, sWidth, sHeight,
                x, y, width, height
            );
        }
        else {
            ctx.drawImage(image, x, y, width, height);
        }
        // 如果没设置宽和高的话自动根据图片宽高设置
        style.width = width;
        style.height = height;
        this.style.width = width;
        this.style.height = height;

        if (style.text) {
            this.drawText(ctx, style, this.style);
        }

        ctx.restore();
    }
},

/**
 * 创建路径,用于判断hover时调用isPointInPath~
 * @param {Context2D} ctx Canvas 2D上下文
 * @param {Object} style 样式
 */
buildPath : function(ctx, style) {
    ctx.rect(style.x, style.y, style.width, style.height);
    return;
},
  • 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;
},

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

  • 分为二次贝塞尔曲线和三次贝塞尔曲线 请看: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

总结

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

时间: 2024-10-06 17:24:53

ZRender源码分析5:Shape绘图详解的相关文章

android源码分析 android toast使用详解 toast自定义

在安卓开发过程中,toast使我们经常使用的一个类,当我们需要向用户传达一些信息,但是不需要和用户交互时,该方式就是一种十分恰当的途径. 我们习惯了这样使用toast:Toast.makeText(Context context, String info, int duration).show();该方法是 系统为我们提供的一个方便的创建toast对象的静态方法,其内部依然是调用toast的相关方法完成.下面 就从其源码对该类的实现做一个分析 在toast类中,最重要的用于显示该toast的sh

memcached源码分析-----memcached启动参数详解以及关键配置的默认值

转载请注明出处: http://blog.csdn.net/luotuo44/article/details/42672913 本文开启本系列博文的代码分析.本系列博文研究是memcached版本是1.4.21. 本文将给出memcached启动时各个参数的详细解释以及一些关键配置的默认值.以便在分析memcached源码的时候好随时查看.当然也方便使用memcached时可以随时查看各个参数的含义.<如何阅读memcached源码>说到memcached有很多全局变量(也就是关键配置),这些

MapReduce阶段源码分析以及shuffle过程详解

MapReducer工作流程图: 1. MapReduce阶段源码分析 1)客户端提交源码分析 解释:   - 判断是否打印日志   - 判断是否使用新的API,检查连接   - 在检查连接时,检查输入输出路径,计算切片,将jar.配置文件复制到HDFS   - 计算切片时,计算最小切片数(默认为1,可自定义)和最大切片数(默认是long的最大值,可以自定义)   - 查看给定的是否是文件,如果是否目录计算目录下所有文件的切片   - 通过block大小和最小切片数.最大切片数计算出切片大小  

Vue.js 源码分析(十) ref属性详解

用法 ref 被用来给元素或子组件注册引用信息.引用信息将会注册在父组件的 $refs 对象上.如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素:如果用在子组件上,引用就指向组件实例,例如: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <scrip

Laravel源码分析--Laravel生命周期详解

一.XDEBUG调试 这里我们需要用到php的 xdebug 拓展,所以需要小伙伴们自己去装一下,因为我这里用的是docker,所以就简单介绍下在docker中使用xdebug的注意点. 1.在phpstorm中的 Perferences >> Languages & Framework >> PHP >> debug >> DBGp Proxy 中的Host填写的是宿主机的IP地址.可以在命令行中使用ifconfig / ipconfig查看你的本

spring源码分析之spring-web http详解

spring-web是spring webmvc的基础,它的功能如下: 1. 封装http协议中client端/server端的request请求和response响应及格式的转换,如json,rss,xml等. 2. 远程调用包括jaxws.caucho.httpinvoker 3. web相关的accept/bind/client/context/filter/jsf/method/multipart 先从http协议中的封装来看: 先了解一下http的基础:(来自:http://blog.

Hadoop1.x源码分析一:Configuration 详解

1.Hadoop配置文件的形式 Hadoop的配置文件是以XML的形式,跟元素是configuration,一般只包含子元素property.每一个property元素就是一个配置项,配置文件不支持分层或分级.每个配置项一般包括配置属性的名称name.值value和一个关于配置项的描述description;元素final和java中的关键字final类似,意味着这个配置项是"固定不变的".final一般不出现,但在合并资源的时候,可以防止配置项的值被覆盖. 在Configuratio

spring源码分析之spring-jdbc模块详解

0 概述 Spring将替我们完成所有使用JDBC API进行开发的单调乏味的.底层细节处理工作.下表描述了哪些是spring帮助我们做好的,哪些是我们要做的. Action  Spring  You Define connection parameters.    X Open the connection.  X   Specify the SQL statement.    X Declare parameters and provide parameter values   X Prep

spring源码分析之spring-messaging模块详解

0 概述 spring-messaging模块为集成messaging api和消息协议提供支持. 其代码结构为: 其中base定义了消息Message(MessageHeader和body).消息处理MessageHandler.发送消息MessageChannel. 1. base模块 其结构如下所示: 其中: message由两部分组成, MessageHandler是一个处理消息的约定,spring messaging提供了丰富的消息处理方式. MessageChannel表现为pipe

spring源码分析之spring-jms模块详解

0 概述 spring提供了一个jms集成框架,这个框架如spring 集成jdbc api一样,简化了jms api的使用. jms可以简单的分成两个功能区,消息的生产和消息的消费.JmsTemplate类用来生成消息和同步接受消息.和其它java ee的消息驱动样式一样,对异步消息,spring也提供了许多消息监听容器用来创建消息驱动的POJO(MDPs).spring同时也提供了创建消息监听器的声明方式. org.springframework.jms.core 提供了使用JMS的核心功能