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

總結

剩余折線,多邊形,正多邊形,路徑,扇形,五角星,內外旋輪曲線,下次再說。


免責聲明!

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



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