Update:
I wanted to see if anything about this idea is viable, so I cheepeeteed a prototype together. It took several iterations, but I’m quite frankly shocked that it managed to arrive at a thing which has first semblances of the writhing tape behaviour. Find the code below.
Some comments and learnings from along the way:
-
This is AI-generated / human-curated code.
-
ClickAndDrag any point on the tape to move it around.
-
Changing stiffness and damping variables give the tape different writhing characteristics.
-
As I posited earlier, the intersection of the tape upon itself is prevented by applying a repulsion force between all points on the tape. It works, but only if you move things around sloooowly. If you move too fast, things can easily intersect again. Though watching the tape “self-unfurl” is kinda neat.
-
The “stiffness” of a band is the bending resistance or angular stiffness. In essence, the stronger the bend at any point (the smaller the angle between the two links to the neighbouring points), the stronger it’s effect on it’s neighbours become.
-
I’m still not sure what to make of AI-generated sketches. For one, I’d absolutely not have been able to build something close like this, in a decent amount of time, on my own. But I hate that I don’t really understand the sketch clearly, line for line. It’s not “mine”. But maybe it’s fine as a rough prototype?I remain cautiously intrigued.
-
Is there a good verb for using ChatGPT? * cheepeeteed* feels… unwieldy. prompted? ai’d? chatted?
int numPoints = 80; // Number of points on the band
float spacing = 10; // Fixed length between points
int constraintIterations = 10; // Number of iterations for enforcing constraints
float damping = 0.5; // Damping factor for velocity
float stiffness = 0.3; // Stiffness coefficient for angular resistance
int selectedPoint = -1; // Index of the point being dragged
float repulsionThreshold = 20; // Minimum distance between segments to avoid collision
float repulsionStrength = 0.5; // Strength of repulsive forces
PVector[] points; // Current positions of the points
PVector[] prevPoints; // Previous positions for Verlet integration
void setup() {
size(800, 600);
points = new PVector[numPoints];
prevPoints = new PVector[numPoints];
// Initialize points along a straight line
for (int i = 0; i < numPoints; i++) {
points[i] = new PVector(100 + i * spacing, height / 2);
prevPoints[i] = points[i].copy(); // Start with no initial motion
}
}
void draw() {
background(30);
// Draw the band
stroke(200);
strokeWeight(2);
for (int i = 0; i < numPoints - 1; i++) {
line(points[i].x, points[i].y, points[i + 1].x, points[i + 1].y);
}
// Draw the points
noStroke();
fill(255, 100, 100);
for (int i = 0; i < numPoints; i++) {
ellipse(points[i].x, points[i].y, 10, 10);
}
// Update physics
updatePhysics();
// Enforce constraints
enforceConstraints();
// Apply bending stiffness
applyBendingResistance();
// Handle self-intersections
handleCollisions();
}
void updatePhysics() {
for (int i = 0; i < numPoints; i++) {
if (i != selectedPoint) { // Skip the dragged point
// Verlet integration with damping
PVector temp = points[i].copy();
PVector velocity = PVector.sub(points[i], prevPoints[i]); // Current - Previous
velocity.mult(damping); // Apply damping to the velocity
points[i].add(velocity); // Update the current position
prevPoints[i] = temp; // Store the old position
}
}
}
void enforceConstraints() {
for (int k = 0; k < constraintIterations; k++) { // Iterative solver
for (int i = 0; i < numPoints - 1; i++) {
PVector dir = PVector.sub(points[i + 1], points[i]);
float dist = dir.mag();
float error = dist - spacing; // How far the distance is from the desired length
dir.normalize();
dir.mult(0.5 * error); // Distribute the correction equally
// Move points to satisfy the distance constraint
if (i != selectedPoint) points[i].add(dir);
if (i + 1 != selectedPoint) points[i + 1].sub(dir);
}
}
}
void applyBendingResistance() {
for (int i = 1; i < numPoints - 1; i++) { // Skip endpoints
PVector prevSegment = PVector.sub(points[i], points[i - 1]);
PVector nextSegment = PVector.sub(points[i + 1], points[i]);
prevSegment.normalize();
nextSegment.normalize();
float cosTheta = PVector.dot(prevSegment, nextSegment); // Cosine of the angle
float angleDeviation = 1 - cosTheta; // Larger when angle is smaller
PVector correction = PVector.add(prevSegment, nextSegment);
correction.normalize();
correction.mult(angleDeviation * stiffness); // Scale correction by stiffness
// Apply correction to neighboring points
if (i - 1 != selectedPoint) points[i - 1].sub(correction);
if (i + 1 != selectedPoint) points[i + 1].add(correction);
}
}
void handleCollisions() {
for (int i = 0; i < numPoints - 1; i++) {
for (int j = i + 2; j < numPoints - 1; j++) {
// Ignore adjacent or overlapping segments
if (abs(i - j) <= 1) continue;
float dist = segmentDistance(points[i], points[i + 1], points[j], points[j + 1]);
if (dist < repulsionThreshold) {
applyRepulsion(i, j, dist);
}
}
}
}
// Calculate the minimum distance between two line segments
float segmentDistance(PVector p1, PVector p2, PVector q1, PVector q2) {
// Check all point-to-segment distances and take the minimum
float d1 = pointToSegmentDistance(q1, p1, p2);
float d2 = pointToSegmentDistance(q2, p1, p2);
float d3 = pointToSegmentDistance(p1, q1, q2);
float d4 = pointToSegmentDistance(p2, q1, q2);
return min(min(d1, d2), min(d3, d4));
}
// Calculate the distance from a point to a line segment
float pointToSegmentDistance(PVector p, PVector a, PVector b) {
PVector ap = PVector.sub(p, a);
PVector ab = PVector.sub(b, a);
float t = constrain(ap.dot(ab) / ab.magSq(), 0, 1); // Project p onto ab, clamped to segment
PVector projection = PVector.add(a, ab.mult(t));
return PVector.dist(p, projection);
}
// Apply a repulsive force to separate two intersecting segments
void applyRepulsion(int i, int j, float dist) {
// Calculate the correction vector
float correctionMagnitude = map(dist, 0, repulsionThreshold, repulsionStrength, 0);
PVector correction = PVector.sub(points[j], points[i]).normalize().mult(correctionMagnitude);
// Apply corrections to segment points
if (i != selectedPoint) points[i].sub(correction);
if (i + 1 != selectedPoint) points[i + 1].sub(correction);
if (j != selectedPoint) points[j].add(correction);
if (j + 1 != selectedPoint) points[j + 1].add(correction);
}
void mousePressed() {
// Check if a point is clicked
for (int i = 0; i < numPoints; i++) {
if (dist(mouseX, mouseY, points[i].x, points[i].y) < 10) {
selectedPoint = i;
break;
}
}
}
void mouseDragged() {
if (selectedPoint != -1) {
// Move the selected point with the mouse
points[selectedPoint].set(mouseX, mouseY);
}
}
void mouseReleased() {
if (selectedPoint != -1) {
// Reset velocity by syncing current and previous positions
prevPoints[selectedPoint] = points[selectedPoint].copy();
selectedPoint = -1; // Release the selected point
}
}