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?
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":
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)PVector
class which is both handy to encapsulate 2D/3D point properties (.x, .y, .z
), but also provides a lerp()
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.