Public
Edited
Jun 28, 2023
Insert cell
Insert cell
Insert cell
function memo(this1, name, calcFun) {
const name1 = '_' + name;
return () => {
if (!this1[name1]) {
this1[name1] = calcFun(this1);
}
return this1[name1];
}
}
Insert cell
class ForceRect {
constructor(params) {
this.params = params;
}
ctx = memo(this, 'ctx', () => {
return DOM.context2d(this.params.width, this.params.height);
});
canvas = memo(this, 'canvas', () => {
return this.ctx().canvas;
})
nodesCount = 1500;
nodesAndEdges = memo(this, 'nodesAndEdges', () => {
const res = {
nodes: [],
edges: [],
};
for (let i=0; i<this.nodesCount; i++) {
const h = 60 / (1 + Math.random() * 10) ** 0.5;
const w = h * 0.6 * Math.random() * 15;
res.nodes.push({
index: i,
width: w,
height: h,
...(i === 0 ? {
fx: Math.random() * this.params.width,
fy: Math.random() * this.params.height,
} : {}),
});
if (i > 0) {
res.edges.push({
source: i,
target: Math.floor(Math.random() * i),
});
}
}
return res;
});
simulation = memo(this, 'simulation', () => {
const sim = d3.forceSimulation(this.nodesAndEdges().nodes)
.force("link", d3.forceLink(this.nodesAndEdges().edges))
.force("charge", d3.forceManyBody())
.force("rectCollide", null)
.force("center", d3.forceCenter(this.params.width/2, this.params.height/2))
.on("tick", () => this.tick());
if (this.params.invalidation != null) this.params.invalidation.then(() => sim.stop());
return sim;
});
drawNode({x, y, width, height}) {
const ctx = this.ctx();
ctx.fillStyle = 'black';
ctx.fillRect(x - width / 2, y - height / 2, width, height);
}
drawEdge({source, target}) {
const ctx = this.ctx();
const {nodes} = this.nodesAndEdges();
ctx.strokeStyle = 'black';
ctx.beginPath();
ctx.moveTo(source.x, source.y);
ctx.lineTo(target.x, target.y);
ctx.stroke();
}
stepNum = 0;
tick() {
this.stepNum ++;
if (this.stepNum > 100) {
this.simulation().force("rectCollide", forceCollide());
}
this.draw();
}
draw() {
const ctx = this.ctx();
ctx.save();
ctx.clearRect(0, 0, this.params.width, this.params.height);
const dpi = 1 / window.devicePixelRatio;
ctx.translate(this.params.width * dpi / 2, this.params.height * dpi / 2);
const s = 0.2;
ctx.scale(s, s);
const {nodes, edges} = this.nodesAndEdges();
edges.forEach(edge => this.drawEdge(edge));
nodes.forEach(node => this.drawNode(node));
ctx.restore();
}
}
Insert cell
padding = 3
Insert cell
// taken from https://observablehq.com/@roblallier/rectangle-collision-force
function forceCollide() {
let nodes;

function force(alpha) {
const quad = d3.quadtree(nodes, d => d.x, d => d.y);
for (const d of nodes) {
quad.visit((q, x1, y1, x2, y2) => {
let updated = false;
if(q.data && q.data !== d){
let x = d.x - q.data.x,
y = d.y - q.data.y,
xSpacing = padding + (q.data.width + d.width) / 2,
ySpacing = padding + (q.data.height + d.height) / 2,
absX = Math.abs(x),
absY = Math.abs(y),
l,
lx,
ly;

if (absX < xSpacing && absY < ySpacing) {
l = Math.sqrt(x * x + y * y);

lx = (absX - xSpacing) / l;
ly = (absY - ySpacing) / l;

// the one that's barely within the bounds probably triggered the collision
if (Math.abs(lx) > Math.abs(ly)) {
lx = 0;
} else {
ly = 0;
}
d.x -= x *= lx;
d.y -= y *= ly;
q.data.x += x;
q.data.y += y;

updated = true;
}
}
return updated;
});
}
}

force.initialize = _ => nodes = _;

return force;
}
Insert cell
// taken from https://gist.github.com/lvngd/5f120a517a3b7bf607b56eeac093c65
function rectCollide() {
var nodes,sizes,masses;
var strength = 1;
var iterations = 1;
var nodeCenterX;
var nodeMass;
var nodeCenterY;

function force() {

var node;
var i = -1;
while (++i < iterations){iterate();}
function iterate(){
var quadtree = d3.quadtree(nodes, xCenter, yCenter);
var j = -1
while (++j < nodes.length){
node = nodes[j];
nodeMass = masses[j];
nodeCenterX = xCenter(node);
nodeCenterY = yCenter(node);
quadtree.visit(collisionDetection);
}
}

function collisionDetection(quad, x0, y0, x1, y1) {
var updated = false;
var data = quad.data;
if(data){
if (data.index > node.index) {

let xSize = (node.width + data.width) / 2;
let ySize = (node.height + data.height) / 2;
let dataCenterX = xCenter(data);
let dataCenterY = yCenter(data);
let dx = nodeCenterX - dataCenterX;
let dy = nodeCenterY - dataCenterY;
let absX = Math.abs(dx);
let absY = Math.abs(dy);
let xDiff = absX - xSize;
let yDiff = absY - ySize;

if(xDiff < 0 && yDiff < 0){
//collision has occurred

//separation vector
let sx = xSize - absX;
let sy = ySize - absY;
if(sx < sy){
if(sx > 0){
sy = 0;
}
}else{
if(sy > 0){
sx = 0;
}
}
if (dx < 0){
sx = -sx;
}
if(dy < 0){
sy = -sy;
}

let distance = Math.sqrt(sx*sx + sy*sy);
let vCollisionNorm = {x: sx / distance, y: sy / distance};
let vRelativeVelocity = {x: data.vx - node.vx, y: data.vy - node.vy};
let speed = vRelativeVelocity.x * vCollisionNorm.x + vRelativeVelocity.y * vCollisionNorm.y;
if (speed < 0){
//negative speed = rectangles moving away
}else{
var collisionImpulse = 2*speed / (masses[data.index] + masses[node.index]);
if(Math.abs(xDiff) < Math.abs(yDiff)){
//x overlap is less
data.vx -= (collisionImpulse * masses[node.index] * vCollisionNorm.x);
node.vx += (collisionImpulse * masses[data.index] * vCollisionNorm.x);
}else{
//y overlap is less
data.vy -= (collisionImpulse * masses[node.index] * vCollisionNorm.y);
node.vy += (collisionImpulse * masses[data.index] * vCollisionNorm.y);
}
updated = true;
}
}
}
}
return updated
}
}//end force

function xCenter(d) { return d.x + d.vx + sizes[d.index][0] / 2 }
function yCenter(d) { return d.y + d.vy + sizes[d.index][1] / 2 }

force.initialize = function (_) {
sizes = (nodes = _).map(function(d){return [d.width,d.height]})
masses = sizes.map(function (d) { return d[0] * d[1] })
}

force.strength = function (_) {
return (arguments.length ? (strength = +_, force) : strength)
}

force.iterations = function (_) {
return (arguments.length ? (iterations = +_, force) : iterations)
}

return force
}
Insert cell
fr = new ForceRect({width, height: 1000, invalidation})
Insert cell
fr.simulation()
Insert cell
fr.canvas()
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more