CeNiEi
CeNiEi

Reputation: 107

How to make Canvas vertex instructions relative to a widget in Kivy

I just started learning Kivy and I was trying to understand how the Canvas instructions are affected on resizing of the window of the kivyApp.

In the Kivy documentation it is mentioned that -

Note

Kivy drawing instructions are not automatically relative to the position or size of the widget. You, therefore, need to consider these factors when drawing. In order to make your drawing instructions relative to the widget, the instructions need either to be declared in the KvLang or bound to pos and size changes.

The example which follows, shows how to bind the size and position of a Rectangle instruction to the size and position of the widget in which it is being drawn. Therefore the size and the position changes proportionally when the window is resized.

But, how can I do the same for somthing like a Bezier instruction, which uses points.

I have a custom widget HangManFig1 which extend the Widget class, which is defined in KVlang like this:

<HangManFig1>:
    canvas:
        Line:
            points: (150, 100, 150, 700)
            width: 10
        Line:
            points: (150, 700, 600, 700)
            width: 10
        Line:
            points: (150, 600, 250, 700)
            width: 10
        Line:
            points: (600, 700, 600, 600)
            width: 3
        Ellipse:
            pos: (550, 500)
        Line:
            bezier: (610, 510, 630, 400, 570, 350)
            width: 10
        Line:
            bezier: (570, 350, 510, 370, 450, 270)
            width: 10
        Line:
            bezier: (570, 350, 600, 300, 550, 200)
            width: 10
        Line:
            bezier: (610, 480, 530, 430, 500, 430)
            width: 10
        Line:
            bezier: (610, 480, 630, 500, 680, 390)
            width: 10

I use this widget in a Screen, in the following manner:

#:import HangManFig1 figures.hangmanfig1

<MainScreen>:
    name: '_main_screen_'
    BoxLayout:
        RelativeLayout:
            size_hint_x: 0.7
            HangManFig1:

        RelativeLayout:
            size_hint_x: 0.3
            Button:
                text: 'Play'
                pos_hint: {'x': 0.1, 'y': 0.80}
                size_hint: (0.8, 0.1)
                on_release:
                    root.manager.transition.direction = 'left'
                    root.manager.current = '_game_screen_'

            Button:
                text: 'Practice'
                pos_hint: {'x': 0.1, 'y': 0.60}
                size_hint: (0.8, 0.1)
                on_release:
                    root.manager.transition.direction = 'left'
                    root.manager.current = '_game_screen_'

            Button:
                text: 'Share'
                pos_hint: {'x': 0.1, 'y': 0.40}
                size_hint: (0.8, 0.1)
                on_release:
                    root.manager.transition.direction = 'left'
                    root.manager.current = '_share_screen_'

            Button:
                text: 'Credits'
                pos_hint: {'x': 0.1, 'y': 0.20}
                size_hint: (0.8, 0.1)
                on_release:
                    root.manager.transition.direction = 'left'
                    root.manager.current = '_credits_screen_'

KivyApp in full screen

When I am resizing the window, I see that although the Buttons are being positioned correctly, but not HangManFig1.

Enter image description here

Is there a way, in which I can bind the size of this widget to that of the Parent Widget so that it is positioned correctly even when the Window size changes?

Upvotes: 0

Views: 900

Answers (4)

Tshirtman
Tshirtman

Reputation: 5947

While you used RelativeLayout to make the coordinates of your instructions relative to the position of your widget, it doesn't do anything regarding its size.

As you hardcoded all the positions by numeric values, you'll need a way to scale these values relative to the size of your widget, and you have to consider what you want to happen regarding the width of your lines in this situation, should it grow relative to the size of the widget as well? linearly? Something else? Depending on what you want various possibility exist.

The easiest starting point, IMHO, would be to use a Scale instruction, to set all the canvas instructions relative to the size of your widget, over the size you used to design your current hangman.

<HangManFig1>:
    h: 600
    w: 800 # just guessing the size you used to design it, adjust as needed
    canvas:
        PushMatrix:
        Scale:
           xy: self.width / self.w, self.height / self.h

        Line:
            points: (150, 100, 150, 700)
            width: 10

        Ellipse:
            pos: (550, 500)
        ... # etc, all the other instructions remain unchanged

        PopMatrix: # restore the initial canvas so it doesn't affect other instructions after your drawing

If that's not enough for you, because you want to keep the width of the line constant for example, you could either not do it with a Scale instruction, but instead have a function that takes the size of your widget and a set of coordinates as input, and returns the value relative to that size:

