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