Untitled

 avatar
unknown
plain_text
a month ago
11 kB
4
Indexable
import customtkinter as ctk
from PIL import Image, ImageTk, ImageDraw
import time
import random
import json

class CircularProgress(ctk.CTkCanvas):
    _instances = []
    
    def __init__(self, parent, bin_id, size=200, base_color='#f5f5f7'):
        super().__init__(
            parent,
            width=size,
            height=size,
            bg='white',
            highlightthickness=0
        )
        
        # Keep track of destroyed status
        self._destroyed = False
        CircularProgress._instances.append(self)
        
        # Initialize properties
        self.size = size
        self.stroke_width = 8
        self.radius = (size - self.stroke_width) / 2
        self.center = size / 2
        self.fill_level = 0
        self.base_color = base_color
        self.dark_mode = False
        
        # Enhanced animation properties
        self.last_press_time = 0
        self.cooldown_period = 5.0  # Global cooldown between activations
        self.can_press = True
        self.is_pressed = False
        self.press_scale = 1.0
        self.press_animation_active = False
        self.servo_active = False
        
        # Animation timing constants
        self.PRESS_SCALE_MIN = 0.9
        self.PRESS_SCALE_MAX = 1.0
        self.SCALE_STEP = 0.05
        self.SERVO_OPEN_TIME = 10
        
        # Pulse animation properties
        self.pulse_active = False
        self.pulse_scale = 1.0
        self.pulse_growing = True
        self.PULSE_MIN = 0.9
        self.PULSE_MAX = 1.0
        self.PULSE_STEP = 0.007
        
        # Create image for drawing
        self.im = Image.new('RGBA', (1000, 1000))
        self.arc = None
        
        # Bind events
        self.bind('<Button-1>', self.on_press)
        self.bind('<ButtonRelease-1>', self.on_release)
        self.bind('<Destroy>', self._on_destroy)
        
        # Initialize
        self.target_fill_level = self.fill_level
        self.animate()
        
        # Set initial fill level
        self.set_fill_level(random.randint(0, 100))
        self.draw()

    def _on_destroy(self, event):
        if event.widget is self:
            self._destroyed = True
            try:
                CircularProgress._instances.remove(self)
            except ValueError:
                pass

    def get_progress_color(self):
        if self.dark_mode:
            if self.fill_level >= 80:
                return '#ff3b30'
            if self.fill_level >= 60:
                return '#ff9f0a'
            if self.fill_level >= 40:
                return '#ffd60a'
            return '#30d158'
        else:
            if self.fill_level >= 80:
                return '#ff453a'
            if self.fill_level >= 60:
                return '#ff9f0a'
            if self.fill_level >= 40:
                return '#ffd60a'
            return '#34c759'

    def get_ring_color(self):
        return '#2c2c2e' if self.dark_mode else '#e5e5e7'

    def set_dark_mode(self, is_dark):
        self.dark_mode = is_dark
        self.configure(bg='#1c1c1e' if is_dark else '#f5f5f7')
        self.draw()

    def set_fill_level(self, level):
        self.target_fill_level = float(level)
        self.fill_level = float(level)
        self.draw()

    def animate(self):
        if self._destroyed:
            return
            
        current_time = time.time()
        
        # Update can_press status
        if not self.can_press and current_time - self.last_press_time >= self.cooldown_period:
            self.can_press = True
        
        # Handle fill level animation
        if self.target_fill_level != self.fill_level:
            diff = self.target_fill_level - self.fill_level
            self.fill_level += diff * 0.2
            
            if abs(diff) < 0.5:
                self.fill_level = self.target_fill_level
        
        # Handle press animation
        if self.press_animation_active:
            if self.is_pressed:
                self.press_scale = max(self.PRESS_SCALE_MIN, self.press_scale - self.SCALE_STEP)
                if self.press_scale == self.PRESS_SCALE_MIN:
                    self.press_animation_active = self.is_pressed
            else:
                self.press_scale = min(self.PRESS_SCALE_MAX, self.press_scale + self.SCALE_STEP)
                if self.press_scale == self.PRESS_SCALE_MAX:
                    self.press_animation_active = False
        
        # Handle pulse animation during servo active state
        if self.servo_active:
            if self.pulse_growing:
                self.pulse_scale = min(self.PULSE_MAX, self.pulse_scale + self.PULSE_STEP)
                if self.pulse_scale >= self.PULSE_MAX:
                    self.pulse_growing = False
            else:
                self.pulse_scale = max(self.PULSE_MIN, self.pulse_scale - self.PULSE_STEP)
                if self.pulse_scale <= self.PULSE_MIN:
                    self.pulse_growing = True
        else:
            # Reset scale when servo is not active
            if self.pulse_scale != 1.0:
                self.pulse_scale = min(1.0, self.pulse_scale + self.PULSE_STEP)
        
        # Apply combined scale
        final_scale = self.press_scale * self.pulse_scale
        
        # Only redraw if there's been a change
        if self.press_animation_active or self.servo_active or self.target_fill_level != self.fill_level:
            self.draw(scale=final_scale)
        
        self.after(16, self.animate)

    def draw(self, scale=1.0):
        self.delete('all')
        
        # Define colors
        circle_color = '#34c759' if self.dark_mode else '#30d158'
        ring_color = self.get_ring_color()
        progress_color = self.get_progress_color()
        bg_color = '#1c1c1e' if self.dark_mode else '#f5f5f7'
        
        # Apply scale transform for animation
        scaled_size = self.size * scale
        
        # Create new image for drawing
        self.im = Image.new('RGBA', (1000, 1000), bg_color)
        draw = ImageDraw.Draw(self.im)
        
        # Calculate dimensions
        outer_padding = 40
        ring_width = 40
        circle_padding = 15
        
        # For light mode, draw white background circle
        if not self.dark_mode:
            draw.ellipse((outer_padding-ring_width, outer_padding-ring_width, 
                        1000-outer_padding+ring_width, 1000-outer_padding+ring_width), 
                        fill='white')
        
        # Draw ring
        draw.arc((outer_padding-ring_width, outer_padding-ring_width, 
                1000-outer_padding+ring_width, 1000-outer_padding+ring_width), 
                -90, 270, ring_color, ring_width)
        
        # Draw progress arc
        if self.fill_level > 0:
            angle = int(self.fill_level * 360 / 100)
            draw.arc((outer_padding-ring_width, outer_padding-ring_width, 
                    1000-outer_padding+ring_width, 1000-outer_padding+ring_width), 
                    -90, -90 + angle, progress_color, ring_width)
        
        # Draw base circle
        if not self.dark_mode:
            draw.ellipse((outer_padding + circle_padding - 1, 
                        outer_padding + circle_padding - 1, 
                        1000-outer_padding - circle_padding + 1, 
                        1000-outer_padding - circle_padding + 1), 
                        fill='white')
        
        draw.ellipse((outer_padding + circle_padding, 
                    outer_padding + circle_padding, 
                    1000-outer_padding - circle_padding, 
                    1000-outer_padding - circle_padding), 
                    fill=circle_color)
        
        # Resize and create PhotoImage
        resized = self.im.resize((int(scaled_size), int(scaled_size)), Image.Resampling.LANCZOS)
        self.arc = ImageTk.PhotoImage(resized)
        
        # Display the image
        self.create_image(self.size/2, self.size/2, image=self.arc)
        
        # Add text
        self.create_text(
            self.size/2,
            self.size/2,
            text=f"Button {self.bin_id}",
            font=('Dongle', int(18 * self.press_scale), 'normal'),
            fill='white' if self.dark_mode else 'black',
            justify='center',
            width=self.size-20
        )

    def on_press(self, event):
        current_time = time.time()
        if current_time - self.last_press_time >= self.cooldown_period:
            print(f"Button {self.bin_id} pressed")
            self.is_pressed = True
            self.press_animation_active = True
            self.last_press_time = current_time
            self.can_press = False
            
            # Toggle servo active state for demo
            self.servo_active = not self.servo_active
            print(f"Servo active: {self.servo_active}")

    def on_release(self, event):
        self.is_pressed = False

