我一直在 D3 中生成一组弹跳球。我已经设法让系统按照以下规则运行:
我设法找到了一个问题然而我无法弄清楚是什么原因造成的,每一次Orbs似乎都会卡住 - 一个节点会卡在另一个节点上,跟着它而不是弹跳离开,有时最终落在另一个球体的顶部,或者在另一个球体的顶部,我认为这是不可能的。
重叠逻辑相当简单,如果2个球体的中心之间的距离小于它们需要反弹的组合半径:
// Calculate the distance between the centres of
// two nodes, we don't have take the square root
// as we don't need it, and it's an expensive operation
var x = d.x - quad.point.x;
var y = d.y - quad.point.y;
var distance = x * x + y * y;
// Calculate the radius of each to determine how
// close they need to be to be considered touching
var radii = getSize(d) + getSize(quad.point);
// If the nodes appear to be touching then we need to move the points
if (distance < (radii * radii)) {
// Overlapping
}
如果它们反弹,我会计算它们应该反弹的角度和速度,保留动量,这是一个记录的算法。因此,我无法弄清楚两颗球如何相互跟随?
function bounce_vectors(d1, d2) {
// Calculate the angle at which the balls collide
var dx = d1.x - d2.x;
var dy = (d1.y - d2.y) * -1;
var collisionAngle = Math.atan2(dy, dx);
// Calculate the speed of d1/d2
var speed1 = Math.sqrt((d1.speedX * d1.speedX) + (d1.speedY * d1.speedY));
var speed2 = Math.sqrt((d2.speedX * d2.speedX) + (d2.speedY * d2.speedY));
// Get angles (in radians) for each ball, given current velocities
var direction1 = Math.atan2(d1.speedY, d1.speedX);
var direction2 = Math.atan2(d2.speedY, d2.speedX);
// Rotate velocity vectors so we can plug into equation for conservation of momentum
var rotatedVelocityX1 = speed1 * Math.cos(direction1 - collisionAngle);
var rotatedVelocityY1 = speed1 * Math.sin(direction1 - collisionAngle);
var rotatedVelocityX2 = speed2 * Math.cos(direction2 - collisionAngle);
var rotatedVelocityY2 = speed2 * Math.sin(direction2 - collisionAngle);
// Update actual velocities using conservation of momentum
var finalVelocityX1 = ((d1.size - d2.size) * rotatedVelocityX1 + (d2.size + d2.size) * rotatedVelocityX2) / (d1.size + d2.size);
var finalVelocityX2 = ((d1.size + d1.size) * rotatedVelocityX1 + (d2.size - d1.size) * rotatedVelocityX2) / (d1.size + d2.size);
// Y velocities remain constant
var finalVelocityY1 = rotatedVelocityY1;
var finalVelocityY2 = rotatedVelocityY2;
// Rotate angles back again so the collision angle is preserved
d1.speedX = Math.cos(collisionAngle) * finalVelocityX1 + Math.cos(collisionAngle + Math.PI/2) * finalVelocityY1;
d1.speedY = Math.sin(collisionAngle) * finalVelocityX1 + Math.sin(collisionAngle + Math.PI/2) * finalVelocityY1;
d2.speedX = Math.cos(collisionAngle) * finalVelocityX2 + Math.cos(collisionAngle + Math.PI/2) * finalVelocityY2;
d2.speedY = Math.sin(collisionAngle) * finalVelocityX2 + Math.sin(collisionAngle + Math.PI/2) * finalVelocityY2;
};
// Add an additional function to the string prototype
if (!String.prototype.format) {
String.prototype.format = function () {
var args = arguments;
return this.replace(/{(\d+)}/g, function (match, number) {
return typeof args[number] != 'undefined'
? args[number]
: match
;
});
};
}
d3.Orbs = function() {
var width = 500;
var height = 500;
var maxSpeed = 10;
var alpha = 0.026;
var nodes = [
{ id: 1, name: "Software", size: 5, children: [
{ id: 4, name: "Bug" },
{ id: 5, name: "Feature" },
{ id: 6, name: "Build" }]
},
{ id: 2, name: "Scrum", size: 8, children: [
{ id: 7, name: "Sprint" },
{ id: 8, name: "Story" },
{ id: 9, name: "Story Point" }]
},
{ id: 3, name: "Resource", size: 12, children: [
{ id: 10, name: "Location" },
{ id: 11, name: "Team" },
{ id: 12, name: "Members" }]
},
{ id: 13, name: "A", size: 12 },
{ id: 14, name: "B", size: 9 },
{ id: 15, name: "C", size: 3 }
];
/**
* Initialize each of the nodes in the dataset to ensure
* that the required positions and speeds exist in the data
* for collision avoidance and applying gravity
* @param {array} The collection of nodes from the raw data
* @param {object} A bounding rectangle to restrict where the nodes can be placed
*/
function initialize_nodes(nodes, bounds) {
// Ensure each node has a random start location and speed
nodes.forEach(function(d) {
d.x = Math.random() * bounds.right;
d.y = Math.random() * bounds.bottom;
d.speedX = (Math.random() - 0.5) * 2 * maxSpeed;
d.speedY = (Math.random() - 0.5) * 2 * maxSpeed;
});
/*nodes[0].x = 100;
nodes[0].y = 100;
nodes[0].speedX = 10;
nodes[0].speedY = -10;
nodes[0].size = 10;
nodes[0].fill = 'red';
nodes[1].x = 200;
nodes[1].y = 400;
nodes[1].speedX = -10;
nodes[1].speedY = 40;
nodes[1].size = 20;
nodes[1].fill = 'steelblue';*/
};
// Initialize the nodes now
initialize_nodes(nodes, {left: 0, top: 0, bottom: height, right: width});
var force = d3.layout.force()
.alpha(alpha)
.gravity(0)
.charge(0)
.nodes(nodes)
.size([width, height])
.on("tick", tick);
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 500);
var node = svg.selectAll(".node")
.data(nodes)
.enter()
.append("circle")
.attr("class", "node")
.style('fill', function(d) { return d.fill; })
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", function(d) { return getSize(d); });
function getSize(node) {
return 5 * node.size;
}
/**
* Return a function that applies gravity to an individual node
* @param {number} The alpha or kinetic energy currently in the system
* @returns {function} The funtion that applies gravity to each individual node
*/
function gravity(alpha) {
/**
* Update the position and speed of the node based upon it's current location
* @param {Object} The data node that is being moved
*/
return function(d) {
var radius = getSize(d);
// If the node is attempting to move off screen then invert the speed
// on that axis to bring it back into scope
if ((d.x - radius) < 0) d.speedX = Math.abs(d.speedX);
if ((d.x + radius) > width) d.speedX = -1 * Math.abs(d.speedX);
if ((d.y - radius) < 0) d.speedY = -1 * Math.abs(d.speedY);
if ((d.y + radius) > height) d.speedY = Math.abs(d.speedY);
// Set the current position of the node
d.x = d.x + (d.speedX * alpha);
d.y = d.y + (-1 * d.speedY * alpha);
// Apply some extra protection - if the node has moved off the screen
// then get it back again rather than waiting for it's speed
d.x = Math.min(Math.max(d.x, 0 + radius), width - radius);
d.y = Math.min(Math.max(d.y, 0 + radius), height - radius);
};
}
/**
* Update the positions of nodes whenever we recieved a tick notification
* from the force layout algorithm
* @param {object} D3 tick event args
*/
function tick(e) {
// Ensure that the Alpha never drops to 0
// Note that on mobile devices this could be a big battery drain
force.alpha(alpha);
// Update the position of each node by applying a gravity function
// and preventing any collisions of the nodes
node.each(gravity(.51 * e.alpha))
.each(function(d) { d.color = '#2B301C';})
.each(function(d) { d.bounced = d.bounced || []; })
.each(resolve_collisions(0.5))
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.style("stroke", function(d) { return d.color || '#2B301C'; });
};
function resolve_collisions(alpha) {
// Build a quad tree from the nodes
var quadtree = d3.geom.quadtree(nodes);
// http://www.exeneva.com/2012/06/multiple-balls-bouncing-and-colliding-example/
function bounce_vectors(d1, d2) {
// Calculate the angle at which the balls collide
var dx = d1.x - d2.x;
var dy = (d1.y - d2.y) * -1;
var collisionAngle = Math.atan2(dy, dx);
// Calculate the speed of d1/d2
var speed1 = Math.sqrt((d1.speedX * d1.speedX) + (d1.speedY * d1.speedY));
var speed2 = Math.sqrt((d2.speedX * d2.speedX) + (d2.speedY * d2.speedY));
// Get angles (in radians) for each ball, given current velocities
var direction1 = Math.atan2(d1.speedY, d1.speedX);
var direction2 = Math.atan2(d2.speedY, d2.speedX);
// Rotate velocity vectors so we can plug into equation for conservation of momentum
var rotatedVelocityX1 = speed1 * Math.cos(direction1 - collisionAngle);
var rotatedVelocityY1 = speed1 * Math.sin(direction1 - collisionAngle);
var rotatedVelocityX2 = speed2 * Math.cos(direction2 - collisionAngle);
var rotatedVelocityY2 = speed2 * Math.sin(direction2 - collisionAngle);
// Update actual velocities using conservation of momentum
var finalVelocityX1 = ((d1.size - d2.size) * rotatedVelocityX1 + (d2.size + d2.size) * rotatedVelocityX2) / (d1.size + d2.size);
var finalVelocityX2 = ((d1.size + d1.size) * rotatedVelocityX1 + (d2.size - d1.size) * rotatedVelocityX2) / (d1.size + d2.size);
// Y velocities remain constant
var finalVelocityY1 = rotatedVelocityY1;
var finalVelocityY2 = rotatedVelocityY2;
// Rotate angles back again so the collision angle is preserved
d1.speedX = Math.cos(collisionAngle) * finalVelocityX1 + Math.cos(collisionAngle + Math.PI/2) * finalVelocityY1;
d1.speedY = Math.sin(collisionAngle) * finalVelocityX1 + Math.sin(collisionAngle + Math.PI/2) * finalVelocityY1;
d2.speedX = Math.cos(collisionAngle) * finalVelocityX2 + Math.cos(collisionAngle + Math.PI/2) * finalVelocityY2;
d2.speedY = Math.sin(collisionAngle) * finalVelocityX2 + Math.sin(collisionAngle + Math.PI/2) * finalVelocityY2;
};
return function(d, i) {
// Using a quad tree to speed things up visit each pair of nodes
quadtree.visit(function(quad, x1, y1, x2, y2) {
// Compare points together - but don't compare the
// current point to itself
if (quad.point && (quad.point !== d)) {
// Calculate the distance between the centres of
// two nodes, we don't have take the square root
// as we don't need it, and it's an expensive operation
var x = d.x - quad.point.x;
var y = d.y - quad.point.y;
var distance = x * x + y * y;
// Calculate the radius of each to determine how
// close they need to be to be considered touching
var radii = getSize(d) + getSize(quad.point);
// If the nodes appear to be touching then we need to move the points
if (distance < (radii * radii)) {
// If these items have already bounced then continue
if(d.bounced.indexOf(quad.point) !== -1) {
return;
}
d.bounced.push(quad.point);
quad.point.bounced.push(d);
setTimeout(function() {
var index = d.bounced.indexOf(quad.point);
d.bounced.splice(index, 1);
index = quad.point.bounced.indexOf(d);
quad.point.bounced.splice(index);
}, 5250);
d.color = 'red';
quad.point.color = 'red';
bounce_vectors(d, quad.point);
}
}
});
};
}
force.start();
};
d3.Orbs();
.node {
fill: #77B72B;
stroke: #2B301C;
stroke-width: 3px;
}
svg {
background: #222;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>