zrender
Version:
A lightweight graphic library providing 2d draw for Apache ECharts
1,300 lines (1,127 loc) • 51.1 kB
text/typescript
import {devicePixelRatio} from '../config';
import * as util from '../core/util';
import Layer, { isIncrementalLayer, LayerConfig, LayerDrawCursor } from './Layer';
import requestAnimationFrame from '../animation/requestAnimationFrame';
import env from '../core/env';
import Displayable from '../graphic/Displayable';
import {
IncrementalIdCompat, NullUndefined, WXCanvasRenderingContext,
ZLevel, ZLevel2, ZLEVEL2_INCREMENTAL, ZLEVEL2_NORMAL_ABOVE, ZLEVEL2_NORMAL_BELOW
} from '../core/types';
import { GradientObject } from '../graphic/Gradient';
import { ImagePatternObject } from '../graphic/Pattern';
import Storage from '../Storage';
import { brush, brushLoopFinalize, BrushScope, brushSingle } from './graphic';
import { PainterBase } from '../PainterBase';
import BoundingRect from '../core/BoundingRect';
import { REDRAW_BIT } from '../graphic/constants';
import { getSize } from './helper';
import { platformApi } from '../core/platform';
const HOVER_LAYER_ZLEVEL = 1e5;
// zlevel for the case that `Painter['_singleCanvas']` is `true`.
const CANVAS_ZLEVEL = 314159;
// Truthy value means dirty.
type HoverLayerDirty =
typeof HOVER_LAYER_DIRTY_NO
| typeof HOVER_LAYER_DIRTY_REPAINT_IF_EXISTING
| typeof HOVER_LAYER_DIRTY_REPAINT
// Do noting to hover layer.
const HOVER_LAYER_DIRTY_NO: undefined = undefined;
// Repaint only if existing. In most cases hover layer is not used,
// do not need to travel one more time to detect hover state.
const HOVER_LAYER_DIRTY_REPAINT_IF_EXISTING = 1;
// Create a hover layer if not existing, and repaint.
const HOVER_LAYER_DIRTY_REPAINT = 2;
function isLayerValid(layer: Layer) {
if (!layer) {
return false;
}
if (layer.__builtin__) {
return true;
}
if (typeof (layer.resize) !== 'function'
|| typeof (layer.refresh) !== 'function'
) {
return false;
}
return true;
}
function createRoot(width: number, height: number) {
const domRoot = document.createElement('div');
// domRoot.onselectstart = returnFalse; // Avoid page selected
domRoot.style.cssText = [
'position:relative',
// IOS13 safari probably has a compositing bug (z order of the canvas and the consequent
// dom does not act as expected) when some of the parent dom has
// `-webkit-overflow-scrolling: touch;` and the webpage is longer than one screen and
// the canvas is not at the top part of the page.
// Check `https://bugs.webkit.org/show_bug.cgi?id=203681` for more details. We remove
// this `overflow:hidden` to avoid the bug.
// 'overflow:hidden',
'width:' + width + 'px',
'height:' + height + 'px',
'padding:0',
'margin:0',
'border-width:0'
].join(';') + ';';
return domRoot;
}
function createBuiltinLayer(
id: string | HTMLCanvasElement,
painter: CanvasPainter,
zlevel: ZLevel,
zlevel2: ZLevel2
): Layer {
const layer = new Layer(id, painter, painter.dpr);
layer.zlevel = zlevel;
layer.zlevel2 = zlevel2;
layer.__builtin__ = true;
resetLayerDrawCursors(layer);
return layer;
}
interface CanvasPainterOption {
devicePixelRatio?: number
width?: number | string // Can be 10 / 10px / auto
height?: number | string
useDirtyRect?: boolean
}
export type CanvasPainterRefreshOpt = {
// repaint all displayable, rather than only dirty ones.
paintAll?: boolean;
// By default true. Can set to false to skip the normal repaint for
// the case that only hover layer need to be repainted.
refresh?: boolean;
// By default false. If true, for repaint hover layer.
// Note that a hover layer will also be repainted if normal layers are
// repainted and mark dirty to hover layer, even if refreshHover is false.
refreshHover?: boolean;
}
type LayerKey = {
zl: ZLevel;
zl2: ZLevel2;
};
// const LAYER_CURSOR_IDS_MAX = 1e3; // A safeguard
function resetLayerDrawCursors(layer: Layer): void {
layer.__cursorStack = [];
layer.__cursors = util.createHashMap();
}
function resetLayerDrawCursor(cursor: LayerDrawCursor): LayerDrawCursor {
cursor.startIdx = cursor.drawIdx = cursor.endIdx = cursor.endIdxNew = 0;
cursor.used = false;
cursor.first = cursor.last = NaN;
cursor.notClearIdx = -1;
// cursor.idsLen = 0;
// NOTE: cursor.key should not be modified after being created.
return cursor;
}
// Get the cursor, create one if not exist.
function ensureLayerDrawCursor(layer: Layer, incrementalCompat: IncrementalIdCompat): LayerDrawCursor {
const cursors = layer.__cursors;
const incremental = +incrementalCompat;
return cursors.get(incremental)
|| (
layer.__cursorStack.push(incremental),
cursors.set(incremental, resetLayerDrawCursor({key: incremental/*, ids: []*/} as LayerDrawCursor))
);
}
function eachCursorInLayer(layer: Layer, cb: (cursor: LayerDrawCursor) => void): void {
const cursorStack = layer.__cursorStack;
for (let i = 0; i < cursorStack.length; i++) {
cb(layer.__cursors.get(cursorStack[i]));
}
}
function ensureLayerListInZLevel(internal: CanvasPainterInternal, zlevel: ZLevel): Layer[] {
const layers = internal.layers;
return layers[zlevel] || (layers[zlevel] = new Array(3)); // See `ZLevel2`
}
/**
* Iterate existing layers in ascending z-order.
*/
function eachLayer(
internal: CanvasPainterInternal,
cb: (
layer: Layer, // Never be null/undefined
zlevel: number, zlevel2: number, idx: number
) => void,
filter?: EachLayerFilter,
) {
const layerStack = internal.layerStack;
for (let i = 0; i < layerStack.length; i++) {
const zlevel = layerStack[i].zl;
const zlevel2 = layerStack[i].zl2;
const layer = internal.layers[zlevel][zlevel2];
if (!filter || (
(!(filter & EACH_LAYER_BUILTIN) || layer.__builtin__)
&& (!(filter & EACH_LAYER_NOT_BUILTIN) || !layer.__builtin__)
&& (!(filter & EACH_LAYER_NOT_HOVER) || layer !== internal.hoverlayer)
)) {
cb(layer, zlevel, zlevel2, i);
}
}
}
// Can be `EACH_LAYER_BUILTIN | EACH_LAYER_NO_HOVER`,
// which means "built-in" and "not hover layer".
// By default `0` means no filter - iterate all layers.
type EachLayerFilter = number;
const EACH_LAYER_BUILTIN = 1;
const EACH_LAYER_NOT_BUILTIN = 2;
const EACH_LAYER_NOT_HOVER = 4;
const EACH_LAYER_BUILTIN_NOT_HOVER = EACH_LAYER_BUILTIN | EACH_LAYER_NOT_HOVER;
interface CanvasPainterInternal {
// Order is maintained by zlevel and zlevel2.
// This list represents the existing layers and the actual z-order.
layerStack: LayerKey[];
// structure: _layers[zlevel][zlevel2]
// See more details in CANVAS_LAYER_STACKING
// CAVEAT:
// Do not iterate `layers`; iterate `layerStack` instead.
layers: Layer[][];
hoverlayer?: Layer;
}
export default class CanvasPainter implements PainterBase {
type = 'canvas'
root: HTMLElement
dpr: number
storage: Storage
private _i: CanvasPainterInternal;
private _singleCanvas: boolean
private _opts: CanvasPainterOption
private _prevDisplayList: Displayable[] = []
private _layerConfig: {[key: number]: LayerConfig} = {} // key is zlevel
/**
* zrender will do compositing when root is a canvas and have multiple zlevels.
*/
private _needsManuallyCompositing = false
private _width: number
private _height: number
private _domRoot: HTMLElement
// hover layer is created only when needed, not save dirty flag separately.
// We need to detect hover layer requirements in this cases:
// (A) Hover state exist when the element is drawing, especially in progressive case.
// (B) Hover state is applied after the element has been drawn and keep no dirty.
// We need to avoid repeatedly drawing the hover layer, especially in progressive case.
// Hover layer should be cleared whenever a normal layer is cleared.
// otherwise it can not follow the elements changing.
// For example, the original el may have been moved.
private _hoverLayerDirty: HoverLayerDirty
private _redrawId: number
private _backgroundColor: string | GradientObject | ImagePatternObject
constructor(root: HTMLElement, storage: Storage, opts: CanvasPainterOption, id: number) {
this.type = 'canvas';
this._i = {
layerStack: [],
layers: [],
};
// In node environment using node-canvas
const singleCanvas = !root.nodeName // In node ?
|| root.nodeName.toUpperCase() === 'CANVAS';
this._opts = opts = util.extend({}, opts || {}) as CanvasPainterOption;
this.dpr = opts.devicePixelRatio || devicePixelRatio;
this._singleCanvas = singleCanvas;
this.root = root;
const rootStyle = root.style;
if (rootStyle) {
util.disableUserSelect(root);
root.innerHTML = '';
}
this.storage = storage;
this._prevDisplayList = [];
if (!singleCanvas) {
this._width = getSize(root, 0, opts);
this._height = getSize(root, 1, opts);
const domRoot = this._domRoot = createRoot(
this._width, this._height
);
root.appendChild(domRoot);
}
else {
const rootCanvas = root as HTMLCanvasElement;
let width = rootCanvas.width;
let height = rootCanvas.height;
if (opts.width != null) {
// TODO sting?
width = opts.width as number;
}
if (opts.height != null) {
// TODO sting?
height = opts.height as number;
}
this.dpr = opts.devicePixelRatio || 1;
// Use canvas width and height directly
rootCanvas.width = width * this.dpr;
rootCanvas.height = height * this.dpr;
this._width = width;
this._height = height;
// Create layer if only one given canvas
// Device can be specified to create a high dpi image.
const singleLayer = createBuiltinLayer(rootCanvas, this, CANVAS_ZLEVEL, ZLEVEL2_NORMAL_BELOW);
singleLayer.initContext();
// FIXME Use canvas width and height
// singleLayer.resize(width, height);
this._insertLayer(singleLayer, CANVAS_ZLEVEL, ZLEVEL2_NORMAL_BELOW, true);
this._domRoot = root;
}
}
getType() {
return 'canvas';
}
/**
* If painter use a single canvas
*/
isSingleCanvas() {
return this._singleCanvas;
}
getViewportRoot() {
return this._domRoot;
}
getViewportRootOffset() {
const viewportRoot = this.getViewportRoot();
if (viewportRoot) {
return {
offsetLeft: viewportRoot.offsetLeft || 0,
offsetTop: viewportRoot.offsetTop || 0
};
}
}
refresh(optOrPaintAll?: CanvasPainterRefreshOpt | CanvasPainterRefreshOpt['paintAll']) {
let opt: CanvasPainterRefreshOpt;
if (optOrPaintAll && !util.isObject(optOrPaintAll)) {
opt = {paintAll: !!optOrPaintAll}; // Backward compatible
}
else {
opt = (optOrPaintAll as CanvasPainterRefreshOpt) || {};
}
const refresh = util.retrieve2(opt.refresh, true);
const refreshHover = util.retrieve2(opt.refreshHover, false);
if (refreshHover) {
this._hoverLayerDirty = HOVER_LAYER_DIRTY_REPAINT;
}
if (!refresh) {
if (refreshHover) {
this._paintHoverList(this.storage.getDisplayList(false));
}
return this;
}
const list = this.storage.getDisplayList(true);
this._updateLayerStatus(list, opt.paintAll);
this._redrawId = Math.random();
const prevList = this._prevDisplayList;
this._paintList(list, prevList, this._redrawId);
// Paint custom layers
const bgColor = this._backgroundColor;
eachLayer(this._i, function (layer, zlevel, zlevel2, idx) {
if (layer.refresh) {
layer.refresh(idx === 0 ? bgColor : null);
}
}, EACH_LAYER_NOT_BUILTIN);
if (this._opts.useDirtyRect) {
this._prevDisplayList = list.slice();
}
return this;
}
private _paintHoverList(list: Displayable[]): void {
let hoverLayer = this._i.hoverlayer;
const hoverLayerDirty = this._hoverLayerDirty;
// Always clear dirty flag before return.
this._hoverLayerDirty = HOVER_LAYER_DIRTY_NO;
if (hoverLayerDirty === HOVER_LAYER_DIRTY_NO) {
return;
}
if (!hoverLayer && hoverLayerDirty === HOVER_LAYER_DIRTY_REPAINT) {
hoverLayer = this._i.hoverlayer = this._ensureLayer(HOVER_LAYER_ZLEVEL);
}
if (!hoverLayer) {
return;
}
// Clear the previous content. But use _hoverLayerDirty to avoid
// unnecessarily repeated clearing.
hoverLayer.clear();
const scope: BrushScope = {
inHover: true,
viewWidth: this._width,
viewHeight: this._height,
beforeBrushParam: {},
};
let ctx;
for (let i = 0, len = list.length; i < len; i++) {
const el = list[i];
if (!el.__inHover) {
continue;
}
if (!ctx) {
ctx = hoverLayer.ctx;
ctx.save();
}
// `el.style` is replaced with `el.__hoverStyle` when and only when hover layer is brushing.
// Any omission or any over replacing may cause incorrect result.
// Consider a problematic case:
// Suppose an element fades out via `opacity:0`, which is set into `this.style` via `el.attr()`,
// and then new styles (including `opacity: 0.8`) are assigned to `this.__hoverStyle` via
// `el.useStyle()`, but `saveCurrentToNormalState` uses `this.style`, the element will never be
// displayed.
// And notice upstream libraries, such as echarts, typically call `useStyle()` in every update
// cycle. It should write to `this.style` as normal, rather than to `__hoverStyle`, since
// `this.style` is the source of `saveCurrentToNormalState` when state switching.
const hoverStyle = el.__hoverStyle;
let originalStyle: Displayable['style'];
if (hoverStyle) {
originalStyle = el.style;
el.style = hoverStyle;
}
brush(ctx, el, scope);
if (hoverStyle) {
el.style = originalStyle;
}
}
if (ctx) {
brushLoopFinalize(ctx, scope);
ctx.restore();
}
}
/**
* @deprecated
*/
getHoverLayer() {
return this._ensureLayer(HOVER_LAYER_ZLEVEL);
}
/**
* @deprecated
*/
paintOne(ctx: CanvasRenderingContext2D, el: Displayable) {
brushSingle(ctx, el);
}
private _paintList(list: Displayable[], prevList: Displayable[], redrawId?: number) {
if (this._redrawId !== redrawId) {
return;
}
const finished = this._doPaintList(list, prevList);
if (this._needsManuallyCompositing) {
this._compositeManually();
}
if (!finished) {
const self = this;
requestAnimationFrame(function () {
self._paintList(list, prevList, redrawId);
});
}
else {
eachLayer(this._i, function (layer) {
layer.afterBrush && layer.afterBrush();
}, EACH_LAYER_BUILTIN_NOT_HOVER);
// Hover layer may be dirty by user interactions before progressive rendering
// finished. Therefore we do NOT paint hover layer per frame following _doPaintList,
// instead, we simply repaint it once after finished.
this._paintHoverList(list);
}
}
private _compositeManually() {
const ctx = this._ensureLayer(CANVAS_ZLEVEL).ctx;
const width = (this._domRoot as HTMLCanvasElement).width;
const height = (this._domRoot as HTMLCanvasElement).height;
ctx.clearRect(0, 0, width, height);
// PENDING, Whether only builtin layer?
eachLayer(this._i, function (layer) {
if (layer.virtual) {
ctx.drawImage(layer.dom, 0, 0, width, height);
}
}, EACH_LAYER_BUILTIN);
}
private _doPaintList(
list: Displayable[],
prevList: Displayable[],
// Return: `finished`
): boolean {
const painter = this;
let finished = true;
eachLayer(this._i, function (layer) {
let needDraw = false;
eachCursorInLayer(layer, function (cursor) {
if (cursor.drawIdx < cursor.endIdx
|| cursor.notClearIdx >= 0
) {
needDraw = true;
}
});
if (!needDraw && !layer.__dirty) {
return;
}
const repaintRects = (painter._opts.useDirtyRect && !isIncrementalLayer(layer))
? layer.createRepaintRects(list, prevList, painter._width, painter._height) : null;
const firstLayerKey = painter._i.layerStack[0];
let contentRetained = true;
if (layer.__dirty) { // Perform layer clear.
contentRetained = false;
layer.__dirty = false;
const clearColor = (layer.zlevel === firstLayerKey.zl && layer.zlevel2 === firstLayerKey.zl2)
? painter._backgroundColor : null;
layer.clear(false, clearColor, repaintRects);
}
eachCursorInLayer(layer, function (cursor) {
const cursorFinished = painter._paintPerCursor(
layer, cursor, list, repaintRects, contentRetained
);
finished = finished && cursorFinished;
});
}, EACH_LAYER_BUILTIN_NOT_HOVER);
if (env.wxa) {
// Flush for weixin application
eachLayer(this._i, function (layer) {
if (layer && layer.ctx && (layer.ctx as WXCanvasRenderingContext).draw) {
(layer.ctx as WXCanvasRenderingContext).draw();
}
});
}
return finished;
}
private _paintPerCursor(
layer: Layer,
layerCursor: LayerDrawCursor,
list: Displayable[],
repaintRects: BoundingRect[] | NullUndefined,
contentRetained: boolean
// Return `finished`
): boolean {
const ctx = layer.ctx;
if (repaintRects) {
if (!repaintRects.length) {
layerCursor.drawIdx = layerCursor.endIdx; // Nothing to repaint, mark as finished
}
else {
const dpr = this.dpr;
// Set repaintRect as clipPath
for (let r = 0; r < repaintRects.length; ++r) {
const rect = repaintRects[r];
ctx.save();
ctx.beginPath();
ctx.rect(
rect.x * dpr,
rect.y * dpr,
rect.width * dpr,
rect.height * dpr
);
ctx.clip();
this._paintPerCursorInRect(layer, layerCursor, list, rect, contentRetained);
ctx.restore();
}
}
}
else {
// Paint all once
ctx.save();
this._paintPerCursorInRect(layer, layerCursor, list, null, contentRetained);
ctx.restore();
}
return layerCursor.drawIdx >= layerCursor.endIdx;
}
private _paintPerCursorInRect(
layer: Layer,
layerCursor: LayerDrawCursor,
list: Displayable[],
repaintRect: BoundingRect | NullUndefined,
contentRetained: boolean,
): void {
const scope: BrushScope = {
inHover: false,
allClipped: false,
prevEl: null,
viewWidth: this._width,
viewHeight: this._height,
beforeBrushParam: {contentRetained}
};
const ctx = layer.ctx;
const useTimer = isIncrementalLayer(layer);
const startTime = useTimer && platformApi.getTime();
// NOTICE: This loop is performance-sensitive, especially for large data.
const drawIdxBegin = layerCursor.drawIdx;
const notClearIdx = layerCursor.notClearIdx;
let idx = notClearIdx >= 0 ? Math.min(notClearIdx, drawIdxBegin) : drawIdxBegin;
for (; idx < layerCursor.endIdx; idx++) {
const el = list[idx];
if (idx < drawIdxBegin && !el.notClear) {
// In this portion, all non-`notClear` elements do not need to be painted.
continue;
}
if (el.__inHover) {
// To avoid repeatedly repaint hover layer in progressive rendering,
// set HOVER_LAYER_DIRTY_REPAINT only when needed.
// Notice rendered el may not be traveled here again if the layer is not dirty,
// in this case HOVER_LAYER_DIRTY_REPAINT is set via markRedraw() calling
// zr.refreshHover().
this._hoverLayerDirty = HOVER_LAYER_DIRTY_REPAINT;
// NOTE: To ensure a consistent composited visual effect, `el` should be
// always painted to normal layers regardless of whether it will be painted
// to a hover layer.
}
if (repaintRect != null) {
const paintRect = el.getPaintRect();
if (paintRect && paintRect.intersect(repaintRect)) {
brush(ctx, el, scope);
el.setPrevPaintRect(paintRect);
}
}
else {
brush(ctx, el, scope);
}
if (useTimer) {
const dTime = platformApi.getTime() - startTime;
// Give 15 millisecond to draw.
// The rest elements will be drawn in the next frame.
// FIXME:
// This 15 is unreasonable enough - draw operations execution time is
// considerable but not a part of JS execution time here.
// We may change to record the last frame end time and compare it here.
if (dTime > 15) {
idx++;
break;
}
}
}
brushLoopFinalize(ctx, scope);
layerCursor.drawIdx = Math.max(idx, drawIdxBegin); // `idx` may < `drawIdxBegin` due to `notClearIdx`.
}
/**
* FIXME:
* Currently layer remove or reuse in different zlevel is not supported due to
* the external link.
*
* Obtain a layer; create one if not exist.
*
* Keep backward compatibile - this method may be called from outside of zrender.
* i.e., get a webGL layer, or built-in layer in some special cases.
*
* A virtual layer can be used in _singleCanvas case.
* A virtual layer can also be a WebGL layer and assigned to a ZRImage element
* But it still under management of zrender.
*/
getLayer(zlevel: ZLevel, virtual?: boolean) {
return this._ensureLayer(zlevel, 0, virtual);
}
/**
* Obtain a layer; create one if not exist.
*/
private _ensureLayer(zlevel: ZLevel, zlevel2?: ZLevel2, virtual?: boolean) {
zlevel2 = zlevel2 || 0;
const singleCanvas = this._singleCanvas;
if (singleCanvas && !this._needsManuallyCompositing) {
zlevel = CANVAS_ZLEVEL;
zlevel2 = 0;
}
let layer = ensureLayerListInZLevel(this._i, zlevel)[zlevel2];
if (!layer) {
layer = createBuiltinLayer('zr_' + zlevel + '.' + zlevel2, this, zlevel, zlevel2);
if (this._layerConfig[zlevel]) {
util.merge(layer, this._layerConfig[zlevel], true);
}
if (virtual
|| (singleCanvas && zlevel !== CANVAS_ZLEVEL)
) {
layer.virtual = true;
}
this._insertLayer(layer, zlevel, zlevel2, false);
// Context is created after dom inserted to document
// Or excanvas will get 0px clientWidth and clientHeight
layer.initContext();
}
return layer;
}
/**
* Keep backward compatibile - this method may be called from outside of zrender.
* e.g., insert a webGL layer by echarts-gl.
*/
insertLayer(zlevel: ZLevel, layer: Layer) {
this._insertLayer(layer, zlevel, 0, false);
}
private _insertLayer(
layer: Layer,
zlevel: ZLevel,
zlevel2: ZLevel2,
suppressDOMInsert: boolean
) {
const internal = this._i;
const layersMap = internal.layers;
const layerStack = internal.layerStack;
const domRoot = this._domRoot;
let prevLayer = null;
if (layersMap[zlevel] && layersMap[zlevel][zlevel2]) {
if (process.env.NODE_ENV !== 'production') {
util.logError('ZLevel ' + zlevel + '.' + zlevel2 + ' has been used already');
}
return;
}
if (!isLayerValid(layer)) {
if (process.env.NODE_ENV !== 'production') {
util.logError('Layer of zlevel ' + zlevel + ' is not valid');
}
return;
}
const len = layerStack.length;
let i = 0;
while (i < len
&& (layerStack[i].zl < zlevel
|| (layerStack[i].zl === zlevel && layerStack[i].zl2 < zlevel2)
)
) {
i++;
}
if (i > 0) {
prevLayer = ensureLayerListInZLevel(internal, layerStack[i - 1].zl)[layerStack[i - 1].zl2];
}
layerStack.splice(i, 0, {zl: zlevel, zl2: zlevel2});
ensureLayerListInZLevel(internal, zlevel)[zlevel2] = layer;
// Virtual layer will not directly show on the screen.
// (It can be a WebGL layer and assigned to a ZRImage element)
// But it still under management of zrender.
if (!suppressDOMInsert && !layer.virtual) {
if (prevLayer) {
const prevDom = prevLayer.dom;
if (prevDom.nextSibling) {
domRoot.insertBefore(
layer.dom,
prevDom.nextSibling
);
}
else {
domRoot.appendChild(layer.dom);
}
}
else {
if (domRoot.firstChild) {
domRoot.insertBefore(layer.dom, domRoot.firstChild);
}
else {
domRoot.appendChild(layer.dom);
}
}
}
layer.painter || (layer.painter = this);
}
/**
* @deprecated
*/
eachLayer<T>(cb: (this: T, layer: Layer, zlevel: number) => void, context?: T) {
return eachLayer(this._i, function (layer, zlevel) {
cb.call(context, layer, zlevel); // zlevel2 should not be exposed.
});
}
/**
* @deprecated
* FIXME: built-in layer should not be exposed.
*
* Iterate each built-in layer (including hover layer)
*/
eachBuiltinLayer<T>(cb: (this: T, layer: Layer, zlevel: number) => void, context?: T) {
return eachLayer(this._i, function (layer, zlevel) {
cb.call(context, layer, zlevel);
}, EACH_LAYER_BUILTIN);
}
/**
* Iterate each other layer except built-in layer
* e.g., get webGL layers by echarts-gl.
*/
eachOtherLayer<T>(cb: (this: T, layer: Layer, z: number) => void, context?: T) {
return eachLayer(this._i, function (layer, zlevel) {
cb.call(context, layer, zlevel);
}, EACH_LAYER_NOT_BUILTIN);
}
/**
* @deprecated
* NOTICE: Only for debugging or testing.
*/
getLayers() {
const layers: Record<string, Layer> = {};
eachLayer(this._i, function (layer, zlevel, zlevel2) {
layers[layer.id] = layer;
});
return layers;
}
/**
* @tutorial [CANVAS_INCREMENTAL_LAYER_USE_CASES]
* Two use patterns are covered per incremental layer:
* [CANVAS_INCREMENTAL_CASE_SINGLE_ELEMENT]
* An single incremental element with a customized `buildPath`, using `Displayable['notClear']`
* to retain the rendered content.
* [CANVAS_INCREMENTAL_CASE_MULTIPLE_ELEMENTS]
* A run of consecutive incremental elements, progressively drawing per frame in `_paintList`. This
* is not an optimal approach for rendering due to the increasing cost of updating and sorting
* `displayList`. However, it support varying styles and can balance the cost between rendering and
* hit testing during hover (which may degrade with excessive points in single shape).
* Notice, these two patterns can exist simultaneously in the same incremental layer.
*
* @tutorial [CANVAS_LAYER_STACKING]
* - An `Layer` instance represents a physical layer, typically a HTML Canvas.
* - Each `zlevel` will be splitted to 2 or 3 physical layers if incremental elements occur,
* designated by `zlevel2`. A full version can be like this:
* [[ layer_hover zlevel:100000 ]]
* [[ layer_normal_above zlevel:0, zlevel2:2 (normal el after incremental el) ]]
* [[ layer_incremental zlevel:0, zlevel2:1 (incremental el) ]]
* [[ layer_normal_below zlevel:0, zlevel2:0 (normal el before incremental el) ]]
* (But layer_normal_below may be omitted if not needed.)
* - Physical layers (HTML Canvas) should not be created excessively, therefore, within a single
* `zlevel`, multiple runs of incremental elements share one physical layer (i.e., `zlevel2: 1`).
* - Theoretically, a physical layer can switch bettween incremental or non-incremental. But currently
* we do not support it.
* - [LIMITED_TO_3_CANVAS_LAYERS_PER_ZLEVEL]
* To avoid excessive HTML Canvas creation, at most 3 layers can be created for a single `zlevel`.
* If two runs of consecutive incremental elements are separated by some normal elements, those normal
* elements are painted on `zlevel: 2`, and all incremental elements are painted on `zlevel: 1`,
* regardless of `el.z` and `el.z2` settings.
* Users can explicitly specify a higher `zlevel` to allow more incremental layers to be created.
* - NOTE: Elements do not necessarily have different z or z2 - even if all z or z2 are 0, z-order is
* determined by `add(el)` order.
*
* @tutorial [DISPLAY_LIST_SORTING_AND_LAYERING]
* Currently there are 5 parameters to determine the layer and z-order for each element:
* <zlevel, zlevel2, incremental(LayerDrawCursor), z, z2>
* Only `zlevel`, `z` and `z2` are user specified.
* The `displayList` is sorted only by `zlevel`, `z` and `z2`.
* A <`zlevel`, `zlevel2`> pair determines a layer.
* A `incremental(LayerDrawCursor)` acts like a "soft layer", representing a run of consecutive
* incremental elements. Multiple `LayerDrawCursor`s share one layer.
* Users must use different `el.incremental` (a number) to distinguish different runs of consecutive
* incremental elements. And each `el.incremental` has its exclusive `LayerDrawCursor`. Take echarts
* as an example: if there are multiple "series" requiring incremental, e.g., a bar series and a
* candlestick series in a Cartesian, and their zlevel/z/z2 are typicall the same.
* See CANVAS_LAYER_SAMPLE_CASE_3 for more details.
*
* Consider sample cases below to check the implementation:
* - [CANVAS_LAYER_SAMPLE_CASE_1]:
* `zlevel:5` is explicitly specified by users.
* `zlevel:0` is the default.
* [[ layer_hover zlevel:100000 ]]
* [[ layer_normal_above_2 zlevel:5, zlevel2:2 ]]
* [[ layer_incremental_2 zlevel:5, zlevel2:1 ]]
* [[ layer_normal_below_2 zlevel:5, zlevel2:0 ]]
* [[ layer_normal_above_1 zlevel:0, zlevel2:2 ]]
* [[ layer_incremental_1 zlevel:0, zlevel2:1 ]]
* [[ layer_normal_below_1 zlevel:0, zlevel2:0 ]]
* - [CANVAS_LAYER_SAMPLE_CASE_2]:
* No elements are before incremental elements.
* [[ layer_hover zlevel: 100000 ]]
* [[ layer_normal_above zlevel:0, zlevel2:2 ]]
* [[ layer_incremental zlevel:0, zlevel2:1 ]]
* - [CANVAS_LAYER_SAMPLE_CASE_3]:
* Multiple runs of consecutive incremental elements, may (or not) be separated by some normal elements.
* Suppose a sorted `displayList` is:
* `[{a_nor}, {b_inc:7}, {c_inc:7}, {d_nor}, {e_inc:9}, {f_inc:9}, {g_nor}]`.
* Then both incremental:7 and incremental:9 have new elements added.
* The sorted `displayList` become:
* `[{a_nor}, {b_inc:7}, {c_inc:7}, {m_inc:7}, {d_nor}, {e_inc:9}, {f_inc:9}, {n_inc:9}, {g_nor}]`.
* The order can not match the original `displayList` - new elements are inserted in the middle rather
* than at the end. Therefore, multiple `layerDrawCursor`s are introduced to manage the pointers separately,
* enabling them to share one physical layer.
* They are arranged into layers like this:
* [[ layer_hover zlevel:100000 ]]
* [[ layer_normal_above zlevel:0, zlevel2:2 layerDrawCursor:0 {d_nor}, {g_nor} ]]
* [[ layer_incremental zlevel:0, zlevel2:1 layerDrawCursor:9 {e_inc:9}, {f_inc:9} {n_inc:9} ]]
* [[ layerDrawCursor:7 {b_inc:7}, {c_inc:7} {m_inc:7} ]]
* [[ layer_normal_below zlevel:0, zlevel2:0 layerDrawCursor:0 {a_nor} ]]
*
* @tutorial [CANVAS_LAYER_DIRTY_RULES]:
* Only dirty layer will be cleared and repaint later. `layer.__dirty` is set by:
* - REDRAW_BIT of every element. [CANVAS_LAYER_DIRTY_BY_REDRAW_BIT]
* For normal layers, currently REDRAW_BIT is the only reliable way to make sure repainting, since
* reorder is not checked. So we conservatively always dirty the layers if any REDRAW_BIT occur.
* For incremental layers, we aggressively dirty the layer only if drawn elements have REDRAW_BIT,
* since redorder of incremental elements hardly occurs.
* - Mismatching of `layerDrawCursor.first` and `layerDrawCursor.endIdx`.
* [CANVAS_LAYER_CONTENT_RETAINED]:
* This strategy is mainly required by progressive rendering, where typicall new elements are
* appended, and repaint from the start per frame should be prevented. Otherwise, increasing
* draw calls can significantly block rendering. Additionally, If displayList indices of incremental
* elements are changed due to preceding elements of other layers, the drawing should not be restarted.
* Therefore, we record the first element to shift indices for this case.
* [CANVAS_LAYER_FAIL_TO_DIRTY_IF_ONLY_REORDER]:
* This strategy has also been applied to normal layers to prevent them from repainting in progressive
* frames. Upstream applications should remain the order of elements unchanged if no REDRAW_BIT is
* set - no checking for this currently. Otherwise, layers fail to dirty unexpectedly.
* Take echarts as an example, consider common patterns: "remove some elements", "modify z/z2 typically
* via useState", "clear and recreate all elements per user interaction", "reuse elements if possible
* but update attributes and styles per user interaction". Layer dirty can be triggered. If bad cases
* occur, more mechanism can be introduced (e.g., record el ids in layerDrawCursor for checking).
*
* PENDING:
* - [PENDING_SEPARATE_DISPLAY_LIST]:
* In CANVAS_INCREMENTAL_CASE_MULTIPLE_ELEMENTS, displayList sorting and `_updateAndAddDisplayable` will be
* executed per frame and significantly consume time in high element counts (indicatively, 1e6 in
* certain environments). Perhaps displayList can be separated by Layer or by LayerDrawCursor,
* and perform targeted optimization - omitting unnecessary sorting and `update()`.
* - Also sort displayList by `el.incremental` to automatically ensure consecutive?
* Currently, the contiguity can only be ensured by the order of `add()` call.
*/
private _updateLayerStatus(list: Displayable[], paintAll: boolean): void {
const painter = this;
if (painter._singleCanvas) {
for (let i = 1; i < list.length; i++) {
const el = list[i];
if (el.zlevel !== list[i - 1].zlevel || el.incremental) {
painter._needsManuallyCompositing = true;
break;
}
}
}
eachLayer(painter._i, function (layer) { // Reset flags
layer.__dirty = false;
eachCursorInLayer(layer, function (cursor) {
cursor.used = false;
cursor.endIdxNew = 0;
cursor.notClearIdx = -1;
});
}, EACH_LAYER_BUILTIN_NOT_HOVER);
let prevZLevel: ZLevel;
let currLayer: Layer = null;
let currCursor: LayerDrawCursor = null;
let aboveIncrementalInCurrZLevel = false;
// NOTE: this loop is performance-sensitive, especially for large data.
for (let idx = 0, len = list.length; idx < len; idx++) {
const el = list[idx];
const zlevel = el.zlevel;
const elIncremental = el.incremental;
let zlevel2: ZLevel2;
if (prevZLevel !== zlevel) { // Then `el` is the first element in this zlevel.
prevZLevel = zlevel;
aboveIncrementalInCurrZLevel = false;
}
if (elIncremental) {
aboveIncrementalInCurrZLevel = true;
zlevel2 = ZLEVEL2_INCREMENTAL;
}
else {
// See LIMITED_TO_3_CANVAS_LAYERS_PER_ZLEVEL
// If incremental elements appear, all subsequent normal elements use `zlevel2: 2`.
// else use `zlevel2: 0`.
zlevel2 = aboveIncrementalInCurrZLevel ? ZLEVEL2_NORMAL_ABOVE : ZLEVEL2_NORMAL_BELOW;
}
if (!currLayer || zlevel !== currLayer.zlevel || zlevel2 !== currLayer.zlevel2) {
// NOTE: now `el` is not necessarily the first element of `currLayer` in this pass, since
// `zlevel2` is not a sort key of `displayList`. See DISPLAY_LIST_SORTING_AND_LAYERING.
currLayer = painter._ensureLayer(zlevel, zlevel2);
currCursor = null;
if (!currLayer.__builtin__) {
util.logError('ZLevel ' + zlevel + ' has been used by unknown layer ' + currLayer.id);
continue;
}
}
// Else `currLayer` is not changed, keep using it. This is the most common case,
// so we retain this past path for performance.
if (!currCursor || elIncremental !== currCursor.key) {
// NOTE: now `el` is not necessarily the first element of `currCursor` in this pass, since
// `incremental` is not a sort key of `displayList`. See DISPLAY_LIST_SORTING_AND_LAYERING.
currCursor = ensureLayerDrawCursor(currLayer, elIncremental);
if (!currCursor.used) { // Now `el` is the first element in `currCursor` in this pass.
currCursor.used = true;
if (!paintAll && currCursor.first === el.id) { // See CANVAS_LAYER_CONTENT_RETAINED
const idxShift = idx - currCursor.startIdx;
currCursor.startIdx = idx;
currCursor.drawIdx += idxShift; // May be further modified at last.
currCursor.endIdx += idxShift; // May be further modified at last.
}
else {
currLayer.__dirty = true;
currCursor.first = el.id;
currCursor.startIdx = currCursor.drawIdx = idx;
currCursor.endIdx = idx + 1;
// Hereafter, `startIdx` should not changed in this pass.
}
}
}
// Else `currCursor` is not changed, keep using it. This is the most common case,
// so we retain this past path for performance.
// See CANVAS_LAYER_FAIL_TO_DIRTY_IF_ONLY_REORDER
// if (zlevel2 !== 1) { // Only for non-incremental layer
// const idxInCursor = idx - currCursor.startIdx;
// if (idxInCursor < LAYER_CURSOR_IDS_MAX && currCursor.ids[idxInCursor] !== el.id) {
// currLayer.__dirty = true;
// currCursor.idsLen = idxInCursor;
// }
// if (currCursor.idsLen < LAYER_CURSOR_IDS_MAX) {
// currCursor.ids[currCursor.idsLen++] = el.id;
// }
// }
currCursor.endIdxNew = idx + 1; // Use `endIdxNew` to further check the retained render at last.
// See CANVAS_LAYER_DIRTY_BY_REDRAW_BIT
if ((el.__dirty & REDRAW_BIT)
&& !el.__inHover // Ignore dirty elements in hover layer.
) {
if (!elIncremental // Always dirty the entire normal layer if any dirty occurs.
|| (!el.notClear && idx < currCursor.drawIdx)
) {
currLayer.__dirty = true;
}
if (elIncremental && el.notClear && currCursor.notClearIdx < 0) {
// If `notClear` elements are dirty, do not clear the layer, but they need to be repainted.
currCursor.notClearIdx = idx;
}
}
} // The end of displayList travel.
eachLayer(painter._i, function (layer) {
const cursorStack = layer.__cursorStack;
const cursors = layer.__cursors;
for (let i = cursorStack.length - 1; i >= 0; i--) {
const cursor = cursors.get(cursorStack[i]);
if (!cursor.used) { // `cursor` is used in the last pass but not in this pass - need clear.
layer.__dirty = true;
cursors.removeKey(cursorStack[i]);
cursorStack.splice(i, 1);
}
else { // `cursor` is newly created or is retained from the last pass.
// Layers with the same `zlevel` may be written alternately, and `layerDrawCursor` within
// the same layer may be writter alternately, since `zlevel2` and `incremental` are not
// sort keys of `displayList`. Therefore, their handling has to be finished at last.
const endIdxNew = cursor.endIdxNew;
if (isIncrementalLayer(layer)
? endIdxNew < cursor.drawIdx
: ( // See CANVAS_LAYER_FAIL_TO_DIRTY_IF_ONLY_REORDER
endIdxNew !== cursor.endIdx
|| !endIdxNew
|| list[endIdxNew - 1].id !== cursor.last
)
) {
layer.__dirty = true;
}
// Otherwise, only drawn tail elements that are not drawn; preserve the drawn ones.
cursor.endIdx = cursor.endIdxNew;
cursor.last = endIdxNew ? list[endIdxNew - 1].id : NaN;
}
}
if (layer.__dirty) {
// Once a layer is dirty, all of its layerDrawCursors need to be reset.
eachCursorInLayer(layer, function (cursor) {
// Once dirty, they need to be repainted from the start, since opacity and z-order
// should be respected.
cursor.drawIdx = cursor.startIdx;
});
if (painter._hoverLayerDirty === HOVER_LAYER_DIRTY_NO) {
painter._hoverLayerDirty = HOVER_LAYER_DIRTY_REPAINT_IF_EXISTING;
}
}
}, EACH_LAYER_BUILTIN_NOT_HOVER);
}
clear() {
eachLayer(this._i, function (layer) {
layer.clear();
resetLayerDrawCursors(layer);
}, EACH_LAYER_BUILTIN);
return this;
}
setBackgroundColor(backgroundColor: string | GradientObject | ImagePatternObject) {
this._backgroundColor = backgroundColor;
eachLayer(this._i, function (layer) {
layer.setUnpainted();
});
}
configLayer(zlevel: number, config: LayerConfig) {
if (config) {
const layerConfig = this._layerConfig;
if (!layerConfig[zlevel]) {
layerConfig[zlevel] = config;
}
else {
util.merge(layerConfig[zlevel], config, true);
}
eachLayer(this._i, function (layer, zlevel) {
util.merge(layer, layerConfig[zlevel], true);
});
}
}
/**
* Delete all layers of the specified zlevel.
* e.g., delete a webGL layer by echarts-gl.
*/
delLayer(zlevel: number) {
const layerStack = this._i.layerStack;
const layersMap = this._i.layers;
for (let i = layerStack.length - 1; i >= 0; i--) {
const key = layerStack[i];
if (key.zl === zlevel) {
const layer = layersMap[zlevel][key.zl2];
if (layer.__builtin__) {
continue;
}
layerStack.splice(i, 1);
layersMap[zlevel][key.zl2] = undefined;
if (!layer.virtual) {
const parentNode = layer.dom.parentNode;
parentNode && parentNode.removeChild(layer.dom);
}
}
}
}
/**
* 区域大小变化后重绘
*/
resize(
width?: number | string,
height?: number | string
) {
if (!this._domRoot.style) { // Maybe in node or worker
if (width == null || height == null) {
return;
}
// TODO width / height may be string
this._width = width as number;
this._height = height as number;
this._ensureLayer(CANVAS_ZLEVEL).resize(width as number, height as number);
}
else {
const domRoot = this._domRoot;
// FIXME Why ?
domRoot.style.display = 'none';
// Save input w/h
const opts = this._opts;
const root = this.root;
width != null && (opts.width = width);
height != null && (opts.height = height);
width = getSize(root, 0, opts);
height = getSize(root, 1, opts);
domRoot.style.display = '';
// 优化没有实际改变的resize
if (this._width !== width || height !== this._height) {
domRoot.style.width = width + 'px';
domRoot.style.height = height + 'px';
eachLayer(this._i, function (layer) {
layer.resize(width as number, height as number);
});
this.refresh({paintAll: true});
}
this._width = width;
this._height = height;
}
return this;
}
/**
* @deprecated
*/
clearLayer(zlevel: number) {
util.each(this._i.layers[zlevel], function (layer) {
if (layer && !layer.__builtin__) {
layer.clear();
}
});
}
dispose() {
this.root.innerHTML = '';
this.root =
this.storage =
this._domRoot =
this._i = null;
}
/**
* Get canvas which has all thing rendered
*/
getRenderedCanvas(opts?: {
backgroundColor?: string | GradientObject | ImagePatternObject
pixelRatio?: number
}) {
opts = opts || {};
if (this._singleCanvas && !this._compositeManually) {
return this._i.layers[CANVAS_ZLEVEL][0].dom;
}
const imageLayer = new Layer('image', this, opts.pixelRatio || this.dpr);
imageLayer.initContext();
imageLayer.clear(false, opts.backgroundColor || this._backgroundColor);
const ctx = imageLayer.ctx;
if (opts.pixelRatio <= this.dpr) {
this.refresh();
const width = imageLayer.dom.width;
const height = imageLayer.dom.height;
each