123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511 |
- 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>
- }
|