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

radius = r.width / 2
for i in range(0, num_objs):
a = 2 * pi * (i+1)/num_objs
r1 = ui.Rect(pos , 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)
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

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.

Edit: your x,y are off. Too much added somewhere • 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, c, c, 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)
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)

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

`````` • posted
0

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.

• posted
0

@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.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.present('sheet')
``````

• posted
0

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

• posted
0

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

• posted
0

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

• posted
0

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

• posted
0

@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

• posted
0

@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 😱😂

• posted
0

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

• posted
0

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

• posted
0

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

Internal error.

Oops! Looks like something went wrong!