maybe a fork of sparrowhe's "bluesky circle" webapp, to frontend only?
1import requests 2from PIL import Image, ImageOps, ImageDraw 3from io import BytesIO 4import math 5import os 6 7PI=3.14 8 9def check_cache_img(url): 10 # Check if the cache path not exists 11 if not os.path.join('static', 'avatars'): 12 os.makedirs(os.path.join('static', 'avatars')) 13 14 # Check if the image is already cached 15 cache_path = os.path.join('static', 'avatars', os.path.basename(url.split('/')[-1]+'.jpg')) 16 if os.path.exists(cache_path): 17 return cache_path 18 return None 19 20def load_image_as_circle(image_url, radius, proxies=None): 21 try: 22 # Add proxy support in the request 23 if check_cache_img(image_url): 24 print('Using cached image') 25 img = Image.open(check_cache_img(image_url)) 26 else: 27 response = requests.get(image_url, proxies=proxies) 28 img = Image.open(BytesIO(response.content)) 29 img.save(os.path.join('static', 'avatars', os.path.basename(image_url.split('/')[-1])+'.jpg')) 30 response = requests.get(image_url, proxies=proxies) 31 img = Image.open(BytesIO(response.content)) 32 33 # Calculate diameter from radius 34 diameter = radius * 2 35 36 # Ensure source image is larger or equal to target size before resizing 37 if img.size[0] < diameter or img.size[1] < diameter: 38 img = img.resize((diameter, diameter), Image.Resampling.LANCZOS) 39 else: 40 # Resize using LANCZOS resampling 41 img = img.resize((diameter, diameter), Image.Resampling.LANCZOS) 42 43 # Create a mask to crop the image into a circle 44 mask = Image.new('L', (diameter, diameter), 0) 45 draw = ImageDraw.Draw(mask) 46 draw.ellipse((0, 0, diameter, diameter), fill=255) 47 48 # Apply the mask to create a circular image 49 img = ImageOps.fit(img, (diameter, diameter), centering=(0.5, 0.5)) 50 img.putalpha(mask) 51 52 return img 53 except Exception as e: 54 print(f"Error loading image: {e}") 55 return None 56 57# Function to plot avatars in circular layout with varying sizes using PIL 58def plot_avatars_full_circle(friends_dict, center_avatar_url, proxies=None): 59 # Create a blank 1024x1024 white canvas 60 canvas_size = (800, 800) 61 canvas = Image.new("RGBA", canvas_size, (255, 255, 255, 0)) 62 63 # Define the sizes for the avatars in each circle 64 base_radius = int(35*1.25) # Radius for avatars in the innermost circle 65 center_avatar_size = 150 # Size for the center avatar (radius 70 means diameter 140) 66 size_step = 5 # Decrease size by 10 pixels for each layer 67 min_avatar_size = 15 # Minimum size to prevent overly small avatars 68 69 # Load and place center avatar 70 center_img = load_image_as_circle(center_avatar_url, center_avatar_size // 2, proxies=proxies) 71 if center_img: 72 center_position = (canvas_size[0] // 2 - center_avatar_size // 2, canvas_size[1] // 2 - center_avatar_size // 2) 73 canvas.paste(center_img, center_position, center_img) 74 75 # Sort friends based on their reply_score, highest score first 76 sorted_friends = sorted(friends_dict.items(), key=lambda x: x[1]['reply_score'], reverse=True) 77 78 # Plot avatars based on score 79 radius_step = 85 # Distance between each circle in pixels 80 initial_radius = 140 # Radius for the first circle 81 friend_idx = 0 82 radius = initial_radius 83 layer = 0 84 gap_size = 10 85 86 while friend_idx < len(sorted_friends) and layer < 4: 87 # Calculate avatar size for this layer (decrease by size_step for each layer) 88 avatar_radius = max(base_radius - layer * size_step, min_avatar_size // 2) # Use radius now 89 avatar_size = avatar_radius * 2 # Diameter for resizing 90 91 # Calculate how many avatars fit in the current circle 92 # Adding gap_size to ensure there is space between avatars 93 num_in_current_circle = int(2 * PI * radius / (avatar_size + gap_size)) # Adjusted based on avatar size + gap 94 theta_step = 2 * PI / num_in_current_circle # Angle between avatars 95 rotation_offset = layer * (PI / 12) # Rotate 15 degrees for each layer 96 97 for i in range(num_in_current_circle): 98 if friend_idx >= len(sorted_friends): 99 break # Stop if no more friends to place 100 101 friend_data = sorted_friends[friend_idx][1] 102 avatar_url = friend_data['avatar'] 103 theta = i * theta_step + rotation_offset # Apply rotation 104 105 # Calculate avatar position 106 x = int(canvas_size[0] // 2 + radius * math.cos(theta) - avatar_radius) # Center the circle 107 y = int(canvas_size[1] // 2 + radius * math.sin(theta) - avatar_radius) 108 109 # Load and place circular avatar 110 img = load_image_as_circle(avatar_url, avatar_radius, proxies=proxies) 111 if img: 112 canvas.paste(img, (x, y), img) 113 114 friend_idx += 1 115 116 # Move to the next circle 117 radius += radius_step 118 layer += 1 119 120 buf = BytesIO() 121 canvas.save(buf, format='PNG') 122 data = buf.getvalue() 123 buf.close() 124 return data