0

Drawing an arc
Moderators: adafruit_support_bill, adafruit

Please be positive and constructive with your questions and comments.

Drawing an arc

by drewablo on Thu Feb 13, 2020 12:41 pm

I am not a programmer and not a mathematician. Just a person who knows enough to do a lot of things badly. So if the answer is obvious, forgive me.

I've got a PyPortal Titano that I want to use to display the readings of some environmental sensors. The readings will be displayed via arcs on the screen, like below:

Image

The demo code is working, but there are empty pixels within the arcs. Is this a problem with my code, or a limitation of the code? Is there an easier implementation of what I want to accomplish?

Code: Select all | TOGGLE FULL SIZE
import board
import displayio
import busio
from math import cos
from math import sin
from math import radians
from time import time
from time import sleep
from random import randint

display = board.DISPLAY
splash = displayio.Group(max_size=50)

# Background
BGbitmap = displayio.Bitmap(display.width, display.height, 1)
BGpalette = displayio.Palette(1)
BGpalette[0] = 0x50b0a8
BGsprite = displayio.TileGrid(BGbitmap, x=0, y=0, pixel_shader=BGpalette)
splash.append(BGsprite)

# Palette for gauge bitmap
palette = displayio.Palette(4)
palette[0] = 0x000000
palette[1] = 0x97f3e8 #lightturq
palette[2] = 0x50b0a8 #medturk
palette[3] - 0x408890 #darkturq
palette.make_transparent(0)

# Create gauge bitmap
gaugeBmp = displayio.Bitmap(display.width, display.height, len(palette))
gauge = displayio.TileGrid(gaugeBmp, pixel_shader=palette)
splash.append(gauge)

# show splash group
display.show(splash)

r = 70 #outer gauge radius
w = 20 #width of gauge

def gaugeDraw(newVal, oldVal, r, w, gaugeCenterX, gaugeCenterY, color):
    if newVal == oldVal:
        pass
    elif newVal > oldVal:
        for i in range(oldVal, newVal):
            outerX = round(cos(radians(i)) * r) + gaugeCenterX
            outerY = round(sin(radians(i)) * r) + gaugeCenterY
            gaugeBmp[outerX,outerY] = color
            for q in range(w):
                x = round(cos(radians(i)) * (r-q)) + gaugeCenterX
                y = round(sin(radians(i)) * (r-q)) + gaugeCenterY
                gaugeBmp[x,y] = color
    elif newVal < oldVal: #for when the value goes down, writes black pixels over the color pixels that need removed
        for a in range(oldVal, newVal, -1):
            outerX = round(cos(radians(a)) * r) + gaugeCenterX
            outerY = round(sin(radians(a)) * r) + gaugeCenterY
            gaugeBmp[outerX,outerY] = 3
            for b in range(w):
                x = round(cos(radians(a)) * (r - b)) + gaugeCenterX
                y = round(sin(radians(a)) * (r - b)) + gaugeCenterY
                gaugeBmp[x,y] = 3

firstRowX = 95
firstRowY = 95

rando2 = randint(179, 315)
rando3 = randint(180, 315)
rando4 = randint(189, 315)

for i in range(3):
    x = (firstRowX + (r*2*i))
    gaugeDraw(315, 180, r+4, w+4, x, firstRowY, 3)
   
while True:
    rando1 = randint(179, 315)
    for i in range(3):
        x = (firstRowX + (r*2*i))
        gaugeDraw(rando1, rando2, r, w, x, firstRowY, 1)
        rando2 = rando1
    sleep(1)

drewablo
 
Posts: 18
Joined: Wed Feb 19, 2014 11:44 am

Re: Drawing an arc

by siddacious on Mon Feb 17, 2020 9:57 pm

For not being a programmer, you're doing a great job! I tried it and see the artifacts you're talking about. They look like a Moiré pattern to me.

I don't know for certain, but I'm thinking that what you're seeing is the result of rounding errors
-LATER:
Yes, I'm pretty sure it's rounding errors. I modified your code to run slower and output the values it was setting:
Code: Select all | TOGGLE FULL SIZE
import board
import displayio
import busio
from math import cos
from math import sin
from math import radians
from time import time
from time import sleep
from random import randint

display = board.DISPLAY
splash = displayio.Group(max_size=50)

# Background
BGbitmap = displayio.Bitmap(display.width, display.height, 1)
BGpalette = displayio.Palette(1)
BGpalette[0] = 0x50b0a8
BGsprite = displayio.TileGrid(BGbitmap, x=0, y=0, pixel_shader=BGpalette)
splash.append(BGsprite)

