Public
Edited
Feb 24, 2023
1 fork
2 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
class CircleScroller {
constructor(width, height) {
this.width = width;
this.height = height;
this.timer = 0;
this.bandwidth = this.width / 10;
this.transformOffsetX = this.width / 2 - this.bandwidth / 2;
this.maxRadius = this.height;
this.padding = 2;
this.xSpeed = width / 200;
this.xStart = 0;
this.circleLifespan = 200;
this.circles = []; // List of circles {x, y, radius, createdTime}
this.cm = d3.scaleSequential().interpolator(d3.interpolateSpectral).domain([0, 1]);
this.showGuides = false;
}

addCircle(k=1) {
// Use Mitchells best candidate algorithm
var bestX, bestY, bestD = 0;
for (let i = 0; i < k; ++i) {
// Generate candidate
const x = (this.bandwidth) * Math.random() + this.xStart;
const y = (this.height) * Math.random();
var minDistance = this.maxRadius;
// Check against existing circles
for (let j = 0; j < this.circles.length && minDistance > 0; ++j) {
const c = this.circles[j];
const dSquared = distanceSquared(x, y, c[0], c[1]);
if (dSquared < c[2] * c[2]) minDistance = 0; // Inside an existing circle.
const d = Math.sqrt(dSquared) - c[2];
if (d < minDistance) minDistance = d;
}
// If that's the best one so far then update best...
if (minDistance > bestD && minDistance > this.padding) bestX = x, bestY = y, bestD = minDistance - this.padding;
}
const best = [bestX, bestY, bestD, this.timer, Math.random()];
this.circles.push(best);
return best;
}

tick() {
this.addCircle(10);
// Advance the timer and range a little.
this.timer++;
this.xStart = this.timer * this.xSpeed;
// Remove circles that fall outside of the drawble range
this.circles = this.circles.filter(c => c[0] > this.xStart - this.transformOffsetX - this.maxRadius)
}

draw(context) {
context.save()
context.fillStyle = 'hsl(216deg 100% 13%)'
context.fillRect(0, 0, this.width, this.height);
context.translate(this.transformOffsetX - this.xStart, 0)
// Fill the canvas with bg.
for (let i = 0; i < this.circles.length; ++i) {
const c = this.circles[i];
const lifeSpanPc = (this.timer - c[3]) / this.circleLifespan
const r = (
// c[2] * Math.max((1 - ((lifeSpanPc - 0.5) * 2) ** 2), 0) // Grows and shrinks over lifespan
Math.min(c[2] * lifeSpanPc * 10, c[2]) // Grows in half the particles lifespan.
);
context.fillStyle = this.cm(c[4]) // Could do this.cm(r/ this.maxRadius);
context.lineWidth = 2;
context.beginPath();
context.arc(c[0], c[1], r, 0, 2 * Math.PI, false);
context.fill();
context.beginPath();
if (this.showGuides) {
// Show generation range.
context.strokeStyle = 'white';
context.moveTo(this.xStart, 0);
context.lineTo(this.xStart, this.height);
// Show generation range.
context.strokeStyle = 'white';
context.moveTo(this.xStart + this.bandwidth, 0);
context.lineTo(this.xStart + this.bandwidth, this.height);
context.stroke();
}
}
context.restore()
}

setBandwidth(v) {
this.bandwidth = v;
this.transformOffsetX = this.width / 2 - v / 2;
}
toggleGuides() {
this.showGuides = !this.showGuides;
}
getCircles() {
return this.circles;
}
}

Insert cell
function distanceSquared(x0,y0,x1,y1) {
return (x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0)
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more