Drawing an arc

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:

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 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))
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

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 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))
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:

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

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

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 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))
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