| /* | |
| * 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. | |
| */ | |
| /** | |
| * Provide effect for line | |
| */ | |
| import * as graphic from '../../util/graphic'; | |
| import Line from './Line'; | |
| import * as zrUtil from 'zrender/src/core/util'; | |
| import {createSymbol} from '../../util/symbol'; | |
| import * as vec2 from 'zrender/src/core/vector'; | |
| import * as curveUtil from 'zrender/src/core/curve'; | |
| import type SeriesData from '../../data/SeriesData'; | |
| import { LineDrawSeriesScope, LineDrawModelOption } from './LineDraw'; | |
| import Model from '../../model/Model'; | |
| import { ColorString } from '../../util/types'; | |
| export type ECSymbolOnEffectLine = ReturnType<typeof createSymbol> & { | |
| __t: number | |
| __lastT: number | |
| __p1: number[] | |
| __p2: number[] | |
| __cp1: number[] | |
| }; | |
| class EffectLine extends graphic.Group { | |
| private _symbolType: string; | |
| private _period: number; | |
| private _loop: boolean; | |
| private _roundTrip: boolean; | |
| private _symbolScale: number[]; | |
| constructor(lineData: SeriesData, idx: number, seriesScope: LineDrawSeriesScope) { | |
| super(); | |
| this.add(this.createLine(lineData, idx, seriesScope)); | |
| this._updateEffectSymbol(lineData, idx); | |
| } | |
| createLine(lineData: SeriesData, idx: number, seriesScope: LineDrawSeriesScope): graphic.Group { | |
| return new Line(lineData, idx, seriesScope); | |
| } | |
| private _updateEffectSymbol(lineData: SeriesData, idx: number) { | |
| const itemModel = lineData.getItemModel<LineDrawModelOption>(idx); | |
| const effectModel = itemModel.getModel('effect'); | |
| let size = effectModel.get('symbolSize'); | |
| const symbolType = effectModel.get('symbol'); | |
| if (!zrUtil.isArray(size)) { | |
| size = [size, size]; | |
| } | |
| const lineStyle = lineData.getItemVisual(idx, 'style'); | |
| const color = effectModel.get('color') || (lineStyle && lineStyle.stroke); | |
| let symbol = this.childAt(1) as ECSymbolOnEffectLine; | |
| if (this._symbolType !== symbolType) { | |
| // Remove previous | |
| this.remove(symbol); | |
| symbol = createSymbol( | |
| symbolType, -0.5, -0.5, 1, 1, color | |
| ) as ECSymbolOnEffectLine; | |
| symbol.z2 = 100; | |
| symbol.culling = true; | |
| this.add(symbol); | |
| } | |
| // Symbol may be removed if loop is false | |
| if (!symbol) { | |
| return; | |
| } | |
| // Shadow color is same with color in default | |
| symbol.setStyle('shadowColor', color as ColorString); | |
| symbol.setStyle(effectModel.getItemStyle(['color'])); | |
| symbol.scaleX = size[0]; | |
| symbol.scaleY = size[1]; | |
| symbol.setColor(color); | |
| this._symbolType = symbolType; | |
| this._symbolScale = size; | |
| this._updateEffectAnimation(lineData, effectModel, idx); | |
| } | |
| private _updateEffectAnimation( | |
| lineData: SeriesData, | |
| effectModel: Model<LineDrawModelOption['effect']>, | |
| idx: number | |
| ) { | |
| const symbol = this.childAt(1) as ECSymbolOnEffectLine; | |
| if (!symbol) { | |
| return; | |
| } | |
| const points = lineData.getItemLayout(idx); | |
| let period = effectModel.get('period') * 1000; | |
| const loop = effectModel.get('loop'); | |
| const roundTrip = effectModel.get('roundTrip'); | |
| const constantSpeed = effectModel.get('constantSpeed'); | |
| const delayExpr = zrUtil.retrieve(effectModel.get('delay'), function (idx) { | |
| return idx / lineData.count() * period / 3; | |
| }); | |
| // Ignore when updating | |
| symbol.ignore = true; | |
| this._updateAnimationPoints(symbol, points); | |
| if (constantSpeed > 0) { | |
| period = this._getLineLength(symbol) / constantSpeed * 1000; | |
| } | |
| if (period !== this._period || loop !== this._loop || roundTrip !== this._roundTrip) { | |
| symbol.stopAnimation(); | |
| let delayNum: number; | |
| if (zrUtil.isFunction(delayExpr)) { | |
| delayNum = delayExpr(idx); | |
| } | |
| else { | |
| delayNum = delayExpr; | |
| } | |
| if (symbol.__t > 0) { | |
| delayNum = -period * symbol.__t; | |
| } | |
| this._animateSymbol( | |
| symbol, period, delayNum, loop, roundTrip | |
| ); | |
| } | |
| this._period = period; | |
| this._loop = loop; | |
| this._roundTrip = roundTrip; | |
| } | |
| private _animateSymbol( | |
| symbol: ECSymbolOnEffectLine, period: number, delayNum: number, loop: boolean, roundTrip: boolean) { | |
| if (period > 0) { | |
| symbol.__t = 0; | |
| const self = this; | |
| const animator = symbol.animate('', loop) | |
| .when(roundTrip ? period * 2 : period, { | |
| __t: roundTrip ? 2 : 1 | |
| }) | |
| .delay(delayNum) | |
| .during(function () { | |
| self._updateSymbolPosition(symbol); | |
| }); | |
| if (!loop) { | |
| animator.done(function () { | |
| self.remove(symbol); | |
| }); | |
| } | |
| animator.start(); | |
| } | |
| } | |
| protected _getLineLength(symbol: ECSymbolOnEffectLine) { | |
| // Not so accurate | |
| return (vec2.dist(symbol.__p1, symbol.__cp1) | |
| + vec2.dist(symbol.__cp1, symbol.__p2)); | |
| } | |
| protected _updateAnimationPoints(symbol: ECSymbolOnEffectLine, points: number[][]) { | |
| symbol.__p1 = points[0]; | |
| symbol.__p2 = points[1]; | |
| symbol.__cp1 = points[2] || [ | |
| (points[0][0] + points[1][0]) / 2, | |
| (points[0][1] + points[1][1]) / 2 | |
| ]; | |
| } | |
| updateData(lineData: SeriesData, idx: number, seriesScope: LineDrawSeriesScope) { | |
| (this.childAt(0) as Line).updateData(lineData, idx, seriesScope); | |
| this._updateEffectSymbol(lineData, idx); | |
| } | |
| protected _updateSymbolPosition(symbol: ECSymbolOnEffectLine) { | |
| const p1 = symbol.__p1; | |
| const p2 = symbol.__p2; | |
| const cp1 = symbol.__cp1; | |
| const t = symbol.__t < 1 ? symbol.__t : 2 - symbol.__t; | |
| const pos = [symbol.x, symbol.y]; | |
| const lastPos = pos.slice(); | |
| const quadraticAt = curveUtil.quadraticAt; | |
| const quadraticDerivativeAt = curveUtil.quadraticDerivativeAt; | |
| pos[0] = quadraticAt(p1[0], cp1[0], p2[0], t); | |
| pos[1] = quadraticAt(p1[1], cp1[1], p2[1], t); | |
| // Tangent | |
| const tx = symbol.__t < 1 ? quadraticDerivativeAt(p1[0], cp1[0], p2[0], t) | |
| : quadraticDerivativeAt(p2[0], cp1[0], p1[0], 1 - t); | |
| const ty = symbol.__t < 1 ? quadraticDerivativeAt(p1[1], cp1[1], p2[1], t) | |
| : quadraticDerivativeAt(p2[1], cp1[1], p1[1], 1 - t); | |
| symbol.rotation = -Math.atan2(ty, tx) - Math.PI / 2; | |
| // enable continuity trail for 'line', 'rect', 'roundRect' symbolType | |
| if (this._symbolType === 'line' || this._symbolType === 'rect' || this._symbolType === 'roundRect') { | |
| if (symbol.__lastT !== undefined && symbol.__lastT < symbol.__t) { | |
| symbol.scaleY = vec2.dist(lastPos, pos) * 1.05; | |
| // make sure the last segment render within endPoint | |
| if (t === 1) { | |
| pos[0] = lastPos[0] + (pos[0] - lastPos[0]) / 2; | |
| pos[1] = lastPos[1] + (pos[1] - lastPos[1]) / 2; | |
| } | |
| } | |
| else if (symbol.__lastT === 1) { | |
| // After first loop, symbol.__t does NOT start with 0, so connect p1 to pos directly. | |
| symbol.scaleY = 2 * vec2.dist(p1, pos); | |
| } | |
| else { | |
| symbol.scaleY = this._symbolScale[1]; | |
| } | |
| } | |
| symbol.__lastT = symbol.__t; | |
| symbol.ignore = false; | |
| symbol.x = pos[0]; | |
| symbol.y = pos[1]; | |
| } | |
| updateLayout(lineData: SeriesData, idx: number) { | |
| (this.childAt(0) as Line).updateLayout(lineData, idx); | |
| const effectModel = lineData.getItemModel<LineDrawModelOption>(idx).getModel('effect'); | |
| this._updateEffectAnimation(lineData, effectModel, idx); | |
| } | |
| } | |
| export default EffectLine; |