File size: 4,033 Bytes
5d70f0e
 
0b187ab
5d70f0e
 
0b187ab
5d70f0e
 
 
 
 
 
84d0fd2
0b187ab
 
 
 
 
84d0fd2
 
5d70f0e
 
 
 
 
84d0fd2
5d70f0e
 
 
0b187ab
5d70f0e
84d0fd2
 
 
 
 
5d70f0e
84d0fd2
0b187ab
84d0fd2
5d70f0e
 
 
 
84d0fd2
 
 
 
 
 
 
 
 
 
5d70f0e
 
84d0fd2
 
 
5d70f0e
0b187ab
84d0fd2
 
5d70f0e
 
 
 
0b187ab
84d0fd2
 
 
 
 
0b187ab
 
84d0fd2
 
0b187ab
84d0fd2
0b187ab
5d70f0e
84d0fd2
0b187ab
84d0fd2
0b187ab
5d70f0e
0b187ab
 
 
5d70f0e
84d0fd2
 
0b187ab
 
 
84d0fd2
5d70f0e
84d0fd2
5d70f0e
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
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