Creating a circle from smaller circles on a TFT- arrrgh the math! The math!

Ok, so this is quite possibly a bit too much. But I really want to create a good looking UI for my latest project: a measurement tool for pH and temperatur for a hobbyist laboratorium.

I got everything practical to work beautifully, and I''ve started on the UI design.

I'm using an Arduino Due connected via a shield to a SD1963 7" TFT display, running with the UTFT libraries.
I want to make a nice looking meter made out of small circles forming a larger circle, which is easy enough in my design programs (see attached image). I want these circles to gradually become lighter towards the red part as the temperature rises. That means they have to be drawn individually.

I can figure out how to do the gradual drawing and the colour change, but I can't for the life of me figure out how to do the math to get the circles to form the outline of the largercircle. I'm not good enough at math.
The UTFT library allows me to draw circles with the function

myGLCD.fillCircle(*x starting point, y starting point, radius*)

So I basically just need to draw these 52 little circles with the right x and y offset to get them to form a circle.
Does anyone have any suggestions on how to calculate that?

Rectangles ? I don't think so !

I suggest that you change the title of this topic as it does not describe what you are trying to do

Polar coordinates.

R = constant.
Theta = n * 2 * pi / nDots
X = R * cos(Theta)
Y = R * sin(Theta)

Edit: fixed sin vs cos. oops.

Yes, sorry. I started with rectangles, but then realised the UTFT lib don't include rotation of shapes. So I moved on to circles mid post. But forgot to change the title.

void OLED() {
  float theta;
  uint8_t x;
  uint8_t y;
  static uint8_t lasttx;
  static uint8_t lastty;
  static uint8_t lastwdx;
  static uint8_t lastwdy;
  const uint8_t radiust = 60;
  const uint8_t radiuswd = 50;
  const uint8_t centre = 64;
  int8_t secondAdjusted = (second(t_now) - 15) % 60;
  char timestring[12];
  
  theta = (secondAdjusted * pi) / 30;
  x = centre + (uint8_t)radiust * (cos(theta));
  y = centre + (uint8_t)radiust * (sin(theta));
  
  tft.fillCircle(lasttx, lastty, 3, BLACK);
  tft.drawCircle(centre, centre, radiust, 0x528A);
  tft.fillCircle(x, y, 3, GREEN);
  lasttx = x;
  lastty = y;

  float windAngleAdjusted = windAngle - (pi / 2);
  if (windAngleAdjusted < 0) {
    windAngleAdjusted = windAngleAdjusted + (2 * pi);
  }
  
  x = centre + (uint8_t)radiuswd * (cos(windAngleAdjusted));
  y = centre + (uint8_t)radiuswd * (sin(windAngleAdjusted));
  
  if (lastwdx != x || lastwdy != y) {
    tft.fillCircle(lastwdx, lastwdy, 3, BLACK);
    tft.drawCircle(centre, centre, radiuswd, 0x528A);
    tft.fillCircle(x, y, 3, YELLOW);
    lastwdx = x;
    lastwdy = y;
  }
  
  tft.setTextColor(GREEN, BLACK); 
  tft.setTextSize(1);
  sprintf(timestring, "%02d:%02d:%02d", hour(t_now), minute(t_now), second(t_now));
  tft.setCursor(41, 32);
  tft.print(timestring);

  if (windSpeedMPH < 100){
    if (windSpeedMPH < 10){
      sprintf(timestring, "0%3.2f", windSpeedMPH);
    } else {
      sprintf(timestring, "%4.2f", windSpeedMPH);
    }
    tft.setTextSize(2);
    tft.setCursor(35, 48);
    tft.setTextColor(YELLOW, BLACK);  
    tft.print(timestring);
  }
  if (windSpeedMPHMax < 100){
    if (windSpeedMPHMax < 10){
      sprintf(timestring, "0%3.2f", windSpeedMPHMax);
    } else {
      sprintf(timestring, "%4.2f", windSpeedMPHMax);
    }
    
    tft.setTextSize(2);
    tft.setCursor(35, 64);
    tft.setTextColor(ORANGE, BLACK);  
    tft.print(timestring);
  }
}

The above was me experimenting with a 128*128 OLED display. The code draws 2 circles, one to show seconds from a clock and the other to show the angle of the wind from wind vane. While not the same as what you want it is the same principle. Can you make sense of it or would you like me to explain it?

1 Like

This looks great. I have no idea how it works out, though, sucking at trig. No, by the way, I don't know enough to even suck.
"n" I guess is the radius of the circle I want to create, right? And "R" is a constant, but is it a number that I have to add?

I'd LOVE for you to walk me through it if you don't mind? Mind you, I have the trig understanding of a 12-year-old.
It would probably help me a lot if I could see a picture of the output though.