# Palette for gauge bitmap
palette = displayio.Palette(4)
palette[0] = 0x000000
palette[1] = 0x97f3e8 #lightturq
palette[2] = 0x50b0a8 #medturk
palette[3] - 0x408890 #darkturq
palette.make_transparent(0)

# Create gauge bitmap
gaugeBmp = displayio.Bitmap(display.width, display.height, len(palette))
gauge = displayio.TileGrid(gaugeBmp, pixel_shader=palette)
splash.append(gauge)

# show splash group
display.show(splash)

r = 70 #outer gauge radius
w = 20 #width of gauge

def gaugeDraw(newVal, oldVal, r, w, gaugeCenterX, gaugeCenterY, color):
    if newVal == oldVal:
        pass
    elif newVal > oldVal:
        for cnt, i in enumerate(range(oldVal, newVal)):
            if cnt == 3:
                print("sleeping for 20s after 3rd row")
                sleep(20)
           
            print("newval", newVal, "oldVal", oldVal, "r", r, "w", w, "gcX", gaugeCenterX, "gcY", gaugeCenterY, "color", color)
            sleep(2)
            outerX = round(cos(radians(i)) * r) + gaugeCenterX
            outerY = round(sin(radians(i)) * r) + gaugeCenterY
            gaugeBmp[outerX,outerY] = color
            print("Outer: setting gaugeBmp[%d, %d]"%(outerX,outerY))
            for q in range(w):
                x = round(cos(radians(i)) * (r-q)) + gaugeCenterX
                y = round(sin(radians(i)) * (r-q)) + gaugeCenterY
                gaugeBmp[x,y] = color
                print("setting gaugeBmp[%d, %d]"%(x, y))
    elif newVal < oldVal: #for when the value goes down, writes black pixels over the color pixels that need removed
        for a in range(oldVal, newVal, -1):
            outerX = round(cos(radians(a)) * r) + gaugeCenterX
            outerY = round(sin(radians(a)) * r) + gaugeCenterY
            gaugeBmp[outerX,outerY] = 3
            for b in range(w):
                x = round(cos(radians(a)) * (r - b)) + gaugeCenterX
                y = round(sin(radians(a)) * (r - b)) + gaugeCenterY
                gaugeBmp[x,y] = 3

firstRowX = 95
firstRowY = 95

rando2 = randint(179, 315)
rando3 = randint(180, 315)
rando4 = randint(189, 315)

for i in range(3):
    x = (firstRowX + (r*2*i))
    gaugeDraw(315, 180, r+4, w+4, x, firstRowY, 3)
   
while True:
    rando1 = randint(179, 315)
    for i in range(3):
        x = (firstRowX + (r*2*i))
        gaugeDraw(rando1, rando2, r, w, x, firstRowY, 1)
        rando2 = rando1
    sleep(1)

Here's the output from the first three lines within the arc:
Code: Select all | TOGGLE FULL SIZE
newval 315 oldVal 180 r 74 w 24 gcX 95 gcY 95 color 3
Outer: setting gaugeBmp[21, 95]
setting gaugeBmp[21, 95]
setting gaugeBmp[22, 95]
setting gaugeBmp[23, 95]
setting gaugeBmp[24, 95]
setting gaugeBmp[25, 95]
setting gaugeBmp[26, 95]
setting gaugeBmp[27, 95]
setting gaugeBmp[28, 95]
setting gaugeBmp[29, 95]
setting gaugeBmp[30, 95]
setting gaugeBmp[31, 95]
setting gaugeBmp[32, 95]
setting gaugeBmp[33, 95]
setting gaugeBmp[34, 95]
setting gaugeBmp[35, 95]
setting gaugeBmp[36, 95]
setting gaugeBmp[37, 95]
setting gaugeBmp[38, 95]
setting gaugeBmp[39, 95]
setting gaugeBmp[40, 95]
setting gaugeBmp[41, 95]
setting gaugeBmp[42, 95]
setting gaugeBmp[43, 95]
setting gaugeBmp[44, 95]
newval 315 oldVal 180 r 74 w 24 gcX 95 gcY 95 color 3
Outer: setting gaugeBmp[21, 94]
setting gaugeBmp[21, 94]
setting gaugeBmp[22, 94]
setting gaugeBmp[23, 94]
setting gaugeBmp[24, 94]
setting gaugeBmp[25, 94]
setting gaugeBmp[26, 94]
setting gaugeBmp[27, 94]
setting gaugeBmp[28, 94]
setting gaugeBmp[29, 94]
setting gaugeBmp[30, 94]
setting gaugeBmp[31, 94]
setting gaugeBmp[32, 94]
setting gaugeBmp[33, 94]
setting gaugeBmp[34, 94]
setting gaugeBmp[35, 94]
setting gaugeBmp[36, 94]
setting gaugeBmp[37, 94]
setting gaugeBmp[38, 94]
setting gaugeBmp[39, 94]
setting gaugeBmp[40, 94]
setting gaugeBmp[41, 94]
setting gaugeBmp[42, 94]
setting gaugeBmp[43, 94]
setting gaugeBmp[44, 94]
newval 315 oldVal 180 r 74 w 24 gcX 95 gcY 95 color 3
Outer: setting gaugeBmp[21, 92]
setting gaugeBmp[21, 92]
setting gaugeBmp[22, 92]
setting gaugeBmp[23, 92]
setting gaugeBmp[24, 93]
setting gaugeBmp[25, 93]
setting gaugeBmp[26, 93]
setting gaugeBmp[27, 93]
setting gaugeBmp[28, 93]
setting gaugeBmp[29, 93]
setting gaugeBmp[30, 93]
setting gaugeBmp[31, 93]
setting gaugeBmp[32, 93]
setting gaugeBmp[33, 93]
setting gaugeBmp[34, 93]
setting gaugeBmp[35, 93]
setting gaugeBmp[36, 93]
setting gaugeBmp[37, 93]
setting gaugeBmp[38, 93]
setting gaugeBmp[39, 93]
setting gaugeBmp[40, 93]
setting gaugeBmp[41, 93]
setting gaugeBmp[42, 93]
setting gaugeBmp[43, 93]
setting gaugeBmp[44, 93]

You can see that on the third chunk of data there is a discontinuity in the y values where it jumps from 92 up to 93. I'm guessing the first three pixels were getting rounded down to 92.

I can't tell you off the top of my head how to do it, but I'd suggest trying to adapt the code to draw contiguous lines at the same y value rather than calculating it per pixel. I think it might be faster as well.

I'll bring this to the attention of my more graphics savvy coworkers but hopefully this will get you started.

You may also want to see if you can make use of the Display Shapes library:
https://github.com/adafruit/Adafruit_Ci ... lay_Shapes

Once you fix this bug I think this could be a really neat addition to that library.

siddacious
 
Posts: 202
Joined: Fri Apr 21, 2017 3:09 pm

Re: Drawing an arc

by MakerMelissa on Mon Feb 17, 2020 10:49 pm

You may want to check out the Adafruit_CircuitPython_Display_Shapes library. The Circle is based on the RoundRect and basically just uses the 4 corners. However, it might give you an example to go by: https://github.com/adafruit/Adafruit_Ci ... undrect.py

An arc would be a great shape to add to the library so that use cases like this are easier.

MakerMelissa
 
Posts: 94
Joined: Wed Jun 05, 2013 2:10 am

Re: Drawing an arc

by drewablo on Wed Feb 19, 2020 5:10 pm

Thanks all! I was thinking about drawing lines instead of the per pixel, too. While I work on that, here's my temporary fix, which is really terrible in terms of efficiency. I basically draw way more pixels that what's needed to make sure I cover everything. Rounding errors be damned! It doesn't delay response time TOO bad, but still, not great.

Code: Select all | TOGGLE FULL SIZE
import board
import displayio
import busio
from math import cos
from math import sin
from math import radians
from time import time
from time import sleep
from random import randint

display = board.DISPLAY
splash = displayio.Group(max_size=50)

# Background
BGbitmap = displayio.Bitmap(display.width, display.height, 1)
BGpalette = displayio.Palette(1)
BGpalette[0] = 0x50b0a8
BGsprite = displayio.TileGrid(BGbitmap, x=0, y=0, pixel_shader=BGpalette)
splash.append(BGsprite)

# Palette for gauge bitmap
palette = displayio.Palette(4)
palette[0] = 0x000000
palette[1] = 0x97f3e8 #lightturq
palette[2] = 0x50b0a8 #medturk
palette[3] - 0x408890 #darkturq
palette.make_transparent(0)

# Create gauge bitmap
gaugeBmp = displayio.Bitmap(display.width, display.height, len(palette))
gauge = displayio.TileGrid(gaugeBmp, pixel_shader=palette)
splash.append(gauge)

# show splash group
display.show(splash)

r = 70 #outer gauge radius
w = 20 #width of gauge

