I have built a two-dimensional grid of rectangles with a nested loop. Now I want to “roll up” this grid in three-dimensional space, or in other words “form a cylinder” or a “column”. With the movement of my mouse pointer. Up to the “roll up” I get everything programmed as desired – but then my mathematics fails.
float size; float pixel = 75; void setup() { size(1920, 1080, P3D); frameRate(30); size = width/pixel; rectMode(CENTER); noStroke(); } void draw() { background(0); rotateX(radians(45)); translate(pixel*size/2, -pixel*size, -pixel*size); translate(-pixel*size/2, -pixel*size/2, -pixel*size/4); pushMatrix(); for (int x = 0; x < pixel; x++) { for (int y = 0; y < pixel; y++) { pushMatrix(); float sin = sin(radians(x * 10)) * mouseX; float cos = cos(radians(x * 10)) * mouseX; translate(x*size, y*size); rotate(radians(45)); fill(255); rect(sin, cos, size/5, size/5); popMatrix(); } } popMatrix(); }
Instead of a roll up, the grid twists twice…. I thought I could achieve the “roll up” by concatenating sin(); and cos(); – similar to this example:
float sin, cos; void setup() { size(900, 900); background(0); } void draw() { translate(width/2, height/2); for (int i = 0; i < 200; i++) { sin = sin(radians(frameCount + i *10)) * 400; cos = cos(radians(frameCount + i *10)) * 400; ellipse(sin, cos, 10, 10); } }
What is the best way to achieve this roll up?
Advertisement
Answer
You are on the right track using the polar to cartesian coordinate system transformation formula.
There are multiple ways to solve this. Here’s an idea, starting in 2D first: unrolling a circle to a line. I don’t know the 100% mathematically correct way of doing this and I hope someone else posts this. I can however post a hopepfully convincing enough estimation using these “ingredients”:
- The length of the circle (circumference) is 2πR
- Processing’s
lerp()
linearly interpolates between two values (first two arguments of the function) by a percentage (expressed a value between 0.0 and 1.0 (called a normalized value) -> 0 = 0% = start value, 0.5 = 50% = half-way between stard and end value, 1.0 = 100% = at end value) - Processing provides a
PVector
class which is both handy to encapsulate 2D/3D point properties (.x, .y, .z
), but also provides alerp()
method which is a nice shorthand to avoid manually lerping 3 times (once for each dimension (x, y, z))
Here’s a basic commented sketch to illustrate the above:
// total number of points int numPoints = 24; // circle radius float radius = 50; // circle length (circumference) = 2πR float circleLength = TWO_PI * radius; // spacing between each point for the length of the circle float lengthIncrement = circleLength / numPoints; // how many radians should each point on a circle increment by float angleIncrement = TWO_PI / numPoints; // cache for points on a the circle PVector[] pointsCircle = new PVector[numPoints]; // cache for points on a line the lenght of the circle PVector[] pointsLine = new PVector[numPoints]; void setup(){ size(300, 300); noStroke(); // cache: pre-compute start(circle) and end(line) points for(int i = 0 ; i < numPoints; i++){ // compute the angle using the increment but also offset by 90 degrees so 1st point is at bottom float angle = (angleIncrement * i) + HALF_PI; pointsCircle[i] = new PVector(cos(angle) * radius, sin(angle) * radius); // compute positions on a line, offsetting by half: avoids most self-intersections when animating pointsLine[i] = new PVector(lengthIncrement * i - (circleLength * 0.5), 0); } } void draw(){ background(0); translate(width * 0.5, height * 0.5); // map interpolation amount to mouse X position float interpolationAmount = (float)mouseX / width; // for each point for(int i = 0 ; i < numPoints; i++){ // compute the interpolated position PVector pointAnimated = PVector.lerp(pointsCircle[i], pointsLine[i], interpolationAmount); // optional: visualise the first point as the darkest and last point as the brightest fill(map(i, 0, numPoints -1, 64, 255)); // render the point as a circle circle(pointAnimated.x, pointAnimated.y, 9); } }
The same logic can be applied in 3D with an extra loop to repeat circles/lines to appear as a cylinder/grid:
// total number of points int numPointsX = 24; // circle radius float radius = 50; // circle length (circumference) = 2πR float circleLength = TWO_PI * radius; // spacing between each point for the length of the circle float lengthIncrement = circleLength / numPointsX; // how many radians should each point on a circle increment by float angleIncrement = TWO_PI / numPointsX; // cache for points on a the circle PVector[] pointsCircle = new PVector[numPointsX]; // cache for points on a line the lenght of the circle PVector[] pointsLine = new PVector[numPointsX]; // number of points on Z axis int numPointsZ = 24; void setup(){ size(300, 300, P3D); // render circles as thick points noFill(); strokeWeight(9); // cache: pre-compute start(circle) and end(line) points for(int i = 0 ; i < numPointsX; i++){ // compute the angle using the increment but also offset by 90 degrees so 1st point is at bottom float angle = (angleIncrement * i) + HALF_PI; pointsCircle[i] = new PVector(cos(angle) * radius, sin(angle) * radius); // compute positions on a line, offsetting by half: avoids most self-intersections when animating pointsLine[i] = new PVector(lengthIncrement * i - (circleLength * 0.5), 0); } } void draw(){ background(0); translate(width * 0.5, height * 0.5, 0); rotateY(map(mouseX, 0, width, -PI, PI)); rotateX(map(mouseY, 0, height, PI, -PI)); float interpolationAmount = (float)mouseX / width; // render the grid (circular or rectangular) beginShape(POINTS); for(int j = 0 ; j < numPointsZ; j++){ // offset by half the size to pivot from center float z = (circleLength * 0.5) - (lengthIncrement * j); for(int i = 0 ; i < numPointsX; i++){ // compute the interpolated position PVector pointAnimated = PVector.lerp(pointsCircle[i], pointsLine[i], interpolationAmount); // render point stroke(map(i, 0, numPointsX -1, 64, 255)); vertex(pointAnimated.x, pointAnimated.y, z); } } endShape(); }
The above code would’ve worked without PVector
, but it would be more verbose.
The other thing to keep in mind is that a the static PVector.lerp()
method will generate a new PVector instance per call: this is ok for small demo such as this, but caching a bunch of PVector
s to lerp()
should waste less memory.
For the sake of completeness here are interactive versions your can run right here via p5.js:
// total number of points let numPoints = 24; // circle radius let radius = 50; // circle length (circumference) = 2πR let circleLength; // spacing between each point for the length of the circle let lengthIncrement; // how many radians should each point on a circle increment by let angleIncrement; // cache for points on a the circle let pointsCircle = new Array(numPoints); // cache for points on a line the lenght of the circle let pointsLine = new Array(numPoints); function setup(){ createCanvas(300, 300); noStroke(); // ensure TWO_PI is defined before assignment circleLength = TWO_PI * radius; lengthIncrement = circleLength / numPoints; angleIncrement = TWO_PI / numPoints; // cache: pre-compute start(circle) and end(line) points for(let i = 0 ; i < numPoints; i++){ // compute the angle using the increment but also offset by 90 degrees so 1st point is at bottom let angle = (angleIncrement * i) + HALF_PI; pointsCircle[i] = createVector(cos(angle) * radius, sin(angle) * radius); // compute positions on a line, offsetting by half: avoids most self-intersections when animating pointsLine[i] = createVector(lengthIncrement * i - (circleLength * 0.5), 0); } } function draw(){ background(0); translate(width * 0.5, height * 0.5); // map interpolation amount to mouse X position let interpolationAmount = constrain(mouseX, 0, width) / width; // for each point for(let i = 0 ; i < numPoints; i++){ // compute the interpolated position let pointAnimated = p5.Vector.lerp(pointsCircle[i], pointsLine[i], interpolationAmount); // optional: visualise the first point as the darkest and last point as the brightest fill(map(i, 0, numPoints -1, 64, 255)); // render the point as a circle circle(pointAnimated.x, pointAnimated.y, 9); } }
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>
// total number of points let numPointsX = 24; // circle radius let radius = 50; // circle length (circumference) = 2πR let circleLength; // spacing between each point for the length of the circle let lengthIncrement; // how many radians should each point on a circle increment by let angleIncrement; // cache for points on a the circle let pointsCircle = new Array(numPointsX); // cache for points on a line the lenght of the circle let pointsLine = new Array(numPointsX); // number of points on Z axis let numPointsZ = 24; function setup(){ createCanvas(600, 600, WEBGL); // ensure TWO_PI is defined before assignment circleLength = TWO_PI * radius; lengthIncrement = circleLength / numPointsX; angleIncrement = TWO_PI / numPointsX; // render circles as thick points noFill(); strokeWeight(9); stroke(255); // cache: pre-compute start(circle) and end(line) points for(let i = 0 ; i < numPointsX; i++){ // compute the angle using the increment but also offset by 90 degrees so 1st point is at bottom let angle = (angleIncrement * i) + HALF_PI; pointsCircle[i] = createVector(cos(angle) * radius, sin(angle) * radius); // compute positions on a line, offsetting by half: avoids most self-intersections when animating pointsLine[i] = createVector(lengthIncrement * i - (circleLength * 0.5), 0); } } function draw(){ background(0); orbitControl(); rotateX(HALF_PI); let interpolationAmount = constrain(mouseX, 0, width) / width; // render the grid (circular or rectangular) beginShape(POINTS); for(let j = 0 ; j < numPointsZ; j++){ // offset by half the size to pivot from center let z = (circleLength * 0.5) - (lengthIncrement * j); for(let i = 0 ; i < numPointsX; i++){ // compute the interpolated position let pointAnimated = p5.Vector.lerp(pointsCircle[i], pointsLine[i], interpolationAmount); // render point vertex(pointAnimated.x, pointAnimated.y, z); } } endShape(); }
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>
Update
As previously mentioned, you can work without PVector in this simple case:
// total number of points int numPointsX = 24; // circle radius float radius = 50; // circle length (circumference) = 2πR float circleLength = TWO_PI * radius; // spacing between each point for the length of the circle float lengthIncrement = circleLength / numPointsX; // how many radians should each point on a circle increment by float angleIncrement = TWO_PI / numPointsX; // cache for points on a the circle float[][] pointsCircle = new float[numPointsX][2]; // cache for points on a line the lenght of the circle float[][] pointsLine = new float[numPointsX][2]; // number of points on Z axis int numPointsZ = 24; void setup(){ size(300, 300, P3D); // render circles as thick points noFill(); strokeWeight(9); // cache: pre-compute start(circle) and end(line) points for(int i = 0 ; i < numPointsX; i++){ // compute the angle using the increment but also offset by 90 degrees so 1st point is at bottom float angle = (angleIncrement * i) + HALF_PI; pointsCircle[i] = new float[]{cos(angle) * radius, sin(angle) * radius}; // compute positions on a line, offsetting by half: avoids most self-intersections when animating pointsLine[i] = new float[]{lengthIncrement * i - (circleLength * 0.5), 0}; } } void draw(){ background(0); translate(width * 0.5, height * 0.5, 0); rotateY(map(mouseX, 0, width, -PI, PI)); rotateX(map(mouseY, 0, height, PI, -PI)); float interpolationAmount = (float)mouseX / width; // render the grid (circular or rectangular) beginShape(POINTS); for(int j = 0 ; j < numPointsZ; j++){ // offset by half the size to pivot from center float z = (circleLength * 0.5) - (lengthIncrement * j); for(int i = 0 ; i < numPointsX; i++){ // compute the interpolated position float pointAnimatedX = lerp(pointsCircle[i][0], pointsLine[i][0], interpolationAmount); float pointAnimatedY = lerp(pointsCircle[i][1], pointsLine[i][1], interpolationAmount); // render point stroke(map(i, 0, numPointsX -1, 64, 255)); vertex(pointAnimatedX, pointAnimatedY, z); } } endShape(); }
Personally, I find the PVector version slightly more readable.