12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030 |
- 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;
|