fee1d25ddb9619dad8edbe0bc519f36e81f77366016185186f52d72c221e7a5f3430bb1768e2bbc963d27c346f85ef7d7c4387a651a96090a44894cc129d7d 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  1. /**
  2. * Base class of all displayable graphic objects
  3. */
  4. import Element, {ElementProps, ElementStatePropNames, ElementAnimateConfig, ElementCommonState} from '../Element';
  5. import BoundingRect from '../core/BoundingRect';
  6. import { PropType, Dictionary, MapToType } from '../core/types';
  7. import Path from './Path';
  8. import { keys, extend, createObject } from '../core/util';
  9. import Animator from '../animation/Animator';
  10. import { REDRAW_BIT, STYLE_CHANGED_BIT } from './constants';
  11. // type CalculateTextPositionResult = ReturnType<typeof calculateTextPosition>
  12. const STYLE_MAGIC_KEY = '__zr_style_' + Math.round((Math.random() * 10));
  13. export interface CommonStyleProps {
  14. shadowBlur?: number
  15. shadowOffsetX?: number
  16. shadowOffsetY?: number
  17. shadowColor?: string
  18. opacity?: number
  19. /**
  20. * https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
  21. */
  22. blend?: string
  23. }
  24. export const DEFAULT_COMMON_STYLE: CommonStyleProps = {
  25. shadowBlur: 0,
  26. shadowOffsetX: 0,
  27. shadowOffsetY: 0,
  28. shadowColor: '#000',
  29. opacity: 1,
  30. blend: 'source-over'
  31. };
  32. export const DEFAULT_COMMON_ANIMATION_PROPS: MapToType<DisplayableProps, boolean> = {
  33. style: {
  34. shadowBlur: true,
  35. shadowOffsetX: true,
  36. shadowOffsetY: true,
  37. shadowColor: true,
  38. opacity: true
  39. }
  40. };
  41. (DEFAULT_COMMON_STYLE as any)[STYLE_MAGIC_KEY] = true;
  42. export interface DisplayableProps extends ElementProps {
  43. style?: Dictionary<any>
  44. zlevel?: number
  45. z?: number
  46. z2?: number
  47. culling?: boolean
  48. // TODO list all cursors
  49. cursor?: string
  50. rectHover?: boolean
  51. progressive?: boolean
  52. incremental?: boolean
  53. ignoreCoarsePointer?: boolean
  54. batch?: boolean
  55. invisible?: boolean
  56. }
  57. type DisplayableKey = keyof DisplayableProps
  58. type DisplayablePropertyType = PropType<DisplayableProps, DisplayableKey>
  59. export type DisplayableStatePropNames = ElementStatePropNames | 'style' | 'z' | 'z2' | 'invisible';
  60. export type DisplayableState = Pick<DisplayableProps, DisplayableStatePropNames> & ElementCommonState;
  61. const PRIMARY_STATES_KEYS = ['z', 'z2', 'invisible'] as const;
  62. const PRIMARY_STATES_KEYS_IN_HOVER_LAYER = ['invisible'] as const;
  63. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  64. interface Displayable<Props extends DisplayableProps = DisplayableProps> {
  65. animate(key?: '', loop?: boolean): Animator<this>
  66. animate(key: 'style', loop?: boolean): Animator<this['style']>
  67. getState(stateName: string): DisplayableState
  68. ensureState(stateName: string): DisplayableState
  69. states: Dictionary<DisplayableState>
  70. stateProxy: (stateName: string) => DisplayableState
  71. }
  72. class Displayable<Props extends DisplayableProps = DisplayableProps> extends Element<Props> {
  73. /**
  74. * Whether the displayable object is visible. when it is true, the displayable object
  75. * is not drawn, but the mouse event can still trigger the object.
  76. */
  77. invisible: boolean
  78. z: number
  79. z2: number
  80. /**
  81. * The z level determines the displayable object can be drawn in which layer canvas.
  82. */
  83. zlevel: number
  84. /**
  85. * If enable culling
  86. */
  87. culling: boolean
  88. /**
  89. * Mouse cursor when hovered
  90. */
  91. cursor: string
  92. /**
  93. * If hover area is bounding rect
  94. */
  95. rectHover: boolean
  96. /**
  97. * For increamental rendering
  98. */
  99. incremental: boolean
  100. /**
  101. * Never increase to target size
  102. */
  103. ignoreCoarsePointer?: boolean
  104. style: Dictionary<any>
  105. protected _normalState: DisplayableState
  106. protected _rect: BoundingRect
  107. protected _paintRect: BoundingRect
  108. protected _prevPaintRect: BoundingRect
  109. dirtyRectTolerance: number
  110. /************* Properties will be inejected in other modules. *******************/
  111. // @deprecated.
  112. useHoverLayer?: boolean
  113. __hoverStyle?: CommonStyleProps
  114. // TODO use WeakMap?
  115. // Shapes for cascade clipping.
  116. // Can only be `null`/`undefined` or an non-empty array, MUST NOT be an empty array.
  117. // because it is easy to only using null to check whether clipPaths changed.
  118. __clipPaths?: Path[]
  119. // FOR CANVAS PAINTER
  120. __canvasFillGradient: CanvasGradient
  121. __canvasStrokeGradient: CanvasGradient
  122. __canvasFillPattern: CanvasPattern
  123. __canvasStrokePattern: CanvasPattern
  124. // FOR SVG PAINTER
  125. __svgEl: SVGElement
  126. constructor(props?: Props) {
  127. super(props);
  128. }
  129. protected _init(props?: Props) {
  130. // Init default properties
  131. const keysArr = keys(props);
  132. for (let i = 0; i < keysArr.length; i++) {
  133. const key = keysArr[i];
  134. if (key === 'style') {
  135. this.useStyle(props[key] as Props['style']);
  136. }
  137. else {
  138. super.attrKV(key as any, props[key]);
  139. }
  140. }
  141. // Give a empty style
  142. if (!this.style) {
  143. this.useStyle({});
  144. }
  145. }
  146. // Hook provided to developers.
  147. beforeBrush() {}
  148. afterBrush() {}
  149. // Hook provided to inherited classes.
  150. // Executed between beforeBrush / afterBrush
  151. innerBeforeBrush() {}
  152. innerAfterBrush() {}
  153. shouldBePainted(
  154. viewWidth: number,
  155. viewHeight: number,
  156. considerClipPath: boolean,
  157. considerAncestors: boolean
  158. ) {
  159. const m = this.transform;
  160. if (
  161. this.ignore
  162. // Ignore invisible element
  163. || this.invisible
  164. // Ignore transparent element
  165. || this.style.opacity === 0
  166. // Ignore culled element
  167. || (this.culling
  168. && isDisplayableCulled(this, viewWidth, viewHeight)
  169. )
  170. // Ignore scale 0 element, in some environment like node-canvas
  171. // Draw a scale 0 element can cause all following draw wrong
  172. // And setTransform with scale 0 will cause set back transform failed.
  173. || (m && !m[0] && !m[3])
  174. ) {
  175. return false;
  176. }
  177. if (considerClipPath && this.__clipPaths) {
  178. for (let i = 0; i < this.__clipPaths.length; ++i) {
  179. if (this.__clipPaths[i].isZeroArea()) {
  180. return false;
  181. }
  182. }
  183. }
  184. if (considerAncestors && this.parent) {
  185. let parent = this.parent;
  186. while (parent) {
  187. if (parent.ignore) {
  188. return false;
  189. }
  190. parent = parent.parent;
  191. }
  192. }
  193. return true;
  194. }
  195. /**
  196. * If displayable element contain coord x, y
  197. */
  198. contain(x: number, y: number) {
  199. return this.rectContain(x, y);
  200. }
  201. traverse<Context>(
  202. cb: (this: Context, el: this) => void,
  203. context?: Context
  204. ) {
  205. cb.call(context, this);
  206. }
  207. /**
  208. * If bounding rect of element contain coord x, y
  209. */
  210. rectContain(x: number, y: number) {
  211. const coord = this.transformCoordToLocal(x, y);
  212. const rect = this.getBoundingRect();
  213. return rect.contain(coord[0], coord[1]);
  214. }
  215. getPaintRect(): BoundingRect {
  216. let rect = this._paintRect;
  217. if (!this._paintRect || this.__dirty) {
  218. const transform = this.transform;
  219. const elRect = this.getBoundingRect();
  220. const style = this.style;
  221. const shadowSize = style.shadowBlur || 0;
  222. const shadowOffsetX = style.shadowOffsetX || 0;
  223. const shadowOffsetY = style.shadowOffsetY || 0;
  224. rect = this._paintRect || (this._paintRect = new BoundingRect(0, 0, 0, 0));
  225. if (transform) {
  226. BoundingRect.applyTransform(rect, elRect, transform);
  227. }
  228. else {
  229. rect.copy(elRect);
  230. }
  231. if (shadowSize || shadowOffsetX || shadowOffsetY) {
  232. rect.width += shadowSize * 2 + Math.abs(shadowOffsetX);
  233. rect.height += shadowSize * 2 + Math.abs(shadowOffsetY);
  234. rect.x = Math.min(rect.x, rect.x + shadowOffsetX - shadowSize);
  235. rect.y = Math.min(rect.y, rect.y + shadowOffsetY - shadowSize);
  236. }
  237. // For the accuracy tolerance of text height or line joint point
  238. const tolerance = this.dirtyRectTolerance;
  239. if (!rect.isZero()) {
  240. rect.x = Math.floor(rect.x - tolerance);
  241. rect.y = Math.floor(rect.y - tolerance);
  242. rect.width = Math.ceil(rect.width + 1 + tolerance * 2);
  243. rect.height = Math.ceil(rect.height + 1 + tolerance * 2);
  244. }
  245. }
  246. return rect;
  247. }
  248. setPrevPaintRect(paintRect: BoundingRect) {
  249. if (paintRect) {
  250. this._prevPaintRect = this._prevPaintRect || new BoundingRect(0, 0, 0, 0);
  251. this._prevPaintRect.copy(paintRect);
  252. }
  253. else {
  254. this._prevPaintRect = null;
  255. }
  256. }
  257. getPrevPaintRect(): BoundingRect {
  258. return this._prevPaintRect;
  259. }
  260. /**
  261. * Alias for animate('style')
  262. * @param loop
  263. */
  264. animateStyle(loop: boolean) {
  265. return this.animate('style', loop);
  266. }
  267. // Override updateDuringAnimation
  268. updateDuringAnimation(targetKey: string) {
  269. if (targetKey === 'style') {
  270. this.dirtyStyle();
  271. }
  272. else {
  273. this.markRedraw();
  274. }
  275. }
  276. attrKV(key: DisplayableKey, value: DisplayablePropertyType) {
  277. if (key !== 'style') {
  278. super.attrKV(key as keyof DisplayableProps, value);
  279. }
  280. else {
  281. if (!this.style) {
  282. this.useStyle(value as Dictionary<any>);
  283. }
  284. else {
  285. this.setStyle(value as Dictionary<any>);
  286. }
  287. }
  288. }
  289. setStyle(obj: Props['style']): this
  290. setStyle<T extends keyof Props['style']>(obj: T, value: Props['style'][T]): this
  291. setStyle(keyOrObj: keyof Props['style'] | Props['style'], value?: unknown): this {
  292. if (typeof keyOrObj === 'string') {
  293. this.style[keyOrObj] = value;
  294. }
  295. else {
  296. extend(this.style, keyOrObj as Props['style']);
  297. }
  298. this.dirtyStyle();
  299. return this;
  300. }
  301. // getDefaultStyleValue<T extends keyof Props['style']>(key: T): Props['style'][T] {
  302. // // Default value is on the prototype.
  303. // return this.style.prototype[key];
  304. // }
  305. dirtyStyle(notRedraw?: boolean) {
  306. if (!notRedraw) {
  307. this.markRedraw();
  308. }
  309. this.__dirty |= STYLE_CHANGED_BIT;
  310. // Clear bounding rect.
  311. if (this._rect) {
  312. this._rect = null;
  313. }
  314. }
  315. dirty() {
  316. this.dirtyStyle();
  317. }
  318. /**
  319. * Is style changed. Used with dirtyStyle.
  320. */
  321. styleChanged() {
  322. return !!(this.__dirty & STYLE_CHANGED_BIT);
  323. }
  324. /**
  325. * Mark style updated. Only useful when style is used for caching. Like in the text.
  326. */
  327. styleUpdated() {
  328. this.__dirty &= ~STYLE_CHANGED_BIT;
  329. }
  330. /**
  331. * Create a style object with default values in it's prototype.
  332. */
  333. createStyle(obj?: Props['style']) {
  334. return createObject(DEFAULT_COMMON_STYLE, obj);
  335. }
  336. /**
  337. * Replace style property.
  338. * It will create a new style if given obj is not a valid style object.
  339. */
  340. // PENDING should not createStyle if it's an style object.
  341. useStyle(obj: Props['style']) {
  342. if (!obj[STYLE_MAGIC_KEY]) {
  343. obj = this.createStyle(obj);
  344. }
  345. if (this.__inHover) {
  346. this.__hoverStyle = obj; // Not affect exists style.
  347. }
  348. else {
  349. this.style = obj;
  350. }
  351. this.dirtyStyle();
  352. }
  353. /**
  354. * Determine if an object is a valid style object.
  355. * Which means it is created by `createStyle.`
  356. *
  357. * A valid style object will have all default values in it's prototype.
  358. * To avoid get null/undefined values.
  359. */
  360. isStyleObject(obj: Props['style']) {
  361. return obj[STYLE_MAGIC_KEY];
  362. }
  363. protected _innerSaveToNormal(toState: DisplayableState) {
  364. super._innerSaveToNormal(toState);
  365. const normalState = this._normalState;
  366. if (toState.style && !normalState.style) {
  367. // Clone style object.
  368. // TODO: Only save changed style.
  369. normalState.style = this._mergeStyle(this.createStyle(), this.style);
  370. }
  371. this._savePrimaryToNormal(toState, normalState, PRIMARY_STATES_KEYS);
  372. }
  373. protected _applyStateObj(
  374. stateName: string,
  375. state: DisplayableState,
  376. normalState: DisplayableState,
  377. keepCurrentStates: boolean,
  378. transition: boolean,
  379. animationCfg: ElementAnimateConfig
  380. ) {
  381. super._applyStateObj(stateName, state, normalState, keepCurrentStates, transition, animationCfg);
  382. const needsRestoreToNormal = !(state && keepCurrentStates);
  383. let targetStyle: Props['style'];
  384. if (state && state.style) {
  385. // Only animate changed properties.
  386. if (transition) {
  387. if (keepCurrentStates) {
  388. targetStyle = state.style;
  389. }
  390. else {
  391. targetStyle = this._mergeStyle(this.createStyle(), normalState.style);
  392. this._mergeStyle(targetStyle, state.style);
  393. }
  394. }
  395. else {
  396. targetStyle = this._mergeStyle(
  397. this.createStyle(),
  398. keepCurrentStates ? this.style : normalState.style
  399. );
  400. this._mergeStyle(targetStyle, state.style);
  401. }
  402. }
  403. else if (needsRestoreToNormal) {
  404. targetStyle = normalState.style;
  405. }
  406. if (targetStyle) {
  407. if (transition) {
  408. // Clone a new style. Not affect the original one.
  409. const sourceStyle = this.style;
  410. this.style = this.createStyle(needsRestoreToNormal ? {} : sourceStyle);
  411. // const sourceStyle = this.style = this.createStyle(this.style);
  412. if (needsRestoreToNormal) {
  413. const changedKeys = keys(sourceStyle);
  414. for (let i = 0; i < changedKeys.length; i++) {
  415. const key = changedKeys[i];
  416. if (key in targetStyle) { // Not use `key == null` because == null may means no stroke/fill.
  417. // Pick out from prototype. Or the property won't be animated.
  418. (targetStyle as any)[key] = targetStyle[key];
  419. // Omit the property has no default value.
  420. (this.style as any)[key] = sourceStyle[key];
  421. }
  422. }
  423. }
  424. // If states is switched twice in ONE FRAME, for example:
  425. // one property(for example shadowBlur) changed from default value to a specifed value,
  426. // then switched back in immediately. this.style may don't set this property yet when switching back.
  427. // It won't treat it as an changed property when switching back. And it won't be animated.
  428. // So here we make sure the properties will be animated from default value to a specifed value are set.
  429. const targetKeys = keys(targetStyle);
  430. for (let i = 0; i < targetKeys.length; i++) {
  431. const key = targetKeys[i];
  432. this.style[key] = this.style[key];
  433. }
  434. this._transitionState(stateName, {
  435. style: targetStyle
  436. } as Props, animationCfg, this.getAnimationStyleProps() as MapToType<Props, boolean>);
  437. }
  438. else {
  439. this.useStyle(targetStyle);
  440. }
  441. }
  442. // Don't change z, z2 for element moved into hover layer.
  443. // It's not necessary and will cause paint list order changed.
  444. const statesKeys = this.__inHover ? PRIMARY_STATES_KEYS_IN_HOVER_LAYER : PRIMARY_STATES_KEYS;
  445. for (let i = 0; i < statesKeys.length; i++) {
  446. let key = statesKeys[i];
  447. if (state && state[key] != null) {
  448. // Replace if it exist in target state
  449. (this as any)[key] = state[key];
  450. }
  451. else if (needsRestoreToNormal) {
  452. // Restore to normal state
  453. if (normalState[key] != null) {
  454. (this as any)[key] = normalState[key];
  455. }
  456. }
  457. }
  458. }
  459. protected _mergeStates(states: DisplayableState[]) {
  460. const mergedState = super._mergeStates(states) as DisplayableState;
  461. let mergedStyle: Props['style'];
  462. for (let i = 0; i < states.length; i++) {
  463. const state = states[i];
  464. if (state.style) {
  465. mergedStyle = mergedStyle || {};
  466. this._mergeStyle(mergedStyle, state.style);
  467. }
  468. }
  469. if (mergedStyle) {
  470. mergedState.style = mergedStyle;
  471. }
  472. return mergedState;
  473. }
  474. protected _mergeStyle(
  475. targetStyle: CommonStyleProps,
  476. sourceStyle: CommonStyleProps
  477. ) {
  478. extend(targetStyle, sourceStyle);
  479. return targetStyle;
  480. }
  481. getAnimationStyleProps() {
  482. return DEFAULT_COMMON_ANIMATION_PROPS;
  483. }
  484. /**
  485. * The string value of `textPosition` needs to be calculated to a real postion.
  486. * For example, `'inside'` is calculated to `[rect.width/2, rect.height/2]`
  487. * by default. See `contain/text.js#calculateTextPosition` for more details.
  488. * But some coutom shapes like "pin", "flag" have center that is not exactly
  489. * `[width/2, height/2]`. So we provide this hook to customize the calculation
  490. * for those shapes. It will be called if the `style.textPosition` is a string.
  491. * @param out Prepared out object. If not provided, this method should
  492. * be responsible for creating one.
  493. * @param style
  494. * @param rect {x, y, width, height}
  495. * @return out The same as the input out.
  496. * {
  497. * x: number. mandatory.
  498. * y: number. mandatory.
  499. * textAlign: string. optional. use style.textAlign by default.
  500. * textVerticalAlign: string. optional. use style.textVerticalAlign by default.
  501. * }
  502. */
  503. // calculateTextPosition: (out: CalculateTextPositionResult, style: Dictionary<any>, rect: RectLike) => CalculateTextPositionResult
  504. protected static initDefaultProps = (function () {
  505. const dispProto = Displayable.prototype;
  506. dispProto.type = 'displayable';
  507. dispProto.invisible = false;
  508. dispProto.z = 0;
  509. dispProto.z2 = 0;
  510. dispProto.zlevel = 0;
  511. dispProto.culling = false;
  512. dispProto.cursor = 'pointer';
  513. dispProto.rectHover = false;
  514. dispProto.incremental = false;
  515. dispProto._rect = null;
  516. dispProto.dirtyRectTolerance = 0;
  517. dispProto.__dirty = REDRAW_BIT | STYLE_CHANGED_BIT;
  518. })()
  519. }
  520. const tmpRect = new BoundingRect(0, 0, 0, 0);
  521. const viewRect = new BoundingRect(0, 0, 0, 0);
  522. function isDisplayableCulled(el: Displayable, width: number, height: number) {
  523. tmpRect.copy(el.getBoundingRect());
  524. if (el.transform) {
  525. tmpRect.applyTransform(el.transform);
  526. }
  527. viewRect.width = width;
  528. viewRect.height = height;
  529. return !tmpRect.intersect(viewRect);
  530. }
  531. export default Displayable;