Forum Archive

[Share] a list of rects distributed around 360 degrees

Phuket2

This share is basically for people like me , when they hear circle they think of crop circles not pi. My point being, if you know the math, this share is useless to you.
But it's just about the distribution of rects on a circular path. Personally I think it would be nice if someone could rewrite the function properly and add it to the Pythonista Tools lib.
But I was able to put this together by taking parts of the AnalogClock.py example that comes with Pythonista.

'''
    Pythonista Forum - @Phuket2
'''
import ui, editor
from math import pi, sin, cos

def rects_on_circle_path(rect_path, obj_width,
                            margin = 2, num_objs = 12):
    '''
    rects_on_circle_path
    PARAMS:
        1. rect_path = the bounding rect of the circle 
        **Note the rect is inseted half of the shape_width param + the
        margin param. the resulting rects are centered on the bounding
        circle. 

        2. obj_width = the width of the shape/rect you are placing on the
        path. 

        3. margin = 2 , additionally insets the rect_path by this value

        4. num_objects = 12.  the number of objects to distribute around
        rect_path. set to 12 as default, a clock face.  odd and even 
        numbers are ok.

    RETURNS:
        tuple(Rect, list)
        1. Rect = the adjusted rect_path after transformations in the func.
        2. a list[] containing a ui.Rect's. the length of the list is
        equal to the num_objs param. 

    NOTES:
        For some reason i can't do the math if my life depended on it.
        I copied the math from the AnalogClock.py pythonista example.  

        ALSO should have a param to shift the basline of the rects, off
        the center line of the rect_path. 

        the reason why i return a list of rects in the tuple is for 
        flexibility.  in the example, just drawing. but could just as 
        easily be positioning ui.Button/ui.Label object or whatever.  

        oh, btw i know its a bit of a mess. hard when you are not sure 
        of the math to get it as concise as it should be. 

    '''

    rects = []

    r = ui.Rect(*rect_path).inset((obj_width/2) + margin, (obj_width/2) + margin)

    radius = r.width / 2
    for i in range(0, num_objs):
        a = 2 * pi * (i+1)/num_objs
        pos = (sin(a)*(radius*1), cos(a)*(radius*1))
        r1 = ui.Rect(pos[0] , pos[1] , obj_width, obj_width)
        r1.x += ((r.width/2) - (obj_width/2)+r.x)
        r1.y += ((r.height/2) - (obj_width/2)+r.y)
        rects.append(r1)

    return (r,rects)



class MyClass(ui.View):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def draw(self):
        r = ui.Rect(*self.bounds)
        r, rects = rects_on_circle_path(r, 10, margin = 20 ,
                                            num_objs = 36 )
        s = ui.Path.oval(*r)
        ui.set_color('lime')
        s.stroke()

        ui.set_color('orange')
        for r in rects:
            s = ui.Path.oval(*r)
            s.fill()

        r = ui.Rect(*self.bounds)
        r, rects = rects_on_circle_path(r, 15, margin = 40 ,
                                            num_objs = 12 )
        s = ui.Path.oval(*r)
        ui.set_color('yellow')
        s.stroke()

        ui.set_color('purple')
        for r in rects:
            s = ui.Path.oval(*r)
            s.fill()

        r = ui.Rect(*self.bounds)
        r, rects = rects_on_circle_path(r, 25, margin = 80 ,
                                            num_objs = 6 )
        s = ui.Path.oval(*r)
        ui.set_color('orange')
        s.stroke()

        ui.set_color('lime')
        for r in rects:
            s = ui.Path.rect(*r)
            s.fill()        

if __name__ == '__main__':
    _use_theme = True
    w, h = 600, 600
    f = (0, 0, w, h)
    name = 'Silly Demo'
    mc = MyClass(frame=f, bg_color='white', name = name)

    if not _use_theme:
        mc.present('sheet', animated=False)
    else:
        editor.present_themed(mc, theme_name='Oceanic', style='sheet', animated=False)

Output

Phuket2

Well this is another Epic Fail... I assumed it was working as I expected it to. But it does not. The rects are not in the position going around clockwise. Should be easy to fix, with math.radiants or math.degrees or combo of both., but I can't get it. In the below I added :
a += radians(-210) Jan is first as it should be, but the rects are coming back in counter clockwise order. I know it should be so easy to reverse it, but I am lost. And even if I get it, I am just guessing.
Any help appreciated 😂

