235 lines
7.1 KiB
Python
235 lines
7.1 KiB
Python
|
"""
|
||
|
This module provides `Paths` to create animation effects with Sprites. For more details see
|
||
|
http://asciimatics.readthedocs.io/en/latest/animation.html
|
||
|
"""
|
||
|
from __future__ import division
|
||
|
from __future__ import absolute_import
|
||
|
from __future__ import print_function
|
||
|
from __future__ import unicode_literals
|
||
|
from future.utils import with_metaclass
|
||
|
from abc import ABCMeta, abstractmethod
|
||
|
from builtins import object
|
||
|
from builtins import range
|
||
|
|
||
|
|
||
|
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(with_metaclass(ABCMeta, object)):
|
||
|
"""
|
||
|
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(Path, self).__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(with_metaclass(ABCMeta, _AbstractPath)):
|
||
|
"""
|
||
|
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(DynamicPath, self).__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.
|
||
|
"""
|