1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
|
#!/usr/bin/python
"""
__version__ = "$Revision: 1.6 $"
__date__ = "$Date: 2004/10/10 01:20:20 $"
"""
# KEA 2004-09-25
# based on the excellent work by Keith Peters
# http://www.bit-101.com/tutorials/gravity.html
# I started with the doodle sample, so this sample still
# contains some cruft
# the big missing item is that I'm not currently doing hit
# detection on the individual balls so they go right through each
# other. I guess that needs to be part of the move method
# with a reference back to the sprites list, but I have
# a feeling it will be more complicated because if ball 1
# runs into ball 2, then they both need to react...
from PythonCard import clipboard, dialog, graphic, model, util
import wx
import os
import random
# helper functions
def pointInRect(xy, rect):
x, y = xy
left, top, right, bottom = rect
if x >= left and x <= right and y >= top and y <= bottom:
return True
else:
return False
def randomColor():
return (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
class Sprite(object):
def __init__(self, canvas):
self.canvas = canvas
class BallSprite(Sprite):
def __init__(self, canvas, x, y, radius, color='red'):
Sprite.__init__(self, canvas)
self.x = x
self.y = y
self.oldx = x
self.oldy = y
self.radius = radius
self.color = color
self.xspeed = random.random() * 30
self.yspeed = random.random() * 30
self.gravity = 2
self.drag = .98
self.bounce = .9
self.dragging = False
def move(self):
# this is sort of pointless right now
# but maybe the dragging code could be moved
# into the class in which case we might need
# to know if we're dragging
if not self.dragging:
self.x += self.xspeed
self.y += self.yspeed
rightedge, bottomedge = self.canvas.size
# bounce when we hit the edge
if (self.x - self.radius) < 0:
self.x = self.radius
self.xspeed = -self.xspeed * self.bounce
elif (self.x + self.radius) > rightedge:
self.x = rightedge - self.radius
self.xspeed = -self.xspeed * self.bounce
if (self.y - self.radius) < 0:
self.y = self.radius
self.yspeed = -self.yspeed * self.bounce
elif (self.y + self.radius) > bottomedge:
self.y = bottomedge - self.radius
self.yspeed = -self.yspeed * self.bounce
self.xspeed = self.xspeed * self.drag
self.yspeed = self.yspeed * self.drag + self.gravity
else:
self.xspeed = self.x - self.oldx
self.yspeed = self.y - self.oldy
self.oldx = self.x
self.oldy = self.y
#print "speed", self.xspeed, self.yspeed
self.draw()
def draw(self):
self.canvas.fillColor = self.color
#self.canvas.foregroundColor = self.color
self.canvas.drawCircle((self.x, self.y), self.radius)
class Gravity(model.Background):
def on_initialize(self, event):
self.x = 0
self.y = 0
self.dragging = False
self.animate = False
self.filename = None
# leave it black for now
#self.components.bufOff.foregroundColor = self.color
self.sprites = []
self.initSizers()
# go ahead and create one ball to get started
# if you want to stress the animation loop
# uncomment the loop below
# my Win2K box can do approximately 130 balls and still
# maintain 30 (29.9...) FPS, but above that and it starts
# to slow down
## for i in range(130):
## self.sprites.append(BallSprite(self.components.bufOff, 20, 20, 15, randomColor()))
self.on_btnNewBall_mouseClick(None)
def initSizers(self):
sizer1 = wx.BoxSizer(wx.VERTICAL)
sizer2 = wx.BoxSizer(wx.HORIZONTAL)
comp = self.components
flags = wx.ALL | wx.ALIGN_CENTER_VERTICAL
padding = 7
sizer2.Add(comp.btnNewBall, 0, flags, padding)
sizer2.Add(comp.btnAnimate, 0, flags, padding)
sizer2.Add(comp.btnStop, 0, flags, padding)
sizer1.Add(sizer2, 0)
sizer1.Add(comp.bufOff, 1, wx.EXPAND)
sizer1.Fit(self)
sizer1.SetSizeHints(self)
self.panel.SetSizer(sizer1)
self.panel.SetAutoLayout(1)
self.panel.Layout()
def on_btnNewBall_mouseClick(self, event):
canvas = self.components.bufOff
self.sprites.append(BallSprite(canvas, 20, 20, 15, randomColor()))
if not self.animate:
canvas.clear()
for ball in self.sprites:
ball.draw()
canvas.redraw()
self.statusBar.text = "Balls: %d" % len(self.sprites)
def on_btnAnimate_mouseClick(self, event):
event.target.enabled = False
canvas = self.components.bufOff
canvas.autoRefresh = False
self.animate = True
avgfps = 0
frame = 0
seconds = util.time()
# number of frames to display a second
# on faster boxes this will throttle the number
# of frames calculated and displayed, but on slower
# boxes it won't prevent slowdown
fps = 30
# amount of time that should pass before drawing another frame
timePerFrame = 1.0 / fps
#print timePerFrame
# hack to force a difference
# between time and startTime
# to avoid a division by zero exception on fast machines
# aka my Windows box ;-)
startTime = util.time() - 0.001
while self.animate:
newSeconds = util.time()
if newSeconds - seconds >= timePerFrame:
seconds = newSeconds
canvas.clear()
for ball in self.sprites:
ball.move()
if wx.Platform == '__WXMAC__':
canvas.redraw()
else:
canvas.refresh()
frame += 1
self.statusBar.text = "Average FPS: %.4f Balls: %d" % (frame / (seconds - startTime), len(self.sprites))
# give the user a chance to click Stop
wx.SafeYield(self, True)
def on_btnStop_mouseClick(self, event):
self.animate = False
self.components.btnAnimate.enabled = True
def on_close(self, event):
self.animate = False
event.skip()
def on_bufOff_mouseDown(self, event):
#event.target.drawLine((self.x, self.y), (self.x + 1, self.y + 1))
# figure out which ball the user clicked on, if any
# i want to search the list from back to front
# but I don't change the list in place, so I make a copy
# is there a way to iterate over a list in reverse without
# using something awkward like
# for i in range(len(self.sprites) - 1, -1, -1):
sprites = self.sprites[:]
sprites.reverse()
for ball in sprites:
radius = ball.radius
x = ball.x
y = ball.y
#print "point", event.position
#print "rect", x - radius, y - radius, x + radius, y + radius
if pointInRect(event.position, (x - radius, y - radius, x + radius, y + radius)):
self.dragging = ball
ball.dragging = True
x, y = event.position
self.dx = ball.x - x
self.dy = ball.y - y
#print self.dx, self.dy
break
# on a mouseLeave we will no longer
# get drag messages, so we could end dragging
# at that point, just keep the current behavior
def on_bufOff_mouseDrag(self, event):
if self.dragging:
x, y = event.position
ball = self.dragging
ball.x = x + self.dx
ball.y = y + self.dy
# reraw all the objects
self.components.bufOff.clear()
for ball in self.sprites:
ball.move()
if wx.Platform == '__WXMAC__':
self.components.bufOff.redraw()
else:
self.components.bufOff.refresh()
def on_bufOff_mouseUp(self, event):
if self.dragging:
self.dragging.dragging = False
self.dragging = None
# older Doodle code
# I'm keeping it around in case I need it later
def on_bufOff_mouseEnter(self, event):
self.x, self.y = event.position
## def on_bufOff_mouseDown(self, event):
## self.x, self.y = event.position
## event.target.drawLine((self.x, self.y), (self.x + 1, self.y + 1))
##
## def on_bufOff_mouseDrag(self, event):
## x, y = event.position
## event.target.drawLine((self.x, self.y), (x, y))
## self.x = x
## self.y = y
def on_btnColor_mouseClick(self, event):
result = dialog.colorDialog(self, color=self.color)
if result.accepted:
#self.components.bufOff.foregroundColor = result.color
event.target.backgroundColor = result.color
self.color = result.color
def openFile(self):
result = dialog.openFileDialog(None, "Import which file?")
if result.accepted:
path = result.paths[0]
os.chdir(os.path.dirname(path))
self.filename = path
bmp = graphic.Bitmap(self.filename)
self.components.bufOff.drawBitmap(bmp, (0, 0))
def on_menuFileOpen_select(self, event):
self.openFile()
def on_menuFileSaveAs_select(self, event):
if self.filename is None:
path = ''
filename = ''
else:
path, filename = os.path.split(self.filename)
result = dialog.saveFileDialog(None, "Save As", path, filename)
if result.accepted:
path = result.paths[0]
fileType = graphic.bitmapType(path)
print fileType, path
try:
bmp = self.components.bufOff.getBitmap()
bmp.SaveFile(path, fileType)
return 1
except:
return 0
else:
return 0
def on_menuEditCopy_select(self, event):
clipboard.setClipboard(self.components.bufOff.getBitmap())
def on_menuEditPaste_select(self, event):
bmp = clipboard.getClipboard()
if isinstance(bmp, wx.Bitmap):
self.components.bufOff.drawBitmap(bmp)
def on_editClear_command(self, event):
self.components.bufOff.clear()
if __name__ == '__main__':
app = model.Application(Gravity)
app.MainLoop()
|