def rects_on_circle_path(rect_path, obj_width,
                            margin = 2, num_objs = 12):
    rects = []

    r = ui.Rect(*rect_path).inset((obj_width/2) + margin, (obj_width/2) + margin)

    radius = r.width / 2
    for i in range(0, num_objs):
        a = 2 * pi * (i+1)/num_objs
        a += radians(-210) # <---- changed this

        pos = (sin(a)*(radius*1), cos(a)*(radius*1))
        r1 = ui.Rect(pos[0] , pos[1] , obj_width, obj_width)
        r1.x += ((r.width/2) - (obj_width/2)+r.x)
        r1.y += ((r.height/2) - (obj_width/2)+r.y)
        rects.append(r1)

    return (r,rects)

The below pic is just 12 ui.Buttons with the frame set to rects returned from the function above. I have checked the indices to make sure the ui.Button names are correct, they are.

cvp
a = -2 * pi * (i+1)/num_objs # inverse
a += radians(-150) # change delta
cvp
# Normally, in Algebra, we compute point coordinates with
#  x = r*cos(a)
#  y = r*sin(a)
#  where a is the angle versus the horizontal axe
#  positive reverse clockwise (sorry)
#  thus a = 0 for 3 hour
#  if you want index 0 at 12 hour, you need to turn 90° left, thus -90°
# thus the best solution is
a = 2 * pi * i/num_objs - pi/2
pos = (cos(a)*(radius*1), sin(a)*(radius*1)) # careful: cos,sin! not sin,cos
Phuket2

@cvp , thanks. It works! I will not ask why, but again thank you.

'''
    Pythonista Forum - @Phuket2
'''
import ui, editor
from math import pi, sin, cos, radians, degrees
import calendar

# example, playing around, for 12 items its ok no math :)
_range_12 = [.3, .34, .38, .42, .46 , .5, .55, .6, .63, .7, .85, 1.0]

def rects_on_circle_path(rect_path, obj_width,
                            margin = 2, num_objs = 12):

    rects = []

    r = ui.Rect(*rect_path).inset((obj_width/2) + margin, (obj_width/2) + margin)

    radius = r.width / 2
    for i in range(0, num_objs):
        #a = 2 * pi * (i+1)/num_objs
        #a += radians(-210)
        # thanks @cvp, now the rects start at 0 degrees, yeah!! 
        a = -2 * pi * (i+1)/num_objs # inverse
        a += radians(-150) # change delta

        pos = (sin(a)*(radius*1), cos(a)*(radius*1))
        r1 = ui.Rect(pos[0] , pos[1] , obj_width, obj_width)
        r1.x += ((r.width/2) - (obj_width/2)+r.x)
        r1.y += ((r.height/2) - (obj_width/2)+r.y)
        rects.append(r1)

    return (r,rects)

class MyClass(ui.View):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.btns = []
        self.make_view()

    def make_view(self):
        for i in range(0,12):
            btn = ui.Button()
            btn.title = calendar.month_abbr[i+1]
            btn.bg_color = 'orange'
            btn.tint_color = 'black'
            btn.border_width = .5
            self.add_subview(btn)
            self.btns.append(btn)

    def layout(self):
        r = ui.Rect(*self.bounds)
        obj_width = 80
        r, rects = rects_on_circle_path(r, obj_width, margin = 20 ,
                                            num_objs = 12)
        ui.set_color('orange')
        for i, btn in enumerate(self.btns):
            btn.frame = rects[i]
            btn.corner_radius = btn.width / 2
            btn.text_color = 'black'
            btn.alpha = _range_12[i]

if __name__ == '__main__':
    _use_theme = False
    w, h = 600, 600
    f = (0, 0, w, h)
    name = 'Silly Demo'
    mc = MyClass(frame=f, bg_color='white', name = name)

    if not _use_theme:
        mc.present('sheet', animated=False)
    else:
        editor.present_themed(mc, theme_name='Oceanic', style='sheet', animated=False)

JonB

Look, don't fret about radians/degres. Work all in degrees if it helps., and just convert to radians before using sin or cos.

Here are a few easy tricks to help remember:
sin(0)==0. cos(0)==1
sin(radians(90)) ==1 cos(radians(90)) == 0

In a traditional coordinate system(or scene coordinate system), with positive x pointing right, and positive y pointing up, x,y = sin(a), cos(a) would travel counter clockwise, starting at 3 oclock. In the ui coordinate system, this would also start at 3 oclock, but go clockwise.

If you want equal spacing going clockwise, starting at midnight:
y=-cos(a) # note negative sign
x=sin(a)
which you can check using the two sets of relations above.
This would be the same as
x=sin(a-radians(90) )
y=cos(a-radians(90))
if you prefer to keep a sort of standard form with simply a starting angle offset.

you might like something like
for i in range(0,num_obj):
a_deg=360/num_obj*i # divide circle into N segments, starting at 0
x,y=-cos(radians(a_deg)), sin(radians(a_deg))

