| import * as util from '../core/util'; | 
| import {devicePixelRatio} from '../config'; | 
| import { ImagePatternObject } from '../graphic/Pattern'; | 
| import CanvasPainter from './Painter'; | 
| import { GradientObject, InnerGradientObject } from '../graphic/Gradient'; | 
| import { ZRCanvasRenderingContext } from '../core/types'; | 
| import Eventful from '../core/Eventful'; | 
| import { ElementEventCallback } from '../Element'; | 
| import { getCanvasGradient } from './helper'; | 
| import { createCanvasPattern } from './graphic'; | 
| import Displayable from '../graphic/Displayable'; | 
| import BoundingRect from '../core/BoundingRect'; | 
| import { REDRAW_BIT } from '../graphic/constants'; | 
| import { platformApi } from '../core/platform'; | 
|   | 
| function createDom(id: string, painter: CanvasPainter, dpr: number) { | 
|     const newDom = platformApi.createCanvas(); | 
|     const width = painter.getWidth(); | 
|     const height = painter.getHeight(); | 
|   | 
|     const newDomStyle = newDom.style; | 
|     if (newDomStyle) {  // In node or some other non-browser environment | 
|         newDomStyle.position = 'absolute'; | 
|         newDomStyle.left = '0'; | 
|         newDomStyle.top = '0'; | 
|         newDomStyle.width = width + 'px'; | 
|         newDomStyle.height = height + 'px'; | 
|   | 
|         newDom.setAttribute('data-zr-dom-id', id); | 
|     } | 
|   | 
|     newDom.width = width * dpr; | 
|     newDom.height = height * dpr; | 
|   | 
|     return newDom; | 
| } | 
|   | 
| export interface LayerConfig { | 
|     // 每次清空画布的颜色 | 
|     clearColor?: string | GradientObject | ImagePatternObject | 
|     // 是否开启动态模糊 | 
|     motionBlur?: boolean | 
|     // 在开启动态模糊的时候使用,与上一帧混合的alpha值,值越大尾迹越明显 | 
|     lastFrameAlpha?: number | 
| }; | 
|   | 
| export default class Layer extends Eventful { | 
|   | 
|     id: string | 
|   | 
|     dom: HTMLCanvasElement | 
|     domBack: HTMLCanvasElement | 
|   | 
|     ctx: CanvasRenderingContext2D | 
|     ctxBack: CanvasRenderingContext2D | 
|   | 
|     painter: CanvasPainter | 
|   | 
|     // Configs | 
|     /** | 
|      * 每次清空画布的颜色 | 
|      */ | 
|     clearColor: string | GradientObject | ImagePatternObject | 
|     /** | 
|      * 是否开启动态模糊 | 
|      */ | 
|     motionBlur = false | 
|     /** | 
|      * 在开启动态模糊的时候使用,与上一帧混合的alpha值,值越大尾迹越明显 | 
|      */ | 
|     lastFrameAlpha = 0.7 | 
|     /** | 
|      * Layer dpr | 
|      */ | 
|     dpr = 1 | 
|   | 
|     /** | 
|      * Virtual layer will not be inserted into dom. | 
|      */ | 
|     virtual = false | 
|   | 
|     config = {} | 
|   | 
|     incremental = false | 
|   | 
|     zlevel = 0 | 
|   | 
|     maxRepaintRectCount = 5 | 
|   | 
|     private _paintRects: BoundingRect[] | 
|   | 
|     __dirty = true | 
|     __firstTimePaint = true | 
|   | 
|     __used = false | 
|   | 
|     __drawIndex = 0 | 
|     __startIndex = 0 | 
|     __endIndex = 0 | 
|   | 
|     // indices in the previous frame | 
|     __prevStartIndex: number = null | 
|     __prevEndIndex: number = null | 
|   | 
|     __builtin__: boolean | 
|   | 
|     constructor(id: string | HTMLCanvasElement, painter: CanvasPainter, dpr?: number) { | 
|         super(); | 
|   | 
|         let dom; | 
|         dpr = dpr || devicePixelRatio; | 
|         if (typeof id === 'string') { | 
|             dom = createDom(id, painter, dpr); | 
|         } | 
|         // Not using isDom because in node it will return false | 
|         else if (util.isObject(id)) { | 
|             dom = id; | 
|             id = dom.id; | 
|         } | 
|         this.id = id as string; | 
|         this.dom = dom; | 
|   | 
|         const domStyle = dom.style; | 
|         if (domStyle) { // Not in node | 
|             util.disableUserSelect(dom); | 
|             dom.onselectstart = () => false; | 
|             domStyle.padding = '0'; | 
|             domStyle.margin = '0'; | 
|             domStyle.borderWidth = '0'; | 
|         } | 
|   | 
|         this.painter = painter; | 
|   | 
|         this.dpr = dpr; | 
|     } | 
|   | 
|     getElementCount() { | 
|         return this.__endIndex - this.__startIndex; | 
|     } | 
|   | 
|     afterBrush() { | 
|         this.__prevStartIndex = this.__startIndex; | 
|         this.__prevEndIndex = this.__endIndex; | 
|     } | 
|   | 
|     initContext() { | 
|         this.ctx = this.dom.getContext('2d'); | 
|         (this.ctx as ZRCanvasRenderingContext).dpr = this.dpr; | 
|     } | 
|   | 
|     setUnpainted() { | 
|         this.__firstTimePaint = true; | 
|     } | 
|   | 
|     createBackBuffer() { | 
|         const dpr = this.dpr; | 
|   | 
|         this.domBack = createDom('back-' + this.id, this.painter, dpr); | 
|         this.ctxBack = this.domBack.getContext('2d'); | 
|   | 
|         if (dpr !== 1) { | 
|             this.ctxBack.scale(dpr, dpr); | 
|         } | 
|     } | 
|   | 
|     /** | 
|      * Create repaint list when using dirty rect rendering. | 
|      * | 
|      * @param displayList current rendering list | 
|      * @param prevList last frame rendering list | 
|      * @return repaint rects. null for the first frame, [] for no element dirty | 
|      */ | 
|     createRepaintRects( | 
|         displayList: Displayable[], | 
|         prevList: Displayable[], | 
|         viewWidth: number, | 
|         viewHeight: number | 
|     ) { | 
|         if (this.__firstTimePaint) { | 
|             this.__firstTimePaint = false; | 
|             return null; | 
|         } | 
|   | 
|         const mergedRepaintRects: BoundingRect[] = []; | 
|         const maxRepaintRectCount = this.maxRepaintRectCount; | 
|         let full = false; | 
|         const pendingRect = new BoundingRect(0, 0, 0, 0); | 
|   | 
|         function addRectToMergePool(rect: BoundingRect) { | 
|             if (!rect.isFinite() || rect.isZero()) { | 
|                 return; | 
|             } | 
|   | 
|             if (mergedRepaintRects.length === 0) { | 
|                 // First rect, create new merged rect | 
|                 const boundingRect = new BoundingRect(0, 0, 0, 0); | 
|                 boundingRect.copy(rect); | 
|                 mergedRepaintRects.push(boundingRect); | 
|             } | 
|             else { | 
|                 let isMerged = false; | 
|                 let minDeltaArea = Infinity; | 
|                 let bestRectToMergeIdx = 0; | 
|                 for (let i = 0; i < mergedRepaintRects.length; ++i) { | 
|                     const mergedRect = mergedRepaintRects[i]; | 
|   | 
|                     // Merge if has intersection | 
|                     if (mergedRect.intersect(rect)) { | 
|                         const pendingRect = new BoundingRect(0, 0, 0, 0); | 
|                         pendingRect.copy(mergedRect); | 
|                         pendingRect.union(rect); | 
|                         mergedRepaintRects[i] = pendingRect; | 
|                         isMerged = true; | 
|                         break; | 
|                     } | 
|                     else if (full) { | 
|                         // Merged to exists rectangles if full | 
|                         pendingRect.copy(rect); | 
|                         pendingRect.union(mergedRect); | 
|                         const aArea = rect.width * rect.height; | 
|                         const bArea = mergedRect.width * mergedRect.height; | 
|                         const pendingArea = pendingRect.width * pendingRect.height; | 
|                         const deltaArea = pendingArea - aArea - bArea; | 
|                         if (deltaArea < minDeltaArea) { | 
|                             minDeltaArea = deltaArea; | 
|                             bestRectToMergeIdx = i; | 
|                         } | 
|                     } | 
|                 } | 
|   | 
|                 if (full) { | 
|                     mergedRepaintRects[bestRectToMergeIdx].union(rect); | 
|                     isMerged = true; | 
|                 } | 
|   | 
|                 if (!isMerged) { | 
|                     // Create new merged rect if cannot merge with current | 
|                     const boundingRect = new BoundingRect(0, 0, 0, 0); | 
|                     boundingRect.copy(rect); | 
|                     mergedRepaintRects.push(boundingRect); | 
|                 } | 
|                 if (!full) { | 
|                     full = mergedRepaintRects.length >= maxRepaintRectCount; | 
|                 } | 
|             } | 
|         } | 
|   | 
|         /** | 
|          * Loop the paint list of this frame and get the dirty rects of elements | 
|          * in this frame. | 
|          */ | 
|         for (let i = this.__startIndex; i < this.__endIndex; ++i) { | 
|             const el = displayList[i]; | 
|             if (el) { | 
|                 /** | 
|                  * `shouldPaint` is true only when the element is not ignored or | 
|                  * invisible and all its ancestors are not ignored. | 
|                  * `shouldPaint` being true means it will be brushed this frame. | 
|                  * | 
|                  * `__isRendered` being true means the element is currently on | 
|                  * the canvas. | 
|                  * | 
|                  * `__dirty` being true means the element should be brushed this | 
|                  * frame. | 
|                  * | 
|                  * We only need to repaint the element's previous painting rect | 
|                  * if it's currently on the canvas and needs repaint this frame | 
|                  * or not painted this frame. | 
|                  */ | 
|                 const shouldPaint = el.shouldBePainted(viewWidth, viewHeight, true, true); | 
|                 const prevRect = el.__isRendered && ((el.__dirty & REDRAW_BIT) || !shouldPaint) | 
|                     ? el.getPrevPaintRect() | 
|                     : null; | 
|                 if (prevRect) { | 
|                     addRectToMergePool(prevRect); | 
|                 } | 
|   | 
|                 /** | 
|                  * On the other hand, we only need to paint the current rect | 
|                  * if the element should be brushed this frame and either being | 
|                  * dirty or not rendered before. | 
|                  */ | 
|                 const curRect = shouldPaint && ((el.__dirty & REDRAW_BIT) || !el.__isRendered) | 
|                     ? el.getPaintRect() | 
|                     : null; | 
|                 if (curRect) { | 
|                     addRectToMergePool(curRect); | 
|                 } | 
|             } | 
|         } | 
|   | 
|         /** | 
|          * The above loop calculates the dirty rects of elements that are in the | 
|          * paint list this frame, which does not include those elements removed | 
|          * in this frame. So we loop the `prevList` to get the removed elements. | 
|          */ | 
|         for (let i = this.__prevStartIndex; i < this.__prevEndIndex; ++i) { | 
|             const el = prevList[i]; | 
|             /** | 
|              * Consider the elements whose ancestors are invisible, they should | 
|              * not be painted and their previous painting rects should be | 
|              * cleared if they are rendered on the canvas (`__isRendered` being | 
|              * true). `!shouldPaint` means the element is not brushed in this | 
|              * frame. | 
|              * | 
|              * `!el.__zr` means it's removed from the storage. | 
|              * | 
|              * In conclusion, an element needs to repaint the previous painting | 
|              * rect if and only if it's not painted this frame and was | 
|              * previously painted on the canvas. | 
|              */ | 
|             const shouldPaint = el && el.shouldBePainted(viewWidth, viewHeight, true, true); | 
|             if (el && (!shouldPaint || !el.__zr) && el.__isRendered) { | 
|                 // el was removed | 
|                 const prevRect = el.getPrevPaintRect(); | 
|                 if (prevRect) { | 
|                     addRectToMergePool(prevRect); | 
|                 } | 
|             } | 
|         } | 
|   | 
|         // Merge intersected rects in the result | 
|         let hasIntersections; | 
|         do { | 
|             hasIntersections = false; | 
|             for (let i = 0; i < mergedRepaintRects.length;) { | 
|                 if (mergedRepaintRects[i].isZero()) { | 
|                     mergedRepaintRects.splice(i, 1); | 
|                     continue; | 
|                 } | 
|                 for (let j = i + 1; j < mergedRepaintRects.length;) { | 
|                     if (mergedRepaintRects[i].intersect(mergedRepaintRects[j])) { | 
|                         hasIntersections = true; | 
|                         mergedRepaintRects[i].union(mergedRepaintRects[j]); | 
|                         mergedRepaintRects.splice(j, 1); | 
|                     } | 
|                     else { | 
|                         j++; | 
|                     } | 
|                 } | 
|                 i++; | 
|             } | 
|         } while (hasIntersections); | 
|   | 
|         this._paintRects = mergedRepaintRects; | 
|   | 
|         return mergedRepaintRects; | 
|     } | 
|   | 
|     /** | 
|      * Get paint rects for debug usage. | 
|      */ | 
|     debugGetPaintRects() { | 
|         return (this._paintRects || []).slice(); | 
|     } | 
|   | 
|     resize(width: number, height: number) { | 
|         const dpr = this.dpr; | 
|   | 
|         const dom = this.dom; | 
|         const domStyle = dom.style; | 
|         const domBack = this.domBack; | 
|   | 
|         if (domStyle) { | 
|             domStyle.width = width + 'px'; | 
|             domStyle.height = height + 'px'; | 
|         } | 
|   | 
|         dom.width = width * dpr; | 
|         dom.height = height * dpr; | 
|   | 
|         if (domBack) { | 
|             domBack.width = width * dpr; | 
|             domBack.height = height * dpr; | 
|   | 
|             if (dpr !== 1) { | 
|                 this.ctxBack.scale(dpr, dpr); | 
|             } | 
|         } | 
|     } | 
|   | 
|     /** | 
|      * 清空该层画布 | 
|      */ | 
|     clear( | 
|         clearAll?: boolean, | 
|         clearColor?: string | GradientObject | ImagePatternObject, | 
|         repaintRects?: BoundingRect[] | 
|     ) { | 
|         const dom = this.dom; | 
|         const ctx = this.ctx; | 
|         const width = dom.width; | 
|         const height = dom.height; | 
|   | 
|         clearColor = clearColor || this.clearColor; | 
|         const haveMotionBLur = this.motionBlur && !clearAll; | 
|         const lastFrameAlpha = this.lastFrameAlpha; | 
|   | 
|         const dpr = this.dpr; | 
|         const self = this; | 
|   | 
|         if (haveMotionBLur) { | 
|             if (!this.domBack) { | 
|                 this.createBackBuffer(); | 
|             } | 
|   | 
|             this.ctxBack.globalCompositeOperation = 'copy'; | 
|             this.ctxBack.drawImage( | 
|                 dom, 0, 0, | 
|                 width / dpr, | 
|                 height / dpr | 
|             ); | 
|         } | 
|   | 
|         const domBack = this.domBack; | 
|   | 
|         function doClear(x: number, y: number, width: number, height: number) { | 
|             ctx.clearRect(x, y, width, height); | 
|             if (clearColor && clearColor !== 'transparent') { | 
|                 let clearColorGradientOrPattern; | 
|                 // Gradient | 
|                 if (util.isGradientObject(clearColor)) { | 
|                     // shouldn't cache when clearColor is not global and size changed | 
|                     const shouldCache = clearColor.global || ( | 
|                         (clearColor as InnerGradientObject).__width === width | 
|                         && (clearColor as InnerGradientObject).__height === height | 
|                     ); | 
|                     // Cache canvas gradient | 
|                     clearColorGradientOrPattern = shouldCache | 
|                         && (clearColor as InnerGradientObject).__canvasGradient | 
|                         || getCanvasGradient(ctx, clearColor, { | 
|                             x: 0, | 
|                             y: 0, | 
|                             width: width, | 
|                             height: height | 
|                         }); | 
|   | 
|                     (clearColor as InnerGradientObject).__canvasGradient = clearColorGradientOrPattern; | 
|                     (clearColor as InnerGradientObject).__width = width; | 
|                     (clearColor as InnerGradientObject).__height = height; | 
|                 } | 
|                 // Pattern | 
|                 else if (util.isImagePatternObject(clearColor)) { | 
|                     // scale pattern by dpr | 
|                     clearColor.scaleX = clearColor.scaleX || dpr; | 
|                     clearColor.scaleY = clearColor.scaleY || dpr; | 
|                     clearColorGradientOrPattern = createCanvasPattern( | 
|                         ctx, clearColor, { | 
|                             dirty() { | 
|                                 self.setUnpainted(); | 
|                                 self.painter.refresh(); | 
|                             } | 
|                         } | 
|                     ); | 
|                 } | 
|                 ctx.save(); | 
|                 ctx.fillStyle = clearColorGradientOrPattern || (clearColor as string); | 
|                 ctx.fillRect(x, y, width, height); | 
|                 ctx.restore(); | 
|             } | 
|   | 
|             if (haveMotionBLur) { | 
|                 ctx.save(); | 
|                 ctx.globalAlpha = lastFrameAlpha; | 
|                 ctx.drawImage(domBack, x, y, width, height); | 
|                 ctx.restore(); | 
|             } | 
|         }; | 
|   | 
|         if (!repaintRects || haveMotionBLur) { | 
|             // Clear the full canvas | 
|             doClear(0, 0, width, height); | 
|         } | 
|         else if (repaintRects.length) { | 
|             // Clear the repaint areas | 
|             util.each(repaintRects, rect => { | 
|                 doClear( | 
|                     rect.x * dpr, | 
|                     rect.y * dpr, | 
|                     rect.width * dpr, | 
|                     rect.height * dpr | 
|                 ); | 
|             }); | 
|         } | 
|     } | 
|   | 
|     // Interface of refresh | 
|     refresh: (clearColor?: string | GradientObject | ImagePatternObject) => void | 
|   | 
|     // Interface of renderToCanvas in getRenderedCanvas | 
|     renderToCanvas: (ctx: CanvasRenderingContext2D) => void | 
|   | 
|     // Events | 
|     onclick: ElementEventCallback<unknown, this> | 
|     ondblclick: ElementEventCallback<unknown, this> | 
|     onmouseover: ElementEventCallback<unknown, this> | 
|     onmouseout: ElementEventCallback<unknown, this> | 
|     onmousemove: ElementEventCallback<unknown, this> | 
|     onmousewheel: ElementEventCallback<unknown, this> | 
|     onmousedown: ElementEventCallback<unknown, this> | 
|     onmouseup: ElementEventCallback<unknown, this> | 
|     oncontextmenu: ElementEventCallback<unknown, this> | 
|   | 
|     ondrag: ElementEventCallback<unknown, this> | 
|     ondragstart: ElementEventCallback<unknown, this> | 
|     ondragend: ElementEventCallback<unknown, this> | 
|     ondragenter: ElementEventCallback<unknown, this> | 
|     ondragleave: ElementEventCallback<unknown, this> | 
|     ondragover: ElementEventCallback<unknown, this> | 
|     ondrop: ElementEventCallback<unknown, this> | 
| } |