Canvas bounded circles
Circles bounded to different areas with canvas transition between them.
Based on Mike Bostock Circles. Canvas transition taken from this Bocoup's article
license: gpl-3.0 |
Canvas bounded circles
Circles bounded to different areas with canvas transition between them.
Based on Mike Bostock Circles. Canvas transition taken from this Bocoup's article
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<style> | |
body { | |
font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif; | |
} | |
</style> | |
<body> | |
<form> | |
<legend>Please select the particle limits</legend> | |
<div> | |
<input type="radio" id="large" name="bound-selector" data-w="940" data-h="480"> | |
<label for="large">Large</label> | |
<input type="radio" id="medium" name="bound-selector" data-w="600" data-h="320" checked> | |
<label for="medium">Medium</label> | |
<input type="radio" id="small" name="bound-selector" data-w="220" data-h="120"> | |
<label for="small">Small</label> | |
</div> | |
</form> | |
<script src="//d3js.org/d3.v5.min.js"></script> | |
<script> | |
// - - - - - constant values - - - - - // | |
const FILL = 'rgba(5, 5, 5, 0.5)', | |
STROKE = 'rgba(5, 5, 5, 0.2)', | |
SIZE = {w: 960, h: 500}, | |
RADIUS = {min: 1, max: 3}, | |
DENSITY = 0.00080, // particles per sq px; | |
DURATION = 750; | |
// - - - - - canvas - - - - - // | |
const canvas = d3.select('body').append('canvas') | |
.attr('width', SIZE.w + 'px') | |
.attr('height', SIZE.h + 'px'); | |
const context = canvas.node().getContext('2d'); | |
// - - - - - scales - - - - - // | |
const xScale = d3.scaleLinear() | |
.domain([0, SIZE.w]) | |
.range(getBounds().x); | |
const yScale = d3.scaleLinear() | |
.domain([0, SIZE.h]) | |
.range(getBounds().y); | |
// - - - - - random nodes - - - - - // | |
let N = Math.round(SIZE.w * SIZE.h * DENSITY); | |
const nodes = d3.range(N).map(function() { | |
return { | |
r: Math.round(Math.random() * (RADIUS.max - RADIUS.min) + RADIUS.min), | |
x: xScale(Math.round(Math.random() * SIZE.w)), | |
y: yScale(Math.round(Math.random() * SIZE.h)), | |
dx: (Math.random() - 0.5) * 0.5, | |
dy: (Math.random() - 0.5) * 0.5 | |
}; | |
}); | |
// - - - - - interaction - - - - - // | |
d3.selectAll('input').on('click', function(d) { | |
// update scales | |
xScale | |
.domain(d3.extent(nodes, d => d.x)) | |
.range(getBounds().x); | |
yScale | |
.domain(d3.extent(nodes, d => d.y)) | |
.range(getBounds().y); | |
// set nodes source and target position | |
// for the canvas animation | |
// https://bocoup.com/blog/smoothly-animate-thousands-of-points-with-html5-canvas-and-d3 | |
nodes.forEach(d => { | |
d.sx = d.x; | |
d.sy = d.y; | |
d.tx = xScale(d.x); | |
d.ty = yScale(d.y); | |
}); | |
// trigger transition animation | |
timer.restart(transition); | |
}); | |
// - - - - - trigger animation - - - - - // | |
var timer = d3.timer(floating); | |
// - - - - - functions - - - - - // | |
function floating(elapsed) { | |
if (elapsed > 10000) timer.stop() | |
nodes.forEach(d => { | |
d.x += d.dx; if (d.x > getBounds().x[1] || d.x < getBounds().x[0]) d.dx *= -1; | |
d.y += d.dy; if (d.y > getBounds().y[1] || d.y < getBounds().y[0]) d.dy *= -1; | |
}); | |
draw(); | |
} | |
function transition(elapsed) { | |
const t = Math.min(1, d3.easeCubic(elapsed / DURATION)); | |
// At end, switch to 'floating' animation | |
if (t === 1) return timer.restart(floating); | |
nodes.forEach(d => { | |
d.x = (d.sx * (1 - t) + d.tx * t) + d.dx; | |
d.y = (d.sy * (1 - t) + d.ty * t) + d.dy; | |
}); | |
draw(); | |
} | |
function draw() { | |
context.clearRect(0, 0, SIZE.w, SIZE.h) | |
// background rect | |
context.beginPath(); | |
context.strokeStyle = STROKE; | |
context.rect(getBounds().x[0], getBounds().y[0], getBounds().w, getBounds().h); | |
context.stroke(); | |
// circles | |
for (let i = 0; i < nodes.length; ++i) { | |
context.beginPath() | |
context.fillStyle = FILL | |
context.arc(nodes[i].x, nodes[i].y, nodes[i].r, 0, 2 * Math.PI) | |
context.fill() | |
} | |
} | |
function getBounds() { | |
const target = d3.selectAll('input').filter(function(d) {return this.checked; }).node().dataset; | |
return { | |
get w() { | |
return parseInt(target.w); | |
}, | |
get h() { | |
return parseInt(target.h) | |
}, | |
get x() { | |
return [(SIZE.w - this.w) / 2, (SIZE.w + this.w) / 2]; | |
}, | |
get y() { | |
return [(SIZE.h - this.h) / 2, (SIZE.h + this.h) / 2]; | |
} | |
} | |
} | |
</script> |