blob: bc31f39c4834d2772e61bc1b198e167c9cb4d8e1 [file] [log] [blame]
/*
* 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;
}