1

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:

enter image description here

1 Answer 1

1

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:

Concentric circles fading into the background

Sign up to request clarification or add additional context in comments.

1 Comment

Perfect - thanks! :)

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.