The clock example labeled numbers from 1 to 12, rather than 0 to 11, so they needed to start one segment in, and also was probably in the scene coordinate system which is different than

Phuket2

@JonB , thanks. Was not really going for an effect. I just really wanted to experiment with choosing a month a different way in the ui. Then I know I can't just put things on a arc on a circular path at will (meaning my ability). So I thought, look at the AnalogClock.py example from @omz and make a function for myself out of it. That's how it started. My brain is not fresh enough now to digest your comments. I will look again tomorrow and see if I can get my head around it.
I know picking a month like this looks stupid. I just wanted to try it. I thought maybe something might standout. But as usual, I got caught up in the details. But I still think that small function tweaked and written properly could be useful a lot of people, well maybe just me 😱
But again thanks @JonB and @cvp. Not sure it scares me so much. But circles and arcs do.... Of well, I will be fresh again in 12 hours.

ccc

Lots of small changes... See: calculate_a_rect()

'''
    Pythonista Forum - @Phuket2
'''

import calendar
import editor
import math
import ui

# example, playing around, for 12 items its ok no math :)
_range_12 = (.3, .34, .38, .42, .46, .5, .55, .6, .63, .7, .85, 1.0)


def rects_on_circle_path(rect_path, obj_width, margin=2, num_objs=12):
    def calculate_a_rect(i):
        a = -2 * math.pi * (i + 1) / num_objs + math.radians(-150)
        pos = math.sin(a) * radius, math.cos(a) * radius
        r1 = ui.Rect(*pos, obj_width, obj_width)
        r1.x += r.width / 2 - obj_width / 2 + r.x
        r1.y += r.height / 2 - obj_width / 2 + r.y
        return r1

    r = ui.Rect(*rect_path).inset(obj_width / 2 + margin,
                                  obj_width / 2 + margin)
    radius = r.width / 2
    return r, [calculate_a_rect(i) for i in range(num_objs)]


def make_button(i):
    def button_action(sender):
        print('Button {} was pressed.'.format(sender.title))

    btn = ui.Button(title=calendar.month_abbr[i+1])
    btn.action = button_action
    btn.alpha = _range_12[i]
    btn.border_width = .5
    btn.bg_color = 'orange'
    btn.text_color = btn.tint_color = 'black'
    return btn


class MyClass(ui.View):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for i in range(12):
            self.add_subview(make_button(i))

    def layout(self):
        r, rects = rects_on_circle_path(self.bounds, obj_width=80, margin=20,
                                        num_objs=12)
        for i, btn in enumerate(self.subviews):
            btn.frame = rects[i]
            btn.corner_radius = btn.width / 2


if __name__ == '__main__':
    _use_theme = False
    w = h = 600
    f = (0, 0, w, h)
    mc = MyClass(frame=f, bg_color='white', name='Silly Demo')

    if not _use_theme:
        mc.present('sheet', animated=False)
    else:
        editor.present_themed(mc, theme_name='Oceanic', style='sheet',
                              animated=False)
Phuket2

@ccc , thanks. I know I over use parentheses. But I have read numerous times it's not Pythonetic to rely on operator precedence, better to be explicit with the use of parentheses. Sort of makes sense, a little of hard to find bugs can creep with small mistakes/understanding.

In my classes now, just out of habit I always have a make_view method. I know also can iterate though the subviews, I normally don't do it. I need to add one more subview then I need to add something somewhere to take of the anomaly. I also normally have a make_button or make ui_object as you normally do. Cleans up things a lot.

But as far as I know Pythonista Tools GitHub doesn't have a list of useful functions. I think it should have. I think it would be great if you or @JonB or other talented guys rewrote this function and added it the Pythonista Tools Lib. I say rewrite, because I am sure you would normally not use the variable names i am using. Also the function could use a baseline param (or whatever the correct name would be) to alter the center position of the obj. Now it's centered on the rect_path with no adjustment available.

Anyway, it's not I am lazy to submit to Pythonista Tools. But, I make to many simple errors. The code uploaded there should be trust worthy. The other problem I have is, if it's my repo, I can't respond properly to forks etc. Just saying....

Food for thought 🎉😬🎉🎉

Phuket2

@cvp, @ccc hmmmm, it's all gone to sh*t again. 😂😂😂😂 @cvp , the change that returned the shapes in the correct order stopped the distribution of the shapes evenly around the path. In my first post , you can see that items are evenly placed around the path. Odd or even number of objects. Both your and @ccc methods produce different results. But it's not the same as my first post ( you can see in the pic in the first post)Not sure if you guys can see the error or not.

