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