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的問題,前一篇有解釋。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM