| /* |
| * 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. |
| */ |
| |
| // Basic transitions in the same series when shapes are the same. |
| |
| import { |
| AnimationOptionMixin, |
| AnimationDelayCallbackParam, |
| PayloadAnimationPart, |
| AnimationOption |
| } from '../util/types'; |
| import { AnimationEasing } from 'zrender/src/animation/easing'; |
| import Element, { ElementAnimateConfig } from 'zrender/src/Element'; |
| import Model from '../model/Model'; |
| import { |
| isObject, |
| retrieve2 |
| } from 'zrender/src/core/util'; |
| import Displayable from 'zrender/src/graphic/Displayable'; |
| import Group from 'zrender/src/graphic/Group'; |
| import { makeInner } from '../util/model'; |
| |
| // Stored properties for further transition. |
| |
| export const transitionStore = makeInner<{ |
| oldStyle: Displayable['style'] |
| }, Displayable>(); |
| |
| |
| type AnimateOrSetPropsOption = { |
| dataIndex?: number; |
| cb?: () => void; |
| during?: (percent: number) => void; |
| removeOpt?: AnimationOption |
| isFrom?: boolean; |
| }; |
| |
| /** |
| * Return null if animation is disabled. |
| */ |
| export function getAnimationConfig( |
| animationType: 'init' | 'update' | 'remove', |
| animatableModel: Model<AnimationOptionMixin>, |
| dataIndex: number, |
| // Extra opts can override the option in animatable model. |
| extraOpts?: Pick<ElementAnimateConfig, 'easing' | 'duration' | 'delay'>, |
| // TODO It's only for pictorial bar now. |
| extraDelayParams?: unknown |
| ): Pick<ElementAnimateConfig, 'easing' | 'duration' | 'delay'> | null { |
| let animationPayload: PayloadAnimationPart; |
| // Check if there is global animation configuration from dataZoom/resize can override the config in option. |
| // If animation is enabled. Will use this animation config in payload. |
| // If animation is disabled. Just ignore it. |
| if (animatableModel && animatableModel.ecModel) { |
| const updatePayload = animatableModel.ecModel.getUpdatePayload(); |
| animationPayload = (updatePayload && updatePayload.animation) as PayloadAnimationPart; |
| } |
| const animationEnabled = animatableModel && animatableModel.isAnimationEnabled(); |
| |
| const isUpdate = animationType === 'update'; |
| |
| if (animationEnabled) { |
| let duration: number | Function; |
| let easing: AnimationEasing; |
| let delay: number | Function; |
| if (extraOpts) { |
| duration = retrieve2(extraOpts.duration, 200); |
| easing = retrieve2(extraOpts.easing, 'cubicOut'); |
| delay = 0; |
| } |
| else { |
| duration = animatableModel.getShallow( |
| isUpdate ? 'animationDurationUpdate' : 'animationDuration' |
| ); |
| easing = animatableModel.getShallow( |
| isUpdate ? 'animationEasingUpdate' : 'animationEasing' |
| ); |
| delay = animatableModel.getShallow( |
| isUpdate ? 'animationDelayUpdate' : 'animationDelay' |
| ); |
| } |
| // animation from payload has highest priority. |
| if (animationPayload) { |
| animationPayload.duration != null && (duration = animationPayload.duration); |
| animationPayload.easing != null && (easing = animationPayload.easing); |
| animationPayload.delay != null && (delay = animationPayload.delay); |
| } |
| if (typeof delay === 'function') { |
| delay = delay( |
| dataIndex as number, |
| extraDelayParams |
| ); |
| } |
| if (typeof duration === 'function') { |
| duration = duration(dataIndex as number); |
| } |
| const config = { |
| duration: duration as number || 0, |
| delay: delay as number, |
| easing |
| }; |
| |
| return config; |
| } |
| else { |
| return null; |
| } |
| } |
| |
| function animateOrSetProps<Props>( |
| animationType: 'init' | 'update' | 'remove', |
| el: Element<Props>, |
| props: Props, |
| animatableModel?: Model<AnimationOptionMixin> & { |
| getAnimationDelayParams?: (el: Element<Props>, dataIndex: number) => AnimationDelayCallbackParam |
| }, |
| dataIndex?: AnimateOrSetPropsOption['dataIndex'] | AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption, |
| cb?: AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption['during'], |
| during?: AnimateOrSetPropsOption['during'] |
| ) { |
| let isFrom = false; |
| let removeOpt: AnimationOption; |
| if (typeof dataIndex === 'function') { |
| during = cb; |
| cb = dataIndex; |
| dataIndex = null; |
| } |
| else if (isObject(dataIndex)) { |
| cb = dataIndex.cb; |
| during = dataIndex.during; |
| isFrom = dataIndex.isFrom; |
| removeOpt = dataIndex.removeOpt; |
| dataIndex = dataIndex.dataIndex; |
| } |
| |
| const isRemove = (animationType === 'remove'); |
| |
| if (!isRemove) { |
| // Must stop the remove animation. |
| el.stopAnimation('remove'); |
| } |
| |
| const animationConfig = getAnimationConfig( |
| animationType, |
| animatableModel, |
| dataIndex as number, |
| isRemove ? (removeOpt || {}) : null, |
| (animatableModel && animatableModel.getAnimationDelayParams) |
| ? animatableModel.getAnimationDelayParams(el, dataIndex as number) |
| : null |
| ); |
| if (animationConfig && animationConfig.duration > 0) { |
| const duration = animationConfig.duration; |
| const animationDelay = animationConfig.delay; |
| const animationEasing = animationConfig.easing; |
| |
| const animateConfig: ElementAnimateConfig = { |
| duration: duration as number, |
| delay: animationDelay as number || 0, |
| easing: animationEasing, |
| done: cb, |
| force: !!cb || !!during, |
| // Set to final state in update/init animation. |
| // So the post processing based on the path shape can be done correctly. |
| setToFinal: !isRemove, |
| scope: animationType, |
| during: during |
| }; |
| |
| isFrom |
| ? el.animateFrom(props, animateConfig) |
| : el.animateTo(props, animateConfig); |
| } |
| else { |
| el.stopAnimation(); |
| // If `isFrom`, the props is the "from" props. |
| !isFrom && el.attr(props); |
| // Call during at least once. |
| during && during(1); |
| cb && (cb as AnimateOrSetPropsOption['cb'])(); |
| } |
| } |
| |
| |
| |
| /** |
| * Update graphic element properties with or without animation according to the |
| * configuration in series. |
| * |
| * Caution: this method will stop previous animation. |
| * So do not use this method to one element twice before |
| * animation starts, unless you know what you are doing. |
| * @example |
| * graphic.updateProps(el, { |
| * position: [100, 100] |
| * }, seriesModel, dataIndex, function () { console.log('Animation done!'); }); |
| * // Or |
| * graphic.updateProps(el, { |
| * position: [100, 100] |
| * }, seriesModel, function () { console.log('Animation done!'); }); |
| */ |
| function updateProps<Props>( |
| el: Element<Props>, |
| props: Props, |
| // TODO: TYPE AnimatableModel |
| animatableModel?: Model<AnimationOptionMixin>, |
| dataIndex?: AnimateOrSetPropsOption['dataIndex'] | AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption, |
| cb?: AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption['during'], |
| during?: AnimateOrSetPropsOption['during'] |
| ) { |
| animateOrSetProps('update', el, props, animatableModel, dataIndex, cb, during); |
| } |
| |
| export {updateProps}; |
| |
| /** |
| * Init graphic element properties with or without animation according to the |
| * configuration in series. |
| * |
| * Caution: this method will stop previous animation. |
| * So do not use this method to one element twice before |
| * animation starts, unless you know what you are doing. |
| */ |
| export function initProps<Props>( |
| el: Element<Props>, |
| props: Props, |
| animatableModel?: Model<AnimationOptionMixin>, |
| dataIndex?: AnimateOrSetPropsOption['dataIndex'] | AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption, |
| cb?: AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption['during'], |
| during?: AnimateOrSetPropsOption['during'] |
| ) { |
| animateOrSetProps('init', el, props, animatableModel, dataIndex, cb, during); |
| } |
| |
| /** |
| * If element is removed. |
| * It can determine if element is having remove animation. |
| */ |
| export function isElementRemoved(el: Element) { |
| if (!el.__zr) { |
| return true; |
| } |
| for (let i = 0; i < el.animators.length; i++) { |
| const animator = el.animators[i]; |
| if (animator.scope === 'remove') { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Remove graphic element |
| */ |
| export function removeElement<Props>( |
| el: Element<Props>, |
| props: Props, |
| animatableModel?: Model<AnimationOptionMixin>, |
| dataIndex?: AnimateOrSetPropsOption['dataIndex'] | AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption, |
| cb?: AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption['during'], |
| during?: AnimateOrSetPropsOption['during'] |
| ) { |
| // Don't do remove animation twice. |
| if (isElementRemoved(el)) { |
| return; |
| } |
| |
| animateOrSetProps('remove', el, props, animatableModel, dataIndex, cb, during); |
| } |
| |
| function fadeOutDisplayable( |
| el: Displayable, |
| animatableModel?: Model<AnimationOptionMixin>, |
| dataIndex?: number, |
| done?: AnimateOrSetPropsOption['cb'] |
| ) { |
| el.removeTextContent(); |
| el.removeTextGuideLine(); |
| removeElement(el, { |
| style: { |
| opacity: 0 |
| } |
| }, animatableModel, dataIndex, done); |
| } |
| |
| export function removeElementWithFadeOut( |
| el: Element, |
| animatableModel?: Model<AnimationOptionMixin>, |
| dataIndex?: number |
| ) { |
| function doRemove() { |
| el.parent && el.parent.remove(el); |
| } |
| // Hide label and labelLine first |
| // TODO Also use fade out animation? |
| if (!el.isGroup) { |
| fadeOutDisplayable(el as Displayable, animatableModel, dataIndex, doRemove); |
| } |
| else { |
| (el as Group).traverse(function (disp: Displayable) { |
| if (!disp.isGroup) { |
| // Can invoke doRemove multiple times. |
| fadeOutDisplayable(disp as Displayable, animatableModel, dataIndex, doRemove); |
| } |
| }); |
| } |
| } |
| |
| /** |
| * Save old style for style transition in universalTransition module. |
| * It's used when element will be reused in each render. |
| * For chart like map, heatmap, which will always create new element. |
| * We don't need to save this because universalTransition can get old style from the old element |
| */ |
| export function saveOldStyle(el: Displayable) { |
| transitionStore(el).oldStyle = el.style; |
| } |
| |
| export function getOldStyle(el: Displayable) { |
| return transitionStore(el).oldStyle; |
| } |