# Painter 类

# 概述

绘图工具,这里暂时只介绍 canvas 的绘图工具

# 构造函数

首先是 type 属性,这里指的是 canvas ,然后 singleCanvas ,以前一直不明白为什么要搞一个 single ,自从预研了微信小程序后,才发现这个特性非常的有用,并不是所有的使用场景都适合多层 canvas 。微信小程序中没有DOM,而canvas也是一个独立的组件,因此,只能使用 singleCanvas

zrenderdevicePixelRatio,只有在 canvas 中才会被支持,后面看具体的使用。

_zlevelList 存放所有的层级号,以备后期使用;_layers 中存在所有的层级;_layerConfig 前面提起过,用于配置重绘属性。

_needsManuallyCompositing 当根目录是一个画布并且有多个 zlevel 时,zrender 会进行合成

如果是多层应用, 会创建一个 div 用来存放 canvas (可能是多个)

如果是单层应用的话,会直接将当前的 canvas 进行处理,并创建 Layer

# 方法

# getType

返回 canvas ,对应于 svg 和 vml

getType: function () {
    return 'canvas';
}

# isSingleCanvas

是否单层应用

isSingleCanvas: function () {
    return this._singleCanvas;
}

# getViewportRoot

获取 canvas 的上层 DOM ,如果是单层应用的话,返回 canvas 标签

getViewportRoot: function () {
    return this._domRoot;
}

# getViewportRootOffset

获取 rootDom 的位置信息

getViewportRootOffset: function () {
    var viewportRoot = this.getViewportRoot();
    if (viewportRoot) {
        return {
            offsetLeft: viewportRoot.offsetLeft || 0,
            offsetTop: viewportRoot.offsetTop || 0
        };
    }
}

# refresh

刷新,这个地方比较核心

关于 paintAll 为 true 的情况, 在 painter.resize 方法中尺寸发生变化后才会出现。

refresh: function (paintAll) {
    // 获取所有可显示元素,并更新
    var list = this.storage.getDisplayList(true);

    var zlevelList = this._zlevelList;

    this._redrawId = Math.random();

    // 绘制所有元素
    this._paintList(list, paintAll, this._redrawId);

    // Paint custum layers
    for (var i = 0; i < zlevelList.length; i++) {
        var z = zlevelList[i];
        var layer = this._layers[z];
        // 默认情况下, layer 没有 refresh 方法
        if (!layer.__builtin__ && layer.refresh) {
            var clearColor = i === 0 ? this._backgroundColor : null;
            layer.refresh(clearColor);
        }
    }

    this.refreshHover();

    return this;
}

# addHover

# removeHover

# clearHover

# refreshHover

# getHoverLayer

# _paintList

绘制所有的元素

  • list 要绘制的 displayable 列表
  • paintAll 强制绘制所有displayable
  • redrawId 重绘ID, 随机数
_paintList: function (list, paintAll, redrawId) {
    // 防止重复调用
    if (this._redrawId !== redrawId) {
        return;
    }
    // 
    paintAll = paintAll || false;

    // 获取层与元素的索引关系,以及是否需要更新
    this._updateLayerStatus(list);


    var finished = this._doPaintList(list, paintAll);

    if (this._needsManuallyCompositing) {
        this._compositeManually();
    }

    if (!finished) {
        var self = this;
        requestAnimationFrame(function () {
            self._paintList(list, paintAll, redrawId);
        });
    }
}

# _compositeManually

看方法的意思是将所有的层都绘制到同一个canvas, 看情况是用于单层应用,暂不理会。

_compositeManually: function () {
    var ctx = this.getLayer(CANVAS_ZLEVEL).ctx;
    var width = this._domRoot.width;
    var height = this._domRoot.height;
    ctx.clearRect(0, 0, width, height);
    // PENDING, If only builtin layer?
    this.eachBuiltinLayer(function (layer) {
        if (layer.virtual) {
            ctx.drawImage(layer.dom, 0, 0, width, height);
        }
    });
}

# _doPaintList

挺长的,分解, 因为暂时不考虑 layer.incremental , 所以剩下的流程就比较简单了。

  • 获取需要更新的 layer
  • 遍历需要更新的 layer
  • 根据 layer 在所有元素中的索引,遍历所有的元素
  • 绘制每一个元素,然后将设置为不需要更新
  • 微信小程序(不是应该是单独的代码吗?)
_doPaintList: function (list, paintAll) {
    //todo
}

1.获取需要刷新的层

var layerList = [];
for (var zi = 0; zi < this._zlevelList.length; zi++) {
    var zlevel = this._zlevelList[zi];
    var layer = this._layers[zlevel];
    if (layer.__builtin__
        && layer !== this._hoverlayer
        && (layer.__dirty || paintAll)
    ) {
        layerList.push(layer);
    }
}

2.遍历需要刷新的层