I just seen the problem. I wanted to do like a compass selection rather than a month selection. So eight items, ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW' ]. It should of just worked. But didn't, the distribution part of the function is broken now.

I will try and figure it out, but if you see an easy answer that would be great also. 💋💋💋

Phuket2

So if in @ccc function I put it back the way I had it in my first post, I get the correct rect placements, just out of order. Trying to get the same result, just in order.

def rects_on_circle_path(rect_path, obj_width, margin=2, num_objs=12):
    def calculate_a_rect(i):
        '''
        a = -2 * math.pi * (i + 1) / num_objs + math.radians(-150)
        pos = math.sin(a) * radius, math.cos(a) * radius
        r1 = ui.Rect(*pos, obj_width, obj_width)
        r1.x += r.width / 2 - obj_width / 2 + r.x
        r1.y += r.height / 2 - obj_width / 2 + r.y
        '''
        a = 2 * math.pi * (i+1)/num_objs
        pos = (math.sin(a)*(radius*1), math.cos(a)*(radius*1))
        r1 = ui.Rect(pos[0] , pos[1] , obj_width, obj_width)
        r1.x += ((r.width/2) - (obj_width/2)+r.x)
        r1.y += ((r.height/2) - (obj_width/2)+r.y)

        return r1

    r = ui.Rect(*rect_path).inset(obj_width / 2 + margin,
    obj_width / 2 + margin)
    radius = r.width / 2
    return r, [calculate_a_rect(i) for i in range(num_objs)]

cvp
a = -2 * math.pi * (i + 1) / num_objs + math.radians(-135)
pos = math.sin(a) * radius, math.cos(a) * radius
cvp

Or, if you want to keep always my formulae,

a = 2 * math.pi * i/num_objs - math.pi/2
pos = (math.cos(a)*(radius*1), math.sin(a)*(radius*1)) # careful: cos,sin! not sin,cos
cvp

Why pos = cos,sin or pos = sin,cos
to better understand x,y coordinates with radius,angle

Phuket2

@cvp , thanks the below code is working now. I can't be sure everything I reported above was correct. Is possible I had some mis matched Params 😂 I still the the code below is cool. That one function just makes it so easy to put/draw objects in a circular shape, many interfaces could require this....well maybe. I just wanted to get that function working, I didn't try to make the class generic. Maybe I am dreaming. But thanks again for your help and diagrams. I was still 14 when I left school and I was a trouble maker. So, it's difficult to understand your diagrams, just because I have no real foundation. I was also looking at wiki for radians, it has some great diagrams and animations, hmmm but still it doesn't sink in. But I am going to try to find a math tutor. But it will not be easy in Thailand. We're I live and language barrier.
But again thanks....


'''
    Pythonista Forum - @Phuket2
'''

import calendar
import editor
import math
import ui

# example, playing around, for 12 items its ok no math :)
_range_12 = (.3, .34, .38, .42, .46, .5, .55, .6, .63, .7, .85, 1.0)

def css_clr_to_rgba(css_name, a):
    c = ui.parse_color(css_name)
    return (c[0], c[1], c[2], a)

def rects_on_circle_path(rect_path, obj_width, margin=2, num_objs=12):
    def calculate_a_rect(i):
        a = 2 * math.pi * i/num_objs - math.pi/2
        # careful: cos,sin! not sin,cos
        pos = (math.cos(a)*(radius*1), math.sin(a)*(radius*1)) 
        r1 = ui.Rect(*pos, obj_width, obj_width)
        r1.x += r.width / 2 - obj_width / 2 + r.x
        r1.y += r.height / 2 - obj_width / 2 + r.y

        return r1

    r = ui.Rect(*rect_path).inset(obj_width / 2 + margin,
    obj_width / 2 + margin)
    radius = r.width / 2
    return r, [calculate_a_rect(i) for i in range(num_objs)]


def make_button(idx, title):
    def button_action(sender):
        print('Button {} was pressed.'.format(sender.title))

    #btn = ui.Button(title=calendar.month_abbr[i+1])
    btn = ui.Button(title=title)
    btn.action = button_action
    #btn.alpha = _range_12[idx]
    btn.border_width = .5
    btn.bg_color = 'white'
    btn.text_color = btn.tint_color = 'black'
    return btn

