[Share] a list of rects distributed around 360 degrees
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 , 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)
@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 😬
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.
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')
@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
@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 😱😂
@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.
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.
@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 😬😬😬
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).
@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
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
I have changed two lines in your program to make it work. See the following gist
self.degree = 90
self.degree = sender.value *360 -90
Sorry. Typo. line 103 not 183.
@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.
@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