# ---------------------------------------------------------------------------- # pyglet # Copyright (c) 2006-2008 Alex Holkner # Copyright (c) 2008-2021 pyglet contributors # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in # the documentation and/or other materials provided with the # distribution. # * Neither the name of pyglet nor the names of its # contributors may be used to endorse or promote products # derived from this software without specific prior written # permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ---------------------------------------------------------------------------- """2D shapes. This module provides classes for a variety of simplistic 2D shapes, such as Rectangles, Circles, and Lines. These shapes are are made internally from OpenGL primitives, and provide excellent performance when drawn as part of a :py:class:`~pyglet.graphics.Batch`. Convenience methods are provided for positioning, changing color and opacity, and rotation (where applicible). To create more complex shapes than what is provided here, the lower evel graphics API is more appropriate. See the :ref:`guide_graphics` for more details. A simple example of drawing shapes:: import pyglet from pyglet import shapes window = pyglet.window.Window(960, 540) batch = pyglet.graphics.Batch() circle = shapes.Circle(700, 150, 100, color=(50, 225, 30), batch=batch) square = shapes.Rectangle(200, 200, 200, 200, color=(55, 55, 255), batch=batch) rectangle = shapes.Rectangle(250, 300, 400, 200, color=(255, 22, 20), batch=batch) rectangle.opacity = 128 rectangle.rotation = 33 line = shapes.Line(100, 100, 100, 200, width=19, batch=batch) line2 = shapes.Line(150, 150, 444, 111, width=4, color=(200, 20, 20), batch=batch) @window.event def on_draw(): window.clear() batch.draw() pyglet.app.run() .. versionadded:: 1.5.4 """ import math from pyglet.gl import GL_COLOR_BUFFER_BIT, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA from pyglet.gl import GL_TRIANGLES, GL_LINES, GL_BLEND from pyglet.gl import glPushAttrib, glPopAttrib, glBlendFunc, glEnable, glDisable from pyglet.graphics import Group, Batch class _ShapeGroup(Group): """Shared Shape rendering Group. The group is automatically coalesced with other shape groups sharing the same parent group and blend parameters. """ def __init__(self, blend_src, blend_dest, parent=None): """Create a Shape group. The group is created internally. Usually you do not need to explicitly create it. :Parameters: `blend_src` : int OpenGL blend source mode; for example, ``GL_SRC_ALPHA``. `blend_dest` : int OpenGL blend destination mode; for example, ``GL_ONE_MINUS_SRC_ALPHA``. `parent` : `~pyglet.graphics.Group` Optional parent group. """ super().__init__(parent) self.blend_src = blend_src self.blend_dest = blend_dest def set_state(self): glPushAttrib(GL_COLOR_BUFFER_BIT) glEnable(GL_BLEND) glBlendFunc(self.blend_src, self.blend_dest) def unset_state(self): glDisable(GL_BLEND) glPopAttrib() def __eq__(self, other): return (other.__class__ is self.__class__ and self.parent is other.parent and self.blend_src == other.blend_src and self.blend_dest == other.blend_dest) def __hash__(self): return hash((id(self.parent), self.blend_src, self.blend_dest)) class _ShapeBase: """Base class for Shape objects""" _rgb = (255, 255, 255) _opacity = 255 _visible = True _x = 0 _y = 0 _anchor_x = 0 _anchor_y = 0 _batch = None _group = None _vertex_list = None def __del__(self): try: if self._vertex_list is not None: self._vertex_list.delete() except: pass def _update_position(self): raise NotImplementedError def _update_color(self): raise NotImplementedError def draw(self): """Draw the shape at its current position. Using this method is not recommended. Instead, add the shape to a `pyglet.graphics.Batch` for efficient rendering. """ self._group.set_state_recursive() self._vertex_list.draw(GL_TRIANGLES) self._group.unset_state_recursive() def delete(self): self._vertex_list.delete() self._vertex_list = None @property def x(self): """X coordinate of the shape. :type: int or float """ return self._x @x.setter def x(self, value): self._x = value self._update_position() @property def y(self): """Y coordinate of the shape. :type: int or float """ return self._y @y.setter def y(self, value): self._y = value self._update_position() @property def position(self): """The (x, y) coordinates of the shape, as a tuple. :Parameters: `x` : int or float X coordinate of the sprite. `y` : int or float Y coordinate of the sprite. """ return self._x, self._y @position.setter def position(self, values): self._x, self._y = values self._update_position() @property def anchor_x(self): """The X coordinate of the anchor point :type: int or float """ return self._anchor_x @anchor_x.setter def anchor_x(self, value): self._anchor_x = value self._update_position() @property def anchor_y(self): """The Y coordinate of the anchor point :type: int or float """ return self._anchor_y @anchor_y.setter def anchor_y(self, value): self._anchor_y = value self._update_position() @property def anchor_position(self): """The (x, y) coordinates of the anchor point, as a tuple. :Parameters: `x` : int or float X coordinate of the anchor point. `y` : int or float Y coordinate of the anchor point. """ return self._anchor_x, self._anchor_y @anchor_position.setter def anchor_position(self, values): self._anchor_x, self._anchor_y = values self._update_position() @property def color(self): """The shape color. This property sets the color of the shape. The color is specified as an RGB tuple of integers '(red, green, blue)'. Each color component must be in the range 0 (dark) to 255 (saturated). :type: (int, int, int) """ return self._rgb @color.setter def color(self, values): self._rgb = list(map(int, values)) self._update_color() @property def opacity(self): """Blend opacity. This property sets the alpha component of the color of the shape. With the default blend mode (see the constructor), this allows the shape to be drawn with fractional opacity, blending with the background. An opacity of 255 (the default) has no effect. An opacity of 128 will make the shape appear translucent. :type: int """ return self._opacity @opacity.setter def opacity(self, value): self._opacity = value self._update_color() @property def visible(self): """True if the shape will be drawn. :type: bool """ return self._visible @visible.setter def visible(self, value): self._visible = value self._update_position() class Arc(_ShapeBase): def __init__(self, x, y, radius, segments=None, angle=math.tau, start_angle=0, closed=False, color=(255, 255, 255), batch=None, group=None): """Create an Arc. The Arc's anchor point (x, y) defaults to it's center. :Parameters: `x` : float X coordinate of the circle. `y` : float Y coordinate of the circle. `radius` : float The desired radius. `segments` : int You can optionally specifify how many distict line segments the arc should be made from. If not specified it will be automatically calculated using the formula: `max(14, int(radius / 1.25))`. `angle` : float The angle of the arc, in radians. Defaults to tau (pi * 2), which is a full circle. `start_angle` : float The start angle of the arc, in radians. Defaults to 0. `closed` : bool If True, the ends of the arc will be connected with a line. defaults to False. `color` : (int, int, int) The RGB color of the circle, specified as a tuple of three ints in the range of 0-255. `batch` : `~pyglet.graphics.Batch` Optional batch to add the circle to. `group` : `~pyglet.graphics.Group` Optional parent group of the circle. """ self._x = x self._y = y self._radius = radius self._segments = segments or max(14, int(radius / 1.25)) self._num_verts = self._segments * 2 + (2 if closed else 0) self._rgb = color self._angle = angle self._start_angle = start_angle self._closed = closed self._rotation = 0 self._batch = batch or Batch() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, group) self._vertex_list = self._batch.add(self._num_verts, GL_LINES, self._group, 'v2f', 'c4B') self._update_position() self._update_color() def _update_position(self): if not self._visible: vertices = (0,) * self._segments * 4 else: x = self._x + self._anchor_x y = self._y + self._anchor_y r = self._radius tau_segs = self._angle / self._segments start_angle = self._start_angle - math.radians(self._rotation) # Calcuate the outer points of the arc: points = [(x + (r * math.cos((i * tau_segs) + start_angle)), y + (r * math.sin((i * tau_segs) + start_angle))) for i in range(self._segments + 1)] # Create a list of doubled-up points from the points: vertices = [] for i in range(len(points) - 1): line_points = *points[i], *points[i + 1] vertices.extend(line_points) if self._closed: chord_points = *points[-1], *points[0] vertices.extend(chord_points) self._vertex_list.vertices[:] = vertices def _update_color(self): self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * self._num_verts @property def rotation(self): """Clockwise rotation of the arc, in degrees. The arc will be rotated about its (anchor_x, anchor_y) position. :type: float """ return self._rotation @rotation.setter def rotation(self, rotation): self._rotation = rotation self._update_position() def draw(self): """Draw the shape at its current position. Using this method is not recommended. Instead, add the shape to a `pyglet.graphics.Batch` for efficient rendering. """ self._vertex_list.draw(GL_LINES) class Circle(_ShapeBase): def __init__(self, x, y, radius, segments=None, color=(255, 255, 255), batch=None, group=None): """Create a circle. The circle's anchor point (x, y) defaults to the center of the circle. :Parameters: `x` : float X coordinate of the circle. `y` : float Y coordinate of the circle. `radius` : float The desired radius. `segments` : int You can optionally specifify how many distict triangles the circle should be made from. If not specified it will be automatically calculated based using the formula: `max(14, int(radius / 1.25))`. `color` : (int, int, int) The RGB color of the circle, specified as a tuple of three ints in the range of 0-255. `batch` : `~pyglet.graphics.Batch` Optional batch to add the circle to. `group` : `~pyglet.graphics.Group` Optional parent group of the circle. """ self._x = x self._y = y self._radius = radius self._segments = segments or max(14, int(radius / 1.25)) self._rgb = color self._batch = batch or Batch() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, group) self._vertex_list = self._batch.add(self._segments * 3, GL_TRIANGLES, self._group, 'v2f', 'c4B') self._update_position() self._update_color() def _update_position(self): if not self._visible: vertices = (0,) * self._segments * 6 else: x = self._x + self._anchor_x y = self._y + self._anchor_y r = self._radius tau_segs = math.pi * 2 / self._segments # Calcuate the outer points of the circle: points = [(x + (r * math.cos(i * tau_segs)), y + (r * math.sin(i * tau_segs))) for i in range(self._segments)] # Create a list of trianges from the points: vertices = [] for i, point in enumerate(points): triangle = x, y, *points[i - 1], *point vertices.extend(triangle) self._vertex_list.vertices[:] = vertices def _update_color(self): self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * self._segments * 3 @property def radius(self): """The radius of the circle. :type: float """ return self._radius @radius.setter def radius(self, value): self._radius = value self._update_position() class Line(_ShapeBase): def __init__(self, x, y, x2, y2, width=1, color=(255, 255, 255), batch=None, group=None): """Create a line. The line's anchor point defaults to the center of the line's width on the X axis, and the Y axis. :Parameters: `x` : float The first X coordinate of the line. `y` : float The first Y coordinate of the line. `x2` : float The second X coordinate of the line. `y2` : float The second Y coordinate of the line. `width` : float The desired width of the line. `color` : (int, int, int) The RGB color of the line, specified as a tuple of three ints in the range of 0-255. `batch` : `~pyglet.graphics.Batch` Optional batch to add the line to. `group` : `~pyglet.graphics.Group` Optional parent group of the line. """ self._x = x self._y = y self._x2 = x2 self._y2 = y2 self._width = width self._rotation = math.degrees(math.atan2(y2 - y, x2 - x)) self._rgb = color self._batch = batch or Batch() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, group) self._vertex_list = self._batch.add(6, GL_TRIANGLES, self._group, 'v2f', 'c4B') self._update_position() self._update_color() def _update_position(self): if not self._visible: self._vertex_list.vertices[:] = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) else: x1 = -self._anchor_y y1 = self._anchor_x - self._width / 2 x = self._x y = self._y x2 = x1 + math.hypot(self._y2 - y, self._x2 - x) y2 = y1 + self._width r = math.atan2(self._y2 - y, self._x2 - x) cr = math.cos(r) sr = math.sin(r) ax = x1 * cr - y1 * sr + x ay = x1 * sr + y1 * cr + y bx = x2 * cr - y1 * sr + x by = x2 * sr + y1 * cr + y cx = x2 * cr - y2 * sr + x cy = x2 * sr + y2 * cr + y dx = x1 * cr - y2 * sr + x dy = x1 * sr + y2 * cr + y self._vertex_list.vertices[:] = (ax, ay, bx, by, cx, cy, ax, ay, cx, cy, dx, dy) def _update_color(self): self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * 6 @property def x2(self): """Second X coordinate of the shape. :type: int or float """ return self._x2 @x2.setter def x2(self, value): self._x2 = value self._update_position() @property def y2(self): """Second Y coordinate of the shape. :type: int or float """ return self._y2 @y2.setter def y2(self, value): self._y2 = value self._update_position() @property def position(self): """The (x, y, x2, y2) coordinates of the line, as a tuple. :Parameters: `x` : int or float X coordinate of the line. `y` : int or float Y coordinate of the line. `x2` : int or float X2 coordinate of the line. `y2` : int or float Y2 coordinate of the line. """ return self._x, self._y, self._x2, self._y2 @position.setter def position(self, values): self._x, self._y, self._x2, self._y2 = values self._update_position() class Rectangle(_ShapeBase): def __init__(self, x, y, width, height, color=(255, 255, 255), batch=None, group=None): """Create a rectangle or square. The rectangles's anchor point defaults to the (x, y) coordinates, which are at the bottom left. :Parameters: `x` : float The X coordinate of the rectangle. `y` : float The Y coordinate of the rectangle. `width` : float The width of the rectangle. `height` : float The height of the rectangle. `color` : (int, int, int) The RGB color of the rectangle, specified as a tuple of three ints in the range of 0-255. `batch` : `~pyglet.graphics.Batch` Optional batch to add the rectangle to. `group` : `~pyglet.graphics.Group` Optional parent group of the rectangle. """ self._x = x self._y = y self._width = width self._height = height self._rotation = 0 self._rgb = color self._batch = batch or Batch() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, group) self._vertex_list = self._batch.add(6, GL_TRIANGLES, self._group, 'v2f', 'c4B') self._update_position() self._update_color() def _update_position(self): if not self._visible: self._vertex_list.vertices = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) elif self._rotation: x1 = -self._anchor_x y1 = -self._anchor_y x2 = x1 + self._width y2 = y1 + self._height x = self._x y = self._y r = -math.radians(self._rotation) cr = math.cos(r) sr = math.sin(r) ax = x1 * cr - y1 * sr + x ay = x1 * sr + y1 * cr + y bx = x2 * cr - y1 * sr + x by = x2 * sr + y1 * cr + y cx = x2 * cr - y2 * sr + x cy = x2 * sr + y2 * cr + y dx = x1 * cr - y2 * sr + x dy = x1 * sr + y2 * cr + y self._vertex_list.vertices = (ax, ay, bx, by, cx, cy, ax, ay, cx, cy, dx, dy) else: x1 = self._x - self._anchor_x y1 = self._y - self._anchor_y x2 = x1 + self._width y2 = y1 + self._height self._vertex_list.vertices = (x1, y1, x2, y1, x2, y2, x1, y1, x2, y2, x1, y2) def _update_color(self): self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * 6 @property def width(self): """The width of the rectangle. :type: float """ return self._width @width.setter def width(self, value): self._width = value self._update_position() @property def height(self): """The height of the rectangle. :type: float """ return self._height @height.setter def height(self, value): self._height = value self._update_position() @property def rotation(self): """Clockwise rotation of the rectangle, in degrees. The Rectangle will be rotated about its (anchor_x, anchor_y) position. :type: float """ return self._rotation @rotation.setter def rotation(self, rotation): self._rotation = rotation self._update_position() class BorderedRectangle(_ShapeBase): def __init__(self, x, y, width, height, border=1, color=(255, 255, 255), border_color=(100, 100, 100), batch=None, group=None): """Create a rectangle or square. The rectangles's anchor point defaults to the (x, y) coordinates, which are at the bottom left. :Parameters: `x` : float The X coordinate of the rectangle. `y` : float The Y coordinate of the rectangle. `width` : float The width of the rectangle. `height` : float The height of the rectangle. `color` : (int, int, int) The RGB color of the rectangle, specified as a tuple of three ints in the range of 0-255. `batch` : `~pyglet.graphics.Batch` Optional batch to add the rectangle to. `group` : `~pyglet.graphics.Group` Optional parent group of the rectangle. """ self._x = x self._y = y self._width = width self._height = height self._border = border self._rgb = color self._brgb = border_color self._batch = batch or Batch() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, group) indices = [0, 1, 2, 0, 2, 3, 0, 4, 3, 4, 7, 3, 0, 1, 5, 0, 5, 4, 1, 2, 5, 5, 2, 6, 6, 2, 3, 6, 3, 7] self._vertex_list = self._batch.add_indexed(8, GL_TRIANGLES, self._group, indices, 'v2f', 'c4B') self._update_position() self._update_color() def _update_position(self): if not self._visible: self._vertex_list.vertices = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) else: b = self._border bx1 = self._x - self._anchor_x by1 = self._y - self._anchor_y bx2 = bx1 + self._width by2 = by1 + self._height ix1 = bx1 + b iy1 = by1 + b ix2 = bx2 - b iy2 = by2 - b self._vertex_list.vertices[:] = (ix1, iy1, ix2, iy1, ix2, iy2, ix1, iy2, bx1, by1, bx2, by1, bx2, by2, bx1, by2,) def _update_color(self): opacity = int(self._opacity) self._vertex_list.colors[:] = [*self._rgb, opacity] * 4 + [*self._brgb, opacity] * 4 @property def width(self): """The width of the rectangle. :type: float """ return self._width @width.setter def width(self, value): self._width = value self._update_position() @property def height(self): """The height of the rectangle. :type: float """ return self._height @height.setter def height(self, value): self._height = value self._update_position() class Triangle(_ShapeBase): def __init__(self, x, y, x2, y2, x3, y3, color=(255, 255, 255), batch=None, group=None): """Create a triangle. The triangle's anchor point defaults to the first vertex point. :Parameters: `x` : float The first X coordinate of the triangle. `y` : float The first Y coordinate of the triangle. `x2` : float The second X coordinate of the triangle. `y2` : float The second Y coordinate of the triangle. `x3` : float The third X coordinate of the triangle. `y3` : float The third Y coordinate of the triangle. `color` : (int, int, int) The RGB color of the triangle, specified as a tuple of three ints in the range of 0-255. `batch` : `~pyglet.graphics.Batch` Optional batch to add the triangle to. `group` : `~pyglet.graphics.Group` Optional parent group of the triangle. """ self._x = x self._y = y self._x2 = x2 self._y2 = y2 self._x3 = x3 self._y3 = y3 self._rotation = 0 self._rgb = color self._batch = batch or Batch() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, group) self._vertex_list = self._batch.add(3, GL_TRIANGLES, self._group, 'v2f', 'c4B') self._update_position() self._update_color() def _update_position(self): if not self._visible: self._vertex_list.vertices = (0, 0, 0, 0, 0, 0) else: anchor_x = self._anchor_x anchor_y = self._anchor_y x1 = self._x - anchor_x y1 = self._y - anchor_y x2 = self._x2 - anchor_x y2 = self._y2 - anchor_y x3 = self._x3 - anchor_x y3 = self._y3 - anchor_y self._vertex_list.vertices = (x1, y1, x2, y2, x3, y3) def _update_color(self): self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * 3 @property def x2(self): """Second X coordinate of the shape. :type: int or float """ return self._x2 @x2.setter def x2(self, value): self._x2 = value self._update_position() @property def y2(self): """Second Y coordinate of the shape. :type: int or float """ return self._y2 @y2.setter def y2(self, value): self._y2 = value self._update_position() @property def x3(self): """Third X coordinate of the shape. :type: int or float """ return self._x3 @x3.setter def x3(self, value): self._x3 = value self._update_position() @property def y3(self): """Third Y coordinate of the shape. :type: int or float """ return self._y3 @y3.setter def y3(self, value): self._y3 = value self._update_position() @property def position(self): """The (x, y, x2, y2, x3, y3) coordinates of the triangle, as a tuple. :Parameters: `x` : int or float X coordinate of the triangle. `y` : int or float Y coordinate of the triangle. `x2` : int or float X2 coordinate of the triangle. `y2` : int or float Y2 coordinate of the triangle. `x3` : int or float X3 coordinate of the triangle. `y3` : int or float Y3 coordinate of the triangle. """ return self._x, self._y, self._x2, self._y2, self._x3, self._y3 @position.setter def position(self, values): self._x, self._y, self._x2, self._y2, self._x3, self._y3 = values self._update_position() __all__ = ('Arc', 'Circle', 'Line', 'Rectangle', 'BorderedRectangle', 'Triangle')