class MyClass(ui.View):
    # some ideas
    _list=['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW' ]
    #_list=['N', 'E' , 'S' , 'W']
    #_list=['1st', '2nd', '3rd', '4th', '5th']
    #_list=['0', '90', '180', '270' ]
    #_list= [str(d) for d in range(0, 12)]
    _list = [calendar.month_abbr[i] for i in range(1,12)]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.cir_rect = None
        self.make_view()
    def make_view(self):
        for i in range(len(self._list)):
            self.add_subview(make_button(i, title=self._list[i]))

    def layout(self):
        r, rects = rects_on_circle_path(self.bounds, obj_width=70,                              margin=20, num_objs=len(self._list))
        self.cir_rect = r
        for i, btn in enumerate(self.subviews):
            btn.frame = rects[i]
            btn.title = self._list[i]
            btn.corner_radius = btn.width / 2

    def draw(self):
        s = ui.Path.oval(*self.cir_rect)
        with ui.GState():
            ui.set_color(css_clr_to_rgba('lime', .4))
            s.line_width = 1
            s.stroke()

if __name__ == '__main__':
    _use_theme = True
    w = h = 500
    f = (0, 0, w, h)
    mc = MyClass(frame=f, bg_color='white', name='Silly Demo')

    if not _use_theme:
        mc.present('sheet', animated=False)
    else:
        editor.present_themed(mc, theme_name='Solarized Dark', style='sheet',
        animated=False)
cvp

You're welcome for any mathematic/geometric question, but only if you accept an answer in a very poor English 🙄

Phuket2

@cvp , not trying to be smart. But there is nothing wrong with your English

abcabc

@Phuket2 I have implemented the circulartextlayout module to support circular layout. It is a slight modification of the textlayout module. Now the rows in layout text represents circular rings. Columns represent angular positions (Columns are not used for size calculations . Only rows are used for size calculations.) I hope it is useful. There is no need to calculate positions and size. The following git repository contains the code and examples:
https://github.com/balachandrana/textlayout
You can do a git pull if you have already got this.

In the following example, the month buttons are displayed in the outer ring and the images (imageview) are displayed
in the inner ring.

import circulartextlayout
import ui


layout_text = '''
************
************
bbbbbbbbbbbb
************
i*i*i*i*i*i*
************
************
'''

image_list = [ ui.Image.named(i) for i in 'Rabbit_Face Mouse_Face Cat_Face Dog_Face Octopus Cow_Face'.split()]
_range_12 = (.3, .34, .38, .42, .46, .5, .55, .6, .63, .7, .85, 1.0)

def button_action(sender):
    print('Button {} was pressed.'.format(sender.title))

titles = 'jan feb mar apr may jun jul aug sep oct nov dec'.split()

attributes = {'b': [{'action':button_action, 'font' :('Helvetica', 20),
                     'bg_color':'orange', 'alpha':_range_12[i],
                     'border_width':.5, 'text_color':'black', 'tint_color':'black',
                     'title':j } for i, j in enumerate(titles)],
             'i': [{'image':i,  'bg_color':'gray'} for i in image_list ]                  
             }

v = circulartextlayout.BuildView(layout_text, width=600, height=600, view_name='Counter',
    attributes=attributes).build_view()

for i in range(1, len(titles)+1):
    v['button'+str(i)].corner_radius = v['button'+str(i)].width*.5
for i in range(1, len(image_list)+1):
    v['imageview'+str(i)].corner_radius = v['imageview'+str(i)].width*.5
v.present('popover')

The screeshot for this example is given below:
http://imgur.com/a/WUwc5

I have also included an example (circular_directions.py) that is similar to your direction example.

cvp

If you want an alpha property to your button, but independent of the list length, you can do

btn = make_button(i, title=self._list[i])
btn.alpha = (1+i) * (1/len(self._list)) # will go from 1/n to 1.0, where n = Len of list
self.add_subview(btn)

Thus, no need of _range_12

abcabc

Just coped the @ccc's code and wanted to set the attributes as in @ccc's code.

cvp

@abcabc you're right but this code was only valuable for 12 items, not for 8, for instance in the code for N,....,E,.....,S,....W,..., or for any range

abcabc

Yes. You are right. I will update the code as suggested by you. Thanks for the suggestion.

Phuket2

@abcabc , @cvp , _range_12 is ok. Just a list of 12 numbers between 0.0 and 1.0. But purposely not done linearly. That's why I put values into the list. To do it without constants you need some math. You can't start a zero or even .1 or even .2 for that matter, the resulting alpha is too faint. Also depending on what you want to do, the progression normally will look better if its not linear.

Phuket2

@abcabc , ran your code. Works well. But you are not centered in the view. Maybe there is a param I can't see. But on my iPad Pro 12 inch I get the below.

Edit: your x,y are off. Too much added somewhere

Phuket2

Sorry a little out of sync with some of my comments. I am not sure it's just me or not, but I have had a hard time replying the last 40 mins or so.

abcabc

@Phuket2 There could be some problem with centering. I will look into that. Anyway I have to do some more testing.

