UNPKG

di-echarts

Version:

Apache ECharts is a powerful, interactive charting and data visualization library for browser

633 lines (575 loc) 23.3 kB
/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // Helpers for creating transitions in custom series and graphic components. import Element, { ElementAnimateConfig, ElementProps } from 'zrender/src/Element'; import { makeInner, normalizeToArray } from '../util/model'; import { assert, bind, each, eqNaN, extend, hasOwn, indexOf, isArrayLike, keys, reduce } from 'zrender/src/core/util'; import { cloneValue } from 'zrender/src/animation/Animator'; import Displayable, { DisplayableProps } from 'zrender/src/graphic/Displayable'; import Model from '../model/Model'; import { getAnimationConfig } from './basicTransition'; import { Path } from '../util/graphic'; import { warn } from '../util/log'; import { AnimationOption, AnimationOptionMixin, ZRStyleProps } from '../util/types'; import { Dictionary } from 'zrender/src/core/types'; import { PathStyleProps } from 'zrender/src/graphic/Path'; import { TRANSFORMABLE_PROPS, TransformProp } from 'zrender/src/core/Transformable'; const LEGACY_TRANSFORM_PROPS_MAP = { position: ['x', 'y'], scale: ['scaleX', 'scaleY'], origin: ['originX', 'originY'] } as const; const LEGACY_TRANSFORM_PROPS = keys(LEGACY_TRANSFORM_PROPS_MAP); const TRANSFORM_PROPS_MAP = reduce(TRANSFORMABLE_PROPS, (obj, key) => { obj[key] = 1; return obj; }, {} as Record<TransformProp, 1>); const transformPropNamesStr = TRANSFORMABLE_PROPS.join(', '); // '' means root export const ELEMENT_ANIMATABLE_PROPS = ['', 'style', 'shape', 'extra'] as const; export type TransitionProps = string | string[]; export type ElementRootTransitionProp = TransformProp | 'shape' | 'extra' | 'style'; export interface TransitionOptionMixin<T = Record<string, any>> { transition?: (keyof T & string) | ((keyof T & string)[]) | 'all' enterFrom?: T; leaveTo?: T; enterAnimation?: AnimationOption updateAnimation?: AnimationOption leaveAnimation?: AnimationOption }; interface LooseElementProps extends ElementProps { style?: ZRStyleProps; shape?: Dictionary<unknown>; } type TransitionElementOption = Partial<Record<TransformProp, number>> & { shape?: Dictionary<any> & TransitionOptionMixin style?: PathStyleProps & TransitionOptionMixin extra?: Dictionary<any> & TransitionOptionMixin invisible?: boolean silent?: boolean autoBatch?: boolean ignore?: boolean during?: (params: TransitionDuringAPI) => void } & TransitionOptionMixin; const transitionInnerStore = makeInner<{ leaveToProps: ElementProps; userDuring: (params: TransitionDuringAPI) => void; }, Element>(); export interface TransitionBaseDuringAPI { // Usually other props do not need to be changed in animation during. setTransform(key: TransformProp, val: number): this getTransform(key: TransformProp): number; setExtra(key: string, val: unknown): this getExtra(key: string): unknown } export interface TransitionDuringAPI< StyleOpt extends any = any, ShapeOpt extends any = any > extends TransitionBaseDuringAPI { setShape<T extends keyof ShapeOpt>(key: T, val: ShapeOpt[T]): this; getShape<T extends keyof ShapeOpt>(key: T): ShapeOpt[T]; setStyle<T extends keyof StyleOpt>(key: T, val: StyleOpt[T]): this getStyle<T extends keyof StyleOpt>(key: T): StyleOpt[T]; }; function getElementAnimationConfig( animationType: 'enter' | 'update' | 'leave', el: Element, elOption: TransitionElementOption, parentModel: Model<AnimationOptionMixin>, dataIndex?: number ) { const animationProp = `${animationType}Animation` as const; const config: ElementAnimateConfig = getAnimationConfig(animationType, parentModel, dataIndex) || {}; const userDuring = transitionInnerStore(el).userDuring; // Only set when duration is > 0 and it's need to be animated. if (config.duration > 0) { // For simplicity, if during not specified, the previous during will not work any more. config.during = userDuring ? bind(duringCall, { el: el, userDuring: userDuring }) : null; config.setToFinal = true; config.scope = animationType; } extend(config, elOption[animationProp]); return config; } export function applyUpdateTransition( el: Element, elOption: TransitionElementOption, animatableModel?: Model<AnimationOptionMixin>, opts?: { dataIndex?: number, isInit?: boolean, clearStyle?: boolean } ) { opts = opts || {}; const {dataIndex, isInit, clearStyle} = opts; const hasAnimation = animatableModel.isAnimationEnabled(); // Save the meta info for further morphing. Like apply on the sub morphing elements. const store = transitionInnerStore(el); const styleOpt = elOption.style; store.userDuring = elOption.during; const transFromProps = {} as ElementProps; const propsToSet = {} as ElementProps; prepareTransformAllPropsFinal(el, elOption, propsToSet); prepareShapeOrExtraAllPropsFinal('shape', elOption, propsToSet); prepareShapeOrExtraAllPropsFinal('extra', elOption, propsToSet); if (!isInit && hasAnimation) { prepareTransformTransitionFrom(el, elOption, transFromProps); prepareShapeOrExtraTransitionFrom('shape', el, elOption, transFromProps); prepareShapeOrExtraTransitionFrom('extra', el, elOption, transFromProps); prepareStyleTransitionFrom(el, elOption, styleOpt, transFromProps); } (propsToSet as DisplayableProps).style = styleOpt; applyPropsDirectly(el, propsToSet, clearStyle); applyMiscProps(el, elOption); if (hasAnimation) { if (isInit) { const enterFromProps: ElementProps = {}; each(ELEMENT_ANIMATABLE_PROPS, propName => { const prop: TransitionOptionMixin = propName ? elOption[propName] : elOption; if (prop && prop.enterFrom) { if (propName) { (enterFromProps as any)[propName] = (enterFromProps as any)[propName] || {}; } extend(propName ? (enterFromProps as any)[propName] : enterFromProps, prop.enterFrom); } }); const config = getElementAnimationConfig('enter', el, elOption, animatableModel, dataIndex); if (config.duration > 0) { el.animateFrom(enterFromProps, config); } } else { applyPropsTransition(el, elOption, dataIndex || 0, animatableModel, transFromProps); } } // Store leave to be used in leave transition. updateLeaveTo(el, elOption); styleOpt ? el.dirty() : el.markRedraw(); } export function updateLeaveTo(el: Element, elOption: TransitionElementOption) { // Try merge to previous set leaveTo let leaveToProps: ElementProps = transitionInnerStore(el).leaveToProps; for (let i = 0; i < ELEMENT_ANIMATABLE_PROPS.length; i++) { const propName = ELEMENT_ANIMATABLE_PROPS[i]; const prop: TransitionOptionMixin = propName ? elOption[propName] : elOption; if (prop && prop.leaveTo) { if (!leaveToProps) { leaveToProps = transitionInnerStore(el).leaveToProps = {}; } if (propName) { (leaveToProps as any)[propName] = (leaveToProps as any)[propName] || {}; } extend(propName ? (leaveToProps as any)[propName] : leaveToProps, prop.leaveTo); } } } export function applyLeaveTransition( el: Element, elOption: TransitionElementOption, animatableModel: Model<AnimationOptionMixin>, onRemove?: () => void ): void { if (el) { const parent = el.parent; const leaveToProps = transitionInnerStore(el).leaveToProps; if (leaveToProps) { // TODO TODO use leave after leaveAnimation in series is introduced // TODO Data index? const config = getElementAnimationConfig('update', el, elOption, animatableModel, 0); config.done = () => { parent.remove(el); onRemove && onRemove(); }; el.animateTo(leaveToProps, config); } else { parent.remove(el); onRemove && onRemove(); } } } export function isTransitionAll(transition: TransitionProps): transition is 'all' { return transition === 'all'; } function applyPropsDirectly( el: Element, // Can be null/undefined allPropsFinal: ElementProps, clearStyle: boolean ) { const styleOpt = (allPropsFinal as Displayable).style; if (!el.isGroup && styleOpt) { if (clearStyle) { (el as Displayable).useStyle({}); // When style object changed, how to trade the existing animation? // It is probably complicated and not needed to cover all the cases. // But still need consider the case: // (1) When using init animation on `style.opacity`, and before the animation // ended users triggers an update by mousewhel. At that time the init // animation should better be continued rather than terminated. // So after `useStyle` called, we should change the animation target manually // to continue the effect of the init animation. // (2) PENDING: If the previous animation targeted at a `val1`, and currently we need // to update the value to `val2` and no animation declared, should be terminate // the previous animation or just modify the target of the animation? // Therotically That will happen not only on `style` but also on `shape` and // `transfrom` props. But we haven't handle this case at present yet. // (3) PENDING: Is it proper to visit `animators` and `targetName`? const animators = el.animators; for (let i = 0; i < animators.length; i++) { const animator = animators[i]; // targetName is the "topKey". if (animator.targetName === 'style') { animator.changeTarget((el as Displayable).style); } } } (el as Displayable).setStyle(styleOpt); } if (allPropsFinal) { // Not set style here. (allPropsFinal as DisplayableProps).style = null; // Set el to the final state firstly. allPropsFinal && el.attr(allPropsFinal); (allPropsFinal as DisplayableProps).style = styleOpt; } } function applyPropsTransition( el: Element, elOption: TransitionElementOption, dataIndex: number, model: Model<AnimationOptionMixin>, // Can be null/undefined transFromProps: ElementProps ): void { if (transFromProps) { const config = getElementAnimationConfig('update', el, elOption, model, dataIndex); if (config.duration > 0) { el.animateFrom(transFromProps, config); } } } function applyMiscProps( el: Element, elOption: TransitionElementOption ) { // Merge by default. hasOwn(elOption, 'silent') && (el.silent = elOption.silent); hasOwn(elOption, 'ignore') && (el.ignore = elOption.ignore); if (el instanceof Displayable) { hasOwn(elOption, 'invisible') && ((el as Path).invisible = elOption.invisible); } if (el instanceof Path) { hasOwn(elOption, 'autoBatch') && ((el as Path).autoBatch = elOption.autoBatch); } } // Use it to avoid it be exposed to user. const tmpDuringScope = {} as { el: Element; }; const transitionDuringAPI: TransitionDuringAPI = { // Usually other props do not need to be changed in animation during. setTransform(key: TransformProp, val: unknown) { if (__DEV__) { assert(hasOwn(TRANSFORM_PROPS_MAP, key), 'Only ' + transformPropNamesStr + ' available in `setTransform`.'); } tmpDuringScope.el[key] = val as number; return this; }, getTransform(key: TransformProp): number { if (__DEV__) { assert(hasOwn(TRANSFORM_PROPS_MAP, key), 'Only ' + transformPropNamesStr + ' available in `getTransform`.'); } return tmpDuringScope.el[key]; }, setShape(key: any, val: unknown) { if (__DEV__) { assertNotReserved(key); } const el = tmpDuringScope.el as Path; const shape = el.shape || (el.shape = {}); shape[key] = val; el.dirtyShape && el.dirtyShape(); return this; }, getShape(key: any): any { if (__DEV__) { assertNotReserved(key); } const shape = (tmpDuringScope.el as Path).shape; if (shape) { return shape[key]; } }, setStyle(key: any, val: unknown) { if (__DEV__) { assertNotReserved(key); } const el = tmpDuringScope.el as Displayable; const style = el.style; if (style) { if (__DEV__) { if (eqNaN(val)) { warn('style.' + key + ' must not be assigned with NaN.'); } } style[key] = val; el.dirtyStyle && el.dirtyStyle(); } return this; }, getStyle(key: any): any { if (__DEV__) { assertNotReserved(key); } const style = (tmpDuringScope.el as Displayable).style; if (style) { return style[key]; } }, setExtra(key: any, val: unknown) { if (__DEV__) { assertNotReserved(key); } const extra = (tmpDuringScope.el as LooseElementProps).extra || ((tmpDuringScope.el as LooseElementProps).extra = {}); extra[key] = val; return this; }, getExtra(key: string): unknown { if (__DEV__) { assertNotReserved(key); } const extra = (tmpDuringScope.el as LooseElementProps).extra; if (extra) { return extra[key]; } } }; function assertNotReserved(key: string) { if (__DEV__) { if (key === 'transition' || key === 'enterFrom' || key === 'leaveTo') { throw new Error('key must not be "' + key + '"'); } } } function duringCall( this: { el: Element; userDuring: (params: TransitionDuringAPI) => void; } ): void { // Do not provide "percent" until some requirements come. // Because consider thies case: // enterFrom: {x: 100, y: 30}, transition: 'x'. // And enter duration is different from update duration. // Thus it might be confused about the meaning of "percent" in during callback. const scope = this; const el = scope.el; if (!el) { return; } // If el is remove from zr by reason like legend, during still need to called, // because el will be added back to zr and the prop value should not be incorrect. const latestUserDuring = transitionInnerStore(el).userDuring; const scopeUserDuring = scope.userDuring; // Ensured a during is only called once in each animation frame. // If a during is called multiple times in one frame, maybe some users' calculation logic // might be wrong (not sure whether this usage exists). // The case of a during might be called twice can be: by default there is a animator for // 'x', 'y' when init. Before the init animation finished, call `setOption` to start // another animators for 'style'/'shape'/'extra'. if (latestUserDuring !== scopeUserDuring) { // release scope.el = scope.userDuring = null; return; } tmpDuringScope.el = el; // Give no `this` to user in "during" calling. scopeUserDuring(transitionDuringAPI); // FIXME: if in future meet the case that some prop will be both modified in `during` and `state`, // consider the issue that the prop might be incorrect when return to "normal" state. } function prepareShapeOrExtraTransitionFrom( mainAttr: 'shape' | 'extra', fromEl: Element, elOption: TransitionOptionMixin, transFromProps: LooseElementProps ): void { const attrOpt: Dictionary<unknown> & TransitionOptionMixin = (elOption as any)[mainAttr]; if (!attrOpt) { return; } const elPropsInAttr = (fromEl as LooseElementProps)[mainAttr]; let transFromPropsInAttr: Dictionary<unknown>; if (elPropsInAttr) { const transition = elOption.transition; const attrTransition = attrOpt.transition; if (attrTransition) { !transFromPropsInAttr && (transFromPropsInAttr = transFromProps[mainAttr] = {}); if (isTransitionAll(attrTransition)) { extend(transFromPropsInAttr, elPropsInAttr); } else { const transitionKeys = normalizeToArray(attrTransition); for (let i = 0; i < transitionKeys.length; i++) { const key = transitionKeys[i]; const elVal = elPropsInAttr[key]; transFromPropsInAttr[key] = elVal; } } } else if (isTransitionAll(transition) || indexOf(transition, mainAttr) >= 0) { !transFromPropsInAttr && (transFromPropsInAttr = transFromProps[mainAttr] = {}); const elPropsInAttrKeys = keys(elPropsInAttr); for (let i = 0; i < elPropsInAttrKeys.length; i++) { const key = elPropsInAttrKeys[i]; const elVal = elPropsInAttr[key]; if (isNonStyleTransitionEnabled((attrOpt as any)[key], elVal)) { transFromPropsInAttr[key] = elVal; } } } } } function prepareShapeOrExtraAllPropsFinal( mainAttr: 'shape' | 'extra', elOption: TransitionElementOption, allProps: LooseElementProps ): void { const attrOpt: Dictionary<unknown> = (elOption as any)[mainAttr]; if (!attrOpt) { return; } const allPropsInAttr = allProps[mainAttr] = {} as Dictionary<unknown>; const keysInAttr = keys(attrOpt); for (let i = 0; i < keysInAttr.length; i++) { const key = keysInAttr[i]; // To avoid share one object with different element, and // to avoid user modify the object inexpectedly, have to clone. allPropsInAttr[key] = cloneValue((attrOpt as any)[key]); } } function prepareTransformTransitionFrom( el: Element, elOption: TransitionElementOption, transFromProps: ElementProps ): void { const transition = elOption.transition; const transitionKeys = isTransitionAll(transition) ? TRANSFORMABLE_PROPS : normalizeToArray(transition || []); for (let i = 0; i < transitionKeys.length; i++) { const key = transitionKeys[i]; if (key === 'style' || key === 'shape' || key === 'extra') { continue; } const elVal = (el as any)[key]; if (__DEV__) { checkTransformPropRefer(key, 'el.transition'); } // Do not clone, animator will perform that clone. (transFromProps as any)[key] = elVal; } } function prepareTransformAllPropsFinal( el: Element, elOption: TransitionElementOption, allProps: ElementProps ): void { for (let i = 0; i < LEGACY_TRANSFORM_PROPS.length; i++) { const legacyName = LEGACY_TRANSFORM_PROPS[i]; const xyName = LEGACY_TRANSFORM_PROPS_MAP[legacyName]; const legacyArr = (elOption as any)[legacyName]; if (legacyArr) { allProps[xyName[0]] = legacyArr[0]; allProps[xyName[1]] = legacyArr[1]; } } for (let i = 0; i < TRANSFORMABLE_PROPS.length; i++) { const key = TRANSFORMABLE_PROPS[i]; if (elOption[key] != null) { allProps[key] = elOption[key]; } } } function prepareStyleTransitionFrom( fromEl: Element, elOption: TransitionElementOption, styleOpt: TransitionElementOption['style'], transFromProps: LooseElementProps ): void { if (!styleOpt) { return; } const fromElStyle = (fromEl as LooseElementProps).style as LooseElementProps['style']; let transFromStyleProps: LooseElementProps['style']; if (fromElStyle) { const styleTransition = styleOpt.transition; const elTransition = elOption.transition; if (styleTransition && !isTransitionAll(styleTransition)) { const transitionKeys = normalizeToArray(styleTransition); !transFromStyleProps && (transFromStyleProps = transFromProps.style = {}); for (let i = 0; i < transitionKeys.length; i++) { const key = transitionKeys[i]; const elVal = (fromElStyle as any)[key]; // Do not clone, see `checkNonStyleTansitionRefer`. (transFromStyleProps as any)[key] = elVal; } } else if ( (fromEl as Displayable).getAnimationStyleProps && ( isTransitionAll(elTransition) || isTransitionAll(styleTransition) || indexOf(elTransition, 'style') >= 0 ) ) { const animationProps = (fromEl as Displayable).getAnimationStyleProps(); const animationStyleProps = animationProps ? animationProps.style : null; if (animationStyleProps) { !transFromStyleProps && (transFromStyleProps = transFromProps.style = {}); const styleKeys = keys(styleOpt); for (let i = 0; i < styleKeys.length; i++) { const key = styleKeys[i]; if ((animationStyleProps as Dictionary<unknown>)[key]) { const elVal = (fromElStyle as any)[key]; (transFromStyleProps as any)[key] = elVal; } } } } } } function isNonStyleTransitionEnabled(optVal: unknown, elVal: unknown): boolean { // The same as `checkNonStyleTansitionRefer`. return !isArrayLike(optVal) ? (optVal != null && isFinite(optVal as number)) : optVal !== elVal; } let checkTransformPropRefer: (key: string, usedIn: string) => void; if (__DEV__) { checkTransformPropRefer = function (key: string, usedIn: string): void { if (!hasOwn(TRANSFORM_PROPS_MAP, key)) { warn('Prop `' + key + '` is not a permitted in `' + usedIn + '`. ' + 'Only `' + keys(TRANSFORM_PROPS_MAP).join('`, `') + '` are permitted.'); } }; }