# [Share] a list of rects distributed around 360 degrees browsing

• posted
1

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)

for i in range(0, num_objs):
a = 2 * pi * (i+1)/num_objs
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

• posted
0

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

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]

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

• posted
0

@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 ðŸŽ‰ðŸ˜¬ðŸŽ‰ðŸŽ‰

• posted
0

@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. ðŸ’‹ðŸ’‹ðŸ’‹

• posted
0

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)
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
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)
return r, [calculate_a_rect(i) for i in range(num_objs)]
``````

• posted
0

``````a = -2 * math.pi * (i + 1) / num_objs + math.radians(-135)
``````

• posted
0

Or, if you want to keep always my formulae,

``````a = 2 * math.pi * i/num_objs - math.pi/2
``````

• posted
1

Why pos = cos,sin or pos = sin,cos

• posted
0

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

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]

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

• posted
1

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

• posted
0

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

• posted
0

@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):
for i in range(1, len(image_list)+1):
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.

• posted
0

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

Thus, no need of _range_12

• posted
0

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

• posted
0

@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

• posted
0

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

• posted
0

@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.

• posted
0

@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.

• posted
0

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.

• posted
0

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

• posted
0

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

btn = make_button(i, title='C')
self.mid_btn = 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 = self.mid_btn
btn.center = r.center()

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)

``````

Internal error.

Oops! Looks like something went wrong!