Phuket2

Below is a pretty nice adaptation of using the shared function here, after it's been fixed up 😱

But for use in this function or not, the get_rotated_icon function is nice or let's say functional. Simple function, code comes from help from omz on a similar subject. Probably not for 60fps stuff, but I think good for ui stuff.

import editor
import math
import ui

# this is a pretty funky function...
def get_rotated_icon(named_icon_name, wh = 32, degree = 0):
    '''
        help from @omz
        https://forum.omz-software.com/topic/3180/understanding-ui-transform-rotation
    '''
    r = ui.Rect(0, 0, wh, wh)
    img = ui.Image.named(named_icon_name)
    with ui.ImageContext(wh, wh) as ctx:
        ui.concat_ctm(ui.Transform.translation(*r.center()))
        ui.concat_ctm(ui.Transform.rotation(math.radians(degree)))
        ui.concat_ctm(ui.Transform.translation(*r.center() * -1))
        img.draw()
        return ctx.get_image()

def make_button(idx, title, name = None):
    def button_action(sender):
        print('Button {} was pressed.'.format(sender.name))

    #btn = ui.Button(title=calendar.month_abbr[i+1])
    name = name if name else title
    btn = ui.Button(name = name, title=title )
    btn.action = button_action
    #btn.alpha = _range_12[idx]
    btn.border_width = .5
    btn.bg_color = 'white'
    btn.text_color = btn.tint_color = 'black'
    return btn

def css_clr_to_rgba(css_name, a):
    c = ui.parse_color(css_name)
    return (c[0], c[1], c[2], a)    

def rects_on_circle_path(rect_path, obj_width, margin=2, num_objs=12):
    def calculate_a_rect(i):
        a = 2 * math.pi * i/num_objs - math.pi/2
        # careful: cos,sin! not sin,cos
        pos = (math.cos(a)*(radius*1), math.sin(a)*(radius*1)) 
        r1 = ui.Rect(*pos, obj_width, obj_width)
        r1.x += r.width / 2 - obj_width / 2 + r.x
        r1.y += r.height / 2 - obj_width / 2 + r.y

        return r1

    r = ui.Rect(*rect_path).inset(obj_width / 2 + margin,
    obj_width / 2 + margin)
    radius = r.width / 2
    return r, [calculate_a_rect(i) for i in range(num_objs)]

class MyClass(ui.View):
    # some ideas
    _list=['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW' ]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.cir_rect = None
        self.obj_list = []
        self.mid_btn = None

        self.make_view()

    def make_view(self):
        for i in range(len(self._list)):
            obj = make_button(i, title=self._list[i])
            obj.image = get_rotated_icon('iob:arrow_up_a_256', wh = 256, degree = i * 45)
            self.obj_list.append(obj)
            self.add_subview(obj)

        btn = make_button(i, title='C')
        self.mid_btn = btn
        self.add_subview(btn)


    def layout(self):
        r, rects = rects_on_circle_path(self.bounds, obj_width=70,                              margin=20, num_objs=len(self._list))
        self.cir_rect = r
        for i, btn in enumerate(self.obj_list):
            btn.frame = rects[i]
            btn.title = ''
            btn.corner_radius = btn.width / 2

        btn = self.mid_btn
        btn.center = r.center()
        btn.corner_radius = btn.width / 2

    def draw(self):
        # just to see the path when testing...
        s = ui.Path.oval(*self.cir_rect)
        with ui.GState():
            ui.set_color(css_clr_to_rgba('lime', .4))
            s.line_width = 1
            s.stroke()


if __name__ == '__main__':
    _use_theme = True
    w=h = 600
    f = (0, 0, w, h)

    mc = MyClass(frame=f, bg_color='white')

    if not _use_theme:
        mc.present('sheet', animated=False)
    else:
        editor.present_themed(mc, theme_name='Oceanic', style='sheet', animated=False)

JonB

FWIW,
https://gist.github.com/6fc02b7d75eb22111b826cfdb2394697
is an example using ui.Transform to simulate your last imagE. Probably not as useful, but compact in this case.

abcabc

@JonB very nice. I have changed your code slightly to do a "semi-circular layout" which seems to be used in some of the ios applications.

import ui, calendar
from math import pi, sin, radians

def make_button(i, v, N):
    def button_action(sender):
        print('Button {} was pressed.'.format(sender.title))

    btn = ui.Button(title=calendar.month_abbr[i+1])
    btn.action = button_action
    btn.height=btn.width=64
    btn.alpha = sin(radians(15.+75.0/(N-1)*i))
    btn.border_width = .5
    btn.corner_radius = btn.width *.5
    btn.bg_color = 'orange'
    btn.text_color = btn.tint_color = 'black'
    center_x, center_y = v.bounds.center()
    btn.center = (btn.width/2+5.0, center_y) #v.bounds.center()
    btn.transform=ui.Transform.translation(0,-(v.height/2-btn.height/2)
                        ).concat(ui.Transform.rotation(2.*pi*i/N))
    return btn

