Canvas points bounded inside / outside a circle. The points inside the circle have variable radius and opacity.
Based on Mike Bostock Circles. Canvas transition taken from this Bocoup's article
Canvas points bounded inside / outside a circle. The points inside the circle have variable radius and opacity.
Based on Mike Bostock Circles. Canvas transition taken from this Bocoup's article
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<style> | |
button { | |
font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif; | |
font-size: 18px; | |
color: black; | |
pointer-events: all; | |
} | |
.disabled { | |
color: grey; | |
pointer-events: none; | |
} | |
</style> | |
<body> | |
<div> | |
<button class="restart disabled">RESTART</button> | |
<button class="stop">STOP</button> | |
</div> | |
<script src="//d3js.org/d3.v5.min.js"></script> | |
<script> | |
// - - - - - constant values - - - - - // | |
const OPACITY = 0.8, | |
FILL = {inside: `rgba(255, 5, 5, ${OPACITY})`, outside: 'rgba(5, 5, 5, 0.5)', in: 'rgba(5, 255, 5, 0.8)'}, | |
STROKE = 'rgba(5, 5, 5, 0.8)', | |
SIZE = {w: 960, h: 420}, | |
RADIUS = {point: {min: 2, max: 4}, circle: 120}, | |
VELOCITY = {min: 0.15, dev: 0.1}, | |
DENSITY = 0.0015, // particles per sq px; | |
DURATION = 5500; | |
// - - - - - canvas - - - - - // | |
const canvas = d3.select('body').append('canvas') | |
.attr('width', SIZE.w + 'px') | |
.attr('height', SIZE.h + 'px'); | |
const context = canvas.node().getContext('2d'); | |
// - - - - - random circle & points - - - - - // | |
const circle = { | |
r: RADIUS.circle, | |
x: SIZE.w / 2, | |
y: SIZE.h / 2 | |
}; | |
const N = Math.round(SIZE.w * SIZE.h * DENSITY); | |
const points = d3.range(N).map(() => { | |
var point = {}; | |
point.r = Math.random() * (RADIUS.point.max - RADIUS.point.min) + RADIUS.point.min; | |
point.x = Math.round(Math.random() * SIZE.w); | |
point.y = Math.round(Math.random() * SIZE.h); | |
point.dx = (Math.random() * VELOCITY.dev + VELOCITY.min) * (Math.round(Math.random()) * 2 - 1); | |
point.dy = (Math.random() * VELOCITY.dev + VELOCITY.min) * (Math.round(Math.random()) * 2 - 1); | |
point.position = getPoint(point).position; | |
point.o = OPACITY; | |
if (point.position == 'in') getPoint(point).coords; | |
point.fill = FILL[point.position]; | |
return point; | |
}); | |
// - - - - - inside points increase radius and opacity - - - - - // | |
const rScale = d3.scaleThreshold().domain([0.50, 0.90, 0.95, 1]).range([5, 10, 15, 22, 35]); | |
const opacityScale = d3.scaleLinear().domain([5, 35]).range([0.5, 0.9]) | |
const n = points.filter(d => d.position == 'inside').length; | |
points.filter(d => d.position == 'inside').forEach((d, i) => { | |
d.sr = d.r; | |
d.tr = rScale((i + 1) / n) * (Math.random() * (1.1 - 0.8) + 0.8) | |
d.so = OPACITY; | |
d.to = opacityScale(d.tr); | |
}); | |
// - - - - - trigger animation - - - - - // | |
var timer = d3.timer(floating); | |
// - - - - - restart - - - - - // | |
d3.select('.restart').on('click', () => { | |
d3.select('.restart').classed('disabled', true); | |
timer.restart(floating) | |
}); | |
d3.select('.stop').on('click', () => { | |
d3.select('.restart').classed('disabled', false); | |
timer.stop(); | |
}); | |
// - - - - - functions - - - - - // | |
function floating(elapsed) { | |
if (elapsed > 50500) { | |
timer.stop(); | |
d3.select('.restart').classed('disabled', false); | |
return; | |
} | |
const t = Math.min(1, d3.easeCubicIn(elapsed / DURATION)); | |
points.forEach(d => { | |
// Make inside points bigger and lighter | |
if (d.position == 'inside' && t !== 1) { | |
d.o = (d.so * (1 - t) + d.to * t) | |
d.r = (d.sr * (1 - t) + d.tr * t) | |
d.fill = `rgba(255, 5, 5, ${d.o})` | |
} | |
// Change direction of points scaping | |
if (getPoint(d).scape) { | |
d.dx *= -1; | |
d.dy *= -1; | |
// Avoid them to get stuck in the border | |
if (d.position == 'inside' ) getPoint(d).coords; | |
} | |
// Float every point | |
d.x += d.dx; | |
d.y += d.dy; | |
}); | |
draw(); | |
} | |
function getPoint(point) { | |
return { | |
get position() { | |
switch(true) { | |
case _dist() + point.r < circle.r: | |
return 'inside' | |
break; | |
case _dist() - point.r > circle.r: | |
return 'outside' | |
break; | |
default: | |
return 'in' | |
} | |
}, | |
get coords() { | |
const angle = Math.atan2(point.y - circle.y, point.x - circle.x), | |
newRad = _dist() < circle.r ? circle.r - point.r - 0.3: circle.r + point.r + 0.3; | |
point.x = Math.cos(angle) * newRad + circle.x; | |
point.y = Math.sin(angle) * newRad + circle.y; | |
point.position = this.position; | |
}, | |
get scape() { | |
switch(true) { | |
case this.position == 'in': | |
return true | |
break; | |
case point.x + point.r > SIZE.w || point.x - point.r < 0: | |
return true | |
break; | |
case point.y + point.r > SIZE.h || point.y - point.r < 0: | |
return true | |
break; | |
default: | |
return false | |
} | |
} | |
} | |
function _diff(c) { | |
return circle[c] - point[c]; | |
} | |
function _dist() { | |
return Math.sqrt(_diff('x') * _diff('x') + _diff('y') * _diff('y')); | |
} | |
} | |
function draw() { | |
context.clearRect(0, 0, SIZE.w, SIZE.h) | |
// points | |
for (let i = 0; i < points.length; ++i) { | |
context.beginPath(); | |
context.fillStyle = points[i].fill; | |
context.arc(points[i].x, points[i].y, points[i].r, 0, 2 * Math.PI); | |
context.fill(); | |
} | |
// circle | |
context.beginPath(); | |
context.fillStyle = 'rgba(0, 0, 0, 0)'; | |
context.strokeStyle = STROKE; | |
context.arc(circle.x, circle.y, circle.r, 0, 2 * Math.PI); | |
context.fill(); | |
context.stroke(); | |
} | |
</script> |