PageRenderTime 37ms CodeModel.GetById 16ms app.highlight 11ms RepoModel.GetById 7ms app.codeStats 0ms

/robots/graphics/animation.py

https://bitbucket.org/lordmauve/metalwork
Python | 242 lines | 208 code | 30 blank | 4 comment | 38 complexity | 7267e32a68f2314c5d6e30b092636280 MD5 | raw file
  1import os
  2import os.path
  3import re
  4import pygame
  5
  6from pygame.rect import Rect
  7
  8
  9ANIMATIONS_ROOT = 'assets/anims'
 10IMAGES_ROOT = 'assets/images'
 11
 12def load_sprite(fname):
 13	return pygame.image.load(os.path.join(IMAGES_ROOT, fname)).convert_alpha()
 14
 15class AnimationLayer(object):
 16	def __init__(self, fname, num_sprites=1, offs=None):
 17		self.base = fname
 18		self.num_sprites = num_sprites
 19		if offs is not None and len(offs) > num_sprites:
 20			self.frames = len(offs)
 21		else:
 22			self.frames = num_sprites
 23		if self.frames < 1:
 24			raise ValueError("Invalid number of frames for animation layer %s" % fname)
 25
 26		sprites = [self.load_frame(f) for f in range(self.num_sprites)]
 27		self.sprites = []
 28		for i in range(self.frames):
 29			s = sprites[i % self.num_sprites]
 30			if offs is None:
 31				self.sprites.append((0, 0, s))
 32			elif offs[i] is None:
 33				self.sprites.append((0, 0, None))
 34			else:
 35				ox, oy = offs[i]
 36				self.sprites.append((ox, oy, s))
 37
 38	def load_frame(self, frame):
 39		if self.num_sprites < 2:
 40			return load_sprite('%s.png' % self.base)
 41		else:
 42			return load_sprite('%s-%d.png' % (self.base, frame))
 43
 44	def sprite(self, frame):
 45		s = frame % self.frames
 46		return self.sprites[s]
 47
 48	def translate(self, frame, dx, dy):
 49		s = frame % self.frames
 50		x, y, sprite = self.sprites[s]
 51		if sprite is None:
 52			return
 53		self.sprites[s] = x + dx, y + dy, sprite
 54
 55	def draw(self, screen, frame, pos):
 56		x, y, s = self.sprite(frame)
 57		if s is None:
 58			return
 59		screen.blit(s, (x + pos[0], y + pos[1]))
 60
 61	def bounds(self, frame):
 62		x, y, s = self.sprite(frame)
 63		if s is None:
 64			return Rect(0,0,0,0)
 65		return Rect((x, y), s.get_size())
 66
 67
 68class AnimatedSprite(object):
 69	class Instance(object):
 70		"""An instance of an animated sprite.
 71	
 72		The frame timings, layer visibility, etc, are here.
 73		"""
 74		def __init__(self, a, framedelay, loop):
 75			self.a = a
 76			self.ftime = 0
 77			self.currentframe = 0
 78			self.loop = loop
 79			self.framedelay = framedelay
 80			self.paused = False
 81			self.finished = False
 82			self.layer_vis = [True] * len(self.a.layers)
 83
 84			self.listeners = []
 85
 86		def pause(self):
 87			self.paused = True
 88
 89		def play(self):
 90			self.paused = False
 91
 92		def go(self, frame):
 93			if frame < 0:
 94				self.currentframe = (self.a.frames + frame) % self.a.frames
 95			else:
 96				self.currentframe = frame % self.a.frames
 97			
 98			fin = not self.loop and self.currentframe == (self.a.frames - 1)
 99			if fin and not self.finished:
100				self.finished = fin
101				self.fire_finish_event()
102
103		def set_layer_visible(self, layer, vis):
104			self.layer_vis[self.a._get_layer_id(layer)] = vis
105
106		def get_layer_offset(self, layer):
107			layer = self.a.layers[self.a._get_layer_id(layer)]
108			x, y, s = layer.sprite(self.currentframe)
109			return x, y
110
111		def bounds(self, frame=None):
112			if frame is None:
113				frame = self.currentframe
114			r = Rect(0, 0, 0, 0)
115			for i, l in enumerate(self.a.layers):
116				if not self.layer_vis[i]:
117					continue
118				b = l.bounds(frame)
119				r.union_ip(b)
120			return r
121
122		def draw(self, screen, x, y):
123			framedelay = self.framedelay
124
125			if self.finished:
126				return
127
128			if not self.paused:
129				self.ftime += 1
130
131			if self.ftime == framedelay:
132				self.ftime = 0
133				if self.currentframe == (self.a.frames - 1):
134					if self.loop:
135						self.currentframe = 0
136					else:
137						if not self.finished:
138							self.finished = True
139							self.fire_finish_event()
140				else:
141					self.currentframe += 1
142
143			for i, l in enumerate(self.a.layers):
144				if self.layer_vis[i]:
145					l.draw(screen, self.currentframe, (x, y))
146
147		def add_finish_listener(self, listener):
148			self.listeners.append(listener)
149
150		def fire_finish_event(self):
151			for l in self.listeners:
152				l.animation_finished(self)
153
154		def is_finished(self):
155			return self.finished
156
157	def __init__(self, layers=[], layer_names={}, name=''):
158		self.layers = layers
159		self.frames = max([l.frames for l in layers])
160		self.layer_names = layer_names
161		self.name = name
162
163	def new(self, framedelay=3, loop=True, start_paused=False):
164		inst = AnimatedSprite.Instance(self, framedelay=framedelay, loop=loop)
165		if start_paused:
166			inst.pause()
167		return inst
168
169	def draw(self, screen, frame, pos):
170		for l in self.layers:
171			l.draw(screen, frame, pos)
172
173	def bounds(self, frame):
174		r = None
175		for l in self.layers:
176			b = l.bounds(frame)
177			if r is None:
178				r = b
179			else:
180				r.union_ip(b)
181		return r
182				
183
184	def __str__(self):
185		return self.name
186
187	def _get_layer_id(self, layer):
188		if isinstance(layer, int):
189			return layer
190		try:
191			return self.layer_names[layer]
192		except KeyError:
193			raise KeyError('Invalid layer id %s in animation %s' % (layer, self))
194
195	@staticmethod
196	def load(fname):
197		layers = []
198		base = None
199		frames = 0
200		offs = None
201		layer_names = {}
202		name = None
203
204		for l in open(os.path.join(ANIMATIONS_ROOT, fname), 'r'):
205			mo = re.match(r'^(base|name|frames|offsets):\s*(.*)', l)
206			if not mo:
207				continue
208			k = mo.group(1)
209			v = mo.group(2)
210			if k == 'base':
211				if base and offs:
212					if name:
213						layer_names[name] = len(layers) 
214					layers.append(AnimationLayer(base, frames, offs))
215				base = v
216				offs = None
217				angles = 0
218				name = None
219			elif k == 'frames':
220				frames = int(v)
221			elif k == 'name':
222				name = v
223			elif k == 'offsets':
224				tuples = re.split(r'\s+', v.strip())
225				offs = []
226				for t in tuples:
227					if t == '-':
228						offs.append(None)
229					else:
230						x, y = t.split(',')
231						x, y = int(x), int(y)
232						offs.append((x, y))
233		if base and offs:
234			if name:
235				layer_names[name] = len(layers) 
236			layers.append(AnimationLayer(base, frames, offs))
237
238		if not layers:
239			raise ValueError('No layers found')
240
241		return AnimatedSprite(layers, layer_names, name=fname)
242