This program was a sketch for the little bursts in the music sequencer,
but the background makes for an interesting effect.
#!/usr/bin/python
# Draw a cool burst on the screen.
# Has the following interesting features:
# - The area covered by any individual color remains constant as the
# burst expands, once it's out far enough that there's an empty part
# in the middle. I think.
# - Nicely alpha-blends with background.
# - Starts expanding rapidly and then slows down a bit.
# - Renders at about 60fps on my laptop in 320x240, or 30fps in 640x480
# Currently has the following problems:
# - the burst surface is bigger than needed
# - I don't understand how to adjust the clock to do the right thing.
# - I'd like the palette to make a nice gradual transition from white,
# down to red, down to yellow, and then down to transparent.
# - Too symmetrical.
# - The code is too wordy.
import pygame, Numeric, time, math
burstsize = 40
fuzz = 20
def scale(mask, component):
"Encode a floating-point color component in [0, 1] with a bitmask."
mask = mask % (2**32) # make unsigned
rv = int(mask * component) & mask # scale mask by weight
if rv >= 2**31: rv -= 2**32 # convert back to signed
rv = int(rv) # convert back to non-long
return rv
def pixel(masks, components):
"Encode an (r, g, b, alpha) tuple according to given masks."
return sum(map(scale, masks, components))
class World:
def __init__(self, screen):
self.screen = screen
# make a simple, pretty background.
bxs, bys = Numeric.indices(self.screen.get_size())
self.background = bxs * bys * bys / 128
surfsize = (burstsize+fuzz)*2
self.burstsurface = pygame.Surface((surfsize,surfsize)).convert_alpha()
xs, ys = Numeric.indices(self.burstsurface.get_size())
(dx, dy) = (xs - surfsize/2, ys - surfsize/2) # distances from center
self.rsq = dx*dx + dy*dy # square of distances from center
# Compute where to blit the surface
cx, cy = self.screen.get_rect().center
self.blitdest = (cx - surfsize/2, cy - surfsize/2)
# Red and green stay at 100%; blue fades from 80% down to 0,
# and alpha fades from 90% down to 0. Then the last palette
# value is entirely transparent.
masks = self.burstsurface.get_masks()
self.palette = Numeric.array([pixel(masks, (1, 1,
0.8 *
float(fuzz**2-ii)/fuzz**2,
0.9 *
float(fuzz**2-ii)/fuzz**2))
for ii in range(fuzz**2)] + [0])
self.palette = self.palette.astype(Numeric.Int32) # just to make sure
def redraw(self):
pygame.surfarray.blit_array(self.screen, self.background)
# The threshold value of r-squared with maximum brightness. I
# fiddled with this until it was kind of OK, but I don't
# really like it.
clock = burstsize**2 - ((-time.time() / 3 % 2 - 1) * burstsize)**2
# the per-pixel distance from that threshold
burst = Numeric.absolute(self.rsq - clock)
# mapped to fit the palette
burst_clamped = Numeric.clip(burst, 0, fuzz**2).astype(Numeric.Int)
burst_image = Numeric.take(self.palette, burst_clamped)
pygame.surfarray.blit_array(self.burstsurface, burst_image)
self.screen.blit(self.burstsurface, self.blitdest)
def main():
pygame.init()
# It gets really trippy if you specify certain bit depths in place
# of 0. Like 22. I assume this is some kind of SDL bug, but it's
# really cool.
screen = pygame.display.set_mode((320, 240), pygame.FULLSCREEN, 0)
world = World(screen)
frames = 0
start = time.time()
while 1:
ev = pygame.event.poll()
if ev.type == pygame.NOEVENT:
world.redraw()
pygame.display.flip()
frames += 1
elif ev.type == pygame.QUIT: break
elif ev.type == pygame.MOUSEBUTTONDOWN: break
dur = time.time() - start
print "%d frames; %.2f/sec" % (frames, frames/dur)
if __name__ == '__main__': main()