source: nagslang/render.py@ 474:9775055ba2f0

Last change on this file since 474:9775055ba2f0 was 474:9775055ba2f0, checked in by Jeremy Thurgood <firxen@…>, 8 years ago

Prettier bulkheads.

File size: 9.6 KB
Line 
1import math
2import textwrap
3
4import pygame
5import pygame.locals as pgl
6import pymunk
7
8from nagslang.options import options
9from nagslang.utils import tile_surface, vec_from_angle
10from nagslang.widgets.text import LabelWidget, MultiLineWidget
11
12
13class Renderer(object):
14 def set_game_object(self, game_object):
15 self.game_object = game_object
16
17 def _render_shape(self, surface):
18 shape = self.game_object.get_shape()
19 # Less general that pymunk.pygame_util.draw, but also a lot less noisy.
20 color = getattr(shape, 'color', pygame.color.THECOLORS['lightblue'])
21 # We only explicitly draw Circle and Poly shapes. Everything else we
22 # forward to pymunk.
23 if isinstance(shape, pymunk.Circle):
24 centre = pymunk.pygame_util.to_pygame(shape.body.position, surface)
25 radius = int(shape.radius)
26 pygame.draw.circle(surface, color, centre, radius, 2)
27 elif isinstance(shape, pymunk.Poly):
28 # polygon bounding box
29 points = [pymunk.pygame_util.to_pygame(p, surface)
30 for p in shape.get_vertices()]
31 pygame.draw.lines(surface, color, True, points, 2)
32 else:
33 pymunk.pygame_util.draw(surface, shape)
34
35 def render(self, surface):
36 if options.shapes:
37 self._render_shape(surface)
38
39 def update(self, seconds):
40 # Used by time animatations to advance the clock
41 pass
42
43
44class NullRenderer(Renderer):
45 def render(self, surface):
46 pass
47
48
49class BulkheadRenderer(Renderer):
50 def _offset_point(self, vec, to_vec, length):
51 offset = vec_from_angle((to_vec - vec).angle, abs(length))
52 if length < 0:
53 offset = -offset
54 return vec + offset
55
56 def _to_pygame(self, surface, *points):
57 return [pymunk.pygame_util.to_pygame(p, surface) for p in points]
58
59 def draw_bulkhead_line(self, surface, a, b):
60 ai = self._offset_point(a, b, 2)
61 bi = self._offset_point(b, a, 2)
62 a, b, ai, bi = self._to_pygame(surface, a, b, ai, bi)
63 pygame.draw.line(
64 surface, pygame.color.THECOLORS['black'], a, b, 8)
65 pygame.draw.line(
66 surface, pygame.color.THECOLORS['lightblue'], ai, bi, 6)
67
68 def render(self, surface):
69 shape = self.game_object.get_shape()
70 if self.game_object.puzzler.get_state():
71 ai = self._offset_point(shape.a, shape.b, 10)
72 bi = self._offset_point(shape.b, shape.a, 10)
73 self.draw_bulkhead_line(surface, shape.a, ai)
74 self.draw_bulkhead_line(surface, bi, shape.b)
75 else:
76 mid = shape.a + (shape.b - shape.a) / 2
77 self.draw_bulkhead_line(surface, shape.a, mid)
78 self.draw_bulkhead_line(surface, mid, shape.b)
79
80
81def image_pos(image, pos):
82 return (pos[0] - image.get_width() / 2,
83 pos[1] - image.get_height() / 2)
84
85
86class ImageRenderer(Renderer):
87 def __init__(self, image):
88 self._image = image
89
90 def get_image(self):
91 return self._image
92
93 def rotate_image(self, image):
94 angle = self.game_object.get_render_angle() * 180 / math.pi
95 return pygame.transform.rotate(image, angle)
96
97 def render_image(self, surface, image):
98 image = self.rotate_image(image)
99 pos = self.game_object.get_render_position(surface)
100 surface.blit(image, image_pos(image, pos))
101
102 def render(self, surface):
103 self.render_image(surface, self.get_image())
104 super(ImageRenderer, self).render(surface)
105
106
107class ImageStateRenderer(ImageRenderer):
108 def __init__(self, state_images):
109 self._state_images = state_images
110
111 def get_image(self):
112 return self._state_images[self.game_object.puzzler.get_state()]
113
114
115class TimedAnimatedRenderer(ImageRenderer):
116 def __init__(self, images, frame_ticks=1):
117 self._images = images
118 self._frame_ticks = frame_ticks
119 self._frame_tick = 0
120 self._frame = 0
121
122 def advance_tick(self):
123 self._frame_tick += 1
124 if self._frame_tick > self._frame_ticks:
125 self._frame_tick = 0
126 self._frame += 1
127 if self._frame >= len(self._images):
128 self._frame = 0
129
130 def reset(self):
131 self._frame_tick = 0
132 self._frame = 0
133
134 def get_image(self):
135 return self._images[self._frame]
136
137 def update(self, seconds):
138 self.advance_tick()
139
140
141class MovementAnimatedRenderer(TimedAnimatedRenderer):
142 def update(self, seconds):
143 if self.game_object.is_moving:
144 self.advance_tick()
145 else:
146 self.reset()
147
148
149class RendererSelectionRenderer(Renderer):
150 def __init__(self, renderers):
151 self._renderers = renderers
152
153 def set_game_object(self, game_object):
154 self.game_object = game_object
155 for renderer in self._renderers.values():
156 renderer.set_game_object(game_object)
157
158 @property
159 def renderer(self):
160 return self._renderers[self.select_renderer()]
161
162 def render(self, surface):
163 return self.renderer.render(surface)
164
165 def update(self, seconds):
166 return self.renderer.update(seconds)
167
168 def select_renderer(self):
169 raise NotImplementedError()
170
171
172class FacingSelectionRenderer(RendererSelectionRenderer):
173 def select_renderer(self):
174 return self.game_object.get_facing_direction()
175
176
177class ShapeRenderer(Renderer):
178 def render(self, surface):
179 self._render_shape(surface)
180 super(ShapeRenderer, self).render(surface)
181
182
183class ShapeStateRenderer(ShapeRenderer):
184 """Renders the shape in a different colour depending on the state.
185
186 Requires the game object it's attached to to have a puzzler.
187 """
188 def render(self, surface):
189 if self.game_object.puzzler.get_state():
190 color = pygame.color.THECOLORS['green']
191 else:
192 color = pygame.color.THECOLORS['red']
193
194 self.game_object.get_shape().color = color
195 super(ShapeStateRenderer, self).render(surface)
196
197
198class Overlay(object):
199 def set_game_object(self, game_object):
200 self.game_object = game_object
201
202 def render(self, surface, display_offset, max_width):
203 pass
204
205 def is_visible(self):
206 return self.game_object.puzzler.get_state()
207
208
209class TextOverlay(Overlay):
210 def __init__(self, text):
211 self.text = text
212 self.widget = LabelWidget((20, 20), self.text)
213
214 def render(self, surface, display_offset, max_width):
215 x, y = 20, 20
216 if display_offset[0] < 0:
217 x += abs(display_offset[0])
218 if display_offset[1] < 0:
219 y += abs(display_offset[1])
220 if self.widget.rect.width > max_width - 40:
221 # Need to relayout the widget
222 factor = 2
223 while self.widget.rect.width > max_width - 40:
224 wrapped = '\n'.join(textwrap.wrap(self.text,
225 len(self.text) // factor))
226 factor *= 2
227 self.widget = MultiLineWidget((20, 20), wrapped)
228 if self.widget.rect.width < 100:
229 # safety valve
230 break
231 self.widget.rect.topleft = (x, y)
232 self.widget.draw(surface)
233 # TODO: undo the mad folding
234 else:
235 self.widget.rect.topleft = (x, y)
236 self.widget.draw(surface)
237
238
239class TiledRenderer(Renderer):
240 """Tile the given image to fit the given outline
241
242 Outline is assumed to be in pymunk coordinates"""
243
244 def __init__(self, outline, tile_image, alpha=255):
245 self._tile_image = tile_image
246 self.outline = outline
247 self._tiled = None
248 self._offset = None
249 self._alpha = alpha
250
251 def _make_surface(self, surface, image):
252 size = surface.get_size()
253 mask = pygame.surface.Surface(size, pgl.SRCALPHA)
254 pointlist = [pymunk.pygame_util.to_pygame(p, surface)
255 for p in self.outline]
256 rect = pygame.draw.polygon(mask,
257 pygame.color.Color(
258 255, 255, 255, self._alpha),
259 pointlist, 0)
260 self._offset = (rect.x, rect.y)
261 tiled = tile_surface((rect.w, rect.h), image)
262 tiled.blit(mask, (0, 0), rect,
263 special_flags=pgl.BLEND_RGBA_MULT)
264 return tiled
265
266 def render(self, surface):
267 if not self._tiled:
268 self._tiled = self._make_surface(surface, self._tile_image)
269 surface.blit(self._tiled, self._offset)
270 super(TiledRenderer, self).render(surface)
271
272
273class TimedTiledRenderer(TiledRenderer):
274 """Animate tiles"""
275
276 # Should make this a mixin with TimeAnimate, but not right now
277
278 def __init__(self, outline, images, frame_ticks=1, alpha=255):
279 self._images = images
280 self._frame_ticks = frame_ticks
281 self.outline = outline
282 self._frames = [None] * len(images)
283 self._offset = None
284 self._alpha = alpha
285 self._frame = 0
286 self._frame_tick = 0
287
288 def advance_tick(self):
289 self._frame_tick += 1
290 if self._frame_tick > self._frame_ticks:
291 self._frame_tick = 0
292 self._frame += 1
293 if self._frame >= len(self._images):
294 self._frame = 0
295
296 def reset(self):
297 self._frame_tick = 0
298 self._frame = 0
299
300 def update(self, seconds):
301 self.advance_tick()
302
303 def render(self, surface):
304 if not self._frames[self._frame]:
305 self._frames[self._frame] = self._make_surface(
306 surface, self._images[self._frame])
307 self._tiled = self._frames[self._frame]
308 super(TimedTiledRenderer, self).render(surface)
Note: See TracBrowser for help on using the repository browser.