Untitled
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