@@ -22,6 +22,24 @@ import { getMapPointer } from '../../utils/mapPointer'
2222
2323const LONG_PRESS_MS = 500
2424
25+ /** Viewport-sized buffer so heat-map segments flatten before opacity is applied. */
26+ let heatMapScratch : HTMLCanvasElement | null = null
27+
28+ function getHeatMapScratch (
29+ width : number ,
30+ height : number ,
31+ ) : HTMLCanvasElement | null {
32+ if ( width < 1 || height < 1 ) return null
33+ if ( ! heatMapScratch ) {
34+ heatMapScratch = document . createElement ( 'canvas' )
35+ }
36+ if ( heatMapScratch . width !== width || heatMapScratch . height !== height ) {
37+ heatMapScratch . width = width
38+ heatMapScratch . height = height
39+ }
40+ return heatMapScratch
41+ }
42+
2543function Track ( {
2644 track,
2745 adjustMode,
@@ -169,31 +187,73 @@ function Track({
169187
170188 const drawHeatMapStroke = ( context : Context ) => {
171189 const ctx = context . _context
172- ctx . save ( )
173- ctx . globalAlpha = track . opacity
174- ctx . lineCap = 'round'
175- ctx . lineJoin = 'round'
176- ctx . lineWidth = strokeWidth
190+ const scratch = getHeatMapScratch ( ctx . canvas . width , ctx . canvas . height )
191+ const octx = scratch ?. getContext ( '2d' )
192+ if ( ! scratch || ! octx ) return
193+
177194 const n = warped . length / 2
195+ if ( n < 2 ) return
196+
197+ const colorAt = ( index : number ) : string => {
198+ if ( paceMode && paceScale ) {
199+ return paceToColor (
200+ paceSamples [ index ] ?. paceMinPerKm ?? null ,
201+ paceScale . min ,
202+ paceScale . max ,
203+ )
204+ }
205+ if ( hrMode && hrScale ) {
206+ return hrToColor (
207+ hrSamples [ index ] ?. hrBpm ?? null ,
208+ hrScale . min ,
209+ hrScale . max ,
210+ )
211+ }
212+ return '#9ca3af'
213+ }
214+
215+ octx . setTransform ( 1 , 0 , 0 , 1 , 0 , 0 )
216+ octx . clearRect ( 0 , 0 , scratch . width , scratch . height )
217+ octx . setTransform ( ctx . getTransform ( ) )
218+ octx . globalAlpha = 1
219+ octx . lineCap = 'round'
220+ octx . lineJoin = 'round'
221+
222+ octx . lineWidth = outlineWidth
223+ octx . strokeStyle = '#000000'
224+ octx . beginPath ( )
225+ octx . moveTo ( warped [ 0 ] ! , warped [ 1 ] ! )
226+ for ( let i = 1 ; i < n ; i ++ ) {
227+ octx . lineTo ( warped [ i * 2 ] ! , warped [ i * 2 + 1 ] ! )
228+ }
229+ octx . stroke ( )
230+
231+ octx . lineWidth = strokeWidth
178232 for ( let i = 1 ; i < n ; i ++ ) {
179233 const x0 = warped [ ( i - 1 ) * 2 ] !
180234 const y0 = warped [ ( i - 1 ) * 2 + 1 ] !
181235 const x1 = warped [ i * 2 ] !
182236 const y1 = warped [ i * 2 + 1 ] !
183- if ( paceMode && paceScale ) {
184- const pace = paceSamples [ i ] ?. paceMinPerKm ?? null
185- ctx . strokeStyle = paceToColor ( pace , paceScale . min , paceScale . max )
186- } else if ( hrMode && hrScale ) {
187- const hr = hrSamples [ i ] ?. hrBpm ?? null
188- ctx . strokeStyle = hrToColor ( hr , hrScale . min , hrScale . max )
237+ const cEnd = colorAt ( i )
238+ const cStart = i === 1 ? cEnd : colorAt ( i - 1 )
239+ if ( cStart === cEnd ) {
240+ octx . strokeStyle = cEnd
189241 } else {
190- continue
242+ const gradient = octx . createLinearGradient ( x0 , y0 , x1 , y1 )
243+ gradient . addColorStop ( 0 , cStart )
244+ gradient . addColorStop ( 1 , cEnd )
245+ octx . strokeStyle = gradient
191246 }
192- ctx . beginPath ( )
193- ctx . moveTo ( x0 , y0 )
194- ctx . lineTo ( x1 , y1 )
195- ctx . stroke ( )
247+ octx . beginPath ( )
248+ octx . moveTo ( x0 , y0 )
249+ octx . lineTo ( x1 , y1 )
250+ octx . stroke ( )
196251 }
252+
253+ ctx . save ( )
254+ ctx . setTransform ( 1 , 0 , 0 , 1 , 0 , 0 )
255+ ctx . globalAlpha = track . opacity
256+ ctx . drawImage ( scratch , 0 , 0 )
197257 ctx . restore ( )
198258 }
199259
@@ -208,7 +268,7 @@ function Track({
208268 hitStrokeWidth = { strokeWidth * 5 }
209269 lineCap = "round"
210270 lineJoin = "round"
211- opacity = { track . opacity }
271+ opacity = { 0 }
212272 listening = { adjustMode }
213273 onClick = { addAnchorAtPointer }
214274 onTap = { addAnchorAtPointer }
0 commit comments