Use of for-loop to create a stair of ellipses

Below is code for a variant of the project that uses a different approach. It is written for the Processing Python Mode .

Note that the code does not contain any for loops. Instead, it uses recursion, whereby a function calls itself. The concept behind this approach is that we can build a large object from smaller versions of itself. For example, to build a staircase of n steps, we can build a large step, then build a smaller staircase of n - 1 steps on top of that step. The smallest staircase is one that consists of only one step.

Here’s a picture of the result:

The code is below. See the comments for explanations. As a challenge, try to draw the above by translating the code into p5.js.

Also see Wikipedia: Recursion.

def setup():
  size(440, 440)
  noLoop()
  background(249)
  fill(0, 0, 0, 48)
  
def draw():
  # Draw a staircase with 16 stairsteps composed of
  # round building units with a diameter of 40.
  diam = 40
  dimension = 10
  draw_staircase(diam, height - diam, diam, dimension)

# Three recursive functions follow.
# A recursive function is one that calls itself.
# Each recursion consists of ...
#   a recursive case wherein the function calls itself.
#   a base case wherein the function does not call itself.

# To avoid an infinite recursion, the recursive calls must ultimately resolve to the base case.

# Within the three recursions here ...
#   The recursive cases build features from smaller versions of themselves.
#   The base cases draw the smallest versions of the features.

