| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172 |
- 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';
- import { invert } from './core/matrix';
- 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 boundingRect of the host element.
- * The coords of `layoutRect` is based on the target element, but not global.
- *
- * [NOTICE]: boundingRect includes `lineWidth`, which is inconsistent with
- * the general element placement principle, where `lineWidth` is not counted.
- */
- 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
- *
- * [NOTICE]: If the host element may rotate to non-parallel to screen x/y,
- * need to use `local:true`, otherwise the transformed layout rect may not be expected.
- *
- * @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
- /**
- * Auto calculate overflow area by `textConfig.layoutRect` (if any) or `host.boundingRect`.
- * It makes sense only if label is inside. It ensure the text does not overflow the host.
- * Useful in `text.style.overflow` and `text.style.lineOverflow`.
- *
- * If `textConfig.rotation` or `text.rotation exists`, it works correctly only when the rotated text is parallel
- * to its host (i.e. 0, PI/2, PI, PI*3/2, 2*PI, ...). Do not supported other cases until a real scenario arises.
- */
- autoOverflowArea?: 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
- ignoreHostSilent?: 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 = {
- /**
- * NOTICE: Only canvas renderer supports hover layer. Users must not set hoverLayer
- * flag in non-canvas renderer, otherwise it may cause unexpected behavior.
- *
- * A truthy value (regardless of number or boolean) means hover layer is used.
- */
- hoverLayer?: boolean | number
- }
- export type ElementCalculateTextPosition = (
- out: TextPositionCalculationResult,
- style: ElementTextConfig,
- rect: RectLike
- ) => TextPositionCalculationResult;
- const tmpTextPosCalcRes = {} as TextPositionCalculationResult;
- const tmpBoundingRect = new BoundingRect(0, 0, 0, 0);
- const tmpInnerTextTrans: number[] = [];
- // It indicates a status of the element - whether it should be rendered or have been rendered
- // in a hover layer.
- // It also record the restriction of props changes when entering the hover status.
- // A falsy value means not in haver layer; a truthy value means in haver layer.
- export type InHoverLayerKind =
- typeof IN_HOVER_LAYER_KIND_NO
- | typeof IN_HOVER_LAYER_KIND_ONLY_STYLE_CHANGE;
- // | typeof IN_HOVER_LAYER_KIND_NO_LIMIT;
- // Not in hover layer.
- export const IN_HOVER_LAYER_KIND_NO = 0;
- // In hover layer and only style change when entering hover layer.
- export const IN_HOVER_LAYER_KIND_ONLY_STYLE_CHANGE = 1;
- // In hover layer and no restriction of changing.
- // export const IN_HOVER_LAYER_KIND_NO_LIMIT = 2;
- // 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
- /**
- * When this element has `__hostTarget` (e.g., this is a `textContent`), whether
- * its silent is controlled by that host silent. They may need separate silent
- * settings. e.g., the host do not have `fill` but only `stroke`, or their mouse
- * events serve for different features.
- */
- ignoreHostSilent: 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;
- /**
- * Whether this element has been moved to the hover layer.
- * If so, dirty will only trigger the zrender refresh hover layer.
- *
- * Hover layer is typically useful for progressive rendering case,
- * where the underlying layers can remain not dirty for most hovering
- * interactions.
- *
- * [HOVER_LAYER_CONSTRAINTS]:
- *
- * The "hover layer" mechanism expects the changes are applied only on a hover layer, while the original
- * layer should not be repainted. However, subsequent user operations may still require the original layer
- * to be repainted. If the element props have been modified due to hover state switching, the final effect
- * will differ unexpectedly after repainting.
- * For example, suppose a hover state defines different opacity, color and transform scale. when hovering
- * triggers that state, an extra glyph with those props is rendered on the hover layer and overlays the
- * the original glyph, but the original layer remains unchanged. The final effect is a visual composition
- * of the two. Then if clicking something to trigger a repaint of all layers (e.g., click echarts legend
- * to hide and then show them, or triggered by axisPointer, where hover style is expected to keep displaying),
- * and if it is rendered on the normal layer differently, the final composition is changed unexpectedly.
- *
- * Several candidate approches may resolve this issue:
- * (A) Clone elements for hover layer rendering. This might be a thorough solution, since all of the original
- * elements remain intact and can be repainted to the original layer without changes.
- * (B) Introduce a separate `__hoverStyle` to keep the original `this.style` unchanged, and only styles
- * changes are allowed in entering or leaving hover layer via `useState` and `useStates` while other changes
- * are ignored. And for simplicity, and no separate structures are provided for storing other props.
- * This approach can resolve many cases, but is still problematic in some cases.
- *
- * PENDING:
- * 1. Currently we simply implement (B), until some concrete scenarios require (A) in future.
- * 2. [HOVER_LAYER_CONSTRAINTS_TEXT]
- * Consider:
- * - Text style change may lead to creating or updating of subText elements (TSpan).
- * - An special handling can be make in (B) - if the element is not rendered on the original layer
- * (typically due to `ignore: true` or `invisible: true`), it can be rendered to the hover layer
- * without the restriction "only style can change". This is useful to the scenario "hover an
- * element to show its _textContent".
- * All these cases require display list to be re-built, or need a exclusive display list for hover layer,
- * and more precise dirty bit (REDRAW_BIT) handling is needed for that.
- * But it would introduce considerable complexity. And unlike `Path`, rendering the same text in multiple
- * layers may cause undesirable visual effect. Therefore, we do not implement it unless required. Currently
- * hover layer is disabled for text. Text must still be rendered, since it may carry important infomation.
- */
- __inHover: InHoverLayerKind
- __clipPaths?: Path[]
- /**
- * 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);
- const hasPosition = textConfig.position != null;
- const autoOverflowArea = textConfig.autoOverflowArea;
- let layoutRect: BoundingRect;
- if (autoOverflowArea || hasPosition) {
- layoutRect = tmpBoundingRect;
- if (textConfig.layoutRect) {
- layoutRect.copy(textConfig.layoutRect);
- }
- else {
- layoutRect.copy(this.getBoundingRect());
- }
- if (!isLocal) {
- layoutRect.applyTransform(this.transform);
- }
- }
- // Force set attached text's position if `position` is in config.
- if (hasPosition) {
- 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];
- }
- }
- const innerTextDefaultStyle = this._innerTextDefaultStyle || (this._innerTextDefaultStyle = {});
- if (autoOverflowArea) {
- const overflowRect = innerTextDefaultStyle.overflowRect =
- innerTextDefaultStyle.overflowRect || new BoundingRect(0, 0, 0, 0);
- innerTransformable.getLocalTransform(tmpInnerTextTrans);
- invert(tmpInnerTextTrans, tmpInnerTextTrans);
- BoundingRect.copy(overflowRect, layoutRect);
- // If transform to a non-orthogonal state (e.g. rotate PI/3), the result of this "apply"
- // is not expected. But we don't need to address it until a real scenario arises.
- overflowRect.applyTransform(tmpInnerTextTrans);
- }
- else {
- innerTextDefaultStyle.overflowRect = null;
- }
- // [CAUTION] Do not change `innerTransformable` below.
- // 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;
- 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 textContent = this._textContent;
- const useHoverLayer = shouldUseHoverLayer(this, textContent, state, forceUseHoverLayer);
- if (useHoverLayer && !this.__inHover) {
- // Enter hover layer before states update.
- this.__inHover = useHoverLayer;
- }
- this._applyStateObj(
- stateName,
- state,
- this._normalState,
- keepCurrentStates,
- canTransition(this, noAnimation, animationCfg),
- animationCfg,
- );
- // Also set text content.
- 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.__inHover = IN_HOVER_LAYER_KIND_NO;
- // 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 textContent = this._textContent;
- const useHoverLayer = shouldUseHoverLayer(this, textContent, lastStateObj, forceUseHoverLayer);
- if (useHoverLayer && !this.__inHover) {
- // Enter hover layer before states update.
- this.__inHover = useHoverLayer;
- }
- const mergedState = this._mergeStates(stateObjects);
- const animationCfg = this.stateTransition;
- this.saveCurrentToNormalState(mergedState);
- this._applyStateObj(
- states.join(','),
- mergedState,
- this._normalState,
- false,
- canTransition(this, noAnimation, animationCfg),
- animationCfg,
- );
- 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.__inHover = IN_HOVER_LAYER_KIND_NO;
- // 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() {
- // Follow the logic of `Handler.ts`#`isHover`.
- let el: Element = this;
- while (el) {
- if (el.silent) {
- return true;
- }
- const hostEl = el.__hostTarget;
- el = hostEl ? (el.ignoreHostSilent ? null : hostEl) : el.parent;
- }
- return false;
- }
- /**
- * 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
- ) {
- if (this.__inHover === IN_HOVER_LAYER_KIND_ONLY_STYLE_CHANGE) {
- return;
- }
- 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();
- }
- /**
- * 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.ignoreHostSilent =
- elProto.isGroup =
- elProto.draggable =
- elProto.dragging =
- elProto.ignoreClip = false;
- elProto.__inHover = IN_HOVER_LAYER_KIND_NO;
- 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);
- }
- }
- function shouldUseHoverLayer(
- el: Element,
- textContent: Element,
- nextState: ElementState,
- forceUseHoverLayer: boolean
- ): InHoverLayerKind {
- return (
- !((nextState && nextState.hoverLayer) || forceUseHoverLayer)
- // PENDING: See HOVER_LAYER_CONSTRAINTS_TEXT for the reasons.
- || isTextRelatedEl(el)
- || (textContent && isTextRelatedEl(textContent))
- )
- ? IN_HOVER_LAYER_KIND_NO
- // If using haver layer and previously it is not in a hover layer and invisible.
- // PENDING: See HOVER_LAYER_CONSTRAINTS_TEXT for the reasons.
- // : (!el.__inHover && (el.ignore || (el as DisplayableProps).invisible))
- // ? IN_HOVER_LAYER_KIND_NO_LIMIT
- // Otherwise (typically, perviously anything has been painted on the original layer),
- // only styles can be modified. See more detailed reasons in `HOVER_LAYER_CONSTRAINTS`.
- : IN_HOVER_LAYER_KIND_ONLY_STYLE_CHANGE;
- }
- function isTextRelatedEl(el: Element<ElementProps>): boolean {
- return el.type === 'text' || el.type === 'tspan';
- }
- function canTransition(
- el: Element,
- noAnimation: boolean,
- animationCfg: ElementAnimateConfig
- ): boolean {
- return !noAnimation && !el.__inHover && animationCfg && animationCfg.duration > 0;
- }
- export default Element;
|