| /* |
| * 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.'); |
| } |
| }; |
| } |