ZRender源碼分析4:Painter(View層)-中


回顧

上一篇說到: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方法
    
    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;
            }
        }
    };
    
    在原生的context賦值樣式時,都是context.fillStyle = '#aaa'; 但是經過zrender的抽象變得更加的易用,setContext就是負責原生canvasAPI與zrender.shape.style的轉換, 其實有變化的就只有fillStyle,strokeStyle,globalAlpha。分別用style.color,style.strokeColor,opacity進行替換,不過這些原生API的屬性名確實不那么平易近人。
  • 關於變形,暫時跳過
  • 開始beginPath,然后調用Base.buildPath,發現Base中沒有buildPath的實現,上面說了嘛,在子類實現了,模板方法。下面舉例 進行buildPath的分析
    
    // 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;
    },
    
    
    可以看到,在Circle類的buildPath中,只有一句話,那就是真正的Canvas畫圖的API調用,而在Rectangle中,用moveTo和lineTo畫出了一個路徑出來。
  • 如果是只能划線的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的分析。


免責聲明!

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



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