From 3d7bf245bfabf2ec2155378b839a7b2573b5bebd Mon Sep 17 00:00:00 2001 From: pissang Date: Thu, 18 Nov 2021 12:12:47 +0800 Subject: [PATCH 01/35] refact(graphic): seperate view and model --- src/component/graphic/GraphicModel.ts | 404 +++++++++++++ src/component/graphic/GraphicView.ts | 352 ++++++++++++ src/component/graphic/install.ts | 777 +------------------------- src/export/option.ts | 2 +- 4 files changed, 780 insertions(+), 755 deletions(-) create mode 100644 src/component/graphic/GraphicModel.ts create mode 100644 src/component/graphic/GraphicView.ts diff --git a/src/component/graphic/GraphicModel.ts b/src/component/graphic/GraphicModel.ts new file mode 100644 index 0000000000..e0d8f884ea --- /dev/null +++ b/src/component/graphic/GraphicModel.ts @@ -0,0 +1,404 @@ +/* +* 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. +*/ + +import * as zrUtil from 'zrender/src/core/util'; +import * as modelUtil from '../../util/model'; +import { + ComponentOption, + BoxLayoutOptionMixin, + Dictionary, + ZRStyleProps, + OptionId, + OptionPreprocessor, + CommonTooltipOption +} from '../../util/types'; +import ComponentModel from '../../model/Component'; +import Element, { ElementTextConfig } from 'zrender/src/Element'; +import Displayable from 'zrender/src/graphic/Displayable'; +import { PathProps } from 'zrender/src/graphic/Path'; +import { ImageStyleProps } from 'zrender/src/graphic/Image'; +import GlobalModel from '../../model/Global'; +import { TextStyleProps } from 'zrender/src/graphic/Text'; +import { copyLayoutParams, mergeLayoutParam } from '../../util/layout'; + +interface GraphicComponentBaseElementOption extends + Partial>, + /** + * left/right/top/bottom: (like 12, '22%', 'center', default undefined) + * If left/rigth is set, shape.x/shape.cx/position will not be used. + * If top/bottom is set, shape.y/shape.cy/position will not be used. + * This mechanism is useful when you want to position a group/element + * against the right side or the center of this container. + */ + Partial> { + + /** + * element type, mandatory. + * Only can be omit if call setOption not at the first time and perform merge. + */ + type?: string; + + id?: OptionId; + name?: string; + + // Only internal usage. Use specified value does NOT make sense. + parentId?: OptionId; + parentOption?: GraphicComponentElementOption; + children?: GraphicComponentElementOption[]; + hv?: [boolean, boolean]; + + /** + * bounding: (enum: 'all' (default) | 'raw') + * Specify how to calculate boundingRect when locating. + * 'all': Get uioned and transformed boundingRect + * from both itself and its descendants. + * This mode simplies confining a group of elements in the bounding + * of their ancester container (e.g., using 'right: 0'). + * 'raw': Only use the boundingRect of itself and before transformed. + * This mode is similar to css behavior, which is useful when you + * want an element to be able to overflow its container. (Consider + * a rotated circle needs to be located in a corner.) + */ + bounding?: 'raw' | 'all'; + + /** + * info: custom info. enables user to mount some info on elements and use them + * in event handlers. Update them only when user specified, otherwise, remain. + */ + info?: GraphicExtraElementInfo; + + textContent?: GraphicComponentTextOption; + textConfig?: ElementTextConfig; + + $action?: 'merge' | 'replace' | 'remove'; + + tooltip?: CommonTooltipOption; +}; + + +export type TransformProp = 'x' | 'y' | 'scaleX' | 'scaleY' | 'originX' | 'originY' | 'skewX' | 'skewY' | 'rotation'; + +export interface GraphicComponentDisplayableOption extends + GraphicComponentBaseElementOption, Partial> { + + style?: ZRStyleProps; +} +// TODO: states? +// interface GraphicComponentDisplayableOptionOnState extends Partial> { +// style?: ZRStyleProps; +// } +export interface GraphicComponentGroupOption extends GraphicComponentBaseElementOption { + type?: 'group'; + + /** + * width/height: (can only be pixel value, default 0) + * Only be used to specify contianer(group) size, if needed. And + * can not be percentage value (like '33%'). See the reason in the + * layout algorithm below. + */ + width?: number; + height?: number; + + // TODO: Can only set focus, blur on the root element. + // children: Omit[]; + children: GraphicComponentElementOption[]; +} +export interface GraphicComponentZRPathOption extends GraphicComponentDisplayableOption { + shape?: PathProps['shape']; +} +export interface GraphicComponentImageOption extends GraphicComponentDisplayableOption { + type?: 'image'; + style?: ImageStyleProps; +} +// TODO: states? +// interface GraphicComponentImageOptionOnState extends GraphicComponentDisplayableOptionOnState { +// style?: ImageStyleProps; +// } +interface GraphicComponentTextOption + extends Omit { + type?: 'text'; + style?: TextStyleProps; +} +export type GraphicComponentElementOption = + GraphicComponentGroupOption | + GraphicComponentZRPathOption | + GraphicComponentImageOption | + GraphicComponentTextOption; +// type GraphicComponentElementOptionOnState = +// GraphicComponentDisplayableOptionOnState +// | GraphicComponentImageOptionOnState; +type GraphicExtraElementInfo = Dictionary; +export type ElementMap = zrUtil.HashMap; + + +export type GraphicComponentLooseOption = (GraphicComponentOption | GraphicComponentElementOption) & { + mainType?: 'graphic'; +}; + +export interface GraphicComponentOption extends ComponentOption { + // Note: elements is always behind its ancestors in this elements array. + elements?: GraphicComponentElementOption[]; +} +; + +export function setKeyInfoToNewElOption( + resultItem: ReturnType[number], + newElOption: GraphicComponentElementOption +): void { + const existElOption = resultItem.existing as GraphicComponentElementOption; + + // Set id and type after id assigned. + newElOption.id = resultItem.keyInfo.id; + !newElOption.type && existElOption && (newElOption.type = existElOption.type); + + // Set parent id if not specified + if (newElOption.parentId == null) { + const newElParentOption = newElOption.parentOption; + if (newElParentOption) { + newElOption.parentId = newElParentOption.id; + } + else if (existElOption) { + newElOption.parentId = existElOption.parentId; + } + } + + // Clear + newElOption.parentOption = null; +} + +function isSetLoc( + obj: GraphicComponentElementOption, + props: ('left' | 'right' | 'top' | 'bottom')[] +): boolean { + let isSet; + zrUtil.each(props, function (prop) { + obj[prop] != null && obj[prop] !== 'auto' && (isSet = true); + }); + return isSet; +} +function mergeNewElOptionToExist( + existList: GraphicComponentElementOption[], + index: number, + newElOption: GraphicComponentElementOption +): void { + // Update existing options, for `getOption` feature. + const newElOptCopy = zrUtil.extend({}, newElOption); + const existElOption = existList[index]; + + const $action = newElOption.$action || 'merge'; + if ($action === 'merge') { + if (existElOption) { + + if (__DEV__) { + const newType = newElOption.type; + zrUtil.assert( + !newType || existElOption.type === newType, + 'Please set $action: "replace" to change `type`' + ); + } + + // We can ensure that newElOptCopy and existElOption are not + // the same object, so `merge` will not change newElOptCopy. + zrUtil.merge(existElOption, newElOptCopy, true); + // Rigid body, use ignoreSize. + mergeLayoutParam(existElOption, newElOptCopy, { ignoreSize: true }); + // Will be used in render. + copyLayoutParams(newElOption, existElOption); + } + else { + existList[index] = newElOptCopy; + } + } + else if ($action === 'replace') { + existList[index] = newElOptCopy; + } + else if ($action === 'remove') { + // null will be cleaned later. + existElOption && (existList[index] = null); + } +} + +function setLayoutInfoToExist( + existItem: GraphicComponentElementOption, + newElOption: GraphicComponentElementOption +) { + if (!existItem) { + return; + } + existItem.hv = newElOption.hv = [ + // Rigid body, dont care `width`. + isSetLoc(newElOption, ['left', 'right']), + // Rigid body, dont care `height`. + isSetLoc(newElOption, ['top', 'bottom']) + ]; + // Give default group size. Otherwise layout error may occur. + if (existItem.type === 'group') { + const existingGroupOpt = existItem as GraphicComponentGroupOption; + const newGroupOpt = newElOption as GraphicComponentGroupOption; + existingGroupOpt.width == null && (existingGroupOpt.width = newGroupOpt.width = 0); + existingGroupOpt.height == null && (existingGroupOpt.height = newGroupOpt.height = 0); + } +} + +export class GraphicComponentModel extends ComponentModel { + + static type = 'graphic'; + type = GraphicComponentModel.type; + + preventAutoZ = true; + + static defaultOption: GraphicComponentOption = { + elements: [] + // parentId: null + }; + + /** + * Save el options for the sake of the performance (only update modified graphics). + * The order is the same as those in option. (ancesters -> descendants) + */ + private _elOptionsToUpdate: GraphicComponentElementOption[]; + + mergeOption(option: GraphicComponentOption, ecModel: GlobalModel): void { + // Prevent default merge to elements + const elements = this.option.elements; + this.option.elements = null; + + super.mergeOption(option, ecModel); + + this.option.elements = elements; + } + + optionUpdated(newOption: GraphicComponentOption, isInit: boolean): void { + const thisOption = this.option; + const newList = (isInit ? thisOption : newOption).elements; + const existList = thisOption.elements = isInit ? [] : thisOption.elements; + + const flattenedList = [] as GraphicComponentElementOption[]; + this._flatten(newList, flattenedList, null); + + const mappingResult = modelUtil.mappingToExists(existList, flattenedList, 'normalMerge'); + + // Clear elOptionsToUpdate + const elOptionsToUpdate = this._elOptionsToUpdate = [] as GraphicComponentElementOption[]; + + zrUtil.each(mappingResult, function (resultItem, index) { + const newElOption = resultItem.newOption as GraphicComponentElementOption; + + if (__DEV__) { + zrUtil.assert( + zrUtil.isObject(newElOption) || resultItem.existing, + 'Empty graphic option definition' + ); + } + + if (!newElOption) { + return; + } + + elOptionsToUpdate.push(newElOption); + + setKeyInfoToNewElOption(resultItem, newElOption); + + mergeNewElOptionToExist(existList, index, newElOption); + + setLayoutInfoToExist(existList[index], newElOption); + + }, this); + + // Clean + thisOption.elements = zrUtil.filter(existList, (item) => { + // $action should be volatile, otherwise option gotten from + // `getOption` will contain unexpected $action. + item && delete item.$action; + return item != null; + }); + } + + /** + * Convert + * [{ + * type: 'group', + * id: 'xx', + * children: [{type: 'circle'}, {type: 'polygon'}] + * }] + * to + * [ + * {type: 'group', id: 'xx'}, + * {type: 'circle', parentId: 'xx'}, + * {type: 'polygon', parentId: 'xx'} + * ] + */ + private _flatten( + optionList: GraphicComponentElementOption[], + result: GraphicComponentElementOption[], + parentOption: GraphicComponentElementOption + ): void { + zrUtil.each(optionList, function (option) { + if (!option) { + return; + } + + if (parentOption) { + option.parentOption = parentOption; + } + + result.push(option); + + const children = option.children; + if (option.type === 'group' && children) { + this._flatten(children, result, option); + } + // Deleting for JSON output, and for not affecting group creation. + delete option.children; + }, this); + } + + // FIXME + // Pass to view using payload? setOption has a payload? + useElOptionsToUpdate(): GraphicComponentElementOption[] { + const els = this._elOptionsToUpdate; + // Clear to avoid render duplicately when zooming. + this._elOptionsToUpdate = null; + return els; + } +} diff --git a/src/component/graphic/GraphicView.ts b/src/component/graphic/GraphicView.ts new file mode 100644 index 0000000000..8d3f5c2a4b --- /dev/null +++ b/src/component/graphic/GraphicView.ts @@ -0,0 +1,352 @@ +/* +* 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. +*/ + +import * as zrUtil from 'zrender/src/core/util'; +import * as modelUtil from '../../util/model'; +import * as graphicUtil from '../../util/graphic'; +import * as layoutUtil from '../../util/layout'; +import { parsePercent } from '../../util/number'; +import Element from 'zrender/src/Element'; +import GlobalModel from '../../model/Global'; +import ComponentView from '../../view/Component'; +import ExtensionAPI from '../../core/ExtensionAPI'; +import { getECData } from '../../util/innerStore'; +import { TextStyleProps } from 'zrender/src/graphic/Text'; +import { isEC4CompatibleStyle, convertFromEC4CompatibleStyle } from '../../util/styleCompat'; +import { + ElementMap, + GraphicComponentModel, + GraphicComponentDisplayableOption, + GraphicComponentZRPathOption, + GraphicComponentGroupOption, + GraphicComponentElementOption +} from './GraphicModel'; + +const _nonShapeGraphicElements = { + // Reserved but not supported in graphic component. + path: null as unknown, + compoundPath: null as unknown, + + // Supported in graphic component. + group: graphicUtil.Group, + image: graphicUtil.Image, + text: graphicUtil.Text +} as const; +type NonShapeGraphicElementType = keyof typeof _nonShapeGraphicElements; + +export const inner = modelUtil.makeInner<{ + widthOption: number; + heightOption: number; + width: number; + height: number; + id: string; +}, Element>(); +// ------------------------ +// View +// ------------------------ +export class GraphicComponentView extends ComponentView { + + static type = 'graphic'; + type = GraphicComponentView.type; + + private _elMap: ElementMap; + private _lastGraphicModel: GraphicComponentModel; + + init() { + this._elMap = zrUtil.createHashMap(); + } + + render(graphicModel: GraphicComponentModel, ecModel: GlobalModel, api: ExtensionAPI): void { + // Having leveraged between use cases and algorithm complexity, a very + // simple layout mechanism is used: + // The size(width/height) can be determined by itself or its parent (not + // implemented yet), but can not by its children. (Top-down travel) + // The location(x/y) can be determined by the bounding rect of itself + // (can including its descendants or not) and the size of its parent. + // (Bottom-up travel) + + // When `chart.clear()` or `chart.setOption({...}, true)` with the same id, + // view will be reused. + if (graphicModel !== this._lastGraphicModel) { + this._clear(); + } + this._lastGraphicModel = graphicModel; + + this._updateElements(graphicModel); + this._relocate(graphicModel, api); + } + + /** + * Update graphic elements. + */ + private _updateElements(graphicModel: GraphicComponentModel): void { + const elOptionsToUpdate = graphicModel.useElOptionsToUpdate(); + + if (!elOptionsToUpdate) { + return; + } + + const elMap = this._elMap; + const rootGroup = this.group; + + // Top-down tranverse to assign graphic settings to each elements. + zrUtil.each(elOptionsToUpdate, function (elOption) { + const id = modelUtil.convertOptionIdName(elOption.id, null); + const elExisting = id != null ? elMap.get(id) : null; + const parentId = modelUtil.convertOptionIdName(elOption.parentId, null); + const targetElParent = (parentId != null ? elMap.get(parentId) : rootGroup) as graphicUtil.Group; + + const elType = elOption.type; + const elOptionStyle = (elOption as GraphicComponentDisplayableOption).style; + if (elType === 'text' && elOptionStyle) { + // In top/bottom mode, textVerticalAlign should not be used, which cause + // inaccurately locating. + if (elOption.hv && elOption.hv[1]) { + (elOptionStyle as any).textVerticalAlign = + (elOptionStyle as any).textBaseline = + (elOptionStyle as TextStyleProps).verticalAlign = + (elOptionStyle as TextStyleProps).align = null; + } + } + + let textContentOption = (elOption as GraphicComponentZRPathOption).textContent; + let textConfig = (elOption as GraphicComponentZRPathOption).textConfig; + if (elOptionStyle + && isEC4CompatibleStyle(elOptionStyle, elType, !!textConfig, !!textContentOption)) { + const convertResult = + convertFromEC4CompatibleStyle(elOptionStyle, elType, true) as GraphicComponentZRPathOption; + if (!textConfig && convertResult.textConfig) { + textConfig = (elOption as GraphicComponentZRPathOption).textConfig = convertResult.textConfig; + } + if (!textContentOption && convertResult.textContent) { + textContentOption = convertResult.textContent; + } + } + + // Remove unnecessary props to avoid potential problems. + const elOptionCleaned = getCleanedElOption(elOption); + + // For simple, do not support parent change, otherwise reorder is needed. + if (__DEV__) { + elExisting && zrUtil.assert( + targetElParent === elExisting.parent, + 'Changing parent is not supported.' + ); + } + + const $action = elOption.$action || 'merge'; + if ($action === 'merge') { + elExisting + ? elExisting.attr(elOptionCleaned) + : createEl(id, targetElParent, elOptionCleaned, elMap); + } + else if ($action === 'replace') { + removeEl(elExisting, elMap); + createEl(id, targetElParent, elOptionCleaned, elMap); + } + else if ($action === 'remove') { + removeEl(elExisting, elMap); + } + + const el = elMap.get(id); + + if (el && textContentOption) { + if ($action === 'merge') { + const textContentExisting = el.getTextContent(); + textContentExisting + ? textContentExisting.attr(textContentOption) + : el.setTextContent(new graphicUtil.Text(textContentOption)); + } + else if ($action === 'replace') { + el.setTextContent(new graphicUtil.Text(textContentOption)); + } + } + + if (el) { + const elInner = inner(el); + elInner.widthOption = (elOption as GraphicComponentGroupOption).width; + elInner.heightOption = (elOption as GraphicComponentGroupOption).height; + setEventData(el, graphicModel, elOption); + + graphicUtil.setTooltipConfig({ + el: el, + componentModel: graphicModel, + itemName: el.name, + itemTooltipOption: elOption.tooltip + }); + } + }); + } + + /** + * Locate graphic elements. + */ + private _relocate(graphicModel: GraphicComponentModel, api: ExtensionAPI): void { + const elOptions = graphicModel.option.elements; + const rootGroup = this.group; + const elMap = this._elMap; + const apiWidth = api.getWidth(); + const apiHeight = api.getHeight(); + + // Top-down to calculate percentage width/height of group + for (let i = 0; i < elOptions.length; i++) { + const elOption = elOptions[i]; + const id = modelUtil.convertOptionIdName(elOption.id, null); + const el = id != null ? elMap.get(id) : null; + + if (!el || !el.isGroup) { + continue; + } + const parentEl = el.parent; + const isParentRoot = parentEl === rootGroup; + // Like 'position:absolut' in css, default 0. + const elInner = inner(el); + const parentElInner = inner(parentEl); + elInner.width = parsePercent( + elInner.widthOption, + isParentRoot ? apiWidth : parentElInner.width + ) || 0; + elInner.height = parsePercent( + elInner.heightOption, + isParentRoot ? apiHeight : parentElInner.height + ) || 0; + } + + // Bottom-up tranvese all elements (consider ec resize) to locate elements. + for (let i = elOptions.length - 1; i >= 0; i--) { + const elOption = elOptions[i]; + const id = modelUtil.convertOptionIdName(elOption.id, null); + const el = id != null ? elMap.get(id) : null; + + if (!el) { + continue; + } + + const parentEl = el.parent; + const parentElInner = inner(parentEl); + const containerInfo = parentEl === rootGroup + ? { + width: apiWidth, + height: apiHeight + } + : { + width: parentElInner.width, + height: parentElInner.height + }; + + // PENDING + // Currently, when `bounding: 'all'`, the union bounding rect of the group + // does not include the rect of [0, 0, group.width, group.height], which + // is probably weird for users. Should we make a break change for it? + layoutUtil.positionElement( + el, elOption, containerInfo, null, + { hv: elOption.hv, boundingMode: elOption.bounding } + ); + } + } + + /** + * Clear all elements. + */ + private _clear(): void { + const elMap = this._elMap; + elMap.each(function (el) { + removeEl(el, elMap); + }); + this._elMap = zrUtil.createHashMap(); + } + + dispose(): void { + this._clear(); + } +} +function createEl( + id: string, + targetElParent: graphicUtil.Group, + elOption: GraphicComponentElementOption, + elMap: ElementMap +): void { + const graphicType = elOption.type; + + if (__DEV__) { + zrUtil.assert(graphicType, 'graphic type MUST be set'); + } + + const Clz = ( + zrUtil.hasOwn(_nonShapeGraphicElements, graphicType) + // Those graphic elements are not shapes. They should not be + // overwritten by users, so do them first. + ? _nonShapeGraphicElements[graphicType as NonShapeGraphicElementType] + : graphicUtil.getShapeClass(graphicType) + ) as { new(opt: GraphicComponentElementOption): Element; }; + + if (__DEV__) { + zrUtil.assert(Clz, 'graphic type can not be found'); + } + + const el = new Clz(elOption); + targetElParent.add(el); + elMap.set(id, el); + inner(el).id = id; +} +function removeEl(elExisting: Element, elMap: ElementMap): void { + const existElParent = elExisting && elExisting.parent; + if (existElParent) { + elExisting.type === 'group' && elExisting.traverse(function (el) { + removeEl(el, elMap); + }); + elMap.removeKey(inner(elExisting).id); + existElParent.remove(elExisting); + } +} +// Remove unnecessary props to avoid potential problems. +function getCleanedElOption( + elOption: GraphicComponentElementOption +): Omit { + elOption = zrUtil.extend({}, elOption); + zrUtil.each( + ['id', 'parentId', '$action', 'hv', 'bounding', 'textContent'].concat(layoutUtil.LOCATION_PARAMS), + function (name) { + delete (elOption as any)[name]; + } + ); + return elOption; +} + +function setEventData( + el: Element, + graphicModel: GraphicComponentModel, + elOption: GraphicComponentElementOption +): void { + let eventData = getECData(el).eventData; + // Simple optimize for large amount of elements that no need event. + if (!el.silent && !el.ignore && !eventData) { + eventData = getECData(el).eventData = { + componentType: 'graphic', + componentIndex: graphicModel.componentIndex, + name: el.name + }; + } + + // `elOption.info` enables user to mount some info on + // elements and use them in event handlers. + if (eventData) { + eventData.info = elOption.info; + } +} diff --git a/src/component/graphic/install.ts b/src/component/graphic/install.ts index bfa2a82cc6..4251932ecc 100644 --- a/src/component/graphic/install.ts +++ b/src/component/graphic/install.ts @@ -18,769 +18,38 @@ */ -import * as zrUtil from 'zrender/src/core/util'; -import * as modelUtil from '../../util/model'; -import * as graphicUtil from '../../util/graphic'; -import * as layoutUtil from '../../util/layout'; -import {parsePercent} from '../../util/number'; -import { - ComponentOption, - BoxLayoutOptionMixin, - Dictionary, - ZRStyleProps, - OptionId, - OptionPreprocessor, - CommonTooltipOption -} from '../../util/types'; -import ComponentModel from '../../model/Component'; -import Element, { ElementTextConfig } from 'zrender/src/Element'; -import Displayable from 'zrender/src/graphic/Displayable'; -import { PathProps } from 'zrender/src/graphic/Path'; -import { ImageStyleProps } from 'zrender/src/graphic/Image'; -import GlobalModel from '../../model/Global'; -import ComponentView from '../../view/Component'; -import ExtensionAPI from '../../core/ExtensionAPI'; -import { getECData } from '../../util/innerStore'; -import { TextStyleProps } from 'zrender/src/graphic/Text'; -import { isEC4CompatibleStyle, convertFromEC4CompatibleStyle } from '../../util/styleCompat'; +import { isArray } from 'zrender/src/core/util'; import { EChartsExtensionInstallRegisters } from '../../extension'; +import { GraphicComponentModel, GraphicComponentOption } from './GraphicModel'; +import { GraphicComponentView } from './GraphicView'; -type TransformProp = 'x' | 'y' | 'scaleX' | 'scaleY' | 'originX' | 'originY' | 'skewX' | 'skewY' | 'rotation'; - -interface GraphicComponentBaseElementOption extends - Partial>, - /** - * left/right/top/bottom: (like 12, '22%', 'center', default undefined) - * If left/rigth is set, shape.x/shape.cx/position will not be used. - * If top/bottom is set, shape.y/shape.cy/position will not be used. - * This mechanism is useful when you want to position a group/element - * against the right side or the center of this container. - */ - Partial> { - - /** - * element type, mandatory. - * Only can be omit if call setOption not at the first time and perform merge. - */ - type?: string; - - id?: OptionId; - name?: string; - - // Only internal usage. Use specified value does NOT make sense. - parentId?: OptionId; - parentOption?: GraphicComponentElementOption; - children?: GraphicComponentElementOption[]; - hv?: [boolean, boolean]; - - /** - * bounding: (enum: 'all' (default) | 'raw') - * Specify how to calculate boundingRect when locating. - * 'all': Get uioned and transformed boundingRect - * from both itself and its descendants. - * This mode simplies confining a group of elements in the bounding - * of their ancester container (e.g., using 'right: 0'). - * 'raw': Only use the boundingRect of itself and before transformed. - * This mode is similar to css behavior, which is useful when you - * want an element to be able to overflow its container. (Consider - * a rotated circle needs to be located in a corner.) - */ - bounding?: 'raw' | 'all'; - - /** - * info: custom info. enables user to mount some info on elements and use them - * in event handlers. Update them only when user specified, otherwise, remain. - */ - info?: GraphicExtraElementInfo; - - textContent?: GraphicComponentTextOption; - textConfig?: ElementTextConfig; - - $action?: 'merge' | 'replace' | 'remove'; - - tooltip?: CommonTooltipOption; -}; - -interface GraphicComponentDisplayableOption extends - GraphicComponentBaseElementOption, - Partial> { - - style?: ZRStyleProps; - - // TODO: states? - // emphasis?: GraphicComponentDisplayableOptionOnState; - // blur?: GraphicComponentDisplayableOptionOnState; - // select?: GraphicComponentDisplayableOptionOnState; -} -// TODO: states? -// interface GraphicComponentDisplayableOptionOnState extends Partial> { -// style?: ZRStyleProps; -// } -interface GraphicComponentGroupOption extends GraphicComponentBaseElementOption { - type?: 'group'; - - /** - * width/height: (can only be pixel value, default 0) - * Only be used to specify contianer(group) size, if needed. And - * can not be percentage value (like '33%'). See the reason in the - * layout algorithm below. - */ - width?: number; - height?: number; - - // TODO: Can only set focus, blur on the root element. - // children: Omit[]; - children: GraphicComponentElementOption[]; -} -export interface GraphicComponentZRPathOption extends GraphicComponentDisplayableOption { - shape?: PathProps['shape']; -} -export interface GraphicComponentImageOption extends GraphicComponentDisplayableOption { - type?: 'image'; - style?: ImageStyleProps; - // TODO: states? - // emphasis?: GraphicComponentImageOptionOnState; - // blur?: GraphicComponentImageOptionOnState; - // select?: GraphicComponentImageOptionOnState; -} -// TODO: states? -// interface GraphicComponentImageOptionOnState extends GraphicComponentDisplayableOptionOnState { -// style?: ImageStyleProps; -// } -interface GraphicComponentTextOption - extends Omit { - type?: 'text'; - style?: TextStyleProps; -} -type GraphicComponentElementOption = - GraphicComponentGroupOption - | GraphicComponentZRPathOption - | GraphicComponentImageOption - | GraphicComponentTextOption; -// type GraphicComponentElementOptionOnState = -// GraphicComponentDisplayableOptionOnState -// | GraphicComponentImageOptionOnState; - -type GraphicExtraElementInfo = Dictionary; - -type ElementMap = zrUtil.HashMap; - -const inner = modelUtil.makeInner<{ - widthOption: number; - heightOption: number; - width: number; - height: number; - id: string; -}, Element>(); - - -const _nonShapeGraphicElements = { - - // Reserved but not supported in graphic component. - path: null as unknown, - compoundPath: null as unknown, - - // Supported in graphic component. - group: graphicUtil.Group, - image: graphicUtil.Image, - text: graphicUtil.Text -} as const; -type NonShapeGraphicElementType = keyof typeof _nonShapeGraphicElements; - -// ------------------------ -// Preprocessor -// ------------------------ - -const preprocessor: OptionPreprocessor = function (option) { - const graphicOption = option.graphic as GraphicComponentOption | GraphicComponentOption[]; - - // Convert - // {graphic: [{left: 10, type: 'circle'}, ...]} - // or - // {graphic: {left: 10, type: 'circle'}} - // to - // {graphic: [{elements: [{left: 10, type: 'circle'}, ...]}]} - if (zrUtil.isArray(graphicOption)) { - if (!graphicOption[0] || !graphicOption[0].elements) { - option.graphic = [{elements: graphicOption}]; - } - else { - // Only one graphic instance can be instantiated. (We dont - // want that too many views are created in echarts._viewMap) - option.graphic = [(option.graphic as any)[0]]; - } - } - else if (graphicOption && !graphicOption.elements) { - option.graphic = [{elements: [graphicOption]}]; - } -}; - -// ------------------------ -// Model -// ------------------------ - -export type GraphicComponentLooseOption = (GraphicComponentOption | GraphicComponentElementOption) & { - mainType?: 'graphic'; -}; - -export interface GraphicComponentOption extends ComponentOption { - // Note: elements is always behind its ancestors in this elements array. - elements?: GraphicComponentElementOption[]; - // parentId: string; -}; - - -class GraphicComponentModel extends ComponentModel { - - static type = 'graphic'; - type = GraphicComponentModel.type; - - preventAutoZ = true; - - static defaultOption: GraphicComponentOption = { - elements: [] - // parentId: null - }; - - /** - * Save el options for the sake of the performance (only update modified graphics). - * The order is the same as those in option. (ancesters -> descendants) - */ - private _elOptionsToUpdate: GraphicComponentElementOption[]; - - mergeOption(option: GraphicComponentOption, ecModel: GlobalModel): void { - // Prevent default merge to elements - const elements = this.option.elements; - this.option.elements = null; - - super.mergeOption(option, ecModel); - - this.option.elements = elements; - } - - optionUpdated(newOption: GraphicComponentOption, isInit: boolean): void { - const thisOption = this.option; - const newList = (isInit ? thisOption : newOption).elements; - const existList = thisOption.elements = isInit ? [] : thisOption.elements; - - const flattenedList = [] as GraphicComponentElementOption[]; - this._flatten(newList, flattenedList, null); - - const mappingResult = modelUtil.mappingToExists(existList, flattenedList, 'normalMerge'); - - // Clear elOptionsToUpdate - const elOptionsToUpdate = this._elOptionsToUpdate = [] as GraphicComponentElementOption[]; - - zrUtil.each(mappingResult, function (resultItem, index) { - const newElOption = resultItem.newOption as GraphicComponentElementOption; - - if (__DEV__) { - zrUtil.assert( - zrUtil.isObject(newElOption) || resultItem.existing, - 'Empty graphic option definition' - ); - } - - if (!newElOption) { - return; - } - - elOptionsToUpdate.push(newElOption); - - setKeyInfoToNewElOption(resultItem, newElOption); - - mergeNewElOptionToExist(existList, index, newElOption); - - setLayoutInfoToExist(existList[index], newElOption); - - }, this); - - // Clean - thisOption.elements = zrUtil.filter(existList, (item) => { - // $action should be volatile, otherwise option gotten from - // `getOption` will contain unexpected $action. - item && delete item.$action; - return item != null; - }); - } - - /** - * Convert - * [{ - * type: 'group', - * id: 'xx', - * children: [{type: 'circle'}, {type: 'polygon'}] - * }] - * to - * [ - * {type: 'group', id: 'xx'}, - * {type: 'circle', parentId: 'xx'}, - * {type: 'polygon', parentId: 'xx'} - * ] - */ - private _flatten( - optionList: GraphicComponentElementOption[], - result: GraphicComponentElementOption[], - parentOption: GraphicComponentElementOption - ): void { - zrUtil.each(optionList, function (option) { - if (!option) { - return; - } - - if (parentOption) { - option.parentOption = parentOption; - } - - result.push(option); - - const children = option.children; - if (option.type === 'group' && children) { - this._flatten(children, result, option); - } - // Deleting for JSON output, and for not affecting group creation. - delete option.children; - }, this); - } - - // FIXME - // Pass to view using payload? setOption has a payload? - useElOptionsToUpdate(): GraphicComponentElementOption[] { - const els = this._elOptionsToUpdate; - // Clear to avoid render duplicately when zooming. - this._elOptionsToUpdate = null; - return els; - } -} - -// ------------------------ -// View -// ------------------------ - -class GraphicComponentView extends ComponentView { - - static type = 'graphic'; - type = GraphicComponentView.type; - - private _elMap: ElementMap; - private _lastGraphicModel: GraphicComponentModel; - - init() { - this._elMap = zrUtil.createHashMap(); - } - - render(graphicModel: GraphicComponentModel, ecModel: GlobalModel, api: ExtensionAPI): void { - - // Having leveraged between use cases and algorithm complexity, a very - // simple layout mechanism is used: - // The size(width/height) can be determined by itself or its parent (not - // implemented yet), but can not by its children. (Top-down travel) - // The location(x/y) can be determined by the bounding rect of itself - // (can including its descendants or not) and the size of its parent. - // (Bottom-up travel) - - // When `chart.clear()` or `chart.setOption({...}, true)` with the same id, - // view will be reused. - if (graphicModel !== this._lastGraphicModel) { - this._clear(); - } - this._lastGraphicModel = graphicModel; - - this._updateElements(graphicModel); - this._relocate(graphicModel, api); - } - - /** - * Update graphic elements. - */ - private _updateElements(graphicModel: GraphicComponentModel): void { - const elOptionsToUpdate = graphicModel.useElOptionsToUpdate(); - - if (!elOptionsToUpdate) { - return; - } - - const elMap = this._elMap; - const rootGroup = this.group; - - // Top-down tranverse to assign graphic settings to each elements. - zrUtil.each(elOptionsToUpdate, function (elOption) { - const id = modelUtil.convertOptionIdName(elOption.id, null); - const elExisting = id != null ? elMap.get(id) : null; - const parentId = modelUtil.convertOptionIdName(elOption.parentId, null); - const targetElParent = (parentId != null ? elMap.get(parentId) : rootGroup) as graphicUtil.Group; - - const elType = elOption.type; - const elOptionStyle = (elOption as GraphicComponentDisplayableOption).style; - if (elType === 'text' && elOptionStyle) { - // In top/bottom mode, textVerticalAlign should not be used, which cause - // inaccurately locating. - if (elOption.hv && elOption.hv[1]) { - (elOptionStyle as any).textVerticalAlign = - (elOptionStyle as any).textBaseline = - (elOptionStyle as TextStyleProps).verticalAlign = - (elOptionStyle as TextStyleProps).align = null; - } - } - - let textContentOption = (elOption as GraphicComponentZRPathOption).textContent; - let textConfig = (elOption as GraphicComponentZRPathOption).textConfig; - if (elOptionStyle - && isEC4CompatibleStyle(elOptionStyle, elType, !!textConfig, !!textContentOption) - ) { - const convertResult = - convertFromEC4CompatibleStyle(elOptionStyle, elType, true) as GraphicComponentZRPathOption; - if (!textConfig && convertResult.textConfig) { - textConfig = (elOption as GraphicComponentZRPathOption).textConfig = convertResult.textConfig; - } - if (!textContentOption && convertResult.textContent) { - textContentOption = convertResult.textContent; - } - } - - // Remove unnecessary props to avoid potential problems. - const elOptionCleaned = getCleanedElOption(elOption); - - // For simple, do not support parent change, otherwise reorder is needed. - if (__DEV__) { - elExisting && zrUtil.assert( - targetElParent === elExisting.parent, - 'Changing parent is not supported.' - ); - } - - const $action = elOption.$action || 'merge'; - if ($action === 'merge') { - elExisting - ? elExisting.attr(elOptionCleaned) - : createEl(id, targetElParent, elOptionCleaned, elMap); - } - else if ($action === 'replace') { - removeEl(elExisting, elMap); - createEl(id, targetElParent, elOptionCleaned, elMap); - } - else if ($action === 'remove') { - removeEl(elExisting, elMap); - } - - const el = elMap.get(id); +export function install(registers: EChartsExtensionInstallRegisters) { - if (el && textContentOption) { - if ($action === 'merge') { - const textContentExisting = el.getTextContent(); - textContentExisting - ? textContentExisting.attr(textContentOption) - : el.setTextContent(new graphicUtil.Text(textContentOption)); - } - else if ($action === 'replace') { - el.setTextContent(new graphicUtil.Text(textContentOption)); - } - } + registers.registerComponentModel(GraphicComponentModel); + registers.registerComponentView(GraphicComponentView); - if (el) { - const elInner = inner(el); - elInner.widthOption = (elOption as GraphicComponentGroupOption).width; - elInner.heightOption = (elOption as GraphicComponentGroupOption).height; - setEventData(el, graphicModel, elOption); + registers.registerPreprocessor(function (option) { + const graphicOption = option.graphic as GraphicComponentOption | GraphicComponentOption[]; - graphicUtil.setTooltipConfig({ - el: el, - componentModel: graphicModel, - itemName: el.name, - itemTooltipOption: elOption.tooltip - }); + // Convert + // {graphic: [{left: 10, type: 'circle'}, ...]} + // or + // {graphic: {left: 10, type: 'circle'}} + // to + // {graphic: [{elements: [{left: 10, type: 'circle'}, ...]}]} + if (isArray(graphicOption)) { + if (!graphicOption[0] || !graphicOption[0].elements) { + option.graphic = [{ elements: graphicOption }]; } - }); - } - - /** - * Locate graphic elements. - */ - private _relocate(graphicModel: GraphicComponentModel, api: ExtensionAPI): void { - const elOptions = graphicModel.option.elements; - const rootGroup = this.group; - const elMap = this._elMap; - const apiWidth = api.getWidth(); - const apiHeight = api.getHeight(); - - // Top-down to calculate percentage width/height of group - for (let i = 0; i < elOptions.length; i++) { - const elOption = elOptions[i]; - const id = modelUtil.convertOptionIdName(elOption.id, null); - const el = id != null ? elMap.get(id) : null; - - if (!el || !el.isGroup) { - continue; + else { + // Only one graphic instance can be instantiated. (We dont + // want that too many views are created in echarts._viewMap) + option.graphic = [(option.graphic as any)[0]]; } - const parentEl = el.parent; - const isParentRoot = parentEl === rootGroup; - // Like 'position:absolut' in css, default 0. - const elInner = inner(el); - const parentElInner = inner(parentEl); - elInner.width = parsePercent( - elInner.widthOption, - isParentRoot ? apiWidth : parentElInner.width - ) || 0; - elInner.height = parsePercent( - elInner.heightOption, - isParentRoot ? apiHeight : parentElInner.height - ) || 0; } - - // Bottom-up tranvese all elements (consider ec resize) to locate elements. - for (let i = elOptions.length - 1; i >= 0; i--) { - const elOption = elOptions[i]; - const id = modelUtil.convertOptionIdName(elOption.id, null); - const el = id != null ? elMap.get(id) : null; - - if (!el) { - continue; - } - - const parentEl = el.parent; - const parentElInner = inner(parentEl); - const containerInfo = parentEl === rootGroup - ? { - width: apiWidth, - height: apiHeight - } - : { - width: parentElInner.width, - height: parentElInner.height - }; - - // PENDING - // Currently, when `bounding: 'all'`, the union bounding rect of the group - // does not include the rect of [0, 0, group.width, group.height], which - // is probably weird for users. Should we make a break change for it? - layoutUtil.positionElement( - el, elOption, containerInfo, null, - {hv: elOption.hv, boundingMode: elOption.bounding} - ); - } - } - - /** - * Clear all elements. - */ - private _clear(): void { - const elMap = this._elMap; - elMap.each(function (el) { - removeEl(el, elMap); - }); - this._elMap = zrUtil.createHashMap(); - } - - dispose(): void { - this._clear(); - } -} - -function createEl( - id: string, - targetElParent: graphicUtil.Group, - elOption: GraphicComponentElementOption, - elMap: ElementMap -): void { - const graphicType = elOption.type; - - if (__DEV__) { - zrUtil.assert(graphicType, 'graphic type MUST be set'); - } - - const Clz = ( - zrUtil.hasOwn(_nonShapeGraphicElements, graphicType) - // Those graphic elements are not shapes. They should not be - // overwritten by users, so do them first. - ? _nonShapeGraphicElements[graphicType as NonShapeGraphicElementType] - : graphicUtil.getShapeClass(graphicType) - ) as { new(opt: GraphicComponentElementOption): Element }; - - if (__DEV__) { - zrUtil.assert(Clz, 'graphic type can not be found'); - } - - const el = new Clz(elOption); - targetElParent.add(el); - elMap.set(id, el); - inner(el).id = id; -} - -function removeEl(elExisting: Element, elMap: ElementMap): void { - const existElParent = elExisting && elExisting.parent; - if (existElParent) { - elExisting.type === 'group' && elExisting.traverse(function (el) { - removeEl(el, elMap); - }); - elMap.removeKey(inner(elExisting).id); - existElParent.remove(elExisting); - } -} - -// Remove unnecessary props to avoid potential problems. -function getCleanedElOption( - elOption: GraphicComponentElementOption -): Omit { - elOption = zrUtil.extend({}, elOption); - zrUtil.each( - ['id', 'parentId', '$action', 'hv', 'bounding', 'textContent'].concat(layoutUtil.LOCATION_PARAMS), - function (name) { - delete (elOption as any)[name]; + else if (graphicOption && !graphicOption.elements) { + option.graphic = [{ elements: [graphicOption] }]; } - ); - return elOption; -} - -function isSetLoc( - obj: GraphicComponentElementOption, - props: ('left' | 'right' | 'top' | 'bottom')[] -): boolean { - let isSet; - zrUtil.each(props, function (prop) { - obj[prop] != null && obj[prop] !== 'auto' && (isSet = true); }); - return isSet; -} - -function setKeyInfoToNewElOption( - resultItem: ReturnType[number], - newElOption: GraphicComponentElementOption -): void { - const existElOption = resultItem.existing as GraphicComponentElementOption; - - // Set id and type after id assigned. - newElOption.id = resultItem.keyInfo.id; - !newElOption.type && existElOption && (newElOption.type = existElOption.type); - - // Set parent id if not specified - if (newElOption.parentId == null) { - const newElParentOption = newElOption.parentOption; - if (newElParentOption) { - newElOption.parentId = newElParentOption.id; - } - else if (existElOption) { - newElOption.parentId = existElOption.parentId; - } - } - - // Clear - newElOption.parentOption = null; -} - -function mergeNewElOptionToExist( - existList: GraphicComponentElementOption[], - index: number, - newElOption: GraphicComponentElementOption -): void { - // Update existing options, for `getOption` feature. - const newElOptCopy = zrUtil.extend({}, newElOption); - const existElOption = existList[index]; - - const $action = newElOption.$action || 'merge'; - if ($action === 'merge') { - if (existElOption) { - - if (__DEV__) { - const newType = newElOption.type; - zrUtil.assert( - !newType || existElOption.type === newType, - 'Please set $action: "replace" to change `type`' - ); - } - - // We can ensure that newElOptCopy and existElOption are not - // the same object, so `merge` will not change newElOptCopy. - zrUtil.merge(existElOption, newElOptCopy, true); - // Rigid body, use ignoreSize. - layoutUtil.mergeLayoutParam(existElOption, newElOptCopy, {ignoreSize: true}); - // Will be used in render. - layoutUtil.copyLayoutParams(newElOption, existElOption); - } - else { - existList[index] = newElOptCopy; - } - } - else if ($action === 'replace') { - existList[index] = newElOptCopy; - } - else if ($action === 'remove') { - // null will be cleaned later. - existElOption && (existList[index] = null); - } -} - -function setLayoutInfoToExist( - existItem: GraphicComponentElementOption, - newElOption: GraphicComponentElementOption -) { - if (!existItem) { - return; - } - existItem.hv = newElOption.hv = [ - // Rigid body, dont care `width`. - isSetLoc(newElOption, ['left', 'right']), - // Rigid body, dont care `height`. - isSetLoc(newElOption, ['top', 'bottom']) - ]; - // Give default group size. Otherwise layout error may occur. - if (existItem.type === 'group') { - const existingGroupOpt = existItem as GraphicComponentGroupOption; - const newGroupOpt = newElOption as GraphicComponentGroupOption; - existingGroupOpt.width == null && (existingGroupOpt.width = newGroupOpt.width = 0); - existingGroupOpt.height == null && (existingGroupOpt.height = newGroupOpt.height = 0); - } -} - -function setEventData( - el: Element, - graphicModel: GraphicComponentModel, - elOption: GraphicComponentElementOption -): void { - let eventData = getECData(el).eventData; - // Simple optimize for large amount of elements that no need event. - if (!el.silent && !el.ignore && !eventData) { - eventData = getECData(el).eventData = { - componentType: 'graphic', - componentIndex: graphicModel.componentIndex, - name: el.name - }; - } - - // `elOption.info` enables user to mount some info on - // elements and use them in event handlers. - if (eventData) { - eventData.info = elOption.info; - } -} - -export function install(registers: EChartsExtensionInstallRegisters) { - registers.registerComponentModel(GraphicComponentModel); - registers.registerComponentView(GraphicComponentView); - registers.registerPreprocessor(preprocessor); } \ No newline at end of file diff --git a/src/export/option.ts b/src/export/option.ts index dff46af9d9..d25e8197a3 100644 --- a/src/export/option.ts +++ b/src/export/option.ts @@ -94,7 +94,7 @@ import type { CustomSeriesRenderItem } from '../chart/custom/CustomSeries'; -import type { GraphicComponentLooseOption as GraphicComponentOption } from '../component/graphic/install'; +import { GraphicComponentLooseOption as GraphicComponentOption } from '../component/graphic/GraphicModel'; import type { DatasetOption as DatasetComponentOption } from '../component/dataset/install'; import type {ToolboxBrushFeatureOption} from '../component/toolbox/feature/Brush'; From 6199529103faa71f5f95ad0454e2fc212c6dbccd Mon Sep 17 00:00:00 2001 From: pissang Date: Thu, 18 Nov 2021 14:32:11 +0800 Subject: [PATCH 02/35] refact(custom): extract common helpers for transition --- .../customGraphicTransitionHelper.ts | 700 ++++++++++++++++++ src/chart/custom/CustomSeries.ts | 85 +-- src/chart/custom/CustomView.ts | 315 +------- src/chart/custom/prepare.ts | 353 --------- src/component/graphic/GraphicModel.ts | 2 - 5 files changed, 738 insertions(+), 717 deletions(-) create mode 100644 src/animation/customGraphicTransitionHelper.ts delete mode 100644 src/chart/custom/prepare.ts diff --git a/src/animation/customGraphicTransitionHelper.ts b/src/animation/customGraphicTransitionHelper.ts new file mode 100644 index 0000000000..45a5938da2 --- /dev/null +++ b/src/animation/customGraphicTransitionHelper.ts @@ -0,0 +1,700 @@ +/* +* 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 custom graphic elements in custom series and graphic components. + +import Transformable from 'zrender/src/core/Transformable'; +import Element, { ElementProps } from 'zrender/src/Element'; + +import { makeInner, normalizeToArray } from '../util/model'; +import { assert, bind, eqNaN, hasOwn, indexOf, isArrayLike, keys } 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 { initProps, updateProps } from './basicTrasition'; +import { Path } from '../util/graphic'; +import { warn } from '../util/log'; +import { AnimationOptionMixin, ZRStyleProps } from '../util/types'; +import { Dictionary } from 'zrender/lib/core/types'; +import { PathStyleProps } from 'zrender'; + +const LEGACY_TRANSFORM_PROPS = { + position: ['x', 'y'], + scale: ['scaleX', 'scaleY'], + origin: ['originX', 'originY'] +} as const; +type LegacyTransformProp = keyof typeof LEGACY_TRANSFORM_PROPS; + +export type CustomTransitionProps = string | string[]; +export interface ElementTransitionOptionMixin { + transition?: CustomTransitionProps; + enterFrom?: Dictionary; + leaveTo?: Dictionary; +}; + +export type ElementTransformTransitionOptionMixin = { + transition?: ElementRootTransitionProp | ElementRootTransitionProp[]; + enterFrom?: Dictionary; + leaveTo?: Dictionary; +}; +type ElementRootTransitionProp = TransformProp | 'shape' | 'extra' | 'style'; + +export const TRANSFORM_PROPS = { + x: 1, + y: 1, + scaleX: 1, + scaleY: 1, + originX: 1, + originY: 1, + rotation: 1 +} as const; +export type TransformProp = keyof typeof TRANSFORM_PROPS; + +interface LooseElementProps extends ElementProps { + style?: ZRStyleProps; + shape?: Dictionary; +} + +type TransitionElementOption = Partial> & { + shape?: Dictionary & ElementTransitionOptionMixin + style?: PathStyleProps & ElementTransitionOptionMixin + extra?: Dictionary & ElementTransitionOptionMixin + invisible?: boolean + silent?: boolean + autoBatch?: boolean + ignore?: boolean + + during?: (params: TransitionDuringAPI) => void +} & ElementTransformTransitionOptionMixin; + +const transitionInnerStore = makeInner<{ + leaveToProps: ElementProps; + userDuring: (params: TransitionDuringAPI) => void; +}, Element>(); + +const transformPropNamesStr = keys(TRANSFORM_PROPS).join(', '); + +function setLegacyTransformProp( + elOption: TransitionElementOption, + targetProps: Partial>, + legacyName: LegacyTransformProp +): void { + const legacyArr = (elOption as any)[legacyName]; + const xyName = LEGACY_TRANSFORM_PROPS[legacyName]; + if (legacyArr) { + targetProps[xyName[0]] = legacyArr[0]; + targetProps[xyName[1]] = legacyArr[1]; + } +} + +function setTransformProp( + elOption: TransitionElementOption, + allProps: Partial>, + name: TransformProp +): void { + if (elOption[name] != null) { + allProps[name] = elOption[name]; + } +} + +function setTransformPropToTransitionFrom( + transitionFrom: Partial>, + name: TransformProp, + fromTransformable?: Transformable // If provided, retrieve from the element. +): void { + if (fromTransformable) { + transitionFrom[name] = fromTransformable[name]; + } +} + + +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(key: T, val: ShapeOpt[T]): this; + getShape(key: T): ShapeOpt[T]; + setStyle(key: T, val: StyleOpt[T]): this + getStyle(key: T): StyleOpt[T]; +}; + +export function updateTransition( + el: Element, + elOption: TransitionElementOption, + animatableModel?: Model, + dataIndex?: number, + isInit?: boolean +) { + // 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; + + prepareShapeOrExtraTransitionFrom('shape', el, elOption, transFromProps, isInit); + prepareShapeOrExtraAllPropsFinal('shape', elOption, propsToSet); + prepareTransformTransitionFrom(el, elOption, transFromProps, isInit); + prepareTransformAllPropsFinal(el, elOption, propsToSet); + prepareShapeOrExtraTransitionFrom('extra', el, elOption, transFromProps, isInit); + prepareShapeOrExtraAllPropsFinal('extra', elOption, propsToSet); + prepareStyleTransitionFrom(el, elOption, styleOpt, transFromProps, isInit); + (propsToSet as DisplayableProps).style = styleOpt; + applyPropsDirectly(el, propsToSet); + applyPropsTransition(el, dataIndex, animatableModel, transFromProps, isInit); + applyMiscProps(el, elOption); + + styleOpt ? el.dirty() : el.markRedraw(); +} + +export function leaveTransition( + el: Element, + seriesModel: Model +): void { + if (el) { + const parent = el.parent; + const leaveToProps = transitionInnerStore(el).leaveToProps; + leaveToProps + ? updateProps(el, leaveToProps, seriesModel, { + cb: function () { + parent.remove(el); + } + }) + : parent.remove(el); + } +} + + +function applyPropsDirectly( + el: Element, + // Can be null/undefined + allPropsFinal: ElementProps +) { + const elDisplayable = el.isGroup ? null : el as Displayable; + const styleOpt = (allPropsFinal as Displayable).style; + + if (elDisplayable && styleOpt) { + + // PENDING: here the input style object is used directly. + // Good for performance but bad for compatibility control. + elDisplayable.useStyle(styleOpt); + // 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 = elDisplayable.animators; + for (let i = 0; i < animators.length; i++) { + const animator = animators[i]; + // targetName is the "topKey". + if (animator.targetName === 'style') { + animator.changeTarget(elDisplayable.style); + } + } + } + + 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, + dataIndex: number, + model: Model, + // Can be null/undefined + transFromProps: ElementProps, + isInit: boolean +): void { + if (transFromProps) { + // NOTE: Do not use `el.updateDuringAnimation` here becuase `el.updateDuringAnimation` will + // be called mutiple time in each animation frame. For example, if both "transform" props + // and shape props and style props changed, it will generate three animator and called + // one-by-one in each animation frame. + // We use the during in `animateTo/From` params. + const userDuring = transitionInnerStore(el).userDuring; + // For simplicity, if during not specified, the previous during will not work any more. + const cfgDuringCall = userDuring ? bind(duringCall, { el: el, userDuring: userDuring }) : null; + const cfg = { + dataIndex: dataIndex, + isFrom: true, + during: cfgDuringCall + }; + isInit + ? initProps(el, transFromProps, model, cfg) + : updateProps(el, transFromProps, model, cfg); + } +} + + +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; + isShapeDirty: boolean; + isStyleDirty: boolean; +}; +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, key), 'Only ' + transformPropNamesStr + ' available in `setTransform`.'); + } + tmpDuringScope.el[key] = val as number; + return this; + }, + getTransform(key: TransformProp): number { + if (__DEV__) { + assert(hasOwn(TRANSFORM_PROPS, key), 'Only ' + transformPropNamesStr + ' available in `getTransform`.'); + } + return tmpDuringScope.el[key]; + }, + setShape(key: any, val: unknown) { + if (__DEV__) { + assertNotReserved(key); + } + const shape = (tmpDuringScope.el as Path).shape + || ((tmpDuringScope.el as Path).shape = {}); + shape[key] = val; + tmpDuringScope.isShapeDirty = true; + 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 style = (tmpDuringScope.el as Displayable).style; + if (style) { + if (__DEV__) { + if (eqNaN(val)) { + warn('style.' + key + ' must not be assigned with NaN.'); + } + } + style[key] = val; + tmpDuringScope.isStyleDirty = true; + } + 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, + // becuase 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' calulation 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; + tmpDuringScope.isShapeDirty = false; + tmpDuringScope.isStyleDirty = false; + + // Give no `this` to user in "during" calling. + scopeUserDuring(transitionDuringAPI); + + if (tmpDuringScope.isShapeDirty && (el as Path).dirtyShape) { + (el as Path).dirtyShape(); + } + if (tmpDuringScope.isStyleDirty && (el as Displayable).dirtyStyle) { + (el as Displayable).dirtyStyle(); + } + // markRedraw() will be called by default in during. + // FIXME `this.markRedraw();` directly ? + + // 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: TransitionElementOption, + transFromProps: LooseElementProps, + isInit: boolean +): void { + + const attrOpt: Dictionary & ElementTransitionOptionMixin = (elOption as any)[mainAttr]; + if (!attrOpt) { + return; + } + + const elPropsInAttr = (fromEl as LooseElementProps)[mainAttr]; + let transFromPropsInAttr: Dictionary; + + const enterFrom = attrOpt.enterFrom; + if (isInit && enterFrom) { + !transFromPropsInAttr && (transFromPropsInAttr = transFromProps[mainAttr] = {}); + const enterFromKeys = keys(enterFrom); + for (let i = 0; i < enterFromKeys.length; i++) { + // `enterFrom` props are not necessarily also declared in `shape`/`style`/..., + // for example, `opacity` can only declared in `enterFrom` but not in `style`. + const key = enterFromKeys[i]; + // Do not clone, animator will perform that clone. + transFromPropsInAttr[key] = enterFrom[key]; + } + } + + if (!isInit && elPropsInAttr) { + if (attrOpt.transition) { + !transFromPropsInAttr && (transFromPropsInAttr = transFromProps[mainAttr] = {}); + const transitionKeys = normalizeToArray(attrOpt.transition); + for (let i = 0; i < transitionKeys.length; i++) { + const key = transitionKeys[i]; + const elVal = elPropsInAttr[key]; + if (__DEV__) { + checkNonStyleTansitionRefer(key, (attrOpt as any)[key], elVal); + } + // Do not clone, see `checkNonStyleTansitionRefer`. + transFromPropsInAttr[key] = elVal; + } + } + else if (indexOf(elOption.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; + } + } + } + } + + const leaveTo = attrOpt.leaveTo; + if (leaveTo) { + const leaveToProps = getOrCreateLeaveToPropsFromEl(fromEl); + const leaveToPropsInAttr: Dictionary = leaveToProps[mainAttr] || (leaveToProps[mainAttr] = {}); + const leaveToKeys = keys(leaveTo); + for (let i = 0; i < leaveToKeys.length; i++) { + const key = leaveToKeys[i]; + leaveToPropsInAttr[key] = leaveTo[key]; + } + } +} + +function prepareShapeOrExtraAllPropsFinal( + mainAttr: 'shape' | 'extra', + elOption: TransitionElementOption, + allProps: LooseElementProps +): void { + const attrOpt: Dictionary & ElementTransitionOptionMixin = (elOption as any)[mainAttr]; + if (!attrOpt) { + return; + } + const allPropsInAttr = allProps[mainAttr] = {} as Dictionary; + 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, + isInit: boolean +): void { + const enterFrom = elOption.enterFrom; + if (isInit && enterFrom) { + const enterFromKeys = keys(enterFrom); + for (let i = 0; i < enterFromKeys.length; i++) { + const key = enterFromKeys[i] as TransformProp; + if (__DEV__) { + checkTransformPropRefer(key, 'el.enterFrom'); + } + // Do not clone, animator will perform that clone. + transFromProps[key] = enterFrom[key] as number; + } + } + + if (!isInit) { + if (elOption.transition) { + const transitionKeys = normalizeToArray(elOption.transition); + for (let i = 0; i < transitionKeys.length; i++) { + const key = transitionKeys[i]; + if (key === 'style' || key === 'shape' || key === 'extra') { + continue; + } + const elVal = el[key]; + if (__DEV__) { + checkTransformPropRefer(key, 'el.transition'); + checkNonStyleTansitionRefer(key, elOption[key], elVal); + } + // Do not clone, see `checkNonStyleTansitionRefer`. + transFromProps[key] = elVal; + } + } + // This default transition see [STRATEGY_TRANSITION] + else { + setTransformPropToTransitionFrom(transFromProps, 'x', el); + setTransformPropToTransitionFrom(transFromProps, 'y', el); + } + } + + const leaveTo = elOption.leaveTo; + if (leaveTo) { + const leaveToProps = getOrCreateLeaveToPropsFromEl(el); + const leaveToKeys = keys(leaveTo); + for (let i = 0; i < leaveToKeys.length; i++) { + const key = leaveToKeys[i] as TransformProp; + if (__DEV__) { + checkTransformPropRefer(key, 'el.leaveTo'); + } + leaveToProps[key] = leaveTo[key] as number; + } + } +} + +function prepareTransformAllPropsFinal( + el: Element, + elOption: TransitionElementOption, + allProps: ElementProps +): void { + setLegacyTransformProp(elOption, allProps, 'position'); + setLegacyTransformProp(elOption, allProps, 'scale'); + setLegacyTransformProp(elOption, allProps, 'origin'); + + setTransformProp(elOption, allProps, 'x'); + setTransformProp(elOption, allProps, 'y'); + setTransformProp(elOption, allProps, 'scaleX'); + setTransformProp(elOption, allProps, 'scaleY'); + setTransformProp(elOption, allProps, 'originX'); + setTransformProp(elOption, allProps, 'originY'); + setTransformProp(elOption, allProps, 'rotation'); +} + +function prepareStyleTransitionFrom( + fromEl: Element, + elOption: TransitionElementOption, + styleOpt: TransitionElementOption['style'], + transFromProps: LooseElementProps, + isInit: boolean +): void { + if (!styleOpt) { + return; + } + + const fromElStyle = (fromEl as LooseElementProps).style as LooseElementProps['style']; + let transFromStyleProps: LooseElementProps['style']; + + const enterFrom = styleOpt.enterFrom; + if (isInit && enterFrom) { + const enterFromKeys = keys(enterFrom); + !transFromStyleProps && (transFromStyleProps = transFromProps.style = {}); + for (let i = 0; i < enterFromKeys.length; i++) { + const key = enterFromKeys[i]; + // Do not clone, animator will perform that clone. + (transFromStyleProps as any)[key] = enterFrom[key]; + } + } + + if (!isInit && fromElStyle) { + if (styleOpt.transition) { + const transitionKeys = normalizeToArray(styleOpt.transition); + !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 + && indexOf(elOption.transition, '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)[key]) { + const elVal = (fromElStyle as any)[key]; + (transFromStyleProps as any)[key] = elVal; + } + } + } + } + } + + const leaveTo = styleOpt.leaveTo; + if (leaveTo) { + const leaveToKeys = keys(leaveTo); + const leaveToProps = getOrCreateLeaveToPropsFromEl(fromEl); + const leaveToStyleProps = leaveToProps.style || (leaveToProps.style = {}); + for (let i = 0; i < leaveToKeys.length; i++) { + const key = leaveToKeys[i]; + (leaveToStyleProps as any)[key] = leaveTo[key]; + } + } +} + +let checkNonStyleTansitionRefer: (propName: string, optVal: unknown, elVal: unknown) => void; +if (__DEV__) { + checkNonStyleTansitionRefer = function (propName: string, optVal: unknown, elVal: unknown): void { + if (!isArrayLike(optVal)) { + assert( + optVal != null && isFinite(optVal as number), + 'Prop `' + propName + '` must refer to a finite number or ArrayLike for transition.' + ); + } + else { + // Try not to copy array for performance, but if user use the same object in different + // call of `renderItem`, it will casue animation transition fail. + assert( + optVal !== elVal, + 'Prop `' + propName + '` must use different Array object each time for transition.' + ); + } + }; +} + +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 { + assert( + hasOwn(TRANSFORM_PROPS, key), + 'Prop `' + key + '` is not a permitted in `' + usedIn + '`. ' + + 'Only `' + keys(TRANSFORM_PROPS).join('`, `') + '` are permitted.' + ); + }; +} + +function getOrCreateLeaveToPropsFromEl(el: Element): LooseElementProps { + const innerEl = transitionInnerStore(el); + return innerEl.leaveToProps || (innerEl.leaveToProps = {}); +} + diff --git a/src/chart/custom/CustomSeries.ts b/src/chart/custom/CustomSeries.ts index ae4e548400..cf72658c0e 100644 --- a/src/chart/custom/CustomSeries.ts +++ b/src/chart/custom/CustomSeries.ts @@ -43,7 +43,7 @@ import { TextCommonOption, ZRStyleProps } from '../../util/types'; -import Element, { ElementProps } from 'zrender/src/Element'; +import Element from 'zrender/src/Element'; import SeriesData, { DefaultDataVisual } from '../../data/SeriesData'; import GlobalModel from '../../model/Global'; import createSeriesData from '../helper/createSeriesData'; @@ -64,24 +64,15 @@ import { Sector } from '../../util/graphic'; import { TextStyleProps } from 'zrender/src/graphic/Text'; - - -export interface LooseElementProps extends ElementProps { - style?: ZRStyleProps; - shape?: Dictionary; -} +import { + ElementTransformTransitionOptionMixin, + ElementTransitionOptionMixin, + TransitionBaseDuringAPI, + TransitionDuringAPI +} from '../../animation/customGraphicTransitionHelper'; +import { TransformProp } from 'zrender/lib/core/Transformable'; export type CustomExtraElementInfo = Dictionary; -export const TRANSFORM_PROPS = { - x: 1, - y: 1, - scaleX: 1, - scaleY: 1, - originX: 1, - originY: 1, - rotation: 1 -} as const; -export type TransformProp = keyof typeof TRANSFORM_PROPS; // Also compat with ec4, where // `visual('color') visual('borderColor')` is supported. @@ -102,19 +93,7 @@ export const NON_STYLE_VISUAL_PROPS = { } as const; export type NonStyleVisualProps = keyof typeof NON_STYLE_VISUAL_PROPS; -// Do not declare "Dictionary" in TransitionAnyOption to restrict the type check. -export type TransitionAnyOption = { - transition?: TransitionAnyProps; - enterFrom?: Dictionary; - leaveTo?: Dictionary; -}; -type TransitionAnyProps = string | string[]; -type TransitionTransformOption = { - transition?: ElementRootTransitionProp | ElementRootTransitionProp[]; - enterFrom?: Dictionary; - leaveTo?: Dictionary; -}; -type ElementRootTransitionProp = TransformProp | 'shape' | 'extra' | 'style'; +// Do not declare "Dictionary" in ElementTransitionOptions to restrict the type check. type ShapeMorphingOption = { /** * If do shape morphing animation when type is changed. @@ -123,27 +102,9 @@ type ShapeMorphingOption = { morph?: boolean }; -export interface CustomBaseDuringAPI { - // 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 CustomDuringAPI< - StyleOpt extends any = any, - ShapeOpt extends any = any -> extends CustomBaseDuringAPI { - setShape(key: T, val: ShapeOpt[T]): this; - getShape(key: T): ShapeOpt[T]; - setStyle(key: T, val: StyleOpt[T]): this - getStyle(key: T): StyleOpt[T]; -}; - - export interface CustomBaseElementOption extends Partial>, TransitionTransformOption { +>>, ElementTransformTransitionOptionMixin { // element type, required. type: string; id?: string; @@ -155,16 +116,16 @@ export interface CustomBaseElementOption extends Partial & TransitionAnyOption; + extra?: Dictionary & ElementTransitionOptionMixin; // updateDuringAnimation - during?(params: CustomBaseDuringAPI): void; + during?(params: TransitionBaseDuringAPI): void; }; export interface CustomDisplayableOption extends CustomBaseElementOption, Partial> { - style?: ZRStyleProps & TransitionAnyOption; - during?(params: CustomDuringAPI): void; + style?: ZRStyleProps & ElementTransitionOptionMixin; + during?(params: TransitionDuringAPI): void; /** * @deprecated */ @@ -178,10 +139,10 @@ export interface CustomDisplayableOptionOnState extends Partial> { // `false` means remove emphasis trigger. - style?: (ZRStyleProps & TransitionAnyOption) | false; + style?: (ZRStyleProps & ElementTransitionOptionMixin) | false; - during?(params: CustomDuringAPI): void; + during?(params: TransitionDuringAPI): void; } export interface CustomGroupOption extends CustomBaseElementOption { type: 'group'; @@ -195,9 +156,9 @@ export interface CustomGroupOption extends CustomBaseElementOption { export interface CustomBaseZRPathOption extends CustomDisplayableOption, ShapeMorphingOption { autoBatch?: boolean; - shape?: T & TransitionAnyOption; + shape?: T & ElementTransitionOptionMixin; style?: PathProps['style'] - during?(params: CustomDuringAPI): void; + during?(params: TransitionDuringAPI): void; } interface BuiltinShapes { @@ -240,22 +201,22 @@ export type CustomPathOption = CreateCustomBuitinPathOption | CustomSVGPathOption; export interface CustomImageOptionOnState extends CustomDisplayableOptionOnState { - style?: ImageStyleProps & TransitionAnyOption; + style?: ImageStyleProps & ElementTransitionOptionMixin; } export interface CustomImageOption extends CustomDisplayableOption { type: 'image'; - style?: ImageStyleProps & TransitionAnyOption; + style?: ImageStyleProps & ElementTransitionOptionMixin; emphasis?: CustomImageOptionOnState; blur?: CustomImageOptionOnState; select?: CustomImageOptionOnState; } export interface CustomTextOptionOnState extends CustomDisplayableOptionOnState { - style?: TextStyleProps & TransitionAnyOption; + style?: TextStyleProps & ElementTransitionOptionMixin; } export interface CustomTextOption extends CustomDisplayableOption { type: 'text'; - style?: TextStyleProps & TransitionAnyOption; + style?: TextStyleProps & ElementTransitionOptionMixin; emphasis?: CustomTextOptionOnState; blur?: CustomTextOptionOnState; select?: CustomTextOptionOnState; @@ -394,9 +355,7 @@ export const customInnerStore = makeInner<{ customImagePath: CustomImageOption['style']['image']; // customText: string; txConZ2Set: number; - leaveToProps: ElementProps; option: CustomElementOption; - userDuring: CustomBaseElementOption['during']; }, Element>(); export default class CustomSeriesModel extends SeriesModel { diff --git a/src/chart/custom/CustomView.ts b/src/chart/custom/CustomView.ts index 91854af208..2621341964 100644 --- a/src/chart/custom/CustomView.ts +++ b/src/chart/custom/CustomView.ts @@ -18,8 +18,7 @@ */ import { - hasOwn, assert, isString, retrieve2, retrieve3, defaults, each, - keys, bind, eqNaN, indexOf + hasOwn, assert, isString, retrieve2, retrieve3, defaults, each, indexOf } from 'zrender/src/core/util'; import * as graphicUtil from '../../util/graphic'; import { setDefaultStateProxy, enableHoverEmphasis } from '../../util/states'; @@ -45,7 +44,7 @@ import { OrdinalRawValue, InnerDecalObject } from '../../util/types'; -import Element, { ElementProps, ElementTextConfig } from 'zrender/src/Element'; +import Element, { ElementTextConfig } from 'zrender/src/Element'; import prepareCartesian2d from '../../coord/cartesian/prepareCustom'; import prepareGeo from '../../coord/geo/prepareCustom'; import prepareSingleAxis from '../../coord/single/prepareCustom'; @@ -54,7 +53,7 @@ import prepareCalendar from '../../coord/calendar/prepareCustom'; import SeriesData, { DefaultDataVisual } from '../../data/SeriesData'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; -import Displayable, { DisplayableProps } from 'zrender/src/graphic/Displayable'; +import Displayable from 'zrender/src/graphic/Displayable'; import Axis2D from '../../coord/cartesian/Axis2D'; import { RectLike } from 'zrender/src/core/BoundingRect'; import { PathStyleProps } from 'zrender/src/graphic/Path'; @@ -67,14 +66,10 @@ import { warnDeprecated } from '../../util/styleCompat'; import { ItemStyleProps } from '../../model/mixin/itemStyle'; -import { warn, throwError } from '../../util/log'; +import { throwError } from '../../util/log'; import { createOrUpdatePatternFromDecal } from '../../util/decal'; import CustomSeriesModel, { - CustomDuringAPI, - TransformProp, - TRANSFORM_PROPS, CustomImageOption, - CustomBaseElementOption, CustomElementOption, CustomElementOptionOnState, CustomSVGPathOption, @@ -89,22 +84,13 @@ import CustomSeriesModel, { STYLE_VISUAL_TYPE, NON_STYLE_VISUAL_PROPS, customInnerStore, - LooseElementProps, PrepareCustomInfo, CustomPathOption, CustomRootElementOption } from './CustomSeries'; -import { - prepareShapeOrExtraAllPropsFinal, - prepareShapeOrExtraTransitionFrom, - prepareStyleTransitionFrom, - prepareTransformAllPropsFinal, - prepareTransformTransitionFrom -} from './prepare'; import { PatternObject } from 'zrender/src/graphic/Pattern'; import { CustomSeriesOption } from '../../export/option'; - -const transformPropNamesStr = keys(TRANSFORM_PROPS).join(', '); +import { leaveTransition, updateTransition } from '../../animation/customGraphicTransitionHelper'; const EMPHASIS = 'emphasis' as const; const NORMAL = 'normal' as const; @@ -232,7 +218,7 @@ export default class CustomChartView extends ChartView { ); }) .remove(function (oldIdx) { - doRemoveEl(oldData.getItemGraphicEl(oldIdx), customSeries, group); + leaveTransition(oldData.getItemGraphicEl(oldIdx), customSeries); }) .update(function (newIdx, oldIdx) { const oldEl = oldData.getItemGraphicEl(oldIdx); @@ -482,274 +468,22 @@ function updateElNormal( (styleOpt as InnerCustomZRPathOptionStyle).__decalPattern = decalPattern; } - // Save the meta info for further morphing. Like apply on the sub morphing elements. - const store = customInnerStore(el); - store.userDuring = elOption.during; - - const transFromProps = {} as ElementProps; - const propsToSet = {} as ElementProps; - - prepareShapeOrExtraTransitionFrom('shape', el, elOption, transFromProps, isInit); - prepareShapeOrExtraAllPropsFinal('shape', elOption, propsToSet); - prepareTransformTransitionFrom(el, elOption, transFromProps, isInit); - prepareTransformAllPropsFinal(el, elOption, propsToSet); - prepareShapeOrExtraTransitionFrom('extra', el, elOption, transFromProps, isInit); - prepareShapeOrExtraAllPropsFinal('extra', elOption, propsToSet); - prepareStyleTransitionFrom(el, elOption, styleOpt, transFromProps, isInit); - (propsToSet as DisplayableProps).style = styleOpt; - applyPropsDirectly(el, propsToSet); - applyPropsTransition(el, dataIndex, seriesModel, transFromProps, isInit); - applyMiscProps(el, elOption, isTextContent); - - styleOpt ? el.dirty() : el.markRedraw(); -} - -function applyMiscProps( - el: Element, elOption: CustomElementOption, isTextContent: boolean -) { - // Merge by default. - hasOwn(elOption, 'silent') && (el.silent = elOption.silent); - hasOwn(elOption, 'ignore') && (el.ignore = elOption.ignore); - if (isDisplayable(el)) { - hasOwn(elOption, 'invisible') && (el.invisible = (elOption as CustomDisplayableOption).invisible); - } - if (isPath(el)) { - hasOwn(elOption, 'autoBatch') && (el.autoBatch = (elOption as CustomBaseZRPathOption).autoBatch); - } - - if (!isTextContent) { - // `elOption.info` enables user to mount some info on - // elements and use them in event handlers. - // Update them only when user specified, otherwise, remain. - hasOwn(elOption, 'info') && (customInnerStore(el).info = elOption.info); - } -} - -function applyPropsDirectly( - el: Element, - // Can be null/undefined - allPropsFinal: ElementProps -) { - const elDisplayable = el.isGroup ? null : el as Displayable; - const styleOpt = (allPropsFinal as Displayable).style; - - if (elDisplayable && styleOpt) { - - // PENDING: here the input style object is used directly. - // Good for performance but bad for compatibility control. - elDisplayable.useStyle(styleOpt); - + if (isDisplayable(el) && styleOpt) { const decalPattern = (styleOpt as InnerCustomZRPathOptionStyle).__decalPattern; if (decalPattern) { - elDisplayable.style.decal = decalPattern; - } - - // 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 = elDisplayable.animators; - for (let i = 0; i < animators.length; i++) { - const animator = animators[i]; - // targetName is the "topKey". - if (animator.targetName === 'style') { - animator.changeTarget(elDisplayable.style); - } - } - } - - 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, - dataIndex: number, - seriesModel: CustomSeriesModel, - // Can be null/undefined - transFromProps: ElementProps, - isInit: boolean -): void { - if (transFromProps) { - // NOTE: Do not use `el.updateDuringAnimation` here becuase `el.updateDuringAnimation` will - // be called mutiple time in each animation frame. For example, if both "transform" props - // and shape props and style props changed, it will generate three animator and called - // one-by-one in each animation frame. - // We use the during in `animateTo/From` params. - const userDuring = customInnerStore(el).userDuring; - // For simplicity, if during not specified, the previous during will not work any more. - const cfgDuringCall = userDuring ? bind(duringCall, { el: el, userDuring: userDuring }) : null; - const cfg = { - dataIndex: dataIndex, - isFrom: true, - during: cfgDuringCall - }; - isInit - ? graphicUtil.initProps(el, transFromProps, seriesModel, cfg) - : graphicUtil.updateProps(el, transFromProps, seriesModel, cfg); - } -} - - -// Use it to avoid it be exposed to user. -const tmpDuringScope = {} as { - el: Element; - isShapeDirty: boolean; - isStyleDirty: boolean; -}; -const customDuringAPI: CustomDuringAPI = { - // Usually other props do not need to be changed in animation during. - setTransform(key: TransformProp, val: unknown) { - if (__DEV__) { - assert(hasOwn(TRANSFORM_PROPS, key), 'Only ' + transformPropNamesStr + ' available in `setTransform`.'); - } - tmpDuringScope.el[key] = val as number; - return this; - }, - getTransform(key: TransformProp): number { - if (__DEV__) { - assert(hasOwn(TRANSFORM_PROPS, key), 'Only ' + transformPropNamesStr + ' available in `getTransform`.'); - } - return tmpDuringScope.el[key]; - }, - setShape(key: any, val: unknown) { - if (__DEV__) { - assertNotReserved(key); - } - const shape = (tmpDuringScope.el as graphicUtil.Path).shape - || ((tmpDuringScope.el as graphicUtil.Path).shape = {}); - shape[key] = val; - tmpDuringScope.isShapeDirty = true; - return this; - }, - getShape(key: any): any { - if (__DEV__) { - assertNotReserved(key); - } - const shape = (tmpDuringScope.el as graphicUtil.Path).shape; - if (shape) { - return shape[key]; - } - }, - setStyle(key: any, val: unknown) { - if (__DEV__) { - assertNotReserved(key); - } - const style = (tmpDuringScope.el as Displayable).style; - if (style) { - if (__DEV__) { - if (eqNaN(val)) { - warn('style.' + key + ' must not be assigned with NaN.'); - } - } - style[key] = val; - tmpDuringScope.isStyleDirty = true; - } - 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 + '"'); + (styleOpt as PathStyleProps).decal = decalPattern; } } -} - -function duringCall( - this: { - el: Element; - userDuring: CustomBaseElementOption['during'] - } -): 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, - // becuase el will be added back to zr and the prop value should not be incorrect. - - const latestUserDuring = customInnerStore(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' calulation 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; - tmpDuringScope.isShapeDirty = false; - tmpDuringScope.isStyleDirty = false; + updateTransition(el, elOption, seriesModel, dataIndex, isInit); - // Give no `this` to user in "during" calling. - scopeUserDuring(customDuringAPI); - if (tmpDuringScope.isShapeDirty && (el as graphicUtil.Path).dirtyShape) { - (el as graphicUtil.Path).dirtyShape(); - } - if (tmpDuringScope.isStyleDirty && (el as Displayable).dirtyStyle) { - (el as Displayable).dirtyStyle(); + if (!isTextContent) { + // `elOption.info` enables user to mount some info on + // elements and use them in event handlers. + // Update them only when user specified, otherwise, remain. + hasOwn(elOption, 'info') && (customInnerStore(el).info = elOption.info); } - // markRedraw() will be called by default in during. - // FIXME `this.markRedraw();` directly ? - - // 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 updateElOnState( @@ -1576,7 +1310,7 @@ function mergeChildren( // Do not supprot leave elements that are not mentioned in the latest // `renderItem` return. Otherwise users may not have a clear and simple // concept that how to contorl all of the elements. - doRemoveEl(el.childAt(i), seriesModel, el); + leaveTransition(el.childAt(i), seriesModel); } } @@ -1630,24 +1364,7 @@ function processAddUpdate( function processRemove(this: DataDiffer, oldIndex: number): void { const context = this.context; const child = context.oldChildren[oldIndex]; - doRemoveEl(child, context.seriesModel, context.group); -} - -function doRemoveEl( - el: Element, - seriesModel: CustomSeriesModel, - group: ViewRootGroup -): void { - if (el) { - const leaveToProps = customInnerStore(el).leaveToProps; - leaveToProps - ? graphicUtil.updateProps(el, leaveToProps, seriesModel, { - cb: function () { - group.remove(el); - } - }) - : group.remove(el); - } + leaveTransition(child, context.seriesModel); } /** diff --git a/src/chart/custom/prepare.ts b/src/chart/custom/prepare.ts deleted file mode 100644 index 37f8d99e30..0000000000 --- a/src/chart/custom/prepare.ts +++ /dev/null @@ -1,353 +0,0 @@ -/* -* 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. -*/ - -import Transformable from 'zrender/src/core/Transformable'; -import Element, { ElementProps } from 'zrender/src/Element'; -import { Dictionary } from '../../util/types'; -import { - CustomDisplayableOption, - CustomElementOption, - customInnerStore, - LooseElementProps, - TransformProp, - TRANSFORM_PROPS, - TransitionAnyOption -} from './CustomSeries'; -import { normalizeToArray } from '../../util/model'; -import { assert, hasOwn, indexOf, isArrayLike, keys } from 'zrender/src/core/util'; -import { cloneValue } from 'zrender/src/animation/Animator'; -import Displayable from 'zrender/src/graphic/Displayable'; - -const LEGACY_TRANSFORM_PROPS = { - position: ['x', 'y'], - scale: ['scaleX', 'scaleY'], - origin: ['originX', 'originY'] -} as const; -type LegacyTransformProp = keyof typeof LEGACY_TRANSFORM_PROPS; - -function setLegacyTransformProp( - elOption: CustomElementOption, - targetProps: Partial>, - legacyName: LegacyTransformProp -): void { - const legacyArr = (elOption as any)[legacyName]; - const xyName = LEGACY_TRANSFORM_PROPS[legacyName]; - if (legacyArr) { - targetProps[xyName[0]] = legacyArr[0]; - targetProps[xyName[1]] = legacyArr[1]; - } -} - -function setTransformProp( - elOption: CustomElementOption, - allProps: Partial>, - name: TransformProp -): void { - if (elOption[name] != null) { - allProps[name] = elOption[name]; - } -} - -function setTransformPropToTransitionFrom( - transitionFrom: Partial>, - name: TransformProp, - fromTransformable?: Transformable // If provided, retrieve from the element. -): void { - if (fromTransformable) { - transitionFrom[name] = fromTransformable[name]; - } -} - - -// See [STRATEGY_TRANSITION] -export function prepareShapeOrExtraTransitionFrom( - mainAttr: 'shape' | 'extra', - fromEl: Element, - elOption: CustomElementOption, - transFromProps: LooseElementProps, - isInit: boolean -): void { - - const attrOpt: Dictionary & TransitionAnyOption = (elOption as any)[mainAttr]; - if (!attrOpt) { - return; - } - - const elPropsInAttr = (fromEl as LooseElementProps)[mainAttr]; - let transFromPropsInAttr: Dictionary; - - const enterFrom = attrOpt.enterFrom; - if (isInit && enterFrom) { - !transFromPropsInAttr && (transFromPropsInAttr = transFromProps[mainAttr] = {}); - const enterFromKeys = keys(enterFrom); - for (let i = 0; i < enterFromKeys.length; i++) { - // `enterFrom` props are not necessarily also declared in `shape`/`style`/..., - // for example, `opacity` can only declared in `enterFrom` but not in `style`. - const key = enterFromKeys[i]; - // Do not clone, animator will perform that clone. - transFromPropsInAttr[key] = enterFrom[key]; - } - } - - if (!isInit && elPropsInAttr) { - if (attrOpt.transition) { - !transFromPropsInAttr && (transFromPropsInAttr = transFromProps[mainAttr] = {}); - const transitionKeys = normalizeToArray(attrOpt.transition); - for (let i = 0; i < transitionKeys.length; i++) { - const key = transitionKeys[i]; - const elVal = elPropsInAttr[key]; - if (__DEV__) { - checkNonStyleTansitionRefer(key, (attrOpt as any)[key], elVal); - } - // Do not clone, see `checkNonStyleTansitionRefer`. - transFromPropsInAttr[key] = elVal; - } - } - else if (indexOf(elOption.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; - } - } - } - } - - const leaveTo = attrOpt.leaveTo; - if (leaveTo) { - const leaveToProps = getOrCreateLeaveToPropsFromEl(fromEl); - const leaveToPropsInAttr: Dictionary = leaveToProps[mainAttr] || (leaveToProps[mainAttr] = {}); - const leaveToKeys = keys(leaveTo); - for (let i = 0; i < leaveToKeys.length; i++) { - const key = leaveToKeys[i]; - leaveToPropsInAttr[key] = leaveTo[key]; - } - } -} - -export function prepareShapeOrExtraAllPropsFinal( - mainAttr: 'shape' | 'extra', - elOption: CustomElementOption, - allProps: LooseElementProps -): void { - const attrOpt: Dictionary & TransitionAnyOption = (elOption as any)[mainAttr]; - if (!attrOpt) { - return; - } - const allPropsInAttr = allProps[mainAttr] = {} as Dictionary; - 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]); - } -} - -// See [STRATEGY_TRANSITION]. -export function prepareTransformTransitionFrom( - el: Element, - elOption: CustomElementOption, - transFromProps: ElementProps, - isInit: boolean -): void { - const enterFrom = elOption.enterFrom; - if (isInit && enterFrom) { - const enterFromKeys = keys(enterFrom); - for (let i = 0; i < enterFromKeys.length; i++) { - const key = enterFromKeys[i] as TransformProp; - if (__DEV__) { - checkTransformPropRefer(key, 'el.enterFrom'); - } - // Do not clone, animator will perform that clone. - transFromProps[key] = enterFrom[key] as number; - } - } - - if (!isInit) { - if (elOption.transition) { - const transitionKeys = normalizeToArray(elOption.transition); - for (let i = 0; i < transitionKeys.length; i++) { - const key = transitionKeys[i]; - if (key === 'style' || key === 'shape' || key === 'extra') { - continue; - } - const elVal = el[key]; - if (__DEV__) { - checkTransformPropRefer(key, 'el.transition'); - checkNonStyleTansitionRefer(key, elOption[key], elVal); - } - // Do not clone, see `checkNonStyleTansitionRefer`. - transFromProps[key] = elVal; - } - } - // This default transition see [STRATEGY_TRANSITION] - else { - setTransformPropToTransitionFrom(transFromProps, 'x', el); - setTransformPropToTransitionFrom(transFromProps, 'y', el); - } - } - - const leaveTo = elOption.leaveTo; - if (leaveTo) { - const leaveToProps = getOrCreateLeaveToPropsFromEl(el); - const leaveToKeys = keys(leaveTo); - for (let i = 0; i < leaveToKeys.length; i++) { - const key = leaveToKeys[i] as TransformProp; - if (__DEV__) { - checkTransformPropRefer(key, 'el.leaveTo'); - } - leaveToProps[key] = leaveTo[key] as number; - } - } -} - -export function prepareTransformAllPropsFinal( - el: Element, - elOption: CustomElementOption, - allProps: ElementProps -): void { - setLegacyTransformProp(elOption, allProps, 'position'); - setLegacyTransformProp(elOption, allProps, 'scale'); - setLegacyTransformProp(elOption, allProps, 'origin'); - - setTransformProp(elOption, allProps, 'x'); - setTransformProp(elOption, allProps, 'y'); - setTransformProp(elOption, allProps, 'scaleX'); - setTransformProp(elOption, allProps, 'scaleY'); - setTransformProp(elOption, allProps, 'originX'); - setTransformProp(elOption, allProps, 'originY'); - setTransformProp(elOption, allProps, 'rotation'); -} - -// See [STRATEGY_TRANSITION]. -export function prepareStyleTransitionFrom( - fromEl: Element, - elOption: CustomElementOption, - styleOpt: CustomDisplayableOption['style'], - transFromProps: LooseElementProps, - isInit: boolean -): void { - if (!styleOpt) { - return; - } - - const fromElStyle = (fromEl as LooseElementProps).style as LooseElementProps['style']; - let transFromStyleProps: LooseElementProps['style']; - - const enterFrom = styleOpt.enterFrom; - if (isInit && enterFrom) { - const enterFromKeys = keys(enterFrom); - !transFromStyleProps && (transFromStyleProps = transFromProps.style = {}); - for (let i = 0; i < enterFromKeys.length; i++) { - const key = enterFromKeys[i]; - // Do not clone, animator will perform that clone. - (transFromStyleProps as any)[key] = enterFrom[key]; - } - } - - if (!isInit && fromElStyle) { - if (styleOpt.transition) { - const transitionKeys = normalizeToArray(styleOpt.transition); - !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 - && indexOf(elOption.transition, '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)[key]) { - const elVal = (fromElStyle as any)[key]; - (transFromStyleProps as any)[key] = elVal; - } - } - } - } - } - - const leaveTo = styleOpt.leaveTo; - if (leaveTo) { - const leaveToKeys = keys(leaveTo); - const leaveToProps = getOrCreateLeaveToPropsFromEl(fromEl); - const leaveToStyleProps = leaveToProps.style || (leaveToProps.style = {}); - for (let i = 0; i < leaveToKeys.length; i++) { - const key = leaveToKeys[i]; - (leaveToStyleProps as any)[key] = leaveTo[key]; - } - } -} - -let checkNonStyleTansitionRefer: (propName: string, optVal: unknown, elVal: unknown) => void; -if (__DEV__) { - checkNonStyleTansitionRefer = function (propName: string, optVal: unknown, elVal: unknown): void { - if (!isArrayLike(optVal)) { - assert( - optVal != null && isFinite(optVal as number), - 'Prop `' + propName + '` must refer to a finite number or ArrayLike for transition.' - ); - } - else { - // Try not to copy array for performance, but if user use the same object in different - // call of `renderItem`, it will casue animation transition fail. - assert( - optVal !== elVal, - 'Prop `' + propName + '` must use different Array object each time for transition.' - ); - } - }; -} - -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 { - assert( - hasOwn(TRANSFORM_PROPS, key), - 'Prop `' + key + '` is not a permitted in `' + usedIn + '`. ' - + 'Only `' + keys(TRANSFORM_PROPS).join('`, `') + '` are permitted.' - ); - }; -} - -function getOrCreateLeaveToPropsFromEl(el: Element): LooseElementProps { - const innerEl = customInnerStore(el); - return innerEl.leaveToProps || (innerEl.leaveToProps = {}); -} - diff --git a/src/component/graphic/GraphicModel.ts b/src/component/graphic/GraphicModel.ts index e0d8f884ea..5131597769 100644 --- a/src/component/graphic/GraphicModel.ts +++ b/src/component/graphic/GraphicModel.ts @@ -25,7 +25,6 @@ import { Dictionary, ZRStyleProps, OptionId, - OptionPreprocessor, CommonTooltipOption } from '../../util/types'; import ComponentModel from '../../model/Component'; @@ -228,7 +227,6 @@ function mergeNewElOptionToExist( const $action = newElOption.$action || 'merge'; if ($action === 'merge') { if (existElOption) { - if (__DEV__) { const newType = newElOption.type; zrUtil.assert( From fb3daa222758e1fcb300126a4c302f3f27a014c7 Mon Sep 17 00:00:00 2001 From: pissang Date: Thu, 18 Nov 2021 23:26:30 +0800 Subject: [PATCH 03/35] feat(graphic): add transition api --- .../customGraphicTransitionHelper.ts | 28 ++-- src/chart/custom/CustomSeries.ts | 20 +-- src/chart/custom/CustomView.ts | 10 +- src/component/graphic/GraphicModel.ts | 24 ++-- src/component/graphic/GraphicView.ts | 58 +++++--- test/graphic-transition.html | 124 ++++++++++++++++++ 6 files changed, 207 insertions(+), 57 deletions(-) create mode 100644 test/graphic-transition.html diff --git a/src/animation/customGraphicTransitionHelper.ts b/src/animation/customGraphicTransitionHelper.ts index 45a5938da2..6ae21065ff 100644 --- a/src/animation/customGraphicTransitionHelper.ts +++ b/src/animation/customGraphicTransitionHelper.ts @@ -42,13 +42,13 @@ const LEGACY_TRANSFORM_PROPS = { type LegacyTransformProp = keyof typeof LEGACY_TRANSFORM_PROPS; export type CustomTransitionProps = string | string[]; -export interface ElementTransitionOptionMixin { +export interface TransitionOptionMixin { transition?: CustomTransitionProps; enterFrom?: Dictionary; leaveTo?: Dictionary; }; -export type ElementTransformTransitionOptionMixin = { +export type ElementTransitionOptionMixin = { transition?: ElementRootTransitionProp | ElementRootTransitionProp[]; enterFrom?: Dictionary; leaveTo?: Dictionary; @@ -72,16 +72,16 @@ interface LooseElementProps extends ElementProps { } type TransitionElementOption = Partial> & { - shape?: Dictionary & ElementTransitionOptionMixin - style?: PathStyleProps & ElementTransitionOptionMixin - extra?: Dictionary & ElementTransitionOptionMixin + shape?: Dictionary & TransitionOptionMixin + style?: PathStyleProps & TransitionOptionMixin + extra?: Dictionary & TransitionOptionMixin invisible?: boolean silent?: boolean autoBatch?: boolean ignore?: boolean during?: (params: TransitionDuringAPI) => void -} & ElementTransformTransitionOptionMixin; +} & ElementTransitionOptionMixin; const transitionInnerStore = makeInner<{ leaveToProps: ElementProps; @@ -141,7 +141,7 @@ export interface TransitionDuringAPI< getStyle(key: T): StyleOpt[T]; }; -export function updateTransition( +export function applyUpdateTransition( el: Element, elOption: TransitionElementOption, animatableModel?: Model, @@ -171,20 +171,22 @@ export function updateTransition( styleOpt ? el.dirty() : el.markRedraw(); } -export function leaveTransition( +export function applyLeaveTransition( el: Element, - seriesModel: Model + animatableModel: Model, + onRemove?: () => void ): void { if (el) { const parent = el.parent; const leaveToProps = transitionInnerStore(el).leaveToProps; leaveToProps - ? updateProps(el, leaveToProps, seriesModel, { + ? updateProps(el, leaveToProps, animatableModel, { cb: function () { parent.remove(el); + onRemove && onRemove(); } }) - : parent.remove(el); + : (parent.remove(el), onRemove && onRemove()); } } @@ -433,7 +435,7 @@ function prepareShapeOrExtraTransitionFrom( isInit: boolean ): void { - const attrOpt: Dictionary & ElementTransitionOptionMixin = (elOption as any)[mainAttr]; + const attrOpt: Dictionary & TransitionOptionMixin = (elOption as any)[mainAttr]; if (!attrOpt) { return; } @@ -498,7 +500,7 @@ function prepareShapeOrExtraAllPropsFinal( elOption: TransitionElementOption, allProps: LooseElementProps ): void { - const attrOpt: Dictionary & ElementTransitionOptionMixin = (elOption as any)[mainAttr]; + const attrOpt: Dictionary & TransitionOptionMixin = (elOption as any)[mainAttr]; if (!attrOpt) { return; } diff --git a/src/chart/custom/CustomSeries.ts b/src/chart/custom/CustomSeries.ts index cf72658c0e..3ef34d0ec3 100644 --- a/src/chart/custom/CustomSeries.ts +++ b/src/chart/custom/CustomSeries.ts @@ -65,8 +65,8 @@ import { } from '../../util/graphic'; import { TextStyleProps } from 'zrender/src/graphic/Text'; import { - ElementTransformTransitionOptionMixin, ElementTransitionOptionMixin, + TransitionOptionMixin, TransitionBaseDuringAPI, TransitionDuringAPI } from '../../animation/customGraphicTransitionHelper'; @@ -104,7 +104,7 @@ type ShapeMorphingOption = { export interface CustomBaseElementOption extends Partial>, ElementTransformTransitionOptionMixin { +>>, ElementTransitionOptionMixin { // element type, required. type: string; id?: string; @@ -116,7 +116,7 @@ export interface CustomBaseElementOption extends Partial & ElementTransitionOptionMixin; + extra?: Dictionary & TransitionOptionMixin; // updateDuringAnimation during?(params: TransitionBaseDuringAPI): void; }; @@ -124,7 +124,7 @@ export interface CustomBaseElementOption extends Partial> { - style?: ZRStyleProps & ElementTransitionOptionMixin; + style?: ZRStyleProps & TransitionOptionMixin; during?(params: TransitionDuringAPI): void; /** * @deprecated @@ -139,7 +139,7 @@ export interface CustomDisplayableOptionOnState extends Partial> { // `false` means remove emphasis trigger. - style?: (ZRStyleProps & ElementTransitionOptionMixin) | false; + style?: (ZRStyleProps & TransitionOptionMixin) | false; during?(params: TransitionDuringAPI): void; @@ -156,7 +156,7 @@ export interface CustomGroupOption extends CustomBaseElementOption { export interface CustomBaseZRPathOption extends CustomDisplayableOption, ShapeMorphingOption { autoBatch?: boolean; - shape?: T & ElementTransitionOptionMixin; + shape?: T & TransitionOptionMixin; style?: PathProps['style'] during?(params: TransitionDuringAPI): void; } @@ -201,22 +201,22 @@ export type CustomPathOption = CreateCustomBuitinPathOption | CustomSVGPathOption; export interface CustomImageOptionOnState extends CustomDisplayableOptionOnState { - style?: ImageStyleProps & ElementTransitionOptionMixin; + style?: ImageStyleProps & TransitionOptionMixin; } export interface CustomImageOption extends CustomDisplayableOption { type: 'image'; - style?: ImageStyleProps & ElementTransitionOptionMixin; + style?: ImageStyleProps & TransitionOptionMixin; emphasis?: CustomImageOptionOnState; blur?: CustomImageOptionOnState; select?: CustomImageOptionOnState; } export interface CustomTextOptionOnState extends CustomDisplayableOptionOnState { - style?: TextStyleProps & ElementTransitionOptionMixin; + style?: TextStyleProps & TransitionOptionMixin; } export interface CustomTextOption extends CustomDisplayableOption { type: 'text'; - style?: TextStyleProps & ElementTransitionOptionMixin; + style?: TextStyleProps & TransitionOptionMixin; emphasis?: CustomTextOptionOnState; blur?: CustomTextOptionOnState; select?: CustomTextOptionOnState; diff --git a/src/chart/custom/CustomView.ts b/src/chart/custom/CustomView.ts index 2621341964..3dcf328cb0 100644 --- a/src/chart/custom/CustomView.ts +++ b/src/chart/custom/CustomView.ts @@ -90,7 +90,7 @@ import CustomSeriesModel, { } from './CustomSeries'; import { PatternObject } from 'zrender/src/graphic/Pattern'; import { CustomSeriesOption } from '../../export/option'; -import { leaveTransition, updateTransition } from '../../animation/customGraphicTransitionHelper'; +import { applyLeaveTransition, applyUpdateTransition } from '../../animation/customGraphicTransitionHelper'; const EMPHASIS = 'emphasis' as const; const NORMAL = 'normal' as const; @@ -218,7 +218,7 @@ export default class CustomChartView extends ChartView { ); }) .remove(function (oldIdx) { - leaveTransition(oldData.getItemGraphicEl(oldIdx), customSeries); + applyLeaveTransition(oldData.getItemGraphicEl(oldIdx), customSeries); }) .update(function (newIdx, oldIdx) { const oldEl = oldData.getItemGraphicEl(oldIdx); @@ -475,7 +475,7 @@ function updateElNormal( } } - updateTransition(el, elOption, seriesModel, dataIndex, isInit); + applyUpdateTransition(el, elOption, seriesModel, dataIndex, isInit); if (!isTextContent) { @@ -1310,7 +1310,7 @@ function mergeChildren( // Do not supprot leave elements that are not mentioned in the latest // `renderItem` return. Otherwise users may not have a clear and simple // concept that how to contorl all of the elements. - leaveTransition(el.childAt(i), seriesModel); + applyLeaveTransition(el.childAt(i), seriesModel); } } @@ -1364,7 +1364,7 @@ function processAddUpdate( function processRemove(this: DataDiffer, oldIndex: number): void { const context = this.context; const child = context.oldChildren[oldIndex]; - leaveTransition(child, context.seriesModel); + applyLeaveTransition(child, context.seriesModel); } /** diff --git a/src/component/graphic/GraphicModel.ts b/src/component/graphic/GraphicModel.ts index 5131597769..92f0160564 100644 --- a/src/component/graphic/GraphicModel.ts +++ b/src/component/graphic/GraphicModel.ts @@ -25,7 +25,8 @@ import { Dictionary, ZRStyleProps, OptionId, - CommonTooltipOption + CommonTooltipOption, + AnimationOptionMixin } from '../../util/types'; import ComponentModel from '../../model/Component'; import Element, { ElementTextConfig } from 'zrender/src/Element'; @@ -35,6 +36,7 @@ import { ImageStyleProps } from 'zrender/src/graphic/Image'; import GlobalModel from '../../model/Global'; import { TextStyleProps } from 'zrender/src/graphic/Text'; import { copyLayoutParams, mergeLayoutParam } from '../../util/layout'; +import { ElementTransitionOptionMixin, TransitionOptionMixin } from '../../animation/customGraphicTransitionHelper'; interface GraphicComponentBaseElementOption extends Partial> { + GraphicComponentBaseElementOption, + ElementTransitionOptionMixin, + Partial> { - style?: ZRStyleProps; + style?: ZRStyleProps & TransitionOptionMixin } // TODO: states? // interface GraphicComponentDisplayableOptionOnState extends Partial> { // style?: ZRStyleProps; // } -export interface GraphicComponentGroupOption extends GraphicComponentBaseElementOption { +export interface GraphicComponentGroupOption + extends GraphicComponentBaseElementOption, ElementTransitionOptionMixin { type?: 'group'; /** @@ -141,13 +146,13 @@ export interface GraphicComponentGroupOption extends GraphicComponentBaseElement // TODO: Can only set focus, blur on the root element. // children: Omit[]; children: GraphicComponentElementOption[]; -} +}; export interface GraphicComponentZRPathOption extends GraphicComponentDisplayableOption { - shape?: PathProps['shape']; + shape?: PathProps['shape'] & TransitionOptionMixin; } export interface GraphicComponentImageOption extends GraphicComponentDisplayableOption { type?: 'image'; - style?: ImageStyleProps; + style?: ImageStyleProps & TransitionOptionMixin; } // TODO: states? // interface GraphicComponentImageOptionOnState extends GraphicComponentDisplayableOptionOnState { @@ -174,11 +179,10 @@ export type GraphicComponentLooseOption = (GraphicComponentOption | GraphicCompo mainType?: 'graphic'; }; -export interface GraphicComponentOption extends ComponentOption { +export interface GraphicComponentOption extends ComponentOption, AnimationOptionMixin { // Note: elements is always behind its ancestors in this elements array. elements?: GraphicComponentElementOption[]; -} -; +}; export function setKeyInfoToNewElOption( resultItem: ReturnType[number], diff --git a/src/component/graphic/GraphicView.ts b/src/component/graphic/GraphicView.ts index 8d3f5c2a4b..e6f5f1528f 100644 --- a/src/component/graphic/GraphicView.ts +++ b/src/component/graphic/GraphicView.ts @@ -37,8 +37,9 @@ import { GraphicComponentGroupOption, GraphicComponentElementOption } from './GraphicModel'; +import { applyLeaveTransition, applyUpdateTransition } from '../../animation/customGraphicTransitionHelper'; -const _nonShapeGraphicElements = { +const nonShapeGraphicElements = { // Reserved but not supported in graphic component. path: null as unknown, compoundPath: null as unknown, @@ -48,7 +49,7 @@ const _nonShapeGraphicElements = { image: graphicUtil.Image, text: graphicUtil.Text } as const; -type NonShapeGraphicElementType = keyof typeof _nonShapeGraphicElements; +type NonShapeGraphicElementType = keyof typeof nonShapeGraphicElements; export const inner = modelUtil.makeInner<{ widthOption: number; @@ -152,16 +153,34 @@ export class GraphicComponentView extends ComponentView { const $action = elOption.$action || 'merge'; if ($action === 'merge') { - elExisting - ? elExisting.attr(elOptionCleaned) - : createEl(id, targetElParent, elOptionCleaned, elMap); + const isInit = !elExisting; + let el = elExisting; + if (isInit) { + el = createEl(id, targetElParent, elOption.type, elMap); + } + el && applyUpdateTransition( + el, + elOptionCleaned, + graphicModel, + 0, // TODO Fixed dataIndex to be 0 + isInit + ); } else if ($action === 'replace') { - removeEl(elExisting, elMap); - createEl(id, targetElParent, elOptionCleaned, elMap); + removeEl(elExisting, elMap, graphicModel); + const el = createEl(id, targetElParent, elOption.type, elMap); + if (el) { + applyUpdateTransition( + el, + elOptionCleaned, + graphicModel, + 0, // TODO Fixed dataIndex to be 0 + true + ); + } } else if ($action === 'remove') { - removeEl(elExisting, elMap); + removeEl(elExisting, elMap, graphicModel); } const el = elMap.get(id); @@ -266,8 +285,8 @@ export class GraphicComponentView extends ComponentView { */ private _clear(): void { const elMap = this._elMap; - elMap.each(function (el) { - removeEl(el, elMap); + elMap.each((el) => { + removeEl(el, elMap, this._lastGraphicModel); }); this._elMap = zrUtil.createHashMap(); } @@ -279,20 +298,19 @@ export class GraphicComponentView extends ComponentView { function createEl( id: string, targetElParent: graphicUtil.Group, - elOption: GraphicComponentElementOption, + graphicType: string, elMap: ElementMap -): void { - const graphicType = elOption.type; +): Element { if (__DEV__) { zrUtil.assert(graphicType, 'graphic type MUST be set'); } const Clz = ( - zrUtil.hasOwn(_nonShapeGraphicElements, graphicType) + zrUtil.hasOwn(nonShapeGraphicElements, graphicType) // Those graphic elements are not shapes. They should not be // overwritten by users, so do them first. - ? _nonShapeGraphicElements[graphicType as NonShapeGraphicElementType] + ? nonShapeGraphicElements[graphicType as NonShapeGraphicElementType] : graphicUtil.getShapeClass(graphicType) ) as { new(opt: GraphicComponentElementOption): Element; }; @@ -300,19 +318,21 @@ function createEl( zrUtil.assert(Clz, 'graphic type can not be found'); } - const el = new Clz(elOption); + const el = new Clz({}); targetElParent.add(el); elMap.set(id, el); inner(el).id = id; + + return el; } -function removeEl(elExisting: Element, elMap: ElementMap): void { +function removeEl(elExisting: Element, elMap: ElementMap, graphicModel: GraphicComponentModel): void { const existElParent = elExisting && elExisting.parent; if (existElParent) { elExisting.type === 'group' && elExisting.traverse(function (el) { - removeEl(el, elMap); + removeEl(el, elMap, graphicModel); }); + applyLeaveTransition(elExisting, graphicModel); elMap.removeKey(inner(elExisting).id); - existElParent.remove(elExisting); } } // Remove unnecessary props to avoid potential problems. diff --git a/test/graphic-transition.html b/test/graphic-transition.html new file mode 100644 index 0000000000..20bd92c8ba --- /dev/null +++ b/test/graphic-transition.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + From 4501552ee06efa6daf3a47fba10a6eead67ca79d Mon Sep 17 00:00:00 2001 From: pissang Date: Fri, 19 Nov 2021 10:36:30 +0800 Subject: [PATCH 04/35] refact(transition): rename modules --- ...tomGraphicTransitionHelper.ts => customGraphicTransition.ts} | 0 src/chart/custom/CustomSeries.ts | 2 +- src/chart/custom/CustomView.ts | 2 +- src/component/graphic/GraphicModel.ts | 2 +- src/component/graphic/GraphicView.ts | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename src/animation/{customGraphicTransitionHelper.ts => customGraphicTransition.ts} (100%) diff --git a/src/animation/customGraphicTransitionHelper.ts b/src/animation/customGraphicTransition.ts similarity index 100% rename from src/animation/customGraphicTransitionHelper.ts rename to src/animation/customGraphicTransition.ts diff --git a/src/chart/custom/CustomSeries.ts b/src/chart/custom/CustomSeries.ts index 3ef34d0ec3..3faaa85b23 100644 --- a/src/chart/custom/CustomSeries.ts +++ b/src/chart/custom/CustomSeries.ts @@ -69,7 +69,7 @@ import { TransitionOptionMixin, TransitionBaseDuringAPI, TransitionDuringAPI -} from '../../animation/customGraphicTransitionHelper'; +} from '../../animation/customGraphicTransition'; import { TransformProp } from 'zrender/lib/core/Transformable'; export type CustomExtraElementInfo = Dictionary; diff --git a/src/chart/custom/CustomView.ts b/src/chart/custom/CustomView.ts index 3dcf328cb0..2d80864723 100644 --- a/src/chart/custom/CustomView.ts +++ b/src/chart/custom/CustomView.ts @@ -90,7 +90,7 @@ import CustomSeriesModel, { } from './CustomSeries'; import { PatternObject } from 'zrender/src/graphic/Pattern'; import { CustomSeriesOption } from '../../export/option'; -import { applyLeaveTransition, applyUpdateTransition } from '../../animation/customGraphicTransitionHelper'; +import { applyLeaveTransition, applyUpdateTransition } from '../../animation/customGraphicTransition'; const EMPHASIS = 'emphasis' as const; const NORMAL = 'normal' as const; diff --git a/src/component/graphic/GraphicModel.ts b/src/component/graphic/GraphicModel.ts index 92f0160564..8b14ea9e57 100644 --- a/src/component/graphic/GraphicModel.ts +++ b/src/component/graphic/GraphicModel.ts @@ -36,7 +36,7 @@ import { ImageStyleProps } from 'zrender/src/graphic/Image'; import GlobalModel from '../../model/Global'; import { TextStyleProps } from 'zrender/src/graphic/Text'; import { copyLayoutParams, mergeLayoutParam } from '../../util/layout'; -import { ElementTransitionOptionMixin, TransitionOptionMixin } from '../../animation/customGraphicTransitionHelper'; +import { ElementTransitionOptionMixin, TransitionOptionMixin } from '../../animation/customGraphicTransition'; interface GraphicComponentBaseElementOption extends Partial Date: Fri, 19 Nov 2021 11:33:24 +0800 Subject: [PATCH 05/35] fix(transition): not do strict checking. some code cleanup --- src/animation/customGraphicTransition.ts | 123 ++++++++--------------- src/chart/custom/CustomView.ts | 8 +- src/component/graphic/GraphicModel.ts | 1 + src/component/graphic/GraphicView.ts | 42 ++++++-- test/graphic-transition.html | 39 +++++++ 5 files changed, 118 insertions(+), 95 deletions(-) diff --git a/src/animation/customGraphicTransition.ts b/src/animation/customGraphicTransition.ts index 6ae21065ff..7459f538df 100644 --- a/src/animation/customGraphicTransition.ts +++ b/src/animation/customGraphicTransition.ts @@ -34,12 +34,26 @@ import { AnimationOptionMixin, ZRStyleProps } from '../util/types'; import { Dictionary } from 'zrender/lib/core/types'; import { PathStyleProps } from 'zrender'; -const LEGACY_TRANSFORM_PROPS = { +const LEGACY_TRANSFORM_PROPS_MAP = { position: ['x', 'y'], scale: ['scaleX', 'scaleY'], origin: ['originX', 'originY'] } as const; -type LegacyTransformProp = keyof typeof LEGACY_TRANSFORM_PROPS; +const LEGACY_TRANSFORM_PROPS = keys(LEGACY_TRANSFORM_PROPS_MAP); +type LegacyTransformProp = keyof typeof LEGACY_TRANSFORM_PROPS_MAP; + +const TRANSFORM_PROPS_MAP = { + x: 1, + y: 1, + scaleX: 1, + scaleY: 1, + originX: 1, + originY: 1, + rotation: 1 +} as const; +type TransformProp = keyof typeof TRANSFORM_PROPS_MAP; +const TRANSFORM_PROPS = keys(TRANSFORM_PROPS_MAP); +const transformPropNamesStr = TRANSFORM_PROPS.join(', '); export type CustomTransitionProps = string | string[]; export interface TransitionOptionMixin { @@ -55,17 +69,6 @@ export type ElementTransitionOptionMixin = { }; type ElementRootTransitionProp = TransformProp | 'shape' | 'extra' | 'style'; -export const TRANSFORM_PROPS = { - x: 1, - y: 1, - scaleX: 1, - scaleY: 1, - originX: 1, - originY: 1, - rotation: 1 -} as const; -export type TransformProp = keyof typeof TRANSFORM_PROPS; - interface LooseElementProps extends ElementProps { style?: ZRStyleProps; shape?: Dictionary; @@ -88,30 +91,6 @@ const transitionInnerStore = makeInner<{ userDuring: (params: TransitionDuringAPI) => void; }, Element>(); -const transformPropNamesStr = keys(TRANSFORM_PROPS).join(', '); - -function setLegacyTransformProp( - elOption: TransitionElementOption, - targetProps: Partial>, - legacyName: LegacyTransformProp -): void { - const legacyArr = (elOption as any)[legacyName]; - const xyName = LEGACY_TRANSFORM_PROPS[legacyName]; - if (legacyArr) { - targetProps[xyName[0]] = legacyArr[0]; - targetProps[xyName[1]] = legacyArr[1]; - } -} - -function setTransformProp( - elOption: TransitionElementOption, - allProps: Partial>, - name: TransformProp -): void { - if (elOption[name] != null) { - allProps[name] = elOption[name]; - } -} function setTransformPropToTransitionFrom( transitionFrom: Partial>, @@ -158,10 +137,13 @@ export function applyUpdateTransition( prepareShapeOrExtraTransitionFrom('shape', el, elOption, transFromProps, isInit); prepareShapeOrExtraAllPropsFinal('shape', elOption, propsToSet); + prepareTransformTransitionFrom(el, elOption, transFromProps, isInit); prepareTransformAllPropsFinal(el, elOption, propsToSet); + prepareShapeOrExtraTransitionFrom('extra', el, elOption, transFromProps, isInit); prepareShapeOrExtraAllPropsFinal('extra', elOption, propsToSet); + prepareStyleTransitionFrom(el, elOption, styleOpt, transFromProps, isInit); (propsToSet as DisplayableProps).style = styleOpt; applyPropsDirectly(el, propsToSet); @@ -291,14 +273,14 @@ 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, key), 'Only ' + transformPropNamesStr + ' available in `setTransform`.'); + 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, key), 'Only ' + transformPropNamesStr + ' available in `getTransform`.'); + assert(hasOwn(TRANSFORM_PROPS_MAP, key), 'Only ' + transformPropNamesStr + ' available in `getTransform`.'); } return tmpDuringScope.el[key]; }, @@ -463,10 +445,6 @@ function prepareShapeOrExtraTransitionFrom( for (let i = 0; i < transitionKeys.length; i++) { const key = transitionKeys[i]; const elVal = elPropsInAttr[key]; - if (__DEV__) { - checkNonStyleTansitionRefer(key, (attrOpt as any)[key], elVal); - } - // Do not clone, see `checkNonStyleTansitionRefer`. transFromPropsInAttr[key] = elVal; } } @@ -544,9 +522,8 @@ function prepareTransformTransitionFrom( const elVal = el[key]; if (__DEV__) { checkTransformPropRefer(key, 'el.transition'); - checkNonStyleTansitionRefer(key, elOption[key], elVal); } - // Do not clone, see `checkNonStyleTansitionRefer`. + // Do not clone, animator will perform that clone. transFromProps[key] = elVal; } } @@ -576,17 +553,22 @@ function prepareTransformAllPropsFinal( elOption: TransitionElementOption, allProps: ElementProps ): void { - setLegacyTransformProp(elOption, allProps, 'position'); - setLegacyTransformProp(elOption, allProps, 'scale'); - setLegacyTransformProp(elOption, allProps, 'origin'); - - setTransformProp(elOption, allProps, 'x'); - setTransformProp(elOption, allProps, 'y'); - setTransformProp(elOption, allProps, 'scaleX'); - setTransformProp(elOption, allProps, 'scaleY'); - setTransformProp(elOption, allProps, 'originX'); - setTransformProp(elOption, allProps, 'originY'); - setTransformProp(elOption, allProps, 'rotation'); + 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 < TRANSFORM_PROPS.length; i++) { + const key = TRANSFORM_PROPS[i]; + if (elOption[key] != null) { + allProps[key] = elOption[key]; + } + } } function prepareStyleTransitionFrom( @@ -657,26 +639,6 @@ function prepareStyleTransitionFrom( } } -let checkNonStyleTansitionRefer: (propName: string, optVal: unknown, elVal: unknown) => void; -if (__DEV__) { - checkNonStyleTansitionRefer = function (propName: string, optVal: unknown, elVal: unknown): void { - if (!isArrayLike(optVal)) { - assert( - optVal != null && isFinite(optVal as number), - 'Prop `' + propName + '` must refer to a finite number or ArrayLike for transition.' - ); - } - else { - // Try not to copy array for performance, but if user use the same object in different - // call of `renderItem`, it will casue animation transition fail. - assert( - optVal !== elVal, - 'Prop `' + propName + '` must use different Array object each time for transition.' - ); - } - }; -} - function isNonStyleTransitionEnabled(optVal: unknown, elVal: unknown): boolean { // The same as `checkNonStyleTansitionRefer`. return !isArrayLike(optVal) @@ -687,11 +649,10 @@ function isNonStyleTransitionEnabled(optVal: unknown, elVal: unknown): boolean { let checkTransformPropRefer: (key: string, usedIn: string) => void; if (__DEV__) { checkTransformPropRefer = function (key: string, usedIn: string): void { - assert( - hasOwn(TRANSFORM_PROPS, key), - 'Prop `' + key + '` is not a permitted in `' + usedIn + '`. ' - + 'Only `' + keys(TRANSFORM_PROPS).join('`, `') + '` are permitted.' - ); + if (!hasOwn(TRANSFORM_PROPS_MAP, key)) { + warn('Prop `' + key + '` is not a permitted in `' + usedIn + '`. ' + + 'Only `' + keys(TRANSFORM_PROPS_MAP).join('`, `') + '` are permitted.'); + } }; } diff --git a/src/chart/custom/CustomView.ts b/src/chart/custom/CustomView.ts index 2d80864723..47f90f1bc9 100644 --- a/src/chart/custom/CustomView.ts +++ b/src/chart/custom/CustomView.ts @@ -491,9 +491,7 @@ function updateElOnState( el: Element, elStateOpt: CustomElementOptionOnState, styleOpt: CustomElementOptionOnState['style'], - attachedTxInfo: AttachedTxInfo, - isRoot: boolean, - isTextContent: boolean + attachedTxInfo: AttachedTxInfo ): void { const elDisplayable = el.isGroup ? null : el as Displayable; const txCfgOpt = attachedTxInfo && attachedTxInfo[state].cfg; @@ -1009,7 +1007,7 @@ function doCreateOrUpdateEl( if (stateName !== NORMAL) { const otherStateOpt = retrieveStateOption(elOption, stateName); const otherStyleOpt = retrieveStyleOptionOnState(elOption, otherStateOpt, stateName); - updateElOnState(stateName, el, otherStateOpt, otherStyleOpt, attachedTxInfoTmp, isRoot, false); + updateElOnState(stateName, el, otherStateOpt, otherStyleOpt, attachedTxInfoTmp); } } @@ -1161,7 +1159,7 @@ function doCreateOrUpdateAttachedTx( textContent, txConOptOtherState, retrieveStyleOptionOnState(txConOptNormal, txConOptOtherState, stateName), - null, false, true + null ); } } diff --git a/src/component/graphic/GraphicModel.ts b/src/component/graphic/GraphicModel.ts index 8b14ea9e57..8c4986991e 100644 --- a/src/component/graphic/GraphicModel.ts +++ b/src/component/graphic/GraphicModel.ts @@ -123,6 +123,7 @@ export interface GraphicComponentDisplayableOption extends Partial> { style?: ZRStyleProps & TransitionOptionMixin + z2?: number } // TODO: states? // interface GraphicComponentDisplayableOptionOnState extends Partial require(['echarts'/*, 'map/js/china' */], function (echarts) { var option; + // Enter transition + }); + + + + + From a3eedb3b29fc1527313e14583ef63999c93cdf00 Mon Sep 17 00:00:00 2001 From: pissang Date: Fri, 19 Nov 2021 14:27:00 +0800 Subject: [PATCH 06/35] feat(transition): support transition to be configured all. --- src/animation/customGraphicTransition.ts | 107 ++++++++--------------- src/chart/custom/CustomView.ts | 48 ++++++++-- src/component/graphic/GraphicModel.ts | 33 +++++++ test/custom-transition2.html | 4 +- test/graphic-transition.html | 68 ++++++++++++-- 5 files changed, 175 insertions(+), 85 deletions(-) diff --git a/src/animation/customGraphicTransition.ts b/src/animation/customGraphicTransition.ts index 7459f538df..90d7dfac6f 100644 --- a/src/animation/customGraphicTransition.ts +++ b/src/animation/customGraphicTransition.ts @@ -18,12 +18,10 @@ */ // Helpers for custom graphic elements in custom series and graphic components. - -import Transformable from 'zrender/src/core/Transformable'; import Element, { ElementProps } from 'zrender/src/Element'; import { makeInner, normalizeToArray } from '../util/model'; -import { assert, bind, eqNaN, hasOwn, indexOf, isArrayLike, keys } from 'zrender/src/core/util'; +import { assert, bind, eqNaN, extend, hasOwn, indexOf, isArrayLike, keys } 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'; @@ -40,7 +38,6 @@ const LEGACY_TRANSFORM_PROPS_MAP = { origin: ['originX', 'originY'] } as const; const LEGACY_TRANSFORM_PROPS = keys(LEGACY_TRANSFORM_PROPS_MAP); -type LegacyTransformProp = keyof typeof LEGACY_TRANSFORM_PROPS_MAP; const TRANSFORM_PROPS_MAP = { x: 1, @@ -55,19 +52,20 @@ type TransformProp = keyof typeof TRANSFORM_PROPS_MAP; const TRANSFORM_PROPS = keys(TRANSFORM_PROPS_MAP); const transformPropNamesStr = TRANSFORM_PROPS.join(', '); -export type CustomTransitionProps = string | string[]; +export type TransitionProps = string | string[]; +export type ElementRootTransitionProp = TransformProp | 'shape' | 'extra' | 'style'; + export interface TransitionOptionMixin { - transition?: CustomTransitionProps; + transition?: TransitionProps | 'all'; enterFrom?: Dictionary; leaveTo?: Dictionary; }; export type ElementTransitionOptionMixin = { - transition?: ElementRootTransitionProp | ElementRootTransitionProp[]; + transition?: ElementRootTransitionProp | ElementRootTransitionProp[] | 'all'; enterFrom?: Dictionary; leaveTo?: Dictionary; }; -type ElementRootTransitionProp = TransformProp | 'shape' | 'extra' | 'style'; interface LooseElementProps extends ElementProps { style?: ZRStyleProps; @@ -91,18 +89,6 @@ const transitionInnerStore = makeInner<{ userDuring: (params: TransitionDuringAPI) => void; }, Element>(); - -function setTransformPropToTransitionFrom( - transitionFrom: Partial>, - name: TransformProp, - fromTransformable?: Transformable // If provided, retrieve from the element. -): void { - if (fromTransformable) { - transitionFrom[name] = fromTransformable[name]; - } -} - - export interface TransitionBaseDuringAPI { // Usually other props do not need to be changed in animation during. setTransform(key: TransformProp, val: number): this @@ -182,32 +168,7 @@ function applyPropsDirectly( const styleOpt = (allPropsFinal as Displayable).style; if (elDisplayable && styleOpt) { - - // PENDING: here the input style object is used directly. - // Good for performance but bad for compatibility control. - elDisplayable.useStyle(styleOpt); - // 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 = elDisplayable.animators; - for (let i = 0; i < animators.length; i++) { - const animator = animators[i]; - // targetName is the "topKey". - if (animator.targetName === 'style') { - animator.changeTarget(elDisplayable.style); - } - } + elDisplayable.setStyle(styleOpt); } if (allPropsFinal) { @@ -438,17 +399,25 @@ function prepareShapeOrExtraTransitionFrom( } } + if (!isInit && elPropsInAttr) { - if (attrOpt.transition) { + const transition = elOption.transition; + const attrTransition = attrOpt.transition; + if (attrTransition) { !transFromPropsInAttr && (transFromPropsInAttr = transFromProps[mainAttr] = {}); - const transitionKeys = normalizeToArray(attrOpt.transition); - for (let i = 0; i < transitionKeys.length; i++) { - const key = transitionKeys[i]; - const elVal = elPropsInAttr[key]; - transFromPropsInAttr[key] = elVal; + if (attrTransition === 'all') { + 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 (indexOf(elOption.transition, mainAttr) >= 0) { + else if (transition === 'all' || indexOf(transition, mainAttr) >= 0) { !transFromPropsInAttr && (transFromPropsInAttr = transFromProps[mainAttr] = {}); const elPropsInAttrKeys = keys(elPropsInAttr); for (let i = 0; i < elPropsInAttrKeys.length; i++) { @@ -512,25 +481,21 @@ function prepareTransformTransitionFrom( } if (!isInit) { - if (elOption.transition) { - const transitionKeys = normalizeToArray(elOption.transition); - for (let i = 0; i < transitionKeys.length; i++) { - const key = transitionKeys[i]; - if (key === 'style' || key === 'shape' || key === 'extra') { - continue; - } - const elVal = el[key]; - if (__DEV__) { - checkTransformPropRefer(key, 'el.transition'); - } - // Do not clone, animator will perform that clone. - transFromProps[key] = elVal; + const transition = elOption.transition; + const transitionKeys = transition === 'all' + ? TRANSFORM_PROPS + : normalizeToArray(transition || []); + for (let i = 0; i < transitionKeys.length; i++) { + const key = transitionKeys[i]; + if (key === 'style' || key === 'shape' || key === 'extra') { + continue; } - } - // This default transition see [STRATEGY_TRANSITION] - else { - setTransformPropToTransitionFrom(transFromProps, 'x', el); - setTransformPropToTransitionFrom(transFromProps, 'y', el); + const elVal = el[key]; + if (__DEV__) { + checkTransformPropRefer(key, 'el.transition'); + } + // Do not clone, animator will perform that clone. + transFromProps[key] = elVal; } } diff --git a/src/chart/custom/CustomView.ts b/src/chart/custom/CustomView.ts index 47f90f1bc9..0024c825cc 100644 --- a/src/chart/custom/CustomView.ts +++ b/src/chart/custom/CustomView.ts @@ -90,7 +90,11 @@ import CustomSeriesModel, { } from './CustomSeries'; import { PatternObject } from 'zrender/src/graphic/Pattern'; import { CustomSeriesOption } from '../../export/option'; -import { applyLeaveTransition, applyUpdateTransition } from '../../animation/customGraphicTransition'; +import { + applyLeaveTransition, + applyUpdateTransition, + ElementRootTransitionProp +} from '../../animation/customGraphicTransition'; const EMPHASIS = 'emphasis' as const; const NORMAL = 'normal' as const; @@ -109,6 +113,7 @@ const PATH_LABEL = { blur: [BLUR, 'label'], select: [SELECT, 'label'] } as const; +const DEFAULT_TRANSITION: ElementRootTransitionProp[] = ['x', 'y']; // Use prefix to avoid index to be the same as el.name, // which will cause weird update animation. const GROUP_DIFF_PREFIX = 'e\0\0'; @@ -443,6 +448,11 @@ function updateElNormal( el.setTextConfig(txCfgOpt); } + // Default transition ['x', 'y'] + if (elOption && elOption.transition == null) { + elOption.transition = DEFAULT_TRANSITION; + } + // Do some normalization on style. const styleOpt = elOption && (elOption as CustomDisplayableOption).style; @@ -468,10 +478,38 @@ function updateElNormal( (styleOpt as InnerCustomZRPathOptionStyle).__decalPattern = decalPattern; } - if (isDisplayable(el) && styleOpt) { - const decalPattern = (styleOpt as InnerCustomZRPathOptionStyle).__decalPattern; - if (decalPattern) { - (styleOpt as PathStyleProps).decal = decalPattern; + if (isDisplayable(el)) { + if (styleOpt) { + const decalPattern = (styleOpt as InnerCustomZRPathOptionStyle).__decalPattern; + if (decalPattern) { + (styleOpt as PathStyleProps).decal = decalPattern; + } + } + + // Clear style + el.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.style); + } } } diff --git a/src/component/graphic/GraphicModel.ts b/src/component/graphic/GraphicModel.ts index 8c4986991e..2a64d74f3c 100644 --- a/src/component/graphic/GraphicModel.ts +++ b/src/component/graphic/GraphicModel.ts @@ -210,6 +210,7 @@ export function setKeyInfoToNewElOption( newElOption.parentOption = null; } +const TRANSITION_PROPS = ['transition', 'enterFrom', 'leaveTo'] as const; function isSetLoc( obj: GraphicComponentElementOption, props: ('left' | 'right' | 'top' | 'bottom')[] @@ -247,6 +248,13 @@ function mergeNewElOptionToExist( mergeLayoutParam(existElOption, newElOptCopy, { ignoreSize: true }); // Will be used in render. copyLayoutParams(newElOption, existElOption); + + // Copy transition info to new option so it can be used in the transition. + // DO IT AFTER merge + copyTransitionInfo(newElOption, existElOption); + copyTransitionInfo(newElOption, existElOption, 'shape'); + copyTransitionInfo(newElOption, existElOption, 'style'); + copyTransitionInfo(newElOption, existElOption, 'extra'); } else { existList[index] = newElOptCopy; @@ -261,6 +269,31 @@ function mergeNewElOptionToExist( } } +function copyTransitionInfo( + target: GraphicComponentElementOption, source: GraphicComponentElementOption, targetProp?: string +) { + if (targetProp) { + if (!(target as any)[targetProp] + && (source as any)[targetProp] + ) { + // TODO avoid creating this empty object when there is no transition configuration. + (target as any)[targetProp] = {}; + } + target = (target as any)[targetProp]; + source = (source as any)[targetProp]; + } + if (!target || !source) { + return; + } + + for (let i = 0; i < TRANSITION_PROPS.length; i++) { + const prop = TRANSITION_PROPS[i]; + if (target[prop] == null && source[prop] != null) { + (target as any)[prop] = source[prop]; + } + } +} + function setLayoutInfoToExist( existItem: GraphicComponentElementOption, newElOption: GraphicComponentElementOption diff --git a/test/custom-transition2.html b/test/custom-transition2.html index 7c03dad566..e28095d056 100644 --- a/test/custom-transition2.html +++ b/test/custom-transition2.html @@ -214,7 +214,7 @@ var textOpt = { type: 'text', extra: { }, - transition: [], // disable the default transition of x y. + // transition: [], // disable the default transition of x y. style: { x: 20, y: 20, fontSize: 20, stroke: 'green' }, during: function (apiDuring) { var x = apiDuring.getExtra('x'); @@ -227,7 +227,7 @@ shape: { cx: 0, cy: 0, r: 10 }, extra: { }, style: { fill: 'red' }, - transition: [], // disable the default transition of x y. + // transition: [], // disable the default transition of x y. during: function (apiDuring) { var x = apiDuring.getExtra('x'); var y = apiDuring.getExtra('y'); diff --git a/test/graphic-transition.html b/test/graphic-transition.html index b4c8de9c2d..9d80be37a5 100644 --- a/test/graphic-transition.html +++ b/test/graphic-transition.html @@ -51,6 +51,7 @@ type: 'circle', x: 100, y: 50, + transition: ['x', 'y'], shape: { cx: 0, cy: 0, @@ -65,7 +66,7 @@ var chart = testHelper.create(echarts, 'main0', { title: [ - 'Basic transition' + 'Transform transition' ], option: option, buttons: [ @@ -75,8 +76,6 @@ chart.setOption({ graphic: { elements: [{ - type: 'circle', - transition: ['x', 'y'], x: 200 }] } @@ -91,8 +90,6 @@ chart.setOption({ graphic: { elements: [{ - type: 'circle', - transition: ['x', 'y'], y: 200 }] } @@ -106,14 +103,26 @@ chart.setOption({ graphic: { elements: [{ - type: 'circle', - transition: ['x', 'y'], x: 100, y: 50 }] } }) } + }, + + { + text: 'Move to center', + onclick() { + chart.setOption({ + graphic: { + elements: [{ + left: 'center', + top: 'center' + }] + } + }) + } } ] }); @@ -122,6 +131,51 @@ + + From 37c4b1bb6fb24d47f01187e46232619a4b3c56b4 Mon Sep 17 00:00:00 2001 From: pissang Date: Fri, 19 Nov 2021 15:58:06 +0800 Subject: [PATCH 07/35] feat(transition): fix transition in layout params --- src/component/graphic/GraphicView.ts | 35 ++++++++++++++++++++++++++-- src/util/layout.ts | 26 ++++++++++++++------- test/graphic-transition.html | 24 +++++++++++++++++-- 3 files changed, 73 insertions(+), 12 deletions(-) diff --git a/src/component/graphic/GraphicView.ts b/src/component/graphic/GraphicView.ts index 9158cc0342..50dbbf09c8 100644 --- a/src/component/graphic/GraphicView.ts +++ b/src/component/graphic/GraphicView.ts @@ -39,6 +39,7 @@ import { GraphicComponentElementOption } from './GraphicModel'; import { applyLeaveTransition, applyUpdateTransition } from '../../animation/customGraphicTransition'; +import { updateProps } from '../../animation/basicTrasition'; const nonShapeGraphicElements = { // Reserved but not supported in graphic component. @@ -57,6 +58,7 @@ export const inner = modelUtil.makeInner<{ heightOption: number; width: number; height: number; + isNew: boolean; id: string; }, Element>(); // ------------------------ @@ -162,6 +164,9 @@ export class GraphicComponentView extends ComponentView { if (isInit) { el = createEl(id, targetElParent, elOption.type, elMap); } + else { + el && (inner(el).isNew = false); + } if (el) { applyUpdateTransition( el, @@ -231,6 +236,8 @@ export class GraphicComponentView extends ComponentView { const apiWidth = api.getWidth(); const apiHeight = api.getHeight(); + const xy = ['x', 'y'] as const; + // Top-down to calculate percentage width/height of group for (let i = 0; i < elOptions.length; i++) { const elOption = elOptions[i]; @@ -277,14 +284,37 @@ export class GraphicComponentView extends ComponentView { height: parentElInner.height }; + + // PENDING // Currently, when `bounding: 'all'`, the union bounding rect of the group // does not include the rect of [0, 0, group.width, group.height], which // is probably weird for users. Should we make a break change for it? - layoutUtil.positionElement( + const layoutPos = {} as Record<'x' | 'y', number>; + const layouted = layoutUtil.positionElement( el, elOption, containerInfo, null, - { hv: elOption.hv, boundingMode: elOption.bounding } + { hv: elOption.hv, boundingMode: elOption.bounding }, + layoutPos ); + + if (!inner(el).isNew && layouted) { + const transition = elOption.transition; + const animatePos = {} as Record<'x' | 'y', number>; + for (let k = 0; k < xy.length; k++) { + const key = xy[k]; + const val = layoutPos[key]; + if (transition && (transition === 'all' || zrUtil.indexOf(transition, key) >= 0)) { + animatePos[key] = val; + } + else { + el[key] = val; + } + } + updateProps(el, animatePos, graphicModel, 0); + } + else { + el.attr(layoutPos); + } } } @@ -330,6 +360,7 @@ function createEl( targetElParent.add(el); elMap.set(id, el); inner(el).id = id; + inner(el).isNew = true; return el; } diff --git a/src/util/layout.ts b/src/util/layout.ts index 7e4cddcd09..1c30e72948 100644 --- a/src/util/layout.ts +++ b/src/util/layout.ts @@ -308,6 +308,8 @@ export function getLayoutRect( * * If be called repeatly with the same input el, the same result will be gotten. * + * Return true if the layout happend. + * * @param el Should have `getBoundingRect` method. * @param positionInfo * @param positionInfo.left @@ -339,14 +341,19 @@ export function positionElement( opt?: { hv: [1 | 0 | boolean, 1 | 0 | boolean], boundingMode: 'all' | 'raw' - } -) { + }, + out?: { x?: number, y?: number } +): boolean { const h = !opt || !opt.hv || opt.hv[0]; const v = !opt || !opt.hv || opt.hv[1]; const boundingMode = opt && opt.boundingMode || 'all'; + out = out || el; + + out.x = el.x; + out.y = el.y; if (!h && !v) { - return; + return false; } let rect; @@ -383,14 +390,17 @@ export function positionElement( const dy = v ? layoutRect.y - rect.y : 0; if (boundingMode === 'raw') { - el.x = dx; - el.y = dy; + out.x = dx; + out.y = dy; } else { - el.x += dx; - el.y += dy; + out.x += dx; + out.y += dy; + } + if (out === el) { + el.markRedraw(); } - el.markRedraw(); + return true; } /** diff --git a/test/graphic-transition.html b/test/graphic-transition.html index 9d80be37a5..cb45354735 100644 --- a/test/graphic-transition.html +++ b/test/graphic-transition.html @@ -49,8 +49,8 @@ graphic: { elements: [{ type: 'circle', - x: 100, - y: 50, + left: 'left', + y: 100, transition: ['x', 'y'], shape: { cx: 0, @@ -76,6 +76,8 @@ chart.setOption({ graphic: { elements: [{ + left: null, + top: null, x: 200 }] } @@ -90,6 +92,8 @@ chart.setOption({ graphic: { elements: [{ + left: null, + top: null, y: 200 }] } @@ -103,6 +107,8 @@ chart.setOption({ graphic: { elements: [{ + left: null, + top: null, x: 100, y: 50 }] @@ -123,6 +129,20 @@ } }) } + }, + + { + text: 'Move to top right', + onclick() { + chart.setOption({ + graphic: { + elements: [{ + left: 'right', + top: 'top' + }] + } + }) + } } ] }); From 24a9984362c29b6ddc293f086238dc2330187abf Mon Sep 17 00:00:00 2001 From: pissang Date: Fri, 19 Nov 2021 16:20:22 +0800 Subject: [PATCH 08/35] feat(transition): fix style transition all --- src/animation/customGraphicTransition.ts | 22 ++++++++++++++++------ src/component/graphic/GraphicView.ts | 4 ++-- test/graphic-transition.html | 11 +++++++++-- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/animation/customGraphicTransition.ts b/src/animation/customGraphicTransition.ts index 90d7dfac6f..7ad28be06a 100644 --- a/src/animation/customGraphicTransition.ts +++ b/src/animation/customGraphicTransition.ts @@ -158,6 +158,10 @@ export function applyLeaveTransition( } } +export function isTransitionAll(transition: TransitionProps): transition is 'all' { + return transition === 'all'; +} + function applyPropsDirectly( el: Element, @@ -405,7 +409,7 @@ function prepareShapeOrExtraTransitionFrom( const attrTransition = attrOpt.transition; if (attrTransition) { !transFromPropsInAttr && (transFromPropsInAttr = transFromProps[mainAttr] = {}); - if (attrTransition === 'all') { + if (isTransitionAll(attrTransition)) { extend(transFromPropsInAttr, elPropsInAttr); } else { @@ -417,7 +421,7 @@ function prepareShapeOrExtraTransitionFrom( } } } - else if (transition === 'all' || indexOf(transition, mainAttr) >= 0) { + else if (isTransitionAll(transition) || indexOf(transition, mainAttr) >= 0) { !transFromPropsInAttr && (transFromPropsInAttr = transFromProps[mainAttr] = {}); const elPropsInAttrKeys = keys(elPropsInAttr); for (let i = 0; i < elPropsInAttrKeys.length; i++) { @@ -482,7 +486,7 @@ function prepareTransformTransitionFrom( if (!isInit) { const transition = elOption.transition; - const transitionKeys = transition === 'all' + const transitionKeys = isTransitionAll(transition) ? TRANSFORM_PROPS : normalizeToArray(transition || []); for (let i = 0; i < transitionKeys.length; i++) { @@ -562,8 +566,10 @@ function prepareStyleTransitionFrom( } if (!isInit && fromElStyle) { - if (styleOpt.transition) { - const transitionKeys = normalizeToArray(styleOpt.transition); + 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]; @@ -574,7 +580,11 @@ function prepareStyleTransitionFrom( } else if ( (fromEl as Displayable).getAnimationStyleProps - && indexOf(elOption.transition, 'style') >= 0 + && ( + isTransitionAll(elTransition) + || isTransitionAll(styleTransition) + || indexOf(elTransition, 'style') >= 0 + ) ) { const animationProps = (fromEl as Displayable).getAnimationStyleProps(); const animationStyleProps = animationProps ? animationProps.style : null; diff --git a/src/component/graphic/GraphicView.ts b/src/component/graphic/GraphicView.ts index 50dbbf09c8..9d90fe01f4 100644 --- a/src/component/graphic/GraphicView.ts +++ b/src/component/graphic/GraphicView.ts @@ -38,7 +38,7 @@ import { GraphicComponentGroupOption, GraphicComponentElementOption } from './GraphicModel'; -import { applyLeaveTransition, applyUpdateTransition } from '../../animation/customGraphicTransition'; +import { applyLeaveTransition, applyUpdateTransition, isTransitionAll } from '../../animation/customGraphicTransition'; import { updateProps } from '../../animation/basicTrasition'; const nonShapeGraphicElements = { @@ -303,7 +303,7 @@ export class GraphicComponentView extends ComponentView { for (let k = 0; k < xy.length; k++) { const key = xy[k]; const val = layoutPos[key]; - if (transition && (transition === 'all' || zrUtil.indexOf(transition, key) >= 0)) { + if (transition && (isTransitionAll(transition) || zrUtil.indexOf(transition, key) >= 0)) { animatePos[key] = val; } else { diff --git a/test/graphic-transition.html b/test/graphic-transition.html index cb45354735..8a1b0c3637 100644 --- a/test/graphic-transition.html +++ b/test/graphic-transition.html @@ -159,7 +159,7 @@ type: 'circle', x: 100, y: 50, - transition: ['x', 'y'], + transition: 'all', shape: { cx: 0, cy: 0, @@ -184,7 +184,14 @@ chart.setOption({ graphic: { elements: [{ - x: 200 + x: Math.random() * chart.getWidth(), + y: Math.random() * chart.getHeight(), + shape: { + r: Math.random() * 30 + 50 + }, + style: { + fill: echarts.color.random() + } }] } }) From 2eea25e762515aa93708157baa3177a277d7647d Mon Sep 17 00:00:00 2001 From: pissang Date: Fri, 19 Nov 2021 17:46:26 +0800 Subject: [PATCH 09/35] fix wrong import from lib --- src/animation/customGraphicTransition.ts | 2 +- src/chart/custom/CustomSeries.ts | 2 +- test/graphic-transition.html | 47 +++++++++++++++++++++++- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/animation/customGraphicTransition.ts b/src/animation/customGraphicTransition.ts index 7ad28be06a..ef602bf37f 100644 --- a/src/animation/customGraphicTransition.ts +++ b/src/animation/customGraphicTransition.ts @@ -29,7 +29,7 @@ import { initProps, updateProps } from './basicTrasition'; import { Path } from '../util/graphic'; import { warn } from '../util/log'; import { AnimationOptionMixin, ZRStyleProps } from '../util/types'; -import { Dictionary } from 'zrender/lib/core/types'; +import { Dictionary } from 'zrender/src/core/types'; import { PathStyleProps } from 'zrender'; const LEGACY_TRANSFORM_PROPS_MAP = { diff --git a/src/chart/custom/CustomSeries.ts b/src/chart/custom/CustomSeries.ts index 3faaa85b23..bc11e34e32 100644 --- a/src/chart/custom/CustomSeries.ts +++ b/src/chart/custom/CustomSeries.ts @@ -70,7 +70,7 @@ import { TransitionBaseDuringAPI, TransitionDuringAPI } from '../../animation/customGraphicTransition'; -import { TransformProp } from 'zrender/lib/core/Transformable'; +import { TransformProp } from 'zrender/src/core/Transformable'; export type CustomExtraElementInfo = Dictionary; diff --git a/test/graphic-transition.html b/test/graphic-transition.html index 8a1b0c3637..7b2cf01fca 100644 --- a/test/graphic-transition.html +++ b/test/graphic-transition.html @@ -210,8 +210,51 @@ From b30b63f764e4a1e88075018c1f39a288eba05d47 Mon Sep 17 00:00:00 2001 From: pissang Date: Sun, 21 Nov 2021 15:31:47 +0800 Subject: [PATCH 10/35] feat(transition) optimize enterFrom and leaveTo. fix style loose in custom --- src/animation/customGraphicTransition.ts | 239 +++++++++++------------ src/chart/custom/CustomView.ts | 33 +--- src/component/graphic/GraphicView.ts | 14 +- test/graphic-transition.html | 92 ++++++--- 4 files changed, 188 insertions(+), 190 deletions(-) diff --git a/src/animation/customGraphicTransition.ts b/src/animation/customGraphicTransition.ts index ef602bf37f..5bcba6457b 100644 --- a/src/animation/customGraphicTransition.ts +++ b/src/animation/customGraphicTransition.ts @@ -52,11 +52,14 @@ type TransformProp = keyof typeof TRANSFORM_PROPS_MAP; const TRANSFORM_PROPS = keys(TRANSFORM_PROPS_MAP); const transformPropNamesStr = TRANSFORM_PROPS.join(', '); +// '' means root +const ELEMENT_TRANSITION_PROPS = ['', 'style', 'shape', 'extra'] as const; + export type TransitionProps = string | string[]; export type ElementRootTransitionProp = TransformProp | 'shape' | 'extra' | 'style'; export interface TransitionOptionMixin { - transition?: TransitionProps | 'all'; + transition?: TransitionProps | 'all' enterFrom?: Dictionary; leaveTo?: Dictionary; }; @@ -106,13 +109,17 @@ export interface TransitionDuringAPI< getStyle(key: T): StyleOpt[T]; }; + export function applyUpdateTransition( el: Element, elOption: TransitionElementOption, animatableModel?: Model, - dataIndex?: number, - isInit?: boolean + 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; @@ -121,24 +128,67 @@ export function applyUpdateTransition( const transFromProps = {} as ElementProps; const propsToSet = {} as ElementProps; - prepareShapeOrExtraTransitionFrom('shape', el, elOption, transFromProps, isInit); - prepareShapeOrExtraAllPropsFinal('shape', elOption, propsToSet); - - prepareTransformTransitionFrom(el, elOption, transFromProps, isInit); prepareTransformAllPropsFinal(el, elOption, propsToSet); - - prepareShapeOrExtraTransitionFrom('extra', el, elOption, transFromProps, isInit); + prepareShapeOrExtraAllPropsFinal('shape', elOption, propsToSet); prepareShapeOrExtraAllPropsFinal('extra', elOption, propsToSet); - prepareStyleTransitionFrom(el, elOption, styleOpt, transFromProps, isInit); + 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); - applyPropsTransition(el, dataIndex, animatableModel, transFromProps, isInit); + + applyPropsDirectly(el, propsToSet, clearStyle); applyMiscProps(el, elOption); + if (hasAnimation) { + if (isInit) { + const enterFromProps: ElementProps = {}; + for (let i = 0; i < ELEMENT_TRANSITION_PROPS.length; i++) { + const propName = ELEMENT_TRANSITION_PROPS[i]; + 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); + } + } + initProps(el, enterFromProps, animatableModel, { + dataIndex: dataIndex || 0, isFrom: true + }); + } + else { + applyPropsTransition(el, 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_TRANSITION_PROPS.length; i++) { + const propName = ELEMENT_TRANSITION_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, animatableModel: Model, @@ -166,13 +216,38 @@ export function isTransitionAll(transition: TransitionProps): transition is 'all function applyPropsDirectly( el: Element, // Can be null/undefined - allPropsFinal: ElementProps + allPropsFinal: ElementProps, + clearStyle: boolean ) { - const elDisplayable = el.isGroup ? null : el as Displayable; const styleOpt = (allPropsFinal as Displayable).style; - - if (elDisplayable && styleOpt) { - elDisplayable.setStyle(styleOpt); + 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) { @@ -189,8 +264,7 @@ function applyPropsTransition( dataIndex: number, model: Model, // Can be null/undefined - transFromProps: ElementProps, - isInit: boolean + transFromProps: ElementProps ): void { if (transFromProps) { // NOTE: Do not use `el.updateDuringAnimation` here becuase `el.updateDuringAnimation` will @@ -206,9 +280,7 @@ function applyPropsTransition( isFrom: true, during: cfgDuringCall }; - isInit - ? initProps(el, transFromProps, model, cfg) - : updateProps(el, transFromProps, model, cfg); + updateProps(el, transFromProps, model, cfg); } } @@ -377,9 +449,8 @@ function duringCall( function prepareShapeOrExtraTransitionFrom( mainAttr: 'shape' | 'extra', fromEl: Element, - elOption: TransitionElementOption, - transFromProps: LooseElementProps, - isInit: boolean + elOption: ElementTransitionOptionMixin, + transFromProps: LooseElementProps ): void { const attrOpt: Dictionary & TransitionOptionMixin = (elOption as any)[mainAttr]; @@ -390,21 +461,8 @@ function prepareShapeOrExtraTransitionFrom( const elPropsInAttr = (fromEl as LooseElementProps)[mainAttr]; let transFromPropsInAttr: Dictionary; - const enterFrom = attrOpt.enterFrom; - if (isInit && enterFrom) { - !transFromPropsInAttr && (transFromPropsInAttr = transFromProps[mainAttr] = {}); - const enterFromKeys = keys(enterFrom); - for (let i = 0; i < enterFromKeys.length; i++) { - // `enterFrom` props are not necessarily also declared in `shape`/`style`/..., - // for example, `opacity` can only declared in `enterFrom` but not in `style`. - const key = enterFromKeys[i]; - // Do not clone, animator will perform that clone. - transFromPropsInAttr[key] = enterFrom[key]; - } - } - - if (!isInit && elPropsInAttr) { + if (elPropsInAttr) { const transition = elOption.transition; const attrTransition = attrOpt.transition; if (attrTransition) { @@ -433,17 +491,6 @@ function prepareShapeOrExtraTransitionFrom( } } } - - const leaveTo = attrOpt.leaveTo; - if (leaveTo) { - const leaveToProps = getOrCreateLeaveToPropsFromEl(fromEl); - const leaveToPropsInAttr: Dictionary = leaveToProps[mainAttr] || (leaveToProps[mainAttr] = {}); - const leaveToKeys = keys(leaveTo); - for (let i = 0; i < leaveToKeys.length; i++) { - const key = leaveToKeys[i]; - leaveToPropsInAttr[key] = leaveTo[key]; - } - } } function prepareShapeOrExtraAllPropsFinal( @@ -468,52 +515,23 @@ function prepareShapeOrExtraAllPropsFinal( function prepareTransformTransitionFrom( el: Element, elOption: TransitionElementOption, - transFromProps: ElementProps, - isInit: boolean + transFromProps: ElementProps ): void { - const enterFrom = elOption.enterFrom; - if (isInit && enterFrom) { - const enterFromKeys = keys(enterFrom); - for (let i = 0; i < enterFromKeys.length; i++) { - const key = enterFromKeys[i] as TransformProp; - if (__DEV__) { - checkTransformPropRefer(key, 'el.enterFrom'); - } - // Do not clone, animator will perform that clone. - transFromProps[key] = enterFrom[key] as number; + const transition = elOption.transition; + const transitionKeys = isTransitionAll(transition) + ? TRANSFORM_PROPS + : normalizeToArray(transition || []); + for (let i = 0; i < transitionKeys.length; i++) { + const key = transitionKeys[i]; + if (key === 'style' || key === 'shape' || key === 'extra') { + continue; } - } - - if (!isInit) { - const transition = elOption.transition; - const transitionKeys = isTransitionAll(transition) - ? TRANSFORM_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[key]; - if (__DEV__) { - checkTransformPropRefer(key, 'el.transition'); - } - // Do not clone, animator will perform that clone. - transFromProps[key] = elVal; - } - } - - const leaveTo = elOption.leaveTo; - if (leaveTo) { - const leaveToProps = getOrCreateLeaveToPropsFromEl(el); - const leaveToKeys = keys(leaveTo); - for (let i = 0; i < leaveToKeys.length; i++) { - const key = leaveToKeys[i] as TransformProp; - if (__DEV__) { - checkTransformPropRefer(key, 'el.leaveTo'); - } - leaveToProps[key] = leaveTo[key] as number; + const elVal = el[key]; + if (__DEV__) { + checkTransformPropRefer(key, 'el.transition'); } + // Do not clone, animator will perform that clone. + transFromProps[key] = elVal; } } @@ -544,8 +562,7 @@ function prepareStyleTransitionFrom( fromEl: Element, elOption: TransitionElementOption, styleOpt: TransitionElementOption['style'], - transFromProps: LooseElementProps, - isInit: boolean + transFromProps: LooseElementProps ): void { if (!styleOpt) { return; @@ -554,18 +571,7 @@ function prepareStyleTransitionFrom( const fromElStyle = (fromEl as LooseElementProps).style as LooseElementProps['style']; let transFromStyleProps: LooseElementProps['style']; - const enterFrom = styleOpt.enterFrom; - if (isInit && enterFrom) { - const enterFromKeys = keys(enterFrom); - !transFromStyleProps && (transFromStyleProps = transFromProps.style = {}); - for (let i = 0; i < enterFromKeys.length; i++) { - const key = enterFromKeys[i]; - // Do not clone, animator will perform that clone. - (transFromStyleProps as any)[key] = enterFrom[key]; - } - } - - if (!isInit && fromElStyle) { + if (fromElStyle) { const styleTransition = styleOpt.transition; const elTransition = elOption.transition; if (styleTransition && !isTransitionAll(styleTransition)) { @@ -601,17 +607,6 @@ function prepareStyleTransitionFrom( } } } - - const leaveTo = styleOpt.leaveTo; - if (leaveTo) { - const leaveToKeys = keys(leaveTo); - const leaveToProps = getOrCreateLeaveToPropsFromEl(fromEl); - const leaveToStyleProps = leaveToProps.style || (leaveToProps.style = {}); - for (let i = 0; i < leaveToKeys.length; i++) { - const key = leaveToKeys[i]; - (leaveToStyleProps as any)[key] = leaveTo[key]; - } - } } function isNonStyleTransitionEnabled(optVal: unknown, elVal: unknown): boolean { @@ -629,10 +624,4 @@ if (__DEV__) { + 'Only `' + keys(TRANSFORM_PROPS_MAP).join('`, `') + '` are permitted.'); } }; -} - -function getOrCreateLeaveToPropsFromEl(el: Element): LooseElementProps { - const innerEl = transitionInnerStore(el); - return innerEl.leaveToProps || (innerEl.leaveToProps = {}); -} - +} \ No newline at end of file diff --git a/src/chart/custom/CustomView.ts b/src/chart/custom/CustomView.ts index 0024c825cc..cac33f70c1 100644 --- a/src/chart/custom/CustomView.ts +++ b/src/chart/custom/CustomView.ts @@ -485,36 +485,13 @@ function updateElNormal( (styleOpt as PathStyleProps).decal = decalPattern; } } - - // Clear style - el.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.style); - } - } } - applyUpdateTransition(el, elOption, seriesModel, dataIndex, isInit); - + applyUpdateTransition(el, elOption, seriesModel, { + dataIndex, + isInit, + clearStyle: true + }); if (!isTextContent) { // `elOption.info` enables user to mount some info on diff --git a/src/component/graphic/GraphicView.ts b/src/component/graphic/GraphicView.ts index 9d90fe01f4..6b8d25f96d 100644 --- a/src/component/graphic/GraphicView.ts +++ b/src/component/graphic/GraphicView.ts @@ -38,7 +38,12 @@ import { GraphicComponentGroupOption, GraphicComponentElementOption } from './GraphicModel'; -import { applyLeaveTransition, applyUpdateTransition, isTransitionAll } from '../../animation/customGraphicTransition'; +import { + applyLeaveTransition, + applyUpdateTransition, + isTransitionAll, + updateLeaveTo +} from '../../animation/customGraphicTransition'; import { updateProps } from '../../animation/basicTrasition'; const nonShapeGraphicElements = { @@ -172,8 +177,7 @@ export class GraphicComponentView extends ComponentView { el, elOptionCleaned, graphicModel, - 0, // TODO Fixed dataIndex to be 0 - isInit + { isInit } ); updateZ(el, elOption, globalZ, globalZLevel); } @@ -186,13 +190,13 @@ export class GraphicComponentView extends ComponentView { el, elOptionCleaned, graphicModel, - 0, // TODO Fixed dataIndex to be 0 - true + { isInit: true} ); updateZ(el, elOption, globalZ, globalZLevel); } } else if ($action === 'remove') { + updateLeaveTo(elExisting, elOption); removeEl(elExisting, elMap, graphicModel); } diff --git a/test/graphic-transition.html b/test/graphic-transition.html index 7b2cf01fca..497e3520af 100644 --- a/test/graphic-transition.html +++ b/test/graphic-transition.html @@ -39,6 +39,7 @@
+
@@ -184,10 +185,10 @@ chart.setOption({ graphic: { elements: [{ - x: Math.random() * chart.getWidth(), - y: Math.random() * chart.getHeight(), + x: Math.random() * (chart.getWidth() - 100) + 50, + y: Math.random() * (chart.getHeight() - 100) + 50, shape: { - r: Math.random() * 30 + 50 + r: Math.random() * 40 + 10 }, style: { fill: echarts.color.random() @@ -210,47 +211,74 @@ + + + + + + + + + + + + +
+
+
+ + + + + + + + + diff --git a/test/graphic-transition.html b/test/graphic-transition.html index 497e3520af..5fe22afa36 100644 --- a/test/graphic-transition.html +++ b/test/graphic-transition.html @@ -234,7 +234,7 @@ var chart = testHelper.create(echarts, 'main2', { title: [ - 'Enter transition' + 'Enter / Leave transition' ], option: option, buttons: [ @@ -253,6 +253,13 @@ r: 0 } }, + leaveTo: { + x: 500, + y: 100, + shape: { + r: 0 + } + }, shape: { cx: 0, cy: 0, @@ -266,9 +273,21 @@ } }, { - text: 'Remove', + text: 'Replace', onclick() { elements[1] = { + type: 'circle', + name: 'center', + $action: 'replace', + x: 300, + y: 100, + enterFrom: { + x: 0, + y: 100, + shape: { + r: 0 + } + }, leaveTo: { x: 500, y: 100, @@ -276,6 +295,22 @@ r: 0 } }, + shape: { + cx: 0, + cy: 0, + r: 80 + }, + style: { + fill: echarts.color.random() + } + }; + chart.setOption(option) + } + }, + { + text: 'Remove', + onclick() { + elements[1] = { $action: 'remove' }; chart.setOption(option) @@ -286,22 +321,6 @@ }); - - - - + @@ -57,7 +58,7 @@ cy: 0, r: 50 }, - animation: { + keyframeAnimation: { duration: 1000, loop: true, keyframes: [{ @@ -90,6 +91,67 @@ }); + + diff --git a/test/graphic-transition.html b/test/graphic-transition.html index 5fe22afa36..5266774571 100644 --- a/test/graphic-transition.html +++ b/test/graphic-transition.html @@ -28,6 +28,7 @@ + @@ -40,6 +41,8 @@
+
+
@@ -322,11 +325,64 @@ + + diff --git a/test/lib/testHelper.js b/test/lib/testHelper.js index 7d4908475c..7d17bf34b2 100644 --- a/test/lib/testHelper.js +++ b/test/lib/testHelper.js @@ -115,9 +115,7 @@ + ''; } - if (opt.option) { - chart = testHelper.createChart(echarts, chartContainer, opt.option, opt, opt.setOptionOpts); - } + chart = testHelper.createChart(echarts, chartContainer, opt.option, opt, opt.setOptionOpts); var dataTables = opt.dataTables; if (!dataTables && opt.dataTable) { From 7e96d4cc43e40f75aa12dd1f1c1ebddd9eb377eb Mon Sep 17 00:00:00 2001 From: pissang Date: Thu, 25 Nov 2021 14:14:01 +0800 Subject: [PATCH 17/35] feat(animation): improve validation logs --- .../customGraphicKeyframeAnimation.ts | 20 +++++++++ src/component/graphic/GraphicView.ts | 4 +- src/data/helper/transform.ts | 4 +- src/util/log.ts | 45 ++++++++----------- test/graphic-animation.html | 8 +--- test/graphic-transition.html | 6 +-- 6 files changed, 46 insertions(+), 41 deletions(-) diff --git a/src/animation/customGraphicKeyframeAnimation.ts b/src/animation/customGraphicKeyframeAnimation.ts index d72afbffa7..af1a980150 100644 --- a/src/animation/customGraphicKeyframeAnimation.ts +++ b/src/animation/customGraphicKeyframeAnimation.ts @@ -24,6 +24,7 @@ import { ELEMENT_ANIMATABLE_PROPS } from './customGraphicTransition'; import { AnimationOption, AnimationOptionMixin } from '../util/types'; import { Model } from '../echarts.all'; import { getAnimationConfig } from './basicTrasition'; +import { warn } from '../util/log'; // Helpers for creating keyframe based animations in custom series and graphic components. @@ -63,6 +64,8 @@ export function applyKeyframeAnimation>( } const animator = el.animate(propName, animationOpts.loop); + let endFrameIsSet = false; + let hasAnimation = false; each(keyframes, kf => { // Stop current animation. const animators = el.animators; @@ -88,8 +91,25 @@ export function applyKeyframeAnimation>( } } + if (__DEV__) { + if (kf.percent >= 1) { + endFrameIsSet = true; + } + } + + hasAnimation = true; animator.whenWithKeys(duration * kf.percent, kfValues, propKeys, kf.easing); }); + if (!hasAnimation) { + return; + } + + if (__DEV__) { + if (!endFrameIsSet) { + warn('End frame with percent: 1 is missing in the keyframeAnimation.', true); + } + } + animator .delay(animationOpts.delay || 0) .start(animationOpts.easing); diff --git a/src/component/graphic/GraphicView.ts b/src/component/graphic/GraphicView.ts index 49b44fa0fc..05074985af 100644 --- a/src/component/graphic/GraphicView.ts +++ b/src/component/graphic/GraphicView.ts @@ -394,8 +394,8 @@ function updateZ(el: Element, elOption: GraphicComponentElementOption, defaultZ: const elDisplayable = el as Displayable; // We should not support configure z and zlevel in the element level. // But seems we didn't limit it previously. So here still use it to avoid breaking. - elDisplayable.z = zrUtil.retrieve2((elOption as any).z, defaultZ); - elDisplayable.zlevel = zrUtil.retrieve2((elOption as any).zlevel, defaultZlevel); + elDisplayable.z = zrUtil.retrieve2((elOption as any).z, defaultZ || 0); + elDisplayable.zlevel = zrUtil.retrieve2((elOption as any).zlevel, defaultZlevel || 0); // z2 must not be null/undefined, otherwise sort error may occur. const optZ2 = (elOption as GraphicComponentDisplayableOption).z2; optZ2 != null && (elDisplayable.z2 = optZ2 || 0); diff --git a/src/data/helper/transform.ts b/src/data/helper/transform.ts index 6e5181f140..9abce7e671 100644 --- a/src/data/helper/transform.ts +++ b/src/data/helper/transform.ts @@ -32,7 +32,7 @@ import { getRawSourceItemGetter, getRawSourceDataCounter, getRawSourceValueGetter } from './dataProvider'; import { parseDataValue } from './dataValueHelper'; -import { consoleLog, makePrintable, throwError } from '../../util/log'; +import { log, makePrintable, throwError } from '../../util/log'; import { createSource, Source, SourceMetaRawOption, detectSourceFormat } from '../Source'; @@ -443,7 +443,7 @@ function applySingleDataTransform( makePrintable(extSource.dimensions) ].join('\n'); }).join('\n'); - consoleLog(printStrArr); + log(printStrArr); } } diff --git a/src/util/log.ts b/src/util/log.ts index 527051c247..7d8ca56285 100644 --- a/src/util/log.ts +++ b/src/util/log.ts @@ -27,34 +27,35 @@ const hasConsole = typeof console !== 'undefined' // eslint-disable-next-line && console.warn && console.log; -export function log(str: string) { +function outputLog(type: 'log' | 'warn' | 'error', str: string, onlyOnce?: boolean) { if (hasConsole) { + if (onlyOnce) { + if (storedLogs[str]) { + return; + } + storedLogs[str] = true; + } // eslint-disable-next-line - console.log(ECHARTS_PREFIX + str); + console[type](ECHARTS_PREFIX + str); } } -export function warn(str: string) { - if (hasConsole) { - console.warn(ECHARTS_PREFIX + str); - } +export function log(str: string, onlyOnce?: boolean) { + outputLog('log', str, onlyOnce); } -export function error(str: string) { - if (hasConsole) { - console.error(ECHARTS_PREFIX + str); - } +export function warn(str: string, onlyOnce?: boolean) { + outputLog('warn', str, onlyOnce); +} + +export function error(str: string, onlyOnce?: boolean) { + outputLog('error', str, onlyOnce); } export function deprecateLog(str: string) { if (__DEV__) { - if (storedLogs[str]) { // Not display duplicate message. - return; - } - if (hasConsole) { - storedLogs[str] = true; - console.warn(ECHARTS_PREFIX + 'DEPRECATED: ' + str); - } + // Not display duplicate message. + outputLog('warn', 'DEPRECATED: ' + str, true); } } @@ -64,16 +65,6 @@ export function deprecateReplaceLog(oldOpt: string, newOpt: string, scope?: stri } } -export function consoleLog(...args: unknown[]) { - if (__DEV__) { - /* eslint-disable no-console */ - if (typeof console !== 'undefined' && console.log) { - console.log.apply(console, args); - } - /* eslint-enable no-console */ - } -} - /** * If in __DEV__ environment, get console printable message for users hint. * Parameters are separated by ' '. diff --git a/test/graphic-animation.html b/test/graphic-animation.html index dfd3340d93..2abc3c7b09 100644 --- a/test/graphic-animation.html +++ b/test/graphic-animation.html @@ -117,12 +117,6 @@ loop: true, delay: (rand - 1) * 2000, keyframes: [{ - percent: 0, - easing: 'sinusoidalInOut', - shape: { - r: 20 - } - }, { percent: 0.5, easing: 'sinusoidalInOut', shape: { @@ -139,7 +133,7 @@ shape: { cx: 0, cy: 0, - r: 10 + r: 20 } }); } diff --git a/test/graphic-transition.html b/test/graphic-transition.html index 5266774571..2954f3f958 100644 --- a/test/graphic-transition.html +++ b/test/graphic-transition.html @@ -347,7 +347,7 @@ y: y, enterFrom: { extra: { - p: 0 + time: 0 } }, enterAnimation: { @@ -355,10 +355,10 @@ easing: 'linear' }, extra: { - p: 1e6 + time: 1e6 }, during(api) { - const r = Math.sin(rand * 100 + api.getExtra('p') / 200) * 5 + 5; + const r = Math.sin(rand * 100 + api.getExtra('time') / 200) * 5 + 5; api.setShape('r', r); }, shape: { From b5b21856944d2b19c75872267b0509d305da7c31 Mon Sep 17 00:00:00 2001 From: pissang Date: Thu, 25 Nov 2021 19:01:50 +0800 Subject: [PATCH 18/35] feat(animation): fix some animation abort bug --- .../customGraphicKeyframeAnimation.ts | 27 +-- src/chart/custom/CustomView.ts | 4 +- src/component/graphic/GraphicView.ts | 7 +- test/graphic-animation-wave.html | 186 ++++++++++++++++++ test/graphic-animation.html | 56 ------ 5 files changed, 206 insertions(+), 74 deletions(-) create mode 100644 test/graphic-animation-wave.html diff --git a/src/animation/customGraphicKeyframeAnimation.ts b/src/animation/customGraphicKeyframeAnimation.ts index af1a980150..a483090cbc 100644 --- a/src/animation/customGraphicKeyframeAnimation.ts +++ b/src/animation/customGraphicKeyframeAnimation.ts @@ -42,9 +42,8 @@ export interface ElementKeyframeAnimationOption>( el: Element, animationOpts: ElementKeyframeAnimationOption, animatableModel: Model ) { - if (!animationOpts) { - return; - } + // Stop previous keyframe animation. + el.stopAnimation('keyframe'); const keyframes = animationOpts.keyframes; let duration = animationOpts.duration; @@ -63,9 +62,8 @@ export function applyKeyframeAnimation>( return; } - const animator = el.animate(propName, animationOpts.loop); + let animator: ReturnType; let endFrameIsSet = false; - let hasAnimation = false; each(keyframes, kf => { // Stop current animation. const animators = el.animators; @@ -85,11 +83,6 @@ export function applyKeyframeAnimation>( if (!propKeys.length) { return; } - for (let i = 0; i < animators.length; i++) { - if (animators[i] !== animator) { - animators[i].stopTracks(propKeys); - } - } if (__DEV__) { if (kf.percent >= 1) { @@ -97,10 +90,20 @@ export function applyKeyframeAnimation>( } } - hasAnimation = true; + if (!animator) { + animator = el.animate(propName, animationOpts.loop); + animator.scope = 'keyframe'; + } + for (let i = 0; i < animators.length; i++) { + // Stop all other animation that is not keyframe. + if (animators[i] !== animator && animators[i].targetName === animator.targetName) { + animators[i].stopTracks(propKeys); + } + } + animator.whenWithKeys(duration * kf.percent, kfValues, propKeys, kf.easing); }); - if (!hasAnimation) { + if (!animator) { return; } diff --git a/src/chart/custom/CustomView.ts b/src/chart/custom/CustomView.ts index e98b11c74c..abfc4fa5c1 100644 --- a/src/chart/custom/CustomView.ts +++ b/src/chart/custom/CustomView.ts @@ -86,10 +86,10 @@ import CustomSeriesModel, { customInnerStore, PrepareCustomInfo, CustomPathOption, - CustomRootElementOption + CustomRootElementOption, + CustomSeriesOption } from './CustomSeries'; import { PatternObject } from 'zrender/src/graphic/Pattern'; -import { CustomSeriesOption } from '../../export/option'; import { applyLeaveTransition, applyUpdateTransition, diff --git a/src/component/graphic/GraphicView.ts b/src/component/graphic/GraphicView.ts index 05074985af..a308b8c7d7 100644 --- a/src/component/graphic/GraphicView.ts +++ b/src/component/graphic/GraphicView.ts @@ -46,7 +46,6 @@ import { } from '../../animation/customGraphicTransition'; import { updateProps } from '../../animation/basicTrasition'; import { applyKeyframeAnimation } from '../../animation/customGraphicKeyframeAnimation'; -import { graphic } from '../../echarts.all'; const nonShapeGraphicElements = { // Reserved but not supported in graphic component. @@ -217,6 +216,8 @@ export class GraphicComponentView extends ComponentView { if (el) { const elInner = inner(el); + const keyframeAnimation = elOption.keyframeAnimation; + elInner.option = elOption; setEventData(el, graphicModel, elOption); @@ -227,7 +228,7 @@ export class GraphicComponentView extends ComponentView { itemTooltipOption: elOption.tooltip }); - applyKeyframeAnimation(el, elOption.keyframeAnimation, graphicModel); + keyframeAnimation && applyKeyframeAnimation(el, keyframeAnimation, graphicModel); } }); } @@ -290,8 +291,6 @@ export class GraphicComponentView extends ComponentView { height: parentElInner.height }; - - // PENDING // Currently, when `bounding: 'all'`, the union bounding rect of the group // does not include the rect of [0, 0, group.width, group.height], which diff --git a/test/graphic-animation-wave.html b/test/graphic-animation-wave.html new file mode 100644 index 0000000000..c2477a1d11 --- /dev/null +++ b/test/graphic-animation-wave.html @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + diff --git a/test/graphic-animation.html b/test/graphic-animation.html index 2abc3c7b09..56856d6fad 100644 --- a/test/graphic-animation.html +++ b/test/graphic-animation.html @@ -90,62 +90,6 @@ }); - - - From 6eb6cd921d3ff214620233bf3e84e6b7e69b6f76 Mon Sep 17 00:00:00 2001 From: pissang Date: Wed, 1 Dec 2021 11:59:42 +0800 Subject: [PATCH 19/35] fix(graphic): fix textConfig not work --- .../customGraphicKeyframeAnimation.ts | 8 ++- src/component/graphic/GraphicModel.ts | 3 +- src/component/graphic/GraphicView.ts | 2 + test/graphic-animation.html | 53 ++++++++++++++++++- 4 files changed, 61 insertions(+), 5 deletions(-) diff --git a/src/animation/customGraphicKeyframeAnimation.ts b/src/animation/customGraphicKeyframeAnimation.ts index a483090cbc..3116a33181 100644 --- a/src/animation/customGraphicKeyframeAnimation.ts +++ b/src/animation/customGraphicKeyframeAnimation.ts @@ -40,8 +40,14 @@ export interface ElementKeyframeAnimationOption>( - el: Element, animationOpts: ElementKeyframeAnimationOption, animatableModel: Model + el: Element, + animationOpts: ElementKeyframeAnimationOption, + animatableModel: Model ) { + if (!animatableModel.isAnimationEnabled()) { + return; + } + // Stop previous keyframe animation. el.stopAnimation('keyframe'); diff --git a/src/component/graphic/GraphicModel.ts b/src/component/graphic/GraphicModel.ts index 811c7152b3..eab5b9ae0b 100644 --- a/src/component/graphic/GraphicModel.ts +++ b/src/component/graphic/GraphicModel.ts @@ -40,6 +40,7 @@ import { copyLayoutParams, mergeLayoutParam } from '../../util/layout'; import { TransitionOptionMixin } from '../../animation/customGraphicTransition'; import { ElementKeyframeAnimationOption } from '../../animation/customGraphicKeyframeAnimation'; import { GroupProps } from 'zrender/src/graphic/Group'; +import { TransformProp } from 'zrender/src/core/Transformable'; interface GraphicComponentBaseElementOption extends Partial> { diff --git a/src/component/graphic/GraphicView.ts b/src/component/graphic/GraphicView.ts index a308b8c7d7..4ca8b95c37 100644 --- a/src/component/graphic/GraphicView.ts +++ b/src/component/graphic/GraphicView.ts @@ -218,6 +218,8 @@ export class GraphicComponentView extends ComponentView { const elInner = inner(el); const keyframeAnimation = elOption.keyframeAnimation; + el.setTextConfig(textConfig); + elInner.option = elOption; setEventData(el, graphicModel, elOption); diff --git a/test/graphic-animation.html b/test/graphic-animation.html index 56856d6fad..2cb5ec6dfc 100644 --- a/test/graphic-animation.html +++ b/test/graphic-animation.html @@ -44,7 +44,7 @@ - + + + From 48ba9213aeddb10c85b66a101d42ebd3c27cdf6f Mon Sep 17 00:00:00 2001 From: pissang Date: Wed, 1 Dec 2021 12:27:13 +0800 Subject: [PATCH 20/35] feat(animation): restore props after keyframe animation is stopped --- .../customGraphicKeyframeAnimation.ts | 40 ++++++++++++++----- src/component/graphic/GraphicView.ts | 8 +++- test/graphic-animation-wave.html | 8 ---- test/graphic-animation.html | 4 +- 4 files changed, 40 insertions(+), 20 deletions(-) diff --git a/src/animation/customGraphicKeyframeAnimation.ts b/src/animation/customGraphicKeyframeAnimation.ts index 3116a33181..20c9d7ed8c 100644 --- a/src/animation/customGraphicKeyframeAnimation.ts +++ b/src/animation/customGraphicKeyframeAnimation.ts @@ -21,10 +21,11 @@ import { AnimationEasing } from 'zrender/src/animation/easing'; import Element from 'zrender/src/Element'; import { keys, filter, each } from 'zrender/src/core/util'; import { ELEMENT_ANIMATABLE_PROPS } from './customGraphicTransition'; -import { AnimationOption, AnimationOptionMixin } from '../util/types'; +import { AnimationOption, AnimationOptionMixin, Dictionary } from '../util/types'; import { Model } from '../echarts.all'; import { getAnimationConfig } from './basicTrasition'; import { warn } from '../util/log'; +import { makeInner } from '../util/model'; // Helpers for creating keyframe based animations in custom series and graphic components. @@ -33,12 +34,26 @@ type AnimationKeyframe> = T & { percent?: number // 0 - 1 }; +type StateToRestore = Dictionary; +const getStateToRestore = makeInner(); + export interface ElementKeyframeAnimationOption> extends AnimationOption { // Animation configuration for keyframe based animation. loop?: boolean keyframes?: AnimationKeyframe[] } +/** + * Stopped previous keyframe animation and restore the attributes. + * Avoid new keyframe animation starts with wrong internal state when the percent: 0 is not set. + */ +export function stopPreviousKeyframeAnimationAndRestore(el: Element) { + // Stop previous keyframe animation. + el.stopAnimation('keyframe'); + // Restore + el.attr(getStateToRestore(el)); +} + export function applyKeyframeAnimation>( el: Element, animationOpts: ElementKeyframeAnimationOption, @@ -48,9 +63,6 @@ export function applyKeyframeAnimation>( return; } - // Stop previous keyframe animation. - el.stopAnimation('keyframe'); - const keyframes = animationOpts.keyframes; let duration = animationOpts.duration; @@ -63,8 +75,10 @@ export function applyKeyframeAnimation>( return; } - function applyKeyframeAnimationOnProp(propName: typeof ELEMENT_ANIMATABLE_PROPS[number]) { - if (propName && !(el as any)[propName]) { + const stateToRestore: StateToRestore = getStateToRestore(el); + + function applyKeyframeAnimationOnProp(targetPropName: typeof ELEMENT_ANIMATABLE_PROPS[number]) { + if (targetPropName && !(el as any)[targetPropName]) { return; } @@ -73,13 +87,13 @@ export function applyKeyframeAnimation>( each(keyframes, kf => { // Stop current animation. const animators = el.animators; - const kfValues = propName ? kf[propName] : kf; + const kfValues = targetPropName ? kf[targetPropName] : kf; if (!kfValues) { return; } let propKeys = keys(kfValues); - if (!propName) { + if (!targetPropName) { // PENDING performance? propKeys = filter( propKeys, key => key !== 'percent' && key !== 'easing' @@ -97,7 +111,7 @@ export function applyKeyframeAnimation>( } if (!animator) { - animator = el.animate(propName, animationOpts.loop); + animator = el.animate(targetPropName, animationOpts.loop); animator.scope = 'keyframe'; } for (let i = 0; i < animators.length; i++) { @@ -107,6 +121,14 @@ export function applyKeyframeAnimation>( } } + targetPropName && (stateToRestore[targetPropName] = stateToRestore[targetPropName] || {}); + + const savedTarget = targetPropName ? stateToRestore[targetPropName] : stateToRestore; + each(propKeys, key => { + // Save original value. + savedTarget[key] = ((targetPropName ? (el as any)[targetPropName] : el) || {})[key]; + }); + animator.whenWithKeys(duration * kf.percent, kfValues, propKeys, kf.easing); }); if (!animator) { diff --git a/src/component/graphic/GraphicView.ts b/src/component/graphic/GraphicView.ts index 4ca8b95c37..f790db8a81 100644 --- a/src/component/graphic/GraphicView.ts +++ b/src/component/graphic/GraphicView.ts @@ -45,7 +45,10 @@ import { updateLeaveTo } from '../../animation/customGraphicTransition'; import { updateProps } from '../../animation/basicTrasition'; -import { applyKeyframeAnimation } from '../../animation/customGraphicKeyframeAnimation'; +import { + applyKeyframeAnimation, + stopPreviousKeyframeAnimationAndRestore +} from '../../animation/customGraphicKeyframeAnimation'; const nonShapeGraphicElements = { // Reserved but not supported in graphic component. @@ -154,6 +157,7 @@ export class GraphicComponentView extends ComponentView { // Remove unnecessary props to avoid potential problems. const elOptionCleaned = getCleanedElOption(elOption); + // For simple, do not support parent change, otherwise reorder is needed. if (__DEV__) { elExisting && zrUtil.assert( @@ -171,6 +175,8 @@ export class GraphicComponentView extends ComponentView { } else { el && (inner(el).isNew = false); + // Stop and restore before update any other attributes. + stopPreviousKeyframeAnimationAndRestore(el); } if (el) { applyUpdateTransition( diff --git a/test/graphic-animation-wave.html b/test/graphic-animation-wave.html index c2477a1d11..44a48648d8 100644 --- a/test/graphic-animation-wave.html +++ b/test/graphic-animation-wave.html @@ -101,14 +101,6 @@ loop: true, delay: (rand - 1) * 4000, keyframes: [{ - percent: 0., - easing: 'sinusoidalInOut', - style: { - fill: config.color1 - }, - scaleX: 1, - scaleY: 1 - }, { percent: 0.5, easing: 'sinusoidalInOut', style: { diff --git a/test/graphic-animation.html b/test/graphic-animation.html index 2cb5ec6dfc..4ebf2dbb7b 100644 --- a/test/graphic-animation.html +++ b/test/graphic-animation.html @@ -44,7 +44,7 @@ - + + + + diff --git a/test/graphic-cases.html b/test/graphic-cases.html index 625a1c1b68..8b913ed9ba 100644 --- a/test/graphic-cases.html +++ b/test/graphic-cases.html @@ -38,17 +38,18 @@
-
+
+
+ From 58f96bdc516525f8eac4c2d06cf09d12f7de6c52 Mon Sep 17 00:00:00 2001 From: pissang Date: Sun, 5 Dec 2021 23:58:39 +0800 Subject: [PATCH 22/35] feat(animation): force set duration regardless the kfs --- src/animation/customGraphicKeyframeAnimation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/animation/customGraphicKeyframeAnimation.ts b/src/animation/customGraphicKeyframeAnimation.ts index 609efd3e1d..a58531369e 100644 --- a/src/animation/customGraphicKeyframeAnimation.ts +++ b/src/animation/customGraphicKeyframeAnimation.ts @@ -150,6 +150,6 @@ export function applyKeyframeAnimation>( animator .delay(animationOpts.delay || 0) - .start(animationOpts.easing); + .start(animationOpts.easing, duration); }); } \ No newline at end of file From 72ea7a6897d650a91f853fba82a22a279f23c150 Mon Sep 17 00:00:00 2001 From: pissang Date: Fri, 10 Dec 2021 20:48:38 +0800 Subject: [PATCH 23/35] feat(graphic): support clipPath --- .../customGraphicKeyframeAnimation.ts | 19 ++++-- src/component/graphic/GraphicModel.ts | 9 ++- src/component/graphic/GraphicView.ts | 65 +++++++++++++++---- src/util/model.ts | 5 +- 4 files changed, 77 insertions(+), 21 deletions(-) diff --git a/src/animation/customGraphicKeyframeAnimation.ts b/src/animation/customGraphicKeyframeAnimation.ts index a58531369e..0de270af56 100644 --- a/src/animation/customGraphicKeyframeAnimation.ts +++ b/src/animation/customGraphicKeyframeAnimation.ts @@ -91,10 +91,21 @@ export function applyKeyframeAnimation>( let animator: ReturnType; let endFrameIsSet = false; + + // Sort keyframes by percent. + keyframes.sort((a, b) => a.percent - b.percent); + each(keyframes, kf => { // Stop current animation. const animators = el.animators; const kfValues = targetPropName ? kf[targetPropName] : kf; + + if (__DEV__) { + if (kf.percent >= 1) { + endFrameIsSet = true; + } + } + if (!kfValues) { return; } @@ -111,14 +122,8 @@ export function applyKeyframeAnimation>( return; } - if (__DEV__) { - if (kf.percent >= 1) { - endFrameIsSet = true; - } - } - if (!animator) { - animator = el.animate(targetPropName, animationOpts.loop); + animator = el.animate(targetPropName, animationOpts.loop, true); animator.scope = 'keyframe'; } for (let i = 0; i < animators.length; i++) { diff --git a/src/component/graphic/GraphicModel.ts b/src/component/graphic/GraphicModel.ts index bf619a1931..5f32583973 100644 --- a/src/component/graphic/GraphicModel.ts +++ b/src/component/graphic/GraphicModel.ts @@ -110,7 +110,11 @@ interface GraphicComponentBaseElementOption extends */ info?: GraphicExtraElementInfo; - textContent?: GraphicComponentTextOption; + + // `false` means remove the clipPath + clipPath?: Omit | false; + + textContent?: Omit; textConfig?: ElementTextConfig; $action?: 'merge' | 'replace' | 'remove'; @@ -270,6 +274,9 @@ function mergeNewElOptionToExist( copyTransitionInfo(newElOption, existElOption, 'shape'); copyTransitionInfo(newElOption, existElOption, 'style'); copyTransitionInfo(newElOption, existElOption, 'extra'); + + // Copy clipPath + newElOption.clipPath = existElOption.clipPath; } else { existList[index] = newElOptCopy; diff --git a/src/component/graphic/GraphicView.ts b/src/component/graphic/GraphicView.ts index 131a223bd6..7dadea845d 100644 --- a/src/component/graphic/GraphicView.ts +++ b/src/component/graphic/GraphicView.ts @@ -67,6 +67,7 @@ export const inner = modelUtil.makeInner<{ height: number; isNew: boolean; id: string; + type: string; option: GraphicComponentElementOption }, Element>(); // ------------------------ @@ -167,7 +168,9 @@ export class GraphicComponentView extends ComponentView { } const $action = elOption.$action || 'merge'; - if ($action === 'merge') { + const isMerge = $action === 'merge'; + const isReplace = $action === 'replace'; + if (isMerge) { const isInit = !elExisting; let el = elExisting; if (isInit) { @@ -188,7 +191,7 @@ export class GraphicComponentView extends ComponentView { updateZ(el, elOption, globalZ, globalZLevel); } } - else if ($action === 'replace') { + else if (isReplace) { removeEl(elExisting, elOption, elMap, graphicModel); const el = createEl(id, targetElParent, elOption.type, elMap); if (el) { @@ -209,18 +212,49 @@ export class GraphicComponentView extends ComponentView { const el = elMap.get(id); if (el && textContentOption) { - if ($action === 'merge') { + if (isMerge) { const textContentExisting = el.getTextContent(); textContentExisting ? textContentExisting.attr(textContentOption) : el.setTextContent(new graphicUtil.Text(textContentOption)); } - else if ($action === 'replace') { + else if (isReplace) { el.setTextContent(new graphicUtil.Text(textContentOption)); } } if (el) { + const clipPathOption = elOption.clipPath; + if (clipPathOption) { + const clipPathType = clipPathOption.type; + let clipPath: graphicUtil.Path; + let isInit = false; + if (isMerge) { + const oldClipPath = el.getClipPath(); + isInit = !oldClipPath + || inner(oldClipPath).type !== clipPathType; + clipPath = isInit ? newEl(clipPathType) as graphicUtil.Path : oldClipPath; + } + else if (isReplace) { + isInit = true; + clipPath = newEl(clipPathType) as graphicUtil.Path; + } + + el.setClipPath(clipPath); + + applyUpdateTransition( + clipPath, + clipPathOption, + graphicModel, + { isInit} + ); + applyKeyframeAnimation( + clipPath, + clipPathOption.keyframeAnimation, + graphicModel + ); + } + const elInner = inner(el); el.setTextConfig(textConfig); @@ -345,13 +379,8 @@ export class GraphicComponentView extends ComponentView { this._clear(); } } -function createEl( - id: string, - targetElParent: graphicUtil.Group, - graphicType: string, - elMap: ElementMap -): Element { +function newEl(graphicType: string) { if (__DEV__) { zrUtil.assert(graphicType, 'graphic type MUST be set'); } @@ -365,10 +394,22 @@ function createEl( ) as { new(opt: GraphicComponentElementOption): Element; }; if (__DEV__) { - zrUtil.assert(Clz, 'graphic type can not be found'); + zrUtil.assert(Clz, `graphic type ${graphicType} can not be found`); } const el = new Clz({}); + inner(el).type = graphicType; + return el; +} +function createEl( + id: string, + targetElParent: graphicUtil.Group, + graphicType: string, + elMap: ElementMap +): Element { + + const el = newEl(graphicType); + targetElParent.add(el); elMap.set(id, el); inner(el).id = id; @@ -413,7 +454,7 @@ function getCleanedElOption( ): Omit { elOption = zrUtil.extend({}, elOption); zrUtil.each( - ['id', 'parentId', '$action', 'hv', 'bounding', 'textContent'].concat(layoutUtil.LOCATION_PARAMS), + ['id', 'parentId', '$action', 'hv', 'bounding', 'textContent', 'clipPath'].concat(layoutUtil.LOCATION_PARAMS), function (name) { delete (elOption as any)[name]; } diff --git a/src/util/model.ts b/src/util/model.ts index 9f78c1583e..2495723049 100644 --- a/src/util/model.ts +++ b/src/util/model.ts @@ -51,9 +51,12 @@ import SeriesModel from '../model/Series'; import CartesianAxisModel from '../coord/cartesian/AxisModel'; import GridModel from '../coord/cartesian/GridModel'; import { isNumeric, getRandomIdBase, getPrecision, round } from './number'; -import { interpolateNumber } from 'zrender/src/animation/Animator'; import { warn } from './log'; +function interpolateNumber(p0: number, p1: number, percent: number): number { + return (p1 - p0) * percent + p0; +} + /** * Make the name displayable. But we should * make sure it is not duplicated with user From e8f0c74e4cb5a90c703c39f47b20101e887c9325 Mon Sep 17 00:00:00 2001 From: pissang Date: Mon, 13 Dec 2021 23:13:41 +0800 Subject: [PATCH 24/35] feat(emphasis): support gradient color highlight --- src/util/states.ts | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/src/util/states.ts b/src/util/states.ts index 3cff89901f..29179f4b2d 100644 --- a/src/util/states.ts +++ b/src/util/states.ts @@ -38,7 +38,18 @@ import { DownplayPayload, ComponentMainType } from './types'; -import { extend, indexOf, isArrayLike, isObject, keys, isArray, each } from 'zrender/src/core/util'; +import { + extend, + indexOf, + isArrayLike, + isObject, + keys, + isArray, + each, + isString, + isGradientObject, + map +} from 'zrender/src/core/util'; import { getECData } from './innerStore'; import * as colorTool from 'zrender/src/tool/color'; import SeriesData from '../data/SeriesData'; @@ -96,16 +107,25 @@ function hasFillOrStroke(fillOrStroke: string | PatternObject | GradientObject) } // Most lifted color are duplicated. const liftedColorCache = new LRU(100); -function liftColor(color: string): string { - if (typeof color !== 'string') { - return color; - } - let liftedColor = liftedColorCache.get(color); - if (!liftedColor) { - liftedColor = colorTool.lift(color, -0.1); - liftedColorCache.put(color, liftedColor); +function liftColor(color: GradientObject): GradientObject; +function liftColor(color: string): string; +function liftColor(color: string | GradientObject): string | GradientObject { + if (isString(color)) { + let liftedColor = liftedColorCache.get(color); + if (!liftedColor) { + liftedColor = colorTool.lift(color, -0.1); + liftedColorCache.put(color, liftedColor); + } + return liftedColor; + } + else if (isGradientObject(color)) { + const ret = extend({}, color) as GradientObject; + ret.colorStops = map(color.colorStops, stop => ({ + offset: stop.offset, + color: colorTool.lift(stop.color, -0.1) + })); + return ret; } - return liftedColor; } function doChangeHoverState(el: ECElement, stateName: DisplayState, hoverStateEnum: 0 | 1 | 2) { From c59ebb72f64a8860af505a7d5344dfbf09fb4b66 Mon Sep 17 00:00:00 2001 From: pissang Date: Tue, 14 Dec 2021 15:01:16 +0800 Subject: [PATCH 25/35] refact(animation): use new duration api --- src/animation/customGraphicKeyframeAnimation.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/animation/customGraphicKeyframeAnimation.ts b/src/animation/customGraphicKeyframeAnimation.ts index 0de270af56..9ac59e1f36 100644 --- a/src/animation/customGraphicKeyframeAnimation.ts +++ b/src/animation/customGraphicKeyframeAnimation.ts @@ -155,6 +155,7 @@ export function applyKeyframeAnimation>( animator .delay(animationOpts.delay || 0) - .start(animationOpts.easing, duration); + .duration(duration) + .start(animationOpts.easing); }); } \ No newline at end of file From 343ec4990f6ac36a81a2968d5c7d07655c256ae7 Mon Sep 17 00:00:00 2001 From: pissang Date: Wed, 15 Dec 2021 15:53:28 +0800 Subject: [PATCH 26/35] fix(graphic): fix events --- src/component/graphic/GraphicModel.ts | 20 +++----------------- src/component/graphic/GraphicView.ts | 21 ++++++++++++++++++--- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/component/graphic/GraphicModel.ts b/src/component/graphic/GraphicModel.ts index 5f32583973..a6ab11ff51 100644 --- a/src/component/graphic/GraphicModel.ts +++ b/src/component/graphic/GraphicModel.ts @@ -41,30 +41,16 @@ import { TransitionOptionMixin } from '../../animation/customGraphicTransition'; import { ElementKeyframeAnimationOption } from '../../animation/customGraphicKeyframeAnimation'; import { GroupProps } from 'zrender/src/graphic/Group'; import { TransformProp } from 'zrender/src/core/Transformable'; +import { ElementEventNameWithOn } from 'zrender/src/core/types'; interface GraphicComponentBaseElementOption extends Partial>, /** * left/right/top/bottom: (like 12, '22%', 'center', default undefined) diff --git a/src/component/graphic/GraphicView.ts b/src/component/graphic/GraphicView.ts index 7dadea845d..b5c341c487 100644 --- a/src/component/graphic/GraphicView.ts +++ b/src/component/graphic/GraphicView.ts @@ -188,7 +188,7 @@ export class GraphicComponentView extends ComponentView { graphicModel, { isInit } ); - updateZ(el, elOption, globalZ, globalZLevel); + updateCommonAttrs(el, elOption, globalZ, globalZLevel); } } else if (isReplace) { @@ -201,7 +201,7 @@ export class GraphicComponentView extends ComponentView { graphicModel, { isInit: true} ); - updateZ(el, elOption, globalZ, globalZLevel); + updateCommonAttrs(el, elOption, globalZ, globalZLevel); } } else if ($action === 'remove') { @@ -433,7 +433,12 @@ function removeEl( } } -function updateZ(el: Element, elOption: GraphicComponentElementOption, defaultZ: number, defaultZlevel: number) { +function updateCommonAttrs( + el: Element, + elOption: GraphicComponentElementOption, + defaultZ: number, + defaultZlevel: number +) { if (el.isGroup) { return; } @@ -447,6 +452,16 @@ function updateZ(el: Element, elOption: GraphicComponentElementOption, defaultZ: const optZ2 = (elOption as GraphicComponentDisplayableOption).z2; optZ2 != null && (elDisplayable.z2 = optZ2 || 0); + zrUtil.each(zrUtil.keys(elOption), key => { + const val = (elOption as any)[key]; + // Assign event handlers. + // PENDING: should enumerate all event names or use pattern matching? + if (key.indexOf('on') === 0 && zrUtil.isFunction(val)) { + (el as any)[key] = val; + } + }); + el.draggable = elOption.draggable; + } // Remove unnecessary props to avoid potential problems. function getCleanedElOption( From a3d537f0446418e777664016f555f89eb9ac4f57 Mon Sep 17 00:00:00 2001 From: pissang Date: Thu, 16 Dec 2021 10:43:19 +0800 Subject: [PATCH 27/35] fix(graphic): fix group event not work --- src/component/graphic/GraphicView.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/component/graphic/GraphicView.ts b/src/component/graphic/GraphicView.ts index b5c341c487..51f42ae13f 100644 --- a/src/component/graphic/GraphicView.ts +++ b/src/component/graphic/GraphicView.ts @@ -439,19 +439,17 @@ function updateCommonAttrs( defaultZ: number, defaultZlevel: number ) { - if (el.isGroup) { - return; + if (!el.isGroup) { + const elDisplayable = el as Displayable; + // We should not support configure z and zlevel in the element level. + // But seems we didn't limit it previously. So here still use it to avoid breaking. + elDisplayable.z = zrUtil.retrieve2((elOption as any).z, defaultZ || 0); + elDisplayable.zlevel = zrUtil.retrieve2((elOption as any).zlevel, defaultZlevel || 0); + // z2 must not be null/undefined, otherwise sort error may occur. + const optZ2 = (elOption as GraphicComponentDisplayableOption).z2; + optZ2 != null && (elDisplayable.z2 = optZ2 || 0); } - const elDisplayable = el as Displayable; - // We should not support configure z and zlevel in the element level. - // But seems we didn't limit it previously. So here still use it to avoid breaking. - elDisplayable.z = zrUtil.retrieve2((elOption as any).z, defaultZ || 0); - elDisplayable.zlevel = zrUtil.retrieve2((elOption as any).zlevel, defaultZlevel || 0); - // z2 must not be null/undefined, otherwise sort error may occur. - const optZ2 = (elOption as GraphicComponentDisplayableOption).z2; - optZ2 != null && (elDisplayable.z2 = optZ2 || 0); - zrUtil.each(zrUtil.keys(elOption), key => { const val = (elOption as any)[key]; // Assign event handlers. From 35d1ac8d353c185900870b3b2b5d4cea1fb48ad8 Mon Sep 17 00:00:00 2001 From: pissang Date: Thu, 16 Dec 2021 11:32:42 +0800 Subject: [PATCH 28/35] fix(gauge): display on the start point for NaN value --- src/chart/gauge/GaugeView.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/chart/gauge/GaugeView.ts b/src/chart/gauge/GaugeView.ts index d81e10ba59..46173183db 100644 --- a/src/chart/gauge/GaugeView.ts +++ b/src/chart/gauge/GaugeView.ts @@ -421,11 +421,15 @@ class GaugeView extends ChartView { if (showProgress || showPointer) { data.diff(oldData) .add(function (idx) { + const val = data.get(valueDim, idx) as number; if (showPointer) { const pointer = createPointer(idx, startAngle); + // TODO hide pointer on NaN value? graphic.initProps(pointer, { - rotation: -(linearMap(data.get(valueDim, idx) as number, valueExtent, angleExtent, true) - + Math.PI / 2) + rotation: -( + (isNaN(+val) ? angleExtent[0] : linearMap(val, valueExtent, angleExtent, true)) + + Math.PI / 2 + ) }, seriesModel); group.add(pointer); data.setItemGraphicEl(idx, pointer); @@ -436,7 +440,7 @@ class GaugeView extends ChartView { const isClip = progressModel.get('clip'); graphic.initProps(progress, { shape: { - endAngle: linearMap(data.get(valueDim, idx) as number, valueExtent, angleExtent, isClip) + endAngle: linearMap(val, valueExtent, angleExtent, isClip) } }, seriesModel); group.add(progress); @@ -447,6 +451,7 @@ class GaugeView extends ChartView { } }) .update(function (newIdx, oldIdx) { + const val = data.get(valueDim, newIdx) as number; if (showPointer) { const previousPointer = oldData.getItemGraphicEl(oldIdx) as PointerPath; const previousRotate = previousPointer ? previousPointer.rotation : startAngle; @@ -454,7 +459,7 @@ class GaugeView extends ChartView { pointer.rotation = previousRotate; graphic.updateProps(pointer, { rotation: -( - linearMap(data.get(valueDim, newIdx) as number, valueExtent, angleExtent, true) + (isNaN(+val) ? angleExtent[0] : linearMap(val, valueExtent, angleExtent, true)) + Math.PI / 2 ) }, seriesModel); @@ -469,9 +474,7 @@ class GaugeView extends ChartView { const isClip = progressModel.get('clip'); graphic.updateProps(progress, { shape: { - endAngle: linearMap( - data.get(valueDim, newIdx) as number, valueExtent, angleExtent, isClip - ) + endAngle: linearMap(val, valueExtent, angleExtent, isClip) } }, seriesModel); group.add(progress); From 79d98b1d6d6214f9941af1f0557a3fa23c9aabbb Mon Sep 17 00:00:00 2001 From: pissang Date: Fri, 17 Dec 2021 13:26:52 +0800 Subject: [PATCH 29/35] fix(animation): fix new added transform props not work --- src/animation/customGraphicTransition.ts | 28 ++++++++++-------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/animation/customGraphicTransition.ts b/src/animation/customGraphicTransition.ts index cfdd8ea49c..1f58a95b3a 100644 --- a/src/animation/customGraphicTransition.ts +++ b/src/animation/customGraphicTransition.ts @@ -21,7 +21,7 @@ import Element, { ElementAnimateConfig, ElementProps } from 'zrender/src/Element'; import { makeInner, normalizeToArray } from '../util/model'; -import { assert, bind, each, eqNaN, extend, hasOwn, indexOf, isArrayLike, keys } from 'zrender/src/core/util'; +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'; @@ -30,7 +30,8 @@ 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'; +import { PathStyleProps } from 'zrender/src/graphic/Path'; +import { TRANSFORMABLE_PROPS, TransformProp } from 'zrender/src/core/Transformable'; const LEGACY_TRANSFORM_PROPS_MAP = { position: ['x', 'y'], @@ -39,18 +40,11 @@ const LEGACY_TRANSFORM_PROPS_MAP = { } as const; const LEGACY_TRANSFORM_PROPS = keys(LEGACY_TRANSFORM_PROPS_MAP); -const TRANSFORM_PROPS_MAP = { - x: 1, - y: 1, - scaleX: 1, - scaleY: 1, - originX: 1, - originY: 1, - rotation: 1 -} as const; -type TransformProp = keyof typeof TRANSFORM_PROPS_MAP; -const TRANSFORM_PROPS = keys(TRANSFORM_PROPS_MAP); -const transformPropNamesStr = TRANSFORM_PROPS.join(', '); +const TRANSFORM_PROPS_MAP = reduce(TRANSFORMABLE_PROPS, (obj, key) => { + obj[key] = 1; + return obj; +}, {} as Record); +const transformPropNamesStr = TRANSFORMABLE_PROPS.join(', '); // '' means root export const ELEMENT_ANIMATABLE_PROPS = ['', 'style', 'shape', 'extra'] as const; @@ -526,7 +520,7 @@ function prepareTransformTransitionFrom( ): void { const transition = elOption.transition; const transitionKeys = isTransitionAll(transition) - ? TRANSFORM_PROPS + ? TRANSFORMABLE_PROPS : normalizeToArray(transition || []); for (let i = 0; i < transitionKeys.length; i++) { const key = transitionKeys[i]; @@ -557,8 +551,8 @@ function prepareTransformAllPropsFinal( } } - for (let i = 0; i < TRANSFORM_PROPS.length; i++) { - const key = TRANSFORM_PROPS[i]; + for (let i = 0; i < TRANSFORMABLE_PROPS.length; i++) { + const key = TRANSFORMABLE_PROPS[i]; if (elOption[key] != null) { allProps[key] = elOption[key]; } From 14b0b8e7d42111fd12ca8f17cdb2018c492a7a06 Mon Sep 17 00:00:00 2001 From: pissang Date: Fri, 17 Dec 2021 19:44:23 +0800 Subject: [PATCH 30/35] style: optimize code according to the review --- src/animation/customGraphicKeyframeAnimation.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/animation/customGraphicKeyframeAnimation.ts b/src/animation/customGraphicKeyframeAnimation.ts index 9ac59e1f36..68b58282ce 100644 --- a/src/animation/customGraphicKeyframeAnimation.ts +++ b/src/animation/customGraphicKeyframeAnimation.ts @@ -19,7 +19,7 @@ import { AnimationEasing } from 'zrender/src/animation/easing'; import Element from 'zrender/src/Element'; -import { keys, filter, each, isArray } from 'zrender/src/core/util'; +import { keys, filter, each, isArray, indexOf } from 'zrender/src/core/util'; import { ELEMENT_ANIMATABLE_PROPS } from './customGraphicTransition'; import { AnimationOption, AnimationOptionMixin, Dictionary } from '../util/types'; import { Model } from '../echarts.all'; @@ -37,6 +37,8 @@ type AnimationKeyframe> = T & { type StateToRestore = Dictionary; const getStateToRestore = makeInner(); +const KEYFRAME_EXCLUDE_KEYS = ['percent', 'easing', 'shape', 'style', 'extra'] as const; + export interface ElementKeyframeAnimationOption> extends AnimationOption { // Animation configuration for keyframe based animation. loop?: boolean @@ -44,7 +46,7 @@ export interface ElementKeyframeAnimationOption>( let propKeys = keys(kfValues); if (!targetPropName) { // PENDING performance? - propKeys = filter( - propKeys, key => key !== 'percent' && key !== 'easing' - && key !== 'shape' && key !== 'style' && key !== 'extra' - ); + propKeys = filter(propKeys, key => indexOf(KEYFRAME_EXCLUDE_KEYS, key) < 0); } if (!propKeys.length) { return; From 19e8bedfd9a46615ccd9d65953e3fe1af9a8493c Mon Sep 17 00:00:00 2001 From: pissang Date: Mon, 20 Dec 2021 10:47:09 +0800 Subject: [PATCH 31/35] test(graphic): add vrt recordings. --- test/custom-animation.html | 174 ++++++++++++++++++ test/graphic-animation.html | 3 + test/graphic-transition.html | 2 +- test/runTest/actions/__meta__.json | 3 + .../actions/graphic-animation-wave.json | 1 + test/runTest/actions/graphic-animation.json | 1 + test/runTest/actions/graphic-transition.json | 1 + 7 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 test/custom-animation.html create mode 100644 test/runTest/actions/graphic-animation-wave.json create mode 100644 test/runTest/actions/graphic-animation.json create mode 100644 test/runTest/actions/graphic-transition.json diff --git a/test/custom-animation.html b/test/custom-animation.html new file mode 100644 index 0000000000..29131bdd93 --- /dev/null +++ b/test/custom-animation.html @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + + + diff --git a/test/graphic-animation.html b/test/graphic-animation.html index 7ddea9405a..d112dd8e9e 100644 --- a/test/graphic-animation.html +++ b/test/graphic-animation.html @@ -101,6 +101,9 @@ x: greenCirclePosition, y: 50, transition: 'all', + updateAnimation: { + duration: 2000 + }, shape: { cx: 0, cy: 0, diff --git a/test/graphic-transition.html b/test/graphic-transition.html index 2954f3f958..4f057d39ab 100644 --- a/test/graphic-transition.html +++ b/test/graphic-transition.html @@ -328,7 +328,7 @@