Difference in how collisions are handled by d3.forceBounce and d3.forceCollide.
d3.forceCollide with full strength (1) is equivalent to d3.forceBounce with 0 elasticity (inelastic collision), which loses kinetic energy at each impact.
Difference in how collisions are handled by d3.forceBounce and d3.forceCollide.
d3.forceCollide with full strength (1) is equivalent to d3.forceBounce with 0 elasticity (inelastic collision), which loses kinetic energy at each impact.
<head> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/4.8.0/d3.min.js"></script> | |
<script src="//unpkg.com/d3-force-bounce"></script> | |
<link rel="stylesheet" href="style.css"> | |
</head> | |
<body> | |
<div class="canvas-wrapper"> | |
<a class="canvas-title" href="https://github.com/vasturiano/d3-force-bounce">d3.forceBounce</a> | |
<div class="controls"> | |
Elasticity: | |
<input class="slider" type="range" min="0" max="1" step="0.1" value="1" oninput="onControlChange(this.value, 'bounce', 'elasticity')"> | |
<span class="val">1</span> | |
</div> | |
<svg id="canvasBounce"> | |
<g class="trails"></g> | |
</svg> | |
</div> | |
<div class="canvas-wrapper"> | |
<a class="canvas-title" href="https://github.com/d3/d3-force#forceCollide">d3.forceCollide</a> | |
<div class="controls"> | |
Strength: | |
<input class="slider" type="range" min="0" max="1" step="0.1" value="1" oninput="onControlChange(this.value, 'collide', 'strength')"> | |
<span class="val">1</span> | |
</div> | |
<svg id="canvasCollide"> | |
<g class="trails"></g> | |
</svg> | |
</div> | |
<script src="index.js"></script> | |
</body> |
const BALL_RADIUS = 8, | |
BALL_COLORS = ['green', 'blue']; | |
const canvasWidth = window.innerWidth/2 - 12, | |
canvasHeight = window.innerHeight - 8; | |
const state = { | |
bounce: { canvas: d3.select('svg#canvasBounce') }, | |
collide: { canvas: d3.select('svg#canvasCollide') } | |
}; | |
[state.bounce, state.collide].forEach(state => { | |
// Size canvi | |
state.canvas.attr('width', canvasWidth) | |
.attr('height', canvasHeight); | |
// Setup force system | |
state.forceSim = d3.forceSimulation() | |
.alphaDecay(0) | |
.velocityDecay(0) | |
.on('tick', () => { ballDigest(state); }); | |
}); | |
state.bounce.forceSim.force('collision', d3.forceBounce().elasticity(1)); | |
state.collide.forceSim.force('collision', d3.forceCollide().strength(1)); | |
// Set collision radius | |
[state.bounce, state.collide].forEach(state => { | |
state.forceSim.force('collision') | |
.radius(n => n.r || BALL_RADIUS); | |
}); | |
// Periodical kickstart | |
kickStart(); | |
setInterval(kickStart, 15000); | |
// Event handlers | |
function onControlChange(val, mode, prop) { | |
const module = state[mode]; | |
d3.select(module.canvas.node().parentNode).select('.val').text(val); | |
module.forceSim.force('collision')[prop](val); | |
kickStart(); | |
} | |
// | |
function ballDigest(state) { | |
let ball = state.canvas.selectAll('circle.ball').data(state.forceSim.nodes()); | |
ball.exit().remove(); | |
ball.merge( | |
ball.enter().append('circle') | |
.classed('ball', true) | |
.attr('fill', (d,idx) => BALL_COLORS[idx%BALL_COLORS.length]) | |
) | |
.attr('r', d => d.r || BALL_RADIUS) | |
.attr('cx', d => d.x) | |
.attr('cy', d => d.y); | |
// Update trails | |
const trailsG = state.canvas.select('.trails'); | |
state.forceSim.nodes().forEach((node, idx) => { | |
trailsG.append('circle') | |
.attr('r', 1) | |
.attr('cx', node.x) | |
.attr('cy', node.y) | |
.attr('fill', BALL_COLORS[idx%BALL_COLORS.length]) | |
.style('opacity', 0.2) | |
.transition().delay(30000) | |
.remove(); | |
}); | |
} | |
function kickStart() { | |
const numExamples = 7, | |
h = [ 0.25, 0.5, 0.75].map(r => canvasWidth * r), | |
v = d3.range(numExamples).map(n => canvasHeight * (n+0.5)/numExamples); | |
// Clear all trails | |
d3.selectAll('.trails').selectAll('*').remove(); | |
[state.bounce, state.collide].forEach(state => { | |
const sim = state.forceSim, | |
balls = [ | |
{x: h[0], y: v[0] , future: { vx: 3 }}, {x: h[1], y: v[0]}, | |
{x: h[0], y: v[1] - 3 , future: { vx: 3 }}, {x: h[1], y: v[1]}, | |
{x: h[0], y: v[2] , future: { vx: 3 }}, {x: h[2], y: v[2], future: { vx: -3 }}, | |
{x: h[0], y: v[3] , future: { vx: 6 }}, {x: h[2], y: v[3], future: { vx: -2 }}, | |
{x: h[0], y: v[4], r: BALL_RADIUS*4 , future: { vx: 3 }}, {x: h[2], y: v[4], future: { vx: -3 }}, | |
{x: h[0], y: v[5] - BALL_RADIUS , future: { vx: 3 }}, {x: h[2], y: v[5], future: { vx: -3 }}, | |
{x: h[0], y: v[6] , future: { vx: 100 }}, {x: h[1], y: v[6]} | |
]; | |
// Initial state | |
sim.nodes(balls); | |
setTimeout(() => { | |
// Apply future | |
balls.filter(ball => ball.future).forEach(ball => { | |
Object.keys(ball.future).forEach(attr => { ball[attr] = ball.future[attr]}); | |
}); | |
}, 800); | |
}); | |
} |
body { | |
margin: 0; | |
text-align: center; | |
font-family: sans-serif; | |
} | |
.canvas-wrapper { | |
position: relative; | |
display: inline-block; | |
box-sizing: border-box; | |
border: 1px solid grey; | |
margin-top: 3px; | |
} | |
.canvas-title { | |
position: absolute; | |
top: 10px; | |
left: 10px; | |
font-size: 18px; | |
} | |
.controls { | |
position: absolute; | |
right: 0; | |
margin: 8px; | |
padding: 1px 5px 5px 5px; | |
background: rgba(230, 230, 250, 0.7); | |
opacity: 0.5; | |
border-radius: 3px; | |
font-size: 14px; | |
z-index: 1000; | |
} | |
.controls:hover { | |
opacity: 1; | |
} | |
.slider { | |
position: relative; | |
top: 3px; | |
cursor: grab; | |
cursor: -webkit-grab; | |
} | |
.slider:active { | |
cursor: grabbing; | |
cursor: -webkit-grabbing; | |
} |