def gaugeDraw(newVal, oldVal, r, w, gaugeCenterX, gaugeCenterY, color):
    if newVal == oldVal:
        pass
    elif newVal > oldVal:
        for cnt, i in enumerate(range(oldVal, newVal)):
            if cnt == 3:
                print("sleeping for 20s after 3rd row")
                sleep(20)
           
            print("newval", newVal, "oldVal", oldVal, "r", r, "w", w, "gcX", gaugeCenterX, "gcY", gaugeCenterY, "color", color)
            sleep(2)
            outerX = round(cos(radians(i * coveragePercent)) * r) + gaugeCenterX
            outerY = round(sin(radians(i * coveragePercent)) * r) + gaugeCenterY
            gaugeBmp[outerX,outerY] = color
            print("Outer: setting gaugeBmp[%d, %d]"%(outerX,outerY))
            for q in range(w):
                x = round(cos(radians(i * coveragePercent)) * (r-q)) + gaugeCenterX
                y = round(sin(radians(i * coveragePercent)) * (r-q)) + gaugeCenterY
                gaugeBmp[x,y] = color
                print("setting gaugeBmp[%d, %d]"%(x, y))
    elif newVal < oldVal: #for when the value goes down, writes black pixels over the color pixels that need removed
        for a in range(oldVal, newVal, -1):
            outerX = round(cos(radians(a * coveragePercent)) * r) + gaugeCenterX
            outerY = round(sin(radians(a * coveragePercent)) * r) + gaugeCenterY
            gaugeBmp[outerX,outerY] = 3
            for b in range(w):
                x = round(cos(radians(a * coveragePercent)) * (r - b)) + gaugeCenterX
                y = round(sin(radians(a * coveragePercent)) * (r - b)) + gaugeCenterY
                gaugeBmp[x,y] = 3

firstRowX = 95
firstRowY = 95
coveragePercent = .25
rando2 = randint(179, 315)
rando3 = randint(180, 315)
rando4 = randint(189, 315)

for i in range(3):
    x = (firstRowX + (r*2*i))
    gaugeDraw(315, 180, r+4, w+4, x, firstRowY, 3)
   
while True:
    rando1 = randint(179, 315)
    for i in range(3):
        x = (firstRowX + (r*2*i))
        gaugeDraw(rando1, rando2, r, w, x, firstRowY, 1)
        rando2 = rando1
    sleep(1)

Here's what I've got in terms of drawing by line. It's a proof of concept, and can only draw 1/4 of a full 360 degree arc right now. Very much a work in progress.

Code: Select all | TOGGLE FULL SIZE
from math import *
from graphics import *
from time import *

win = GraphWin("TITANO",480,320)
r = 70
w = 50
       
yCenter = 200
xCenter = 300
maxAngle = 260
minAngle = 180
arcWidth = r - w
while True:
   outerYmax = round(sin(radians(maxAngle)) * r) + yCenter
   outerYmin = round(sin(radians(minAngle)) * r) + yCenter
   outerXmax = round(cos(radians(maxAngle)) * r ) + xCenter
   innerYmax = round(sin(radians(maxAngle)) * w) + yCenter
   innerYmax = round(sin(radians(maxAngle)) * w) + yCenter
   innerXmax = round(cos(radians(maxAngle)) * w ) + xCenter
   print("outerYmax: " + str(outerYmax))
   print("innerYmax: " + str(innerYmax))
   print("outerYmin: " + str(outerYmin))
         
   if maxAngle <= 270:
      for y in range(outerYmin, outerYmax, -1):
         print("Y: " + str(y))
         if y > innerYmax :
            theta1 = asin((yCenter - y)/r)
            theta2 = asin((yCenter - y)/w)
            x1 = round(cos((theta1)) * r) + xCenter
            x2 = round(cos((theta2)) * w) + xCenter
            for x in range(x2, x1):
               pt = Point(x,y)
               pt.draw(win)
            print("x1: " + str(x1))
            print("x2: " + str(x2))
         elif y <= innerYmax and maxAngle >= 270:
            theta1a = asin((yCenter - y)/r)
            x1a = round(cos((theta1a)) * r) + xCenter
            for xa in range(x1a, xCenter, -1):
               pt = Point(xa, y)
               pt.draw(win)
         elif innerYmax - outerYmax <= arcWidth: #this gives it a angle on the end of the arc instead of a horizontal line
            w += 1
            theta1 = asin((yCenter - y)/r)
            theta2 = asin((yCenter - y)/w)
            print("W: " + str(w))
            x1 = round(cos((theta1)) * r) + xCenter
            x2 = round(cos((theta2)) * w) + xCenter
            for x in range(x2, x1):
               pt = Point(x,y)
               pt.draw(win)
            print("x1: " + str(x1))
            print("x2: " + str(x2))
   sleep(5)
   break

drewablo
 
Posts: 18
Joined: Wed Feb 19, 2014 11:44 am

Please be positive and constructive with your questions and comments.