11import {
22 type Engine ,
33 type IContainerPlugin ,
4+ type IRgb ,
45 getLinkColor as engineGetLinkColor ,
56 getRandom ,
67 getRangeValue ,
@@ -20,6 +21,7 @@ const minOpacity = 0,
2021 defaultFrequency = 0 ;
2122
2223export class LinkInstance implements IContainerPlugin {
24+ private readonly _colorCache = new Map < string , string > ( ) ;
2325 private readonly _container : LinkContainer ;
2426 private readonly _engine : Engine ;
2527 private readonly _freqs : IParticlesFrequencies ;
@@ -37,8 +39,25 @@ export class LinkInstance implements IContainerPlugin {
3739 return ;
3840 }
3941
40- const pos1 = particle . getPosition ( ) ,
41- linkOpts = options . links ;
42+ const linkOpts = options . links ,
43+ width = particle . retina . linksWidth ?? minWidth ,
44+ pos1 = particle . getPosition ( ) ,
45+ twinkle = ( particle . options [ "twinkle" ] as ITwinkle | undefined ) ?. links ,
46+ trianglesEnabled = linkOpts . triangles . enable ,
47+ p1Destinations = trianglesEnabled ? new Set ( links . map ( l => l . destination . id ) ) : null ,
48+ originalAlpha = context . globalAlpha ;
49+
50+ let currentColorStyle = "" ,
51+ currentWidth = - 1 ,
52+ currentAlpha = - 1 ,
53+ pathOpen = false ;
54+
55+ const flushLines = ( ) : void => {
56+ if ( pathOpen ) {
57+ context . stroke ( ) ;
58+ pathOpen = false ;
59+ }
60+ } ;
4261
4362 for ( const link of links ) {
4463 if (
@@ -48,21 +67,98 @@ export class LinkInstance implements IContainerPlugin {
4867 continue ;
4968 }
5069
51- if ( ! link . isWarped ) {
52- this . _drawTriangles ( options , particle , link , links , pos1 , context ) ;
70+ const pos2 = link . destination . getPosition ( ) ;
71+
72+ if ( trianglesEnabled && ! link . isWarped && p1Destinations ) {
73+ flushLines ( ) ;
74+ this . _drawTriangles ( options , particle , link , p1Destinations , pos1 , pos2 , context ) ;
75+ }
76+
77+ if ( link . opacity <= minOpacity || width <= minWidth ) {
78+ continue ;
79+ }
80+
81+ if ( ! linkOpts . enable ) {
82+ continue ;
83+ }
84+
85+ let opacity = link . opacity ,
86+ colorLine = link . color ;
87+
88+ const twinkleRgb =
89+ twinkle ?. enable && getRandom ( ) < twinkle . frequency ? rangeColorToRgb ( this . _engine , twinkle . color ) : undefined ;
90+
91+ if ( twinkle && twinkleRgb ) {
92+ colorLine = twinkleRgb ;
93+ opacity = getRangeValue ( twinkle . opacity ) ;
5394 }
5495
55- if ( link . opacity <= minOpacity || ( particle . retina . linksWidth ?? minWidth ) <= minWidth ) {
96+ if ( ! colorLine ) {
97+ const linkColor =
98+ linkOpts . id !== undefined
99+ ? this . _container . particles . linksColors . get ( linkOpts . id )
100+ : this . _container . particles . linksColor ;
101+
102+ colorLine = engineGetLinkColor ( particle , link . destination , linkColor ) ;
103+ }
104+
105+ if ( ! colorLine ) {
56106 continue ;
57107 }
58108
59- this . _drawLinkLine ( context , particle , link , pos1 ) ;
109+ const colorStyle = this . _getCachedStyle ( colorLine ) ;
110+
111+ if ( colorStyle !== currentColorStyle || width !== currentWidth || opacity !== currentAlpha ) {
112+ flushLines ( ) ;
113+
114+ context . strokeStyle = colorStyle ;
115+ context . lineWidth = width ;
116+ context . globalAlpha = opacity ;
117+
118+ currentColorStyle = colorStyle ;
119+ currentWidth = width ;
120+ currentAlpha = opacity ;
121+
122+ context . beginPath ( ) ;
123+
124+ pathOpen = true ;
125+ }
126+
127+ if ( link . isWarped ) {
128+ const canvasSize = this . _container . canvas . size ,
129+ dx = pos2 . x - pos1 . x ,
130+ dy = pos2 . y - pos1 . y ;
131+
132+ let sx = originPoint . x ,
133+ sy = originPoint . y ;
134+
135+ if ( Math . abs ( dx ) > canvasSize . width * half ) {
136+ sx = dx > minDistance ? - canvasSize . width : canvasSize . width ;
137+ }
138+
139+ if ( Math . abs ( dy ) > canvasSize . height * half ) {
140+ sy = dy > minDistance ? - canvasSize . height : canvasSize . height ;
141+ }
142+
143+ context . moveTo ( pos1 . x , pos1 . y ) ;
144+ context . lineTo ( pos2 . x + sx , pos2 . y + sy ) ;
145+ context . moveTo ( pos1 . x - sx , pos1 . y - sy ) ;
146+ context . lineTo ( pos2 . x , pos2 . y ) ;
147+ } else {
148+ context . moveTo ( pos1 . x , pos1 . y ) ;
149+ context . lineTo ( pos2 . x , pos2 . y ) ;
150+ }
60151 }
152+
153+ flushLines ( ) ;
154+
155+ context . globalAlpha = originalAlpha ;
61156 }
62157
63158 init ( ) : Promise < void > {
64159 this . _freqs . links . clear ( ) ;
65160 this . _freqs . triangles . clear ( ) ;
161+ this . _colorCache . clear ( ) ;
66162 return Promise . resolve ( ) ;
67163 }
68164
@@ -86,90 +182,13 @@ export class LinkInstance implements IContainerPlugin {
86182 particle . links = [ ] ;
87183 }
88184
89- private _drawLinkLine (
90- context : CanvasRenderingContext2D ,
91- p1 : LinkParticle ,
92- link : ILink ,
93- pos1 : ReturnType < LinkParticle [ "getPosition" ] > ,
94- ) : void {
95- const linkOpts = p1 . options . links ;
96-
97- if ( ! linkOpts ?. enable ) {
98- return ;
99- }
100-
101- let opacity = link . opacity ,
102- colorLine = link . color ;
103-
104- const twinkle = ( p1 . options [ "twinkle" ] as ITwinkle | undefined ) ?. links ;
105-
106- if ( twinkle ?. enable && getRandom ( ) < twinkle . frequency ) {
107- const twinkleRgb = rangeColorToRgb ( this . _engine , twinkle . color ) ;
108-
109- if ( twinkleRgb ) {
110- colorLine = twinkleRgb ;
111- opacity = getRangeValue ( twinkle . opacity ) ;
112- }
113- }
114-
115- if ( ! colorLine ) {
116- const linkColor =
117- linkOpts . id !== undefined
118- ? this . _container . particles . linksColors . get ( linkOpts . id )
119- : this . _container . particles . linksColor ;
120-
121- colorLine = engineGetLinkColor ( p1 , link . destination , linkColor ) ;
122- }
123-
124- if ( ! colorLine ) {
125- return ;
126- }
127-
128- const width = p1 . retina . linksWidth ?? minWidth ,
129- pos2 = link . destination . getPosition ( ) ,
130- canvasSize = this . _container . canvas . size ;
131-
132- context . save ( ) ;
133- context . lineWidth = width ;
134- context . strokeStyle = getStyleFromRgb ( colorLine , this . _container . hdr ) ;
135- context . globalAlpha = opacity ;
136- context . beginPath ( ) ;
137-
138- if ( link . isWarped ) {
139- const dx = pos2 . x - pos1 . x ,
140- dy = pos2 . y - pos1 . y ;
141-
142- let sx = originPoint . x ,
143- sy = originPoint . y ;
144-
145- if ( Math . abs ( dx ) > canvasSize . width * half ) {
146- sx = dx > minDistance ? - canvasSize . width : canvasSize . width ;
147- }
148-
149- if ( Math . abs ( dy ) > canvasSize . height * half ) {
150- sy = dy > minDistance ? - canvasSize . height : canvasSize . height ;
151- }
152-
153- /* draw the two half-segments that cross the canvas boundary */
154- context . moveTo ( pos1 . x , pos1 . y ) ;
155- context . lineTo ( pos2 . x + sx , pos2 . y + sy ) ;
156- context . moveTo ( pos1 . x - sx , pos1 . y - sy ) ;
157- context . lineTo ( pos2 . x , pos2 . y ) ;
158- } else {
159- context . moveTo ( pos1 . x , pos1 . y ) ;
160- context . lineTo ( pos2 . x , pos2 . y ) ;
161- }
162-
163- context . stroke ( ) ;
164- context . restore ( ) ;
165- }
166-
167185 private _drawTriangles (
168186 options : ParticlesLinkOptions ,
169187 p1 : LinkParticle ,
170188 link : ILink ,
171- p1Links : ILink [ ] ,
189+ p1Destinations : Set < number > ,
172190 pos1 : ReturnType < LinkParticle [ "getPosition" ] > ,
191+ pos2 : ReturnType < LinkParticle [ "getPosition" ] > ,
173192 context : CanvasRenderingContext2D ,
174193 ) : void {
175194 const p2 = link . destination ,
@@ -179,15 +198,12 @@ export class LinkInstance implements IContainerPlugin {
179198 return ;
180199 }
181200
182- const p1Destinations = new Set ( p1Links . map ( l => l . destination . id ) ) ,
183- p2Links = p2 . links ;
201+ const p2Links = p2 . links ;
184202
185203 if ( ! p2Links ?. length ) {
186204 return ;
187205 }
188206
189- const pos2 = p2 . getPosition ( ) ;
190-
191207 for ( const vertex of p2Links ) {
192208 if (
193209 vertex . isWarped ||
@@ -212,8 +228,10 @@ export class LinkInstance implements IContainerPlugin {
212228
213229 const pos3 = p3 . getPosition ( ) ;
214230
231+ /* triangles each have independent fill state so save/restore is still
232+ * needed here — triangles are typically far fewer than lines */
215233 context . save ( ) ;
216- context . fillStyle = getStyleFromRgb ( colorTriangle , this . _container . hdr ) ;
234+ context . fillStyle = this . _getCachedStyle ( colorTriangle ) ;
217235 context . globalAlpha = opacityTriangle ;
218236 context . beginPath ( ) ;
219237 context . moveTo ( pos1 . x , pos1 . y ) ;
@@ -225,6 +243,18 @@ export class LinkInstance implements IContainerPlugin {
225243 }
226244 }
227245
246+ private _getCachedStyle ( rgb : IRgb ) : string {
247+ const key = `${ rgb . r } ,${ rgb . g } ,${ rgb . b } ` ;
248+ let style = this . _colorCache . get ( key ) ;
249+
250+ if ( ! style ) {
251+ style = getStyleFromRgb ( rgb , this . _container . hdr ) ;
252+ this . _colorCache . set ( key , style ) ;
253+ }
254+
255+ return style ;
256+ }
257+
228258 private _getLinkFrequency ( p1 : LinkParticle , p2 : LinkParticle ) : number {
229259 return setLinkFrequency ( [ p1 , p2 ] , this . _freqs . links ) ;
230260 }
0 commit comments