Inspired by JParticles, I decided to make something similar using RxJs for practice. The source code concatenated follows, but you can also visit the GitHub repo.
import { map, debounceTime } from 'rxjs/operators';
import { BehaviorSubject, fromEvent, Scheduler, interval } from 'rxjs';
const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const colors = [
'#3b4252',
'#e1e1e1',
'#c3c3c3',
'#a5a5a5',
'#878787',
'#696969',
];
const darkGray = colors[0];
const background = '#fffff8';
const nNodes = (canvas.width * canvas.height) / 5000;
const initialState = initState(nNodes);
const nodes$ = new BehaviorSubject(initialState);
function next({ x, y }) {
const { nodes } = nodes$.getValue();
nodes$.next({
nodes,
mousePosClicked: {
x,
y,
},
});
}
const _updateNode = (n, _, ns) => updateNode(n, ns);
function update() {
const { mousePosClicked, nodes } = nodes$.getValue();
nodes$.next({
mousePosClicked,
nodes: nodes.map(_updateNode),
});
}
function render() {
clearCanvas();
const { mousePosClicked, nodes } = nodes$.getValue();
nodes.forEach(renderNode(mousePosClicked, nodes));
}
const clicks = fromEvent(canvas, 'click');
clicks.pipe(debounceTime(50), map(getMouseXY)).subscribe({ next });
interval(0, Scheduler.animationFrame).subscribe(render);
interval(Math.round(1000 / 60)).subscribe(update);
function getRandomValue(min, max) {
return Math.floor(Math.random() * (max - min) + min);
}
function getMouseXY(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const x = Math.round((e.clientX - rect.left) * scaleX);
const y = Math.round((e.clientY - rect.top) * scaleY);
return { x, y };
}
function checkPoint(a, b, x, y, r) {
const distPoints = (a - x) * (a - x) + (b - y) * (b - y);
return distPoints < r ** 2;
}
function getRandomValueNoZero(v1, v2) {
const randomValue = getRandomValue(v1, v2 + 1);
return randomValue !== 0 ? randomValue : getRandomValueNoZero(v1, v2);
}
function initState(nNodes) {
return {
nodes: Array.from({ length: nNodes }, (_) => {
const val = getRandomValue(1, 5);
const dx = getRandomValueNoZero(-5, 5);
const dy = getRandomValueNoZero(-5, 5);
return {
x: getRandomValue(-100, window.innerWidth + 100),
y: getRandomValue(-100, window.innerHeight + 100),
dx,
dy,
color: colors[val],
size: val,
ns: [],
};
}),
mousePosClicked: {
x: -1000,
y: -1000,
},
};
}
function clearCanvas() {
context.fillStyle = background;
context.fillRect(0, 0, window.innerWidth, window.innerHeight);
}
function drawLine(x1, y1, color, x2, y2, w) {
context.strokeStyle = color;
context.lineWidth = w;
context.beginPath();
context.moveTo(x1, y1);
context.lineTo(x2, y2);
context.stroke();
}
function drawCircle(x, y, color, radius) {
context.fillStyle = color;
context.beginPath();
context.arc(x, y, radius, 0, 2 * Math.PI);
context.fill();
}
function drawLinkNeighbors(node, ns) {
for (let i = 0; i < ns.length; i++) {
drawLine(node.x, node.y, node.color, ns[i].x, ns[i].y, node.size);
}
}
function renderNode({ x, y }) {
return (node) => {
if (checkPoint(x, y, node.x, node.y, 200)) {
drawLine(x, y, darkGray, node.x, node.y);
}
drawLinkNeighbors(node, node.ns);
drawCircle(node.x, node.y, node.color, node.size);
};
}
function updateNode(n, nodes) {
const dx = n.x < 0 || n.x > window.innerWidth ? -n.dx : n.dx;
const dy = n.y < 0 || n.y > window.innerHeight ? -n.dy : n.dy;
const ns = nodes.filter((node) => checkPoint(n.x, n.y, node.x, node.y, 100));
return {
...n,
dx,
dy,
x: n.x + dx,
y: n.y + dy,
ns,
};
}