TomSchimansky / CustomTkinter

A modern and customizable python UI-library based on Tkinter
MIT License
11.14k stars 1.05k forks source link

slow animation for items(frame, moving item) #1410

Closed Guendo345 closed 1 year ago

Guendo345 commented 1 year ago

The idea is to integrate slow animations for moving or changing the dimension of a widget.

VasigaranAndAngel commented 1 year ago

Give it a try - https://github.com/VasigaranAndAngel/PyAnimator

image

https://user-images.githubusercontent.com/72515046/229426274-ece227e1-237c-49e4-96c7-a4f08da2f649.mp4

the sad thing is CTk is not smooth for this.

Guendo345 commented 1 year ago

It's not bad but the idea is to integrate directly into Custom Tkinter to add dynamism to elements like smooth move or when a frame dimension is (width=100, height=100) when there is a change of dimension (width=200, height=200) that it is slower

VasigaranAndAngel commented 1 year ago

Tried .moveto in canvas. image

https://user-images.githubusercontent.com/72515046/229476722-567dd922-0d98-464d-8c70-585ace3b7505.mp4

If we animate this slower, it's not smooth. I don't know if we can go more deeper and animate the elements.

https://user-images.githubusercontent.com/72515046/229479472-fa82836c-c7c0-41de-a6d2-b02fc95741c3.mp4

so, adding dynamism to widgets, not will smoother.

Guendo345 commented 1 year ago

try with this code pls

Guendo345 commented 1 year ago
import customtkinter

class smooth_frame(customtkinter.CTk):
    def __init__(self):
        super().__init__()

        self.title("smooth frame")
        self.maxsize(600, 500)
        self.minsize(600, 500)

        self.frame = customtkinter.CTkFrame(self, width=200, height=200, fg_color='black')
        self.frame.place(x=10, y=10)

        self.button = customtkinter.CTkButton(self, text="Smooth resize frame")
        self.button.place(x=10, y=350)

if __name__ == "__main__":
    app = smooth_frame()
    app.overrideredirect(False)
    app.mainloop()
VasigaranAndAngel commented 1 year ago

https://user-images.githubusercontent.com/72515046/229608696-3a72e8c2-55b4-4bf6-85eb-c14273641da4.mp4

if we look at this closer, it's not moving in some values for some reason.

https://user-images.githubusercontent.com/72515046/229610417-320224c0-b3c0-4883-b52c-36fceef16414.mp4

Guendo345 commented 1 year ago

can you show me the code

