I've been trying to write a script to draw some arcs which fade out to the background colour (see sample below).
from PIL import Image, ImageDraw
import math
import random
def draw_concentric_circles(size=800, num_circles=15):
# Create a grey background
bg_color = (0, 0, 0, 255) # Dark grey
img = Image.new('RGBA', (size, size), bg_color)
draw = ImageDraw.Draw(img)
center = size // 2
max_radius = (size // 2) - 40
radii = [int(r) for r in range(50, max_radius, max_radius // num_circles)]
colors = [
(232, 69, 76), # Red
(117, 176, 217) # Blue
]
for r in radii:
base_color = random.choice(colors)
# Random position for the break (in radians)
break_pos = random.uniform(0, 2 * math.pi)
gap_half_width = 0.05 # Size of the break
# We draw the circle by connecting tiny segments
# 1000 steps ensures the line looks smooth
steps = 1000
for i in range(steps):
# Current angle
angle = (i / steps) * 2 * math.pi
# Calculate distance from the break to determine alpha
# This logic finds the shortest angular distance to the break
diff = abs(angle - break_pos)
if diff > math.pi:
diff = 2 * math.pi - diff
# If we are inside the "break" zone, don't draw
if diff < gap_half_width:
continue
# Normalize distance for fading:
# Near the break (diff approx gap_half_width) -> Alpha 255 (100%)
# Far from break (diff approx PI) -> Alpha 127 (50%)
# Linear interpolation:
alpha_ratio = 1.0 - (1.5 * (diff / math.pi))
alpha = max(min(int(255 * alpha_ratio), 255), 0)
# Calculate coordinates
x = center + r * math.cos(angle)
y = center + r * math.sin(angle)
# Draw a tiny point/rectangle to represent the segment
# We use a small offset to give the line some thickness
thickness = 5
draw.ellipse([x - thickness, y - thickness, x + thickness, y + thickness],
fill=(base_color[0], base_color[1], base_color[2], alpha), outline=None)
print("Showing..")
img.show()
draw_concentric_circles()
Gives me:
Draw does not blend the new pixels with the existing pixels. It replaces the existing (background) pixel values with the new pixel values.
One way of solving it is having two images: a solid background; and a transparent foreground and then creating an alpha composite image from the two (also see the ImageDraw module documentation which gives an example of Drawing Partial-Opacity Text, using the same technique):
from PIL import Image, ImageDraw
import math
import random
def draw_concentric_circles(size=800, num_circles=15):
# Create a grey background
background_color = (0, 0, 0) # Dark grey
background = Image.new('RGBA', (size, size), background_color + (255,))
foreground = Image.new('RGBA', (size, size), background_color + (0,))
draw = ImageDraw.Draw(foreground)
center = size // 2
max_radius = (size // 2) - 40
radii = [int(r) for r in range(50, max_radius, max_radius // num_circles)]
colors = [
(232, 69, 76), # Red
(117, 176, 217) # Blue
]
for r in radii:
base_color = random.choice(colors)
# Random position for the break (in radians)
break_pos = random.uniform(0, 2 * math.pi)
gap_half_width = 0.05 # Size of the break
# We draw the circle by connecting tiny segments
# 1000 steps ensures the line looks smooth
steps = 1000
for i in range(steps):
# Current angle
angle = (i / steps) * 2 * math.pi
# Calculate distance from the break to determine alpha
# This logic finds the shortest angular distance to the break
diff = abs(angle - break_pos)
if diff > math.pi:
diff = 2 * math.pi - diff
# If we are inside the "break" zone, don't draw
if diff < gap_half_width:
continue
# Normalize distance for fading:
# Near the break (diff approx gap_half_width) -> Alpha 255 (100%)
# Far from break (diff approx PI) -> Alpha 127 (50%)
# Linear interpolation:
alpha_ratio = 1.0 - (1.5 * (diff / math.pi))
alpha = max(min(int(255 * alpha_ratio), 255), 0)
# Calculate coordinates
x = center + r * math.cos(angle)
y = center + r * math.sin(angle)
# Draw a tiny point/rectangle to represent the segment
# We use a small offset to give the line some thickness
thickness = 5
draw.ellipse([x - thickness, y - thickness, x + thickness, y + thickness],
fill=(base_color[0], base_color[1], base_color[2], alpha), outline=None)
print("Showing..")
image = Image.alpha_composite(background, foreground)
image.show()
draw_concentric_circles()
Which outputs: