UNPKG

zrender

Version:

A lightweight graphic library providing 2d draw for Apache ECharts

977 lines (820 loc) 29.6 kB
import {devicePixelRatio} from '../config'; import * as util from '../core/util'; import Layer, { LayerConfig } from './Layer'; import requestAnimationFrame from '../animation/requestAnimationFrame'; import env from '../core/env'; import Displayable from '../graphic/Displayable'; import { WXCanvasRenderingContext } from '../core/types'; import { GradientObject } from '../graphic/Gradient'; import { ImagePatternObject } from '../graphic/Pattern'; import Storage from '../Storage'; import { brush, BrushScope, brushSingle } from './graphic'; import { PainterBase } from '../PainterBase'; import BoundingRect from '../core/BoundingRect'; import { REDRAW_BIT } from '../graphic/constants'; import { getSize } from './helper'; import type IncrementalDisplayable from '../graphic/IncrementalDisplayable'; const HOVER_LAYER_ZLEVEL = 1e5; const CANVAS_ZLEVEL = 314159; const EL_AFTER_INCREMENTAL_INC = 0.01; const INCREMENTAL_INC = 0.001; 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; } interface CanvasPainterOption { devicePixelRatio?: number width?: number | string // Can be 10 / 10px / auto height?: number | string, useDirtyRect?: boolean } export default class CanvasPainter implements PainterBase { type = 'canvas' root: HTMLElement dpr: number storage: Storage private _singleCanvas: boolean private _opts: CanvasPainterOption private _zlevelList: number[] = [] private _prevDisplayList: Displayable[] = [] private _layers: {[key: number]: Layer} = {} // key is zlevel 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 private _hoverlayer: Layer private _redrawId: number private _backgroundColor: string | GradientObject | ImagePatternObject constructor(root: HTMLElement, storage: Storage, opts: CanvasPainterOption, id: number) { this.type = 'canvas'; // In node environment using node-canvas const singleCanvas = !root.nodeName // In node ? || root.nodeName.toUpperCase() === 'CANVAS'; this._opts = opts = util.extend({}, opts || {}) as CanvasPainterOption; /** * @type {number} */ this.dpr = opts.devicePixelRatio || devicePixelRatio; /** * @type {boolean} * @private */ this._singleCanvas = singleCanvas; /** * 绘图容器 * @type {HTMLElement} */ this.root = root; const rootStyle = root.style; if (rootStyle) { // @ts-ignore util.disableUserSelect(root); root.innerHTML = ''; } /** * @type {module:zrender/Storage} */ this.storage = storage; const zlevelList: number[] = this._zlevelList; this._prevDisplayList = []; const layers = this._layers; 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 mainLayer = new Layer(rootCanvas, this, this.dpr); mainLayer.__builtin__ = true; mainLayer.initContext(); // FIXME Use canvas width and height // mainLayer.resize(width, height); layers[CANVAS_ZLEVEL] = mainLayer; mainLayer.zlevel = CANVAS_ZLEVEL; // Not use common zlevel. zlevelList.push(CANVAS_ZLEVEL); 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 }; } } /** * 刷新 * @param paintAll 强制绘制所有displayable */ refresh(paintAll?: boolean) { const list = this.storage.getDisplayList(true); const prevList = this._prevDisplayList; const zlevelList = this._zlevelList; this._redrawId = Math.random(); this._paintList(list, prevList, paintAll, this._redrawId); // Paint custum layers for (let i = 0; i < zlevelList.length; i++) { const z = zlevelList[i]; const layer = this._layers[z]; if (!layer.__builtin__ && layer.refresh) { const clearColor = i === 0 ? this._backgroundColor : null; layer.refresh(clearColor); } } if (this._opts.useDirtyRect) { this._prevDisplayList = list.slice(); } return this; } refreshHover() { this._paintHoverList(this.storage.getDisplayList(false)); } private _paintHoverList(list: Displayable[]) { let len = list.length; let hoverLayer = this._hoverlayer; hoverLayer && hoverLayer.clear(); if (!len) { return; } const scope: BrushScope = { inHover: true, viewWidth: this._width, viewHeight: this._height }; let ctx; for (let i = 0; i < len; i++) { const el = list[i]; if (el.__inHover) { // Use a extream large zlevel // FIXME? if (!hoverLayer) { hoverLayer = this._hoverlayer = this.getLayer(HOVER_LAYER_ZLEVEL); } if (!ctx) { ctx = hoverLayer.ctx; ctx.save(); } brush(ctx, el, scope, i === len - 1); } } if (ctx) { ctx.restore(); } } getHoverLayer() { return this.getLayer(HOVER_LAYER_ZLEVEL); } paintOne(ctx: CanvasRenderingContext2D, el: Displayable) { brushSingle(ctx, el); } private _paintList(list: Displayable[], prevList: Displayable[], paintAll: boolean, redrawId?: number) { if (this._redrawId !== redrawId) { return; } paintAll = paintAll || false; this._updateLayerStatus(list); const {finished, needsRefreshHover} = this._doPaintList(list, prevList, paintAll); if (this._needsManuallyCompositing) { this._compositeManually(); } if (needsRefreshHover) { this._paintHoverList(list); } if (!finished) { const self = this; requestAnimationFrame(function () { self._paintList(list, prevList, paintAll, redrawId); }); } else { this.eachLayer(layer => { layer.afterBrush && layer.afterBrush(); }); } } private _compositeManually() { const ctx = this.getLayer(CANVAS_ZLEVEL).ctx; const width = (this._domRoot as HTMLCanvasElement).width; const height = (this._domRoot as HTMLCanvasElement).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); } }); } private _doPaintList( list: Displayable[], prevList: Displayable[], paintAll?: boolean ): { finished: boolean needsRefreshHover: boolean } { const layerList = []; const useDirtyRect = this._opts.useDirtyRect; for (let zi = 0; zi < this._zlevelList.length; zi++) { const zlevel = this._zlevelList[zi]; const layer = this._layers[zlevel]; if (layer.__builtin__ && layer !== this._hoverlayer && (layer.__dirty || paintAll) // Layer with hover elements can't be redrawn. // && !layer.__hasHoverLayerELement ) { layerList.push(layer); } } let finished = true; let needsRefreshHover = false; for (let k = 0; k < layerList.length; k++) { const layer = layerList[k]; const ctx = layer.ctx; const repaintRects = useDirtyRect && layer.createRepaintRects(list, prevList, this._width, this._height); let start = paintAll ? layer.__startIndex : layer.__drawIndex; const useTimer = !paintAll && layer.incremental && Date.now; const startTime = useTimer && Date.now(); const clearColor = layer.zlevel === this._zlevelList[0] ? this._backgroundColor : null; // All elements in this layer are removed. if (layer.__startIndex === layer.__endIndex) { layer.clear(false, clearColor, repaintRects); } else if (start === layer.__startIndex) { const firstEl = list[start]; if (!firstEl.incremental || !(firstEl as IncrementalDisplayable).notClear || paintAll) { layer.clear(false, clearColor, repaintRects); } } if (start === -1) { console.error('For some unknown reason. drawIndex is -1'); start = layer.__startIndex; } let i: number; /* eslint-disable-next-line */ const repaint = (repaintRect?: BoundingRect) => { const scope: BrushScope = { inHover: false, allClipped: false, prevEl: null, viewWidth: this._width, viewHeight: this._height }; for (i = start; i < layer.__endIndex; i++) { const el = list[i]; if (el.__inHover) { needsRefreshHover = true; } this._doPaintEl(el, layer, useDirtyRect, repaintRect, scope, i === layer.__endIndex - 1); if (useTimer) { // Date.now can be executed in 13,025,305 ops/second. const dTime = Date.now() - startTime; // Give 15 millisecond to draw. // The rest elements will be drawn in the next frame. if (dTime > 15) { break; } } } if (scope.prevElClipPaths) { // Needs restore the state. If last drawn element is in the clipping area. ctx.restore(); } }; if (repaintRects) { if (repaintRects.length === 0) { // Nothing to repaint, mark as finished i = layer.__endIndex; } else { const dpr = this.dpr; // Set repaintRect as clipPath for (var 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(); repaint(rect); ctx.restore(); } } } else { // Paint all once ctx.save(); repaint(); ctx.restore(); } layer.__drawIndex = i; if (layer.__drawIndex < layer.__endIndex) { finished = false; } } if (env.wxa) { // Flush for weixin application util.each(this._layers, function (layer) { if (layer && layer.ctx && (layer.ctx as WXCanvasRenderingContext).draw) { (layer.ctx as WXCanvasRenderingContext).draw(); } }); } return { finished, needsRefreshHover }; } private _doPaintEl( el: Displayable, currentLayer: Layer, useDirtyRect: boolean, repaintRect: BoundingRect, scope: BrushScope, isLast: boolean ) { const ctx = currentLayer.ctx; if (useDirtyRect) { const paintRect = el.getPaintRect(); if (!repaintRect || paintRect && paintRect.intersect(repaintRect)) { brush(ctx, el, scope, isLast); el.setPrevPaintRect(paintRect); } } else { brush(ctx, el, scope, isLast); } } /** * 获取 zlevel 所在层,如果不存在则会创建一个新的层 * @param zlevel * @param virtual Virtual layer will not be inserted into dom. */ getLayer(zlevel: number, virtual?: boolean) { if (this._singleCanvas && !this._needsManuallyCompositing) { zlevel = CANVAS_ZLEVEL; } let 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); } 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; } insertLayer(zlevel: number, layer: Layer) { const layersMap = this._layers; const zlevelList = this._zlevelList; const len = zlevelList.length; const domRoot = this._domRoot; let prevLayer = null; let i = -1; if (layersMap[zlevel]) { if (process.env.NODE_ENV !== 'production') { util.logError('ZLevel ' + zlevel + ' has been used already'); } return; } // Check if is a valid layer if (!isLayerValid(layer)) { if (process.env.NODE_ENV !== 'production') { util.logError('Layer of zlevel ' + zlevel + ' is not valid'); } return; } if (len > 0 && zlevel > zlevelList[0]) { for (i = 0; i < len - 1; i++) { if ( zlevelList[i] < zlevel && zlevelList[i + 1] > zlevel ) { break; } } prevLayer = layersMap[zlevelList[i]]; } zlevelList.splice(i + 1, 0, zlevel); layersMap[zlevel] = 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 (!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); } // Iterate each layer eachLayer<T>(cb: (this: T, layer: Layer, z: number) => void, context?: T) { const zlevelList = this._zlevelList; for (let i = 0; i < zlevelList.length; i++) { const z = zlevelList[i]; cb.call(context, this._layers[z], z); } } // Iterate each buildin layer eachBuiltinLayer<T>(cb: (this: T, layer: Layer, z: number) => void, context?: T) { const zlevelList = this._zlevelList; for (let i = 0; i < zlevelList.length; i++) { const z = zlevelList[i]; const layer = this._layers[z]; if (layer.__builtin__) { cb.call(context, layer, z); } } } // Iterate each other layer except buildin layer eachOtherLayer<T>(cb: (this: T, layer: Layer, z: number) => void, context?: T) { const zlevelList = this._zlevelList; for (let i = 0; i < zlevelList.length; i++) { const z = zlevelList[i]; const layer = this._layers[z]; if (!layer.__builtin__) { cb.call(context, layer, z); } } } /** * 获取所有已创建的层 * @param prevLayer */ getLayers() { return this._layers; } _updateLayerStatus(list: Displayable[]) { this.eachBuiltinLayer(function (layer, z) { layer.__dirty = layer.__used = false; }); function updatePrevLayer(idx: number) { if (prevLayer) { if (prevLayer.__endIndex !== idx) { prevLayer.__dirty = true; } prevLayer.__endIndex = idx; } } if (this._singleCanvas) { for (let i = 1; i < list.length; i++) { const el = list[i]; if (el.zlevel !== list[i - 1].zlevel || el.incremental) { this._needsManuallyCompositing = true; break; } } } let prevLayer: Layer = null; let incrementalLayerCount = 0; let prevZlevel; let i; for (i = 0; i < list.length; i++) { const el = list[i]; const zlevel = el.zlevel; let layer; if (prevZlevel !== zlevel) { prevZlevel = zlevel; incrementalLayerCount = 0; } // TODO Not use magic number on zlevel. // Each layer with increment element can be separated to 3 layers. // (Other Element drawn after incremental element) // -----------------zlevel + EL_AFTER_INCREMENTAL_INC-------------------- // (Incremental element) // ----------------------zlevel + INCREMENTAL_INC------------------------ // (Element drawn before incremental element) // --------------------------------zlevel-------------------------------- 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 ); } if (!layer.__builtin__) { util.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 & REDRAW_BIT) && !el.__inHover) { // Ignore dirty elements in hover layer. layer.__dirty = true; if (layer.incremental && layer.__drawIndex < 0) { // Start draw from the first dirty element. layer.__drawIndex = i; } } } updatePrevLayer(i); 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; } }); } /** * 清除hover层外所有内容 */ clear() { this.eachBuiltinLayer(this._clearLayer); return this; } _clearLayer(layer: Layer) { layer.clear(); } setBackgroundColor(backgroundColor: string | GradientObject | ImagePatternObject) { this._backgroundColor = backgroundColor; util.each(this._layers, layer => { layer.setUnpainted(); }); } /** * 修改指定zlevel的绘制参数 */ configLayer(zlevel: number, config: LayerConfig) { if (config) { const layerConfig = this._layerConfig; if (!layerConfig[zlevel]) { layerConfig[zlevel] = config; } else { util.merge(layerConfig[zlevel], config, true); } for (let i = 0; i < this._zlevelList.length; i++) { const _zlevel = this._zlevelList[i]; // TODO Remove EL_AFTER_INCREMENTAL_INC magic number if (_zlevel === zlevel || _zlevel === zlevel + EL_AFTER_INCREMENTAL_INC) { const layer = this._layers[_zlevel]; util.merge(layer, layerConfig[zlevel], true); } } } } /** * 删除指定层 * @param zlevel 层所在的zlevel */ delLayer(zlevel: number) { const layers = this._layers; const zlevelList = this._zlevelList; const layer = layers[zlevel]; if (!layer) { return; } layer.dom.parentNode.removeChild(layer.dom); delete layers[zlevel]; zlevelList.splice(util.indexOf(zlevelList, zlevel), 1); } /** * 区域大小变化后重绘 */ 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.getLayer(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'; for (let id in this._layers) { if (this._layers.hasOwnProperty(id)) { this._layers[id].resize(width, height); } } this.refresh(true); } this._width = width; this._height = height; } return this; } /** * 清除单独的一个层 * @param {number} zlevel */ clearLayer(zlevel: number) { const layer = this._layers[zlevel]; if (layer) { layer.clear(); } } /** * 释放 */ dispose() { this.root.innerHTML = ''; this.root = this.storage = this._domRoot = this._layers = 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._layers[CANVAS_ZLEVEL].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; this.eachLayer(function (layer) { if (layer.__builtin__) { ctx.drawImage(layer.dom, 0, 0, width, height); } else if (layer.renderToCanvas) { ctx.save(); layer.renderToCanvas(ctx); ctx.restore(); } }); } else { // PENDING, echarts-gl and incremental rendering. const scope = { inHover: false, viewWidth: this._width, viewHeight: this._height }; const displayList = this.storage.getDisplayList(true); for (let i = 0, len = displayList.length; i < len; i++) { const el = displayList[i]; brush(ctx, el, scope, i === len - 1); } } return imageLayer.dom; } /** * 获取绘图区域宽度 */ getWidth() { return this._width; } /** * 获取绘图区域高度 */ getHeight() { return this._height; } };