zoukankan      html  css  js  c++  java
  • ZRender源码分析6:Shape对象详解之路径

    开始

    说到这里,就不得不提SVG的路径操作了,因为ZRender完全的模拟了SVG原生的path元素的用法,很是强大。 关于SVG的Path,请看这里: Path (英文版) 或者 【MDN】SVG教程(5) 路径 [译] (中文版), 很明显的是canvas中的路径没有SVG的用着舒服,那到底ZRender是如何实现的呢,让我给你娓娓道来(不过要想继续进行下去,上面的SVG的PATH必须了解。)。

    示例

    打开API,shape.path,可以看到,path的配置有MLHVCSQTZ等字母组成的字符串,svg的path也支持小写,也有一个A命令,难道ZRender没有实现? 错,实现了,只是在API上没有写明而已,支持大小写,支持A(圆弧)命令!为了证明我所说,来个示例:

    
    require(
    [
        '../src/zrender', '../src/shape/Path'
    ], function( zrender, PathShape )
    {
    
    	var box = document.getElementById('box');
    	var zr = zrender.init(box);
    
    	zr.addShape(new PathShape(
    	{
    		style:
    		{
    			x: 0,
    			y: 0,
    			path: 'M80 80 A 45 45, 0, 0, 0, 125 125 L 125 80 Z',
    			color: '#F60',
    			textPosition: 'inside',
    			textColor: 'red',
    			strokeColor: 'black'
    		},
    		draggable: true
    	}));
    	
    	zr.addShape(new PathShape(
    	{
    		style:
    		{
    			x: 0,
    			y: 0,
    			path: 'M230 80 A 45 45, 0, 1, 0, 275 125 L 275 80 Z',
    			color: '#F60',
    			textPosition: 'inside',
    			textColor: 'red',
    			strokeColor: 'black'
    		},
    		draggable: true
    	}));
    
    	zr.render();
    });
    
    

    得到如下结果:


    再用SVG来一个相同配置的:

    <svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
        <path d="M80 80
            A 45 45, 0, 0, 0, 125 125
            L 125 80 Z" fill="#F60"/>
        <path d="M230 80
            A 45 45, 0, 1, 0, 275 125
            L 275 80 Z" fill="#F60"/>
    </svg>

    好吧,得到的结果一模一样,我就不贴图了。不多说了,这就是移植,我喜欢。

    _parsePathData

    打开zrender/shape/Path,buildPath先调用的就是_parsePathData,作用为:解析path字符串为数组命令,也就是个解析器嘛。

    
     _parsePathData : function(data) {
         if (!data) {
             return [];
         }
    
         // command string
         var cs = data;
    
         // command chars
         var cc = [
             'm', 'M', 'l', 'L', 'v', 'V', 'h', 'H', 'z', 'Z',
             'c', 'C', 'q', 'Q', 't', 'T', 's', 'S', 'a', 'A'
         ];
         
         cs = cs.replace(/-/g, ' -');// M 100 100 L 100 200 L 100-200 Z -> M 100 100 L 100 200 L 100 -200 Z
         cs = cs.replace(/  /g, ' ');// M 100 100 L 100 200 L 100 -200 -> M 100 100 L 100 200 L 100  -200 -> M 100 100 L 100 200 L 100 -200
         cs = cs.replace(/ /g, ',');// M 100 100 L 100 200 L 100 -200 -> M,100,100,L,100,200,L,100,-200
         cs = cs.replace(/,,/g, ',');//如果出现两个逗号,换成一个逗号 -> M,100,100,L,100,200,L,100,-200
         
         //cs = cs.replace(/-/g, ' -').replace(/  /g, ' ').replace(/ /g, ',').replace(/,,/g, ','); 这样写,会不会很帅气,(-
         
    
         var n;
         // create pipes so that we can split the data
         for (n = 0; n < cc.length; n++) {
             cs = cs.replace(new RegExp(cc[n], 'g'), '|' + cc[n]);
         }
         
         // |M,100,100,|L,100,200,|L,100,-200
    
         // create array
         var arr = cs.split('|'); // ['','M,100,100,','L,100,200,','L,100,-200']
         var ca = [];
         // init context point
         var cpx = 0; //cpx和cpy是循环里的全局都在使用,小写命令是累计计算,大写命令是复制计算。
         var cpy = 0;
         for (n = 1; n < arr.length; n++) { // 从1开始,因为第一个元素肯定为空
             var str = arr[n]; // M,100,100,
             var c = str.charAt(0); // M
             str = str.slice(1); //,100,100,
             str = str.replace(new RegExp('e,-', 'g'), 'e-');
    
             var p = str.split(',');// ['','100','100','']
             if (p.length > 0 && p[0] === '') {
                 p.shift();
             }
             // ['100','100','']
    
             for (var i = 0; i < p.length; i++) {
                 p[i] = parseFloat(p[i]);
             }
             
             // [100,100,NaN]
             
             while (p.length > 0) {
                 if (isNaN(p[0])) {
                     break;
                 }
                 var cmd = null;
                 var points = [];
    
                 var ctlPtx;
                 var ctlPty;
                 var prevCmd;
    
                 var rx;
                 var ry;
                 var psi;
                 var fa;
                 var fs;
    
                 var x1 = cpx;
                 var y1 = cpy;
    
                 // convert l, H, h, V, and v to L
                 switch (c) {
                 case 'l':
                     cpx += p.shift();
                     cpy += p.shift();
                     cmd = 'L';
                     points.push(cpx, cpy);
                     break;
                 case 'L':
                     cpx = p.shift();
                     cpy = p.shift();
                     points.push(cpx, cpy);
                     break;
                 //在l的时候,是直接相加的,而L的时候,是直接赋值的 ,这就说明大小写是不一样的   
                 // L 表示lineTo
                 case 'm':
                     cpx += p.shift();
                     cpy += p.shift();
                     cmd = 'M';
                     points.push(cpx, cpy);
                     c = 'l';
                     break;
                 case 'M':
                     cpx = p.shift();
                     cpy = p.shift();
                     cmd = 'M';
                     points.push(cpx, cpy);
                     c = 'L';
                     break;
    // M 表示moveTo
                 case 'h':
                     cpx += p.shift();
                     cmd = 'L';
                     points.push(cpx, cpy);
                     break;
                 case 'H':
                     cpx = p.shift();
                     cmd = 'L';
                     points.push(cpx, cpy);
                     break;
                 // H 表示水平lineTo,只改变X值
                 case 'v':
                     cpy += p.shift();
                     cmd = 'L';
                     points.push(cpx, cpy);
                     break;
                 case 'V':
                     cpy = p.shift();
                     cmd = 'L';
                     points.push(cpx, cpy);
                     break;
                 // H 表示垂直lineTo,只改变Y值
                 case 'C':
                     points.push(p.shift(), p.shift(), p.shift(), p.shift());
                     cpx = p.shift();
                     cpy = p.shift();
                     points.push(cpx, cpy);
                     break;
                 case 'c':
                     points.push(
                         cpx + p.shift(), cpy + p.shift(),
                         cpx + p.shift(), cpy + p.shift()
                     );
                     cpx += p.shift();
                     cpy += p.shift();
                     cmd = 'C';
                     points.push(cpx, cpy);
                     break;
                 // C表示二次贝塞尔曲线
                 case 'S':
                     ctlPtx = cpx;
                     ctlPty = cpy;
                     prevCmd = ca[ca.length - 1];
                     if (prevCmd.command === 'C') {
                         ctlPtx = cpx + (cpx - prevCmd.points[2]);
                         ctlPty = cpy + (cpy - prevCmd.points[3]);
                     }
                     points.push(ctlPtx, ctlPty, p.shift(), p.shift());
                     cpx = p.shift();
                     cpy = p.shift();
                     cmd = 'C';
                     points.push(cpx, cpy);
                     break;
                 case 's':
                     ctlPtx = cpx, ctlPty = cpy;
                     prevCmd = ca[ca.length - 1];
                     if (prevCmd.command === 'C') {
                         ctlPtx = cpx + (cpx - prevCmd.points[2]);
                         ctlPty = cpy + (cpy - prevCmd.points[3]);
                     }
                     points.push(
                         ctlPtx, ctlPty,
                         cpx + p.shift(), cpy + p.shift()
                     );
                     cpx += p.shift();
                     cpy += p.shift();
                     cmd = 'C';
                     points.push(cpx, cpy);
                     break;
                 // C表示光滑二次贝塞尔曲线
                 case 'Q':
                     points.push(p.shift(), p.shift());
                     cpx = p.shift();
                     cpy = p.shift();
                     points.push(cpx, cpy);
                     break;
                 case 'q':
                     points.push(cpx + p.shift(), cpy + p.shift());
                     cpx += p.shift();
                     cpy += p.shift();
                     cmd = 'Q';
                     points.push(cpx, cpy);
                     break;
                 // Q表示三次贝塞尔曲线
                 case 'T':
                     ctlPtx = cpx, ctlPty = cpy;
                     prevCmd = ca[ca.length - 1];
                     if (prevCmd.command === 'Q') {
                         ctlPtx = cpx + (cpx - prevCmd.points[0]);
                         ctlPty = cpy + (cpy - prevCmd.points[1]);
                     }
                     cpx = p.shift();
                     cpy = p.shift();
                     cmd = 'Q';
                     points.push(ctlPtx, ctlPty, cpx, cpy);
                     break;
                 case 't':
                     ctlPtx = cpx, ctlPty = cpy;
                     prevCmd = ca[ca.length - 1];
                     if (prevCmd.command === 'Q') {
                         ctlPtx = cpx + (cpx - prevCmd.points[0]);
                         ctlPty = cpy + (cpy - prevCmd.points[1]);
                     }
                     cpx += p.shift();
                     cpy += p.shift();
                     cmd = 'Q';
                     points.push(ctlPtx, ctlPty, cpx, cpy);
                     break;
                 // Q表示光滑三次贝塞尔曲线
                 case 'A':
                     rx = p.shift(); //椭圆的x轴半径
                     ry = p.shift(); //椭圆的y轴半径
                     psi = p.shift();//椭圆的旋转角度
                     fa = p.shift();//角度大小  0表示小角度,1表示大弧度
                     fs = p.shift();//弧线方向  0表示从起点到终点沿逆时针画弧,1表示从起点到终点沿顺时针画弧
    
                     x1 = cpx, y1 = cpy; //开始的点
                     cpx = p.shift(), cpy = p.shift(); //结束的点
                     cmd = 'A';
                     points = this._convertPoint(
                         x1, y1, cpx, cpy, fa, fs, rx, ry, psi
                     );
                     break;
                 case 'a':
                     rx = p.shift();
                     ry = p.shift();
                     psi = p.shift();
                     fa = p.shift();
                     fs = p.shift();
    
                     x1 = cpx, y1 = cpy;
                     cpx += p.shift();
                     cpy += p.shift();
                     cmd = 'A';
                     points = this._convertPoint(
                         x1, y1, cpx, cpy, fa, fs, rx, ry, psi
                     );
                     break;
    // A是啥玩意?
                 }
    
                 ca.push({
                     command : cmd || c,
                     points : points
                 });
             }
             
             //如果是z,z不去分大小写,直接push进入,points为空数组
             if (c === 'z' || c === 'Z') {
                 ca.push({
                     command : 'z',
                     points : []
                 });
             }
         }
    
         return ca;
     }
    
    • 如果没有data,直接返回空数组
    • 将传入的data赋值给cs,将cs进行一系列的replace(将-换成 -,将两个空格换成一个空格,将一个空格换成逗号,将两个逗号换成一个逗号),这些,都是为了兼容SVG的规法和各种不规范的写法
    • 将cs用竖线加命令字符分隔开,便于下一步进行再次分隔
    • 再用竖线将字符串变成数组,声明ca(最后所返回的值),声明cpx和cpy(绘制路径的起点,相对于下面的循环,是一个全局性质的变量)
    • 遍历arr,其中c是命令符,经过处理,最后的点坐标,被赋值到p变量上
    • 开始while循环,真正的往ca中push值,进入switch,如果是命令是大写的cpx直接被赋值为p中的点,如果是小写的,会在原来的cpx和cpy的基础上进行累加。(具体用法可以参见那篇SVG的文章)
    • 这些命令的意思在注释中已经写明,唯一需要说的是A(圆弧),这个比较复杂,需要细细体会,我就不分析了,不过也可以看这里,如果作者有回应的话。 https://github.com/ecomfe/zrender/issues/98
    • 最后返回的ca是一个数组,看下图:

    创建路径 buildPath

    
    buildPath : function(ctx, style) {
        var path = style.path;
    
        var pathArray = this.pathArray || this._parsePathData(path);
    
        // 平移坐标
        var x = style.x || 0;
        var y = style.y || 0;
    
        var p;
        // 记录边界点,用于判断inside
        var pointList = style.pointList = [];
        var singlePointList = [];
        for (var i = 0, l = pathArray.length; i < l; i++) {
            if (pathArray[i].command.toUpperCase() == 'M') { // 如果是M,说明又画了一个新的区域,就把原来的singlePointList塞入到最终结果中,再把singlePointList清空
                singlePointList.length > 0 
                && pointList.push(singlePointList);
                singlePointList = [];
            }
            p = pathArray[i].points;
            for (var j = 0, k = p.length; j < k; j += 2) { //把所有的point点塞入singlePointList
                singlePointList.push([p[j] + x, p[j+1] + y]);
            }
        }
        singlePointList.length > 0 && pointList.push(singlePointList); //如果存在点,塞入最终结果里
        
        var c;
        for (var i = 0, l = pathArray.length; i < l; i++) {
            c = pathArray[i].command;
            p = pathArray[i].points;
            // 平移变换
            for (var j = 0, k = p.length; j < k; j++) { //style.x和style.y是一个参考点
                if (j % 2 === 0) {
                    p[j] += x;
                } else {
                    p[j] += y;
                }
            }
            switch (c) {
                case 'L':
                    ctx.lineTo(p[0], p[1]);
                    break;
                case 'M':
                    ctx.moveTo(p[0], p[1]);
                    break;
                case 'C':
                    ctx.bezierCurveTo(p[0], p[1], p[2], p[3], p[4], p[5]);
                    break;
                case 'Q':
                    ctx.quadraticCurveTo(p[0], p[1], p[2], p[3]);
                    break;
                // 这几个做法就比较明显了,调用了原生CanvasAPI,但是A呢,对了,在SVG中,是弧形,
                // 文档中也不写,作者好低调,赞!
                case 'A':
                    var cx = p[0];
                    var cy = p[1];
                    var rx = p[2];
                    var ry = p[3];
                    var theta = p[4];
                    var dTheta = p[5];
                    var psi = p[6];
                    var fs = p[7];
                    var r = (rx > ry) ? rx : ry;
                    var scaleX = (rx > ry) ? 1 : rx / ry;
                    var scaleY = (rx > ry) ? ry / rx : 1;
    
                    ctx.translate(cx, cy);
                    ctx.rotate(psi);
                    ctx.scale(scaleX, scaleY);
                    ctx.arc(0, 0, r, theta, theta + dTheta, 1 - fs);
                    ctx.scale(1 / scaleX, 1 / scaleY);
                    ctx.rotate(-psi);
                    ctx.translate(-cx, -cy);
                    break;
                case 'z':
                    ctx.closePath();
                    break;
            }
        }
    
        return;
    },
    
    • xy是一个绘制路径的参考点,如果用户没有指定,这里默认为0 0
    • 记录边界点,用于判断inside,这里对M的判断主要是处理一个命令画多个区域的问题。而判断inside的作用主要是在Base类里drawText的时候用到
    • 开始遍历pathArray(即上面说的ca),style.x/style.y是一个参考点,所有的坐标都会加上这个参考点,即为平移变换。
    • 进入switch进行真正的canvas原生API绘制路径,最后碰到z,进行closePath
    • A我就不说了,没找到这个算法的相关资料,欢迎大家指导。

    热区 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;
        }
    
        var minX = Number.MAX_VALUE;
        var maxX = Number.MIN_VALUE;
    
        var minY = Number.MAX_VALUE;
        var maxY = Number.MIN_VALUE;
    
        // 平移坐标
        var x = style.x || 0;
        var y = style.y || 0;
    
        var pathArray = this.pathArray || this._parsePathData(style.path);
        for (var i = 0; i < pathArray.length; i++) {
            var p = pathArray[i].points;
    
            for (var j = 0; j < p.length; j++) {
                if (j % 2 === 0) { // 0,2,4,6,8....为x值
                    if (p[j] + x < minX) {
                        minX = p[j] + x;
                    }
                    if (p[j] + x > maxX) {
                        maxX = p[j] + x;
                    }
                } 
                else { // 1,3,5,7,9...为y值
                    if (p[j] + y < minY) {
                        minY = p[j] + y;
                    }
                    if (p[j] + y > maxY) {
                        maxY = p[j] + y;
                    }
                }
            }
        }
    
        var rect;
        if (minX === Number.MAX_VALUE
            || maxX === Number.MIN_VALUE
            || minY === Number.MAX_VALUE
            || maxY === Number.MIN_VALUE
        ) {
            rect = {
                x : 0,
                y : 0,
                width : 0,
                height : 0
            };
        }
        else {
            rect = {
                x : Math.round(minX - lineWidth / 2),
                y : Math.round(minY - lineWidth / 2),
                width : maxX - minX + lineWidth,
                height : maxY - minY + lineWidth
            };
        }
        style.__rect = rect;
        return rect;
    }
    
    • 关于Number.MAX_VALUE和Number.MIN_VALUE,请看这里:JavaScript Number 对象
    • 获得pathArray(即为上面说的ca),遍历之
    • 加上参考点x/y后分别跟最大值最小值作比较,最后得出靠谱的minX,minY,maxX,maxY,木有什么惊喜
    • 如果minX,minY,maxX,maxY原封未动,那就是pathArray出了问题(没有取到或者什么的),返回一个都是0的对象
    • 如果正常返回x,y,width,height,关于lineWidth的问题,前一篇有解释。
  • 相关阅读:
    Qt 4.7
    CMake记录(一)
    CMake记录(二)
    a critical review of preetham skylight model 笔记
    在webform中清空多个控件的值的简单方法
    Java学习网站 [摘自互联网]
    关于Google API的学习
    DataGrid连接的
    http://www.dnc.com.cn
    在DataGrid中显示数据库中的图片
  • 原文地址:https://www.cnblogs.com/hhstuhacker/p/zrender-source-painter-shape-path.html
Copyright © 2011-2022 走看看