n is the dot 0 - first, 1 - second…until nDots. R is radius

Now I have to remember what I did!
Most of it you don't need.

So, for the seconds count:

Centre of the display:
const uint8_t centre = 64;

Radius of the circle for the seconds count:
const uint8_t radiust = 60;

The orientation of the display was wrong, this adjusts the seconds to make 0 at the top.
int8_t secondAdjusted = (second(t_now) - 15) % 60;
To use this code to test all you really need is a counter from 0 to 59 and back to 0.

theta = (secondAdjusted * pi) / 30;
Gives the angle in radians (all trig functions are in radian in C/C++) for where the circle is to be drawn. 30 is what is referred to as a 'magic number' which is bad practice. I dropped 30 into the calculation to make it work without explanation. Remember there are 2 pi radians in a circle. It 0 is at the top, 30 at the bottom, consider 30 seconds, which is at the bottom. Half way round a circle is 1 pi radian. So 30 seconds * pi divided by magic number has to equal pi. As pi was multiplied by 30 the magic number must be 30 to get back to 1 pi.

You want to draw a circle some fixed distance from the centre at angle theta, so you need x and y co-ordinates offset by the distance to the centre (64) and calculated from theta.
These are the co-ordinates, I'll let you work out how the calculation works.

  x = centre + (uint8_t)radiust * (cos(theta));
  y = centre + (uint8_t)radiust * (sin(theta));

Before drawing the new circle the old one has to be erased, which is done by redrawing it in the background colour:
tft.fillCircle(lasttx, lastty, 3, BLACK);

I had a pitch circle over which the moving dot for seconds was drawn, this now has a gap in it so needs re-drawing:
tft.drawCircle(centre, centre, radiust, 0x528A);

Then draw the dot that indicates the seconds:
tft.fillCircle(x, y, 3, GREEN);
3 is another magic number, it is the size of the dot indicating the seconds.

Finally save the value of x and y for next time:

  lasttx = x;
  lastty = y;

As this was displaying the time and has to be updated every second that function was called, unsurprisingly, every second.

Be aware that for some libraries and displays you have to explicitly send the data to the display, I thought that was needed for this one but I can't find the code for that, so I guess it doesn't (or I just can't see the code!)

The other stuff in there is for the wind and the time, you can ignore it.

1 Like

Thanks! I did almost get there by just breaking down the code and removing all the unnecessary stuff. But the secondAdjusted thing gave me a slight case of brain cancer. Anyhow, seems to me what I'm really looking for is this part:

  x = centre + (uint8_t)radiust * (cos(theta));
  y = centre + (uint8_t)radiust * (sin(theta));

which holds most of the math I couldn't produce myself, having something like 20 years between now and any practical use or understanding of cos, sin and theta.

The theta definition is however somewhat blurry to me still, probably in part because of the secondAdjusted- thing which still is unclear to me. If this part

theta = (secondAdjusted * pi) / 30;

did not use that correction, what would secondAdjusted be?

I will be drawing my dots based on pH and temperature values mapped to a suitable integer, There will be a set end value (11 in the picture I posted), and the number of small circles that are lit up will depend on how many percent of that value that has been reached by the temp/pH reading (if that is understandable). In this scenario, what would "secondAdjusted" be? Number of seconds left in a minute?

The seconds adjust thing just rotates the whole thing so 0 is at the top. Without it 0 was (I think) where 15 should be. I could have left it out for the example but leaving it in gives you an idea how to achieve rotation of the display if you need to. Also, that code was just copied directly from my experimental code and I didn't want to leave anything out.

Without the adjust bit it's just seconds (0 to 59)

I suggest you first get my code working on your display, then play around with it to see the effect of changing things. Put some serial prints in to see what is going on.

1 Like

Maybe this will help?

2 Likes

Yes, thanks! Perfect. Love this forum! Thank you all!

1 Like

Almost perfect. But in @westfw 's example diagram, the sin() & cos() functions take a parameter in degrees. The Arduino sin() & cos() functions take a parameter in radians. To convert degrees to radians, multiply by PI/180.0. Also, in @westfw 's diagram, the origin (X=0, Y=0) is in the centre of the diagram and the Y values increase towards the top of the diagram. On every LCD/OLED screen I've ever come across, the origin is at the top left of the screen and the Y values increase towards the bottom of the screen.

Yes, now that you point it out. But I wasn’t using the image as a reference for actual code, just a visual reference to understand the concepts of the code. And for that, it was perfect.

I’ve already constructed the UI based on the above tips. It was fairly easy to decipher the code once I started playing around with the variables. There’s a little tweaking left, but I’ll be uploading an image as soon as it’s done.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.