| /* | |
| * 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; |