def draw_building_unit(x, y, diam):
  if diam > 5:
    # Recursive case: 
    # First, draw a smaller round building unit.
    draw_building_unit(x - diam // 16, y - diam // 16, diam * 3 // 4)
    # Then enclose it in a full-size ellipse.
    ellipse(x, y, diam, diam)
  else:
    # Base case: simply draw a small ellipse
    ellipse(x, y, diam, diam)

def draw_step(x, y, diam, dimension):
  # First, draw a round building unit.
  draw_building_unit(x, y, diam)
  # If <dimension> > 1, extend the step toward the right 
  # by a smaller step with <dimension - 1> round building units.
  if (dimension > 1):
    draw_step(x + diam, y, diam, dimension - 1)
    
def draw_staircase(x, y, diam, dimension):
  # First, draw a step of <dimension> circles.
  draw_step(x, y, diam, dimension)
  # If <dimension> > 1, draw a smaller staircase
  # of <dimension - 1> steps on top of it.
  if dimension > 1:
    draw_staircase(x, y - diam, diam, dimension - 1) # recursive case
  else:
    pass # Base case; do nothing
2 Likes

So cool !
I try it on P5.js

How should I made this with only nested loop
Untitled

I can just make this

1 Like

It is better to use x,y in this case instead of i,j

What do you think why this is?

You can rename by right click on the j for example and choose Rename from the local menu. I use it all the time.

What to do

You are almost there. Where do you think is the problem? Which line? What values do you want to generate?

How can you modify the line?

2 Likes

Hello, @yoyo, and welcome to Processing Foundation Discourse!

It is best to post code as text, rather than as an image capture. Code, as text, is easy for others to copy and paste, so that they can test and experiment with it.

Please also format code when you post it, so that it is easy for others to read. For information on all of this, see Guidelines—Asking Questions.

Following is your code as text, formatted for posting, and with the names the variables i and j changed to x and y, respectively:

function setup() {
  createCanvas(400, 400);
  background(0);
  for (let x = 0; x <= 20; x++) {
    for (let y = 0; y <= x; y++) {
      circle((x + 0.5) * 20, (y + 0.5) * 20, 20);
    }
  }
}

function draw() {
}

Examining the code, including the ranges of the values of the loop variables and the size of the canvas, do you think some circles are being drawn that are outside the canvas? How can you find out?

To figure out how to draw the staircase as you would like it, consider how the values of x in the outer loop should be used to control the range of values of y in the inner loop. Consider what values of y are needed when x is 5 or 15, for example. How can you modify the code to make that happen? Think about the inner loop.

1 Like

Hello. As advised, try to understand what you need to do and what you are doing…
To help you here, the same code added some debug info. Run and look to the canvas and the console output.

function setup() {
  createCanvas(400, 400);
  background(0);
  let number = 0
  const diameter = 20;
  const wanted_x = 0 + diameter / 2;
  const wanted_y = height - diameter / 2;
  fill("red");
  ellipse(wanted_x, wanted_y, diameter);
  logDebug1(wanted_x,wanted_y);
  noFill();
  stroke(255);
  textSize(8);
  for (let x = 0; x <= 20; x++) {
    for (let y = 0; y <= x; y++) {
      logDebug2(x, y);
      const xp = (x + 0.5) * 20;
      const xy = (y + 0.5) * 20
      circle(xp, xy, 20);
      
      text(number, xp - 5, xy +5);
      number++;
    }
  }
}

function logDebug1(wanted_x,wanted_y) {
  console.log(`my ideal first circle is red and draw, (by hard code) at:
xpos = ${wanted_x}
ypos = ${wanted_y} `);
}

function logDebug2(x, y) {
  if (x === 0) {
    console.log(`my first cricle is draw at:
xpos = ${(x + 0.5) * 20}
ypos = ${(y + 0.5) * 20}\n`);
  } else if (x === 20) {
    console.log(`my last circle was draw at:
xpos = ${(x + 0.5) * 20}
ypos = ${(y + 0.5) * 20}
my window is 400x400...`);
  } else {
    console.log(
      `Those are al positions I'm drawing with;
when x = ${x} and y = ${y}
xpos = ${(x + 0.5) * 20}
ypos = ${(y + 0.5) * 20}\n`
    );
  }
}

1 Like

Hello again, @yoyo.

We hope you are making progress with this project. Please let us know how you are doing with it.

If you run @vkbr’s code, which provides information for debugging, observe the output in the console, especially the final output. For circles to appear on the canvas, their coordinates must be within the proper boundaries. Note the size of your canvas and, in the output, the locations of the circles that are being drawn.

1 Like

Hmm, how about a solution with only one for-loop?
The downside is it does some unnesesary stuff…

// side defines the width of an ellipse
for (let i=0; i<side*side; i++) {
    // check if ellipse shoud be drawn
    if (floor(i/side) < (i % side)) {
        // position of ellipse 
        ellipse(floor(i/side) * side, (i % side) * side, side, side
    }
  }
1 Like

A solution with one loop is a great idea. For whatever the reason is, @yoyo would like to do it with a nested loop, as stated here:

@philipplehmann, this line needs to be edited for it to work:

        ellipse(floor(i/side) * side, (i % side) * side, side, 10side

@yoyo, if some of the circles are outside the bounds of the canvas, you can correct that by changing the upper limit of the variable in the outer loop, which you have as 20. Perhaps it needs to be just a little bit smaller.

Then you can achieve the correct configuration of the staircase by revising the header of the inner loop. Think about what values of the variable are needed in the inner loop header with relation to each value of the other variable in the outer loop header.

1 Like

cool! I am trying this

I have tried the different combinations on x and y. but it’s just made this stair of ellipses and the stair that I made before with different starting points. How should I make the up stair from left to right?

Sorry guys, I am temporarily limited to 3 replies on the same topic. For adding another reply, I may need to edit my previous replies. So I can’t have a new reply. I’ll edit this reply. BTW thx a lot!!!

@yoyo,

Let’s focus on the two loop headers from your original code.

Following are those two headers, with i changed to x and j changed to y:

  for (let x = 0; x <= 20; x++) {
    for (let y = 0; y <= x; y++) {

If you run the debugging code supplied by @vkbr, and look at the JavaScript console, you will find that your code produces 21 steps, even though your original image showed that you needed only 20. The extra step is not visible in the sketch, because it is being drawn outside your canvas. The problem is that your outer loop iterates 21 times instead of 20. There are 21 numbers in the range 0 to 20, inclusive. That 20 in the outer loop header needs to be 19. Alternatively, you could change the condition to exclude 20.

Each iteration of the outer loop is responsible for one column of circles. The first column needs to have 1 circle, and they must increase by 1 in number in each column that follows. This must occur while the value of x is increasing. We need to make sure to attach the steps to the bottom, instead of to the top of the canvas. If you subtract x from 19 and assign the result to y, and specify that y must be less than or equal to 19, what would happen with the inner loop as x increases by 1 during each iteration of the outer loop?

Hello @yoyo,

Hint:

function setup()
  {
  createCanvas(400, 400);
  textSize(16);
  
  for(let num = 0; num<=9; num++)
    {
    fill(0);
    text(num, num*30, 30);    // Left to right
    text(9-num, num*30, 60)
    text(num, (9-num)*30, 90) // Reverse - Right to left
    }
  }

Tutorial:
4.2: Nested Loops - p5.js Tutorial

:)

1 Like

There’s different ways to do it… In my solution, one for loop goes up and the other goes down…
like: x+=d y-=d`


But I also find nested loops confuse. When I’m lost with them, I usually reduce the number of iterations to a really small one, like 3 or 5 and use text to log the values so its clearer what is happening.
cheers

For thinking about nested loops, it might help to label the circles by column and row.

EDIT (September 29, 2022):

Within each pair of numbers in all of the circles in the sketch, the first number represents the value of the control variable of the outer loop and the second represents the value of the control variable of the inner loop. These are the column and row numbers.

Take a look at the pairs of numbers in the circles in the diagonal. Is there a consistency regarding the sum of the two numbers in the pairs?

Also note the sums of the pairs of numbers below the diagonal. For each one, how does it relate to the sum of the two numbers in the circle in the diagonal above?

Do these observations suggest anything regarding what would be a good header for the inner loop? There are several possibilities that would work.

2 Likes

omg!! I finally get it

3 Likes

thank you guys

4 Likes

Good work, @yoyo!

Also, thanks go to @Chrisir, @vkbr, @glv, and @philipplehmann.

Since you’ve succeeded with a solution, we can now reveal some additional code of our own that applies to the project.

As mentioned earlier, there are multiple ways to achieve this task. For example …

Now, shifting our focus to the diagram here, which displays the column and row coordinate pairs, we can note that on the diagonal, the column and row numbers of each circle add up to 19. Below the diagonal, where the canvas is filled with circles, the two numbers in the coordinate pairs add up to a number greater than 19. In the entire space above the diagonal, where there are no circles, the numbers in the coordinate pairs would add up to a sum less than 19. From those observations, we can derive something like this regarding the nested loops:

  // column numbers from left to right
  for (let col = 0; col <= 19; col++) {
    // row numbers from the bottom up to and including the diagonal
    // for (let row = 19; row >= 19 - col; row--) {
    for (let row = 19; row + col >= 19; row--) {
      circle((col + 0.5) * 20, (row + 0.5) * 20, 20);
    }
  }

The code above could be refactored slightly to be equivalent to solutions that others have proposed.