From 5056eed1eda06537640e4d2dd8037f512c15d87c Mon Sep 17 00:00:00 2001 From: Mayank Mehta Date: Fri, 17 Oct 2025 13:57:30 +0530 Subject: [PATCH 1/3] fix(lines): Prevent effect arrow from flipping direction at end of unidirectional loop. close #20216 --- src/chart/helper/EffectLine.ts | 368 ++++++++++++++--------------- src/chart/helper/EffectPolyline.ts | 172 +++++++------- 2 files changed, 270 insertions(+), 270 deletions(-) diff --git a/src/chart/helper/EffectLine.ts b/src/chart/helper/EffectLine.ts index f368d83ac0..d8ea2c8074 100644 --- a/src/chart/helper/EffectLine.ts +++ b/src/chart/helper/EffectLine.ts @@ -24,7 +24,7 @@ import * as graphic from '../../util/graphic'; import Line from './Line'; import * as zrUtil from 'zrender/src/core/util'; -import {createSymbol} from '../../util/symbol'; +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'; @@ -33,226 +33,226 @@ import Model from '../../model/Model'; import { ColorString } from '../../util/types'; export type ECSymbolOnEffectLine = ReturnType & { - __t: number - __lastT: number - __p1: number[] - __p2: number[] - __cp1: number[] + __t: number + __lastT: number + __p1: number[] + __p2: number[] + __cp1: number[] }; class EffectLine extends graphic.Group { - private _symbolType: string; + private _symbolType: string; - private _period: number; + private _period: number; - private _loop: boolean; + private _loop: boolean; - private _roundTrip: boolean; + private _roundTrip: boolean; - private _symbolScale: number[]; + private _symbolScale: number[]; - constructor(lineData: SeriesData, idx: number, seriesScope: LineDrawSeriesScope) { - super(); - this.add(this.createLine(lineData, idx, seriesScope)); + 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(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); + this._updateEffectSymbol(lineData, idx); + } - symbol = createSymbol( - symbolType, -0.5, -0.5, 1, 1, color - ) as ECSymbolOnEffectLine; - symbol.z2 = 100; - symbol.culling = true; + createLine(lineData: SeriesData, idx: number, seriesScope: LineDrawSeriesScope): graphic.Group { + return new Line(lineData, idx, seriesScope); + } - this.add(symbol); - } - - // Symbol may be removed if loop is false - if (!symbol) { - return; - } + private _updateEffectSymbol(lineData: SeriesData, idx: number) { + const itemModel = lineData.getItemModel(idx); + const effectModel = itemModel.getModel('effect'); + let size = effectModel.get('symbolSize'); + const symbolType = effectModel.get('symbol'); + if (!zrUtil.isArray(size)) { + size = [size, size]; + } - // Shadow color is same with color in default - symbol.setStyle('shadowColor', color as ColorString); - symbol.setStyle(effectModel.getItemStyle(['color'])); + const lineStyle = lineData.getItemVisual(idx, 'style'); + const color = effectModel.get('color') || (lineStyle && lineStyle.stroke); + let symbol = this.childAt(1) as ECSymbolOnEffectLine; - symbol.scaleX = size[0]; - symbol.scaleY = size[1]; + if (this._symbolType !== symbolType) { + // Remove previous + this.remove(symbol); - symbol.setColor(color); + symbol = createSymbol( + symbolType, -0.5, -0.5, 1, 1, color + ) as ECSymbolOnEffectLine; + symbol.z2 = 100; + symbol.culling = true; - this._symbolType = symbolType; - this._symbolScale = size; + this.add(symbol); + } - this._updateEffectAnimation(lineData, effectModel, idx); + // Symbol may be removed if loop is false + if (!symbol) { + return; } - private _updateEffectAnimation( - lineData: SeriesData, - effectModel: Model, - idx: number - ) { + // Shadow color is same with color in default + symbol.setStyle('shadowColor', color as ColorString); + symbol.setStyle(effectModel.getItemStyle(['color'])); - const symbol = this.childAt(1) as ECSymbolOnEffectLine; - if (!symbol) { - return; - } + symbol.scaleX = size[0]; + symbol.scaleY = size[1]; - const points = lineData.getItemLayout(idx); + symbol.setColor(color); - 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; - }); + this._symbolType = symbolType; + this._symbolScale = size; - // Ignore when updating - symbol.ignore = true; + this._updateEffectAnimation(lineData, effectModel, idx); + } - this._updateAnimationPoints(symbol, points); + private _updateEffectAnimation( + lineData: SeriesData, + effectModel: Model, + idx: number + ) { - if (constantSpeed > 0) { - period = this._getLineLength(symbol) / constantSpeed * 1000; - } + const symbol = this.childAt(1) as ECSymbolOnEffectLine; + if (!symbol) { + return; + } - 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 - ); - } + const points = lineData.getItemLayout(idx); - this._period = period; - this._loop = loop; - this._roundTrip = roundTrip; - } + 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; + }); - 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(); - } - } + // Ignore when updating + symbol.ignore = true; - protected _getLineLength(symbol: ECSymbolOnEffectLine) { - // Not so accurate - return (vec2.dist(symbol.__p1, symbol.__cp1) - + vec2.dist(symbol.__cp1, symbol.__p2)); - } + this._updateAnimationPoints(symbol, points); - 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 - ]; + if (constantSpeed > 0) { + period = this._getLineLength(symbol) / constantSpeed * 1000; } - updateData(lineData: SeriesData, idx: number, seriesScope: LineDrawSeriesScope) { - (this.childAt(0) as Line).updateData(lineData, idx, seriesScope); - this._updateEffectSymbol(lineData, idx); + 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 + ); } - 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]; - } + 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; } - symbol.__lastT = symbol.__t; - symbol.ignore = false; - symbol.x = pos[0]; - symbol.y = pos[1]; + } + 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); + updateLayout(lineData: SeriesData, idx: number) { + (this.childAt(0) as Line).updateLayout(lineData, idx); - const effectModel = lineData.getItemModel(idx).getModel('effect'); - this._updateEffectAnimation(lineData, effectModel, idx); - } + const effectModel = lineData.getItemModel(idx).getModel('effect'); + this._updateEffectAnimation(lineData, effectModel, idx); + } } export default EffectLine; diff --git a/src/chart/helper/EffectPolyline.ts b/src/chart/helper/EffectPolyline.ts index 389dbe4d83..9ed4ad4a0f 100644 --- a/src/chart/helper/EffectPolyline.ts +++ b/src/chart/helper/EffectPolyline.ts @@ -18,104 +18,104 @@ */ import Polyline from './Polyline'; -import EffectLine, {ECSymbolOnEffectLine} from './EffectLine'; +import EffectLine, { ECSymbolOnEffectLine } from './EffectLine'; import * as vec2 from 'zrender/src/core/vector'; import { LineDrawSeriesScope } from './LineDraw'; import SeriesData from '../../data/SeriesData'; class EffectPolyline extends EffectLine { - private _lastFrame = 0; - private _lastFramePercent = 0; - private _length: number; - - private _points: number[][]; - private _offsets: number[]; - - // Override - createLine(lineData: SeriesData, idx: number, seriesScope: LineDrawSeriesScope) { - return new Polyline(lineData, idx, seriesScope); - }; - - // Override - protected _updateAnimationPoints(symbol: ECSymbolOnEffectLine, points: number[][]) { - this._points = points; - const accLenArr = [0]; - let len = 0; - for (let i = 1; i < points.length; i++) { - const p1 = points[i - 1]; - const p2 = points[i]; - len += vec2.dist(p1, p2); - accLenArr.push(len); + private _lastFrame = 0; + private _lastFramePercent = 0; + private _length: number; + + private _points: number[][]; + private _offsets: number[]; + + // Override + createLine(lineData: SeriesData, idx: number, seriesScope: LineDrawSeriesScope) { + return new Polyline(lineData, idx, seriesScope); + }; + + // Override + protected _updateAnimationPoints(symbol: ECSymbolOnEffectLine, points: number[][]) { + this._points = points; + const accLenArr = [0]; + let len = 0; + for (let i = 1; i < points.length; i++) { + const p1 = points[i - 1]; + const p2 = points[i]; + len += vec2.dist(p1, p2); + accLenArr.push(len); + } + if (len === 0) { + this._length = 0; + return; + } + + for (let i = 0; i < accLenArr.length; i++) { + accLenArr[i] /= len; + } + this._offsets = accLenArr; + this._length = len; + }; + + // Override + protected _getLineLength() { + return this._length; + }; + + // Override + protected _updateSymbolPosition(symbol: ECSymbolOnEffectLine) { + const t = symbol.__t < 1 ? symbol.__t : 2 - symbol.__t; + const points = this._points; + const offsets = this._offsets; + const len = points.length; + + if (!offsets) { + // Has length 0 + return; + } + + const lastFrame = this._lastFrame; + let frame: number; + + if (t < this._lastFramePercent) { + // Start from the next frame + // PENDING start from lastFrame ? + const start = Math.min(lastFrame + 1, len - 1); + for (frame = start; frame >= 0; frame--) { + if (offsets[frame] <= t) { + break; } - if (len === 0) { - this._length = 0; - return; + } + // PENDING really need to do this ? + frame = Math.min(frame, len - 2); + } + else { + for (frame = lastFrame; frame < len; frame++) { + if (offsets[frame] > t) { + break; } + } + frame = Math.min(frame - 1, len - 2); + } - for (let i = 0; i < accLenArr.length; i++) { - accLenArr[i] /= len; - } - this._offsets = accLenArr; - this._length = len; - }; - - // Override - protected _getLineLength() { - return this._length; - }; - - // Override - protected _updateSymbolPosition(symbol: ECSymbolOnEffectLine) { - const t = symbol.__t < 1 ? symbol.__t : 2 - symbol.__t; - const points = this._points; - const offsets = this._offsets; - const len = points.length; - - if (!offsets) { - // Has length 0 - return; - } - - const lastFrame = this._lastFrame; - let frame: number; - - if (t < this._lastFramePercent) { - // Start from the next frame - // PENDING start from lastFrame ? - const start = Math.min(lastFrame + 1, len - 1); - for (frame = start; frame >= 0; frame--) { - if (offsets[frame] <= t) { - break; - } - } - // PENDING really need to do this ? - frame = Math.min(frame, len - 2); - } - else { - for (frame = lastFrame; frame < len; frame++) { - if (offsets[frame] > t) { - break; - } - } - frame = Math.min(frame - 1, len - 2); - } - - const p = (t - offsets[frame]) / (offsets[frame + 1] - offsets[frame]); - const p0 = points[frame]; - const p1 = points[frame + 1]; - symbol.x = p0[0] * (1 - p) + p * p1[0]; - symbol.y = p0[1] * (1 - p) + p * p1[1]; + const p = (t - offsets[frame]) / (offsets[frame + 1] - offsets[frame]); + const p0 = points[frame]; + const p1 = points[frame + 1]; + symbol.x = p0[0] * (1 - p) + p * p1[0]; + symbol.y = p0[1] * (1 - p) + p * p1[1]; - const tx = symbol.__t < 1 ? p1[0] - p0[0] : p0[0] - p1[0]; - const ty = symbol.__t < 1 ? p1[1] - p0[1] : p0[1] - p1[1]; - symbol.rotation = -Math.atan2(ty, tx) - Math.PI / 2; + const tx = symbol.__t <= 1 ? p1[0] - p0[0] : p0[0] - p1[0]; + const ty = symbol.__t <= 1 ? p1[1] - p0[1] : p0[1] - p1[1]; + symbol.rotation = -Math.atan2(ty, tx) - Math.PI / 2; - this._lastFrame = frame; - this._lastFramePercent = t; + this._lastFrame = frame; + this._lastFramePercent = t; - symbol.ignore = false; - }; + symbol.ignore = false; + }; } From a9aaed02adb5a3e0158dd7a1ea588d7540621085 Mon Sep 17 00:00:00 2001 From: plainheart Date: Fri, 24 Oct 2025 14:38:32 +0800 Subject: [PATCH 2/3] Merge remote-tracking branch 'remotes/origin/fix/effect-line/roundTrip' into fix-20216 fix(effectLine): fix symbol flicker at the ends when roundTrip is not enabled (resolves #20216) --- src/chart/helper/EffectLine.ts | 368 ++++++++++++++--------------- src/chart/helper/EffectPolyline.ts | 172 +++++++------- 2 files changed, 270 insertions(+), 270 deletions(-) diff --git a/src/chart/helper/EffectLine.ts b/src/chart/helper/EffectLine.ts index d8ea2c8074..195783ed7e 100644 --- a/src/chart/helper/EffectLine.ts +++ b/src/chart/helper/EffectLine.ts @@ -24,7 +24,7 @@ import * as graphic from '../../util/graphic'; import Line from './Line'; import * as zrUtil from 'zrender/src/core/util'; -import { createSymbol } from '../../util/symbol'; +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'; @@ -33,226 +33,226 @@ import Model from '../../model/Model'; import { ColorString } from '../../util/types'; export type ECSymbolOnEffectLine = ReturnType & { - __t: number - __lastT: number - __p1: number[] - __p2: number[] - __cp1: number[] + __t: number + __lastT: number + __p1: number[] + __p2: number[] + __cp1: number[] }; class EffectLine extends graphic.Group { - private _symbolType: string; + private _symbolType: string; - private _period: number; + private _period: number; - private _loop: boolean; + private _loop: boolean; - private _roundTrip: boolean; + private _roundTrip: boolean; - private _symbolScale: number[]; + private _symbolScale: number[]; - constructor(lineData: SeriesData, idx: number, seriesScope: LineDrawSeriesScope) { - super(); - this.add(this.createLine(lineData, idx, seriesScope)); + 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); - } + this._updateEffectSymbol(lineData, idx); + } - private _updateEffectSymbol(lineData: SeriesData, idx: number) { - const itemModel = lineData.getItemModel(idx); - const effectModel = itemModel.getModel('effect'); - let size = effectModel.get('symbolSize'); - const symbolType = effectModel.get('symbol'); - if (!zrUtil.isArray(size)) { - size = [size, size]; + createLine(lineData: SeriesData, idx: number, seriesScope: LineDrawSeriesScope): graphic.Group { + return new Line(lineData, idx, seriesScope); } - const lineStyle = lineData.getItemVisual(idx, 'style'); - const color = effectModel.get('color') || (lineStyle && lineStyle.stroke); - let symbol = this.childAt(1) as ECSymbolOnEffectLine; + private _updateEffectSymbol(lineData: SeriesData, idx: number) { + const itemModel = lineData.getItemModel(idx); + const effectModel = itemModel.getModel('effect'); + let size = effectModel.get('symbolSize'); + const symbolType = effectModel.get('symbol'); + if (!zrUtil.isArray(size)) { + size = [size, size]; + } - if (this._symbolType !== symbolType) { - // Remove previous - this.remove(symbol); + const lineStyle = lineData.getItemVisual(idx, 'style'); + const color = effectModel.get('color') || (lineStyle && lineStyle.stroke); + let symbol = this.childAt(1) as ECSymbolOnEffectLine; - symbol = createSymbol( - symbolType, -0.5, -0.5, 1, 1, color - ) as ECSymbolOnEffectLine; - symbol.z2 = 100; - symbol.culling = true; + if (this._symbolType !== symbolType) { + // Remove previous + this.remove(symbol); - this.add(symbol); - } + symbol = createSymbol( + symbolType, -0.5, -0.5, 1, 1, color + ) as ECSymbolOnEffectLine; + symbol.z2 = 100; + symbol.culling = true; - // 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'])); + this.add(symbol); + } - symbol.scaleX = size[0]; - symbol.scaleY = size[1]; + // Symbol may be removed if loop is false + if (!symbol) { + return; + } - symbol.setColor(color); + // Shadow color is same with color in default + symbol.setStyle('shadowColor', color as ColorString); + symbol.setStyle(effectModel.getItemStyle(['color'])); - this._symbolType = symbolType; - this._symbolScale = size; + symbol.scaleX = size[0]; + symbol.scaleY = size[1]; - this._updateEffectAnimation(lineData, effectModel, idx); - } + symbol.setColor(color); - private _updateEffectAnimation( - lineData: SeriesData, - effectModel: Model, - idx: number - ) { + this._symbolType = symbolType; + this._symbolScale = size; - const symbol = this.childAt(1) as ECSymbolOnEffectLine; - if (!symbol) { - return; + this._updateEffectAnimation(lineData, effectModel, idx); } - const points = lineData.getItemLayout(idx); + private _updateEffectAnimation( + lineData: SeriesData, + effectModel: Model, + 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; - }); + 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; + // Ignore when updating + symbol.ignore = true; - this._updateAnimationPoints(symbol, points); + this._updateAnimationPoints(symbol, points); - if (constantSpeed > 0) { - period = this._getLineLength(symbol) / constantSpeed * 1000; + 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; } - 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 - ); + 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(); + } } - 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 _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; + + 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]; + } } - } - 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]; } - 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); + updateLayout(lineData: SeriesData, idx: number) { + (this.childAt(0) as Line).updateLayout(lineData, idx); - const effectModel = lineData.getItemModel(idx).getModel('effect'); - this._updateEffectAnimation(lineData, effectModel, idx); - } + const effectModel = lineData.getItemModel(idx).getModel('effect'); + this._updateEffectAnimation(lineData, effectModel, idx); + } } export default EffectLine; diff --git a/src/chart/helper/EffectPolyline.ts b/src/chart/helper/EffectPolyline.ts index 9ed4ad4a0f..282df0725f 100644 --- a/src/chart/helper/EffectPolyline.ts +++ b/src/chart/helper/EffectPolyline.ts @@ -18,104 +18,104 @@ */ import Polyline from './Polyline'; -import EffectLine, { ECSymbolOnEffectLine } from './EffectLine'; +import EffectLine, {ECSymbolOnEffectLine} from './EffectLine'; import * as vec2 from 'zrender/src/core/vector'; import { LineDrawSeriesScope } from './LineDraw'; import SeriesData from '../../data/SeriesData'; class EffectPolyline extends EffectLine { - private _lastFrame = 0; - private _lastFramePercent = 0; - private _length: number; - - private _points: number[][]; - private _offsets: number[]; - - // Override - createLine(lineData: SeriesData, idx: number, seriesScope: LineDrawSeriesScope) { - return new Polyline(lineData, idx, seriesScope); - }; - - // Override - protected _updateAnimationPoints(symbol: ECSymbolOnEffectLine, points: number[][]) { - this._points = points; - const accLenArr = [0]; - let len = 0; - for (let i = 1; i < points.length; i++) { - const p1 = points[i - 1]; - const p2 = points[i]; - len += vec2.dist(p1, p2); - accLenArr.push(len); - } - if (len === 0) { - this._length = 0; - return; - } - - for (let i = 0; i < accLenArr.length; i++) { - accLenArr[i] /= len; - } - this._offsets = accLenArr; - this._length = len; - }; - - // Override - protected _getLineLength() { - return this._length; - }; - - // Override - protected _updateSymbolPosition(symbol: ECSymbolOnEffectLine) { - const t = symbol.__t < 1 ? symbol.__t : 2 - symbol.__t; - const points = this._points; - const offsets = this._offsets; - const len = points.length; - - if (!offsets) { - // Has length 0 - return; - } - - const lastFrame = this._lastFrame; - let frame: number; - - if (t < this._lastFramePercent) { - // Start from the next frame - // PENDING start from lastFrame ? - const start = Math.min(lastFrame + 1, len - 1); - for (frame = start; frame >= 0; frame--) { - if (offsets[frame] <= t) { - break; + private _lastFrame = 0; + private _lastFramePercent = 0; + private _length: number; + + private _points: number[][]; + private _offsets: number[]; + + // Override + createLine(lineData: SeriesData, idx: number, seriesScope: LineDrawSeriesScope) { + return new Polyline(lineData, idx, seriesScope); + }; + + // Override + protected _updateAnimationPoints(symbol: ECSymbolOnEffectLine, points: number[][]) { + this._points = points; + const accLenArr = [0]; + let len = 0; + for (let i = 1; i < points.length; i++) { + const p1 = points[i - 1]; + const p2 = points[i]; + len += vec2.dist(p1, p2); + accLenArr.push(len); } - } - // PENDING really need to do this ? - frame = Math.min(frame, len - 2); - } - else { - for (frame = lastFrame; frame < len; frame++) { - if (offsets[frame] > t) { - break; + if (len === 0) { + this._length = 0; + return; } - } - frame = Math.min(frame - 1, len - 2); - } - const p = (t - offsets[frame]) / (offsets[frame + 1] - offsets[frame]); - const p0 = points[frame]; - const p1 = points[frame + 1]; - symbol.x = p0[0] * (1 - p) + p * p1[0]; - symbol.y = p0[1] * (1 - p) + p * p1[1]; + for (let i = 0; i < accLenArr.length; i++) { + accLenArr[i] /= len; + } + this._offsets = accLenArr; + this._length = len; + }; + + // Override + protected _getLineLength() { + return this._length; + }; + + // Override + protected _updateSymbolPosition(symbol: ECSymbolOnEffectLine) { + const t = symbol.__t <= 1 ? symbol.__t : 2 - symbol.__t; + const points = this._points; + const offsets = this._offsets; + const len = points.length; + + if (!offsets) { + // Has length 0 + return; + } + + const lastFrame = this._lastFrame; + let frame: number; + + if (t < this._lastFramePercent) { + // Start from the next frame + // PENDING start from lastFrame ? + const start = Math.min(lastFrame + 1, len - 1); + for (frame = start; frame >= 0; frame--) { + if (offsets[frame] <= t) { + break; + } + } + // PENDING really need to do this ? + frame = Math.min(frame, len - 2); + } + else { + for (frame = lastFrame; frame < len; frame++) { + if (offsets[frame] > t) { + break; + } + } + frame = Math.min(frame - 1, len - 2); + } + + const p = (t - offsets[frame]) / (offsets[frame + 1] - offsets[frame]); + const p0 = points[frame]; + const p1 = points[frame + 1]; + symbol.x = p0[0] * (1 - p) + p * p1[0]; + symbol.y = p0[1] * (1 - p) + p * p1[1]; - const tx = symbol.__t <= 1 ? p1[0] - p0[0] : p0[0] - p1[0]; - const ty = symbol.__t <= 1 ? p1[1] - p0[1] : p0[1] - p1[1]; - symbol.rotation = -Math.atan2(ty, tx) - Math.PI / 2; + const tx = symbol.__t <= 1 ? p1[0] - p0[0] : p0[0] - p1[0]; + const ty = symbol.__t <= 1 ? p1[1] - p0[1] : p0[1] - p1[1]; + symbol.rotation = -Math.atan2(ty, tx) - Math.PI / 2; - this._lastFrame = frame; - this._lastFramePercent = t; + this._lastFrame = frame; + this._lastFramePercent = t; - symbol.ignore = false; - }; + symbol.ignore = false; + }; } From a86f4640e20c20eac53823a96b4404deb64cf5f7 Mon Sep 17 00:00:00 2001 From: plainheart Date: Fri, 24 Oct 2025 16:58:50 +0800 Subject: [PATCH 3/3] test(lines): add test case for effect symbol flip --- test/lines-symbol.html | 107 +++++++++++++++++++++++++++++------------ 1 file changed, 77 insertions(+), 30 deletions(-) diff --git a/test/lines-symbol.html b/test/lines-symbol.html index 560da4b6c8..d22371fb3f 100644 --- a/test/lines-symbol.html +++ b/test/lines-symbol.html @@ -23,27 +23,19 @@ - - - + + + - -
- + + - \ No newline at end of file +