|  | /* | 
|  | * 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 {createSymbol, normalizeSymbolOffset, normalizeSymbolSize} from '../../util/symbol'; | 
|  | import {Group, Path} from '../../util/graphic'; | 
|  | import { enterEmphasis, leaveEmphasis, toggleHoverEmphasis } from '../../util/states'; | 
|  | import SymbolClz from './Symbol'; | 
|  | import SeriesData from '../../data/SeriesData'; | 
|  | import type { ZRColor, ECElement } from '../../util/types'; | 
|  | import type Displayable from 'zrender/src/graphic/Displayable'; | 
|  | import { SymbolDrawItemModelOption } from './SymbolDraw'; | 
|  |  | 
|  | interface RippleEffectCfg { | 
|  | showEffectOn?: 'emphasis' | 'render' | 
|  | rippleScale?: number | 
|  | brushType?: 'fill' | 'stroke' | 
|  | period?: number | 
|  | effectOffset?: number | 
|  | z?: number | 
|  | zlevel?: number | 
|  | symbolType?: string | 
|  | color?: ZRColor | 
|  | rippleEffectColor?: ZRColor, | 
|  | rippleNumber?: number | 
|  | } | 
|  |  | 
|  | function updateRipplePath(rippleGroup: Group, effectCfg: RippleEffectCfg) { | 
|  | const color = effectCfg.rippleEffectColor || effectCfg.color; | 
|  | rippleGroup.eachChild(function (ripplePath: Displayable) { | 
|  | ripplePath.attr({ | 
|  | z: effectCfg.z, | 
|  | zlevel: effectCfg.zlevel, | 
|  | style: { | 
|  | stroke: effectCfg.brushType === 'stroke' ? color : null, | 
|  | fill: effectCfg.brushType === 'fill' ? color : null | 
|  | } | 
|  | }); | 
|  | }); | 
|  | } | 
|  |  | 
|  | class EffectSymbol extends Group { | 
|  |  | 
|  | private _effectCfg: RippleEffectCfg; | 
|  |  | 
|  | constructor(data: SeriesData, idx: number) { | 
|  | super(); | 
|  |  | 
|  | const symbol = new SymbolClz(data, idx); | 
|  | const rippleGroup = new Group(); | 
|  | this.add(symbol); | 
|  | this.add(rippleGroup); | 
|  |  | 
|  | this.updateData(data, idx); | 
|  | } | 
|  |  | 
|  |  | 
|  | stopEffectAnimation() { | 
|  | (this.childAt(1) as Group).removeAll(); | 
|  | } | 
|  |  | 
|  | startEffectAnimation(effectCfg: RippleEffectCfg) { | 
|  | const symbolType = effectCfg.symbolType; | 
|  | const color = effectCfg.color; | 
|  | const rippleNumber = effectCfg.rippleNumber; | 
|  | const rippleGroup = this.childAt(1) as Group; | 
|  |  | 
|  | for (let i = 0; i < rippleNumber; i++) { | 
|  | // If width/height are set too small (e.g., set to 1) on ios10 | 
|  | // and macOS Sierra, a circle stroke become a rect, no matter what | 
|  | // the scale is set. So we set width/height as 2. See #4136. | 
|  | const ripplePath = createSymbol( | 
|  | symbolType, -1, -1, 2, 2, color | 
|  | ); | 
|  | ripplePath.attr({ | 
|  | style: { | 
|  | strokeNoScale: true | 
|  | }, | 
|  | z2: 99, | 
|  | silent: true, | 
|  | scaleX: 0.5, | 
|  | scaleY: 0.5 | 
|  | }); | 
|  |  | 
|  | const delay = -i / rippleNumber * effectCfg.period + effectCfg.effectOffset; | 
|  | ripplePath.animate('', true) | 
|  | .when(effectCfg.period, { | 
|  | scaleX: effectCfg.rippleScale / 2, | 
|  | scaleY: effectCfg.rippleScale / 2 | 
|  | }) | 
|  | .delay(delay) | 
|  | .start(); | 
|  | ripplePath.animateStyle(true) | 
|  | .when(effectCfg.period, { | 
|  | opacity: 0 | 
|  | }) | 
|  | .delay(delay) | 
|  | .start(); | 
|  |  | 
|  | rippleGroup.add(ripplePath); | 
|  | } | 
|  |  | 
|  | updateRipplePath(rippleGroup, effectCfg); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Update effect symbol | 
|  | */ | 
|  | updateEffectAnimation(effectCfg: RippleEffectCfg) { | 
|  | const oldEffectCfg = this._effectCfg; | 
|  | const rippleGroup = this.childAt(1) as Group; | 
|  |  | 
|  | // Must reinitialize effect if following configuration changed | 
|  | const DIFFICULT_PROPS = ['symbolType', 'period', 'rippleScale', 'rippleNumber'] as const; | 
|  | for (let i = 0; i < DIFFICULT_PROPS.length; i++) { | 
|  | const propName = DIFFICULT_PROPS[i]; | 
|  | if (oldEffectCfg[propName] !== effectCfg[propName]) { | 
|  | this.stopEffectAnimation(); | 
|  | this.startEffectAnimation(effectCfg); | 
|  | return; | 
|  | } | 
|  | } | 
|  |  | 
|  | updateRipplePath(rippleGroup, effectCfg); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Highlight symbol | 
|  | */ | 
|  | highlight() { | 
|  | enterEmphasis(this); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Downplay symbol | 
|  | */ | 
|  | downplay() { | 
|  | leaveEmphasis(this); | 
|  | } | 
|  |  | 
|  | getSymbolType() { | 
|  | const symbol = this.childAt(0) as SymbolClz; | 
|  | return symbol && symbol.getSymbolType(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Update symbol properties | 
|  | */ | 
|  | updateData(data: SeriesData, idx: number) { | 
|  | const seriesModel = data.hostModel; | 
|  |  | 
|  | (this.childAt(0) as SymbolClz).updateData(data, idx); | 
|  |  | 
|  | const rippleGroup = this.childAt(1); | 
|  | const itemModel = data.getItemModel<SymbolDrawItemModelOption>(idx); | 
|  | const symbolType = data.getItemVisual(idx, 'symbol'); | 
|  | const symbolSize = normalizeSymbolSize(data.getItemVisual(idx, 'symbolSize')); | 
|  |  | 
|  | const symbolStyle = data.getItemVisual(idx, 'style'); | 
|  | const color = symbolStyle && symbolStyle.fill; | 
|  | const emphasisModel = itemModel.getModel('emphasis'); | 
|  |  | 
|  | rippleGroup.setScale(symbolSize); | 
|  |  | 
|  | rippleGroup.traverse(function (ripplePath: Path) { | 
|  | ripplePath.setStyle('fill', color); | 
|  | }); | 
|  |  | 
|  | const symbolOffset = normalizeSymbolOffset(data.getItemVisual(idx, 'symbolOffset'), symbolSize); | 
|  | if (symbolOffset) { | 
|  | rippleGroup.x = symbolOffset[0]; | 
|  | rippleGroup.y = symbolOffset[1]; | 
|  | } | 
|  |  | 
|  | const symbolRotate = data.getItemVisual(idx, 'symbolRotate'); | 
|  | rippleGroup.rotation = (symbolRotate || 0) * Math.PI / 180 || 0; | 
|  |  | 
|  | const effectCfg: RippleEffectCfg = {}; | 
|  |  | 
|  | effectCfg.showEffectOn = seriesModel.get('showEffectOn'); | 
|  | effectCfg.rippleScale = itemModel.get(['rippleEffect', 'scale']); | 
|  | effectCfg.brushType = itemModel.get(['rippleEffect', 'brushType']); | 
|  | effectCfg.period = itemModel.get(['rippleEffect', 'period']) * 1000; | 
|  | effectCfg.effectOffset = idx / data.count(); | 
|  | effectCfg.z = seriesModel.getShallow('z') || 0; | 
|  | effectCfg.zlevel = seriesModel.getShallow('zlevel') || 0; | 
|  | effectCfg.symbolType = symbolType; | 
|  | effectCfg.color = color; | 
|  | effectCfg.rippleEffectColor = itemModel.get(['rippleEffect', 'color']); | 
|  | effectCfg.rippleNumber = itemModel.get(['rippleEffect', 'number']); | 
|  |  | 
|  | if (effectCfg.showEffectOn === 'render') { | 
|  | this._effectCfg | 
|  | ? this.updateEffectAnimation(effectCfg) | 
|  | : this.startEffectAnimation(effectCfg); | 
|  |  | 
|  | this._effectCfg = effectCfg; | 
|  | } | 
|  | else { | 
|  | // Not keep old effect config | 
|  | this._effectCfg = null; | 
|  |  | 
|  | this.stopEffectAnimation(); | 
|  |  | 
|  | (this as ECElement).onHoverStateChange = (toState) => { | 
|  | if (toState === 'emphasis') { | 
|  | if (effectCfg.showEffectOn !== 'render') { | 
|  | this.startEffectAnimation(effectCfg); | 
|  | } | 
|  | } | 
|  | else if (toState === 'normal') { | 
|  | if (effectCfg.showEffectOn !== 'render') { | 
|  | this.stopEffectAnimation(); | 
|  | } | 
|  | } | 
|  | }; | 
|  | } | 
|  |  | 
|  | this._effectCfg = effectCfg; | 
|  |  | 
|  | toggleHoverEmphasis( | 
|  | this, | 
|  | emphasisModel.get('focus'), | 
|  | emphasisModel.get('blurScope'), | 
|  | emphasisModel.get('disabled') | 
|  | ); | 
|  | }; | 
|  |  | 
|  | fadeOut(cb: () => void) { | 
|  | cb && cb(); | 
|  | }; | 
|  |  | 
|  | } | 
|  |  | 
|  | export default EffectSymbol; |