def main():
    root = ctk.CTk()
    root.title("Button Test")
    root.geometry("1024x600")
    
    # Set dark mode
    DARK_MODE = False
    ctk.set_appearance_mode("dark" if DARK_MODE else "light")
    
    # Create main frame
    main_frame = ctk.CTkFrame(root)
    main_frame.pack(fill='both', expand=True)
    
    # Create container for circles
    circles_frame = ctk.CTkFrame(main_frame)
    circles_frame.place(relx=0.5, rely=0.5, anchor='center')
    
    # Create padding frame
    padding_frame = ctk.CTkFrame(circles_frame)
    padding_frame.pack(padx=50)
    
    # Create 4 buttons
    for i in range(4):
        container = ctk.CTkFrame(padding_frame)
        container.pack(side='left', padx=15)
        
        progress = CircularProgress(
            container,
            bin_id=i+1,
            size=220
        )
        progress.pack()
        progress.set_dark_mode(DARK_MODE)
    
    # Add dark mode toggle
    dark_mode_switch = ctk.CTkSwitch(
        main_frame,
        text="Dark Mode",
        command=lambda: toggle_dark_mode(DARK_MODE),
        font=("Dongle", 16)
    )
    dark_mode_switch.place(relx=0.9, rely=0.1, anchor='center')
    
    def toggle_dark_mode(current_mode):
        nonlocal DARK_MODE
        DARK_MODE = not current_mode
        ctk.set_appearance_mode("dark" if DARK_MODE else "light")
        for instance in CircularProgress._instances:
            instance.set_dark_mode(DARK_MODE)
    
    root.mainloop()

if __name__ == "__main__":
    main()
Leave a Comment