Add circular fitting to lights.
[tabakrolletjie.git] / tabakrolletjie / lights.py
1 """ May it be a light for you in dark places, when all other lights go out.
2 """
3
4 import pymunk
5 import pymunk.pygame_util
6 import pygame.draw
7
8 from .constants import (
9     SCREEN_SIZE, LIGHT_CATEGORY, FITTINGS_CATEGORY)
10 from .utils import debug_timer
11
12 LIGHT_FILTER = pymunk.ShapeFilter(
13     mask=pymunk.ShapeFilter.ALL_MASKS ^ (
14         LIGHT_CATEGORY | FITTINGS_CATEGORY),
15     categories=LIGHT_CATEGORY)
16
17 FITTINGS_FILTER = pymunk.ShapeFilter(
18     mask=pymunk.ShapeFilter.ALL_MASKS ^ (
19         LIGHT_CATEGORY | FITTINGS_CATEGORY),
20     categories=FITTINGS_CATEGORY)
21
22
23 def screen_rays(pos):
24     """ An iterable that returns ordered rays from pos to the edge of the
25         screen, starting with the edge point (0, 0) and continuing clockwise
26         in pymunk coordinates.
27     """
28     w, h = SCREEN_SIZE
29     left, right, bottom, top = 0, w, 0, h
30     step = 1
31     for y in range(0, h, step):
32         yield pymunk.Vec2d(left, y)
33     for x in range(0, w, step):
34         yield pymunk.Vec2d(x, top)
35     for y in range(top, -1, -step):
36         yield pymunk.Vec2d(right, y)
37     for x in range(right, -1, -step):
38         yield pymunk.Vec2d(x, bottom)
39
40
41 @debug_timer("lights.calculate_ray_polys")
42 def calculate_ray_polys(space, body, position):
43     position = pymunk.Vec2d(position)
44     vertices = [position]
45     ray_polys = []
46     for ray in screen_rays(position):
47         info = space.segment_query_first(position, ray, 1, LIGHT_FILTER)
48         point = ray if info is None else info.point
49         vertices.append(point)
50         if len(vertices) > 3:
51             trial_poly = pymunk.Poly(None, vertices)
52             trial_poly.update(pymunk.Transform.identity())
53             query_prev = trial_poly.point_query(vertices[-2])
54             query_pos = trial_poly.point_query(position)
55             if query_prev.distance < -0.01 or query_pos.distance < -0.01:
56                 new_poly = pymunk.Poly(body, vertices[:-1])
57                 vertices = [position, vertices[-1]]
58                 ray_polys.append(new_poly)
59             else:
60                 vertices = trial_poly.get_vertices() + [point]
61     if len(vertices) > 2:
62         ray_polys.append(pymunk.Poly(body, vertices))
63     return ray_polys
64
65
66 class BaseLight(object):
67     """ Common light functionality. """
68
69     COLOURS = {
70         "red": (255, 0, 0),
71         "green": (0, 255, 0),
72         "blue": (0, 255, 255),
73         "yellow": (255, 255, 0),
74         "white": (255, 255, 255),
75     }
76
77     def __init__(self, colour, position):
78         self.on = True
79         self.body = pymunk.Body(0, 0, pymunk.body.Body.STATIC)
80         self.fitting = pymunk.Circle(self.body, 5.0)
81         self.colour = colour
82         self.position = pymunk.Vec2d(position)
83
84     @classmethod
85     def load(cls, config):
86         kw = config.copy()
87         light_type = kw.pop("type")
88         [light_class] = [
89             c for c in cls.__subclasses__()
90             if c.__name__.lower() == light_type]
91         return light_class(**kw)
92
93     def add(self, space):
94         if self.body.space is not None:
95             space.remove(self.body, *self.body.shapes)
96         shapes = self.shapes_for_ray_polys(
97             calculate_ray_polys(space, self.body, self.position))
98         for shape in shapes:
99             shape.filter = LIGHT_FILTER
100         self.fitting.filter = FITTINGS_FILTER
101         space.add(self.body, self.fitting, *shapes)
102
103     def shapes_for_ray_polys(self, ray_polys):
104         return ray_polys
105
106     def toggle(self):
107         self.on = not self.on
108
109     def render_light(self, surface):
110         if not self.on:
111             return
112         subsurface = surface.copy()
113         light_colour = self.COLOURS[self.colour]
114         for shape in self.body.shapes:
115             if shape is self.fitting:
116                 continue
117             pygame_poly = [
118                 pymunk.pygame_util.to_pygame(v, surface) for v in
119                 shape.get_vertices()]
120             pygame.draw.polygon(
121                 subsurface, light_colour, pygame_poly, 0)
122             pygame.draw.aalines(
123                 subsurface, light_colour, True, pygame_poly, 1)
124         subsurface.set_alpha(50)
125         surface.blit(subsurface, (0, 0), None)
126
127     def render_fittings(self, surface):
128         centre = self.position + self.fitting.offset
129         pygame.draw.circle(
130             surface, (255, 255, 0),
131             pymunk.pygame_util.to_pygame(centre, surface),
132             int(self.fitting.radius))
133
134
135 class SpotLight(BaseLight):
136     def __init__(
137             self, colour="white", position=None, direction=90.0, spread=45.0):
138         super(SpotLight, self).__init__(colour, position)
139         self.direction = direction
140         self.spread = spread
141         self.i = 0
142
143
144 class Lamp(BaseLight):
145     def __init__(self, colour="white", position=None, radius=100.0):
146         super(Lamp, self).__init__(colour, position)
147         self.radius = radius