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