v=ui.View(frame=(0,0,576,576))
v.bg_color=(1,1,1)
N = 12
N1 = 7
for i in range(0,N1):
    v.add_subview(make_button(i, v, N))
v.present('sheet')
Phuket2

Nice guys. As painfully as obvious as it is I have never thought of rotating the icons. After I did my compass test, I thought I would try it with arrows, could be used as a type of controller etc. then I was looking for the arrows offset by 45 degrees. Then it was like a big steel ball dropped from a great height on to my head. But also handy as you are only dealing with one image_name. I don't think speed would be ever a problem. Also simple to write out to PNG files if required. Anyway, I find it amazing how many things are sitting right under my nose I don't see

Phuket2

@JonB , yeah your approach a lot more straight fwd. to be honest I didn't think about rotating the button. But I was also thinking about drawing into the view vrs using buttons. But in this case, no reason not just to use the buttons. Many ways to skin the cat. But I will hold on to your code also, has its own versatility 😬

JonB

For what its worth, ui.concat_ctm along lets you use ui.Transforms inside ImageContexts.
This is perhaps a convolouted example, showing how you can use transforms rather than trying to do math for this sort of thing (in this case the sort of draggable lollipop hour selector). In the end, a little math is needed for the touch handling.
https://gist.github.com/84489b13a17cdd46288f16b50b2f7bc3

abcabc

Thanks @JonB for sharing this code. FWIW, I have modified this code to make a circular slider.

import ui
from math import pi,atan2

class CircularSlider(ui.View):
    def __init__(self,*args,**kwargs):
        ui.View.__init__(self,*args,**kwargs)
        self.a = 0
        self.value = (self.a+pi)/(2*pi)

    def draw(self):
        scl=min(self.width,self.height)
        self.scl=scl
        btn_siz=min(22/scl,0.05)
        #work in normalized units
        ui.concat_ctm(ui.Transform.scale(scl,scl))
        #origin at center
        ui.concat_ctm(ui.Transform.translation(.5,.5))
        ui.set_color('#1aa1b5')
        o = ui.Path.oval(-.5+btn_siz, -.5+btn_siz, 1-2*btn_siz, 1-2*btn_siz)
        o.line_width=2/scl
        o.stroke()
        #rotate by angle
        ui.concat_ctm(ui.Transform.rotation(self.a))
        # center origin at button
        ui.concat_ctm(ui.Transform.translation(.5-btn_siz,0))
        #optional: to keep images upright
        #ui.concat_ctm(ui.Transform.rotation(-self.a))
        p=ui.Path.oval(-btn_siz,-btn_siz,2*btn_siz,2*btn_siz)
        p.fill()

    def touch_moved(self,touch):
        dp=touch.location-touch.prev_location
        self.a=atan2(touch.location.y-self.scl/2.,touch.location.x-self.scl/2.)
        self.value = (self.a+pi)/(2*pi)
        self.set_needs_display()

    def touch_ended(self, touch):
        print(self.value)

d = CircularSlider(frame=(0,0,500, 500),bg_color='white')
d.present('sheet')
Phuket2

@abcabc , that's really nice. Would be great if you made your class a utility class. I mean passing all Params etc. to the class to be able to personalize it and make it a bit more generic. Eg, if you could set an image and set its rotation center, you could make a volume dial control. Maybe not the best explanation, but I think you see what I mean. But this sort of class in a generic form would be very useful too many

Phuket2

@JonB , I am pretty sure there is a good reason for it, but the reason eludes me. I am thinking about ui Elements having rotate, scale, axis etc attrs built in. I do understand that the Transform.xxx can be chained and used in animations etc. But it seems to me that any given ui object should have attrs such as rotate, scale, translate etc...maybe I am missing something about the math, but I don't think so. Just seems like a lot of hoops to go through when it could just as easily be a attr on any ui Element. Eg ui.Button(rotate_deg = 5) etc... Seems reasonable, well more than reasonable to me.

@omz not sure what you think about this. I think it would be really helpful. Not everyone that use Python/Pythonista are going to be geometrically blessed. I can only imagine some things are not that easy to implement given it all has to fit into XCode template also. But still it seems to me if it was possible, would make ui things easier for people like me 😱😂

abcabc

