Layer.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. import * as util from '../core/util';
  2. import {devicePixelRatio} from '../config';
  3. import { ImagePatternObject } from '../graphic/Pattern';
  4. import CanvasPainter from './Painter';
  5. import { GradientObject, InnerGradientObject } from '../graphic/Gradient';
  6. import { ZRCanvasRenderingContext } from '../core/types';
  7. import Eventful from '../core/Eventful';
  8. import { ElementEventCallback } from '../Element';
  9. import { getCanvasGradient } from './helper';
  10. import { createCanvasPattern } from './graphic';
  11. import Displayable from '../graphic/Displayable';
  12. import BoundingRect from '../core/BoundingRect';
  13. import { REDRAW_BIT } from '../graphic/constants';
  14. import { platformApi } from '../core/platform';
  15. function createDom(id: string, painter: CanvasPainter, dpr: number) {
  16. const newDom = platformApi.createCanvas();
  17. const width = painter.getWidth();
  18. const height = painter.getHeight();
  19. const newDomStyle = newDom.style;
  20. if (newDomStyle) { // In node or some other non-browser environment
  21. newDomStyle.position = 'absolute';
  22. newDomStyle.left = '0';
  23. newDomStyle.top = '0';
  24. newDomStyle.width = width + 'px';
  25. newDomStyle.height = height + 'px';
  26. newDom.setAttribute('data-zr-dom-id', id);
  27. }
  28. newDom.width = width * dpr;
  29. newDom.height = height * dpr;
  30. return newDom;
  31. }
  32. export interface LayerConfig {
  33. // 每次清空画布的颜色
  34. clearColor?: string | GradientObject | ImagePatternObject
  35. // 是否开启动态模糊
  36. motionBlur?: boolean
  37. // 在开启动态模糊的时候使用,与上一帧混合的alpha值,值越大尾迹越明显
  38. lastFrameAlpha?: number
  39. };
  40. export default class Layer extends Eventful {
  41. id: string
  42. dom: HTMLCanvasElement
  43. domBack: HTMLCanvasElement
  44. ctx: CanvasRenderingContext2D
  45. ctxBack: CanvasRenderingContext2D
  46. painter: CanvasPainter
  47. // Configs
  48. /**
  49. * 每次清空画布的颜色
  50. */
  51. clearColor: string | GradientObject | ImagePatternObject
  52. /**
  53. * 是否开启动态模糊
  54. */
  55. motionBlur = false
  56. /**
  57. * 在开启动态模糊的时候使用,与上一帧混合的alpha值,值越大尾迹越明显
  58. */
  59. lastFrameAlpha = 0.7
  60. /**
  61. * Layer dpr
  62. */
  63. dpr = 1
  64. /**
  65. * Virtual layer will not be inserted into dom.
  66. */
  67. virtual = false
  68. config = {}
  69. incremental = false
  70. zlevel = 0
  71. maxRepaintRectCount = 5
  72. private _paintRects: BoundingRect[]
  73. __dirty = true
  74. __firstTimePaint = true
  75. __used = false
  76. __drawIndex = 0
  77. __startIndex = 0
  78. __endIndex = 0
  79. // indices in the previous frame
  80. __prevStartIndex: number = null
  81. __prevEndIndex: number = null
  82. __builtin__: boolean
  83. constructor(id: string | HTMLCanvasElement, painter: CanvasPainter, dpr?: number) {
  84. super();
  85. let dom;
  86. dpr = dpr || devicePixelRatio;
  87. if (typeof id === 'string') {
  88. dom = createDom(id, painter, dpr);
  89. }
  90. // Not using isDom because in node it will return false
  91. else if (util.isObject(id)) {
  92. dom = id;
  93. id = dom.id;
  94. }
  95. this.id = id as string;
  96. this.dom = dom;
  97. const domStyle = dom.style;
  98. if (domStyle) { // Not in node
  99. util.disableUserSelect(dom);
  100. dom.onselectstart = () => false;
  101. domStyle.padding = '0';
  102. domStyle.margin = '0';
  103. domStyle.borderWidth = '0';
  104. }
  105. this.painter = painter;
  106. this.dpr = dpr;
  107. }
  108. getElementCount() {
  109. return this.__endIndex - this.__startIndex;
  110. }
  111. afterBrush() {
  112. this.__prevStartIndex = this.__startIndex;
  113. this.__prevEndIndex = this.__endIndex;
  114. }
  115. initContext() {
  116. this.ctx = this.dom.getContext('2d');
  117. (this.ctx as ZRCanvasRenderingContext).dpr = this.dpr;
  118. }
  119. setUnpainted() {
  120. this.__firstTimePaint = true;
  121. }
  122. createBackBuffer() {
  123. const dpr = this.dpr;
  124. this.domBack = createDom('back-' + this.id, this.painter, dpr);
  125. this.ctxBack = this.domBack.getContext('2d');
  126. if (dpr !== 1) {
  127. this.ctxBack.scale(dpr, dpr);
  128. }
  129. }
  130. /**
  131. * Create repaint list when using dirty rect rendering.
  132. *
  133. * @param displayList current rendering list
  134. * @param prevList last frame rendering list
  135. * @return repaint rects. null for the first frame, [] for no element dirty
  136. */
  137. createRepaintRects(
  138. displayList: Displayable[],
  139. prevList: Displayable[],
  140. viewWidth: number,
  141. viewHeight: number
  142. ) {
  143. if (this.__firstTimePaint) {
  144. this.__firstTimePaint = false;
  145. return null;
  146. }
  147. const mergedRepaintRects: BoundingRect[] = [];
  148. const maxRepaintRectCount = this.maxRepaintRectCount;
  149. let full = false;
  150. const pendingRect = new BoundingRect(0, 0, 0, 0);
  151. function addRectToMergePool(rect: BoundingRect) {
  152. if (!rect.isFinite() || rect.isZero()) {
  153. return;
  154. }
  155. if (mergedRepaintRects.length === 0) {
  156. // First rect, create new merged rect
  157. const boundingRect = new BoundingRect(0, 0, 0, 0);
  158. boundingRect.copy(rect);
  159. mergedRepaintRects.push(boundingRect);
  160. }
  161. else {
  162. let isMerged = false;
  163. let minDeltaArea = Infinity;
  164. let bestRectToMergeIdx = 0;
  165. for (let i = 0; i < mergedRepaintRects.length; ++i) {
  166. const mergedRect = mergedRepaintRects[i];
  167. // Merge if has intersection
  168. if (mergedRect.intersect(rect)) {
  169. const pendingRect = new BoundingRect(0, 0, 0, 0);
  170. pendingRect.copy(mergedRect);
  171. pendingRect.union(rect);
  172. mergedRepaintRects[i] = pendingRect;
  173. isMerged = true;
  174. break;
  175. }
  176. else if (full) {
  177. // Merged to exists rectangles if full
  178. pendingRect.copy(rect);
  179. pendingRect.union(mergedRect);
  180. const aArea = rect.width * rect.height;
  181. const bArea = mergedRect.width * mergedRect.height;
  182. const pendingArea = pendingRect.width * pendingRect.height;
  183. const deltaArea = pendingArea - aArea - bArea;
  184. if (deltaArea < minDeltaArea) {
  185. minDeltaArea = deltaArea;
  186. bestRectToMergeIdx = i;
  187. }
  188. }
  189. }
  190. if (full) {
  191. mergedRepaintRects[bestRectToMergeIdx].union(rect);
  192. isMerged = true;
  193. }
  194. if (!isMerged) {
  195. // Create new merged rect if cannot merge with current
  196. const boundingRect = new BoundingRect(0, 0, 0, 0);
  197. boundingRect.copy(rect);
  198. mergedRepaintRects.push(boundingRect);
  199. }
  200. if (!full) {
  201. full = mergedRepaintRects.length >= maxRepaintRectCount;
  202. }
  203. }
  204. }
  205. /**
  206. * Loop the paint list of this frame and get the dirty rects of elements
  207. * in this frame.
  208. */
  209. for (let i = this.__startIndex; i < this.__endIndex; ++i) {
  210. const el = displayList[i];
  211. if (el) {
  212. /**
  213. * `shouldPaint` is true only when the element is not ignored or
  214. * invisible and all its ancestors are not ignored.
  215. * `shouldPaint` being true means it will be brushed this frame.
  216. *
  217. * `__isRendered` being true means the element is currently on
  218. * the canvas.
  219. *
  220. * `__dirty` being true means the element should be brushed this
  221. * frame.
  222. *
  223. * We only need to repaint the element's previous painting rect
  224. * if it's currently on the canvas and needs repaint this frame
  225. * or not painted this frame.
  226. */
  227. const shouldPaint = el.shouldBePainted(viewWidth, viewHeight, true, true);
  228. const prevRect = el.__isRendered && ((el.__dirty & REDRAW_BIT) || !shouldPaint)
  229. ? el.getPrevPaintRect()
  230. : null;
  231. if (prevRect) {
  232. addRectToMergePool(prevRect);
  233. }
  234. /**
  235. * On the other hand, we only need to paint the current rect
  236. * if the element should be brushed this frame and either being
  237. * dirty or not rendered before.
  238. */
  239. const curRect = shouldPaint && ((el.__dirty & REDRAW_BIT) || !el.__isRendered)
  240. ? el.getPaintRect()
  241. : null;
  242. if (curRect) {
  243. addRectToMergePool(curRect);
  244. }
  245. }
  246. }
  247. /**
  248. * The above loop calculates the dirty rects of elements that are in the
  249. * paint list this frame, which does not include those elements removed
  250. * in this frame. So we loop the `prevList` to get the removed elements.
  251. */
  252. for (let i = this.__prevStartIndex; i < this.__prevEndIndex; ++i) {
  253. const el = prevList[i];
  254. /**
  255. * Consider the elements whose ancestors are invisible, they should
  256. * not be painted and their previous painting rects should be
  257. * cleared if they are rendered on the canvas (`__isRendered` being
  258. * true). `!shouldPaint` means the element is not brushed in this
  259. * frame.
  260. *
  261. * `!el.__zr` means it's removed from the storage.
  262. *
  263. * In conclusion, an element needs to repaint the previous painting
  264. * rect if and only if it's not painted this frame and was
  265. * previously painted on the canvas.
  266. */
  267. const shouldPaint = el && el.shouldBePainted(viewWidth, viewHeight, true, true);
  268. if (el && (!shouldPaint || !el.__zr) && el.__isRendered) {
  269. // el was removed
  270. const prevRect = el.getPrevPaintRect();
  271. if (prevRect) {
  272. addRectToMergePool(prevRect);
  273. }
  274. }
  275. }
  276. // Merge intersected rects in the result
  277. let hasIntersections;
  278. do {
  279. hasIntersections = false;
  280. for (let i = 0; i < mergedRepaintRects.length;) {
  281. if (mergedRepaintRects[i].isZero()) {
  282. mergedRepaintRects.splice(i, 1);
  283. continue;
  284. }
  285. for (let j = i + 1; j < mergedRepaintRects.length;) {
  286. if (mergedRepaintRects[i].intersect(mergedRepaintRects[j])) {
  287. hasIntersections = true;
  288. mergedRepaintRects[i].union(mergedRepaintRects[j]);
  289. mergedRepaintRects.splice(j, 1);
  290. }
  291. else {
  292. j++;
  293. }
  294. }
  295. i++;
  296. }
  297. } while (hasIntersections);
  298. this._paintRects = mergedRepaintRects;
  299. return mergedRepaintRects;
  300. }
  301. /**
  302. * Get paint rects for debug usage.
  303. */
  304. debugGetPaintRects() {
  305. return (this._paintRects || []).slice();
  306. }
  307. resize(width: number, height: number) {
  308. const dpr = this.dpr;
  309. const dom = this.dom;
  310. const domStyle = dom.style;
  311. const domBack = this.domBack;
  312. if (domStyle) {
  313. domStyle.width = width + 'px';
  314. domStyle.height = height + 'px';
  315. }
  316. dom.width = width * dpr;
  317. dom.height = height * dpr;
  318. if (domBack) {
  319. domBack.width = width * dpr;
  320. domBack.height = height * dpr;
  321. if (dpr !== 1) {
  322. this.ctxBack.scale(dpr, dpr);
  323. }
  324. }
  325. }
  326. /**
  327. * 清空该层画布
  328. */
  329. clear(
  330. clearAll?: boolean,
  331. clearColor?: string | GradientObject | ImagePatternObject,
  332. repaintRects?: BoundingRect[]
  333. ) {
  334. const dom = this.dom;
  335. const ctx = this.ctx;
  336. const width = dom.width;
  337. const height = dom.height;
  338. clearColor = clearColor || this.clearColor;
  339. const haveMotionBLur = this.motionBlur && !clearAll;
  340. const lastFrameAlpha = this.lastFrameAlpha;
  341. const dpr = this.dpr;
  342. const self = this;
  343. if (haveMotionBLur) {
  344. if (!this.domBack) {
  345. this.createBackBuffer();
  346. }
  347. this.ctxBack.globalCompositeOperation = 'copy';
  348. this.ctxBack.drawImage(
  349. dom, 0, 0,
  350. width / dpr,
  351. height / dpr
  352. );
  353. }
  354. const domBack = this.domBack;
  355. function doClear(x: number, y: number, width: number, height: number) {
  356. ctx.clearRect(x, y, width, height);
  357. if (clearColor && clearColor !== 'transparent') {
  358. let clearColorGradientOrPattern;
  359. // Gradient
  360. if (util.isGradientObject(clearColor)) {
  361. // shouldn't cache when clearColor is not global and size changed
  362. const shouldCache = clearColor.global || (
  363. (clearColor as InnerGradientObject).__width === width
  364. && (clearColor as InnerGradientObject).__height === height
  365. );
  366. // Cache canvas gradient
  367. clearColorGradientOrPattern = shouldCache
  368. && (clearColor as InnerGradientObject).__canvasGradient
  369. || getCanvasGradient(ctx, clearColor, {
  370. x: 0,
  371. y: 0,
  372. width: width,
  373. height: height
  374. });
  375. (clearColor as InnerGradientObject).__canvasGradient = clearColorGradientOrPattern;
  376. (clearColor as InnerGradientObject).__width = width;
  377. (clearColor as InnerGradientObject).__height = height;
  378. }
  379. // Pattern
  380. else if (util.isImagePatternObject(clearColor)) {
  381. // scale pattern by dpr
  382. clearColor.scaleX = clearColor.scaleX || dpr;
  383. clearColor.scaleY = clearColor.scaleY || dpr;
  384. clearColorGradientOrPattern = createCanvasPattern(
  385. ctx, clearColor, {
  386. dirty() {
  387. self.setUnpainted();
  388. self.painter.refresh();
  389. }
  390. }
  391. );
  392. }
  393. ctx.save();
  394. ctx.fillStyle = clearColorGradientOrPattern || (clearColor as string);
  395. ctx.fillRect(x, y, width, height);
  396. ctx.restore();
  397. }
  398. if (haveMotionBLur) {
  399. ctx.save();
  400. ctx.globalAlpha = lastFrameAlpha;
  401. ctx.drawImage(domBack, x, y, width, height);
  402. ctx.restore();
  403. }
  404. };
  405. if (!repaintRects || haveMotionBLur) {
  406. // Clear the full canvas
  407. doClear(0, 0, width, height);
  408. }
  409. else if (repaintRects.length) {
  410. // Clear the repaint areas
  411. util.each(repaintRects, rect => {
  412. doClear(
  413. rect.x * dpr,
  414. rect.y * dpr,
  415. rect.width * dpr,
  416. rect.height * dpr
  417. );
  418. });
  419. }
  420. }
  421. // Interface of refresh
  422. refresh: (clearColor?: string | GradientObject | ImagePatternObject) => void
  423. // Interface of renderToCanvas in getRenderedCanvas
  424. renderToCanvas: (ctx: CanvasRenderingContext2D) => void
  425. // Events
  426. onclick: ElementEventCallback<unknown, this>
  427. ondblclick: ElementEventCallback<unknown, this>
  428. onmouseover: ElementEventCallback<unknown, this>
  429. onmouseout: ElementEventCallback<unknown, this>
  430. onmousemove: ElementEventCallback<unknown, this>
  431. onmousewheel: ElementEventCallback<unknown, this>
  432. onmousedown: ElementEventCallback<unknown, this>
  433. onmouseup: ElementEventCallback<unknown, this>
  434. oncontextmenu: ElementEventCallback<unknown, this>
  435. ondrag: ElementEventCallback<unknown, this>
  436. ondragstart: ElementEventCallback<unknown, this>
  437. ondragend: ElementEventCallback<unknown, this>
  438. ondragenter: ElementEventCallback<unknown, this>
  439. ondragleave: ElementEventCallback<unknown, this>
  440. ondragover: ElementEventCallback<unknown, this>
  441. ondrop: ElementEventCallback<unknown, this>
  442. }