var finished = true;
for (var k = 0; k < layerList.length; k++) {
    var layer = layerList[k];
    var ctx = layer.ctx;
    var scope = {};
    ctx.save();

    // 因为不会使用 incremental 暂时认为两个值是一样的
    // 获取元素中的开始索引
    var start = paintAll ? layer.__startIndex : layer.__drawIndex;

    // 暂不考虑
    var useTimer = !paintAll && layer.incremental && Date.now;
    var startTime = useTimer && Date.now();

    // 清除画布
    var clearColor = layer.zlevel === this._zlevelList[0]
        ? this._backgroundColor : null;
    // All elements in this layer are cleared.
    if (layer.__startIndex === layer.__endIndex) {
        layer.clear(false, clearColor);
    }
    else if (start === layer.__startIndex) {
        var firstEl = list[start];
        if (!firstEl.incremental || !firstEl.notClear || paintAll) {
            layer.clear(false, clearColor);
        }
    }

    // 暂时忽略
    if (start === -1) {
        console.error('For some unknown reason. drawIndex is -1');
        start = layer.__startIndex;
    }

    // 在这里 start 肯定是 __startIndex 了
    // 遍历所有元素
    for (var i = start; i < layer.__endIndex; i++) {
        var el = list[i];
        this._doPaintEl(el, layer, paintAll, scope);
        el.__dirty = el.__dirtyText = false;

        if (useTimer) {
            // Date.now can be executed in 13,025,305 ops/second.
            var dTime = Date.now() - startTime;
            // Give 15 millisecond to draw.
            // The rest elements will be drawn in the next frame.
            if (dTime > 15) {
                break;
            }
        }
    }

    layer.__drawIndex = i;

    if (layer.__drawIndex < layer.__endIndex) {
        finished = false;
    }

    if (scope.prevElClipPaths) {
        // Needs restore the state. If last drawn element is in the clipping area.
        ctx.restore();
    }

    ctx.restore();
}

3.微信小程序

if (env.wxa) {
    // Flush for weixin application
    util.each(this._layers, function (layer) {
        if (layer && layer.ctx && layer.ctx.draw) {
            layer.ctx.draw();
        }
    });
}

return finished;

# _doPaintEl

绘制单个元素

  • el 元素
  • currentLayer 绘制的 layer
  • forcePaint 就是外面的 paintAll , 暂不处理
  • scope 作用域

绘制条件:

  1. 所属 layer 需要更新(或者 paintAll ,在 IncrementalDisplayble 中使用)
  2. 元素必须可见
  3. 元素的透明度不能为0
  4. scale 不能为0, 会导致后续的错误
  5. 元素未被剔除(是否进行裁剪)

关于 el.culling 属性,官方文档的解释是:是否进行裁剪, 必须在外部赋值,代码中没有任何的赋值的地方,isDisplayableCulled 方法后面研究

_doPaintEl: function (el, currentLayer, forcePaint, scope) {
    var ctx = currentLayer.ctx;
    var m = el.transform;
    if (
        (currentLayer.__dirty || forcePaint)
        // Ignore invisible element
        && !el.invisible
        // Ignore transparent element
        && el.style.opacity !== 0
        // Ignore scale 0 element, in some environment like node-canvas
        // Draw a scale 0 element can cause all following draw wrong
        // And setTransform with scale 0 will cause set back transform failed.
        && !(m && !m[0] && !m[3])
        // Ignore culled element
        && !(el.culling && isDisplayableCulled(el, this._width, this._height))
    ) {

        var clipPaths = el.__clipPaths;
        var prevElClipPaths = scope.prevElClipPaths;

        // Optimize when clipping on group with several elements
        if (!prevElClipPaths || isClipPathChanged(clipPaths, prevElClipPaths)) {
            // If has previous clipping state, restore from it
            if (prevElClipPaths) {
                ctx.restore();
                scope.prevElClipPaths = null;
                // Reset prevEl since context has been restored
                scope.prevEl = null;
            }
            // New clipping state
            if (clipPaths) {
                ctx.save();
                doClip(clipPaths, ctx);
                scope.prevElClipPaths = clipPaths;
            }
        }
        el.beforeBrush && el.beforeBrush(ctx);

        el.brush(ctx, scope.prevEl || null);
        scope.prevEl = el;

        el.afterBrush && el.afterBrush(ctx);
    }
}

# getLayer

# insertLayer

# eachLayer

# eachBuiltinLayer

# eachOtherLayer

# getLayers

获取 zlevel 所在层,如果不存在则会创建一个新的层

getLayer: function (zlevel, virtual) {
    if (this._singleCanvas && !this._needsManuallyCompositing) {
        zlevel = CANVAS_ZLEVEL;
    }
    var layer = this._layers[zlevel];
    if (!layer) {
        // Create a new layer
        layer = new Layer('zr_' + zlevel, this, this.dpr);
        layer.zlevel = zlevel;
        layer.__builtin__ = true;

        if (this._layerConfig[zlevel]) {
            util.merge(layer, this._layerConfig[zlevel], true);
        }
        // TODO Remove EL_AFTER_INCREMENTAL_INC magic number
        else if (this._layerConfig[zlevel - EL_AFTER_INCREMENTAL_INC]) {
            util.merge(layer, this._layerConfig[zlevel - EL_AFTER_INCREMENTAL_INC], true);
        }

        // incremental 的时候才会使用
        if (virtual) {
            layer.virtual = virtual;
        }

        this.insertLayer(zlevel, layer);

        // Context is created after dom inserted to document
        // Or excanvas will get 0px clientWidth and clientHeight
        layer.initContext();
    }

    return layer;
}

