/* eslint-disable @typescript-eslint/ban-types */ import { calCanvasSize } from '../../core/util'; import Canvas2D from '../../core/Canvas'; import Point from '../../geo/Point'; import { SizeLike } from '../../geo/Size'; import { TileRenderingContext } from '../types'; import LayerAbstractRenderer from './LayerAbstractRenderer'; /** * 在 HTMLCanvasElement 上渲染图层的基类 * @english * Base Class to render layer on HTMLCanvasElement * @abstract * @protected * @memberOf renderer * @extends Class */ class CanvasRenderer extends LayerAbstractRenderer { gl: TileRenderingContext; //@internal _canvasUpdated: boolean; drawOnInteracting?(...args: any[]): void; checkResources?(): any[]; getImageData?(): ImageData; draw?(...args: any[]): void; /** * Ask whether the layer renderer needs to redraw */ needToRedraw(): boolean { const map = this.getMap(); if (map.isInteracting() || map.getRenderer().isViewChanged()) { // don't redraw when map is moving without any pitch return !(!map.getPitch() && map.isMoving() && !map.isZooming() && !map.isRotating() && !this.layer.options['forceRenderOnMoving']); } return false; } /** * Mark layer's canvas updated */ setCanvasUpdated() { this._canvasUpdated = true; return this; } /** * Only called by map's renderer to check whether the layer's canvas is updated * @protected * @return {Boolean} */ isCanvasUpdated(): boolean { return !!this._canvasUpdated; } /** * Get renderer's Canvas image object */ getCanvasImage(): any { const map = this.getMap(); this._canvasUpdated = false; if (this._renderZoom !== map.getZoom() || !this.canvas || !this._extent2D) { return null; } if (this.isBlank()) { return null; } if (this.layer.isEmpty && this.layer.isEmpty()) { return null; } // size = this._extent2D.getSize(), const containerPoint = map._pointToContainerPoint(this.middleWest)._add(0, -map.height / 2); return { 'image': this.canvas, 'layer': this.layer, 'point': containerPoint/* , 'size': size */ }; } /** * Clear canvas */ clear(): void { this.clearCanvas(); } /** * Create renderer's Canvas */ createCanvas(): void { if (this.canvas) { return; } const map = this.getMap(); const size = map.getSize(); const r = map.getDevicePixelRatio(), w = Math.round(r * size.width), h = Math.round(r * size.height); if (this.layer._canvas) { const canvas = this.layer._canvas; canvas.width = w; canvas.height = h; if (canvas.style) { canvas.style.width = size.width + 'px'; canvas.style.height = size.height + 'px'; } this.canvas = this.layer._canvas; } else { this.canvas = Canvas2D.createCanvas(w, h, map.CanvasClass); } this.onCanvasCreate(); } onCanvasCreate(): void { } //@internal _canvasContextScale(context: CanvasRenderingContext2D, dpr: number) { context.scale(dpr, dpr); context.dpr = dpr; return this; } createContext(): void { //Be compatible with layer renderers that overrides create canvas and create gl/context if (this.gl && this.gl.canvas === this.canvas || this.context) { return; } //disable willReadFrequently for render performance.Performance improved by 200 times this.context = Canvas2D.getCanvas2DPerformanceContext(this.canvas); if (!this.context) { return; } this.context.dpr = 1; if (this.layer.options['globalCompositeOperation']) { this.context.globalCompositeOperation = this.layer.options['globalCompositeOperation']; } const dpr = this.getMap().getDevicePixelRatio(); if (dpr !== 1) { this._canvasContextScale(this.context, dpr); } } resetCanvasTransform(): void { if (!this.context) { return; } const dpr = this.getMap().getDevicePixelRatio(); this.context.setTransform(dpr, 0, 0, dpr, 0, 0); } /** * Resize the canvas * @param canvasSize the size resizing to */ resizeCanvas(canvasSize?: SizeLike): void { const canvas = this.canvas; if (!canvas) { return; } const map = this.getMap(); const size = canvasSize || map.getSize(); const r = map.getDevicePixelRatio(); const { width, height, cssWidth, cssHeight } = calCanvasSize(size, r); // width/height不变并不意味着 css width/height 不变 if (this.layer._canvas && (canvas.style.width !== cssWidth || canvas.style.height !== cssHeight)) { canvas.style.width = cssWidth; canvas.style.height = cssHeight; } if (canvas.width === width && canvas.height === height) { return; } //retina support canvas.height = height; canvas.width = width; if (this.context) { this.context.dpr = 1; } if (r !== 1 && this.context) { this._canvasContextScale(this.context, r); } } /** * Clear the canvas to blank */ clearCanvas(): void { if (!this.context) { return; } const map = this.getMap(); if (!map) { return; } //fix #1597 const r = this.mapDPR || map.getDevicePixelRatio(); const rScale = 1 / r; const w = this.canvas.width * rScale, h = this.canvas.height * rScale; Canvas2D.clearRect(this.context, 0, 0, Math.max(w, this.canvas.width), Math.max(h, this.canvas.height)); } /** * @english * Prepare the canvas for rendering.
* 1. Clear the canvas to blank.
* 2. Clip the canvas by mask if there is any and return the mask's extent * @return {PointExtent} mask's extent of current zoom's 2d point. */ prepareCanvas(): any { if (!this.canvas) { this.createCanvas(); this.createContext(); this.layer.onCanvasCreate(); /** * canvascreate event, fired when canvas created. * * @event Layer#canvascreate * @type {Object} * @property {String} type - canvascreate * @property {Layer} target - layer * @property {CanvasRenderingContext2D} context - canvas's context * @property {WebGLRenderingContext2D} gl - canvas's webgl context */ this.layer.fire('canvascreate', { 'context': this.context, 'gl': this.gl }); } else { this.resetCanvasTransform(); this.clearCanvas(); this.resizeCanvas(); } delete this._maskExtent; const mask = this.layer.getMask(); // this.context may be not available if (!mask) { this.layer.fire('renderstart', { 'context': this.context, 'gl': this.gl }); return null; } const maskExtent2D = this._maskExtent = mask._getMaskPainter().get2DExtent(); //fix vt _extent2D is null if (maskExtent2D && this._extent2D && !maskExtent2D.intersects(this._extent2D)) { this.layer.fire('renderstart', { 'context': this.context, 'gl': this.gl }); return maskExtent2D; } /** * renderstart event, fired when layer starts to render. * * @event Layer#renderstart * @type {Object} * @property {String} type - renderstart * @property {Layer} target - layer * @property {CanvasRenderingContext2D} context - canvas's context */ this.layer.fire('renderstart', { 'context': this.context, 'gl': this.gl }); return maskExtent2D; } clipCanvas(context: CanvasRenderingContext2D) { const mask = this.layer.getMask(); if (!mask) { return false; } if (!this.layer.options.maskClip) { return false; } const old = this.middleWest; const map = this.getMap(); //when clipping, layer's middleWest needs to be reset for mask's containerPoint conversion this.middleWest = map._containerPointToPoint(new Point(0, map.height / 2)); //geometry 渲染逻辑里会修改globalAlpha,这里保存一下 const alpha = context.globalAlpha; context.save(); const dpr = this.mapDPR || map.getDevicePixelRatio(); if (dpr !== 1) { context.save(); this._canvasContextScale(context, dpr); } // Handle MultiPolygon if (mask.getGeometries) { context.isMultiClip = true; const masks = mask.getGeometries() || []; context.beginPath(); masks.forEach(_mask => { const painter = _mask._getMaskPainter(); painter.paint(null, context); }); context.stroke(); context.isMultiClip = false; } else { context.isClip = true; context.beginPath(); const painter = mask._getMaskPainter(); painter.paint(null, context); context.isClip = false; } if (dpr !== 1) { context.restore(); } try { context.clip('evenodd'); } catch (error) { console.error(error); } this.middleWest = old; context.globalAlpha = alpha; return true; } /** * call when rendering completes, this will fire necessary events and call setCanvasUpdated */ completeRender(): void { if (this.getMap()) { this._renderComplete = true; /** * renderend event, fired when layer ends rendering. * * @event Layer#renderend * @type {Object} * @property {String} type - renderend * @property {Layer} target - layer * @property {CanvasRenderingContext2D} context - canvas's context */ this.layer.fire('renderend', { 'context': this.context, 'gl': this.gl }); this.setCanvasUpdated(); } } /** * onResize * @param {Object} param event parameters */ onResize(_param: any) { delete this._extent2D; this.resizeCanvas(); this.setToRedraw(); } } export default CanvasRenderer;