import numpy as np from rembg import remove, new_session from PIL import Image, ImageFilter, ImageOps, ImageChops, ImageColor, ImageEnhance from moviepy.editor import ImageClip, CompositeVideoClip, ColorClip import math # Load AI Model session = new_session("u2net") 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): try: return ImageColor.getcolor(hex_color, "RGB") except: return (255, 255, 255) def get_edges(self, img, thickness=2): """Extracts the outline of the shape to create a 'Neon Tube' effect""" # Get alpha channel a = img.split()[-1] # Dilate (expand) and Erode (shrink) to find edges # Simple trick: Edge = Alpha - Eroded_Alpha eroded = a.filter(ImageFilter.MinFilter(3)) edges = ImageChops.difference(a, eroded) # Boost visibility edges = edges.point(lambda p: 255 if p > 50 else 0) return edges def process_image(self, remove_bg=True, keep_original=False, icon_color_hex="#FFFFFF"): with open(self.input_path, 'rb') as i: input_data = i.read() # 1. Background Removal if remove_bg: try: # alpha_matting=False is faster and safer for icons output_data = remove(input_data, session=session, alpha_matting=False) from io import BytesIO img = Image.open(BytesIO(output_data)).convert("RGBA") except: img = Image.open(self.input_path).convert("RGBA") else: img = Image.open(self.input_path).convert("RGBA") # 2. Resize (High Quality) if max(img.size) > 512: ratio = 512 / max(img.size) new_size = (int(img.width * ratio), int(img.height * ratio)) img = img.resize(new_size, Image.LANCZOS) # 3. Create Visual Style if keep_original: # Just sharpen it a bit img = ImageEnhance.Sharpness(img).enhance(1.2) base_icon = img else: # Create a "Neon Tube" look # A. Solid Fill r, g, b, alpha = img.split() target_rgb = self.hex_to_rgb(icon_color_hex) solid_fill = Image.new("RGB", img.size, target_rgb) fill_layer = Image.merge("RGBA", (*solid_fill.split(), alpha)) # B. White Edge (The Tube) edge_mask = self.get_edges(img) white_fill = Image.new("RGB", img.size, (255, 255, 255)) edge_layer = Image.merge("RGBA", (*white_fill.split(), edge_mask)) # Composite Edge on top of Fill base_icon = Image.alpha_composite(fill_layer, edge_layer) return base_icon def create_deep_glow(self, img, color_hex): """ Creates a 'Deep Glow' by stacking 4 layers of varying blur radii. This mimics professional compositing software. """ padding = 100 w, h = img.size new_size = (w + padding*2, h + padding*2) # 1. Prepare Base Glow Shape canvas = Image.new("RGBA", new_size, (0, 0, 0, 0)) canvas.paste(img, (padding, padding), img) # Colorize to Glow Color r, g, b, a = canvas.split() target_rgb = self.hex_to_rgb(color_hex) glow_fill = Image.new("RGB", new_size, target_rgb) glow_base = Image.merge("RGBA", (*glow_fill.split(), a)) # 2. Generate Stacked Blurs (The "Deep" Effect) # Radii: Tight, Mid, Wide, Atmosphere radii = [5, 15, 40, 80] opacities = [0.8, 0.6, 0.4, 0.2] composite_glow = Image.new("RGBA", new_size, (0, 0, 0, 0)) for radius, opacity in zip(radii, opacities): # Blur layer = glow_base.filter(ImageFilter.GaussianBlur(radius)) # Adjust Opacity r, g, b, a = layer.split() a = a.point(lambda p: int(p * opacity)) layer = Image.merge("RGBA", (r, g, b, a)) # Additive Blend (Screen) composite_glow = ImageChops.add(composite_glow, layer) return composite_glow, padding def create_reflection(self, img): reflection = ImageOps.flip(img) width, height = img.size # Non-linear fade for more realistic floor gradient = Image.new('L', (1, height), color=0xFF) for y in range(height): # Exponential falloff factor = (y / height) ** 0.5 alpha = 120 - int(factor * 255) if alpha < 0: alpha = 0 gradient.putpixel((0, y), alpha) alpha_mask = gradient.resize((width, height)) r, g, b, a = reflection.split() a = ImageChops.multiply(a, alpha_mask) reflection.putalpha(a) return reflection def generate_animation(self, icon_color="#FFFFFF", glow_color="#00E5FF", speed=2.0, intensity=1.2, use_original_colors=False, add_reflection=True, remove_bg=True): # 1. Prepare Base base_pil = self.process_image(remove_bg, use_original_colors, icon_color) w, h = base_pil.size # 2. Dimensions & Reflection reflection_h = 0 reflection_img = None if add_reflection: reflection_img = self.create_reflection(base_pil) reflection_h = int(h * 0.6) pad = 100 final_w = w + (pad * 2) final_h = h + (pad * 2) + reflection_h # 3. Create The Deep Glow Layer glow_pil, _ = self.create_deep_glow(base_pil, glow_color) # 4. Setup Video Clips clips_to_render = [] # Floor (Invisible Transparency Fix) bg_clip = ColorClip(size=(final_w, final_h), color=(0,0,0), duration=speed).set_opacity(0) clips_to_render.append(bg_clip) # A. The Glow Animation (Pulse) glow_clip = ImageClip(np.array(glow_pil)).set_duration(speed).set_position((0, 0)) def pulse_glow(t): # A heartbeat rhythm: fast up, slow down # Sine wave shifted to be always positive wave = math.sin(2 * math.pi * t / speed) # Map -1..1 to 0..1 norm = (wave + 1) / 2 # Intensity Modulation: Base 0.5, Max varies by intensity return 0.5 + (0.5 * norm * intensity) glow_clip.mask = glow_clip.mask.fl(lambda gf, t: gf(t) * pulse_glow(t)) clips_to_render.append(glow_clip) # B. The Reflection if add_reflection and reflection_img: ref_clip = ImageClip(np.array(reflection_img)).set_duration(speed) ref_clip = ref_clip.set_position((pad, pad + h - 5)) clips_to_render.append(ref_clip) # C. The Base Icon (Float Animation) base_clip = ImageClip(np.array(base_pil)).set_duration(speed) # Add a subtle "Hover" effect (bobbing up and down) def hover_pos(t): y_offset = 5 * math.sin(2 * math.pi * t / speed) return (pad, pad + y_offset) base_clip = base_clip.set_position(hover_pos) clips_to_render.append(base_clip) # 5. Composite final = CompositeVideoClip(clips_to_render, size=(final_w, final_h), bg_color=None) # 6. Export WEBM (High Quality) final.write_videofile( self.output_path, fps=24, codec='libvpx-vp9', ffmpeg_params=['-pix_fmt', 'yuva420p', '-auto-alt-ref', '0'], logger=None ) return self.output_path