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 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 PVectors 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.