0cf69b357809e15c6ccaf6c6637a4541aebfe24e66ffb0467bf4766a13f151020bccb0d96bac07aa97dcefbf95a86a71c9f97fdf7246ec29e1dce565eaac04 17 KB

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