""" bubblePaint_0_1_3.py Eric Pavey - 2009-09-03 warpcat@sbcglobal.net www.akeric.com Make bubbles!!! updates: v.0.1.3 * Bubbles inherit motion vectors from mouse action ToDo: * "settle dry" behavior * Create "push brush" Maybe do, maybe not: * Dry to separate layers (maybe not, bad performance) * Images for bubbles (maybe not? (slow)) * Audio for bubble creation, drying, and undoing (maybe not, sounds funny). Directions: * LMB draws bubbles * MMB-hold-move up\down: changes bubble size * RMB-hold-move up\down: change brush hue * Mouse-wheel: change pressure of bubbles (number applied at once) * 's' will save a test.png image * 'c' will clear the screen * 'n' will start a new session * 'd' will 'dry' the bubbles to the background, allowing painting on top * 'g' toggles on\off gravity * 'p' will update the palette of the brush, based on what's under the mouse. * 'ctrl+z' Undo \ remove bubbles in order of creation (no redo) * 'arrows' defines direction of gravity, and turns it on. """ __version__ = '0.1.3' #-------------- # imports & initis import random import math import glob import copy import sys import os import re import pygame from pygame.locals import * import pymunk from pymunk import Vec2d as Vec2d pygame.init() pymunk.init_pymunk() #-------------- # Constants and Global Vars FRAMERATE = 30 # Control the gravity of the scene: GRAV = 10 # damping: 0 = full damping, 1 = no damping (seems backwards...) DAMPING = .05 # make our bubbles get more of the mouse motion: BUBBLE_VEL_MULT = 1 #---------------- # classes and functions: class Bubble(object): """ A 'dynamic bubble object' displayed to the screen. """ def __init__(self, drawSurf, drySurf, space, radius, color, pos=(0,0), impulse=(0,0), friction=0): """ drawSurf : pygame.Surface that this bubble will draw to (main screen). drySurf : pygame.Surface that this bubble will 'dry' to (background surface) space : pymunk.Space radius : float pos : (#, #) : Starting (x, y) position in space impulse : (#, #) : vector to apply to the initial velocity of our bubbles, to make them 'move' a bit based on the mouse motion at time of creation. color : pygame.Color. Will be duplicated for this object. friction : float : 0->1.0 (usefull?) """ self.drawSurf = drawSurf self.drySurf = drySurf self.surfWidth, self.surfHeight = self.drawSurf.get_size() # Update PyMunk stuff:-------------------- self.space = space self.radius = random.uniform(.85, 1.15)*radius self.mass = math.pi * pow(radius, 2) # Make inertia: self.inertia = pymunk.moment_for_circle(self.mass, 0, self.radius, (0,0)) # Create rigid body: self.body = pymunk.Body(self.mass, self.inertia) # Set init posiiton: self.body.position = pos # Add collision shape self.shape = pymunk.Circle(self.body, self.radius, (0,0)) self.shape.friction = friction # Add both body and shape to our space for sim self.space.add(self.body, self.shape) # Give our bubbles some default motion based on the mouse direction: # Remembe to flip the y axis.... self.body.velocity = Vec2d((impulse[0], -impulse[1]))*BUBBLE_VEL_MULT # Add shading info:----------------------- self.color = self.colorAdjust(color) def __del__(self): # If this bubble is deleted, also remove it's pymunk goodies from the current space: self.space.remove(self.shape, self.body) def draw(self, debug=False): # Draw to given surface (probably screen) # Used every frame to dry bubbles #self.body.apply_force(self.impulse*20) pos = (int(self.body.position.x), self.surfHeight-int(self.body.position.y)) pygame.draw.circle(self.drawSurf, self.color, pos, int(self.radius), 0) end = (math.cos(self.body.angle)*self.radius+pos[0], -math.sin(self.body.angle)*self.radius+pos[1]) if debug: # use to debug draw rotation lines: pygame.draw.line(self.drawSurf, complimentary_color(self.color), pos, end) def dry(self): # Draw to given surface (probably background surface) # Used when "drying" the bubbles to the background surface. pos = (int(self.body.position.x), self.surfHeight-int(self.body.position.y)) pygame.draw.circle(self.drySurf, self.color, pos, int(self.radius), 0) def boundsCheck(self): # Test to see if out of bounds of screen or not if -self.radius > self.body.position.x \ or self.body.position.x > self.surfWidth+self.radius \ or -self.radius > self.body.position.y \ or self.body.position.y > self.surfHeight+self.radius: return True else: return False def colorAdjust(self, color, modAmt=10): # subtly modifiy our color when painting. currnetly only modifies hue copyCol = copy.copy(color) hsva = copyCol.hsva mod = random.uniform(-modAmt,modAmt) newHue = int((hsva[0]+mod)%360) try: copyCol.hsva = (newHue, 100, hsva[2], hsva[3]) except ValueError, e: print "Applied Color Values (something is wrong with one or more):", (newHue, hsva[1], hsva[2], hsva[3]) print e return Color(copyCol.r, copyCol.g, copyCol.b, copyCol.a) class BubbleBrush(object): """ Create a representation of the brush that bubbles will be 'drawn with' on screen. Will follow the position of the mouse. """ def __init__(self, surface, drySurf, space, color, bubbleGroup, pressure=1, radius=64, ): """ surface : pygame.Surface to draw to. drySurf : pygame.Surface that bubbles this brush creates will 'dry' to (background surface) space : pymunk.Space color : pygame.Color. This color is expected to change external to this object, thus modifying the object on the fly. bubbleGroup : list : the Bubbles created by the brush are added to this for drawing. events : pygame.event.get() : passed in to parse events pressure : int : how many bubbles to make at once. radius : float """ self.surface = surface self.drySurf = drySurf self.surfWidth, self.surfHeight = self.surface.get_size() self.space = space self.color = color self.bubbleGroup = bubbleGroup self.pressure = pressure self.radius = radius self.pos = pygame.mouse.get_pos() self.relPos = pygame.mouse.get_rel() self.minRadius = 3.0 self.maxRadius = self.surfHeight / 4.0 def set_events(self, events): self.events = events def drawBrush(self): # Draw self to surface at mouse position. self.pos = pygame.mouse.get_pos() self.relPos = pygame.mouse.get_rel() pygame.draw.circle(self.surface, self.color, self.pos, int(self.radius), 0) pygame.draw.circle(self.surface, 0, self.pos, int(self.radius), 2) # Event Detection---------------------------- mouseButtons = pygame.mouse.get_pressed() # If the LMB is held, draw circles: if mouseButtons[0]: self.paint() # If the MMB is held, change radius: if mouseButtons[1]: self.radius = self.radius - self.relPos[1]*.5 if self.radius > self.maxRadius: self.radius = self.maxRadius if self.radius < self.minRadius: self.radius = self.minRadius # If the RMB is held, change color: if mouseButtons[2]: self.colorMod() # mouse wheel, for changing 'pressure' of brush: for event in self.events: if event.type == MOUSEBUTTONDOWN: if event.button == 4: self.pressure = self.pressure+1 if self.pressure > 10: self.pressure = 10 if event.button == 5: self.pressure = self.pressure-1 if self.pressure < 1: self.pressure = 1 def colorMod(self): # Change the brush color based on mouse motion hsva = self.color.hsva newHue = int((hsva[0]+self.relPos[1])%360) newVal = int(hsva[2]+self.relPos[0]) if newVal > 100: newVal = 100 if newVal < 0: newVal = 0 self.color.hsva = (newHue, 100, newVal, hsva[3]) def paint(self): """ paint bubbles to the screen. """ for i in range(self.pressure): # we modify the Y axis, because PyMunk coords are different from PyGame. randX = random.uniform(-2, 2) randY = random.uniform(-2, 2) pos = (self.pos[0] + randX, self.pos[1] + randY) self.bubbleGroup.append( Bubble(self.surface, self.drySurf, self.space, self.radius, self.color, (pos[0], self.surfHeight-pos[1]), self.relPos) ) def getColorUnder(self): # set the current color to the color under the brush self.color = self.surface.get_at(self.pos) def getComplimentary(self): # get the complimentary ('opposite') color based on the current color of the brush. self.color = complimentary_color(self.color) class DrawSurf(object): """ Create a surface used for drawing to the screen. """ def __init__(self, destSurf, color=None): self.color = color self.destSurf = destSurf self.surface = self.makeSurf() def makeSurf(self): """ Make a new pygame.Surface th same size as the given destSurf """ layer = None if self.color is not None: layer = pygame.Surface(self.destSurf.get_size(), flags=pygame.HWSURFACE) layer = layer.convert() layer.fill(self.color) else: layer = pygame.Surface(self.destSurf.get_size(), flags=pygame.SRCALPHA|pygame.HWSURFACE, depth=32) layer.convert_alpha() layer.fill((0,0,0,0)) return layer def draw(self): # Draw this surface to the defined drawSurface self.destSurf.blit(self.surface, (0,0)) def update(self, color): # used when updating the color of the bg (will clear any blitted info to it) self.color = color self.surface.fill(self.color) def complimentary_color(col): # Given a color, find the complimentary color to it. newC = Color(col.r, col.g, col.b) hsva = newC.hsva newHue = (hsva[0]+180)%360 newC.hsva = (newHue, hsva[1], hsva[2], hsva[3]) return newC def setupOverlay(overlay, font): """ Draw text to our overlay screen. """ width, height = overlay.get_size() #bubble = font.render("-- BubblePaint "+__version__+" -- ", 1, Color("white")) toggleText = font.render("t : Toggle this information screen", 1, Color("white")) resize = font.render("Window is resizable", 1, Color("white")) mouse = font.render("Expects a three-button mouse, with mouse wheel. Can't vouch for results otherwise :)", 1, Color("white")) lmb = font.render("LMB : Draw Bubbles", 1, Color("white")) mmb = font.render("MMB-hold-move up/down : changes bubble size", 1, Color("white")) rmb1 = font.render("RMB-hold-move up/down : change brush *hue*", 1, Color("white")) rmb2 = font.render("RMB-hold-move left/right : change brush *value* (light to dark)", 1, Color("white")) mouseWheel = font.render("Mouse-wheel : change the 'spray pressure' of bubbles (number applied at once)", 1, Color("white")) multiMouse = font.render(" -- You can hold down multiple mouse buttons at the same time while drawing to get different effects", 1, Color("white")) s = font.render("'s' : Save a 'bubblePaint.####.png' image in the application directory...", 1, Color("white")) s2 = font.render(" ...based on the current resolution of the window", 1, Color("white")) n = font.render("'n' : Start a new session (be careful, will clear *everything*)", 1, Color("white")) c = font.render("'c' : Clear the *dynamic* bubbles. Don't modify the background", 1, Color("white")) d = font.render("'d' : 'Dry' the dynamic bubbles to the background, allowing painting on top", 1, Color("white")) p = font.render("'p' : Will update the palette of the brush, based on what color is under the brush", 1, Color("white")) o = font.render("'o' : Will swap the current brush color with its complimentary (opposite) color", 1, Color("white")) b = font.render("'b' : Set the background color to the current brush color (will clear the background of any *dried* bubbles)", 1, Color("white")) g = font.render("'g' : Toggles on\off 'gravity'. Default direction is 'down'", 1, Color("white")) arrows = font.render("'arrows' : Defines direction of gravity, and turns gravity on", 1, Color("white")) ctrlz = font.render("'ctrl+z' : Undo \ remove bubbles in order of creation (no redo)", 1, Color("white")) esc = font.render("'Esc' : Exit", 1, Color("white")) pyVer = font.render("Running Python version: "+str(sys.version.split(" ")[0]), 1, Color("white")) pygVer = font.render("Running PyGame version: "+str(pygame.ver), 1, Color("white")) pyMuk = font.render("Running PyMunk version: "+str(pymunk.version), 1, Color("white")) class Row(object): # class to store the current row location on our UI # Each time it is called, it will increment its value def __init__(self, val): self._val = 0 self.orig = val @property def val(self): self._val = self._val + self.orig return self._val row = Row(16) #overlay.blit(bubble, (8, row.val)) overlay.blit(toggleText, (8, row.val)) overlay.blit(resize, (8, row.val)) overlay.blit(mouse, (8, row.val)) row.val overlay.blit(lmb, (8, row.val)) overlay.blit(mmb, (8, row.val)) overlay.blit(rmb1, (8, row.val)) overlay.blit(rmb2, (8, row.val)) overlay.blit(mouseWheel, (8, row.val)) overlay.blit(multiMouse, (8, row.val)) row.val overlay.blit(s, (8, row.val)) overlay.blit(s2, (8, row.val)) overlay.blit(n, (8, row.val)) overlay.blit(c, (8, row.val)) overlay.blit(d, (8, row.val)) row.val overlay.blit(p, (8, row.val)) overlay.blit(o, (8, row.val)) overlay.blit(b, (8, row.val)) row.val overlay.blit(g, (8, row.val)) overlay.blit(arrows, (8, row.val)) row.val overlay.blit(ctrlz, (8, row.val)) overlay.blit(esc, (8, row.val)) overlay.blit(pyVer, (8, height-64)) overlay.blit(pygVer, (8, height-48)) overlay.blit(pyMuk, (8, height-32)) def updateStats(surface, font, clock, bubList, brush, useGrav): """ Update stats on the screen for the user as they paint. """ #surface.fill((0,0,0,0)) width, height = surface.get_size() fps = font.render("FPS : %.2f"%clock.get_fps(), 1, Color("white")) numBub = font.render("Num Bubbles : %s"%len(bubList), 1, Color("white")) sprayPress = font.render("Spray Pressure : %s"%brush.pressure, 1, Color("white")) gravState = "On" if useGrav != 1: gravState = "Off" grav = font.render("Gravity : %s"%gravState, 1, Color("white")) surface.blit(fps, (width-256, 16)) surface.blit(numBub, (width-256, 32)) surface.blit(sprayPress, (width-256, 48)) surface.blit(grav, (width-256, 64)) def saveImage(screen): """ Save the current image to the working directory of the program. """ currentImages = glob.glob(".\\*.png") numList = [0] for img in currentImages: i = os.path.splitext(img)[0] try: num = re.findall('[0-9]+$', i)[0] numList.append(int(num)) except IndexError: pass numList = sorted(numList) newNum = numList[-1]+1 saveName = 'bubblePaint.%04d.png' % newNum print "Saving %s" %saveName pygame.image.save(screen, saveName) def toggle_grav(space, useGrav, currentGrav): """ Used to turn on\off gravity. """ if useGrav: space.gravity = currentGrav else: space.gravity = (0.0, 0.0) #---------------- # Main program def main(): # Yes, this, is bubblepaint. print "Running Python version:", sys.version print "Running PyGame version:", pygame.ver print "Running PyMunk version:", pymunk.version print "Running BubblePaint version:", __version__ info = pygame.display.Info() # Make the default screen 3/4 the res of the computer screen: screen_size = (int(info.current_w*.75), int(info.current_h*.75)) #---------------------------------------------------------- # Top level objects that are passed around: # Color of the paint, that the bubbleBrush uses, and the current bubbles # to be painted: colorPaint = Color("orange") # Color of the background: colorBackground = complimentary_color(colorPaint) # our font: font = pygame.font.Font("Courier.ttf", 16) # Main Screen setup clock = pygame.time.Clock() pygame.mouse.set_visible(0) screen = pygame.display.set_mode(screen_size, pygame.RESIZABLE) pygame.display.set_caption("BubblePaint %s" % __version__) # make our background object: backgroundLayer = DrawSurf(screen, colorBackground) # pumunk setup space = pymunk.Space() space.damping = DAMPING # list that holds all of our active bubbles bubbleGroup = [] brush = BubbleBrush(screen, backgroundLayer.surface, space, colorPaint, bubbleGroup) displayOverlay = 1 useGrav = 0 grav_down = (0.0, -GRAV) grav_up = (0.0, GRAV) grav_left = (-GRAV, 0.0) grav_right = (GRAV, 0.0) currentGrav = grav_down #---------------------------------------------------------- # Main loop: looping = True while looping: # Lock framerate: clock.tick(FRAMERATE) # update our pymunk sim: space.step(1/float(FRAMERATE)) #---------------------------------------------------------- # detect for events doScreenshot = False getColor = False events = pygame.event.get() brush.set_events(events) for event in events: if event.type == pygame.QUIT: # allow for exit: looping = False elif event.type == pygame.VIDEORESIZE: # When resizing the UI: screen_size = event.size screen = pygame.display.set_mode(screen_size, pygame.RESIZABLE) oldBg = copy.copy(backgroundLayer.surface) backgroundLayer = DrawSurf(screen, backgroundLayer.color) # reapply the old background image, so we don't loose it: backgroundLayer.surface.blit(oldBg, (0,0)) #update our brush with the new screen size: brush.surfWidth, brush.surfHeight = screen_size # update all our bubbles with the new screen size: for b in bubbleGroup: b.surfWidth, b.surfHeight = screen_size elif event.type == KEYDOWN: if event.key == K_ESCAPE: # allow for exit: looping = False if event.key == K_s: # do screenshot doScreenshot = True if event.key == K_n: # start a new session for b in bubbleGroup[:]: bubbleGroup.pop() backgroundLayer.surface.fill(backgroundLayer.color) if event.key == K_c: # clear the screen of bubbles for b in bubbleGroup[:]: bubbleGroup.pop() if event.key == K_d: # dry bubbles to the background for b in bubbleGroup[:]: # need to update this based on possible screen resizing b.drySurf = backgroundLayer.surface b.dry() bubbleGroup.pop() if event.key == K_t: # toggle the UI display layers. displayOverlay = abs(displayOverlay-1) if event.key == K_g: useGrav = abs(useGrav-1) toggle_grav(space, useGrav, currentGrav) if event.key == K_UP: currentGrav = grav_up useGrav = 1 toggle_grav(space, 1, currentGrav) pass if event.key == K_DOWN: currentGrav = grav_down useGrav = 1 toggle_grav(space, 1, currentGrav) pass if event.key == K_LEFT: currentGrav = grav_left useGrav = 1 toggle_grav(space, 1, currentGrav) pass if event.key == K_RIGHT: currentGrav = grav_right useGrav = 1 toggle_grav(space, 1, currentGrav) pass if event.key == K_p: # if we're updating the 'palette' of the brush: getColor = True if event.key == K_o: # update the brush with complimentary color: brush.getComplimentary() if event.key == K_b: # Set background color to brush color: backgroundLayer.update(brush.color) # We do get_pressed here for our 'undo', since as long as the user holds the button down, # it will keep repeating. mods = pygame.key.get_mods() if pygame.key.get_pressed()[K_z]: if mods == KMOD_LCTRL or mods == KMOD_RCTRL: try: bubbleGroup.pop() except IndexError: pass #---------------------------------------------------------- # Draw backgroundLayer.draw() for bubble in bubbleGroup[:]: bubble.draw() if bubble.boundsCheck(): bubbleGroup.remove(bubble) if getColor: brush.getColorUnder() if doScreenshot: saveImage(screen) brush.drawBrush() if displayOverlay: setupOverlay(screen, font) updateStats(screen, font, clock, bubbleGroup, brush, useGrav) # update our display: pygame.display.flip() #------------ # Execution from shell\icon: if __name__ == "__main__": main()