Source code for pyglet2d

__version__ = '0.2.1'

from itertools import chain

import numpy as np
import pyglet
from Polygon import Polygon, setDataStyle, STYLE_NUMPY
from Polygon.Utils import pointList as point_list


setDataStyle(STYLE_NUMPY)


[docs]class Shape: """Graphical polygon primitive for use with `pyglet`_. Alternative constructor methods: - |Shape.circle| - |Shape.rectangle| - |Shape.regular_polygon| - |Shape.from_dict| Parameters ---------- vertices : array-like or |Polygon|. If a |Polygon| is passed, its points will be used. Otherwise, `vertices` should be a sequence of `[x, y]` locations or an array with x and y columns. color : str or 3-tuple of int, optional Color, in R, G, B format. Alternatively, a key that refers to an element of `colors`. velocity : array-like Speed and direction of motion, in [dx_dt, dy_dt] format. angular_velocity : float Speed of angular motion, in counter-clockwise radians per second. colors : dict of tuple, optional Named colors, defined as R, G, B tuples. Useful for easily switching between a set of colors. Attributes ---------- poly : |Polygon| Associated |Polygon| object. vertices : |array| An array of points, with x and y columns. Read-only. center : |array| The centroid of the shape. Setting center calls |Shape.translate|. position : |array| Alias for `center`. radius : |array| Mean distance from each point to the center. Setting radius calls |Shape.scale|. color : str or tuple of int The current color, in R, G, B format if `colors` was not passed. Otherwise, the current color is represented as a key in `colors`. colors : dict of tuple Named colors. velocity : |array| Speed and direction of linear motion. Angular_velocity : float Speed of angular motion, in counter-clockwise radians per second. enabled : bool If False, the shape will not be drawn. """ def __init__(self, vertices, color=(255, 255, 255), velocity=(0, 0), angular_velocity=0, colors=None): if isinstance(vertices, Polygon): self.poly = vertices else: self.poly = Polygon(vertices) self.colors = colors self._color = 'primary' if colors: self.color = color else: self.colors = {'primary': color} self.velocity = np.asarray(velocity) self.angular_velocity = angular_velocity # Construct vertex_list. self._vertex_list = self._get_vertex_list() self.enabled = True @classmethod
[docs] def regular_polygon(cls, center, radius, n_vertices, start_angle=0, **kwargs): """Construct a regular polygon. Parameters ---------- center : array-like radius : float n_vertices : int start_angle : float, optional Where to put the first point, relative to `center`, in radians counter-clockwise starting from the horizontal axis. kwargs Other keyword arguments are passed to the |Shape| constructor. """ angles = (np.arange(n_vertices) * 2 * np.pi / n_vertices) + start_angle return cls(center + radius * np.array([np.cos(angles), np.sin(angles)]).T, **kwargs)
@classmethod
[docs] def circle(cls, center, radius, n_vertices=50, **kwargs): """Construct a circle. Parameters ---------- center : array-like radius : float n_vertices : int, optional Number of points to draw. Decrease for performance, increase for appearance. kwargs Other keyword arguments are passed to the |Shape| constructor. """ return cls.regular_polygon(center, radius, n_vertices, **kwargs)
@classmethod
[docs] def rectangle(cls, vertices, **kwargs): """Shortcut for creating a rectangle aligned with the screen axes from only two corners. Parameters ---------- vertices : array-like An array containing the ``[x, y]`` positions of two corners. kwargs Other keyword arguments are passed to the |Shape| constructor. """ bottom_left, top_right = vertices top_left = [bottom_left[0], top_right[1]] bottom_right = [top_right[0], bottom_left[1]] return cls([bottom_left, bottom_right, top_right, top_left], **kwargs)
@classmethod
[docs] def from_dict(cls, spec): """Create a |Shape| from a dictionary specification. Parameters ---------- spec : dict A dictionary with either the fields ``'center'`` and ``'radius'`` (for a circle), ``'center'``, ``'radius'``, and ``'n_vertices'`` (for a regular polygon), or ``'vertices'``. If only two vertices are given, they are assumed to be lower left and top right corners of a rectangle. Other fields are interpreted as keyword arguments. """ spec = spec.copy() center = spec.pop('center', None) radius = spec.pop('radius', None) if center and radius: return cls.circle(center, radius, **spec) vertices = spec.pop('vertices') if len(vertices) == 2: return cls.rectangle(vertices, **spec) return cls(vertices, **spec)
@property def vertices(self): return np.asarray(point_list(self.poly)) @property def color(self): if len(self.colors) == 1: return self.colors[self._color] else: return self._color @color.setter def color(self, value): if value in self.colors: self._color = value else: self.colors[self._color] = value @property def _kwargs(self): """Keyword arguments for recreating the Shape from the vertices. """ return dict(color=self.color, velocity=self.velocity, colors=self.colors) @property def center(self): return np.asarray(self.poly.center()) @center.setter def center(self, value): self.translate(np.asarray(value) - self.center) @property def radius(self): return np.linalg.norm(self.vertices - self.center, axis=1).mean() @radius.setter def radius(self, value): self.scale(value / self.radius) @property def _gl_vertices(self): return list(chain(self.center, *point_list(self.poly))) @property def _gl_colors(self): return (len(self) + 1) * self.colors[self._color]
[docs] def distance_to(self, point): """Distance from center to arbitrary point. Parameters ---------- point : array-like Returns ------- float """ return np.linalg.norm(self.center - point)
[docs] def scale(self, factor, center=None): """Resize the shape by a proportion (e.g., 1 is unchanged), in-place. Parameters ---------- factor : float or array-like If a scalar, the same factor will be applied in the x and y dimensions. center : array-like, optional Point around which to perform the scaling. If not passed, the center of the shape is used. """ factor = np.asarray(factor) if len(factor.shape): args = list(factor) else: args = [factor, factor] if center is not None: args.extend(center) self.poly.scale(*args) return self
[docs] def translate(self, vector): """Translate the shape along a vector, in-place. Parameters ---------- vector : array-like """ self.poly.shift(*vector)
[docs] def rotate(self, angle, center=None): """Rotate the shape, in-place. Parameters ---------- angle : float Angle to rotate, in radians counter-clockwise. center : array-like, optional Point about which to rotate. If not passed, the center of the shape will be used. """ args = [angle] if center is not None: args.extend(center) self.poly.rotate(*args) return self
[docs] def flip_x(self, center=None): """Flip the shape in the x direction, in-place. Parameters ---------- center : array-like, optional Point about which to flip. If not passed, the center of the shape will be used. """ if center is None: self.poly.flip() else: self.poly.flip(center[0])
[docs] def flip_y(self, center=None): """Flip the shape in the y direction, in-place. Parameters ---------- center : array-like, optional Point about which to flip. If not passed, the center of the shape will be used. """ if center is None: self.poly.flop() else: self.poly.flop(center[1]) return self
[docs] def flip(self, angle, center=None): """ Flip the shape in an arbitrary direction. Parameters ---------- angle : array-like The angle, in radians counter-clockwise from the horizontal axis, defining the angle about which to flip the shape (of a line through `center`). center : array-like, optional The point about which to flip. If not passed, the center of the shape will be used. """ return self.rotate(-angle, center=center).flip_y(center=center).rotate(angle, center=center)
def _get_vertex_list(self): indices = [] for i in range(1, len(self) + 1): indices.extend([0, i, i + 1]) indices[-1] = 1 return pyglet.graphics.vertex_list_indexed( len(self) + 1, indices, ('v2f', self._gl_vertices), ('c3B', self._gl_colors))
[docs] def draw(self): """Draw the shape in the current OpenGL context. """ if self.enabled: self._vertex_list.colors = self._gl_colors self._vertex_list.vertices = self._gl_vertices self._vertex_list.draw(pyglet.gl.GL_TRIANGLES)
[docs] def update(self, dt): """Update the shape's position by moving it forward according to its velocity. Parameters ---------- dt : float """ self.translate(dt * self.velocity) self.rotate(dt * self.angular_velocity)
[docs] def enable(self, enabled): """Set whether the shape should be drawn. Parameters ---------- enabled : bool """ self.enabled = enabled return self
[docs] def overlaps(self, other): """Check if two shapes overlap. Parameters ---------- other : |Shape| Returns ------- bool """ return bool(self.poly.overlaps(other.poly))
[docs] def covers(self, other): """Check if the shape completely covers another shape. Parameters ---------- other : |Shape| Returns ------- bool """ return bool(self.poly.covers(other.poly))
def __repr__(self): kwarg_strs = [] for arg, value in self._kwargs.items(): if isinstance(value, str): value_str = "'{}'".format(value) elif isinstance(value, np.ndarray): value_str = '[{}, {}]'.format(*value) else: value_str = str(value) kwarg_strs.append(arg + '=' + value_str) kwargs = ',\n' + ', '.join(kwarg_strs) return '{cls}({points}{kwargs})'.format( cls=type(self).__name__, points='[{}]'.format(',\n'.join('[{}, {}]'.format(x, y) for x, y in self.vertices)), kwargs=kwargs, ) def __eq__(self, other): if isinstance(other, Shape): if len(self) != len(other): return False return (np.all(np.isclose(np.sort(self.vertices, axis=0), np.sort(other.vertices, axis=0))) and self.colors == other.colors and self.color == other.color and np.all(np.isclose(self.velocity, other.velocity))) else: return False def __bool__(self): return True def __getitem__(self, item): return self.vertices[item] def __len__(self): return self.poly.nPoints() def __add__(self, other): if isinstance(other, Shape): return type(self)(self.poly + other.poly) return type(self)(self.vertices + other, **self._kwargs) __radd__ = __add__ def __sub__(self, other): if isinstance(other, Shape): return type(self)(self.poly - other.poly) return type(self)(self.vertices - other, **self._kwargs) def __mul__(self, other): return type(self)(self.vertices * other, **self._kwargs) def __rmul__(self, other): return type(self)(other * self.vertices, **self._kwargs) def __truediv__(self, other): return type(self)(self.vertices / other, **self._kwargs) __div__ = __truediv__ def __xor__(self, other): return type(self)(self.poly ^ other.poly, **self._kwargs) def __and__(self, other): return type(self)(self.poly & other.poly, **self._kwargs) def __or__(self, other): return type(self)(self.poly | other.poly, **self._kwargs) def __iadd__(self, other): self.translate(other) return self def __isub__(self, other): self.translate(-np.asarray(other)) return self def __imul__(self, other): if isinstance(other, int) or isinstance(other, float): self.poly.scale(other, other) elif len(other) == 2: self.poly.scale(*other) return self def __itruediv__(self, other): if isinstance(other, int) or isinstance(other, float): self.poly.scale(1/other, 1/other) elif len(other) == 2: self.poly.scale(1/other[0], 1/other[1]) return self __idiv__ = __itruediv__ position = center