# _updateLayerStatus

更新层状态, 这里有点复杂, 将代码分解吧

大体功能,将元素分配到层中,并且确定层是否需要更新

_updateLayerStatus: function (list) {
    // todo 
}

1.所有层的数据初始化

this.eachBuiltinLayer(function (layer, z) {
    layer.__dirty = layer.__used = false;
});

2.单层应用处理

如果只有一个 canvas 然后元素还添加在很多层中,需要合并所有的元素到一个 canvas

if (this._singleCanvas) {
    for (var i = 1; i < list.length; i++) {
        var el = list[i];
        if (el.zlevel !== list[i - 1].zlevel || el.incremental) {
            this._needsManuallyCompositing = true;
            break;
        }
    }
}

3.遍历所有元素

关于 incremental 属性, 是 IncrementalDisplayble 类中所有,IncrementalDisplayble 像是一个容器,类似于 Group 吧,应该是在循环中增加元素时,会比 Group 的性能相对高很多,暂不分析了,碰到这个属性先暂时过吧

遍历所有的元素,然后分配到各个层中,每个层会获得元素列表的开始和结束索引,并且以元素是否需要更新来判定

使用案例: test\incremental.html

var prevLayer = null;
var incrementalLayerCount = 0;
var prevZlevel;
for (var i = 0; i < list.length; i++) {
    var el = list[i];
    var zlevel = el.zlevel;
    var layer;

    if (prevZlevel !== zlevel) {
        prevZlevel = zlevel;
        incrementalLayerCount = 0;
    }
    // 第一个条件分支暂不考虑, 那么这里将调用 this.getLayer 方法
    // 如果已经存在则直接返回,如果不存在将创建 Layer
    if (el.incremental) {
        layer = this.getLayer(zlevel + INCREMENTAL_INC, this._needsManuallyCompositing);
        layer.incremental = true;
        incrementalLayerCount = 1;
    }
    else {
        layer = this.getLayer(
            zlevel + (incrementalLayerCount > 0 ? EL_AFTER_INCREMENTAL_INC : 0),
            this._needsManuallyCompositing
        );
    }
    // 在 Layer 实例化后, 会赋值 __builtin__ 为 true
    // 为什么会 false 呢?
    if (!layer.__builtin__) {
        logError('ZLevel ' + zlevel + ' has been used by unkown layer ' + layer.id);
    }

    // 所有的元素是根据层级排过序的,所以出现改条件,说明进入了新的层级
    if (layer !== prevLayer) {
        layer.__used = true;
        if (layer.__startIndex !== i) {
            layer.__dirty = true;
        }
        layer.__startIndex = i;
        if (!layer.incremental) {
            // 绘制索引?
            layer.__drawIndex = i;
        }
        else {
            // Mark layer draw index needs to update.
            layer.__drawIndex = -1;
        }
        // 处理上一个层
        updatePrevLayer(i);
        prevLayer = layer;
    }
    // 如果一个层中的某个元素需要更新,则整个层都需要更新
    if (el.__dirty) {
        layer.__dirty = true;
        if (layer.incremental && layer.__drawIndex < 0) {
            // Start draw from the first dirty element.
            layer.__drawIndex = i;
        }
    }
}
// 收尾
updatePrevLayer(i);
function updatePrevLayer(idx) {
    if (prevLayer) {
        if (prevLayer.__endIndex !== idx) {
            prevLayer.__dirty = true;
        }
        prevLayer.__endIndex = idx;
    }
}

4.异常处理

瞎猜的,也没有调试出现

this.eachBuiltinLayer(function (layer, z) {
    // Used in last frame but not in this frame. Needs clear
    if (!layer.__used && layer.getElementCount() > 0) {
        layer.__dirty = true;
        layer.__startIndex = layer.__endIndex = layer.__drawIndex = 0;
    }
    // For incremental layer. In case start index changed and no elements are dirty.
    if (layer.__dirty && layer.__drawIndex < 0) {
        layer.__drawIndex = layer.__startIndex;
    }
});

# clear

# _clearLayer

# setBackgroundColor

# configLayer

# delLayer

# resize

# clearLayer

# dispose

# getRenderedCanvas

# getWidth

# getHeight

# _getSize

# pathToImage

# 通用方法

# isDisplayableCulled

var tmpRect = new BoundingRect(0, 0, 0, 0);
var viewRect = new BoundingRect(0, 0, 0, 0);
function isDisplayableCulled(el, width, height) {
    tmpRect.copy(el.getBoundingRect());
    if (el.transform) {
        tmpRect.applyTransform(el.transform);
    }
    viewRect.width = width;
    viewRect.height = height;
    return !tmpRect.intersect(viewRect);
}