graphic.ts 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857
  1. import Displayable, { BeforeBrushParam, DEFAULT_COMMON_STYLE } from '../graphic/Displayable';
  2. import PathProxy from '../core/PathProxy';
  3. import { GradientObject } from '../graphic/Gradient';
  4. import { ImagePatternObject, InnerImagePatternObject } from '../graphic/Pattern';
  5. import { LinearGradientObject } from '../graphic/LinearGradient';
  6. import { RadialGradientObject } from '../graphic/RadialGradient';
  7. import { ZRCanvasRenderingContext } from '../core/types';
  8. import { createOrUpdateImage, isImageReady } from '../graphic/helper/image';
  9. import { getCanvasGradient, isClipPathChanged } from './helper';
  10. import Path, { PathStyleProps } from '../graphic/Path';
  11. import ZRImage, { ImageStyleProps } from '../graphic/Image';
  12. import TSpan, {TSpanStyleProps} from '../graphic/TSpan';
  13. import { MatrixArray } from '../core/matrix';
  14. import { RADIAN_TO_DEGREE } from '../core/util';
  15. import { getLineDash } from './dashStyle';
  16. import { REDRAW_BIT, SHAPE_CHANGED_BIT } from '../graphic/constants';
  17. import type IncrementalDisplayable from '../graphic/IncrementalDisplayable';
  18. import { DEFAULT_FONT } from '../core/platform';
  19. const pathProxyForDraw = new PathProxy(true);
  20. // Not use el#hasStroke because style may be different.
  21. function styleHasStroke(style: PathStyleProps) {
  22. const stroke = style.stroke;
  23. return !(stroke == null || stroke === 'none' || !(style.lineWidth > 0));
  24. }
  25. // ignore lineWidth and must be string
  26. // Expected color but found '[' when color is gradient
  27. function isValidStrokeFillStyle(
  28. strokeOrFill: PathStyleProps['stroke'] | PathStyleProps['fill']
  29. ): strokeOrFill is string {
  30. return typeof strokeOrFill === 'string' && strokeOrFill !== 'none';
  31. }
  32. function styleHasFill(style: PathStyleProps) {
  33. const fill = style.fill;
  34. return fill != null && fill !== 'none';
  35. }
  36. function doFillPath(ctx: CanvasRenderingContext2D, style: PathStyleProps) {
  37. if (style.fillOpacity != null && style.fillOpacity !== 1) {
  38. const originalGlobalAlpha = ctx.globalAlpha;
  39. ctx.globalAlpha = style.fillOpacity * style.opacity;
  40. ctx.fill();
  41. // Set back globalAlpha
  42. ctx.globalAlpha = originalGlobalAlpha;
  43. }
  44. else {
  45. ctx.fill();
  46. }
  47. }
  48. function doStrokePath(ctx: CanvasRenderingContext2D, style: PathStyleProps) {
  49. if (style.strokeOpacity != null && style.strokeOpacity !== 1) {
  50. const originalGlobalAlpha = ctx.globalAlpha;
  51. ctx.globalAlpha = style.strokeOpacity * style.opacity;
  52. ctx.stroke();
  53. // Set back globalAlpha
  54. ctx.globalAlpha = originalGlobalAlpha;
  55. }
  56. else {
  57. ctx.stroke();
  58. }
  59. }
  60. export function createCanvasPattern(
  61. this: void,
  62. ctx: CanvasRenderingContext2D,
  63. pattern: ImagePatternObject,
  64. el: {dirty: () => void}
  65. ): CanvasPattern {
  66. const image = createOrUpdateImage(pattern.image, (pattern as InnerImagePatternObject).__image, el);
  67. if (isImageReady(image)) {
  68. const canvasPattern = ctx.createPattern(image, pattern.repeat || 'repeat');
  69. if (
  70. typeof DOMMatrix === 'function'
  71. && canvasPattern // image may be not ready
  72. && canvasPattern.setTransform // setTransform may not be supported in some old devices.
  73. ) {
  74. const matrix = new DOMMatrix();
  75. matrix.translateSelf((pattern.x || 0), (pattern.y || 0));
  76. matrix.rotateSelf(0, 0, (pattern.rotation || 0) * RADIAN_TO_DEGREE);
  77. matrix.scaleSelf((pattern.scaleX || 1), (pattern.scaleY || 1));
  78. canvasPattern.setTransform(matrix);
  79. }
  80. return canvasPattern;
  81. }
  82. }
  83. // Draw Path Elements
  84. function brushPath(
  85. ctx: CanvasRenderingContext2D,
  86. el: Path,
  87. style: PathStyleProps,
  88. canBatch: boolean,
  89. scope: BrushScope
  90. ) {
  91. let hasStroke = styleHasStroke(style);
  92. let hasFill = styleHasFill(style);
  93. const strokePercent = style.strokePercent;
  94. const strokePart = strokePercent < 1;
  95. // TODO Reduce path memory cost.
  96. const firstDraw = !el.path;
  97. // Create path for each element when:
  98. // 1. Element has interactions.
  99. // 2. Element draw part of the line.
  100. if ((!el.silent || strokePart) && firstDraw) {
  101. el.createPathProxy();
  102. }
  103. const path = el.path || pathProxyForDraw;
  104. const dirtyFlag = el.__dirty;
  105. if (!canBatch) {
  106. const fill = style.fill;
  107. const stroke = style.stroke;
  108. const hasFillGradient = hasFill && !!(fill as GradientObject).colorStops;
  109. const hasStrokeGradient = hasStroke && !!(stroke as GradientObject).colorStops;
  110. const hasFillPattern = hasFill && !!(fill as ImagePatternObject).image;
  111. const hasStrokePattern = hasStroke && !!(stroke as ImagePatternObject).image;
  112. let fillGradient;
  113. let strokeGradient;
  114. let fillPattern;
  115. let strokePattern;
  116. let rect;
  117. if (hasFillGradient || hasStrokeGradient) {
  118. rect = el.getBoundingRect();
  119. }
  120. // Update gradient because bounding rect may changed
  121. if (hasFillGradient) {
  122. fillGradient = dirtyFlag
  123. ? getCanvasGradient(ctx, fill as (LinearGradientObject | RadialGradientObject), rect)
  124. : el.__canvasFillGradient;
  125. // No need to clear cache when fill is not gradient.
  126. // It will always been updated when fill changed back to gradient.
  127. el.__canvasFillGradient = fillGradient;
  128. }
  129. if (hasStrokeGradient) {
  130. strokeGradient = dirtyFlag
  131. ? getCanvasGradient(ctx, stroke as (LinearGradientObject | RadialGradientObject), rect)
  132. : el.__canvasStrokeGradient;
  133. el.__canvasStrokeGradient = strokeGradient;
  134. }
  135. if (hasFillPattern) {
  136. // Pattern might be null if image not ready (even created from dataURI)
  137. fillPattern = (dirtyFlag || !el.__canvasFillPattern)
  138. ? createCanvasPattern(ctx, fill as ImagePatternObject, el)
  139. : el.__canvasFillPattern;
  140. el.__canvasFillPattern = fillPattern;
  141. }
  142. if (hasStrokePattern) {
  143. // Pattern might be null if image not ready (even created from dataURI)
  144. strokePattern = (dirtyFlag || !el.__canvasStrokePattern)
  145. ? createCanvasPattern(ctx, stroke as ImagePatternObject, el)
  146. : el.__canvasStrokePattern;
  147. el.__canvasStrokePattern = strokePattern;
  148. }
  149. // Use the gradient or pattern
  150. if (hasFillGradient) {
  151. // PENDING If may have affect the state
  152. ctx.fillStyle = fillGradient;
  153. }
  154. else if (hasFillPattern) {
  155. if (fillPattern) { // createCanvasPattern may return false if image is not ready.
  156. ctx.fillStyle = fillPattern;
  157. }
  158. else {
  159. // Don't fill if image is not ready
  160. hasFill = false;
  161. }
  162. }
  163. if (hasStrokeGradient) {
  164. ctx.strokeStyle = strokeGradient;
  165. }
  166. else if (hasStrokePattern) {
  167. if (strokePattern) {
  168. ctx.strokeStyle = strokePattern;
  169. }
  170. else {
  171. // Don't stroke if image is not ready
  172. hasStroke = false;
  173. }
  174. }
  175. }
  176. // Update path sx, sy
  177. const scale = el.getGlobalScale();
  178. path.setScale(scale[0], scale[1], el.segmentIgnoreThreshold);
  179. let lineDash;
  180. let lineDashOffset;
  181. if (ctx.setLineDash && style.lineDash) {
  182. [lineDash, lineDashOffset] = getLineDash(el);
  183. }
  184. let needsRebuild = true;
  185. if (firstDraw || (dirtyFlag & SHAPE_CHANGED_BIT)) {
  186. path.setDPR((ctx as any).dpr);
  187. if (strokePart) {
  188. // Use rebuildPath for percent stroke, so no context.
  189. path.setContext(null);
  190. }
  191. else {
  192. path.setContext(ctx);
  193. needsRebuild = false;
  194. }
  195. path.reset();
  196. el.buildPath(path, el.shape, canBatch);
  197. path.toStatic();
  198. // Clear path dirty flag
  199. el.pathUpdated();
  200. }
  201. // Not support separate fill and stroke. For the compatibility of SVG
  202. if (needsRebuild) {
  203. path.rebuildPath(ctx, strokePart ? strokePercent : 1);
  204. }
  205. if (lineDash) {
  206. ctx.setLineDash(lineDash);
  207. ctx.lineDashOffset = lineDashOffset;
  208. }
  209. if (!canBatch) {
  210. if (style.strokeFirst) {
  211. if (hasStroke) {
  212. doStrokePath(ctx, style);
  213. }
  214. if (hasFill) {
  215. doFillPath(ctx, style);
  216. }
  217. }
  218. else {
  219. if (hasFill) {
  220. doFillPath(ctx, style);
  221. }
  222. if (hasStroke) {
  223. doStrokePath(ctx, style);
  224. }
  225. }
  226. }
  227. else {
  228. // Note that flushPathDrawn has been executed if !canBatchPath .
  229. scope.batchFill = hasFill;
  230. scope.batchStroke = hasStroke;
  231. }
  232. if (lineDash) {
  233. // PENDING
  234. // Remove lineDash
  235. ctx.setLineDash([]);
  236. }
  237. }
  238. // Draw Image Elements
  239. function brushImage(ctx: CanvasRenderingContext2D, el: ZRImage, style: ImageStyleProps) {
  240. const image = el.__image = createOrUpdateImage(
  241. style.image,
  242. el.__image,
  243. el,
  244. el.onload
  245. );
  246. if (!image || !isImageReady(image)) {
  247. return;
  248. }
  249. const x = style.x || 0;
  250. const y = style.y || 0;
  251. let width = el.getWidth();
  252. let height = el.getHeight();
  253. const aspect = image.width / image.height;
  254. if (width == null && height != null) {
  255. // Keep image/height ratio
  256. width = height * aspect;
  257. }
  258. else if (height == null && width != null) {
  259. height = width / aspect;
  260. }
  261. else if (width == null && height == null) {
  262. width = image.width;
  263. height = image.height;
  264. }
  265. if (style.sWidth && style.sHeight) {
  266. const sx = style.sx || 0;
  267. const sy = style.sy || 0;
  268. ctx.drawImage(
  269. image,
  270. sx, sy, style.sWidth, style.sHeight,
  271. x, y, width, height
  272. );
  273. }
  274. else if (style.sx && style.sy) {
  275. const sx = style.sx;
  276. const sy = style.sy;
  277. const sWidth = width - sx;
  278. const sHeight = height - sy;
  279. ctx.drawImage(
  280. image,
  281. sx, sy, sWidth, sHeight,
  282. x, y, width, height
  283. );
  284. }
  285. else {
  286. ctx.drawImage(image, x, y, width, height);
  287. }
  288. }
  289. // Draw Text Elements
  290. function brushText(ctx: CanvasRenderingContext2D, el: TSpan, style: TSpanStyleProps) {
  291. let text = style.text;
  292. // Convert to string
  293. text != null && (text += '');
  294. if (text) {
  295. ctx.font = style.font || DEFAULT_FONT;
  296. ctx.textAlign = style.textAlign;
  297. ctx.textBaseline = style.textBaseline;
  298. let lineDash;
  299. let lineDashOffset;
  300. if (ctx.setLineDash && style.lineDash) {
  301. [lineDash, lineDashOffset] = getLineDash(el);
  302. }
  303. if (lineDash) {
  304. ctx.setLineDash(lineDash);
  305. ctx.lineDashOffset = lineDashOffset;
  306. }
  307. if (style.strokeFirst) {
  308. if (styleHasStroke(style)) {
  309. ctx.strokeText(text, style.x, style.y);
  310. }
  311. if (styleHasFill(style)) {
  312. ctx.fillText(text, style.x, style.y);
  313. }
  314. }
  315. else {
  316. if (styleHasFill(style)) {
  317. ctx.fillText(text, style.x, style.y);
  318. }
  319. if (styleHasStroke(style)) {
  320. ctx.strokeText(text, style.x, style.y);
  321. }
  322. }
  323. if (lineDash) {
  324. // Remove lineDash
  325. ctx.setLineDash([]);
  326. }
  327. }
  328. }
  329. const SHADOW_NUMBER_PROPS = ['shadowBlur', 'shadowOffsetX', 'shadowOffsetY'] as const;
  330. const STROKE_PROPS = [
  331. ['lineCap', 'butt'], ['lineJoin', 'miter'], ['miterLimit', 10]
  332. ] as const;
  333. type AllStyleOption = PathStyleProps | TSpanStyleProps | ImageStyleProps;
  334. // type ShadowPropNames = typeof SHADOW_PROPS[number][0];
  335. // type StrokePropNames = typeof STROKE_PROPS[number][0];
  336. // type DrawPropNames = typeof DRAW_PROPS[number][0];
  337. function bindCommonProps(
  338. ctx: CanvasRenderingContext2D,
  339. style: AllStyleOption,
  340. prevStyle: AllStyleOption,
  341. forceSetAll: boolean,
  342. scope: BrushScope
  343. ): boolean {
  344. let styleChanged = false;
  345. if (!forceSetAll) {
  346. prevStyle = prevStyle || {};
  347. // Shared same style.
  348. if (style === prevStyle) {
  349. return false;
  350. }
  351. }
  352. if (forceSetAll || style.opacity !== prevStyle.opacity) {
  353. flushPathDrawn(ctx, scope);
  354. styleChanged = true;
  355. // Ensure opacity is between 0 ~ 1. Invalid opacity will lead to a failure set and use the leaked opacity from the previous.
  356. const opacity = Math.max(Math.min(style.opacity, 1), 0);
  357. ctx.globalAlpha = isNaN(opacity) ? DEFAULT_COMMON_STYLE.opacity : opacity;
  358. }
  359. if (forceSetAll || style.blend !== prevStyle.blend) {
  360. if (!styleChanged) {
  361. flushPathDrawn(ctx, scope);
  362. styleChanged = true;
  363. }
  364. ctx.globalCompositeOperation = style.blend || DEFAULT_COMMON_STYLE.blend;
  365. }
  366. for (let i = 0; i < SHADOW_NUMBER_PROPS.length; i++) {
  367. const propName = SHADOW_NUMBER_PROPS[i];
  368. if (forceSetAll || style[propName] !== prevStyle[propName]) {
  369. if (!styleChanged) {
  370. flushPathDrawn(ctx, scope);
  371. styleChanged = true;
  372. }
  373. // FIXME Invalid property value will cause style leak from previous element.
  374. ctx[propName] = (ctx as ZRCanvasRenderingContext).dpr * (style[propName] || 0);
  375. }
  376. }
  377. if (forceSetAll || style.shadowColor !== prevStyle.shadowColor) {
  378. if (!styleChanged) {
  379. flushPathDrawn(ctx, scope);
  380. styleChanged = true;
  381. }
  382. ctx.shadowColor = style.shadowColor || DEFAULT_COMMON_STYLE.shadowColor;
  383. }
  384. return styleChanged;
  385. }
  386. function bindPathAndTextCommonStyle(
  387. ctx: CanvasRenderingContext2D,
  388. el: TSpan | Path,
  389. prevEl: TSpan | Path,
  390. forceSetAll: boolean,
  391. scope: BrushScope
  392. ) {
  393. const style = el.style;
  394. const prevStyle = forceSetAll
  395. ? null
  396. : (prevEl && prevEl.style || {});
  397. // Shared same style. prevStyle will be null if forceSetAll.
  398. if (style === prevStyle) {
  399. return false;
  400. }
  401. let styleChanged = bindCommonProps(ctx, style, prevStyle, forceSetAll, scope);
  402. if (forceSetAll || style.fill !== prevStyle.fill) {
  403. if (!styleChanged) {
  404. // Flush before set
  405. flushPathDrawn(ctx, scope);
  406. styleChanged = true;
  407. }
  408. isValidStrokeFillStyle(style.fill) && (ctx.fillStyle = style.fill);
  409. }
  410. if (forceSetAll || style.stroke !== prevStyle.stroke) {
  411. if (!styleChanged) {
  412. flushPathDrawn(ctx, scope);
  413. styleChanged = true;
  414. }
  415. isValidStrokeFillStyle(style.stroke) && (ctx.strokeStyle = style.stroke);
  416. }
  417. if (forceSetAll || style.opacity !== prevStyle.opacity) {
  418. if (!styleChanged) {
  419. flushPathDrawn(ctx, scope);
  420. styleChanged = true;
  421. }
  422. ctx.globalAlpha = style.opacity == null ? 1 : style.opacity;
  423. }
  424. if (el.hasStroke()) {
  425. const lineWidth = style.lineWidth;
  426. const newLineWidth = lineWidth / (
  427. (style.strokeNoScale && el.getLineScale) ? el.getLineScale() : 1
  428. );
  429. if (ctx.lineWidth !== newLineWidth) {
  430. if (!styleChanged) {
  431. flushPathDrawn(ctx, scope);
  432. styleChanged = true;
  433. }
  434. ctx.lineWidth = newLineWidth;
  435. }
  436. }
  437. for (let i = 0; i < STROKE_PROPS.length; i++) {
  438. const prop = STROKE_PROPS[i];
  439. const propName = prop[0];
  440. if (forceSetAll || style[propName] !== prevStyle[propName]) {
  441. if (!styleChanged) {
  442. flushPathDrawn(ctx, scope);
  443. styleChanged = true;
  444. }
  445. // FIXME Invalid property value will cause style leak from previous element.
  446. (ctx as any)[propName] = style[propName] || prop[1];
  447. }
  448. }
  449. return styleChanged;
  450. }
  451. function bindImageStyle(
  452. ctx: CanvasRenderingContext2D,
  453. el: ZRImage,
  454. prevEl: ZRImage,
  455. // forceSetAll must be true if prevEl is null
  456. forceSetAll: boolean,
  457. scope: BrushScope
  458. ) {
  459. return bindCommonProps(
  460. ctx,
  461. el.style,
  462. prevEl && prevEl.style,
  463. forceSetAll,
  464. scope
  465. );
  466. }
  467. function setContextTransform(ctx: CanvasRenderingContext2D, el: Displayable) {
  468. const m = el.transform;
  469. const dpr = (ctx as ZRCanvasRenderingContext).dpr || 1;
  470. if (m) {
  471. ctx.setTransform(dpr * m[0], dpr * m[1], dpr * m[2], dpr * m[3], dpr * m[4], dpr * m[5]);
  472. }
  473. else {
  474. ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  475. }
  476. }
  477. function updateClipStatus(clipPaths: Path[], ctx: CanvasRenderingContext2D, scope: BrushScope) {
  478. let allClipped = false;
  479. for (let i = 0; i < clipPaths.length; i++) {
  480. const clipPath = clipPaths[i];
  481. // Ignore draw following elements if clipPath has zero area.
  482. allClipped = allClipped || clipPath.isZeroArea();
  483. setContextTransform(ctx, clipPath);
  484. ctx.beginPath();
  485. clipPath.buildPath(ctx, clipPath.shape);
  486. ctx.clip();
  487. }
  488. scope.allClipped = allClipped;
  489. }
  490. function isTransformChanged(m0: MatrixArray, m1: MatrixArray): boolean {
  491. if (m0 && m1) {
  492. return m0[0] !== m1[0]
  493. || m0[1] !== m1[1]
  494. || m0[2] !== m1[2]
  495. || m0[3] !== m1[3]
  496. || m0[4] !== m1[4]
  497. || m0[5] !== m1[5];
  498. }
  499. else if (!m0 && !m1) { // All identity matrix.
  500. return false;
  501. }
  502. return true;
  503. }
  504. const DRAW_TYPE_PATH = 1;
  505. const DRAW_TYPE_IMAGE = 2;
  506. const DRAW_TYPE_TEXT = 3;
  507. const DRAW_TYPE_INCREMENTAL = 4;
  508. export type BrushScope = {
  509. inHover: boolean
  510. // width / height of viewport
  511. viewWidth: number
  512. viewHeight: number
  513. // Status for clipping
  514. prevElClipPaths?: Path[] // Only paths with length > 0 can be assigned.
  515. prevEl?: Displayable
  516. allClipped?: boolean // If the whole element can be clipped
  517. // Status for batching
  518. batchFill?: boolean
  519. batchStroke?: boolean
  520. lastDrawType?: number
  521. beforeBrushParam: BeforeBrushParam
  522. }
  523. // If path can be batched
  524. function canPathBatch(style: PathStyleProps) {
  525. const hasFill = styleHasFill(style);
  526. const hasStroke = styleHasStroke(style);
  527. return !(
  528. // Line dash is dynamically set in brush function.
  529. style.lineDash
  530. // Can't batch if element is both set fill and stroke. Or both not set
  531. || !(+hasFill ^ +hasStroke)
  532. // Can't batch if element is drawn with gradient or pattern.
  533. || (hasFill && typeof style.fill !== 'string')
  534. || (hasStroke && typeof style.stroke !== 'string')
  535. // Can't batch if element only stroke part of line.
  536. || style.strokePercent < 1
  537. // Has stroke or fill opacity
  538. || style.strokeOpacity < 1
  539. || style.fillOpacity < 1
  540. );
  541. }
  542. // Should be idempotent - may be called more than necessary.
  543. function flushPathDrawn(ctx: CanvasRenderingContext2D, scope: BrushScope) {
  544. if (scope.batchFill) {
  545. scope.batchFill = false;
  546. ctx.fill();
  547. }
  548. if (scope.batchStroke) {
  549. scope.batchStroke = false;
  550. ctx.stroke();
  551. }
  552. }
  553. export function brushSingle(ctx: CanvasRenderingContext2D, el: Displayable) {
  554. const scope = { inHover: false, viewWidth: 0, viewHeight: 0, beforeBrushParam: {} };
  555. brush(ctx, el, scope);
  556. brushLoopFinalize(ctx, scope);
  557. }
  558. // Brush different type of elements.
  559. export function brush(
  560. ctx: CanvasRenderingContext2D,
  561. el: Displayable,
  562. scope: BrushScope,
  563. ) {
  564. const m = el.transform;
  565. if (!el.shouldBePainted(scope.viewWidth, scope.viewHeight, false, false)) {
  566. // Needs to mark el rendered.
  567. // Or this element will always been rendered in progressive rendering.
  568. // But other dirty bit should not be cleared, otherwise it cause the shape
  569. // can not be updated in this case.
  570. el.__dirty &= ~REDRAW_BIT;
  571. el.__isRendered = false;
  572. return;
  573. }
  574. // HANDLE CLIPPING
  575. const clipPaths = el.__clipPaths;
  576. const prevElClipPaths = scope.prevElClipPaths;
  577. const style = el.style;
  578. let forceSetTransform = false;
  579. let forceSetStyle = false;
  580. // Optimize when clipping on group with several elements
  581. if (!prevElClipPaths || isClipPathChanged(clipPaths, prevElClipPaths)) {
  582. // If has previous clipping state, restore from it
  583. if (prevElClipPaths) {
  584. // Flush restore
  585. flushPathDrawn(ctx, scope);
  586. ctx.restore();
  587. // Must set all style and transform because context changed by restore
  588. forceSetStyle = forceSetTransform = true;
  589. scope.prevElClipPaths = null;
  590. scope.allClipped = false;
  591. // Reset prevEl since context has been restored
  592. scope.prevEl = null;
  593. }
  594. // New clipping state
  595. if (clipPaths && clipPaths.length) {
  596. // Flush before clip
  597. flushPathDrawn(ctx, scope);
  598. ctx.save();
  599. updateClipStatus(clipPaths, ctx, scope);
  600. // Must set transform because it's changed when clip.
  601. forceSetTransform = true;
  602. scope.prevElClipPaths = clipPaths;
  603. }
  604. }
  605. // Not rendering elements if it's clipped by a zero area path.
  606. // Or it may cause bug on some version of IE11 (like 11.0.9600.178**),
  607. // where exception "unexpected call to method or property access"
  608. // might be thrown when calling ctx.fill or ctx.stroke after a path
  609. // whose area size is zero is drawn and ctx.clip() is called and
  610. // shadowBlur is set. See #4572, #3112, #5777.
  611. // (e.g.,
  612. // ctx.moveTo(10, 10);
  613. // ctx.lineTo(20, 10);
  614. // ctx.closePath();
  615. // ctx.clip();
  616. // ctx.shadowBlur = 10;
  617. // ...
  618. // ctx.fill();
  619. // )
  620. if (scope.allClipped) {
  621. // Needs to mark el rendered.
  622. // Or this element will always been rendered in progressive rendering.
  623. // But other dirty bit should not be cleared, otherwise it cause the shape
  624. // can not be updated in this case.
  625. el.__dirty &= ~REDRAW_BIT;
  626. el.__isRendered = false;
  627. return;
  628. }
  629. // START BRUSH
  630. el.beforeBrush && el.beforeBrush(scope.beforeBrushParam);
  631. el.innerBeforeBrush();
  632. const prevEl = scope.prevEl;
  633. // TODO el type changed.
  634. if (!prevEl) {
  635. forceSetStyle = forceSetTransform = true;
  636. }
  637. let canBatchPath = el instanceof Path // Only path supports batch
  638. && el.autoBatch
  639. && canPathBatch(style);
  640. if (forceSetTransform || isTransformChanged(m, prevEl.transform)) {
  641. // Flush
  642. flushPathDrawn(ctx, scope);
  643. setContextTransform(ctx, el);
  644. }
  645. else if (!canBatchPath) {
  646. // Flush
  647. flushPathDrawn(ctx, scope);
  648. }
  649. if (el instanceof Path) {
  650. // PENDING do we need to rebind all style if displayable type changed?
  651. if (scope.lastDrawType !== DRAW_TYPE_PATH) {
  652. forceSetStyle = true;
  653. scope.lastDrawType = DRAW_TYPE_PATH;
  654. }
  655. bindPathAndTextCommonStyle(ctx, el as Path, prevEl as Path, forceSetStyle, scope);
  656. // Begin path at start
  657. // (can be skipped only if this el can be batched and there are previous batched rendering).
  658. if (!canBatchPath || (!scope.batchFill && !scope.batchStroke)) {
  659. ctx.beginPath();
  660. }
  661. brushPath(ctx, el as Path, style, canBatchPath, scope);
  662. }
  663. else {
  664. if (el instanceof TSpan) {
  665. if (scope.lastDrawType !== DRAW_TYPE_TEXT) {
  666. forceSetStyle = true;
  667. scope.lastDrawType = DRAW_TYPE_TEXT;
  668. }
  669. bindPathAndTextCommonStyle(ctx, el as TSpan, prevEl as TSpan, forceSetStyle, scope);
  670. brushText(ctx, el as TSpan, style);
  671. }
  672. else if (el instanceof ZRImage) {
  673. if (scope.lastDrawType !== DRAW_TYPE_IMAGE) {
  674. forceSetStyle = true;
  675. scope.lastDrawType = DRAW_TYPE_IMAGE;
  676. }
  677. bindImageStyle(ctx, el as ZRImage, prevEl as ZRImage, forceSetStyle, scope);
  678. brushImage(ctx, el as ZRImage, style);
  679. }
  680. // Assume it's a IncrementalDisplayable
  681. else if ((el as IncrementalDisplayable).getTemporalDisplayables) {
  682. if (scope.lastDrawType !== DRAW_TYPE_INCREMENTAL) {
  683. forceSetStyle = true;
  684. scope.lastDrawType = DRAW_TYPE_INCREMENTAL;
  685. }
  686. brushIncremental(ctx, el as IncrementalDisplayable, scope);
  687. }
  688. }
  689. el.innerAfterBrush();
  690. if (el.afterBrush) {
  691. if (canBatchPath) {
  692. flushPathDrawn(ctx, scope);
  693. // If !canBatch, flushPathDrawn has been executed before.
  694. // el.afterBrush may call methods such as ctx.fillRect .
  695. }
  696. el.afterBrush();
  697. }
  698. scope.prevEl = el;
  699. // Mark as painted.
  700. el.__dirty = 0;
  701. el.__isRendered = true;
  702. }
  703. /**
  704. * Must be called after `brush()` iteration finished, regardless of whether
  705. * reaching `layer.__endIndex`.
  706. *
  707. * NOTE: This method may be called with all `brush()` are skipped.
  708. */
  709. export function brushLoopFinalize(
  710. ctx: CanvasRenderingContext2D,
  711. scope: BrushScope
  712. ) {
  713. flushPathDrawn(ctx, scope);
  714. if (scope.prevElClipPaths) {
  715. // Needs restore the state. If last drawn element is in the clipping area.
  716. // NOTE: It should not be called before el.afterBrush, since el.afterBrush
  717. // may call methods such as ctx.fillRect.
  718. ctx.restore();
  719. // No need to clear scope, since it should no longer be used.
  720. }
  721. }
  722. function brushIncremental(
  723. ctx: CanvasRenderingContext2D,
  724. el: IncrementalDisplayable,
  725. scope: BrushScope
  726. ) {
  727. let displayables = el.getDisplayables();
  728. let temporalDisplayables = el.getTemporalDisplayables();
  729. // Provide an inner scope.
  730. // Save current context and restore after brushed.
  731. ctx.save();
  732. let innerScope: BrushScope = {
  733. prevElClipPaths: null,
  734. prevEl: null,
  735. allClipped: false,
  736. viewWidth: scope.viewWidth,
  737. viewHeight: scope.viewHeight,
  738. inHover: scope.inHover,
  739. beforeBrushParam: {}
  740. };
  741. let i;
  742. let len;
  743. // Render persistant displayables.
  744. for (i = el.getCursor(), len = displayables.length; i < len; i++) {
  745. const displayable = displayables[i];
  746. displayable.beforeBrush && displayable.beforeBrush(scope.beforeBrushParam);
  747. displayable.innerBeforeBrush();
  748. brush(ctx, displayable, innerScope);
  749. displayable.innerAfterBrush();
  750. displayable.afterBrush && displayable.afterBrush();
  751. innerScope.prevEl = displayable;
  752. }
  753. brushLoopFinalize(ctx, innerScope);
  754. // Render temporary displayables.
  755. for (let i = 0, len = temporalDisplayables.length; i < len; i++) {
  756. const displayable = temporalDisplayables[i];
  757. displayable.beforeBrush && displayable.beforeBrush(scope.beforeBrushParam);
  758. displayable.innerBeforeBrush();
  759. brush(ctx, displayable, innerScope);
  760. displayable.innerAfterBrush();
  761. displayable.afterBrush && displayable.afterBrush();
  762. innerScope.prevEl = displayable;
  763. }
  764. brushLoopFinalize(ctx, innerScope);
  765. el.clearTemporalDisplayables();
  766. el.notClear = true;
  767. ctx.restore();
  768. }