Path.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  1. import Displayable, { DisplayableProps,
  2. CommonStyleProps,
  3. DEFAULT_COMMON_STYLE,
  4. DisplayableStatePropNames,
  5. DEFAULT_COMMON_ANIMATION_PROPS
  6. } from './Displayable';
  7. import Element, { ElementAnimateConfig } from '../Element';
  8. import PathProxy from '../core/PathProxy';
  9. import * as pathContain from '../contain/path';
  10. import { PatternObject } from './Pattern';
  11. import { Dictionary, PropType, MapToType } from '../core/types';
  12. import BoundingRect from '../core/BoundingRect';
  13. import { LinearGradientObject } from './LinearGradient';
  14. import { RadialGradientObject } from './RadialGradient';
  15. import { defaults, keys, extend, clone, isString, createObject } from '../core/util';
  16. import Animator from '../animation/Animator';
  17. import { lum } from '../tool/color';
  18. import { DARK_LABEL_COLOR, LIGHT_LABEL_COLOR, DARK_MODE_THRESHOLD, LIGHTER_LABEL_COLOR } from '../config';
  19. import { REDRAW_BIT, SHAPE_CHANGED_BIT, STYLE_CHANGED_BIT } from './constants';
  20. import { TRANSFORMABLE_PROPS } from '../core/Transformable';
  21. export interface PathStyleProps extends CommonStyleProps {
  22. fill?: string | PatternObject | LinearGradientObject | RadialGradientObject
  23. stroke?: string | PatternObject | LinearGradientObject | RadialGradientObject
  24. decal?: PatternObject
  25. /**
  26. * Still experimental, not works weel on arc with edge cases(large angle).
  27. */
  28. strokePercent?: number
  29. strokeNoScale?: boolean
  30. fillOpacity?: number
  31. strokeOpacity?: number
  32. /**
  33. * `true` is not supported.
  34. * `false`/`null`/`undefined` are the same.
  35. * `false` is used to remove lineDash in some
  36. * case that `null`/`undefined` can not be set.
  37. * (e.g., emphasis.lineStyle in echarts)
  38. */
  39. lineDash?: false | number[] | 'solid' | 'dashed' | 'dotted'
  40. lineDashOffset?: number
  41. lineWidth?: number
  42. lineCap?: CanvasLineCap
  43. lineJoin?: CanvasLineJoin
  44. miterLimit?: number
  45. /**
  46. * Paint order, if do stroke first. Similar to SVG paint-order
  47. * https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/paint-order
  48. */
  49. strokeFirst?: boolean
  50. }
  51. export const DEFAULT_PATH_STYLE: PathStyleProps = defaults({
  52. fill: '#000',
  53. stroke: null,
  54. strokePercent: 1,
  55. fillOpacity: 1,
  56. strokeOpacity: 1,
  57. lineDashOffset: 0,
  58. lineWidth: 1,
  59. lineCap: 'butt',
  60. miterLimit: 10,
  61. strokeNoScale: false,
  62. strokeFirst: false
  63. } as PathStyleProps, DEFAULT_COMMON_STYLE);
  64. export const DEFAULT_PATH_ANIMATION_PROPS: MapToType<PathProps, boolean> = {
  65. style: defaults<MapToType<PathStyleProps, boolean>, MapToType<PathStyleProps, boolean>>({
  66. fill: true,
  67. stroke: true,
  68. strokePercent: true,
  69. fillOpacity: true,
  70. strokeOpacity: true,
  71. lineDashOffset: true,
  72. lineWidth: true,
  73. miterLimit: true
  74. } as MapToType<PathStyleProps, boolean>, DEFAULT_COMMON_ANIMATION_PROPS.style)
  75. };
  76. export interface PathProps extends DisplayableProps {
  77. strokeContainThreshold?: number
  78. segmentIgnoreThreshold?: number
  79. subPixelOptimize?: boolean
  80. style?: PathStyleProps
  81. shape?: Dictionary<any>
  82. autoBatch?: boolean
  83. __value?: (string | number)[] | (string | number)
  84. buildPath?: (
  85. ctx: PathProxy | CanvasRenderingContext2D,
  86. shapeCfg: Dictionary<any>,
  87. inBatch?: boolean
  88. ) => void
  89. }
  90. type PathKey = keyof PathProps
  91. type PathPropertyType = PropType<PathProps, PathKey>
  92. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  93. interface Path<Props extends PathProps = PathProps> {
  94. animate(key?: '', loop?: boolean): Animator<this>
  95. animate(key: 'style', loop?: boolean): Animator<this['style']>
  96. animate(key: 'shape', loop?: boolean): Animator<this['shape']>
  97. getState(stateName: string): PathState
  98. ensureState(stateName: string): PathState
  99. states: Dictionary<PathState>
  100. stateProxy: (stateName: string) => PathState
  101. }
  102. export type PathStatePropNames = DisplayableStatePropNames | 'shape';
  103. export type PathState = Pick<PathProps, PathStatePropNames> & {
  104. hoverLayer?: boolean
  105. }
  106. const pathCopyParams = (TRANSFORMABLE_PROPS as readonly string[]).concat(['invisible',
  107. 'culling', 'z', 'z2', 'zlevel', 'parent'
  108. ]) as (keyof Path)[];
  109. class Path<Props extends PathProps = PathProps> extends Displayable<Props> {
  110. path: PathProxy
  111. strokeContainThreshold: number
  112. // This item default to be false. But in map series in echarts,
  113. // in order to improve performance, it should be set to true,
  114. // so the shorty segment won't draw.
  115. segmentIgnoreThreshold: number
  116. subPixelOptimize: boolean
  117. style: PathStyleProps
  118. /**
  119. * If element can be batched automatically
  120. */
  121. autoBatch: boolean
  122. private _rectStroke: BoundingRect
  123. protected _normalState: PathState
  124. protected _decalEl: Path
  125. // Must have an initial value on shape.
  126. // It will be assigned by default value.
  127. shape: Dictionary<any>
  128. constructor(opts?: Props) {
  129. super(opts);
  130. }
  131. update() {
  132. super.update();
  133. const style = this.style;
  134. if (style.decal) {
  135. const decalEl: Path = this._decalEl = this._decalEl || new Path();
  136. if (decalEl.buildPath === Path.prototype.buildPath) {
  137. decalEl.buildPath = ctx => {
  138. this.buildPath(ctx, this.shape);
  139. };
  140. }
  141. decalEl.silent = true;
  142. const decalElStyle = decalEl.style;
  143. for (let key in style) {
  144. if ((decalElStyle as any)[key] !== (style as any)[key]) {
  145. (decalElStyle as any)[key] = (style as any)[key];
  146. }
  147. }
  148. decalElStyle.fill = style.fill ? style.decal : null;
  149. decalElStyle.decal = null;
  150. decalElStyle.shadowColor = null;
  151. style.strokeFirst && (decalElStyle.stroke = null);
  152. for (let i = 0; i < pathCopyParams.length; ++i) {
  153. (decalEl as any)[pathCopyParams[i]] = this[pathCopyParams[i]];
  154. }
  155. decalEl.__dirty |= REDRAW_BIT;
  156. }
  157. else if (this._decalEl) {
  158. this._decalEl = null;
  159. }
  160. }
  161. getDecalElement() {
  162. return this._decalEl;
  163. }
  164. protected _init(props?: Props) {
  165. // Init default properties
  166. const keysArr = keys(props);
  167. this.shape = this.getDefaultShape();
  168. const defaultStyle = this.getDefaultStyle();
  169. if (defaultStyle) {
  170. this.useStyle(defaultStyle);
  171. }
  172. for (let i = 0; i < keysArr.length; i++) {
  173. const key = keysArr[i];
  174. const value = props[key];
  175. if (key === 'style') {
  176. if (!this.style) {
  177. // PENDING Reuse style object if possible?
  178. this.useStyle(value as Props['style']);
  179. }
  180. else {
  181. extend(this.style, value as Props['style']);
  182. }
  183. }
  184. else if (key === 'shape') {
  185. // this.shape = value;
  186. extend(this.shape, value as Props['shape']);
  187. }
  188. else {
  189. super.attrKV(key as any, value);
  190. }
  191. }
  192. // Create an empty one if no style object exists.
  193. if (!this.style) {
  194. this.useStyle({});
  195. }
  196. // const defaultShape = this.getDefaultShape();
  197. // if (!this.shape) {
  198. // this.shape = defaultShape;
  199. // }
  200. // else {
  201. // defaults(this.shape, defaultShape);
  202. // }
  203. }
  204. protected getDefaultStyle(): Props['style'] {
  205. return null;
  206. }
  207. // Needs to override
  208. protected getDefaultShape() {
  209. return {};
  210. }
  211. protected canBeInsideText() {
  212. return this.hasFill();
  213. }
  214. protected getInsideTextFill() {
  215. const pathFill = this.style.fill;
  216. if (pathFill !== 'none') {
  217. if (isString(pathFill)) {
  218. const fillLum = lum(pathFill, 0);
  219. // Determin text color based on the lum of path fill.
  220. // TODO use (1 - DARK_MODE_THRESHOLD)?
  221. if (fillLum > 0.5) { // TODO Consider background lum?
  222. return DARK_LABEL_COLOR;
  223. }
  224. else if (fillLum > 0.2) {
  225. return LIGHTER_LABEL_COLOR;
  226. }
  227. return LIGHT_LABEL_COLOR;
  228. }
  229. else if (pathFill) {
  230. return LIGHT_LABEL_COLOR;
  231. }
  232. }
  233. return DARK_LABEL_COLOR;
  234. }
  235. protected getInsideTextStroke(textFill?: string) {
  236. const pathFill = this.style.fill;
  237. // Not stroke on none fill object or gradient object
  238. if (isString(pathFill)) {
  239. const zr = this.__zr;
  240. const isDarkMode = !!(zr && zr.isDarkMode());
  241. const isDarkLabel = lum(textFill, 0) < DARK_MODE_THRESHOLD;
  242. // All dark or all light.
  243. if (isDarkMode === isDarkLabel) {
  244. return pathFill;
  245. }
  246. }
  247. }
  248. // When bundling path, some shape may decide if use moveTo to begin a new subpath or closePath
  249. // Like in circle
  250. buildPath(
  251. ctx: PathProxy | CanvasRenderingContext2D,
  252. shapeCfg: Dictionary<any>,
  253. inBatch?: boolean
  254. ) {}
  255. pathUpdated() {
  256. this.__dirty &= ~SHAPE_CHANGED_BIT;
  257. }
  258. getUpdatedPathProxy(inBatch?: boolean) {
  259. // Update path proxy data to latest.
  260. !this.path && this.createPathProxy();
  261. this.path.beginPath();
  262. this.buildPath(this.path, this.shape, inBatch);
  263. return this.path;
  264. }
  265. createPathProxy() {
  266. this.path = new PathProxy(false);
  267. }
  268. hasStroke() {
  269. const style = this.style;
  270. const stroke = style.stroke;
  271. return !(stroke == null || stroke === 'none' || !(style.lineWidth > 0));
  272. }
  273. hasFill() {
  274. const style = this.style;
  275. const fill = style.fill;
  276. return fill != null && fill !== 'none';
  277. }
  278. getBoundingRect(): BoundingRect {
  279. let rect = this._rect;
  280. const style = this.style;
  281. const needsUpdateRect = !rect;
  282. if (needsUpdateRect) {
  283. let firstInvoke = false;
  284. if (!this.path) {
  285. firstInvoke = true;
  286. // Create path on demand.
  287. this.createPathProxy();
  288. }
  289. let path = this.path;
  290. if (firstInvoke || (this.__dirty & SHAPE_CHANGED_BIT)) {
  291. path.beginPath();
  292. this.buildPath(path, this.shape, false);
  293. this.pathUpdated();
  294. }
  295. rect = path.getBoundingRect();
  296. }
  297. this._rect = rect;
  298. if (this.hasStroke() && this.path && this.path.len() > 0) {
  299. // Needs update rect with stroke lineWidth when
  300. // 1. Element changes scale or lineWidth
  301. // 2. Shape is changed
  302. const rectStroke = this._rectStroke || (this._rectStroke = rect.clone());
  303. if (this.__dirty || needsUpdateRect) {
  304. rectStroke.copy(rect);
  305. // PENDING, Min line width is needed when line is horizontal or vertical
  306. const lineScale = style.strokeNoScale ? this.getLineScale() : 1;
  307. // FIXME Must after updateTransform
  308. let w = style.lineWidth;
  309. // Only add extra hover lineWidth when there are no fill
  310. if (!this.hasFill()) {
  311. const strokeContainThreshold = this.strokeContainThreshold;
  312. w = Math.max(w, strokeContainThreshold == null ? 4 : strokeContainThreshold);
  313. }
  314. // Consider line width
  315. // Line scale can't be 0;
  316. if (lineScale > 1e-10) {
  317. rectStroke.width += w / lineScale;
  318. rectStroke.height += w / lineScale;
  319. rectStroke.x -= w / lineScale / 2;
  320. rectStroke.y -= w / lineScale / 2;
  321. }
  322. }
  323. // Return rect with stroke
  324. return rectStroke;
  325. }
  326. return rect;
  327. }
  328. contain(x: number, y: number): boolean {
  329. const localPos = this.transformCoordToLocal(x, y);
  330. const rect = this.getBoundingRect();
  331. const style = this.style;
  332. x = localPos[0];
  333. y = localPos[1];
  334. if (rect.contain(x, y)) {
  335. const pathProxy = this.path;
  336. if (this.hasStroke()) {
  337. let lineWidth = style.lineWidth;
  338. let lineScale = style.strokeNoScale ? this.getLineScale() : 1;
  339. // Line scale can't be 0;
  340. if (lineScale > 1e-10) {
  341. // Only add extra hover lineWidth when there are no fill
  342. if (!this.hasFill()) {
  343. lineWidth = Math.max(lineWidth, this.strokeContainThreshold);
  344. }
  345. if (pathContain.containStroke(
  346. pathProxy, lineWidth / lineScale, x, y
  347. )) {
  348. return true;
  349. }
  350. }
  351. }
  352. if (this.hasFill()) {
  353. return pathContain.contain(pathProxy, x, y);
  354. }
  355. }
  356. return false;
  357. }
  358. /**
  359. * Shape changed
  360. */
  361. dirtyShape() {
  362. this.__dirty |= SHAPE_CHANGED_BIT;
  363. if (this._rect) {
  364. this._rect = null;
  365. }
  366. if (this._decalEl) {
  367. this._decalEl.dirtyShape();
  368. }
  369. this.markRedraw();
  370. }
  371. dirty() {
  372. this.dirtyStyle();
  373. this.dirtyShape();
  374. }
  375. /**
  376. * Alias for animate('shape')
  377. * @param {boolean} loop
  378. */
  379. animateShape(loop: boolean) {
  380. return this.animate('shape', loop);
  381. }
  382. // Override updateDuringAnimation
  383. updateDuringAnimation(targetKey: string) {
  384. if (targetKey === 'style') {
  385. this.dirtyStyle();
  386. }
  387. else if (targetKey === 'shape') {
  388. this.dirtyShape();
  389. }
  390. else {
  391. this.markRedraw();
  392. }
  393. }
  394. // Overwrite attrKV
  395. attrKV(key: PathKey, value: PathPropertyType) {
  396. // FIXME
  397. if (key === 'shape') {
  398. this.setShape(value as Props['shape']);
  399. }
  400. else {
  401. super.attrKV(key as keyof DisplayableProps, value);
  402. }
  403. }
  404. setShape(obj: Props['shape']): this
  405. setShape<T extends keyof Props['shape']>(obj: T, value: Props['shape'][T]): this
  406. setShape(keyOrObj: keyof Props['shape'] | Props['shape'], value?: unknown): this {
  407. let shape = this.shape;
  408. if (!shape) {
  409. shape = this.shape = {};
  410. }
  411. // Path from string may not have shape
  412. if (typeof keyOrObj === 'string') {
  413. shape[keyOrObj] = value;
  414. }
  415. else {
  416. extend(shape, keyOrObj as Props['shape']);
  417. }
  418. this.dirtyShape();
  419. return this;
  420. }
  421. /**
  422. * If shape changed. used with dirtyShape
  423. */
  424. shapeChanged() {
  425. return !!(this.__dirty & SHAPE_CHANGED_BIT);
  426. }
  427. /**
  428. * Create a path style object with default values in it's prototype.
  429. * @override
  430. */
  431. createStyle(obj?: Props['style']) {
  432. return createObject(DEFAULT_PATH_STYLE, obj);
  433. }
  434. protected _innerSaveToNormal(toState: PathState) {
  435. super._innerSaveToNormal(toState);
  436. const normalState = this._normalState;
  437. // Clone a new one. DON'T share object reference between states and current using.
  438. // TODO: Clone array in shape?.
  439. // TODO: Only save changed shape.
  440. if (toState.shape && !normalState.shape) {
  441. normalState.shape = extend({}, this.shape);
  442. }
  443. }
  444. protected _applyStateObj(
  445. stateName: string,
  446. state: PathState,
  447. normalState: PathState,
  448. keepCurrentStates: boolean,
  449. transition: boolean,
  450. animationCfg: ElementAnimateConfig
  451. ) {
  452. super._applyStateObj(stateName, state, normalState, keepCurrentStates, transition, animationCfg);
  453. const needsRestoreToNormal = !(state && keepCurrentStates);
  454. let targetShape: Props['shape'];
  455. if (state && state.shape) {
  456. // Only animate changed properties.
  457. if (transition) {
  458. if (keepCurrentStates) {
  459. targetShape = state.shape;
  460. }
  461. else {
  462. // Inherits from normal state.
  463. targetShape = extend({}, normalState.shape);
  464. extend(targetShape, state.shape);
  465. }
  466. }
  467. else {
  468. // Because the shape will be replaced. So inherits from current shape.
  469. targetShape = extend({}, keepCurrentStates ? this.shape : normalState.shape);
  470. extend(targetShape, state.shape);
  471. }
  472. }
  473. else if (needsRestoreToNormal) {
  474. targetShape = normalState.shape;
  475. }
  476. if (targetShape) {
  477. if (transition) {
  478. // Clone a new shape.
  479. this.shape = extend({}, this.shape);
  480. // Only supports transition on primary props. Because shape is not deep cloned.
  481. const targetShapePrimaryProps: Props['shape'] = {};
  482. const shapeKeys = keys(targetShape);
  483. for (let i = 0; i < shapeKeys.length; i++) {
  484. const key = shapeKeys[i];
  485. if (typeof targetShape[key] === 'object') {
  486. (this.shape as Props['shape'])[key] = targetShape[key];
  487. }
  488. else {
  489. targetShapePrimaryProps[key] = targetShape[key];
  490. }
  491. }
  492. this._transitionState(stateName, {
  493. shape: targetShapePrimaryProps
  494. } as Props, animationCfg);
  495. }
  496. else {
  497. this.shape = targetShape;
  498. this.dirtyShape();
  499. }
  500. }
  501. }
  502. protected _mergeStates(states: PathState[]) {
  503. const mergedState = super._mergeStates(states) as PathState;
  504. let mergedShape: Props['shape'];
  505. for (let i = 0; i < states.length; i++) {
  506. const state = states[i];
  507. if (state.shape) {
  508. mergedShape = mergedShape || {};
  509. this._mergeStyle(mergedShape, state.shape);
  510. }
  511. }
  512. if (mergedShape) {
  513. mergedState.shape = mergedShape;
  514. }
  515. return mergedState;
  516. }
  517. getAnimationStyleProps() {
  518. return DEFAULT_PATH_ANIMATION_PROPS;
  519. }
  520. /**
  521. * If path shape is zero area
  522. */
  523. isZeroArea(): boolean {
  524. return false;
  525. }
  526. /**
  527. * 扩展一个 Path element, 比如星形,圆等。
  528. * Extend a path element
  529. * @DEPRECATED Use class extends
  530. * @param props
  531. * @param props.type Path type
  532. * @param props.init Initialize
  533. * @param props.buildPath Overwrite buildPath method
  534. * @param props.style Extended default style config
  535. * @param props.shape Extended default shape config
  536. */
  537. static extend<Shape extends Dictionary<any>>(defaultProps: {
  538. type?: string
  539. shape?: Shape
  540. style?: PathStyleProps
  541. beforeBrush?: Displayable['beforeBrush']
  542. afterBrush?: Displayable['afterBrush']
  543. getBoundingRect?: Displayable['getBoundingRect']
  544. calculateTextPosition?: Element['calculateTextPosition']
  545. buildPath(this: Path, ctx: CanvasRenderingContext2D | PathProxy, shape: Shape, inBatch?: boolean): void
  546. init?(this: Path, opts: PathProps): void // TODO Should be SubPathOption
  547. }): {
  548. new(opts?: PathProps & {shape: Shape}): Path
  549. } {
  550. interface SubPathOption extends PathProps {
  551. shape: Shape
  552. }
  553. class Sub extends Path {
  554. shape: Shape
  555. getDefaultStyle() {
  556. return clone(defaultProps.style);
  557. }
  558. getDefaultShape() {
  559. return clone(defaultProps.shape);
  560. }
  561. constructor(opts?: SubPathOption) {
  562. super(opts);
  563. defaultProps.init && defaultProps.init.call(this as any, opts);
  564. }
  565. }
  566. // TODO Legacy usage. Extend functions
  567. for (let key in defaultProps) {
  568. if (typeof (defaultProps as any)[key] === 'function') {
  569. (Sub.prototype as any)[key] = (defaultProps as any)[key];
  570. }
  571. }
  572. // Sub.prototype.buildPath = defaultProps.buildPath;
  573. // Sub.prototype.beforeBrush = defaultProps.beforeBrush;
  574. // Sub.prototype.afterBrush = defaultProps.afterBrush;
  575. return Sub as any;
  576. }
  577. protected static initDefaultProps = (function () {
  578. const pathProto = Path.prototype;
  579. pathProto.type = 'path';
  580. pathProto.strokeContainThreshold = 5;
  581. pathProto.segmentIgnoreThreshold = 0;
  582. pathProto.subPixelOptimize = false;
  583. pathProto.autoBatch = false;
  584. pathProto.__dirty = REDRAW_BIT | STYLE_CHANGED_BIT | SHAPE_CHANGED_BIT;
  585. })()
  586. }
  587. export default Path;