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); }); }