VasigaranAndAngel commented 1 year ago
def _resize_frame(self):
        speed = 1
        animator = Animator(200, 480, 1*speed, 60//speed, 'ease')
        st = time.perf_counter()
        for value in animator:
            self.frame.configure(height=value, width=value)
            self.update()
            en = time.perf_counter()
            self.label1.configure(text='rounded value - ' + str(round(value)))
            self.label2.configure(text='direct value  - ' + str(value))
            self.label3.configure(text='fps - ' + str(1/(en-st)))
            self.update()
            st = time.perf_counter()
Guendo345 commented 1 year ago

full code because you add label pls

Guendo345 commented 1 year ago

and i want to know how to set limit for frame size

VasigaranAndAngel commented 1 year ago
import customtkinter
from PyAnimator import Animator
import time

class smooth_frame(customtkinter.CTk):
    def __init__(self):
        super().__init__()

        self.title("smooth frame")
        self.maxsize(600, 500)
        self.minsize(600, 500)

        self.frame = customtkinter.CTkFrame(self, width=200, height=200, fg_color='black')
        self.frame.place(x=10, y=10)

        self.button = customtkinter.CTkButton(self, text="Smooth resize frame", command=self._resize_frame)
        self.button.place(x=10, y=350)

        self.label1 = customtkinter.CTkLabel(self, font=('Consolas', 15), text='rounded value - 0')
        self.label1.place(x=300, y=470, anchor='sw')

        self.label2 = customtkinter.CTkLabel(self, font=('Consolas', 15), text='direct value  - 0.0')
        self.label2.place(x=300, y=490, anchor='sw')

        self.label3 = customtkinter.CTkLabel(self, font=('Consolas', 15), text='fps - 0')
        self.label3.place(x=300, y=450, anchor='sw')

    def _resize_frame(self):
        speed = 60
        animator = Animator(200, 480, 1*speed, 60//speed, 'ease')
        st = time.perf_counter()
        for value in animator:
            self.frame.configure(height=value, width=value)
            self.update()
            en = time.perf_counter()
            self.label1.configure(text='rounded value - ' + str(round(value)))
            self.label2.configure(text='direct value  - ' + str(value))
            self.label3.configure(text='fps - ' + str(1/(en-st)))
            self.update()
            st = time.perf_counter()

if __name__ == "__main__":
    app = smooth_frame()
    app.overrideredirect(False)
    app.attributes('-topmost', True)
    app.mainloop()
VasigaranAndAngel commented 1 year ago

and i want to know how to set limit for frame size

can you explain what frame size?

Guendo345 commented 1 year ago

I have encountered an error

Guendo345 commented 1 year ago

image

Guendo345 commented 1 year ago

i mean frame dimensions(width, height) to set set a limit when the button is pushed

Guendo345 commented 1 year ago

and PyAnimator isn't in library so i can't use it i need your help

VasigaranAndAngel commented 1 year ago

Give it a try - https://github.com/VasigaranAndAngel/PyAnimator

You can download PyAnimator from the link.

def _resize_frame(self):
    animator = Animator(current_value=200, target_value=480, duration=1, fps=60, easing='ease')
    st = time.perf_counter()
    for value in animator:
        self.frame.configure(height=value, width=value)
        self.update()
        en = time.perf_counter()
        self.label1.configure(text='rounded value - ' + str(round(value)))
        self.label2.configure(text='direct value  - ' + str(value))
        self.label3.configure(text='fps - ' + str(1/(en-st)))
        self.update()
        st = time.perf_counter()

You can see the line animator = Animator(current_value=200, target_value=480, duration=1, fps=60, easing='ease'), which sets the current_value and target_value parameters. For current_value, you should provide the current height and width of the frame as the starting point of the animation, and for target_value, you should provide the desired height and width as the ending point of the animation. Currently, PyAnimator only supports animating one value at a time. Therefore, you will need to use two animators to animate both the height and width of the frame:

height_anim = Animator(current_value=200, target_value=480, duration=1, fps=60, easing='ease')
width_anim = Animator(current_value=200, target_value=680, duration=1, fps=60, easing='ease')
for height, width in zip(height_anim, width_anim):
    self.frame.configure(height=height, width=width)
    self.update()

This code will animate the height and width of the frame from 200 to 480 and from 200 to 680, respectively.

In the current implementation of the Animator class, the time.sleep function is used to introduce a delay between each frame of the animation. However, this approach is not very accurate and leads to timing issues, especially when the duration of the animation is long or the frame rate is high. To overcome this issue, I'm working on finding a better solution for timing the animation frames.

I hope this helps you understand better 🙂.

Guendo345 commented 1 year ago
import customtkinter
from PyAnimator import Animator
import time

class smooth_frame(customtkinter.CTk):
    def __init__(self):
        super().__init__()

        self.title("smooth frame")
        self.maxsize(600, 500)
        self.minsize(600, 500)

        self.label1 = customtkinter.CTkLabel(self, font=('Consolas', 15), text='rounded value - 0')
        self.label1.place(x=300, y=470, anchor='sw')

        self.label2 = customtkinter.CTkLabel(self, font=('Consolas', 15), text='direct value  - 0.0')
        self.label2.place(x=300, y=490, anchor='sw')

        self.label3 = customtkinter.CTkLabel(self, font=('Consolas', 15), text='fps - 0')
        self.label3.place(x=300, y=450, anchor='sw')

        self.frame = customtkinter.CTkFrame(self, width=100, height=100, fg_color='black')
        self.frame.place(x=10, y=10)
        self.frame.bind('<Enter>', self._resize_frame)
        self.frame.bind('<Leave>', self._reduce_frame)

    def _resize_frame(self, event):
        speed = 60
        animator = Animator(200, 480, 1*speed, 60//speed, 'ease')
        st = time.perf_counter()
        self.height_anim = Animator(current_value=100, target_value=220, duration=0.5, fps=60, easing='ease')
        self.width_anim = Animator(current_value=100, target_value=220, duration=0.5, fps=60, easing='ease')
        for height, width in zip(self.height_anim, self.width_anim):
            self.frame.configure(height=height, width=width)
            self.update()
        for value in animator:
            self.update()
            en = time.perf_counter()
            self.label1.configure(text='rounded value - ' + str(round(value)))
            self.label2.configure(text='direct value  - ' + str(value))
            self.label3.configure(text='fps - ' + str(1/(en-st)))
            self.update()
            st = time.perf_counter()

    def _reduce_frame(self, event):
        speed = 60
        animator = Animator(200, 480, 1*speed, 60//speed, 'ease')
        st = time.perf_counter()
        height_anim = Animator(current_value=self.height_anim.target_value, target_value=100, duration=0.5, fps=60, easing='ease')
        width_anim = Animator(current_value=self.width_anim.target_value, target_value=100, duration=0.5, fps=60, easing='ease')
        for height, width in zip(height_anim, width_anim):
            self.frame.configure(height=height, width=width)
            self.update()
        for value in animator:
            self.update()
            en = time.perf_counter()
            self.label1.configure(text='rounded value - ' + str(round(value)))
            self.label2.configure(text='direct value  - ' + str(value))
            self.label3.configure(text='fps - ' + str(1/(en-st)))
            self.update()
            st = time.perf_counter()

if __name__ == "__main__":
    app = smooth_frame()
    app.overrideredirect(False)
    app.attributes('-topmost', True)
    app.mainloop()

try this, thank you

Guendo345 commented 1 year ago

But we can't use this code in a complex graphic interface because is not optimised

VasigaranAndAngel commented 1 year ago

The problem with the current implementation of Animator is that it uses time.sleep() for timing, which relies on the system clock. In Linux, it is possible to sleep for even 1 millisecond, but in Windows, the minimum time we can sleep is like 50 milliseconds (I don't know exactly). Moreover, the system clock is not entirely accurate and can change over time, causing issues with the timing of the animation. Although Python can handle about 7 million if statements with a small calculation per second on an average laptop, Python can sleep for 0.0002 milliseconds using a new method if that's properly designed. I'm experimenting with some methods to achieve better timing, but it's using a lot of resources and causing the program to lag. So, I'm still trying to figure out a solution 🥴.

If we increase the fps to 120 instead of 60, the animation will run at around 60 fps, which can make it a little smoother. However, when using Tkinter, all the elements are drawn in integer values. So if the animation runs slower and uses small decimal changes in value, those changes may not be visible. If you're okay with these limitations, I can help you in making animations.

This is one of my projects that involves using the Animator in multiple widgets.

https://user-images.githubusercontent.com/72515046/229839820-47ac30b5-453c-42fd-bff0-206997a31b78.mp4

These switches are animated at 60fps.

Guendo345 commented 1 year ago

share the code

Guendo345 commented 1 year ago

the time() library cannot simply create a timer when the program is starting

Guendo345 commented 1 year ago

otherwise we will say that Python cannot allow the development of very elaborate graphical interfaces.

VasigaranAndAngel commented 1 year ago

We can't say like that. Tkinter has some limitations and may not be the best choice for creating highly elaborate graphical interfaces. However, this does not mean that Python cannot be used for animations and graphics. It depends on the complexity and requirements of your project. If you need to create a very elaborate graphical interface, you may want to explore other frameworks that offer more flexibility and advanced features.

VasigaranAndAngel commented 1 year ago

This function moves the switch and it runs when the switch is clicked or initialized. Still I'm working on it. I'm using threading in my program. But tkinter is not thread safe, so there might be some issues that arise because of that. However, since I'm just using the program for personal use, I'm not too concerned about it.

def _move_to(self, to: str):
    current = self.switch_state
    if to == LEFT: end = 0
    elif to == RIGHT: end = self.WIDTH//2
    elif to == CENTER: end = self.WIDTH//4
    def do():
        self.animating = True
        # int(self._canvas.coords(self.button_front)[0])
        animator = Animator(int(self.current_pos), end, duration=0.3*1, fps=60/1, easing=(0,.52,.39,1))
        def kill(): self.animating = False; self.kill_anim = False; quit()
        for value in animator:
            self._canvas.moveto(self.button_front, value, '')
            self.current_pos = value
            self.update()
            if self.kill_anim:
                kill()
        kill()
    if self.animating:
        self.kill_anim = True
        # self.anim_thread.join()
    self.anim_thread = threading.Thread(target=do)
    self.anim_thread.start()
ElectricCandlelight commented 1 year ago

Threading is fine as long you go about it in the right way. Move any Tkinter operations to the main thread and use an event_generate() in your thread to let tkinter know when something has changed by using bind() in the main Tkinter thread.

VasigaranAndAngel commented 1 year ago

Threading is fine as long you go about it in the right way. Move any Tkinter operations to the main thread and use an event_generate() in your thread to let tkinter know when something has changed by using bind() in the main Tkinter thread.

Oh cool, I wasn't aware of that. So threading is really not a problem then. Thanks for letting me know!