回顧
上一篇說到:ZRender源碼分析3:Painter(View層)-上,接上篇,開始Shape對象
總體理解
先回到上次的Painter的render方法
/**
* 首次繪圖,創建各種dom和context
* 核心方法,zr.render() --> painter.render
*
* render和refersh的區別:render是clear所有,refresh是清除已經改變的layer
*
* @param {Function=} callback 繪畫結束后的回調函數
*/
Painter.prototype.render = function (callback) {
//省略
//升序遍歷,shape上的zlevel指定繪畫圖層的z軸層疊
this.storage.iterShape(
this._brush({ all : true }),
{ normal: 'up' }
);
//省略
return this;
};
/**
* 刷畫圖形
*
* @private
* @param {Object} changedZlevel 需要更新的zlevel索引
*/
Painter.prototype._brush = function (changedZlevel) {
var ctxList = this._ctxList;
var me = this;
function updatePainter(shapeList, callback) {
me.update(shapeList, callback);
}
return function(shape) {
if ((changedZlevel.all || changedZlevel[shape.zlevel])
&& !shape.invisible
) {
var ctx = ctxList[shape.zlevel];
if (ctx) {
if (!shape.onbrush //沒有onbrush
//有onbrush並且調用執行返回false或undefined則繼續粉刷
|| (shape.onbrush && !shape.onbrush(ctx, false))
) {
if (config.catchBrushException) {
try {
shape.brush(ctx, false, updatePainter);
}
catch(error) {
log(
error,
'brush error of ' + shape.type,
shape
);
}
}
else {
shape.brush(ctx, false, updatePainter);
}
}
}
else {
log(
'can not find the specific zlevel canvas!'
);
}
}
};
};
可以看到,在最核心處,便是調用了storage的遍歷shape對象方法,傳入的回調便是Painter._brush方法, 邏輯轉入到_brush方法,這里返回一個回調,在回調中,直接調用了shape對象的brush方法,可見,最后還是要到shape對象中去了。
Shape對象
打開zrender的shape文件夾,可以看到,有很多個JS,其中,Base類是一個基類,而其他的文件都各自是一個圖形類,都繼承自Base類。 很明確的是,這里用的是一個模板方法,接下來,用最簡單的Circle類來分析源碼。先看Circle的結構。
function Circle(options) {
Base.call(this, options);
}
Circle.prototype = {
type: 'circle',
/**
* 創建圓形路徑
* @param {Context2D} ctx Canvas 2D上下文
* @param {Object} style 樣式
*/
buildPath : function (ctx, style) { //省略實現
},
/**
* 返回矩形區域,用於局部刷新和文字定位
* @param {Object} style
*/
getRect : function (style) { //省略實現
}
};
require('../tool/util').inherits(Circle, Base);
最后一行比較重要,繼承了Base類,而Base類實現了brush方法,看見Circle實現的buildPath和getRect方法和type屬性,應該就是覆蓋了Base類的同名方法吧。 來看Base類,依舊是function Base() {} Base.prototype.baba = funciton () {},構造中先設置了一些默認值,然后用用戶自定義的option進行覆蓋。
function Base( options ) {
this.id = options.id || guid();
this.zlevel = 0;
this.draggable = false;
this.clickable = false;
this.hoverable = true;
this.position = [0, 0];
this.rotation = [0, 0, 0];
this.scale = [1, 1, 0, 0];
for ( var key in options ) {
this[ key ] = options[ key ];
}
this.style = this.style || {};
}
再來看核心方法brush
/**
* 畫刷
*
* @param ctx 畫布句柄
* @param isHighlight 是否為高亮狀態
* @param updateCallback 需要異步加載資源的shape可以通過這個callback(e)
* 讓painter更新視圖,base.brush沒用,需要的話重載brush
*/
Base.prototype.brush = function (ctx, isHighlight) {
var style = this.style;
//比如LineShape,配置的有brushTypeOnly
if (this.brushTypeOnly) {
style.brushType = this.brushTypeOnly;
}
if (isHighlight) {
// 根據style擴展默認高亮樣式
style = this.getHighlightStyle(
style,
this.highlightStyle || {},
this.brushTypeOnly
);
}
if (this.brushTypeOnly == 'stroke') {
style.strokeColor = style.strokeColor || style.color;
}
ctx.save();
//根據style設置content對象
this.setContext(ctx, style);
// 設置transform
this.updateTransform(ctx);
ctx.beginPath();
this.buildPath(ctx, style);
if (this.brushTypeOnly != 'stroke') {
ctx.closePath();
}
switch (style.brushType) {
case 'both':
ctx.fill();
case 'stroke':
style.lineWidth > 0 && ctx.stroke();
break;
default:
ctx.fill();
}
if (style.text) {
this.drawText(ctx, style, this.style);
}
ctx.restore();
};
- 1.設置brushTypeOnly,brushType有三種形式:both,stroke,fill。比如在LineShape對象中,划線是不可能fill的,只能是stroke,所以由此特殊處理
- 2.根據當前shape的style來獲取適合的highlightStyle,轉入到getHighlightStyle。
/** * 根據默認樣式擴展高亮樣式 * * @param ctx Canvas 2D上下文 * @param {Object} style 默認樣式 * @param {Object} highlightStyle 高亮樣式 */ Base.prototype.getHighlightStyle = function (style, highlightStyle, brushTypeOnly) { var newStyle = {}; for (var k in style) { newStyle[k] = style[k]; } var color = require('../tool/color'); var highlightColor = color.getHighlightColor(); // rgba(255,255.0.0.5) 半透明黃色 // 根據highlightStyle擴展 if (style.brushType != 'stroke') { // 帶填充則用高亮色加粗邊線 newStyle.strokeColor = highlightColor; newStyle.lineWidth = (style.lineWidth || 1) + this.getHighlightZoom(); //如果是文字,就是6,如果不是文字,是2 newStyle.brushType = 'both'; //如果高亮層並且brushType為both或者fill,強制其為both } else { if (brushTypeOnly != 'stroke') { // 描邊型的則用原色加工高亮 newStyle.strokeColor = highlightColor; newStyle.lineWidth = (style.lineWidth || 1) + this.getHighlightZoom(); } else { // 線型的則用原色加工高亮 newStyle.strokeColor = highlightStyle.strokeColor || color.mix( style.strokeColor, color.toRGB(highlightColor) ); } } // 可自定義覆蓋默認值 for (var k in highlightStyle) { if (typeof highlightStyle[k] != 'undefined') { newStyle[k] = highlightStyle[k]; } } return newStyle; };- 先將默認的樣式拷貝到newStyle變量中,在方法末尾,返回newStyle
- 根據默認的樣式計算出高亮的樣式,如果brushType為both或者fill,將strokeColor變成半透明的黃色,根據圖形類型算出lineWidth,將brushType賦值為both
- 如果brushType為stroke,再如果brushOnly沒有被設置為stroke,將strokeCOlor設置為半透明黃色,設置lineWidth
- 如果brushType為stroke,沒有設置brushOnly為stroke,就用color.mix計算出一個顏色值
- 最后將用戶自定義的highlightStyle覆蓋到newStyle,返回newStyle
- 如果brushTypeOnly為stroke,處理color的多個出處,然后就是ctx.save()與ctx.restore()之間的真正繪圖了。
- 轉到setContext方法
在原生的context賦值樣式時,都是context.fillStyle = '#aaa'; 但是經過zrender的抽象變得更加的易用,setContext就是負責原生canvasAPI與zrender.shape.style的轉換, 其實有變化的就只有fillStyle,strokeStyle,globalAlpha。分別用style.color,style.strokeColor,opacity進行替換,不過這些原生API的屬性名確實不那么平易近人。
var STYLE_CTX_MAP = [ ['color', 'fillStyle'], ['strokeColor', 'strokeStyle'], ['opacity', 'globalAlpha'], ['lineCap'], ['lineJoin'], ['miterLimit'], ['lineWidth'], ['shadowBlur'], ['shadowColor'], ['shadowOffsetX'], ['shadowOffsetY'] ]; /** * 畫布通用設置 * * @param ctx 畫布句柄 * @param style 通用樣式 */ Base.prototype.setContext = function (ctx, style) { for (var i = 0, len = STYLE_CTX_MAP.length; i < len; i++) { var styleProp = STYLE_CTX_MAP[i][0]; var styleValue = style[styleProp]; var ctxProp = STYLE_CTX_MAP[i][1] || styleProp; if (typeof styleValue != 'undefined') { ctx[ctxProp] = styleValue; } } }; - 關於變形,暫時跳過
- 開始beginPath,然后調用Base.buildPath,發現Base中沒有buildPath的實現,上面說了嘛,在子類實現了,模板方法。下面舉例 進行buildPath的分析
可以看到,在Circle類的buildPath中,只有一句話,那就是真正的Canvas畫圖的API調用,而在Rectangle中,用moveTo和lineTo畫出了一個路徑出來。
// shape/Circle.js /** * 創建圓形路徑 * @param {Context2D} ctx Canvas 2D上下文 * @param {Object} style 樣式 */ buildPath : function (ctx, style) { ctx.arc(style.x, style.y, style.r, 0, Math.PI * 2, true); return; }, //shape/Rectangle /** * 創建矩形路徑 * @param {Context2D} ctx Canvas 2D上下文 * @param {Object} style 樣式 */ buildPath : function(ctx, style) { if(!style.radius) { ctx.moveTo(style.x, style.y); ctx.lineTo(style.x + style.width, style.y); ctx.lineTo(style.x + style.width, style.y + style.height); ctx.lineTo(style.x, style.y + style.height); ctx.lineTo(style.x, style.y); //ctx.rect(style.x, style.y, style.width, style.height); } else { this._buildRadiusPath(ctx, style); } return; }, - 如果是只能划線的shape,沒有必要closePath,否則colsePath,以避免圖形的亂線出現,然后根據brushType的類型,進行fill和stroke,注意,第一個case沒有break,所以fill和stroke可以同時進行
- 最后,處理圖形上附屬的文字。
Base.prototype.drawText = function (ctx, style, normalStyle) { // 字體顏色策略 var textColor = style.textColor || style.color || style.strokeColor; ctx.fillStyle = textColor; /* if (style.textPosition == 'inside') { ctx.shadowColor = 'rgba(0,0,0,0)'; // 內部文字不帶shadowColor } */ // 文本與圖形間空白間隙 var dd = 10; var al; // 文本水平對齊 var bl; // 文本垂直對齊 var tx; // 文本橫坐標 var ty; // 文本縱坐標 var textPosition = style.textPosition // 用戶定義 || this.textPosition // shape默認 || 'top'; // 全局默認 switch (textPosition) { case 'inside': case 'top': case 'bottom': case 'left': case 'right': if (this.getRect) { var rect = (normalStyle || style).__rect || this.getRect(normalStyle || style); switch (textPosition) { case 'inside': tx = rect.x + rect.width / 2; ty = rect.y + rect.height / 2; al = 'center'; bl = 'middle'; // 如果brushType為both或者fill,那么就會有fill動作,這時,如果文字顏色跟填充顏色相同,文字就看不見了,所以把它變成白色 // 但是,如果文字顏色是白色呢,哎,不想了,太變態 if (style.brushType != 'stroke' && textColor == style.color ) { ctx.fillStyle = '#fff'; } break; case 'left': tx = rect.x - dd; //間隙 ty = rect.y + rect.height / 2; al = 'end'; bl = 'middle'; break; case 'right': tx = rect.x + rect.width + dd; ty = rect.y + rect.height / 2; al = 'start'; bl = 'middle'; break; case 'top': tx = rect.x + rect.width / 2; ty = rect.y - dd; al = 'center'; bl = 'bottom'; break; case 'bottom': tx = rect.x + rect.width / 2; ty = rect.y + rect.height + dd; al = 'center'; bl = 'top'; break; } } break; case 'start': case 'end': var xStart; var xEnd; var yStart; var yEnd; if (typeof style.pointList != 'undefined') { var pointList = style.pointList; if (pointList.length < 2) { // 少於2個點就不畫了~ return; } var length = pointList.length; switch (textPosition) { case 'start': xStart = pointList[0][0]; xEnd = pointList[1][0]; yStart = pointList[0][1]; yEnd = pointList[1][1]; break; case 'end': xStart = pointList[length - 2][0]; xEnd = pointList[length - 1][0]; yStart = pointList[length - 2][1]; yEnd = pointList[length - 1][1]; break; } } else { xStart = style.xStart || 0; xEnd = style.xEnd || 0; yStart = style.yStart || 0; yEnd = style.yEnd || 0; } switch (textPosition) { case 'start': al = xStart < xEnd ? 'end' : 'start'; bl = yStart < yEnd ? 'bottom' : 'top'; tx = xStart; ty = yStart; break; case 'end': al = xStart < xEnd ? 'start' : 'end'; bl = yStart < yEnd ? 'top' : 'bottom'; tx = xEnd; ty = yEnd; break; } dd -= 4; if (xStart != xEnd) { tx -= (al == 'end' ? dd : -dd); } else { al = 'center'; } if (yStart != yEnd) { ty -= (bl == 'bottom' ? dd : -dd); } else { bl = 'middle'; } break; case 'specific': tx = style.textX || 0; ty = style.textY || 0; al = 'start'; bl = 'middle'; break; } if (tx != null && ty != null) { _fillText( ctx, style.text, tx, ty, style.textFont, style.textAlign || al, style.textBaseline || bl ); } }; // Circle.js 的getRect /** * 返回矩形區域,用於局部刷新和文字定位 * @param {Object} style */ 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; } };- 關於textPosition的具體設置,請移步API
- getRect還是一個模板方法,用來獲取圖形所在的矩形區域。用Circle說明,通過一系列的計算,得到圓形左上角的xy坐標,獲得原型的矩形寬高,返回。其中,__rect是緩存作用
- 其中,al表示的是canvasAPI中的context.textAlign,bl指的是textBaseLine,tx,ty是文字的基准坐標,請看 http://www.w3school.com.cn/tags/canvas_textalign.asp 和 http://www.w3school.com.cn/tags/canvas_textbaseline.asp
- 如果textPosition為inside,left,right,top,bottom(分別表示在圖形的中央,左邊,右邊,上邊,下邊),根據rect的信息進行tx/ty/al/bl的賦值
- 如果是start或者end,只有直線和折線配置這兩個,同理,根據rect的信息分情況進行tx/ty/al/bl的設置
- 最后,拿到了tx/ty/al/bl/font/text,調用真正的畫圖方法_fillText
function _fillText(ctx, text, x, y, textFont, textAlign, textBaseline) { if (textFont) { ctx.font = textFont; } ctx.textAlign = textAlign; ctx.textBaseline = textBaseline; var rect = _getTextRect( text, x, y, textFont, textAlign, textBaseline ); text = (text + '').split('\n'); var lineHeight = require('../tool/area').getTextHeight('國', textFont); switch (textBaseline) { case 'top': y = rect.y; break; case 'bottom': y = rect.y + lineHeight; break; default: y = rect.y + lineHeight / 2; } for (var i = 0, l = text.length; i < l; i++) { ctx.fillText(text[i], x, y); y += lineHeight; } } /** * 返回矩形區域,用於局部刷新和文字定位 * * @inner * @param {Object} style */ function _getTextRect(text, x, y, textFont, textAlign, textBaseline) { var area = require('../tool/area'); var width = area.getTextWidth(text, textFont); var lineHeight = area.getTextHeight('國', textFont); text = (text + '').split('\n'); switch (textAlign) { case 'end': case 'right': x -= width; break; case 'center': x -= (width / 2); break; } switch (textBaseline) { case 'top': break; case 'bottom': y -= lineHeight * text.length; break; default: y -= lineHeight * text.length / 2; } return { x : x, y : y, width : width, height : lineHeight * text.length }; } //以下是tool/area.js中方法 /** * 測算多行文本高度 * @param {Object} text * @param {Object} textFont */ function getTextHeight(text, textFont) { var key = text+':'+textFont; if (_textHeightCache[key]) { return _textHeightCache[key]; } _ctx = _ctx || util.getContext(); _ctx.save(); if (textFont) { _ctx.font = textFont; } text = (text + '').split('\n'); //比較粗暴 var height = (_ctx.measureText('國').width + 2) * text.length; _ctx.restore(); _textHeightCache[key] = height; if (++_textHeightCacheCounter > TEXT_CACHE_MAX) { // 內存釋放 _textHeightCacheCounter = 0; _textHeightCache = {}; } return height; } /** * 測算多行文本寬度 * @param {Object} text * @param {Object} textFont */ function getTextWidth(text, textFont) { var key = text+':'+textFont; if (_textWidthCache[key]) { return _textWidthCache[key]; } _ctx = _ctx || util.getContext(); _ctx.save(); if (textFont) { _ctx.font = textFont; } text = (text + '').split('\n'); var width = 0; for (var i = 0, l = text.length; i < l; i++) { width = Math.max( _ctx.measureText(text[i]).width, width ); } _ctx.restore(); _textWidthCache[key] = width; if (++_textWidthCacheCounter > TEXT_CACHE_MAX) { // 內存釋放 _textWidthCacheCounter = 0; _textWidthCache = {}; } return width; }- 先設置context的textAlign和textBaseLine
- 關於area.getTextHeight和area.getTextWidth,主要是用了canvas的原生measureText方法,還有一個緩存技巧。關於measureText,請看 http://www.w3school.com.cn/tags/canvas_measuretext.asp
- _getTextRect獲取了需要畫的問題的熱點區域,仍舊返回的是x/y/width/height
- 在_fillText,獲取到熱點區域后,對行高做一些特殊處理之后,調用fillText進行真真正的繪制了。
- 至此,brush方法分析完畢。
總結
寫這些東西,真是很費時間,關於變形的設置,和其他圖形的詳細實現,等機緣到了,再續吧。下篇將繼續Painter的分析。