@Phuket2 I have modified the circular slider code so that you can use it like ui slider. The code and examples are are available in the following repository.

https://github.com/balachandrana/pythonista_circular_slider

In test1.py and test2.py you can control the ui slider with circular slider and vice versa. The test1.py code uses continuous mode and
in test2.py tint_color is set to red and continuous mode sets to false. In test3.py you can change the center image by slider.

Knob like designs (see the examples below) would require more effort and better designer skills. Anyway I will try to do some simple things later.
http://www.hongkiat.com/blog/beautiful-volume-dials-knobs/

Phuket2

@abcabc , thanks. I didn't really get the self.a property though. I was thinking if it set that to -90 it would basically set things to 0 on the clock. However it's set at 270 degrees. I tried a few values, but stopped because it was clear I didn't understand it. Btw, i did not download your test .py and pyui files yet. Will get later. But anyway, I did use the action/continuous attrs. Works really well. The results from the self.value appear to be spot on to what I would expect. But I still think a few attrs a little less mathy would be nice 😁 Starting point for example. I am sure it's there, but I think it's ok to assume the users of your class are totally dumb to any math you be using internally in your class. Not to say, you need to block access to attrs that could be set directly by people who,understand the math. I just say this, because a class like this with a nice/easy API would allow people like me to make animated interface items that would otherwise would be hard to make.
Btw, I love the link to the knobs and dials. But I disagree with you a little. If your class just handled the movements/tracking/queries it would be very flexible to be able create the knobs/dials in that link. Some of those fancy dials could be made by just manipulating an array of pics in a container like a ui.Button/ui.Image etc. meaning the effects don't have to real time to get a nice result , but they could also be real time effects if fast enough. I guess what I am saying your class does not need to draw anything and probably shouldn't unless for debugging help. Rather, you could overlay your class transparently on other ui objects and control the underlying objects from from class.
I hope you don't mind my me giving my opinion. I say it because I would love something like this. Of course I think about other functionality, like being able to supply a list of degrees as the only stop points etc. if I could write it, I would offer some code. One thing to mention, you also use inheritance. Have your base class and create different types of user controllers/Guestures whatever to call them off the base. Could keep it a bit cleaner as your ideas exapand.
Ok, that's my 5 cents worth of feedback 😬😬😬

abcabc

Please look at the tests. You need to use self.value (and not self.a which is internal' it varies from -pi tp +pi returned by atan2 function).

Phuket2

@abcabc , ok, I will do. But still I prefer not to have to know about atan etc... Just saying...

I just did this, it's bit of a mess, but just to illustrate what I was saying in my previous post. You circular_slider is just visible, but when using some graphics it would not be, just there as an overlay/controller. Again, sorry, I does nothing other than combine a number of things together. Also would hope to have multiple instances of your circular_slider on the view. For instances when you have a inner and outer control points

Phuket2

Sorry the last gist in the layout should have been. Not a big deal, just trying to show something.

def layout(self):
        r , rects = rects_on_circle_path(self.bounds, self.obj_w, margin=self.margin, N=len(self.obj_list))
        self.r = r

        if not self.cs:
            print('in here')
            self.cs = CS.CircularSlider(frame = r, name = 'CS')
            self.cs.action = self.cs_action
            self.cs.continuous = True
            self.cs.alpha = .05
            self.add_subview(self.cs)

        for i, r in enumerate(rects):
            self.obj_list[i].frame = r
abcabc

I have changed two lines in your program to make it work. See the following gist

https://gist.github.com/5e7b8beb9a0b35aaafbcd06b0cd30340

line 81:
self.degree = 90

line 183
self.degree = sender.value *360 -90

abcabc

Sorry. Typo. line 103 not 183.

Phuket2

@abcabc , thanks. It makes it work. I was not so worried about that. More worried bout you need to know the math inside to get it to work. The reason I tried -90, was because often from what I can see anyway , the geometry done in Pythonista is based on radians. But the point is does not matter what math you use, but as a consumer of your class, the less I have to know the better. Just because you know the math back the front, nothing to say I should have to know it. But something like degrees is pretty understandable for most people. Again, look it's your code and effort. I am just pushing in the case you want to make consumable class that the majority could use with ease. It's like cooking (which I love) you want as many people as possible to try your food. That's what makes the effort worth while and gives you a buzz when you get it right. I think writing classes/functions here is the same thing. Well, for me it's like that.

Phuket2

@abcabc , a small update to the gist. Its not suppose to present an ideal case. But combining the views/funcs can start to get something that looks ok. Have numbers around the outer ring in this example, would look more realistic with major and minor tick marks for example. But for me your circular_slider makes this possible.

Edit: of course the outter ring not even needed