Skip to content

Commit b82a052

Browse files
authored
Merge pull request #44 from calmKyle/master
Create gossip distribution visualiser and heap sort visualiser
2 parents ab6f69c + c8a3a33 commit b82a052

File tree

3 files changed

+449
-0
lines changed

3 files changed

+449
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ be rejected.
5757
| [@pengzxD](https://github.com/pengzxD) | [Rainbow Donut](web/priv/js_effects/RainbowDonut.js) | A classic donut animation with added 🏳️‍🌈✨ |
5858
| [@etmuzzr](https://github.com/etmuzzr) | [boids](web/priv/js_effects/boids.js) | Sparkley colourful boids! |
5959
| [@moolordking](https://github.com/moolordking) | [LavaLamp](web/priv/js_effects/LavaLamp.js) | Gentle moving shapes, akin to a Lava Lamp. |
60+
| [@calmKyle](https://github.com/calmKyle) | [Heap Sort Visualiser](web/priv/js_effects/heapSortVisualiser.js) | Heap sort algorithms visualiser |
61+
| [@calmKyle](https://github.com/calmKyle) | [Gossip Distribution Visualiser](web/priv/js_effects/gossipNetworkVisualiser.js) | Gossip Distribution visualiser |
6062

6163
## Image Overrides
6264

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
return class GossipNetwork {
2+
3+
/**
4+
* @param {object} display The display object (with setPixel, flush, width, height).
5+
* @param {object} options Configuration options:
6+
* - numNodes (default 100)
7+
* - nodeSize (default 1x1)
8+
* - minConnections (default 2)
9+
* - maxConnections (default 6)
10+
* - packetSpeed (default 0.03)
11+
* - connectionStrategy (default "random") // can be "random" or "closest"
12+
* - maxDistance (default Infinity) // used if connectionStrategy = "closest"
13+
*/
14+
constructor(display, options = {}) {
15+
this.display = display;
16+
17+
// Basic config
18+
this.numNodes = options.numNodes ?? 200;
19+
this.nodeSize = options.nodeSize ?? 1;
20+
this.minConnections = options.minConnections ?? 2;
21+
this.maxConnections = options.maxConnections ?? 6;
22+
this.packetSpeed = options.packetSpeed ?? 0.03;
23+
this.connectionStrategy = options.connectionStrategy ?? "closest";
24+
this.maxDistance = (options.maxDistance === 100) ? Infinity : options.maxDistance;
25+
26+
this.nodes = [];
27+
this.packets = [];
28+
29+
this.#initNodes();
30+
this.#initLinks();
31+
32+
// Start gossip at node 0
33+
if (this.nodes.length > 0) {
34+
this.nodes[0].received = true;
35+
this.nodes[0].color = [0, 255, 0]; // green
36+
// Create initial packets from node 0 to its neighbors
37+
this.nodes[0].neighbors.forEach((neighborIdx) => {
38+
this.packets.push({
39+
from: 0,
40+
to: neighborIdx,
41+
fraction: 0,
42+
speed: this.packetSpeed,
43+
});
44+
});
45+
}
46+
47+
// Render the initial state
48+
this.#draw();
49+
}
50+
51+
#initNodes() {
52+
// For nodeSize > 1, you may want to subtract nodeSize instead of 2
53+
const maxX = this.display.width - 2;
54+
const maxY = this.display.height - 2;
55+
56+
for (let i = 0; i < this.numNodes; i++) {
57+
const x = Math.floor(Math.random() * maxX);
58+
const y = Math.floor(Math.random() * maxY);
59+
this.nodes.push({
60+
x,
61+
y,
62+
received: false,
63+
color: [255, 0, 0], // red
64+
neighbors: [],
65+
});
66+
}
67+
}
68+
69+
#initLinks() {
70+
if (this.connectionStrategy === "closest") {
71+
this.#initClosestLinks();
72+
} else {
73+
this.#initRandomLinks();
74+
}
75+
}
76+
77+
78+
#initRandomLinks() {
79+
const n = this.nodes.length;
80+
81+
for (let i = 0; i < n; i++) {
82+
const deg = this.minConnections + Math.floor(
83+
Math.random() * (this.maxConnections - this.minConnections + 1)
84+
);
85+
86+
while (this.nodes[i].neighbors.length < deg) {
87+
const candidate = Math.floor(Math.random() * n);
88+
89+
// Avoid self-loops and duplicates
90+
if (candidate !== i && !this.nodes[i].neighbors.includes(candidate)) {
91+
this.nodes[i].neighbors.push(candidate);
92+
this.nodes[candidate].neighbors.push(i);
93+
}
94+
95+
if (this.nodes[i].neighbors.length >= n - 1) {
96+
break;
97+
}
98+
}
99+
}
100+
}
101+
102+
103+
#initClosestLinks() {
104+
const n = this.nodes.length;
105+
106+
for (let i = 0; i < n; i++) {
107+
// Pick a degree in [minConnections->maxConnections]
108+
const deg = this.minConnections + Math.floor(
109+
Math.random() * (this.maxConnections - this.minConnections + 1)
110+
);
111+
112+
// gather distances
113+
const distances = [];
114+
for (let j = 0; j < n; j++) {
115+
if (j !== i) {
116+
const dx = this.nodes[i].x - this.nodes[j].x;
117+
const dy = this.nodes[i].y - this.nodes[j].y;
118+
const dist = Math.sqrt(dx * dx + dy * dy);
119+
distances.push({ index: j, dist });
120+
}
121+
}
122+
123+
// sort ascending by distance
124+
distances.sort((a, b) => a.dist - b.dist);
125+
126+
// pick those within maxDistance
127+
let withinThreshold = distances.filter(d => d.dist <= this.maxDistance);
128+
129+
// if we can't meet minConnections, revert to the entire sorted array
130+
if (withinThreshold.length < this.minConnections) {
131+
withinThreshold = distances;
132+
}
133+
134+
// pick up to deg from withinThreshold
135+
const selected = withinThreshold.slice(0, deg);
136+
137+
// link them
138+
for (const obj of selected) {
139+
const neighborIdx = obj.index;
140+
if (!this.nodes[i].neighbors.includes(neighborIdx)) {
141+
this.nodes[i].neighbors.push(neighborIdx);
142+
}
143+
if (!this.nodes[neighborIdx].neighbors.includes(i)) {
144+
this.nodes[neighborIdx].neighbors.push(i);
145+
}
146+
}
147+
}
148+
}
149+
150+
151+
update() {
152+
// Package each packet
153+
const arrivedPackets = [];
154+
for (const packet of this.packets) {
155+
packet.fraction += packet.speed;
156+
if (packet.fraction >= 1) {
157+
arrivedPackets.push(packet);
158+
}
159+
}
160+
161+
// Process arrivals
162+
for (const packet of arrivedPackets) {
163+
// Remove from active list
164+
const idx = this.packets.indexOf(packet);
165+
if (idx !== -1) {
166+
this.packets.splice(idx, 1);
167+
}
168+
169+
// Infect the destination node if needed
170+
const destinationNode = this.nodes[packet.to];
171+
if (!destinationNode.received) {
172+
destinationNode.received = true;
173+
destinationNode.color = [0, 255, 0]; // green
174+
175+
// Spread to neighbors, skipping the sender
176+
destinationNode.neighbors.forEach((neighborIdx) => {
177+
178+
if (neighborIdx === packet.from) return;
179+
180+
// Also skip if neighbor is already received
181+
if (this.nodes[neighborIdx].received) return;
182+
183+
// Create new packet
184+
this.packets.push({
185+
from: packet.to,
186+
to: neighborIdx,
187+
fraction: 0,
188+
speed: this.packetSpeed,
189+
});
190+
});
191+
}
192+
}
193+
194+
// Redraw after updating
195+
this.#draw();
196+
}
197+
198+
#draw() {
199+
this.#clear();
200+
201+
// Draw adjacency lines
202+
/*
203+
this.nodes.forEach((nodeA, i) => {
204+
nodeA.neighbors.forEach((j) => {
205+
if (j > i) {
206+
const nodeB = this.nodes[j];
207+
// draw from center of each node if nodeSize > 1
208+
const ax = nodeA.x + Math.floor(this.nodeSize / 2);
209+
const ay = nodeA.y + Math.floor(this.nodeSize / 2);
210+
const bx = nodeB.x + Math.floor(this.nodeSize / 2);
211+
const by = nodeB.y + Math.floor(this.nodeSize / 2);
212+
213+
this.#drawLine(ax, ay, bx, by, [255, 255, 255]);
214+
}
215+
});
216+
});
217+
*/
218+
219+
// Draw nodes
220+
for (const node of this.nodes) {
221+
for (let dx = 0; dx < this.nodeSize; dx++) {
222+
for (let dy = 0; dy < this.nodeSize; dy++) {
223+
this.display.setPixel(node.x + dx, node.y + dy, node.color);
224+
}
225+
}
226+
}
227+
228+
// Draw traveling packets (yellow)
229+
for (const packet of this.packets) {
230+
const nodeA = this.nodes[packet.from];
231+
const nodeB = this.nodes[packet.to];
232+
233+
// If nodeSize > 1, we draw from center
234+
const ax = nodeA.x + Math.floor(this.nodeSize / 2);
235+
const ay = nodeA.y + Math.floor(this.nodeSize / 2);
236+
const bx = nodeB.x + Math.floor(this.nodeSize / 2);
237+
const by = nodeB.y + Math.floor(this.nodeSize / 2);
238+
239+
// Interpolate
240+
const x = ax + packet.fraction * (bx - ax);
241+
const y = ay + packet.fraction * (by - ay);
242+
const px = Math.floor(x);
243+
const py = Math.floor(y);
244+
245+
// packet color = yellow
246+
this.display.setPixel(px, py, [255, 255, 0]);
247+
}
248+
249+
this.display.flush();
250+
}
251+
252+
#clear() {
253+
for (let x = 0; x < this.display.width; x++) {
254+
for (let y = 0; y < this.display.height; y++) {
255+
this.display.setPixel(x, y, [0, 0, 0]);
256+
}
257+
}
258+
}
259+
260+
/**
261+
* Bresenham line if want to draw adjacency links between node.
262+
*/
263+
#drawLine(x1, y1, x2, y2, color) {
264+
let dx = Math.abs(x2 - x1);
265+
let sx = x1 < x2 ? 1 : -1;
266+
let dy = -Math.abs(y2 - y1);
267+
let sy = y1 < y2 ? 1 : -1;
268+
let err = dx + dy;
269+
270+
while (true) {
271+
this.display.setPixel(x1, y1, color);
272+
if (x1 === x2 && y1 === y2) break;
273+
274+
const e2 = 2 * err;
275+
if (e2 >= dy) {
276+
err += dy;
277+
x1 += sx;
278+
}
279+
if (e2 <= dx) {
280+
err += dx;
281+
y1 += sy;
282+
}
283+
}
284+
}
285+
}
286+

0 commit comments

Comments
 (0)