Skip to main content

ShadowEffects — wiki

This module requires Pygame 1.8 and either Numeric or NumPy.

# shadows.py module
"""Tools for adding a shadow effect to pygame images

Exports make_shadow, make_shadow_opaque, place_shadow, add_shadow.

pygame.display.set_mode() must be called before this module's functions
can be used.
"""
import pygame
_arraytype = pygame.surfarray.get_arraytype()
if _arraytype == 'numeric':
    from Numeric import UInt8 as uint8, minimum, array, Float32 as float32, Int32 as int32
elif _arraytype == 'numpy':
    from numpy import uint8, minimum, array, float32, int32
else:
    raise TypeError("Unrecognized surfarray array type %s" % _arraytype)

def make_shadow(image, ambience=None):
    """Return a shadow representation of image for a given ambient lighting

    image - foreground image.
    ambience (optional) - 0.0 to 1.0: Ambient light ratio. 0.0 gives a
                          totally black shadow while 1.0 gives no shadow.
                          Defaults to 0.0 .
    """
    if ambience is None:
        ambience = 0.0
    elif not (0.0 <= ambience <= 1.0):
        raise ValueError("ambience must be between 0.0 and 1.0 inclusive")
    if image.get_masks()[3] != 0:
        image_alpha = pygame.surfarray.pixels_alpha(image)
        if ambience > 0.0:
            shadow_alpha = (image_alpha *
                            (1.0 - ambience)).astype(uint8)
        else:
            shadow_alpha = image_alpha
    elif image.get_colorkey() is not None:
        image_alpha = pygame.surfarray.array_colorkey(image)
        image.unlock(); image.unlock()  # pygame 1.7 bug (fixed in 1.8).
        surface_alpha = image.get_alpha()
        if surface_alpha is not None:
            # Do what array_colorkey should have done: use surface alpha!
            minimum(image_alpha, surface_alpha, image_alpha)
        if ambience > 0.0:
            shadow_alpha = (image_alpha *
                            (1.0 - ambience)).astype(uint8)
        else:
            shadow_alpha = image_alpha
    else:
        image_alpha = image.get_alpha()
        if image_alpha is None:
            image_alpha = 255
        shadow_alpha = int(image_alpha * (1.0 - ambience))
    shadow = image.convert_alpha()
    shading = pygame.Surface(shadow.get_size(), pygame.SRCALPHA, 32)
    pygame.surfarray.pixels_alpha(shading)[...] = image_alpha
    shadow.blit(shading, (0, 0))
    pygame.surfarray.pixels_alpha(shadow)[...] = shadow_alpha
    return shadow

def make_shadow_opaque(size, ambience=None, pixel_size=None):
    """Return a retangular shadow for the given ambient lighting

    size - (width, height): shadow dimensions.
    ambience (optional) - 0.0 to 1.0: Ambient light ratio. 0.0 gives a
                          totally black shadow while 1.0 gives no shadow.
                          Defaults to 0.0 .
    pixel_size (optional) - bits per pixel - defaults to screen value.

    This version is provided for performance.
    """
    if ambience is None:
        ambience = 0.0
    if pixel_size is None:
        rest = ()
    else:
        rest = (pixel_size,)
    shadow = pygame.Surface(size, 0, rest)
    shadow.set_alpha(255 * (1.0 - ambience))
    return shadow
    
def place_shadow(image, shadow, shadow_offset):
    """Return a surface that combines the image and shadow

    image - foreground image.
    shadow - image shadow.
    shadow_offset - (dx, dy) amount, in pixel, to shift
                    shadow center relative to image center.
    """
    image_rect = image.get_rect()
    shadow_rect = shadow.get_rect()
    shadow_rect.center = image_rect.center
    shadow_rect.move_ip(shadow_offset)
    rect = image_rect.union(shadow_rect)
    result = pygame.Surface(rect.size, pygame.SRCALPHA, 32)
    result.blit(shadow, (shadow_rect.left - rect.left,
                         shadow_rect.top - rect.top))
    result.blit(image, (image_rect.left - rect.left,
                        image_rect.top - rect.top))
    return result

