| import Transformable, {TRANSFORMABLE_PROPS, TransformProp} from './core/Transformable'; | 
| import { AnimationEasing } from './animation/easing'; | 
| import Animator, {cloneValue} from './animation/Animator'; | 
| import { ZRenderType } from './zrender'; | 
| import { | 
|     Dictionary, ElementEventName, ZRRawEvent, BuiltinTextPosition, AllPropTypes, | 
|     TextVerticalAlign, TextAlign, MapToType | 
| } from './core/types'; | 
| import Path from './graphic/Path'; | 
| import BoundingRect, { RectLike } from './core/BoundingRect'; | 
| import Eventful from './core/Eventful'; | 
| import ZRText, { DefaultTextStyle } from './graphic/Text'; | 
| import { calculateTextPosition, TextPositionCalculationResult, parsePercent } from './contain/text'; | 
| import { | 
|     guid, | 
|     isObject, | 
|     keys, | 
|     extend, | 
|     indexOf, | 
|     logError, | 
|     mixin, | 
|     isArrayLike, | 
|     isTypedArray, | 
|     isGradientObject, | 
|     filter, | 
|     reduce | 
| } from './core/util'; | 
| import Polyline from './graphic/shape/Polyline'; | 
| import Group from './graphic/Group'; | 
| import Point from './core/Point'; | 
| import { LIGHT_LABEL_COLOR, DARK_LABEL_COLOR } from './config'; | 
| import { parse, stringify } from './tool/color'; | 
| import { REDRAW_BIT } from './graphic/constants'; | 
|   | 
| export interface ElementAnimateConfig { | 
|     duration?: number | 
|     delay?: number | 
|     easing?: AnimationEasing | 
|     during?: (percent: number) => void | 
|   | 
|     // `done` will be called when all of the animations of the target props are | 
|     // "done" or "aborted", and at least one "done" happened. | 
|     // Common cases: animations declared, but some of them are aborted (e.g., by state change). | 
|     // The calling of `animationTo` done rather than aborted if at least one done happened. | 
|     done?: Function | 
|     // `aborted` will be called when all of the animations of the target props are "aborted". | 
|     aborted?: Function | 
|   | 
|     scope?: string | 
|     /** | 
|      * If force animate | 
|      * Prevent stop animation and callback | 
|      * immediently when target values are the same as current values. | 
|      */ | 
|     force?: boolean | 
|     /** | 
|      * If use additive animation. | 
|      */ | 
|     additive?: boolean | 
|     /** | 
|      * If set to final state before animation started. | 
|      * It can be useful if something you want to calcuate depends on the final state of element. | 
|      * Like bounding rect for text layouting. | 
|      * | 
|      * Only available in animateTo | 
|      */ | 
|     setToFinal?: boolean | 
| } | 
|   | 
| export interface ElementTextConfig { | 
|     /** | 
|      * Position relative to the element bounding rect | 
|      * @default 'inside' | 
|      */ | 
|     position?: BuiltinTextPosition | (number | string)[] | 
|   | 
|     /** | 
|      * Rotation of the label. | 
|      */ | 
|     rotation?: number | 
|   | 
|     /** | 
|      * Rect that text will be positioned. | 
|      * Default to be the rect of element. | 
|      */ | 
|     layoutRect?: RectLike | 
|   | 
|     /** | 
|      * Offset of the label. | 
|      * The difference of offset and position is that it will be applied | 
|      * in the rotation | 
|      */ | 
|     offset?: number[] | 
|   | 
|     /** | 
|      * Origin or rotation. Which is relative to the bounding box of the attached element. | 
|      * Can be percent value. Relative to the bounding box. | 
|      * If specified center. It will be center of the bounding box. | 
|      * | 
|      * Only available when position and rotation are both set. | 
|      */ | 
|     origin?: (number | string)[] | 'center' | 
|   | 
|     /** | 
|      * Distance to the rect | 
|      * @default 5 | 
|      */ | 
|     distance?: number | 
|   | 
|     /** | 
|      * If use local user space. Which will apply host's transform | 
|      * @default false | 
|      */ | 
|     local?: boolean | 
|   | 
|     /** | 
|      * `insideFill` is a color string or left empty. | 
|      * If a `textContent` is "inside", its final `fill` will be picked by this priority: | 
|      * `textContent.style.fill` > `textConfig.insideFill` > "auto-calculated-fill" | 
|      * In most cases, "auto-calculated-fill" is white. | 
|      */ | 
|     insideFill?: string | 
|   | 
|     /** | 
|      * `insideStroke` is a color string or left empty. | 
|      * If a `textContent` is "inside", its final `stroke` will be picked by this priority: | 
|      * `textContent.style.stroke` > `textConfig.insideStroke` > "auto-calculated-stroke" | 
|      * | 
|      * The rule of getting "auto-calculated-stroke": | 
|      * If (A) the `fill` is specified in style (either in `textContent.style` or `textContent.style.rich`) | 
|      * or (B) needed to draw text background (either defined in `textContent.style` or `textContent.style.rich`) | 
|      * "auto-calculated-stroke" will be null. | 
|      * Otherwise, "auto-calculated-stroke" will be the same as `fill` of this element if possible, or null. | 
|      * | 
|      * The reason of (A) is not decisive: | 
|      * 1. If users specify `fill` in style and still use "auto-calculated-stroke", the effect | 
|      * is not good and unexpected in some cases. It not easy and seams uncessary to auto calculate | 
|      * a proper `stroke` for the given `fill`, since they can specify `stroke` themselve. | 
|      * 2. Backward compat. | 
|      */ | 
|     insideStroke?: string | 
|   | 
|     /** | 
|      * `outsideFill` is a color string or left empty. | 
|      * If a `textContent` is "inside", its final `fill` will be picked by this priority: | 
|      * `textContent.style.fill` > `textConfig.outsideFill` > #000 | 
|      */ | 
|     outsideFill?: string | 
|   | 
|     /** | 
|      * `outsideStroke` is a color string or left empth. | 
|      * If a `textContent` is not "inside", its final `stroke` will be picked by this priority: | 
|      * `textContent.style.stroke` > `textConfig.outsideStroke` > "auto-calculated-stroke" | 
|      * | 
|      * The rule of getting "auto-calculated-stroke": | 
|      * If (A) the `fill` is specified in style (either in `textContent.style` or `textContent.style.rich`) | 
|      * or (B) needed to draw text background (either defined in `textContent.style` or `textContent.style.rich`) | 
|      * "auto-calculated-stroke" will be null. | 
|      * Otherwise, "auto-calculated-stroke" will be a neer white color to distinguish "front end" | 
|      * label with messy background (like other text label, line or other graphic). | 
|      */ | 
|     outsideStroke?: string | 
|   | 
|     /** | 
|      * Tell zrender I can sure this text is inside or not. | 
|      * In case position is not using builtin `inside` hints. | 
|      */ | 
|     inside?: boolean | 
| } | 
| export interface ElementTextGuideLineConfig { | 
|     /** | 
|      * Anchor for text guide line. | 
|      * Notice: Won't work | 
|      */ | 
|     anchor?: Point | 
|   | 
|     /** | 
|      * If above the target element. | 
|      */ | 
|     showAbove?: boolean | 
|   | 
|     /** | 
|      * Candidates of connectors. Used when autoCalculate is true and anchor is not specified. | 
|      */ | 
|     candidates?: ('left' | 'top' | 'right' | 'bottom')[] | 
| } | 
|   | 
| export interface ElementEvent { | 
|     type: ElementEventName, | 
|     event: ZRRawEvent, | 
|     // target can only be an element that is not silent. | 
|     target: Element, | 
|     // topTarget can be a silent element. | 
|     topTarget: Element, | 
|     cancelBubble: boolean, | 
|     offsetX: number, | 
|     offsetY: number, | 
|     gestureEvent: string, | 
|     pinchX: number, | 
|     pinchY: number, | 
|     pinchScale: number, | 
|     wheelDelta: number, | 
|     zrByTouch: boolean, | 
|     which: number, | 
|     stop: (this: ElementEvent) => void | 
| } | 
|   | 
| export type ElementEventCallback<Ctx, Impl> = ( | 
|     this: CbThis<Ctx, Impl>, e: ElementEvent | 
| ) => boolean | void | 
| type CbThis<Ctx, Impl> = unknown extends Ctx ? Impl : Ctx; | 
|   | 
| interface ElementEventHandlerProps { | 
|     // Events | 
|     onclick: ElementEventCallback<unknown, unknown> | 
|     ondblclick: ElementEventCallback<unknown, unknown> | 
|     onmouseover: ElementEventCallback<unknown, unknown> | 
|     onmouseout: ElementEventCallback<unknown, unknown> | 
|     onmousemove: ElementEventCallback<unknown, unknown> | 
|     onmousewheel: ElementEventCallback<unknown, unknown> | 
|     onmousedown: ElementEventCallback<unknown, unknown> | 
|     onmouseup: ElementEventCallback<unknown, unknown> | 
|     oncontextmenu: ElementEventCallback<unknown, unknown> | 
|   | 
|     ondrag: ElementEventCallback<unknown, unknown> | 
|     ondragstart: ElementEventCallback<unknown, unknown> | 
|     ondragend: ElementEventCallback<unknown, unknown> | 
|     ondragenter: ElementEventCallback<unknown, unknown> | 
|     ondragleave: ElementEventCallback<unknown, unknown> | 
|     ondragover: ElementEventCallback<unknown, unknown> | 
|     ondrop: ElementEventCallback<unknown, unknown> | 
| } | 
|   | 
| export interface ElementProps extends Partial<ElementEventHandlerProps>, Partial<Pick<Transformable, TransformProp>> { | 
|     name?: string | 
|     ignore?: boolean | 
|     isGroup?: boolean | 
|     draggable?: boolean | 'horizontal' | 'vertical' | 
|   | 
|     silent?: boolean | 
|   | 
|     ignoreClip?: boolean | 
|     globalScaleRatio?: number | 
|   | 
|     textConfig?: ElementTextConfig | 
|     textContent?: ZRText | 
|   | 
|     clipPath?: Path | 
|     drift?: Element['drift'] | 
|   | 
|     extra?: Dictionary<unknown> | 
|   | 
|     // For echarts animation. | 
|     anid?: string | 
| } | 
|   | 
| // Properties can be used in state. | 
| export const PRESERVED_NORMAL_STATE = '__zr_normal__'; | 
| // export const PRESERVED_MERGED_STATE = '__zr_merged__'; | 
|   | 
| const PRIMARY_STATES_KEYS = (TRANSFORMABLE_PROPS as any).concat(['ignore']) as [TransformProp, 'ignore']; | 
| const DEFAULT_ANIMATABLE_MAP = reduce(TRANSFORMABLE_PROPS, (obj, key) => { | 
|     obj[key] = true; | 
|     return obj; | 
| }, {ignore: false} as Partial<Record<ElementStatePropNames, boolean>>); | 
|   | 
| export type ElementStatePropNames = (typeof PRIMARY_STATES_KEYS)[number] | 'textConfig'; | 
| export type ElementState = Pick<ElementProps, ElementStatePropNames> & ElementCommonState | 
|   | 
| export type ElementCommonState = { | 
|     hoverLayer?: boolean | 
| } | 
|   | 
| export type ElementCalculateTextPosition = ( | 
|     out: TextPositionCalculationResult, | 
|     style: ElementTextConfig, | 
|     rect: RectLike | 
| ) => TextPositionCalculationResult; | 
|   | 
| let tmpTextPosCalcRes = {} as TextPositionCalculationResult; | 
| let tmpBoundingRect = new BoundingRect(0, 0, 0, 0); | 
|   | 
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | 
| interface Element<Props extends ElementProps = ElementProps> extends Transformable, | 
|     Eventful<{ | 
|         [key in ElementEventName]: (e: ElementEvent) => void | boolean | 
|     } & { | 
|         [key in string]: (...args: any) => void | boolean | 
|     }>, | 
|     ElementEventHandlerProps { | 
| } | 
|   | 
| class Element<Props extends ElementProps = ElementProps> { | 
|   | 
|     id: number = guid() | 
|     /** | 
|      * Element type | 
|      */ | 
|     type: string | 
|   | 
|     /** | 
|      * Element name | 
|      */ | 
|     name: string | 
|   | 
|     /** | 
|      * If ignore drawing and events of the element object | 
|      */ | 
|     ignore: boolean | 
|   | 
|     /** | 
|      * Whether to respond to mouse events. | 
|      */ | 
|     silent: boolean | 
|   | 
|     /** | 
|      * 是否是 Group | 
|      */ | 
|     isGroup: boolean | 
|   | 
|     /** | 
|      * Whether it can be dragged. | 
|      */ | 
|     draggable: boolean | 'horizontal' | 'vertical' | 
|   | 
|     /** | 
|      * Whether is it dragging. | 
|      */ | 
|     dragging: boolean | 
|   | 
|     parent: Group | 
|   | 
|     animators: Animator<any>[] = [] | 
|   | 
|     /** | 
|      * If ignore clip from it's parent or hosts. | 
|      * Applied on itself and all it's children. | 
|      * | 
|      * NOTE: It won't affect the clipPath set on the children. | 
|      */ | 
|     ignoreClip: boolean | 
|   | 
|     /** | 
|      * If element is used as a component of other element. | 
|      */ | 
|     __hostTarget: Element | 
|   | 
|     /** | 
|      * ZRender instance will be assigned when element is associated with zrender | 
|      */ | 
|     __zr: ZRenderType | 
|   | 
|     /** | 
|      * Dirty bits. | 
|      * From which painter will determine if this displayable object needs brush. | 
|      */ | 
|     __dirty: number | 
|   | 
|     /** | 
|      * If element was painted on the screen | 
|      */ | 
|     __isRendered: boolean; | 
|   | 
|     /** | 
|      * If element has been moved to the hover layer. | 
|      * | 
|      * If so, dirty will only trigger the zrender refresh hover layer | 
|      */ | 
|     __inHover: boolean | 
|   | 
|     /** | 
|      * path to clip the elements and its children, if it is a group. | 
|      * @see http://www.w3.org/TR/2dcontext/#clipping-region | 
|      */ | 
|     private _clipPath?: Path | 
|   | 
|     /** | 
|      * Attached text element. | 
|      * `position`, `style.textAlign`, `style.textVerticalAlign` | 
|      * of element will be ignored if textContent.position is set | 
|      */ | 
|     private _textContent?: ZRText | 
|   | 
|     /** | 
|      * Text guide line. | 
|      */ | 
|     private _textGuide?: Polyline | 
|   | 
|     /** | 
|      * Config of textContent. Inlcuding layout, color, ...etc. | 
|      */ | 
|     textConfig?: ElementTextConfig | 
|   | 
|     /** | 
|      * Config for guide line calculating. | 
|      * | 
|      * NOTE: This is just a property signature. READ and WRITE are all done in echarts. | 
|      */ | 
|     textGuideLineConfig?: ElementTextGuideLineConfig | 
|   | 
|     // FOR ECHARTS | 
|     /** | 
|      * Id for mapping animation | 
|      */ | 
|     anid: string | 
|   | 
|     extra: Dictionary<unknown> | 
|   | 
|     currentStates?: string[] = [] | 
|     // prevStates is for storager in echarts. | 
|     prevStates?: string[] | 
|     /** | 
|      * Store of element state. | 
|      * '__normal__' key is preserved for default properties. | 
|      */ | 
|     states: Dictionary<ElementState> = {} | 
|   | 
|     /** | 
|      * Animation config applied on state switching. | 
|      */ | 
|     stateTransition: ElementAnimateConfig | 
|   | 
|     /** | 
|      * Proxy function for getting state with given stateName. | 
|      * ZRender will first try to get with stateProxy. Then find from states if stateProxy returns nothing | 
|      * | 
|      * targetStates will be given in useStates | 
|      */ | 
|     stateProxy?: (stateName: string, targetStates?: string[]) => ElementState | 
|   | 
|     protected _normalState: ElementState | 
|   | 
|     // Temporary storage for inside text color configuration. | 
|     private _innerTextDefaultStyle: DefaultTextStyle | 
|   | 
|     constructor(props?: Props) { | 
|         this._init(props); | 
|     } | 
|   | 
|     protected _init(props?: Props) { | 
|         // Init default properties | 
|         this.attr(props); | 
|     } | 
|   | 
|     /** | 
|      * Drift element | 
|      * @param {number} dx dx on the global space | 
|      * @param {number} dy dy on the global space | 
|      */ | 
|     drift(dx: number, dy: number, e?: ElementEvent) { | 
|         switch (this.draggable) { | 
|             case 'horizontal': | 
|                 dy = 0; | 
|                 break; | 
|             case 'vertical': | 
|                 dx = 0; | 
|                 break; | 
|         } | 
|   | 
|         let m = this.transform; | 
|         if (!m) { | 
|             m = this.transform = [1, 0, 0, 1, 0, 0]; | 
|         } | 
|         m[4] += dx; | 
|         m[5] += dy; | 
|   | 
|         this.decomposeTransform(); | 
|         this.markRedraw(); | 
|     } | 
|   | 
|     /** | 
|      * Hook before update | 
|      */ | 
|     beforeUpdate() {} | 
|     /** | 
|      * Hook after update | 
|      */ | 
|     afterUpdate() {} | 
|     /** | 
|      * Update each frame | 
|      */ | 
|     update() { | 
|         this.updateTransform(); | 
|   | 
|         if (this.__dirty) { | 
|             this.updateInnerText(); | 
|         } | 
|     } | 
|   | 
|     updateInnerText(forceUpdate?: boolean) { | 
|         // Update textContent | 
|         const textEl = this._textContent; | 
|         if (textEl && (!textEl.ignore || forceUpdate)) { | 
|             if (!this.textConfig) { | 
|                 this.textConfig = {}; | 
|             } | 
|             const textConfig = this.textConfig; | 
|             const isLocal = textConfig.local; | 
|             const innerTransformable = textEl.innerTransformable; | 
|   | 
|             let textAlign: TextAlign; | 
|             let textVerticalAlign: TextVerticalAlign; | 
|   | 
|             let textStyleChanged = false; | 
|   | 
|             // Apply host's transform. | 
|             innerTransformable.parent = isLocal ? this as unknown as Group : null; | 
|   | 
|             let innerOrigin = false; | 
|   | 
|             // Reset x/y/rotation | 
|             innerTransformable.copyTransform(textEl); | 
|   | 
|             // Force set attached text's position if `position` is in config. | 
|             if (textConfig.position != null) { | 
|                 let layoutRect = tmpBoundingRect; | 
|                 if (textConfig.layoutRect) { | 
|                     layoutRect.copy(textConfig.layoutRect); | 
|                 } | 
|                 else { | 
|                     layoutRect.copy(this.getBoundingRect()); | 
|                 } | 
|                 if (!isLocal) { | 
|                     layoutRect.applyTransform(this.transform); | 
|                 } | 
|   | 
|                 if (this.calculateTextPosition) { | 
|                     this.calculateTextPosition(tmpTextPosCalcRes, textConfig, layoutRect); | 
|                 } | 
|                 else { | 
|                     calculateTextPosition(tmpTextPosCalcRes, textConfig, layoutRect); | 
|                 } | 
|   | 
|                 // TODO Should modify back if textConfig.position is set to null again. | 
|                 // Or textContent is detached. | 
|                 innerTransformable.x = tmpTextPosCalcRes.x; | 
|                 innerTransformable.y = tmpTextPosCalcRes.y; | 
|   | 
|                 // User specified align/verticalAlign has higher priority, which is | 
|                 // useful in the case that attached text is rotated 90 degree. | 
|                 textAlign = tmpTextPosCalcRes.align; | 
|                 textVerticalAlign = tmpTextPosCalcRes.verticalAlign; | 
|   | 
|                 const textOrigin = textConfig.origin; | 
|                 if (textOrigin && textConfig.rotation != null) { | 
|                     let relOriginX; | 
|                     let relOriginY; | 
|                     if (textOrigin === 'center') { | 
|                         relOriginX = layoutRect.width * 0.5; | 
|                         relOriginY = layoutRect.height * 0.5; | 
|                     } | 
|                     else { | 
|                         relOriginX = parsePercent(textOrigin[0], layoutRect.width); | 
|                         relOriginY = parsePercent(textOrigin[1], layoutRect.height); | 
|                     } | 
|   | 
|                     innerOrigin = true; | 
|                     innerTransformable.originX = -innerTransformable.x + relOriginX + (isLocal ? 0 : layoutRect.x); | 
|                     innerTransformable.originY = -innerTransformable.y + relOriginY + (isLocal ? 0 : layoutRect.y); | 
|                 } | 
|             } | 
|   | 
|   | 
|             if (textConfig.rotation != null) { | 
|                 innerTransformable.rotation = textConfig.rotation; | 
|             } | 
|   | 
|             // TODO | 
|             const textOffset = textConfig.offset; | 
|             if (textOffset) { | 
|                 innerTransformable.x += textOffset[0]; | 
|                 innerTransformable.y += textOffset[1]; | 
|   | 
|                 // Not change the user set origin. | 
|                 if (!innerOrigin) { | 
|                     innerTransformable.originX = -textOffset[0]; | 
|                     innerTransformable.originY = -textOffset[1]; | 
|                 } | 
|             } | 
|   | 
|             // Calculate text color | 
|             const isInside = textConfig.inside == null  // Force to be inside or not. | 
|                 ? (typeof textConfig.position === 'string' && textConfig.position.indexOf('inside') >= 0) | 
|                 : textConfig.inside; | 
|             const innerTextDefaultStyle = this._innerTextDefaultStyle || (this._innerTextDefaultStyle = {}); | 
|   | 
|             let textFill; | 
|             let textStroke; | 
|             let autoStroke; | 
|             if (isInside && this.canBeInsideText()) { | 
|                 // In most cases `textContent` need this "auto" strategy. | 
|                 // So by default be 'auto'. Otherwise users need to literally | 
|                 // set `insideFill: 'auto', insideStroke: 'auto'` each time. | 
|                 textFill = textConfig.insideFill; | 
|                 textStroke = textConfig.insideStroke; | 
|   | 
|                 if (textFill == null || textFill === 'auto') { | 
|                     textFill = this.getInsideTextFill(); | 
|                 } | 
|                 if (textStroke == null || textStroke === 'auto') { | 
|                     textStroke = this.getInsideTextStroke(textFill); | 
|                     autoStroke = true; | 
|                 } | 
|             } | 
|             else { | 
|                 textFill = textConfig.outsideFill; | 
|                 textStroke = textConfig.outsideStroke; | 
|   | 
|                 if (textFill == null || textFill === 'auto') { | 
|                     textFill = this.getOutsideFill(); | 
|                 } | 
|                 // By default give a stroke to distinguish "front end" label with | 
|                 // messy background (like other text label, line or other graphic). | 
|                 // If textContent.style.fill specified, this auto stroke will not be used. | 
|                 if (textStroke == null || textStroke === 'auto') { | 
|                     // If some time need to customize the default stroke getter, | 
|                     // add some kind of override method. | 
|                     textStroke = this.getOutsideStroke(textFill); | 
|                     autoStroke = true; | 
|                 } | 
|             } | 
|             // Default `textFill` should must have a value to ensure text can be displayed. | 
|             textFill = textFill || '#000'; | 
|   | 
|             if (textFill !== innerTextDefaultStyle.fill | 
|                 || textStroke !== innerTextDefaultStyle.stroke | 
|                 || autoStroke !== innerTextDefaultStyle.autoStroke | 
|                 || textAlign !== innerTextDefaultStyle.align | 
|                 || textVerticalAlign !== innerTextDefaultStyle.verticalAlign | 
|             ) { | 
|   | 
|                 textStyleChanged = true; | 
|   | 
|                 innerTextDefaultStyle.fill = textFill; | 
|                 innerTextDefaultStyle.stroke = textStroke; | 
|                 innerTextDefaultStyle.autoStroke = autoStroke; | 
|                 innerTextDefaultStyle.align = textAlign; | 
|                 innerTextDefaultStyle.verticalAlign = textVerticalAlign; | 
|   | 
|                 textEl.setDefaultTextStyle(innerTextDefaultStyle); | 
|             } | 
|   | 
|             // Mark textEl to update transform. | 
|             // DON'T use markRedraw. It will cause Element itself to dirty again. | 
|             textEl.__dirty |= REDRAW_BIT; | 
|   | 
|             if (textStyleChanged) { | 
|                 // Only mark style dirty if necessary. Update ZRText is costly. | 
|                 textEl.dirtyStyle(true); | 
|             } | 
|         } | 
|     } | 
|   | 
|     protected canBeInsideText() { | 
|         return true; | 
|     } | 
|   | 
|     protected getInsideTextFill(): string | undefined { | 
|         return '#fff'; | 
|     } | 
|   | 
|     protected getInsideTextStroke(textFill: string): string | undefined { | 
|         return '#000'; | 
|     } | 
|   | 
|     protected getOutsideFill(): string | undefined { | 
|         return this.__zr && this.__zr.isDarkMode() ? LIGHT_LABEL_COLOR : DARK_LABEL_COLOR; | 
|     } | 
|   | 
|     protected getOutsideStroke(textFill: string): string { | 
|         const backgroundColor = this.__zr && this.__zr.getBackgroundColor(); | 
|         let colorArr = typeof backgroundColor === 'string' && parse(backgroundColor as string); | 
|         if (!colorArr) { | 
|             colorArr = [255, 255, 255, 1]; | 
|         } | 
|         // Assume blending on a white / black(dark) background. | 
|         const alpha = colorArr[3]; | 
|         const isDark = this.__zr.isDarkMode(); | 
|         for (let i = 0; i < 3; i++) { | 
|             colorArr[i] = colorArr[i] * alpha + (isDark ? 0 : 255) * (1 - alpha); | 
|         } | 
|         colorArr[3] = 1; | 
|         return stringify(colorArr, 'rgba'); | 
|     } | 
|   | 
|     traverse<Context>( | 
|         cb: (this: Context, el: Element<Props>) => void, | 
|         context?: Context | 
|     ) {} | 
|   | 
|     protected attrKV(key: string, value: unknown) { | 
|         if (key === 'textConfig') { | 
|             this.setTextConfig(value as ElementTextConfig); | 
|         } | 
|         else if (key === 'textContent') { | 
|             this.setTextContent(value as ZRText); | 
|         } | 
|         else if (key === 'clipPath') { | 
|             this.setClipPath(value as Path); | 
|         } | 
|         else if (key === 'extra') { | 
|             this.extra = this.extra || {}; | 
|             extend(this.extra, value); | 
|         } | 
|         else { | 
|             (this as any)[key] = value; | 
|         } | 
|     } | 
|   | 
|     /** | 
|      * Hide the element | 
|      */ | 
|     hide() { | 
|         this.ignore = true; | 
|         this.markRedraw(); | 
|     } | 
|   | 
|     /** | 
|      * Show the element | 
|      */ | 
|     show() { | 
|         this.ignore = false; | 
|         this.markRedraw(); | 
|     } | 
|   | 
|     attr(keyOrObj: Props): this | 
|     attr<T extends keyof Props>(keyOrObj: T, value: Props[T]): this | 
|     attr(keyOrObj: keyof Props | Props, value?: unknown): this { | 
|         if (typeof keyOrObj === 'string') { | 
|             this.attrKV(keyOrObj as keyof ElementProps, value as AllPropTypes<ElementProps>); | 
|         } | 
|         else if (isObject(keyOrObj)) { | 
|             let obj = keyOrObj as object; | 
|             let keysArr = keys(obj); | 
|             for (let i = 0; i < keysArr.length; i++) { | 
|                 let key = keysArr[i]; | 
|                 this.attrKV(key as keyof ElementProps, keyOrObj[key]); | 
|             } | 
|         } | 
|         this.markRedraw(); | 
|         return this; | 
|     } | 
|   | 
|     // Save current state to normal | 
|     saveCurrentToNormalState(toState: ElementState) { | 
|         this._innerSaveToNormal(toState); | 
|   | 
|         // If we are switching from normal to other state during animation. | 
|         // We need to save final value of animation to the normal state. Not interpolated value. | 
|         const normalState = this._normalState; | 
|         for (let i = 0; i < this.animators.length; i++) { | 
|             const animator = this.animators[i]; | 
|             const fromStateTransition = animator.__fromStateTransition; | 
|             // Ignore animation from state transition(except normal). | 
|             // Ignore loop animation. | 
|             if (animator.getLoop() || fromStateTransition && fromStateTransition !== PRESERVED_NORMAL_STATE) { | 
|                 continue; | 
|             } | 
|   | 
|             const targetName = animator.targetName; | 
|             // Respecting the order of animation if multiple animator is | 
|             // animating on the same property(If additive animation is used) | 
|             const target = targetName | 
|                 ? (normalState as any)[targetName] : normalState; | 
|             // Only save keys that are changed by the states. | 
|             animator.saveTo(target); | 
|         } | 
|     } | 
|   | 
|     protected _innerSaveToNormal(toState: ElementState) { | 
|         let normalState = this._normalState; | 
|         if (!normalState) { | 
|             // Clear previous stored normal states when switching from normalState to otherState. | 
|             normalState = this._normalState = {}; | 
|         } | 
|         if (toState.textConfig && !normalState.textConfig) { | 
|             normalState.textConfig = this.textConfig; | 
|         } | 
|   | 
|         this._savePrimaryToNormal(toState, normalState, PRIMARY_STATES_KEYS); | 
|     } | 
|   | 
|     protected _savePrimaryToNormal( | 
|         toState: Dictionary<any>, normalState: Dictionary<any>, primaryKeys: readonly string[] | 
|     ) { | 
|         for (let i = 0; i < primaryKeys.length; i++) { | 
|             let key = primaryKeys[i]; | 
|             // Only save property that will be changed by toState | 
|             // and has not been saved to normalState yet. | 
|             if (toState[key] != null && !(key in normalState)) { | 
|                 (normalState as any)[key] = (this as any)[key]; | 
|             } | 
|         } | 
|     } | 
|   | 
|     /** | 
|      * If has any state. | 
|      */ | 
|     hasState() { | 
|         return this.currentStates.length > 0; | 
|     } | 
|   | 
|     /** | 
|      * Get state object | 
|      */ | 
|     getState(name: string) { | 
|         return this.states[name]; | 
|     } | 
|   | 
|   | 
|     /** | 
|      * Ensure state exists. If not, will create one and return. | 
|      */ | 
|     ensureState(name: string) { | 
|         const states = this.states; | 
|         if (!states[name]) { | 
|             states[name] = {}; | 
|         } | 
|         return states[name]; | 
|     } | 
|   | 
|     /** | 
|      * Clear all states. | 
|      */ | 
|     clearStates(noAnimation?: boolean) { | 
|         this.useState(PRESERVED_NORMAL_STATE, false, noAnimation); | 
|         // TODO set _normalState to null? | 
|     } | 
|     /** | 
|      * Use state. State is a collection of properties. | 
|      * Will return current state object if state exists and stateName has been changed. | 
|      * | 
|      * @param stateName State name to be switched to | 
|      * @param keepCurrentState If keep current states. | 
|      *      If not, it will inherit from the normal state. | 
|      */ | 
|     useState(stateName: string, keepCurrentStates?: boolean, noAnimation?: boolean, forceUseHoverLayer?: boolean) { | 
|         // Use preserved word __normal__ | 
|         // TODO: Only restore changed properties when restore to normal??? | 
|         const toNormalState = stateName === PRESERVED_NORMAL_STATE; | 
|         const hasStates = this.hasState(); | 
|   | 
|         if (!hasStates && toNormalState) { | 
|             // If switched from normal to normal. | 
|             return; | 
|         } | 
|   | 
|         const currentStates = this.currentStates; | 
|         const animationCfg = this.stateTransition; | 
|   | 
|         // No need to change in following cases: | 
|         // 1. Keep current states. and already being applied before. | 
|         // 2. Don't keep current states. And new state is same with the only one exists state. | 
|         if (indexOf(currentStates, stateName) >= 0 && (keepCurrentStates || currentStates.length === 1)) { | 
|             return; | 
|         } | 
|   | 
|         let state; | 
|         if (this.stateProxy && !toNormalState) { | 
|             state = this.stateProxy(stateName); | 
|         } | 
|   | 
|         if (!state) { | 
|             state = (this.states && this.states[stateName]); | 
|         } | 
|   | 
|         if (!state && !toNormalState) { | 
|             logError(`State ${stateName} not exists.`); | 
|             return; | 
|         } | 
|   | 
|         if (!toNormalState) { | 
|             this.saveCurrentToNormalState(state); | 
|         } | 
|   | 
|         const useHoverLayer = !!((state && state.hoverLayer) || forceUseHoverLayer); | 
|   | 
|         if (useHoverLayer) { | 
|             // Enter hover layer before states update. | 
|             this._toggleHoverLayerFlag(true); | 
|         } | 
|   | 
|         this._applyStateObj( | 
|             stateName, | 
|             state, | 
|             this._normalState, | 
|             keepCurrentStates, | 
|             !noAnimation && !this.__inHover && animationCfg && animationCfg.duration > 0, | 
|             animationCfg | 
|         ); | 
|   | 
|         // Also set text content. | 
|         const textContent = this._textContent; | 
|         const textGuide = this._textGuide; | 
|         if (textContent) { | 
|             // Force textContent use hover layer if self is using it. | 
|             textContent.useState(stateName, keepCurrentStates, noAnimation, useHoverLayer); | 
|         } | 
|         if (textGuide) { | 
|             textGuide.useState(stateName, keepCurrentStates, noAnimation, useHoverLayer); | 
|         } | 
|   | 
|         if (toNormalState) { | 
|             // Clear state | 
|             this.currentStates = []; | 
|             // Reset normal state. | 
|             this._normalState = {}; | 
|         } | 
|         else { | 
|             if (!keepCurrentStates) { | 
|                 this.currentStates = [stateName]; | 
|             } | 
|             else { | 
|                 this.currentStates.push(stateName); | 
|             } | 
|         } | 
|   | 
|         // Update animating target to the new object after state changed. | 
|         this._updateAnimationTargets(); | 
|   | 
|         this.markRedraw(); | 
|   | 
|         if (!useHoverLayer && this.__inHover) { | 
|             // Leave hover layer after states update and markRedraw. | 
|             this._toggleHoverLayerFlag(false); | 
|             // NOTE: avoid unexpected refresh when moving out from hover layer!! | 
|             // Only clear from hover layer. | 
|             this.__dirty &= ~REDRAW_BIT; | 
|         } | 
|   | 
|         // Return used state. | 
|         return state; | 
|     } | 
|   | 
|     /** | 
|      * Apply multiple states. | 
|      * @param states States list. | 
|      */ | 
|     useStates(states: string[], noAnimation?: boolean, forceUseHoverLayer?: boolean) { | 
|         if (!states.length) { | 
|             this.clearStates(); | 
|         } | 
|         else { | 
|             const stateObjects: ElementState[] = []; | 
|             const currentStates = this.currentStates; | 
|             const len = states.length; | 
|             let notChange = len === currentStates.length; | 
|             if (notChange) { | 
|                 for (let i = 0; i < len; i++) { | 
|                     if (states[i] !== currentStates[i]) { | 
|                         notChange = false; | 
|                         break; | 
|                     } | 
|                 } | 
|             } | 
|             if (notChange) { | 
|                 return; | 
|             } | 
|   | 
|             for (let i = 0; i < len; i++) { | 
|                 const stateName = states[i]; | 
|                 let stateObj: ElementState; | 
|                 if (this.stateProxy) { | 
|                     stateObj = this.stateProxy(stateName, states); | 
|                 } | 
|                 if (!stateObj) { | 
|                     stateObj = this.states[stateName]; | 
|                 } | 
|                 if (stateObj) { | 
|                     stateObjects.push(stateObj); | 
|                 } | 
|             } | 
|   | 
|             const lastStateObj = stateObjects[len - 1]; | 
|             const useHoverLayer = !!((lastStateObj && lastStateObj.hoverLayer) || forceUseHoverLayer); | 
|             if (useHoverLayer) { | 
|                 // Enter hover layer before states update. | 
|                 this._toggleHoverLayerFlag(true); | 
|             } | 
|   | 
|             const mergedState = this._mergeStates(stateObjects); | 
|             const animationCfg = this.stateTransition; | 
|   | 
|             this.saveCurrentToNormalState(mergedState); | 
|   | 
|             this._applyStateObj( | 
|                 states.join(','), | 
|                 mergedState, | 
|                 this._normalState, | 
|                 false, | 
|                 !noAnimation && !this.__inHover && animationCfg && animationCfg.duration > 0, | 
|                 animationCfg | 
|             ); | 
|   | 
|             const textContent = this._textContent; | 
|             const textGuide = this._textGuide; | 
|             if (textContent) { | 
|                 textContent.useStates(states, noAnimation, useHoverLayer); | 
|             } | 
|             if (textGuide) { | 
|                 textGuide.useStates(states, noAnimation, useHoverLayer); | 
|             } | 
|   | 
|             this._updateAnimationTargets(); | 
|   | 
|             // Create a copy | 
|             this.currentStates = states.slice(); | 
|             this.markRedraw(); | 
|   | 
|             if (!useHoverLayer && this.__inHover) { | 
|                 // Leave hover layer after states update and markRedraw. | 
|                 this._toggleHoverLayerFlag(false); | 
|                 // NOTE: avoid unexpected refresh when moving out from hover layer!! | 
|                 // Only clear from hover layer. | 
|                 this.__dirty &= ~REDRAW_BIT; | 
|             } | 
|         } | 
|     } | 
|   | 
|     /** | 
|      * Return if el.silent or any ancestor element has silent true. | 
|      */ | 
|     isSilent() { | 
|         let isSilent = this.silent; | 
|         let ancestor = this.parent; | 
|         while (!isSilent && ancestor) { | 
|             if (ancestor.silent) { | 
|                 isSilent = true; | 
|                 break; | 
|             } | 
|             ancestor = ancestor.parent; | 
|         } | 
|         return isSilent; | 
|     } | 
|   | 
|     /** | 
|      * Update animation targets when reference is changed. | 
|      */ | 
|     private _updateAnimationTargets() { | 
|         for (let i = 0; i < this.animators.length; i++) { | 
|             const animator = this.animators[i]; | 
|             if (animator.targetName) { | 
|                 animator.changeTarget((this as any)[animator.targetName]); | 
|             } | 
|         } | 
|     } | 
|   | 
|     /** | 
|      * Remove state | 
|      * @param state State to remove | 
|      */ | 
|     removeState(state: string) { | 
|         const idx = indexOf(this.currentStates, state); | 
|         if (idx >= 0) { | 
|             const currentStates = this.currentStates.slice(); | 
|             currentStates.splice(idx, 1); | 
|             this.useStates(currentStates); | 
|         } | 
|     } | 
|   | 
|     /** | 
|      * Replace exists state. | 
|      * @param oldState | 
|      * @param newState | 
|      * @param forceAdd If still add when even if replaced target not exists. | 
|      */ | 
|     replaceState(oldState: string, newState: string, forceAdd: boolean) { | 
|         const currentStates = this.currentStates.slice(); | 
|         const idx = indexOf(currentStates, oldState); | 
|         const newStateExists = indexOf(currentStates, newState) >= 0; | 
|         if (idx >= 0) { | 
|             if (!newStateExists) { | 
|                 // Replace the old with the new one. | 
|                 currentStates[idx] = newState; | 
|             } | 
|             else { | 
|                 // Only remove the old one. | 
|                 currentStates.splice(idx, 1); | 
|             } | 
|         } | 
|         else if (forceAdd && !newStateExists) { | 
|             currentStates.push(newState); | 
|         } | 
|         this.useStates(currentStates); | 
|     } | 
|   | 
|     /** | 
|      * Toogle state. | 
|      */ | 
|     toggleState(state: string, enable: boolean) { | 
|         if (enable) { | 
|             this.useState(state, true); | 
|         } | 
|         else { | 
|             this.removeState(state); | 
|         } | 
|     } | 
|   | 
|     protected _mergeStates(states: ElementState[]) { | 
|         const mergedState: ElementState = {}; | 
|         let mergedTextConfig: ElementTextConfig; | 
|         for (let i = 0; i < states.length; i++) { | 
|             const state = states[i]; | 
|             extend(mergedState, state); | 
|   | 
|             if (state.textConfig) { | 
|                 mergedTextConfig = mergedTextConfig || {}; | 
|                 extend(mergedTextConfig, state.textConfig); | 
|             } | 
|         } | 
|         if (mergedTextConfig) { | 
|             mergedState.textConfig = mergedTextConfig; | 
|         } | 
|   | 
|         return mergedState; | 
|     } | 
|   | 
|     protected _applyStateObj( | 
|         stateName: string, | 
|         state: ElementState, | 
|         normalState: ElementState, | 
|         keepCurrentStates: boolean, | 
|         transition: boolean, | 
|         animationCfg: ElementAnimateConfig | 
|     ) { | 
|         const needsRestoreToNormal = !(state && keepCurrentStates); | 
|   | 
|         // TODO: Save current state to normal? | 
|         // TODO: Animation | 
|         if (state && state.textConfig) { | 
|             // Inherit from current state or normal state. | 
|             this.textConfig = extend( | 
|                 {}, | 
|                 keepCurrentStates ? this.textConfig : normalState.textConfig | 
|             ); | 
|             extend(this.textConfig, state.textConfig); | 
|         } | 
|         else if (needsRestoreToNormal) { | 
|             if (normalState.textConfig) {   // Only restore if changed and saved. | 
|                 this.textConfig = normalState.textConfig; | 
|             } | 
|         } | 
|   | 
|         const transitionTarget: Dictionary<any> = {}; | 
|         let hasTransition = false; | 
|   | 
|         for (let i = 0; i < PRIMARY_STATES_KEYS.length; i++) { | 
|             const key = PRIMARY_STATES_KEYS[i]; | 
|             const propNeedsTransition = transition && DEFAULT_ANIMATABLE_MAP[key]; | 
|   | 
|             if (state && state[key] != null) { | 
|                 if (propNeedsTransition) { | 
|                     hasTransition = true; | 
|                     transitionTarget[key] = state[key]; | 
|                 } | 
|                 else { | 
|                     // Replace if it exist in target state | 
|                     (this as any)[key] = state[key]; | 
|                 } | 
|             } | 
|             else if (needsRestoreToNormal) { | 
|                 if (normalState[key] != null) { | 
|                     if (propNeedsTransition) { | 
|                         hasTransition = true; | 
|                         transitionTarget[key] = normalState[key]; | 
|                     } | 
|                     else { | 
|                         // Restore to normal state | 
|                         (this as any)[key] = normalState[key]; | 
|                     } | 
|                 } | 
|             } | 
|         } | 
|   | 
|         if (!transition) { | 
|             // Keep the running animation to the new values after states changed. | 
|             // Not simply stop animation. Or it may have jump effect. | 
|             for (let i = 0; i < this.animators.length; i++) { | 
|                 const animator = this.animators[i]; | 
|                 const targetName = animator.targetName; | 
|                 // Ignore loop animation | 
|                 if (!animator.getLoop()) { | 
|                     animator.__changeFinalValue(targetName | 
|                         ? ((state || normalState) as any)[targetName] | 
|                         : (state || normalState) | 
|                     ); | 
|                 } | 
|             } | 
|         } | 
|   | 
|         if (hasTransition) { | 
|             this._transitionState( | 
|                 stateName, | 
|                 transitionTarget as Props, | 
|                 animationCfg | 
|             ); | 
|         } | 
|     } | 
|   | 
|     /** | 
|      * Component is some elements attached on this element for specific purpose. | 
|      * Like clipPath, textContent | 
|      */ | 
|     private _attachComponent(componentEl: Element) { | 
|         if (componentEl.__zr && !componentEl.__hostTarget) { | 
|             if (process.env.NODE_ENV !== 'production') { | 
|                 throw new Error('Text element has been added to zrender.'); | 
|             } | 
|             return; | 
|         } | 
|   | 
|         if (componentEl === this) { | 
|             if (process.env.NODE_ENV !== 'production') { | 
|                 throw new Error('Recursive component attachment.'); | 
|             } | 
|             return; | 
|         } | 
|   | 
|         const zr = this.__zr; | 
|         if (zr) { | 
|             // Needs to add self to zrender. For rerender triggering, or animation. | 
|             componentEl.addSelfToZr(zr); | 
|         } | 
|   | 
|         componentEl.__zr = zr; | 
|         componentEl.__hostTarget = this as unknown as Element; | 
|     } | 
|   | 
|     private _detachComponent(componentEl: Element) { | 
|         if (componentEl.__zr) { | 
|             componentEl.removeSelfFromZr(componentEl.__zr); | 
|         } | 
|   | 
|         componentEl.__zr = null; | 
|         componentEl.__hostTarget = null; | 
|     } | 
|   | 
|     /** | 
|      * Get clip path | 
|      */ | 
|     getClipPath() { | 
|         return this._clipPath; | 
|     } | 
|   | 
|     /** | 
|      * Set clip path | 
|      * | 
|      * clipPath can't be shared between two elements. | 
|      */ | 
|     setClipPath(clipPath: Path) { | 
|         // Remove previous clip path | 
|         if (this._clipPath && this._clipPath !== clipPath) { | 
|             this.removeClipPath(); | 
|         } | 
|   | 
|         this._attachComponent(clipPath); | 
|   | 
|         this._clipPath = clipPath; | 
|         this.markRedraw(); | 
|     } | 
|   | 
|     /** | 
|      * Remove clip path | 
|      */ | 
|     removeClipPath() { | 
|         const clipPath = this._clipPath; | 
|         if (clipPath) { | 
|             this._detachComponent(clipPath); | 
|             this._clipPath = null; | 
|             this.markRedraw(); | 
|         } | 
|     } | 
|   | 
|     /** | 
|      * Get attached text content. | 
|      */ | 
|     getTextContent(): ZRText { | 
|         return this._textContent; | 
|     } | 
|   | 
|     /** | 
|      * Attach text on element | 
|      */ | 
|     setTextContent(textEl: ZRText) { | 
|         const previousTextContent = this._textContent; | 
|         if (previousTextContent === textEl) { | 
|             return; | 
|         } | 
|         // Remove previous textContent | 
|         if (previousTextContent && previousTextContent !== textEl) { | 
|             this.removeTextContent(); | 
|         } | 
|         if (process.env.NODE_ENV !== 'production') { | 
|             if (textEl.__zr && !textEl.__hostTarget) { | 
|                 throw new Error('Text element has been added to zrender.'); | 
|             } | 
|         } | 
|   | 
|         textEl.innerTransformable = new Transformable(); | 
|   | 
|         this._attachComponent(textEl); | 
|   | 
|         this._textContent = textEl; | 
|   | 
|         this.markRedraw(); | 
|     } | 
|   | 
|     /** | 
|      * Set layout of attached text. Will merge with the previous. | 
|      */ | 
|     setTextConfig(cfg: ElementTextConfig) { | 
|         // TODO hide cfg property? | 
|         if (!this.textConfig) { | 
|             this.textConfig = {}; | 
|         } | 
|         extend(this.textConfig, cfg); | 
|         this.markRedraw(); | 
|     } | 
|   | 
|     /** | 
|      * Remove text config | 
|      */ | 
|     removeTextConfig() { | 
|         this.textConfig = null; | 
|         this.markRedraw(); | 
|     } | 
|   | 
|     /** | 
|      * Remove attached text element. | 
|      */ | 
|     removeTextContent() { | 
|         const textEl = this._textContent; | 
|         if (textEl) { | 
|             textEl.innerTransformable = null; | 
|             this._detachComponent(textEl); | 
|             this._textContent = null; | 
|             this._innerTextDefaultStyle = null; | 
|             this.markRedraw(); | 
|         } | 
|     } | 
|   | 
|     getTextGuideLine(): Polyline { | 
|         return this._textGuide; | 
|     } | 
|   | 
|     setTextGuideLine(guideLine: Polyline) { | 
|         // Remove previous clip path | 
|         if (this._textGuide && this._textGuide !== guideLine) { | 
|             this.removeTextGuideLine(); | 
|         } | 
|   | 
|         this._attachComponent(guideLine); | 
|   | 
|         this._textGuide = guideLine; | 
|   | 
|         this.markRedraw(); | 
|     } | 
|   | 
|     removeTextGuideLine() { | 
|         const textGuide = this._textGuide; | 
|         if (textGuide) { | 
|             this._detachComponent(textGuide); | 
|             this._textGuide = null; | 
|             this.markRedraw(); | 
|         } | 
|     } | 
|     /** | 
|      * Mark element needs to be repainted | 
|      */ | 
|     markRedraw() { | 
|         this.__dirty |= REDRAW_BIT; | 
|         const zr = this.__zr; | 
|         if (zr) { | 
|             if (this.__inHover) { | 
|                 zr.refreshHover(); | 
|             } | 
|             else { | 
|                 zr.refresh(); | 
|             } | 
|         } | 
|   | 
|         // Used as a clipPath or textContent | 
|         if (this.__hostTarget) { | 
|             this.__hostTarget.markRedraw(); | 
|         } | 
|     } | 
|   | 
|     /** | 
|      * Besides marking elements to be refreshed. | 
|      * It will also invalid all cache and doing recalculate next frame. | 
|      */ | 
|     dirty() { | 
|         this.markRedraw(); | 
|     } | 
|   | 
|     private _toggleHoverLayerFlag(inHover: boolean) { | 
|         this.__inHover = inHover; | 
|         const textContent = this._textContent; | 
|         const textGuide = this._textGuide; | 
|         if (textContent) { | 
|             textContent.__inHover = inHover; | 
|         } | 
|         if (textGuide) { | 
|             textGuide.__inHover = inHover; | 
|         } | 
|     } | 
|   | 
|     /** | 
|      * Add self from zrender instance. | 
|      * Not recursively because it will be invoked when element added to storage. | 
|      */ | 
|     addSelfToZr(zr: ZRenderType) { | 
|         if (this.__zr === zr) { | 
|             return; | 
|         } | 
|   | 
|         this.__zr = zr; | 
|         // 添加动画 | 
|         const animators = this.animators; | 
|         if (animators) { | 
|             for (let i = 0; i < animators.length; i++) { | 
|                 zr.animation.addAnimator(animators[i]); | 
|             } | 
|         } | 
|   | 
|         if (this._clipPath) { | 
|             this._clipPath.addSelfToZr(zr); | 
|         } | 
|         if (this._textContent) { | 
|             this._textContent.addSelfToZr(zr); | 
|         } | 
|         if (this._textGuide) { | 
|             this._textGuide.addSelfToZr(zr); | 
|         } | 
|     } | 
|   | 
|     /** | 
|      * Remove self from zrender instance. | 
|      * Not recursively because it will be invoked when element added to storage. | 
|      */ | 
|     removeSelfFromZr(zr: ZRenderType) { | 
|         if (!this.__zr) { | 
|             return; | 
|         } | 
|   | 
|         this.__zr = null; | 
|         // Remove animation | 
|         const animators = this.animators; | 
|         if (animators) { | 
|             for (let i = 0; i < animators.length; i++) { | 
|                 zr.animation.removeAnimator(animators[i]); | 
|             } | 
|         } | 
|   | 
|         if (this._clipPath) { | 
|             this._clipPath.removeSelfFromZr(zr); | 
|         } | 
|         if (this._textContent) { | 
|             this._textContent.removeSelfFromZr(zr); | 
|         } | 
|         if (this._textGuide) { | 
|             this._textGuide.removeSelfFromZr(zr); | 
|         } | 
|     } | 
|   | 
|     /** | 
|      * 动画 | 
|      * | 
|      * @param path The key to fetch value from object. Mostly style or shape. | 
|      * @param loop Whether to loop animation. | 
|      * @param allowDiscreteAnimation Whether to allow discrete animation | 
|      * @example: | 
|      *     el.animate('style', false) | 
|      *         .when(1000, {x: 10} ) | 
|      *         .done(function(){ // Animation done }) | 
|      *         .start() | 
|      */ | 
|     animate(key?: string, loop?: boolean, allowDiscreteAnimation?: boolean) { | 
|         let target = key ? (this as any)[key] : this; | 
|   | 
|         if (process.env.NODE_ENV !== 'production') { | 
|             if (!target) { | 
|                 logError( | 
|                     'Property "' | 
|                     + key | 
|                     + '" is not existed in element ' | 
|                     + this.id | 
|                 ); | 
|                 return; | 
|             } | 
|         } | 
|   | 
|         const animator = new Animator(target, loop, allowDiscreteAnimation); | 
|         key && (animator.targetName = key); | 
|         this.addAnimator(animator, key); | 
|         return animator; | 
|     } | 
|   | 
|     addAnimator(animator: Animator<any>, key: string): void { | 
|         const zr = this.__zr; | 
|   | 
|         const el = this; | 
|   | 
|         animator.during(function () { | 
|             el.updateDuringAnimation(key as string); | 
|         }).done(function () { | 
|             const animators = el.animators; | 
|             // FIXME Animator will not be removed if use `Animator#stop` to stop animation | 
|             const idx = indexOf(animators, animator); | 
|             if (idx >= 0) { | 
|                 animators.splice(idx, 1); | 
|             } | 
|         }); | 
|   | 
|         this.animators.push(animator); | 
|   | 
|         // If animate after added to the zrender | 
|         if (zr) { | 
|             zr.animation.addAnimator(animator); | 
|         } | 
|   | 
|         // Wake up zrender to start the animation loop. | 
|         zr && zr.wakeUp(); | 
|     } | 
|   | 
|     updateDuringAnimation(key: string) { | 
|         this.markRedraw(); | 
|     } | 
|   | 
|     /** | 
|      * 停止动画 | 
|      * @param {boolean} forwardToLast If move to last frame before stop | 
|      */ | 
|     stopAnimation(scope?: string, forwardToLast?: boolean) { | 
|         const animators = this.animators; | 
|         const len = animators.length; | 
|         const leftAnimators: Animator<any>[] = []; | 
|         for (let i = 0; i < len; i++) { | 
|             const animator = animators[i]; | 
|             if (!scope || scope === animator.scope) { | 
|                 animator.stop(forwardToLast); | 
|             } | 
|             else { | 
|                 leftAnimators.push(animator); | 
|             } | 
|         } | 
|         this.animators = leftAnimators; | 
|   | 
|         return this; | 
|     } | 
|   | 
|     /** | 
|      * @param animationProps A map to specify which property to animate. If not specified, will animate all. | 
|      * @example | 
|      *  // Animate position | 
|      *  el.animateTo({ | 
|      *      position: [10, 10] | 
|      *  }, { done: () => { // done } }) | 
|      * | 
|      *  // Animate shape, style and position in 100ms, delayed 100ms, with cubicOut easing | 
|      *  el.animateTo({ | 
|      *      shape: { | 
|      *          width: 500 | 
|      *      }, | 
|      *      style: { | 
|      *          fill: 'red' | 
|      *      } | 
|      *      position: [10, 10] | 
|      *  }, { | 
|      *      duration: 100, | 
|      *      delay: 100, | 
|      *      easing: 'cubicOut', | 
|      *      done: () => { // done } | 
|      *  }) | 
|      */ | 
|     animateTo(target: Props, cfg?: ElementAnimateConfig, animationProps?: MapToType<Props, boolean>) { | 
|         animateTo(this, target, cfg, animationProps); | 
|     } | 
|   | 
|     /** | 
|      * Animate from the target state to current state. | 
|      * The params and the value are the same as `this.animateTo`. | 
|      */ | 
|   | 
|     // Overload definitions | 
|     animateFrom( | 
|         target: Props, cfg: ElementAnimateConfig, animationProps?: MapToType<Props, boolean> | 
|     ) { | 
|         animateTo(this, target, cfg, animationProps, true); | 
|     } | 
|   | 
|     protected _transitionState( | 
|         stateName: string, target: Props, cfg?: ElementAnimateConfig, animationProps?: MapToType<Props, boolean> | 
|     ) { | 
|         const animators = animateTo(this, target, cfg, animationProps); | 
|         for (let i = 0; i < animators.length; i++) { | 
|             animators[i].__fromStateTransition = stateName; | 
|         } | 
|     } | 
|   | 
|     /** | 
|      * Interface of getting the minimum bounding box. | 
|      */ | 
|     getBoundingRect(): BoundingRect { | 
|         return null; | 
|     } | 
|   | 
|     getPaintRect(): BoundingRect { | 
|         return null; | 
|     } | 
|   | 
|     /** | 
|      * The string value of `textPosition` needs to be calculated to a real postion. | 
|      * For example, `'inside'` is calculated to `[rect.width/2, rect.height/2]` | 
|      * by default. See `contain/text.js#calculateTextPosition` for more details. | 
|      * But some coutom shapes like "pin", "flag" have center that is not exactly | 
|      * `[width/2, height/2]`. So we provide this hook to customize the calculation | 
|      * for those shapes. It will be called if the `style.textPosition` is a string. | 
|      * @param {Obejct} [out] Prepared out object. If not provided, this method should | 
|      *        be responsible for creating one. | 
|      * @param {module:zrender/graphic/Style} style | 
|      * @param {Object} rect {x, y, width, height} | 
|      * @return {Obejct} out The same as the input out. | 
|      *         { | 
|      *             x: number. mandatory. | 
|      *             y: number. mandatory. | 
|      *             align: string. optional. use style.textAlign by default. | 
|      *             verticalAlign: string. optional. use style.textVerticalAlign by default. | 
|      *         } | 
|      */ | 
|     calculateTextPosition: ElementCalculateTextPosition; | 
|   | 
|     protected static initDefaultProps = (function () { | 
|         const elProto = Element.prototype; | 
|         elProto.type = 'element'; | 
|         elProto.name = ''; | 
|   | 
|         elProto.ignore = | 
|         elProto.silent = | 
|         elProto.isGroup = | 
|         elProto.draggable = | 
|         elProto.dragging = | 
|         elProto.ignoreClip = | 
|         elProto.__inHover = false; | 
|   | 
|         elProto.__dirty = REDRAW_BIT; | 
|   | 
|   | 
|         const logs: Dictionary<boolean> = {}; | 
|         function logDeprecatedError(key: string, xKey: string, yKey: string) { | 
|             if (!logs[key + xKey + yKey]) { | 
|                 console.warn(`DEPRECATED: '${key}' has been deprecated. use '${xKey}', '${yKey}' instead`); | 
|                 logs[key + xKey + yKey] = true; | 
|             } | 
|         } | 
|         // Legacy transform properties. position and scale | 
|         function createLegacyProperty( | 
|             key: string, | 
|             privateKey: string, | 
|             xKey: string, | 
|             yKey: string | 
|         ) { | 
|             Object.defineProperty(elProto, key, { | 
|                 get() { | 
|                     if (process.env.NODE_ENV !== 'production') { | 
|                         logDeprecatedError(key, xKey, yKey); | 
|                     } | 
|                     if (!this[privateKey]) { | 
|                         const pos: number[] = this[privateKey] = []; | 
|                         enhanceArray(this, pos); | 
|                     } | 
|                     return this[privateKey]; | 
|                 }, | 
|                 set(pos: number[]) { | 
|                     if (process.env.NODE_ENV !== 'production') { | 
|                         logDeprecatedError(key, xKey, yKey); | 
|                     } | 
|                     this[xKey] = pos[0]; | 
|                     this[yKey] = pos[1]; | 
|                     this[privateKey] = pos; | 
|                     enhanceArray(this, pos); | 
|                 } | 
|             }); | 
|             function enhanceArray(self: any, pos: number[]) { | 
|                 Object.defineProperty(pos, 0, { | 
|                     get() { | 
|                         return self[xKey]; | 
|                     }, | 
|                     set(val: number) { | 
|                         self[xKey] = val; | 
|                     } | 
|                 }); | 
|                 Object.defineProperty(pos, 1, { | 
|                     get() { | 
|                         return self[yKey]; | 
|                     }, | 
|                     set(val: number) { | 
|                         self[yKey] = val; | 
|                     } | 
|                 }); | 
|             } | 
|         } | 
|         if (Object.defineProperty | 
|             // Just don't support ie8 | 
|             // && (!(env as any).browser.ie || (env as any).browser.version > 8) | 
|         ) { | 
|             createLegacyProperty('position', '_legacyPos', 'x', 'y'); | 
|             createLegacyProperty('scale', '_legacyScale', 'scaleX', 'scaleY'); | 
|             createLegacyProperty('origin', '_legacyOrigin', 'originX', 'originY'); | 
|         } | 
|     })() | 
| } | 
|   | 
| mixin(Element, Eventful); | 
| mixin(Element, Transformable); | 
|   | 
| function animateTo<T>( | 
|     animatable: Element<T>, | 
|     target: Dictionary<any>, | 
|     cfg: ElementAnimateConfig, | 
|     animationProps: Dictionary<any>, | 
|     reverse?: boolean | 
| ) { | 
|     cfg = cfg || {}; | 
|     const animators: Animator<any>[] = []; | 
|     animateToShallow( | 
|         animatable, | 
|         '', | 
|         animatable, | 
|         target, | 
|         cfg, | 
|         animationProps, | 
|         animators, | 
|         reverse | 
|     ); | 
|   | 
|     let finishCount = animators.length; | 
|     let doneHappened = false; | 
|     const cfgDone = cfg.done; | 
|     const cfgAborted = cfg.aborted; | 
|   | 
|     const doneCb = () => { | 
|         doneHappened = true; | 
|         finishCount--; | 
|         if (finishCount <= 0) { | 
|             doneHappened | 
|                 ? (cfgDone && cfgDone()) | 
|                 : (cfgAborted && cfgAborted()); | 
|         } | 
|     }; | 
|   | 
|     const abortedCb = () => { | 
|         finishCount--; | 
|         if (finishCount <= 0) { | 
|             doneHappened | 
|                 ? (cfgDone && cfgDone()) | 
|                 : (cfgAborted && cfgAborted()); | 
|         } | 
|     }; | 
|   | 
|     // No animators. This should be checked before animators[i].start(), | 
|     // because 'done' may be executed immediately if no need to animate. | 
|     if (!finishCount) { | 
|         cfgDone && cfgDone(); | 
|     } | 
|   | 
|     // Adding during callback to the first animator | 
|     if (animators.length > 0 && cfg.during) { | 
|         // TODO If there are two animators in animateTo, and the first one is stopped by other animator. | 
|         animators[0].during((target, percent) => { | 
|             cfg.during(percent); | 
|         }); | 
|     } | 
|   | 
|     // Start after all animators created | 
|     // Incase any animator is done immediately when all animation properties are not changed | 
|     for (let i = 0; i < animators.length; i++) { | 
|         const animator = animators[i]; | 
|         if (doneCb) { | 
|             animator.done(doneCb); | 
|         } | 
|         if (abortedCb) { | 
|             animator.aborted(abortedCb); | 
|         } | 
|         if (cfg.force) { | 
|             animator.duration(cfg.duration); | 
|         } | 
|         animator.start(cfg.easing); | 
|     } | 
|   | 
|     return animators; | 
| } | 
|   | 
| function copyArrShallow(source: number[], target: number[], len: number) { | 
|     for (let i = 0; i < len; i++) { | 
|         source[i] = target[i]; | 
|     } | 
| } | 
|   | 
| function is2DArray(value: any[]): value is number[][] { | 
|     return isArrayLike(value[0]); | 
| } | 
|   | 
| function copyValue(target: Dictionary<any>, source: Dictionary<any>, key: string) { | 
|     if (isArrayLike(source[key])) { | 
|         if (!isArrayLike(target[key])) { | 
|             target[key] = []; | 
|         } | 
|   | 
|         if (isTypedArray(source[key])) { | 
|             const len = source[key].length; | 
|             if (target[key].length !== len) { | 
|                 target[key] = new (source[key].constructor)(len); | 
|                 copyArrShallow(target[key], source[key], len); | 
|             } | 
|         } | 
|         else { | 
|             const sourceArr = source[key] as any[]; | 
|             const targetArr = target[key] as any[]; | 
|   | 
|             const len0 = sourceArr.length; | 
|             if (is2DArray(sourceArr)) { | 
|                 // NOTE: each item should have same length | 
|                 const len1 = sourceArr[0].length; | 
|   | 
|                 for (let i = 0; i < len0; i++) { | 
|                     if (!targetArr[i]) { | 
|                         targetArr[i] = Array.prototype.slice.call(sourceArr[i]); | 
|                     } | 
|                     else { | 
|                         copyArrShallow(targetArr[i], sourceArr[i], len1); | 
|                     } | 
|                 } | 
|             } | 
|             else { | 
|                 copyArrShallow(targetArr, sourceArr, len0); | 
|             } | 
|   | 
|             targetArr.length = sourceArr.length; | 
|         } | 
|     } | 
|     else { | 
|         target[key] = source[key]; | 
|     } | 
| } | 
|   | 
| function isValueSame(val1: any, val2: any) { | 
|     return val1 === val2 | 
|         // Only check 1 dimension array | 
|         || isArrayLike(val1) && isArrayLike(val2) && is1DArraySame(val1, val2); | 
| } | 
|   | 
| function is1DArraySame(arr0: ArrayLike<number>, arr1: ArrayLike<number>) { | 
|     const len = arr0.length; | 
|     if (len !== arr1.length) { | 
|         return false; | 
|     } | 
|     for (let i = 0; i < len; i++) { | 
|         if (arr0[i] !== arr1[i]) { | 
|             return false; | 
|         } | 
|     } | 
|     return true; | 
| } | 
|   | 
| function animateToShallow<T>( | 
|     animatable: Element<T>, | 
|     topKey: string, | 
|     animateObj: Dictionary<any>, | 
|     target: Dictionary<any>, | 
|     cfg: ElementAnimateConfig, | 
|     animationProps: Dictionary<any> | true, | 
|     animators: Animator<any>[], | 
|     reverse: boolean    // If `true`, animate from the `target` to current state. | 
| ) { | 
|     const targetKeys = keys(target); | 
|     const duration = cfg.duration; | 
|     const delay = cfg.delay; | 
|     const additive = cfg.additive; | 
|     const setToFinal = cfg.setToFinal; | 
|     const animateAll = !isObject(animationProps); | 
|     // Find last animator animating same prop. | 
|     const existsAnimators = animatable.animators; | 
|   | 
|     let animationKeys: string[] = []; | 
|     for (let k = 0; k < targetKeys.length; k++) { | 
|         const innerKey = targetKeys[k] as string; | 
|         const targetVal = target[innerKey]; | 
|   | 
|         if ( | 
|             targetVal != null && animateObj[innerKey] != null | 
|             && (animateAll || (animationProps as Dictionary<any>)[innerKey]) | 
|         ) { | 
|             if (isObject(targetVal) | 
|                 && !isArrayLike(targetVal) | 
|                 && !isGradientObject(targetVal) | 
|             ) { | 
|                 if (topKey) { | 
|                     // logError('Only support 1 depth nest object animation.'); | 
|                     // Assign directly. | 
|                     // TODO richText? | 
|                     if (!reverse) { | 
|                         animateObj[innerKey] = targetVal; | 
|                         animatable.updateDuringAnimation(topKey); | 
|                     } | 
|                     continue; | 
|                 } | 
|                 animateToShallow( | 
|                     animatable, | 
|                     innerKey, | 
|                     animateObj[innerKey], | 
|                     targetVal, | 
|                     cfg, | 
|                     animationProps && (animationProps as Dictionary<any>)[innerKey], | 
|                     animators, | 
|                     reverse | 
|                 ); | 
|             } | 
|             else { | 
|                 animationKeys.push(innerKey); | 
|             } | 
|         } | 
|         else if (!reverse) { | 
|             // Assign target value directly. | 
|             animateObj[innerKey] = targetVal; | 
|             animatable.updateDuringAnimation(topKey); | 
|             // Previous animation will be stopped on the changed keys. | 
|             // So direct assign is also included. | 
|             animationKeys.push(innerKey); | 
|         } | 
|     } | 
|   | 
|     let keyLen = animationKeys.length; | 
|     // Stop previous animations on the same property. | 
|     if (!additive && keyLen) { | 
|         // Stop exists animation on specific tracks. Only one animator available for each property. | 
|         // TODO Should invoke previous animation callback? | 
|         for (let i = 0; i < existsAnimators.length; i++) { | 
|             const animator = existsAnimators[i]; | 
|             if (animator.targetName === topKey) { | 
|                 const allAborted = animator.stopTracks(animationKeys); | 
|                 if (allAborted) {   // This animator can't be used. | 
|                     const idx = indexOf(existsAnimators, animator); | 
|                     existsAnimators.splice(idx, 1); | 
|                 } | 
|             } | 
|         } | 
|     } | 
|   | 
|     // Ignore values not changed. | 
|     // NOTE: Must filter it after previous animation stopped | 
|     // and make sure the value to compare is using initial frame if animation is not started yet when setToFinal is used. | 
|     if (!cfg.force) { | 
|         animationKeys = filter(animationKeys, key => !isValueSame(target[key], animateObj[key])); | 
|         keyLen = animationKeys.length; | 
|     } | 
|   | 
|     if (keyLen > 0 | 
|         // cfg.force is mainly for keep invoking onframe and ondone callback even if animation is not necessary. | 
|         // So if there is already has animators. There is no need to create another animator if not necessary. | 
|         // Or it will always add one more with empty target. | 
|         || (cfg.force && !animators.length) | 
|     ) { | 
|         let revertedSource: Dictionary<any>; | 
|         let reversedTarget: Dictionary<any>; | 
|         let sourceClone: Dictionary<any>; | 
|         if (reverse) { | 
|             reversedTarget = {}; | 
|             if (setToFinal) { | 
|                 revertedSource = {}; | 
|             } | 
|             for (let i = 0; i < keyLen; i++) { | 
|                 const innerKey = animationKeys[i]; | 
|                 reversedTarget[innerKey] = animateObj[innerKey]; | 
|                 if (setToFinal) { | 
|                     revertedSource[innerKey] = target[innerKey]; | 
|                 } | 
|                 else { | 
|                     // The usage of "animateFrom" expects that the element props has been updated dirctly to | 
|                     // "final" values outside, and input the "from" values here (i.e., in variable `target` here). | 
|                     // So here we assign the "from" values directly to element here (rather that in the next frame) | 
|                     // to prevent the "final" values from being read in any other places (like other running | 
|                     // animator during callbacks). | 
|                     // But if `setToFinal: true` this feature can not be satisfied. | 
|                     animateObj[innerKey] = target[innerKey]; | 
|                 } | 
|             } | 
|         } | 
|         else if (setToFinal) { | 
|             sourceClone = {}; | 
|             for (let i = 0; i < keyLen; i++) { | 
|                 const innerKey = animationKeys[i]; | 
|                 // NOTE: Must clone source after the stopTracks. The property may be modified in stopTracks. | 
|                 sourceClone[innerKey] = cloneValue(animateObj[innerKey]); | 
|                 // Use copy, not change the original reference | 
|                 // Copy from target to source. | 
|                 copyValue(animateObj, target, innerKey); | 
|             } | 
|         } | 
|   | 
|         const animator = new Animator(animateObj, false, false, additive ? filter( | 
|             // Use key string instead object reference because ref may be changed. | 
|             existsAnimators, animator => animator.targetName === topKey | 
|         ) : null); | 
|   | 
|         animator.targetName = topKey; | 
|         if (cfg.scope) { | 
|             animator.scope = cfg.scope; | 
|         } | 
|   | 
|         if (setToFinal && revertedSource) { | 
|             animator.whenWithKeys(0, revertedSource, animationKeys); | 
|         } | 
|         if (sourceClone) { | 
|             animator.whenWithKeys(0, sourceClone, animationKeys); | 
|         } | 
|   | 
|         animator.whenWithKeys( | 
|             duration == null ? 500 : duration, | 
|             reverse ? reversedTarget : target, | 
|             animationKeys | 
|         ).delay(delay || 0); | 
|   | 
|         animatable.addAnimator(animator, topKey); | 
|         animators.push(animator); | 
|     } | 
| } | 
|   | 
|   | 
| export default Element; |