Skip to content

Commit 91b918c

Browse files
perf: move area ripple animation to overlay canvas
Avoid full main-layer repaints at animation frame rate when candle.area.point.animation is enabled. Fixes #808. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent df0f0d9 commit 91b918c

2 files changed

Lines changed: 68 additions & 18 deletions

File tree

src/view/CandleAreaView.ts

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,20 @@ export default class CandleAreaView extends ChildrenView {
4141
private readonly _animation = new Animation({ iterationCount: Infinity }).doFrame((time) => {
4242
this._animationFrameTime = time
4343
const pane = this.getWidget().getPane()
44-
pane.getChart().updatePane(UpdateLevel.Main, pane.getId())
44+
// Ripple lives on the overlay canvas; avoid full main-layer repaints at 60fps.
45+
pane.getChart().updatePane(UpdateLevel.Overlay, pane.getId())
4546
})
4647

4748
override drawImp (ctx: CanvasRenderingContext2D): void {
4849
const widget = this.getWidget()
4950
const pane = widget.getPane()
5051
const chart = pane.getChart()
51-
const dataList = chart.getDataList()
52-
const lastDataIndex = dataList.length - 1
5352
const bounding = widget.getBounding()
5453
const yAxis = pane.getYAxisComponentById()
5554
const styles = chart.getStyles().candle.area
5655
const coordinates: Coordinate[] = []
5756
let minY = Number.MAX_SAFE_INTEGER
5857
let areaStartX: number = Number.MIN_SAFE_INTEGER
59-
let ripplePointCoordinate: Nullable<Coordinate> = null
6058
this.eachChildren((data) => {
6159
const x = data.x
6260
const { current: kLineData } = data.data
@@ -68,9 +66,6 @@ export default class CandleAreaView extends ChildrenView {
6866
}
6967
coordinates.push({ x, y })
7068
minY = Math.min(minY, y)
71-
if (data.dataIndex === lastDataIndex) {
72-
ripplePointCoordinate = { x, y }
73-
}
7469
}
7570
})
7671

@@ -112,37 +107,90 @@ export default class CandleAreaView extends ChildrenView {
112107
}
113108

114109
const pointStyles = styles.point
110+
const ripplePointCoordinate = this._getLastPointCoordinate()
115111
if (pointStyles.show && isValid(ripplePointCoordinate)) {
116112
this.createFigure({
117113
name: 'circle',
118114
attrs: {
119-
x: ripplePointCoordinate!.x,
120-
y: ripplePointCoordinate!.y,
115+
x: ripplePointCoordinate.x,
116+
y: ripplePointCoordinate.y,
121117
r: pointStyles.radius
122118
},
123119
styles: {
124120
style: 'fill',
125121
color: pointStyles.color
126122
}
127123
})?.draw(ctx)
128-
let rippleRadius = pointStyles.rippleRadius
129124
if (pointStyles.animation) {
130-
rippleRadius = pointStyles.radius + this._animationFrameTime / pointStyles.animationDuration * (pointStyles.rippleRadius - pointStyles.radius)
131125
this._animation.setDuration(pointStyles.animationDuration).start()
126+
} else {
127+
this.stopAnimation()
132128
}
133-
this._ripplePoint
134-
?.setAttrs({
135-
x: ripplePointCoordinate!.x,
136-
y: ripplePointCoordinate!.y,
137-
r: rippleRadius
138-
})
139-
.setStyles({ style: 'fill', color: pointStyles.rippleColor }).draw(ctx)
129+
// Keep overlay ripple in sync after main paints (scroll/zoom/data).
130+
chart.updatePane(UpdateLevel.Overlay, pane.getId())
140131
} else {
141132
this.stopAnimation()
142133
}
143134
}
144135

136+
/**
137+
* Draw the last-price ripple on the overlay canvas so animation does not
138+
* repaint candles, grid, and indicators on every frame.
139+
*/
140+
drawRipple (ctx: CanvasRenderingContext2D): void {
141+
const chart = this.getWidget().getPane().getChart()
142+
if (chart.getStyles().candle.type !== 'area') {
143+
this.stopAnimation()
144+
return
145+
}
146+
const pointStyles = chart.getStyles().candle.area.point
147+
const ripplePointCoordinate = this._getLastPointCoordinate()
148+
if (!pointStyles.show || !isValid(ripplePointCoordinate)) {
149+
this.stopAnimation()
150+
return
151+
}
152+
153+
let rippleRadius = pointStyles.rippleRadius
154+
if (pointStyles.animation) {
155+
rippleRadius = pointStyles.radius + this._animationFrameTime / pointStyles.animationDuration * (pointStyles.rippleRadius - pointStyles.radius)
156+
this._animation.setDuration(pointStyles.animationDuration).start()
157+
} else {
158+
this.stopAnimation()
159+
}
160+
161+
this._ripplePoint
162+
?.setAttrs({
163+
x: ripplePointCoordinate.x,
164+
y: ripplePointCoordinate.y,
165+
r: rippleRadius
166+
})
167+
.setStyles({ style: 'fill', color: pointStyles.rippleColor }).draw(ctx)
168+
}
169+
145170
stopAnimation (): void {
146171
this._animation.stop()
147172
}
173+
174+
private _getLastPointCoordinate (): Nullable<Coordinate> {
175+
const widget = this.getWidget()
176+
const pane = widget.getPane()
177+
const chart = pane.getChart()
178+
const dataList = chart.getDataList()
179+
const lastDataIndex = dataList.length - 1
180+
if (lastDataIndex < 0) {
181+
return null
182+
}
183+
const styles = chart.getStyles().candle.area
184+
const yAxis = pane.getYAxisComponentById()
185+
let coordinate: Nullable<Coordinate> = null
186+
this.eachChildren((data) => {
187+
if (data.dataIndex === lastDataIndex) {
188+
const value = data.data.current?.[styles.value]
189+
if (isNumber(value)) {
190+
coordinate = { x: data.x, y: yAxis.convertToPixel(value) }
191+
}
192+
}
193+
})
194+
return coordinate
195+
}
148196
}

src/widget/CandleWidget.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ export default class CandleWidget extends IndicatorWidget {
5353
}
5454

5555
override updateOverlayContent (ctx: CanvasRenderingContext2D): void {
56+
// Area ripple animates on the overlay layer to avoid Main-level repaints.
57+
this._candleAreaView.drawRipple(ctx)
5658
this._crosshairFeatureView.draw(ctx)
5759
}
5860

0 commit comments

Comments
 (0)