""" A Tkinter widget for displaying animated icons. This module provides the AnimatedIcon class, a custom Tkinter Canvas widget that can display various types of animations. It supports both native Tkinter drawing and Pillow (PIL) for anti-aliased graphics if available. """ import tkinter as tk from math import sin, cos, pi from typing import Tuple, Optional try: from PIL import Image, ImageDraw, ImageTk PIL_AVAILABLE = True except ImportError: PIL_AVAILABLE = False def _hex_to_rgb(hex_color: str) -> Tuple[int, int, int]: """Converts a hex color string to an RGB tuple.""" hex_color = hex_color.lstrip('#') return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) class AnimatedIcon(tk.Canvas): """A custom Tkinter Canvas widget for displaying animations.""" def __init__(self, master: tk.Misc, width: int = 20, height: int = 20, animation_type: str = "counter_arc", color: str = "#2a6fde", highlight_color: str = "#5195ff", use_pillow: bool = False, bg: Optional[str] = None) -> None: """ Initializes the AnimatedIcon widget. Args: master: The parent widget. width (int): The width of the icon. height (int): The height of the icon. animation_type (str): The type of animation to display. Options: "counter_arc", "double_arc", "line", "blink". color (str): The primary color of the icon. highlight_color (str): The highlight color of the icon. use_pillow (bool): Whether to use Pillow for drawing if available. bg (str): The background color of the canvas. """ if bg is None: try: bg = master.cget("background") except tk.TclError: bg = "#f0f0f0" # Fallback color super().__init__(master, width=width, height=height, bg=bg, highlightthickness=0) self.width = width self.height = height self.animation_type = animation_type self.color = color self.highlight_color = highlight_color self.use_pillow = use_pillow and PIL_AVAILABLE self.running = False self.angle = 0 self.pulse_animation = False self.color_rgb = _hex_to_rgb(self.color) self.highlight_color_rgb = _hex_to_rgb(self.highlight_color) if self.use_pillow: self.image = Image.new( "RGBA", (width * 4, height * 4), (0, 0, 0, 0)) self.draw = ImageDraw.Draw(self.image) self.photo_image = None def _draw_frame(self) -> None: """Draws a single frame of the animation.""" if self.use_pillow: self._draw_pillow_frame() else: self._draw_canvas_frame() def _draw_canvas_frame(self) -> None: """Draws a frame using native Tkinter canvas methods.""" self.delete("all") if self.pulse_animation: self._draw_canvas_pulse() elif self.animation_type == "line": self._draw_canvas_line() elif self.animation_type == "double_arc": self._draw_canvas_double_arc() elif self.animation_type == "counter_arc": self._draw_canvas_counter_arc() elif self.animation_type == "blink": self._draw_canvas_blink() def _draw_canvas_pulse(self) -> None: """Draws the pulse animation using canvas methods.""" center_x, center_y = self.width / 2, self.height / 2 alpha = (sin(self.angle * 5) + 1) / 2 # Faster pulse r = int( alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0]) g = int( alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1]) b = int( alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2]) pulse_color = f"#{r:02x}{g:02x}{b:02x}" if self.animation_type == "line": for i in range(8): angle = i * (pi / 4) start_x = center_x + cos(angle) * (self.width * 0.2) start_y = center_y + sin(angle) * (self.height * 0.2) end_x = center_x + cos(angle) * (self.width * 0.4) end_y = center_y + sin(angle) * (self.height * 0.4) self.create_line(start_x, start_y, end_x, end_y, fill=pulse_color, width=2) elif self.animation_type == "double_arc": radius = min(center_x, center_y) * 0.8 bbox = (center_x - radius, center_y - radius, center_x + radius, center_y + radius) self.create_arc(bbox, start=0, extent=359.9, style=tk.ARC, outline=pulse_color, width=2) elif self.animation_type == "counter_arc": radius_outer = min(center_x, center_y) * 0.8 bbox_outer = (center_x - radius_outer, center_y - radius_outer, center_x + radius_outer, center_y + radius_outer) self.create_arc(bbox_outer, start=0, extent=359.9, style=tk.ARC, outline=pulse_color, width=2) radius_inner = min(center_x, center_y) * 0.6 bbox_inner = (center_x - radius_inner, center_y - radius_inner, center_x + radius_inner, center_y + radius_inner) self.create_arc(bbox_inner, start=0, extent=359.9, style=tk.ARC, outline=self.color, width=2) def _draw_canvas_line(self) -> None: """Draws the line animation using canvas methods.""" center_x, center_y = self.width / 2, self.height / 2 for i in range(8): angle = self.angle + i * (pi / 4) start_x = center_x + cos(angle) * (self.width * 0.2) start_y = center_y + sin(angle) * (self.height * 0.2) end_x = center_x + cos(angle) * (self.width * 0.4) end_y = center_y + sin(angle) * (self.height * 0.4) alpha = (cos(self.angle * 2 + i * (pi / 4)) + 1) / 2 r = int( alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0]) g = int( alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1]) b = int( alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2]) color = f"#{r:02x}{g:02x}{b:02x}" self.create_line(start_x, start_y, end_x, end_y, fill=color, width=2) def _draw_canvas_double_arc(self) -> None: """Draws the double arc animation using canvas methods.""" center_x, center_y = self.width / 2, self.height / 2 radius = min(center_x, center_y) * 0.8 bbox = (center_x - radius, center_y - radius, center_x + radius, center_y + radius) start_angle1 = -self.angle * 180 / pi extent1 = 120 + 60 * sin(-self.angle) self.create_arc(bbox, start=start_angle1, extent=extent1, style=tk.ARC, outline=self.highlight_color, width=2) start_angle2 = (-self.angle + pi) * 180 / pi extent2 = 120 + 60 * sin(-self.angle + pi / 2) self.create_arc(bbox, start=start_angle2, extent=extent2, style=tk.ARC, outline=self.color, width=2) def _draw_canvas_counter_arc(self) -> None: """Draws the counter arc animation using canvas methods.""" center_x, center_y = self.width / 2, self.height / 2 radius_outer = min(center_x, center_y) * 0.8 bbox_outer = (center_x - radius_outer, center_y - radius_outer, center_x + radius_outer, center_y + radius_outer) start_angle1 = -self.angle * 180 / pi self.create_arc(bbox_outer, start=start_angle1, extent=150, style=tk.ARC, outline=self.highlight_color, width=2) radius_inner = min(center_x, center_y) * 0.6 bbox_inner = (center_x - radius_inner, center_y - radius_inner, center_x + radius_inner, center_y + radius_inner) start_angle2 = self.angle * 180 / pi + 60 self.create_arc(bbox_inner, start=start_angle2, extent=150, style=tk.ARC, outline=self.color, width=2) def _draw_canvas_blink(self) -> None: """Draws the blink animation using canvas methods.""" center_x, center_y = self.width / 2, self.height / 2 radius = min(center_x, center_y) * 0.8 alpha = (sin(self.angle * 2) + 1) / 2 # Slower blinking speed r = int( alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0]) g = int( alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1]) b = int( alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2]) blink_color = f"#{r:02x}{g:02x}{b:02x}" self.create_arc(center_x - radius, center_y - radius, center_x + radius, center_y + radius, start=0, extent=359.9, style=tk.ARC, outline=blink_color, width=4) def _draw_pillow_frame(self) -> None: """Draws a frame using Pillow for anti-aliased graphics.""" self.draw.rectangle( [0, 0, self.width * 4, self.height * 4], fill=(0, 0, 0, 0)) if self.pulse_animation: self._draw_pillow_pulse() elif self.animation_type == "line": self._draw_pillow_line() elif self.animation_type == "double_arc": self._draw_pillow_double_arc() elif self.animation_type == "counter_arc": self._draw_pillow_counter_arc() elif self.animation_type == "blink": self._draw_pillow_blink() resized_image = self.image.resize( (self.width, self.height), Image.Resampling.LANCZOS) self.photo_image = ImageTk.PhotoImage(resized_image) self.delete("all") self.create_image(0, 0, anchor="nw", image=self.photo_image) def _draw_pillow_pulse(self) -> None: """Draws the pulse animation using Pillow.""" center_x, center_y = self.width * 2, self.height * 2 alpha = (sin(self.angle * 5) + 1) / 2 # Faster pulse r = int( alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0]) g = int( alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1]) b = int( alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2]) pulse_color = (r, g, b) if self.animation_type == "line": for i in range(12): angle = i * (pi / 6) start_x = center_x + cos(angle) * (self.width * 0.8) start_y = center_y + sin(angle) * (self.height * 0.8) end_x = center_x + cos(angle) * (self.width * 1.6) end_y = center_y + sin(angle) * (self.height * 1.6) self.draw.line([(start_x, start_y), (end_x, end_y)], fill=pulse_color, width=6, joint="curve") elif self.animation_type == "double_arc": radius = min(center_x, center_y) * 0.8 bbox = (center_x - radius, center_y - radius, center_x + radius, center_y + radius) self.draw.arc(bbox, start=0, end=360, fill=pulse_color, width=5) elif self.animation_type == "counter_arc": radius_outer = min(center_x, center_y) * 0.8 bbox_outer = (center_x - radius_outer, center_y - radius_outer, center_x + radius_outer, center_y + radius_outer) self.draw.arc(bbox_outer, start=0, end=360, fill=pulse_color, width=7) radius_inner = min(center_x, center_y) * 0.6 bbox_inner = (center_x - radius_inner, center_y - radius_inner, center_x + radius_inner, center_y + radius_inner) self.draw.arc(bbox_inner, start=0, end=360, fill=self.color_rgb, width=7) def _draw_pillow_line(self) -> None: """Draws the line animation using Pillow.""" center_x, center_y = self.width * 2, self.height * 2 for i in range(12): angle = self.angle + i * (pi / 6) start_x = center_x + cos(angle) * (self.width * 0.8) start_y = center_y + sin(angle) * (self.height * 0.8) end_x = center_x + cos(angle) * (self.width * 1.6) end_y = center_y + sin(angle) * (self.height * 1.6) alpha = (cos(self.angle * 2.5 + i * (pi / 6)) + 1) / 2 r = int( alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0]) g = int( alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1]) b = int( alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2]) color = (r, g, b) self.draw.line([(start_x, start_y), (end_x, end_y)], fill=color, width=6, joint="curve") def _draw_pillow_double_arc(self) -> None: """Draws the double arc animation using Pillow.""" center_x, center_y = self.width * 2, self.height * 2 radius = min(center_x, center_y) * 0.8 bbox = (center_x - radius, center_y - radius, center_x + radius, center_y + radius) start_angle1 = self.angle * 180 / pi extent1 = 120 + 60 * sin(self.angle) self.draw.arc(bbox, start=start_angle1, end=start_angle1 + extent1, fill=self.highlight_color_rgb, width=5) start_angle2 = (self.angle + pi) * 180 / pi extent2 = 120 + 60 * sin(self.angle + pi / 2) self.draw.arc(bbox, start=start_angle2, end=start_angle2 + extent2, fill=self.color_rgb, width=5) def _draw_pillow_counter_arc(self) -> None: """Draws the counter arc animation using Pillow.""" center_x, center_y = self.width * 2, self.height * 2 radius_outer = min(center_x, center_y) * 0.8 bbox_outer = (center_x - radius_outer, center_y - radius_outer, center_x + radius_outer, center_y + radius_outer) start_angle1 = self.angle * 180 / pi self.draw.arc(bbox_outer, start=start_angle1, end=start_angle1 + 150, fill=self.highlight_color_rgb, width=7) radius_inner = min(center_x, center_y) * 0.6 bbox_inner = (center_x - radius_inner, center_y - radius_inner, center_x + radius_inner, center_y + radius_inner) start_angle2 = -self.angle * 180 / pi + 60 self.draw.arc(bbox_inner, start=start_angle2, end=start_angle2 + 150, fill=self.color_rgb, width=7) def _draw_pillow_blink(self) -> None: """Draws the blink animation using Pillow.""" center_x, center_y = self.width * 2, self.height * 2 radius = min(center_x, center_y) * 0.8 alpha = (sin(self.angle * 2) + 1) / 2 # Slower blinking speed r = int( alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0]) g = int( alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1]) b = int( alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2]) blink_color = (r, g, b) self.draw.arc((center_x - radius, center_y - radius, center_x + radius, center_y + radius), start=0, end=360, fill=blink_color, width=10) def _draw_stopped_frame(self) -> None: """Draws the icon in its stopped (static) state.""" self.delete("all") if self.use_pillow: self._draw_pillow_stopped_frame() else: self._draw_canvas_stopped_frame() def _draw_canvas_stopped_frame(self) -> None: """Draws the stopped state using canvas methods.""" if self.animation_type == "line": self._draw_canvas_line_stopped() elif self.animation_type == "double_arc": self._draw_canvas_double_arc_stopped() elif self.animation_type == "counter_arc": self._draw_canvas_counter_arc_stopped() elif self.animation_type == "blink": self._draw_canvas_blink_stopped() def _draw_canvas_line_stopped(self) -> None: """Draws the stopped state for the line animation.""" center_x, center_y = self.width / 2, self.height / 2 for i in range(8): angle = i * (pi / 4) start_x = center_x + cos(angle) * (self.width * 0.2) start_y = center_y + sin(angle) * (self.height * 0.2) end_x = center_x + cos(angle) * (self.width * 0.4) end_y = center_y + sin(angle) * (self.height * 0.4) self.create_line(start_x, start_y, end_x, end_y, fill=self.highlight_color, width=2) def _draw_canvas_double_arc_stopped(self) -> None: """Draws the stopped state for the double arc animation.""" center_x, center_y = self.width / 2, self.height / 2 radius = min(center_x, center_y) * 0.8 bbox = (center_x - radius, center_y - radius, center_x + radius, center_y + radius) self.create_arc(bbox, start=0, extent=359.9, style=tk.ARC, outline=self.highlight_color, width=2) def _draw_canvas_counter_arc_stopped(self) -> None: """Draws the stopped state for the counter arc animation.""" center_x, center_y = self.width / 2, self.height / 2 radius_outer = min(center_x, center_y) * 0.8 bbox_outer = (center_x - radius_outer, center_y - radius_outer, center_x + radius_outer, center_y + radius_outer) self.create_arc(bbox_outer, start=0, extent=359.9, style=tk.ARC, outline=self.highlight_color, width=2) radius_inner = min(center_x, center_y) * 0.6 bbox_inner = (center_x - radius_inner, center_y - radius_inner, center_x + radius_inner, center_y + radius_inner) self.create_arc(bbox_inner, start=0, extent=359.9, style=tk.ARC, outline=self.color, width=2) def _draw_canvas_blink_stopped(self) -> None: """Draws the stopped state for the blink animation.""" center_x, center_y = self.width / 2, self.height / 2 radius = min(center_x, center_y) * 0.8 self.create_arc(center_x - radius, center_y - radius, center_x + radius, center_y + radius, start=0, extent=359.9, style=tk.ARC, outline=self.highlight_color, width=4) def _draw_pillow_stopped_frame(self) -> None: """Draws the stopped state using Pillow.""" self.draw.rectangle( [0, 0, self.width * 4, self.height * 4], fill=(0, 0, 0, 0)) if self.animation_type == "line": self._draw_pillow_line_stopped() elif self.animation_type == "double_arc": self._draw_pillow_double_arc_stopped() elif self.animation_type == "counter_arc": self._draw_pillow_counter_arc_stopped() elif self.animation_type == "blink": self._draw_pillow_blink_stopped() resized_image = self.image.resize( (self.width, self.height), Image.Resampling.LANCZOS) self.photo_image = ImageTk.PhotoImage(resized_image) self.create_image(0, 0, anchor="nw", image=self.photo_image) def _draw_pillow_line_stopped(self) -> None: """Draws the stopped state for the line animation using Pillow.""" center_x, center_y = self.width * 2, self.height * 2 for i in range(12): angle = i * (pi / 6) start_x = center_x + cos(angle) * (self.width * 0.8) start_y = center_y + sin(angle) * (self.height * 0.8) end_x = center_x + cos(angle) * (self.width * 1.6) end_y = center_y + sin(angle) * (self.height * 1.6) self.draw.line([(start_x, start_y), (end_x, end_y)], fill=self.highlight_color_rgb, width=6, joint="curve") def _draw_pillow_double_arc_stopped(self) -> None: """Draws the stopped state for the double arc animation using Pillow.""" center_x, center_y = self.width * 2, self.height * 2 radius = min(center_x, center_y) * 0.8 bbox = (center_x - radius, center_y - radius, center_x + radius, center_y + radius) self.draw.arc(bbox, start=0, end=360, fill=self.highlight_color_rgb, width=5) def _draw_pillow_counter_arc_stopped(self) -> None: """Draws the stopped state for the counter arc animation using Pillow.""" center_x, center_y = self.width * 2, self.height * 2 radius_outer = min(center_x, center_y) * 0.8 bbox_outer = (center_x - radius_outer, center_y - radius_outer, center_x + radius_outer, center_y + radius_outer) self.draw.arc(bbox_outer, start=0, end=360, fill=self.highlight_color_rgb, width=7) radius_inner = min(center_x, center_y) * 0.6 bbox_inner = (center_x - radius_inner, center_y - radius_inner, center_x + radius_inner, center_y + radius_inner) self.draw.arc(bbox_inner, start=0, end=360, fill=self.color_rgb, width=7) def _draw_pillow_blink_stopped(self) -> None: """Draws the stopped state for the blink animation using Pillow.""" center_x, center_y = self.width * 2, self.height * 2 radius = min(center_x, center_y) * 0.8 self.draw.arc((center_x - radius, center_y - radius, center_x + radius, center_y + radius), start=0, end=360, fill=self.highlight_color_rgb, width=10) def _animate(self) -> None: """The main animation loop.""" if self.running: self.angle += 0.1 if self.angle > 2 * pi: self.angle -= 2 * pi self._draw_frame() self.after(30, self._animate) def start(self, pulse: bool = False) -> None: """ Starts the animation. Args: pulse (bool): If True, plays a pulsing animation instead of the main one. """ if not self.running: self.pulse_animation = pulse self.running = True self._animate() def stop(self) -> None: """Stops the animation and shows the static 'stopped' frame.""" self.running = False self.pulse_animation = False self._draw_stopped_frame() def hide(self) -> None: """Stops the animation and clears the canvas.""" self.running = False self.pulse_animation = False self.delete("all") def show_full_circle(self) -> None: """Shows the static 'stopped' frame without starting the animation.""" if not self.running: self._draw_stopped_frame() try: from PIL import Image, ImageDraw, ImageTk PIL_AVAILABLE = True except ImportError: PIL_AVAILABLE = False def _hex_to_rgb(hex_color): """Converts a hex color string to an RGB tuple.""" hex_color = hex_color.lstrip('#') return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) class AnimatedIcon(tk.Canvas): """A custom Tkinter Canvas widget for displaying animations.""" def __init__(self, master, width=20, height=20, animation_type="counter_arc", color="#2a6fde", highlight_color="#5195ff", use_pillow=False, bg=None): """ Initializes the AnimatedIcon widget. Args: master: The parent widget. width (int): The width of the icon. height (int): The height of the icon. animation_type (str): The type of animation to display. Options: "counter_arc", "double_arc", "line", "blink". color (str): The primary color of the icon. highlight_color (str): The highlight color of the icon. use_pillow (bool): Whether to use Pillow for drawing if available. bg (str): The background color of the canvas. """ if bg is None: try: bg = master.cget("background") except tk.TclError: bg = "#f0f0f0" # Fallback color super().__init__(master, width=width, height=height, bg=bg, highlightthickness=0) self.width = width self.height = height self.animation_type = animation_type self.color = color self.highlight_color = highlight_color self.use_pillow = use_pillow and PIL_AVAILABLE self.running = False self.angle = 0 self.pulse_animation = False self.color_rgb = _hex_to_rgb(self.color) self.highlight_color_rgb = _hex_to_rgb(self.highlight_color) if self.use_pillow: self.image = Image.new( "RGBA", (width * 4, height * 4), (0, 0, 0, 0)) self.draw = ImageDraw.Draw(self.image) self.photo_image = None def _draw_frame(self): """Draws a single frame of the animation.""" if self.use_pillow: self._draw_pillow_frame() else: self._draw_canvas_frame() def _draw_canvas_frame(self): """Draws a frame using native Tkinter canvas methods.""" self.delete("all") if self.pulse_animation: self._draw_canvas_pulse() elif self.animation_type == "line": self._draw_canvas_line() elif self.animation_type == "double_arc": self._draw_canvas_double_arc() elif self.animation_type == "counter_arc": self._draw_canvas_counter_arc() elif self.animation_type == "blink": self._draw_canvas_blink() def _draw_canvas_pulse(self): """Draws the pulse animation using canvas methods.""" center_x, center_y = self.width / 2, self.height / 2 alpha = (sin(self.angle * 5) + 1) / 2 # Faster pulse r = int( alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0]) g = int( alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1]) b = int( alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2]) pulse_color = f"#{r:02x}{g:02x}{b:02x}" if self.animation_type == "line": for i in range(8): angle = i * (pi / 4) start_x = center_x + cos(angle) * (self.width * 0.2) start_y = center_y + sin(angle) * (self.height * 0.2) end_x = center_x + cos(angle) * (self.width * 0.4) end_y = center_y + sin(angle) * (self.height * 0.4) self.create_line(start_x, start_y, end_x, end_y, fill=pulse_color, width=2) elif self.animation_type == "double_arc": radius = min(center_x, center_y) * 0.8 bbox = (center_x - radius, center_y - radius, center_x + radius, center_y + radius) self.create_arc(bbox, start=0, extent=359.9, style=tk.ARC, outline=pulse_color, width=2) elif self.animation_type == "counter_arc": radius_outer = min(center_x, center_y) * 0.8 bbox_outer = (center_x - radius_outer, center_y - radius_outer, center_x + radius_outer, center_y + radius_outer) self.create_arc(bbox_outer, start=0, extent=359.9, style=tk.ARC, outline=pulse_color, width=2) radius_inner = min(center_x, center_y) * 0.6 bbox_inner = (center_x - radius_inner, center_y - radius_inner, center_x + radius_inner, center_y + radius_inner) self.create_arc(bbox_inner, start=0, extent=359.9, style=tk.ARC, outline=self.color, width=2) def _draw_canvas_line(self): """Draws the line animation using canvas methods.""" center_x, center_y = self.width / 2, self.height / 2 for i in range(8): angle = self.angle + i * (pi / 4) start_x = center_x + cos(angle) * (self.width * 0.2) start_y = center_y + sin(angle) * (self.height * 0.2) end_x = center_x + cos(angle) * (self.width * 0.4) end_y = center_y + sin(angle) * (self.height * 0.4) alpha = (cos(self.angle * 2 + i * (pi / 4)) + 1) / 2 r = int( alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0]) g = int( alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1]) b = int( alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2]) color = f"#{r:02x}{g:02x}{b:02x}" self.create_line(start_x, start_y, end_x, end_y, fill=color, width=2) def _draw_canvas_double_arc(self): """Draws the double arc animation using canvas methods.""" center_x, center_y = self.width / 2, self.height / 2 radius = min(center_x, center_y) * 0.8 bbox = (center_x - radius, center_y - radius, center_x + radius, center_y + radius) start_angle1 = -self.angle * 180 / pi extent1 = 120 + 60 * sin(-self.angle) self.create_arc(bbox, start=start_angle1, extent=extent1, style=tk.ARC, outline=self.highlight_color, width=2) start_angle2 = (-self.angle + pi) * 180 / pi extent2 = 120 + 60 * sin(-self.angle + pi / 2) self.create_arc(bbox, start=start_angle2, extent=extent2, style=tk.ARC, outline=self.color, width=2) def _draw_canvas_counter_arc(self): """Draws the counter arc animation using canvas methods.""" center_x, center_y = self.width / 2, self.height / 2 radius_outer = min(center_x, center_y) * 0.8 bbox_outer = (center_x - radius_outer, center_y - radius_outer, center_x + radius_outer, center_y + radius_outer) start_angle1 = -self.angle * 180 / pi self.create_arc(bbox_outer, start=start_angle1, extent=150, style=tk.ARC, outline=self.highlight_color, width=2) radius_inner = min(center_x, center_y) * 0.6 bbox_inner = (center_x - radius_inner, center_y - radius_inner, center_x + radius_inner, center_y + radius_inner) start_angle2 = self.angle * 180 / pi + 60 self.create_arc(bbox_inner, start=start_angle2, extent=150, style=tk.ARC, outline=self.color, width=2) def _draw_canvas_blink(self): """Draws the blink animation using canvas methods.""" center_x, center_y = self.width / 2, self.height / 2 radius = min(center_x, center_y) * 0.8 alpha = (sin(self.angle * 2) + 1) / 2 # Slower blinking speed r = int( alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0]) g = int( alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1]) b = int( alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2]) blink_color = f"#{r:02x}{g:02x}{b:02x}" self.create_arc(center_x - radius, center_y - radius, center_x + radius, center_y + radius, start=0, extent=359.9, style=tk.ARC, outline=blink_color, width=4) def _draw_pillow_frame(self): """Draws a frame using Pillow for anti-aliased graphics.""" self.draw.rectangle( [0, 0, self.width * 4, self.height * 4], fill=(0, 0, 0, 0)) if self.pulse_animation: self._draw_pillow_pulse() elif self.animation_type == "line": self._draw_pillow_line() elif self.animation_type == "double_arc": self._draw_pillow_double_arc() elif self.animation_type == "counter_arc": self._draw_pillow_counter_arc() elif self.animation_type == "blink": self._draw_pillow_blink() resized_image = self.image.resize( (self.width, self.height), Image.Resampling.LANCZOS) self.photo_image = ImageTk.PhotoImage(resized_image) self.delete("all") self.create_image(0, 0, anchor="nw", image=self.photo_image) def _draw_pillow_pulse(self): """Draws the pulse animation using Pillow.""" center_x, center_y = self.width * 2, self.height * 2 alpha = (sin(self.angle * 5) + 1) / 2 # Faster pulse r = int( alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0]) g = int( alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1]) b = int( alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2]) pulse_color = (r, g, b) if self.animation_type == "line": for i in range(12): angle = i * (pi / 6) start_x = center_x + cos(angle) * (self.width * 0.8) start_y = center_y + sin(angle) * (self.height * 0.8) end_x = center_x + cos(angle) * (self.width * 1.6) end_y = center_y + sin(angle) * (self.height * 1.6) self.draw.line([(start_x, start_y), (end_x, end_y)], fill=pulse_color, width=6, joint="curve") elif self.animation_type == "double_arc": radius = min(center_x, center_y) * 0.8 bbox = (center_x - radius, center_y - radius, center_x + radius, center_y + radius) self.draw.arc(bbox, start=0, end=360, fill=pulse_color, width=5) elif self.animation_type == "counter_arc": radius_outer = min(center_x, center_y) * 0.8 bbox_outer = (center_x - radius_outer, center_y - radius_outer, center_x + radius_outer, center_y + radius_outer) self.draw.arc(bbox_outer, start=0, end=360, fill=pulse_color, width=7) radius_inner = min(center_x, center_y) * 0.6 bbox_inner = (center_x - radius_inner, center_y - radius_inner, center_x + radius_inner, center_y + radius_inner) self.draw.arc(bbox_inner, start=0, end=360, fill=self.color_rgb, width=7) def _draw_pillow_line(self): """Draws the line animation using Pillow.""" center_x, center_y = self.width * 2, self.height * 2 for i in range(12): angle = self.angle + i * (pi / 6) start_x = center_x + cos(angle) * (self.width * 0.8) start_y = center_y + sin(angle) * (self.height * 0.8) end_x = center_x + cos(angle) * (self.width * 1.6) end_y = center_y + sin(angle) * (self.height * 1.6) alpha = (cos(self.angle * 2.5 + i * (pi / 6)) + 1) / 2 r = int( alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0]) g = int( alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1]) b = int( alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2]) color = (r, g, b) self.draw.line([(start_x, start_y), (end_x, end_y)], fill=color, width=6, joint="curve") def _draw_pillow_double_arc(self): """Draws the double arc animation using Pillow.""" center_x, center_y = self.width * 2, self.height * 2 radius = min(center_x, center_y) * 0.8 bbox = (center_x - radius, center_y - radius, center_x + radius, center_y + radius) start_angle1 = self.angle * 180 / pi extent1 = 120 + 60 * sin(self.angle) self.draw.arc(bbox, start=start_angle1, end=start_angle1 + extent1, fill=self.highlight_color_rgb, width=5) start_angle2 = (self.angle + pi) * 180 / pi extent2 = 120 + 60 * sin(self.angle + pi / 2) self.draw.arc(bbox, start=start_angle2, end=start_angle2 + extent2, fill=self.color_rgb, width=5) def _draw_pillow_counter_arc(self): """Draws the counter arc animation using Pillow.""" center_x, center_y = self.width * 2, self.height * 2 radius_outer = min(center_x, center_y) * 0.8 bbox_outer = (center_x - radius_outer, center_y - radius_outer, center_x + radius_outer, center_y + radius_outer) start_angle1 = self.angle * 180 / pi self.draw.arc(bbox_outer, start=start_angle1, end=start_angle1 + 150, fill=self.highlight_color_rgb, width=7) radius_inner = min(center_x, center_y) * 0.6 bbox_inner = (center_x - radius_inner, center_y - radius_inner, center_x + radius_inner, center_y + radius_inner) start_angle2 = -self.angle * 180 / pi + 60 self.draw.arc(bbox_inner, start=start_angle2, end=start_angle2 + 150, fill=self.color_rgb, width=7) def _draw_pillow_blink(self): """Draws the blink animation using Pillow.""" center_x, center_y = self.width * 2, self.height * 2 radius = min(center_x, center_y) * 0.8 alpha = (sin(self.angle * 2) + 1) / 2 # Slower blinking speed r = int( alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0]) g = int( alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1]) b = int( alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2]) blink_color = (r, g, b) self.draw.arc((center_x - radius, center_y - radius, center_x + radius, center_y + radius), start=0, end=360, fill=blink_color, width=10) def _draw_stopped_frame(self): """Draws the icon in its stopped (static) state.""" self.delete("all") if self.use_pillow: self._draw_pillow_stopped_frame() else: self._draw_canvas_stopped_frame() def _draw_canvas_stopped_frame(self): """Draws the stopped state using canvas methods.""" if self.animation_type == "line": self._draw_canvas_line_stopped() elif self.animation_type == "double_arc": self._draw_canvas_double_arc_stopped() elif self.animation_type == "counter_arc": self._draw_canvas_counter_arc_stopped() elif self.animation_type == "blink": self._draw_canvas_blink_stopped() def _draw_canvas_line_stopped(self): """Draws the stopped state for the line animation.""" center_x, center_y = self.width / 2, self.height / 2 for i in range(8): angle = i * (pi / 4) start_x = center_x + cos(angle) * (self.width * 0.2) start_y = center_y + sin(angle) * (self.height * 0.2) end_x = center_x + cos(angle) * (self.width * 0.4) end_y = center_y + sin(angle) * (self.height * 0.4) self.create_line(start_x, start_y, end_x, end_y, fill=self.highlight_color, width=2) def _draw_canvas_double_arc_stopped(self): """Draws the stopped state for the double arc animation.""" center_x, center_y = self.width / 2, self.height / 2 radius = min(center_x, center_y) * 0.8 bbox = (center_x - radius, center_y - radius, center_x + radius, center_y + radius) self.create_arc(bbox, start=0, extent=359.9, style=tk.ARC, outline=self.highlight_color, width=2) def _draw_canvas_counter_arc_stopped(self): """Draws the stopped state for the counter arc animation.""" center_x, center_y = self.width / 2, self.height / 2 radius_outer = min(center_x, center_y) * 0.8 bbox_outer = (center_x - radius_outer, center_y - radius_outer, center_x + radius_outer, center_y + radius_outer) self.create_arc(bbox_outer, start=0, extent=359.9, style=tk.ARC, outline=self.highlight_color, width=2) radius_inner = min(center_x, center_y) * 0.6 bbox_inner = (center_x - radius_inner, center_y - radius_inner, center_x + radius_inner, center_y + radius_inner) self.create_arc(bbox_inner, start=0, extent=359.9, style=tk.ARC, outline=self.color, width=2) def _draw_canvas_blink_stopped(self): """Draws the stopped state for the blink animation.""" center_x, center_y = self.width / 2, self.height / 2 radius = min(center_x, center_y) * 0.8 self.create_arc(center_x - radius, center_y - radius, center_x + radius, center_y + radius, start=0, extent=359.9, style=tk.ARC, outline=self.highlight_color, width=4) def _draw_pillow_stopped_frame(self): """Draws the stopped state using Pillow.""" self.draw.rectangle( [0, 0, self.width * 4, self.height * 4], fill=(0, 0, 0, 0)) if self.animation_type == "line": self._draw_pillow_line_stopped() elif self.animation_type == "double_arc": self._draw_pillow_double_arc_stopped() elif self.animation_type == "counter_arc": self._draw_pillow_counter_arc_stopped() elif self.animation_type == "blink": self._draw_pillow_blink_stopped() resized_image = self.image.resize( (self.width, self.height), Image.Resampling.LANCZOS) self.photo_image = ImageTk.PhotoImage(resized_image) self.create_image(0, 0, anchor="nw", image=self.photo_image) def _draw_pillow_line_stopped(self): """Draws the stopped state for the line animation using Pillow.""" center_x, center_y = self.width * 2, self.height * 2 for i in range(12): angle = i * (pi / 6) start_x = center_x + cos(angle) * (self.width * 0.8) start_y = center_y + sin(angle) * (self.height * 0.8) end_x = center_x + cos(angle) * (self.width * 1.6) end_y = center_y + sin(angle) * (self.height * 1.6) self.draw.line([(start_x, start_y), (end_x, end_y)], fill=self.highlight_color_rgb, width=6, joint="curve") def _draw_pillow_double_arc_stopped(self): """Draws the stopped state for the double arc animation using Pillow.""" center_x, center_y = self.width * 2, self.height * 2 radius = min(center_x, center_y) * 0.8 bbox = (center_x - radius, center_y - radius, center_x + radius, center_y + radius) self.draw.arc(bbox, start=0, end=360, fill=self.highlight_color_rgb, width=5) def _draw_pillow_counter_arc_stopped(self): """Draws the stopped state for the counter arc animation using Pillow.""" center_x, center_y = self.width * 2, self.height * 2 radius_outer = min(center_x, center_y) * 0.8 bbox_outer = (center_x - radius_outer, center_y - radius_outer, center_x + radius_outer, center_y + radius_outer) self.draw.arc(bbox_outer, start=0, end=360, fill=self.highlight_color_rgb, width=7) radius_inner = min(center_x, center_y) * 0.6 bbox_inner = (center_x - radius_inner, center_y - radius_inner, center_x + radius_inner, center_y + radius_inner) self.draw.arc(bbox_inner, start=0, end=360, fill=self.color_rgb, width=7) def _draw_pillow_blink_stopped(self): """Draws the stopped state for the blink animation using Pillow.""" center_x, center_y = self.width * 2, self.height * 2 radius = min(center_x, center_y) * 0.8 self.draw.arc((center_x - radius, center_y - radius, center_x + radius, center_y + radius), start=0, end=360, fill=self.highlight_color_rgb, width=10) def _animate(self): """The main animation loop.""" if self.running: self.angle += 0.1 if self.angle > 2 * pi: self.angle -= 2 * pi self._draw_frame() self.after(30, self._animate) def start(self, pulse=False): """ Starts the animation. Args: pulse (bool): If True, plays a pulsing animation instead of the main one. """ if not self.running: self.pulse_animation = pulse self.running = True self._animate() def stop(self): """Stops the animation and shows the static 'stopped' frame.""" self.running = False self.pulse_animation = False self._draw_stopped_frame() def hide(self): """Stops the animation and clears the canvas.""" self.running = False self.pulse_animation = False self.delete("all") def show_full_circle(self): """Shows the static 'stopped' frame without starting the animation.""" if not self.running: self._draw_stopped_frame()