def rscale(size, *args):
    w, h = size
    ws = w / 800  # adjust accordingly, as in the first example
    hs = h / 600
    return (x / (ws if i % 2 else hs) for i, x in enumerate(args))

This function could be used like this.

        Line:
           points: rscale(self.size, 150, 100, 150, 700)

And if you want something more sophisticated, like preserving the aspect ratio of your hangman, while staying in the boundaries of your size, you could adjust accordingly to something like:


def rscale(size, *args):
    w, h = size
    scale = min(w / 800, h / 600) # pick the smallest scale of the two
    return (x / s for x in args)

Upvotes: 2

FLAW
FLAW

Reputation: 347

Well, I have bad news. Once I wanted to animate Bézier curves, I could animate the Bezier class beziers, but not Line(bezier=(...)). and width of Bezier is can't be changed, because there is no such property. And I ended up with animating only 1px width Bézier curves. So, there is not much with dynamic vector lines in Kivy... yet (I hope).

I love the simplicity of that library, but it is not mature yet I guess. And I decided to move on.

There are a lot of things that doesn't have Kivy's problems, such as web and webframeworks (which I chose over Kivy). I love Python, and Kivy is so capable. but when it comes a little bit down to specific things, Kivy really lacks :'(

Upvotes: 0

FLAW
FLAW

Reputation: 347

I've got:

class ourLine(Line):
    widget=None
    vLines=[]
    firstime=True

    def __init__(self,relto=Window,**kwargs):
        self.kwargs=kwargs
        if not self.widget:
            print('you should inherit a new class with \'widget\' attr')
        super(ourLine,self).__init__(**kwargs)
        W, H = Window.width, Window.height
        self.rBezier=kwargs['bezier']
        self.vBezier=[]
        c=0
        for p in self.rBezier:
            c+=1
            if not c%2:
                self.vBezier.append(p/H)
            else:
                self.vBezier.append(p/W)
        self.__class__.vLines.append(self)
        del self.kwargs['bezier']

    def deAbstractor(self):
        W, H = self.__class__.widget.width, self.__class__.widget.height
        _vBezier=[]
        c=0
        with self.__class__.widget.canvas:
            for p in self.vBezier:
                c+=1
                if not c%2:
                    _vBezier.append(p*H)
                else:
                    _vBezier.append(p*W)
            Line(bezier=_vBezier, **self.kwargs)

    def dyna(c,w,s):
        print(w)
        for l in c.vLines:
            l.__class__.widget.canvas.clear()
        for l in c.vLines:
            l.deAbstractor()
    def activate(c):
        c.widget.bind(size=lambda w,s: myLine.dyna(myLine,w,s))

This may be a little messy and can contain something unnecessary. That's because, first I wanted to make this more advanced. But then it went a little crazy. But still it is a good way to do vector lines. It supports width and other properties you give in Line (I hope). color isn't changing, but it can be implemented. Let’s see it in action:

import kivy
from kivy.app import App
from kivy.graphics import *
from kivy.core.window import Window
from kivy.uix.floatlayout import FloatLayout
from guy.who.helped import ourLine
root=FloatLayout()

#tell us your widget so we can refer
class myLine(ourLine):
    widget=root

#you may want to start listening later, so it is manual.
ourLine.activate(myLine)

#you can start using like a normal Line
with root.canvas:
    Color(1,1,0,1)
    myLine(bezier=[2,70,90,80,Window.width,Window.height])
    myLine(bezier=[200,170,309,80,Window.width/2,Window.height*0.8], width=12)

class MyApp(App):
    def build(self):
        return root

if __name__ == '__main__':
    MyApp().run()

Enter image description here

With what, is this post better than others? Well, this can use Bézier curves, not points and is responsive.

And weirdly, it is not so bad with performance. It fires when you resize the window. So, the rest of the time, as good as basic Line + a few bytes of RAM to hold relative values and extras.

Upvotes: 1

John Anderson
John Anderson

Reputation: 38947

Yes, you can. It takes a bit of work, but instead of using explicit coordinates for the Canvas Lines, use values that are based on the size of the HangManFig1 object. For example, the first Line of your drawing could be something like:

<HangManFig1>:
    canvas:
        Line:
            points: (root.width*0.1, root.height * 0.1, root.width * 0.1, root.height * 0.8)
            width: 10

Upvotes: 2

Related Questions