Skip to content

Commit 35aa461

Browse files
committed
fix: fixed links issues
1 parent b548da3 commit 35aa461

1 file changed

Lines changed: 120 additions & 90 deletions

File tree

interactions/particles/links/src/LinkInstance.ts

Lines changed: 120 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
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

2223
export 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

Comments
 (0)