Quick answer: Per-pixel alpha surfaces ignore set_alpha. Fade with a temp surface filled (255,255,255,alpha) and BLEND_RGBA_MULT.
A menu screen fades from black using a PNG logo. Surface.set_alpha(128) does nothing — logo stays fully opaque. The PNG converted to per-pixel alpha; set_alpha doesn’t apply.
The Trick: Multiply Alpha
def draw_with_fade(screen, image, pos, alpha):
if alpha >= 255:
screen.blit(image, pos)
return
temp = image.copy()
temp.fill((255, 255, 255, alpha), special_flags=pygame.BLEND_RGBA_MULT)
screen.blit(temp, pos)
Copy the image, multiply with desired alpha across all pixels. RGB stays unchanged; alpha scaled.
Performance Note
Copy+fill+blit per frame for a fade. For a static fade (menu intro), pre-compute the faded surface. For dynamic alpha (e.g., per-frame), it’s ~O(width×height) work.
Numpy Variant
import pygame.surfarray as sa
import numpy as np
def set_pixel_alpha(surface, factor):
arr = sa.pixels_alpha(surface)
arr[:] = (arr * factor).astype(np.uint8)
del arr
Direct numpy access; faster for large surfaces. Modifies in place — copy first if you don’t want to mutate the original.
Loss-Less Repeated Fade
If you call this each frame with diminishing factor, the original alpha gets multiplied repeatedly — lossy. Keep a master copy:
master = image.copy()
while running:
faded = master.copy()
set_pixel_alpha(faded, current_factor)
screen.blit(faded, pos)
Or Avoid Per-Pixel Alpha
For sprites that don’t need per-pixel transparency (just colorkey + global alpha), use convert() + set_colorkey() + set_alpha(). Simpler.
Verifying
Animate alpha from 0 to 255. Logo smoothly fades in. No flicker or banding (assuming linear alpha range). Frame time within target.
“Per-pixel alpha and global alpha don’t mix. Pick one mode per surface, or multiply manually.”
For UI especially, batch your fadeable surfaces — one temp surface per group is faster than per-element copies.