File: paths.py

package info (click to toggle)
python-asciimatics 1.15.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 4,488 kB
  • sloc: python: 15,713; sh: 8; makefile: 2
file content (228 lines) | stat: -rw-r--r-- 6,998 bytes parent folder | download
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
"""
This module provides `Paths` to create animation effects with Sprites.  For more details see
http://asciimatics.readthedocs.io/en/latest/animation.html
"""

from abc import ABCMeta, abstractmethod


def _spline(t, p0, p1, p2, p3):
    """
    Catmull-Rom cubic spline to interpolate 4 given points.

    :param t: Time index through the spline (must be 0-1).
    :param p0: The previous point in the curve (for continuity).
    :param p1: The first point to interpolate.
    :param p2: The second point to interpolate.
    :param p3: The last point to interpolate.
    """
    return (
        t * ((2 - t) * t - 1) * p0 +
        (t * t * (3 * t - 5) + 2) * p1 +
        t * ((4 - 3 * t) * t + 1) * p2 +
        (t - 1) * t * t * p3) / 2


class _AbstractPath(metaclass=ABCMeta):
    """
    Class to represent the motion of a Sprite.

    The Screen will reset() the Path before iterating through each position
    using next_pos() and checking whether it has reached the end using
    is_finished().
    """

    def __init__(self):
        """
        To define a Path, use the methods to jump to a location, wait or move
        between points.
        """
        self._steps = []
        self._index = None
        self._rec_x = 0
        self._rec_y = 0

    @abstractmethod
    def reset(self):
        """
        Reset the Path for use next time.
        """

    @abstractmethod
    def next_pos(self):
        """
        :return: The next position tuple (x, y) for the Sprite on this path.
        """

    @abstractmethod
    def is_finished(self):
        """
        :return: Whether this path has got to the end.
        """


class Path(_AbstractPath):
    """
    Class to record and play back the motion of a Sprite.

    The Screen will reset() the Path before iterating through each position
    using next_pos() and checking whether it has reached the end using
    is_finished().
    """

    def __init__(self):
        """
        To define a Path, use the methods to jump to a location, wait or move
        between points.
        """
        super().__init__()
        self._steps = []
        self._index = 0
        self._rec_x = 0
        self._rec_y = 0
        self.reset()

    def reset(self):
        """
        Reset the Path for use next time.
        """
        self._index = 0

    def next_pos(self):
        """
        :return: The next position tuple (x, y) for the Sprite on this path.
        """
        result = None
        if self._index <= len(self._steps):
            result = self._steps[self._index]
            self._index += 1
        return result

    def is_finished(self):
        """
        :return: Whether this path has got to the end.
        """
        return self._index >= len(self._steps)

    def _add_step(self, pos):
        """
        Add a step to the end of the current recorded path.

        :param pos: The position tuple (x, y) to add to the list.
        """
        self._steps.append(pos)
        self._rec_x = pos[0]
        self._rec_y = pos[1]

    def wait(self, delay):
        """
        Wait at the current location for the specified number of iterations.

        :param delay: The time to wait (in animation frames).
        """
        for _ in range(0, delay):
            self._add_step((self._rec_x, self._rec_y))

    def jump_to(self, x, y):
        """
        Jump straight to the newly specified location - i.e. teleport there and
        don't create a path to get there.

        :param x:  X coord for the end position.
        :param y: Y coord for the end position.
        """
        self._add_step((x, y))

    def move_straight_to(self, x, y, steps):
        """
        Move straight to the newly specified location - i.e. create a straight
        line Path from the current location to the specified point.

        :param x:  X coord for the end position.
        :param y: Y coord for the end position.
        :param steps: How many steps to take for the move.
        """
        start_x = self._rec_x
        start_y = self._rec_y
        for i in range(1, steps + 1):
            self._add_step((
                int(start_x + (x - start_x) / float(steps) * i),
                int(start_y + (y - start_y) / float(steps) * i)))

    def move_round_to(self, points, steps):
        """
        Follow a path pre-defined by a set of at least 4 points.  This Path will
        interpolate the points into a curve and follow that curve.

        :param points: The list of points that defines the path.
        :param steps: The number of steps to take to follow the path.
        """
        # Spline interpolation needs a before and after point for the curve.
        # Duplicate the first and last points to handle this.  We also need
        # to move from the current position to the first specified point.
        points.insert(0, (self._rec_x, self._rec_y))
        points.insert(0, (self._rec_x, self._rec_y))
        points.append(points[-1])

        # Convert the points into an interpolated set of more detailed points.
        steps_per_spline = steps // (len(points) - 3)
        for j in range(1, len(points) - 2):
            for t in range(1, steps_per_spline + 1):
                y = _spline(float(t) / steps_per_spline,
                            float(points[j - 1][1]),
                            float(points[j][1]),
                            float(points[j + 1][1]),
                            float(points[j + 2][1]))
                x = int(points[j][0] + ((points[j + 1][0] - points[j][0]) *
                                        float(t) / steps_per_spline))
                self._add_step((x, int(y)))


class DynamicPath(_AbstractPath, metaclass=ABCMeta):
    """
    Class to create a dynamic path that reacts to events

    The Screen will reset() the Path before iterating through each position
    using next_pos() and checking whether it has reached the end using
    is_finished().
    """

    def __init__(self, screen, x, y):
        """
        To implement a DynamicPath, override the :py:meth:`.process_event()`
        method to react to any user input.
        """
        super().__init__()
        self._screen = screen
        self._x = self._start_x = x
        self._y = self._start_y = y
        self.reset()

    def reset(self):
        """
        Reset the Path for use next time.
        """
        self._x = self._start_x
        self._y = self._start_y

    def next_pos(self):
        """
        :return: The next position tuple (x, y) for the Sprite on this path.
        """
        return self._x, self._y

    def is_finished(self):
        """
        :return: Whether this path has got to the end.
        """
        return False

    @abstractmethod
    def process_event(self, event):
        """
        Process any mouse event.

        :param event: The event that was triggered.
        :returns: None if the Effect processed the event, else the original
                  event.
        """