Spaces:
Sleeping
Sleeping
| import numpy as np | |
| from rembg import remove | |
| from PIL import Image, ImageFilter, ImageColor | |
| from moviepy.editor import ImageClip, CompositeVideoClip | |
| import os | |
| import math | |
| class IconAnimator: | |
| def __init__(self, input_path, output_path): | |
| self.input_path = input_path | |
| self.output_path = output_path | |
| def hex_to_rgb(self, hex_color): | |
| # Converts hex string (e.g., "#00ffc8") to RGB tuple | |
| try: | |
| return ImageColor.getcolor(hex_color, "RGB") | |
| except: | |
| return (255, 255, 255) # Fallback to white | |
| def process_image(self, icon_color_hex="#FFFFFF"): | |
| # 1. Remove Background | |
| with open(self.input_path, 'rb') as i: | |
| input_data = i.read() | |
| output_data = remove(input_data) | |
| # Load into PIL | |
| from io import BytesIO | |
| img = Image.open(BytesIO(output_data)).convert("RGBA") | |
| # 2. Recolor the Icon Body | |
| r, g, b, a = img.split() | |
| target_r, target_g, target_b = self.hex_to_rgb(icon_color_hex) | |
| solid_color = Image.new("RGB", img.size, (target_r, target_g, target_b)) | |
| img_colored = Image.merge("RGBA", (*solid_color.split(), a)) | |
| return img_colored | |
| def create_glow_layer(self, img, blur_radius, color_hex): | |
| # Create padding so glow isn't cut off | |
| padding = 60 | |
| new_size = (img.width + padding*2, img.height + padding*2) | |
| canvas = Image.new("RGBA", new_size, (0, 0, 0, 0)) | |
| canvas.paste(img, (padding, padding), img) | |
| # Extract Alpha to create the glow shape | |
| r, g, b, a = canvas.split() | |
| # Create solid color for glow | |
| target_r, target_g, target_b = self.hex_to_rgb(color_hex) | |
| glow_base = Image.new("RGB", new_size, (target_r, target_g, target_b)) | |
| glow_shape = Image.merge("RGBA", (*glow_base.split(), a)) | |
| # Apply Blur | |
| glow = glow_shape.filter(ImageFilter.GaussianBlur(radius=blur_radius)) | |
| return glow, padding | |
| def generate_animation(self, icon_color="#FFFFFF", glow_color="#00ffc8", speed=2.0, intensity=1.0): | |
| # 1. Prepare Base Image | |
| base_pil = self.process_image(icon_color) | |
| # 2. Create Glow Layers (Inner and Outer) | |
| glow_small, pad = self.create_glow_layer(base_pil, blur_radius=10, color_hex=glow_color) | |
| glow_large, _ = self.create_glow_layer(base_pil, blur_radius=30, color_hex=glow_color) | |
| w, h = base_pil.size | |
| final_w, final_h = w + (pad*2), h + (pad*2) | |
| # 3. Create MoviePy Clips | |
| # Center the base icon | |
| base_clip = ImageClip(np.array(base_pil)).set_duration(speed).set_position("center") | |
| glow_s_clip = ImageClip(np.array(glow_small)).set_duration(speed) | |
| glow_l_clip = ImageClip(np.array(glow_large)).set_duration(speed) | |
| # 4. Apply Animation (THE FIX) | |
| # Instead of set_opacity(func), we modify the mask directly using fl() | |
| def pulse_small(t): | |
| # Oscillates between 0.6 and 1.0 | |
| val = 0.6 + 0.4 * math.sin(2 * math.pi * t / speed) | |
| return max(0.0, min(val * intensity, 1.0)) | |
| def pulse_large(t): | |
| # Oscillates between 0.4 and 0.7 | |
| val = 0.4 + 0.3 * math.sin(2 * math.pi * t / speed) | |
| return max(0.0, min(val * intensity, 1.0)) | |
| # We multiply the existing mask frame (gf(t)) by the pulse value | |
| glow_s_clip.mask = glow_s_clip.mask.fl(lambda gf, t: gf(t) * pulse_small(t)) | |
| glow_l_clip.mask = glow_l_clip.mask.fl(lambda gf, t: gf(t) * pulse_large(t)) | |
| # 5. Composite | |
| final = CompositeVideoClip([ | |
| glow_l_clip, # Outer glow (Background) | |
| glow_s_clip, # Inner glow (Rim light) | |
| base_clip # Sharp Icon (Foreground) | |
| ], size=(final_w, final_h)) | |
| # 6. Export | |
| final.write_gif(self.output_path, fps=15, program='ffmpeg', opt='OptimizeTransparency') | |
| return self.output_path |