def add_shadow(image, shadow_offset, shadow_scale=None, ambience=None):
    """Return a copy of image with a shadow added

    image - a surface with or without alpha or colorkey.
    shadow_offset - (dx, dy) amount, in pixel, to shift
                    shadow center relative to image center.
    shadow_scale (optional) - amount by which to scale the shadow.
                              defaults to 1.0 (no scaling).
    ambience (optional) - 0.0 to 1.0: Ambient light ratio. 0.0 gives a
                          totally black shadow while 1.0 gives no shadow.
                          Defaults to 0.0 .
    """
    if (image.get_flags() & pygame.SRCALPHA or
        image.get_colorkey() is not None):
        shadow = make_shadow(image, ambience)
        if shadow_scale is not None:
            size = (array(shadow.get_size(), float32) * shadow_scale).astype(int32)
            shadow = pygame.transform.smoothscale(shadow, size)
    else:
        size = (array(image.get_size(), float32) * shadow_scale).astype(int32)
        if shadow_scale is None:
            shadow_scale = 1.0
        shadow = make_shadow_opaque(size, ambience, image.get_bitsize())
    return place_shadow(image, shadow, shadow_offset)

__all__ = ['make_shadow', 'make_shadow_opaque', 'place_shadow', 'add_shadow']

Manipulating a surface's alpha values is one use for Pygame's surfarray module and an array package like Numeric or NumPy. The alpha values of a surface can be isolated as a two dimensional array of bytes.

image_alpha = pygame.surfarray.pixels_alpha(image)
Array operations can be performed on the alpha bytes.
shadow_alpha = (image_alpha *
                            (1.0 - ambience)).astype(uint8)
This multiplies all alpha bytes in the array by a floating point value. The intermediate floating point array is then cast into the final array of single byte integers. Finally, a surface's alpha values can be replaced with new values using an assignment to an array slice.
pygame.surfarray.pixels_alpha(shadow)[...] = shadow_alpha

Below is a demonstration program that adds shadows to text.

<code class="python">
import pygame, sys
try:
    pygame.surfarray.use_arraytype(sys.argv[1])
except IndexError:
    pass
except ValueError:
    print ("Unknown array type %s. Valid types are %s." %
           (sys.argv[1], ", ".join(pygame.surfarray.get_arraytypes())))
    sys.exit()
from pygame.locals import *
from shadows import *

def main():
    screen_size = (400, 200)
    pygame.init()
    screen = pygame.display.set_mode(screen_size)
    screen.fill((255, 255, 200, 255))
    rect = screen.get_rect()
    for i in range(3):
        rect.inflate_ip(-60, -60)
        pygame.draw.rect(screen, Color('gray'), rect, 1)
    pygame.display.flip()

    font = pygame.font.SysFont([], 24)
    ambience = 0.4
    textA = font.render("Per-pixel alpha, ambience %.1f" % ambience,
                        True,
                        Color('blue'))
    labelA = add_shadow(textA, (20, 10), shadow_scale=0.8, ambience=ambience)

    ambience = 0.2
    alpha = 180
    textB = font.render("Surface alpha %i, ambience %.1f" % (alpha, ambience),
                        True,
                        Color('red'), Color('white'))
    textB.set_alpha(alpha)
    labelB = add_shadow(textB, (20, 10), shadow_scale=0.8, ambience=ambience)

    ambience = 0.2
    textC = font.render("No alpha, ambience %.1f" % ambience,
                        True,
                        Color('white'), Color('black'))
    labelC = add_shadow(textC, (20, 10), shadow_scale=0.8, ambience=ambience)

    ambience = 0.8
    textD = font.render("Colorkey, ambience %.1f" % ambience,
                        True,
                        Color('white'), Color('black'))
    textD.set_colorkey(Color('white'))
    labelD = add_shadow(textD, (20, 10), shadow_scale=0.8, ambience=ambience)

    screen.blit(labelA, (50, 20))
    screen.blit(labelB, (50, 67))
    screen.blit(labelC, (50, 114))
    screen.blit(labelD, (50, 160))

    pygame.event.set_blocked(MOUSEMOTION)
    repeat = 1
    while repeat:
        for e in pygame.event.get():
            if e.type in [pygame.QUIT, pygame.MOUSEBUTTONDOWN]:
            	repeat = 0
                break
            elif e.type == pygame.KEYDOWN:
                key = e.key
                if key == K_q or key == K_ESCAPE:
                    repeat = 0
                    break
    
        pygame.display.flip()

if __name__ == '__main__':
    main()

This is the sampling of shadow effects it displays.