D3 Simulation
Canvas o Svg?
El primer planteamiento es si usar canvas o svg. Ambos tienen sus ventajas y desventajas.
Por un lado Canvas es más eficiente ya que no tenemos que trabajar con d3-select
para todos los elementos.
Tambien nos evita tener en variables las lineas que conectan los circulos.
Sin embargo implementar el hover y un fade-in
fade-out
es más complicado.
Svg por otro nos soluciona facilmente el control del hover y los fades de entrada y salida,
pero sin embargo para un gran número de elementos es costoso, pues debe tener n elementos en el DOM y manejar sus atributos
En este caso Canvas es una mejor solucion, pues queremos tener un gran número de elementos
Implementación
Para su implementación usaremos d3@6
.
definimos un height y un width
Para crear las particulas:
const particles = new Array(n);
for (let i = 0; i < n; ++i) {
particles[i] = {
x: Math.random() * width,
y: Math.random() * height,
radius: Math.abs(d3.randomNormal(2, 3)()),
vx: Math.sign(Math.random() - 0.5) * d3.randomNormal(1, 1)(),
vy: Math.sign(Math.random() - 0.5) * d3.randomNormal(1, 1)()
};
}
Como queremos que nuestras simulación no pare nunca ponemos la velocityDecay
y el alphaDecay
a 0.
const simulation = d3
.forceSimulation(particles)
.velocityDecay(0)
.alphaDecay(0)
En este caso quería que las bolas pasaran de un lado a otro como si fuese un portal. Para hacer esto he hecho una fuerza que si sobrepasa el tamaño del width o height entonces aplica el módulo. De esta forma nos garantiza que siempre estará el circulo dentro del canvas:
function mod(n, m) {
if (n > 0) return n % m;
else return (n + m * (Math.trunc(n / m) + 1)) % m;
}
function forceBorderModule(width, height) {
var _nodes = [];
function force(alpha) {
_nodes.forEach((n) => {
n.x = mod(n.x, width);
n.y = mod(n.y, height);
});
}
function initialize(nodes) {
_nodes = nodes;
}
force.initialize = initialize;
return force;
}
Por lo que nuestra simulación queda así:
const simulation =d3
.forceSimulation(particles)
.alphaDecay(0)
.velocityDecay(0)
.force("module", forceBorderModule(width, height));
Para dibujar:
function drawCircle(n){
context.beginPath();
context.arc(n.x, n.y, n.radius, 0, 2 * Math.PI, false);
context.stroke();
}
simulation.on("tick", tick);
function tick() {
context.clearRect(0, 0, width, height);
particles.forEach((n) => drawCircle(m))
}
Para identificar que circulos están cerca de otro circulo vamos a usar el (algoritmo de quadtree)[https://en.wikipedia.org/wiki/Quadtree]. Se basa en trocear el espacio buscando aquellos elementos dentro de ese espacio. Si encuentra más de un elemento en un trozo, lo vuelve a trocear hasta encontrar únicamente uno. d3 nos facilita de una librería para relaizar esto:
const tree = d3
.quadtree()
.x((n) => n.x)
.y((n) => n.y)
.addAll(particles);
tree.visit((visited, x0, y0, x1, y1) => {
// visited.data elemento, si hay
// x0,y0,x1,y1 rectangulo en el que se encuentra
})
Por lo que para encontrar los elementos que se encuentran a una distancia r
definimos:
const radius = 40
function drawCircle(n){
context.beginPath();
context.arc(n.x, n.y, n.radius, 0, 2 * Math.PI, false);
context.stroke();
}
function drawLine(n1,n2){
context.beginPath();
context.moveTo(n1.x, n1.y);
context.lineTo(n2.x, n2.y);
context.stroke();
}
simulation.on("tick", tick);
function tick() {
context.clearRect(0, 0, width, height);
particles.forEach((n) => {
tree.visit((visited, x0, y0, x1, y1) => {
if (visited.data && visited.data !== n) {
const d = Math.hypot(n.x - visited.data.x, n.y - visited.data.y);
if (d < radius) {
drawLine(n,visited.data)
}
}
});
drawCircle(n)
})
}