Public
Edited
Jul 5, 2023
14 stars
Insert cell
Insert cell
Insert cell
class Pipe {
constructor(start, end, distance) {
this.distance = distance;
this.points = [];
this.start = start;
this.end = end;
// Add enough points to span the distance between the start and end locations.
this.points.push(start.clone());
const maximumPoints = 100;
var currentPoint = start.clone();
var currentPoints = 1;
while (currentPoint.distance(end) > distance && currentPoints < maximumPoints - 1) {
var difference = end.clone().subtract(currentPoint).normalize().multiply(new Victor(distance, distance));
var newPoint = currentPoint.clone().add(difference);
this.points.push(newPoint);
currentPoint = newPoint;
currentPoints += 1;
}
this.points.push(end.clone());
}
// Determine if the pipe needs to be longer, and insert an extra control point.
stretch(index, toward) { // O(1)
// You cannot stretch on the ends of the pipe.
index = Math.max(Math.min(index, this.points.length - 2), 1);
// If the pipe hasn't reached its goal, stretch to get closer.
const sensitivity = 3; // Lower is more sensitive.
var distanceFromGoal = toward.distance(this.points[index]);
if (distanceFromGoal > this.distance * sensitivity) {
// Stretch in the direction that the pipe is being pulled.
var point = this.points[index];
var distanceToPrev = toward.distance(this.points[index - 1]);
var distanceToNext = toward.distance(this.points[index + 1]);
if (distanceToPrev > distanceToNext) {
// Add a point between previous and point.
this.points.splice(index, 0, this.points[index].clone());
} else {
// Add a point between point and next.
this.points.splice(index + 1, 0, this.points[index].clone());
}
}
// If the pipe is shorter than the distance between start to end, stretch.
var pipeLength = this.points.length * this.distance;
var minimumLength = this.start.distance(this.end);
if (pipeLength < minimumLength) {
this.points.splice(index + 1, 0, this.points[index].clone());
}
}
// Look through the list for points that are too close to each other.
shrink() { // O(nlog(n))
const sensitivity = 1;
for (var i = 1; i < this.points.length -1; i++) {
var point = this.points[i];
for (var j = this.points.length - 2; j > i + 1; j--) {
var otherPoint = this.points[j];
if (point.distance(otherPoint) < this.distance * sensitivity) {
// Remove every point between those points.
let removeAmount = j - i - 2;
this.points.splice(i + 1, removeAmount);
break;
}
}
}
}
// Update this.start and then call updateControlPoint() with the new location.
setStart(location) {
this.start = location;
this.updateControlPoint(0, location);
}
// Update this.end and then call updateControlPoint() with the new location.
setEnd(location) {
this.end = location;
this.updateControlPoint(this.points.length - 1, location);
}
// Return all control points as arrays of [x, y] coordinates, as required by d3.curve modules.
getControlPoints() { // O(n)
var array = [...this.points];
for (var index in array) {
array[index] = array[index].toArray();
}
return array;
}
// Force the first and last control points to be start and end, respectively.
updateBases() { // O(n)
this.points[0] = this.start;
for (var i = 0; i < this.points.length - 2; i++) {
var point = this.points[i];
var nextPoint = this.points[i + 1];
if (typeof point !== 'undefined' && typeof nextPoint !== 'undefined' ) {
var vector = point.clone().subtract(nextPoint); // Vector from the next point to this one.
vector.normalize().multiply(new Victor(this.distance, this.distance));
this.points[i + 1] = point.clone().subtract(vector);
} else continue;
}
this.points[this.points.length - 1] = this.end; // Force location of the end vector.
for (var i = this.points.length - 1; i > 1; i--) {
var point = this.points[i];
var nextPoint = this.points[i - 1];
if (typeof point !== 'undefined' && typeof nextPoint !== 'undefined' ) {
var vector = point.clone().subtract(nextPoint); // Vector from the next point to this one.
vector.normalize().multiply(new Victor(this.distance, this.distance));
this.points[i - 1] = point.clone().subtract(vector);
} else continue;
}
}
// Return the index of the closest control point to location.
getNearestControlPointIndex(location) { // O(n)
var closest = 0;
var closestDistance = location.distanceSq(this.points[closest])
for (var i = 0; i < this.points.length; i++) {
var currentDistance = location.distanceSq(this.points[i]);
if (currentDistance < closestDistance) {
closest = i;
closestDistance = currentDistance;
}
}
return Number(closest);
}
getControlPoint(index) { // O(1)
return this.points[index].toArray();
}
// Pull the pipe, starting at the specified index, closer to location.
updateControlPoint(index, location) { // O(n), calls make it O(nlog(n))
// Start by updating the location of the selected vector.
this.points[index] = location.clone();
// Move forward and back through the pipe, pulling the rest of it with the selected point.
for (var i = index; i > 0; i--) {
var point = this.points[i];
var prevPoint = this.points[i - 1];
if (typeof point !== 'undefined' && typeof prevPoint !== 'undefined' ) {
var vector = point.clone().subtract(prevPoint); // Vector from the previous point to this one.
vector.normalize().multiply(new Victor(this.distance, this.distance));
this.points[i - 1] = point.clone().subtract(vector);
} else continue;
}
for (var i = index; i < this.points.length - 1; i++) {
var point = this.points[i];
var nextPoint = this.points[i + 1];
if (typeof point !== 'undefined' && typeof nextPoint !== 'undefined' ) {
var vector = point.clone().subtract(nextPoint); // Vector from the next point to this one.
vector.normalize().multiply(new Victor(this.distance, this.distance));
this.points[i + 1] = point.clone().subtract(vector);
} else continue;
}
// Ensure that the ends of the pipe stay where they belong.
this.updateBases()
//If the pipe loops over itself, remove the points within the loop.
this.shrink();
//If the nearest point still isn't close enough to location, inject another control point adjacent to it.
this.stretch(index, location);
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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