1# ----------------------------------------------------------------------------
2# pyglet
3# Copyright (c) 2006-2008 Alex Holkner
4# Copyright (c) 2008-2021 pyglet contributors
5# All rights reserved.
6#
7# Redistribution and use in source and binary forms, with or without
8# modification, are permitted provided that the following conditions
9# are met:
10#
11#  * Redistributions of source code must retain the above copyright
12#    notice, this list of conditions and the following disclaimer.
13#  * Redistributions in binary form must reproduce the above copyright
14#    notice, this list of conditions and the following disclaimer in
15#    the documentation and/or other materials provided with the
16#    distribution.
17#  * Neither the name of pyglet nor the names of its
18#    contributors may be used to endorse or promote products
19#    derived from this software without specific prior written
20#    permission.
21#
22# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
25# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
26# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
27# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
28# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
29# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
31# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
32# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
33# POSSIBILITY OF SUCH DAMAGE.
34# ----------------------------------------------------------------------------
35
36"""2D shapes.
37
38This module provides classes for a variety of simplistic 2D shapes,
39such as Rectangles, Circles, and Lines. These shapes are made
40internally from OpenGL primitives, and provide excellent performance
41when drawn as part of a :py:class:`~pyglet.graphics.Batch`.
42Convenience methods are provided for positioning, changing color
43and opacity, and rotation (where applicable). To create more
44complex shapes than what is provided here, the lower level
45graphics API is more appropriate.
46See the :ref:`guide_graphics` for more details.
47
48A simple example of drawing shapes::
49
50    import pyglet
51    from pyglet import shapes
52
53    window = pyglet.window.Window(960, 540)
54    batch = pyglet.graphics.Batch()
55
56    circle = shapes.Circle(700, 150, 100, color=(50, 225, 30), batch=batch)
57    square = shapes.Rectangle(200, 200, 200, 200, color=(55, 55, 255), batch=batch)
58    rectangle = shapes.Rectangle(250, 300, 400, 200, color=(255, 22, 20), batch=batch)
59    rectangle.opacity = 128
60    rectangle.rotation = 33
61    line = shapes.Line(100, 100, 100, 200, width=19, batch=batch)
62    line2 = shapes.Line(150, 150, 444, 111, width=4, color=(200, 20, 20), batch=batch)
63    star = shapes.Star(800, 400, 60, 40, num_spikes=20, color=(255, 255, 0), batch=batch)
64
65    @window.event
66    def on_draw():
67        window.clear()
68        batch.draw()
69
70    pyglet.app.run()
71
72
73
74.. versionadded:: 1.5.4
75"""
76
77import math
78
79from pyglet.gl import GL_COLOR_BUFFER_BIT, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA
80from pyglet.gl import GL_TRIANGLES, GL_LINES, GL_BLEND
81from pyglet.gl import glPushAttrib, glPopAttrib, glBlendFunc, glEnable, glDisable
82from pyglet.graphics import Group, Batch
83
84
85class _ShapeGroup(Group):
86    """Shared Shape rendering Group.
87
88    The group is automatically coalesced with other shape groups
89    sharing the same parent group and blend parameters.
90    """
91
92    def __init__(self, blend_src, blend_dest, parent=None):
93        """Create a Shape group.
94
95        The group is created internally. Usually you do not
96        need to explicitly create it.
97
98        :Parameters:
99            `blend_src` : int
100                OpenGL blend source mode; for example,
101                ``GL_SRC_ALPHA``.
102            `blend_dest` : int
103                OpenGL blend destination mode; for example,
104                ``GL_ONE_MINUS_SRC_ALPHA``.
105            `parent` : `~pyglet.graphics.Group`
106                Optional parent group.
107        """
108        super().__init__(parent)
109        self.blend_src = blend_src
110        self.blend_dest = blend_dest
111
112    def set_state(self):
113        glPushAttrib(GL_COLOR_BUFFER_BIT)
114        glEnable(GL_BLEND)
115        glBlendFunc(self.blend_src, self.blend_dest)
116
117    def unset_state(self):
118        glDisable(GL_BLEND)
119        glPopAttrib()
120
121    def __eq__(self, other):
122        return (other.__class__ is self.__class__ and
123                self.parent is other.parent and
124                self.blend_src == other.blend_src and
125                self.blend_dest == other.blend_dest)
126
127    def __hash__(self):
128        return hash((id(self.parent), self.blend_src, self.blend_dest))
129
130
131class _ShapeBase:
132    """Base class for Shape objects"""
133
134    _rgb = (255, 255, 255)
135    _opacity = 255
136    _visible = True
137    _x = 0
138    _y = 0
139    _anchor_x = 0
140    _anchor_y = 0
141    _batch = None
142    _group = None
143    _vertex_list = None
144
145    def __del__(self):
146        if self._vertex_list is not None:
147            self._vertex_list.delete()
148
149    def _update_position(self):
150        raise NotImplementedError
151
152    def _update_color(self):
153        raise NotImplementedError
154
155    def draw(self):
156        """Draw the shape at its current position.
157
158        Using this method is not recommended. Instead, add the
159        shape to a `pyglet.graphics.Batch` for efficient rendering.
160        """
161        self._group.set_state_recursive()
162        self._vertex_list.draw(GL_TRIANGLES)
163        self._group.unset_state_recursive()
164
165    def delete(self):
166        self._vertex_list.delete()
167        self._vertex_list = None
168
169    @property
170    def x(self):
171        """X coordinate of the shape.
172
173        :type: int or float
174        """
175        return self._x
176
177    @x.setter
178    def x(self, value):
179        self._x = value
180        self._update_position()
181
182    @property
183    def y(self):
184        """Y coordinate of the shape.
185
186        :type: int or float
187        """
188        return self._y
189
190    @y.setter
191    def y(self, value):
192        self._y = value
193        self._update_position()
194
195    @property
196    def position(self):
197        """The (x, y) coordinates of the shape, as a tuple.
198
199        :Parameters:
200            `x` : int or float
201                X coordinate of the sprite.
202            `y` : int or float
203                Y coordinate of the sprite.
204        """
205        return self._x, self._y
206
207    @position.setter
208    def position(self, values):
209        self._x, self._y = values
210        self._update_position()
211
212    @property
213    def anchor_x(self):
214        """The X coordinate of the anchor point
215
216        :type: int or float
217        """
218        return self._anchor_x
219
220    @anchor_x.setter
221    def anchor_x(self, value):
222        self._anchor_x = value
223        self._update_position()
224
225    @property
226    def anchor_y(self):
227        """The Y coordinate of the anchor point
228
229        :type: int or float
230        """
231        return self._anchor_y
232
233    @anchor_y.setter
234    def anchor_y(self, value):
235        self._anchor_y = value
236        self._update_position()
237
238    @property
239    def anchor_position(self):
240        """The (x, y) coordinates of the anchor point, as a tuple.
241
242        :Parameters:
243            `x` : int or float
244                X coordinate of the anchor point.
245            `y` : int or float
246                Y coordinate of the anchor point.
247        """
248        return self._anchor_x, self._anchor_y
249
250    @anchor_position.setter
251    def anchor_position(self, values):
252        self._anchor_x, self._anchor_y = values
253        self._update_position()
254
255    @property
256    def color(self):
257        """The shape color.
258
259        This property sets the color of the shape.
260
261        The color is specified as an RGB tuple of integers '(red, green, blue)'.
262        Each color component must be in the range 0 (dark) to 255 (saturated).
263
264        :type: (int, int, int)
265        """
266        return self._rgb
267
268    @color.setter
269    def color(self, values):
270        self._rgb = list(map(int, values))
271        self._update_color()
272
273    @property
274    def opacity(self):
275        """Blend opacity.
276
277        This property sets the alpha component of the color of the shape.
278        With the default blend mode (see the constructor), this allows the
279        shape to be drawn with fractional opacity, blending with the
280        background.
281
282        An opacity of 255 (the default) has no effect.  An opacity of 128
283        will make the shape appear translucent.
284
285        :type: int
286        """
287        return self._opacity
288
289    @opacity.setter
290    def opacity(self, value):
291        self._opacity = value
292        self._update_color()
293
294    @property
295    def visible(self):
296        """True if the shape will be drawn.
297
298        :type: bool
299        """
300        return self._visible
301
302    @visible.setter
303    def visible(self, value):
304        self._visible = value
305        self._update_position()
306
307
308class Arc(_ShapeBase):
309    def __init__(self, x, y, radius, segments=None, angle=math.tau, start_angle=0,
310                 closed=False, color=(255, 255, 255), batch=None, group=None):
311        """Create an Arc.
312
313        The Arc's anchor point (x, y) defaults to it's center.
314
315        :Parameters:
316            `x` : float
317                X coordinate of the circle.
318            `y` : float
319                Y coordinate of the circle.
320            `radius` : float
321                The desired radius.
322            `segments` : int
323                You can optionally specify how many distinct line segments
324                the arc should be made from. If not specified it will be
325                automatically calculated using the formula:
326                `max(14, int(radius / 1.25))`.
327            `angle` : float
328                The angle of the arc, in radians. Defaults to tau (pi * 2),
329                which is a full circle.
330            `start_angle` : float
331                The start angle of the arc, in radians. Defaults to 0.
332            `closed` : bool
333                If True, the ends of the arc will be connected with a line.
334                defaults to False.
335            `color` : (int, int, int)
336                The RGB color of the circle, specified as a tuple of
337                three ints in the range of 0-255.
338            `batch` : `~pyglet.graphics.Batch`
339                Optional batch to add the circle to.
340            `group` : `~pyglet.graphics.Group`
341                Optional parent group of the circle.
342        """
343        self._x = x
344        self._y = y
345        self._radius = radius
346        self._segments = segments or max(14, int(radius / 1.25))
347        self._num_verts = self._segments * 2 + (2 if closed else 0)
348
349        self._rgb = color
350        self._angle = angle
351        self._start_angle = start_angle
352        self._closed = closed
353        self._rotation = 0
354
355        self._batch = batch or Batch()
356        self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, group)
357
358        self._vertex_list = self._batch.add(self._num_verts, GL_LINES, self._group, 'v2f', 'c4B')
359        self._update_position()
360        self._update_color()
361
362    def _update_position(self):
363        if not self._visible:
364            vertices = (0,) * self._segments * 4
365        else:
366            x = self._x + self._anchor_x
367            y = self._y + self._anchor_y
368            r = self._radius
369            tau_segs = self._angle / self._segments
370            start_angle = self._start_angle - math.radians(self._rotation)
371
372            # Calculate the outer points of the arc:
373            points = [(x + (r * math.cos((i * tau_segs) + start_angle)),
374                       y + (r * math.sin((i * tau_segs) + start_angle))) for i in range(self._segments + 1)]
375
376            # Create a list of doubled-up points from the points:
377            vertices = []
378            for i in range(len(points) - 1):
379                line_points = *points[i], *points[i + 1]
380                vertices.extend(line_points)
381
382            if self._closed:
383                chord_points = *points[-1], *points[0]
384                vertices.extend(chord_points)
385
386        self._vertex_list.vertices[:] = vertices
387
388    def _update_color(self):
389        self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * self._num_verts
390
391    @property
392    def rotation(self):
393        """Clockwise rotation of the arc, in degrees.
394
395        The arc will be rotated about its (anchor_x, anchor_y)
396        position.
397
398        :type: float
399        """
400        return self._rotation
401
402    @rotation.setter
403    def rotation(self, rotation):
404        self._rotation = rotation
405        self._update_position()
406
407    def draw(self):
408        """Draw the shape at its current position.
409
410        Using this method is not recommended. Instead, add the
411        shape to a `pyglet.graphics.Batch` for efficient rendering.
412        """
413        self._vertex_list.draw(GL_LINES)
414
415
416class Circle(_ShapeBase):
417    def __init__(self, x, y, radius, segments=None, color=(255, 255, 255), batch=None, group=None):
418        """Create a circle.
419
420        The circle's anchor point (x, y) defaults to the center of the circle.
421
422        :Parameters:
423            `x` : float
424                X coordinate of the circle.
425            `y` : float
426                Y coordinate of the circle.
427            `radius` : float
428                The desired radius.
429            `segments` : int
430                You can optionally specify how many distinct triangles
431                the circle should be made from. If not specified it will
432                be automatically calculated based using the formula:
433                `max(14, int(radius / 1.25))`.
434            `color` : (int, int, int)
435                The RGB color of the circle, specified as a tuple of
436                three ints in the range of 0-255.
437            `batch` : `~pyglet.graphics.Batch`
438                Optional batch to add the circle to.
439            `group` : `~pyglet.graphics.Group`
440                Optional parent group of the circle.
441        """
442        self._x = x
443        self._y = y
444        self._radius = radius
445        self._segments = segments or max(14, int(radius / 1.25))
446        self._rgb = color
447
448        self._batch = batch or Batch()
449        self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, group)
450
451        self._vertex_list = self._batch.add(self._segments * 3, GL_TRIANGLES, self._group, 'v2f', 'c4B')
452        self._update_position()
453        self._update_color()
454
455    def _update_position(self):
456        if not self._visible:
457            vertices = (0,) * self._segments * 6
458        else:
459            x = self._x + self._anchor_x
460            y = self._y + self._anchor_y
461            r = self._radius
462            tau_segs = math.pi * 2 / self._segments
463
464            # Calculate the outer points of the circle:
465            points = [(x + (r * math.cos(i * tau_segs)),
466                       y + (r * math.sin(i * tau_segs))) for i in range(self._segments)]
467
468            # Create a list of triangles from the points:
469            vertices = []
470            for i, point in enumerate(points):
471                triangle = x, y, *points[i - 1], *point
472                vertices.extend(triangle)
473
474        self._vertex_list.vertices[:] = vertices
475
476    def _update_color(self):
477        self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * self._segments * 3
478
479    @property
480    def radius(self):
481        """The radius of the circle.
482
483        :type: float
484        """
485        return self._radius
486
487    @radius.setter
488    def radius(self, value):
489        self._radius = value
490        self._update_position()
491
492
493class Ellipse(_ShapeBase):
494    def __init__(self, x, y, a, b, color=(255, 255, 255), batch=None, group=None):
495        """Create an ellipse.
496
497        The ellipse's anchor point (x, y) defaults to the center of the ellipse.
498
499        :Parameters:
500            `x` : float
501                X coordinate of the ellipse.
502            `y` : float
503                Y coordinate of the ellipse.
504            `a` : float
505                Semi-major axes of the ellipse.
506            `b`: float
507                Semi-minor axes of the ellipse.
508            `color` : (int, int, int)
509                The RGB color of the ellipse. specify as a tuple of
510                three ints in the range of 0~255.
511            `batch` : `~pyglet.graphics.Batch`
512                Optional batch to add the circle to.
513            `group` : `~pyglet.graphics.Group`
514                Optional parent group of the circle.
515        """
516        self._x = x
517        self._y = y
518        self._a = a
519        self._b = b
520        self._rgb = color
521        self._rotation = 0
522        self._segments = int(max(a, b) / 1.25)
523        self._num_verts = self._segments * 2
524
525        self._batch = batch or Batch()
526        self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, group)
527        self._vertex_list = self._batch.add(self._num_verts, GL_LINES, self._group, 'v2f', 'c4B')
528
529        self._update_position()
530        self._update_color()
531
532    def _update_position(self):
533        if not self._visible:
534            vertices = (0,) * self._num_verts * 4
535        else:
536            x = self._x + self._anchor_x
537            y = self._y + self._anchor_y
538            tau_segs = math.pi * 2 / self._segments
539
540            # Calculate the points of the ellipse by formula:
541            points = [(x + self._a * math.cos(i * tau_segs),
542                       y + self._b * math.sin(i * tau_segs)) for i in range(self._segments + 1)]
543
544            # Rotate all points:
545            if self._rotation:
546                r = -math.radians(self._rotation)
547                cr = math.cos(r)
548                sr = math.sin(r)
549                now_points = []
550                for point in points:
551                    now_x = (point[0] - x) * cr - (point[1] - y) * sr + x
552                    now_y = (point[1] - y) * cr + (point[0] - x) * sr + y
553                    now_points.append((now_x, now_y))
554                points = now_points
555
556            # Create a list of lines from the points:
557            vertices = []
558            for i in range(len(points) - 1):
559                line_points = *points[i], *points[i + 1]
560                vertices.extend(line_points)
561        self._vertex_list.vertices[:] = vertices
562
563    def _update_color(self):
564        self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * self._num_verts
565
566    @property
567    def a(self):
568        """The semi-major axes of the ellipse.
569
570        :type: float
571        """
572        return self._a
573
574    @a.setter
575    def a(self, value):
576        self._a = value
577        self._update_position()
578
579    @property
580    def b(self):
581        """The semi-minor axes of the ellipse.
582
583        :type: float
584        """
585        return self._b
586
587    @b.setter
588    def b(self, value):
589        self._b = value
590        self._update_position()
591
592    @property
593    def rotation(self):
594        """Clockwise rotation of the arc, in degrees.
595
596        The arc will be rotated about its (anchor_x, anchor_y)
597        position.
598
599        :type: float
600        """
601        return self._rotation
602
603    @rotation.setter
604    def rotation(self, rotation):
605        self._rotation = rotation
606        self._update_position()
607
608    def draw(self):
609        """Draw the shape at its current position.
610
611        Using this method is not recommended. Instead, add the
612        shape to a `pyglet.graphics.Batch` for efficient rendering.
613        """
614        self._vertex_list.draw(GL_LINES)
615
616
617class Sector(_ShapeBase):
618    def __init__(self, x, y, radius, segments=None, angle=math.tau, start_angle=0,
619                 color=(255, 255, 255), batch=None, group=None):
620        """Create a sector of a circle.
621
622                The sector's anchor point (x, y) defaults to the center of the circle.
623
624                :Parameters:
625                    `x` : float
626                        X coordinate of the sector.
627                    `y` : float
628                        Y coordinate of the sector.
629                    `radius` : float
630                        The desired radius.
631                    `segments` : int
632                        You can optionally specify how many distinct triangles
633                        the sector should be made from. If not specified it will
634                        be automatically calculated based using the formula:
635                        `max(14, int(radius / 1.25))`.
636                    `angle` : float
637                        The angle of the sector, in radians. Defaults to tau (pi * 2),
638                        which is a full circle.
639                    `start_angle` : float
640                        The start angle of the sector, in radians. Defaults to 0.
641                    `color` : (int, int, int)
642                        The RGB color of the sector, specified as a tuple of
643                        three ints in the range of 0-255.
644                    `batch` : `~pyglet.graphics.Batch`
645                        Optional batch to add the sector to.
646                    `group` : `~pyglet.graphics.Group`
647                        Optional parent group of the sector.
648                """
649        self._x = x
650        self._y = y
651        self._radius = radius
652        self._segments = segments or max(14, int(radius / 1.25))
653
654        self._rgb = color
655        self._angle = angle
656        self._start_angle = start_angle
657        self._rotation = 0
658
659        self._batch = batch or Batch()
660        self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, group)
661
662        self._vertex_list = self._batch.add(self._segments * 3, GL_TRIANGLES, self._group, 'v2f', 'c4B')
663        self._update_position()
664        self._update_color()
665
666    def _update_position(self):
667        if not self._visible:
668            vertices = (0,) * self._segments * 6
669        else:
670            x = self._x + self._anchor_x
671            y = self._y + self._anchor_y
672            r = self._radius
673            tau_segs = self._angle / self._segments
674            start_angle = self._start_angle - math.radians(self._rotation)
675
676            # Calculate the outer points of the sector.
677            points = [(x + (r * math.cos((i * tau_segs) + start_angle)),
678                       y + (r * math.sin((i * tau_segs) + start_angle))) for i in range(self._segments + 1)]
679
680            # Create a list of triangles from the points
681            vertices = []
682            for i, point in enumerate(points[1:], start=1):
683                triangle = x, y, *points[i - 1], *point
684                vertices.extend(triangle)
685
686        self._vertex_list.vertices[:] = vertices
687
688    def _update_color(self):
689        self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * self._segments * 3
690
691    @property
692    def radius(self):
693        """The radius of the circle.
694
695        :type: float
696        """
697        return self._radius
698
699    @radius.setter
700    def radius(self, value):
701        self._radius = value
702        self._update_position()
703
704    @property
705    def rotation(self):
706        """Clockwise rotation of the sector, in degrees.
707
708        The sector will be rotated about its (anchor_x, anchor_y)
709        position.
710
711        :type: float
712        """
713        return self._rotation
714
715    @rotation.setter
716    def rotation(self, rotation):
717        self._rotation = rotation
718        self._update_position()
719
720
721class Line(_ShapeBase):
722    def __init__(self, x, y, x2, y2, width=1, color=(255, 255, 255), batch=None, group=None):
723        """Create a line.
724
725        The line's anchor point defaults to the center of the line's
726        width on the X axis, and the Y axis.
727
728        :Parameters:
729            `x` : float
730                The first X coordinate of the line.
731            `y` : float
732                The first Y coordinate of the line.
733            `x2` : float
734                The second X coordinate of the line.
735            `y2` : float
736                The second Y coordinate of the line.
737            `width` : float
738                The desired width of the line.
739            `color` : (int, int, int)
740                The RGB color of the line, specified as a tuple of
741                three ints in the range of 0-255.
742            `batch` : `~pyglet.graphics.Batch`
743                Optional batch to add the line to.
744            `group` : `~pyglet.graphics.Group`
745                Optional parent group of the line.
746        """
747        self._x = x
748        self._y = y
749        self._x2 = x2
750        self._y2 = y2
751
752        self._width = width
753        self._rotation = math.degrees(math.atan2(y2 - y, x2 - x))
754        self._rgb = color
755
756        self._batch = batch or Batch()
757        self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, group)
758        self._vertex_list = self._batch.add(6, GL_TRIANGLES, self._group, 'v2f', 'c4B')
759        self._update_position()
760        self._update_color()
761
762    def _update_position(self):
763        if not self._visible:
764            self._vertex_list.vertices[:] = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
765        else:
766            x1 = -self._anchor_y
767            y1 = self._anchor_x - self._width / 2
768            x = self._x
769            y = self._y
770            x2 = x1 + math.hypot(self._y2 - y, self._x2 - x)
771            y2 = y1 + self._width
772
773            r = math.atan2(self._y2 - y, self._x2 - x)
774            cr = math.cos(r)
775            sr = math.sin(r)
776            ax = x1 * cr - y1 * sr + x
777            ay = x1 * sr + y1 * cr + y
778            bx = x2 * cr - y1 * sr + x
779            by = x2 * sr + y1 * cr + y
780            cx = x2 * cr - y2 * sr + x
781            cy = x2 * sr + y2 * cr + y
782            dx = x1 * cr - y2 * sr + x
783            dy = x1 * sr + y2 * cr + y
784            self._vertex_list.vertices[:] = (ax, ay, bx, by, cx, cy, ax, ay, cx, cy, dx, dy)
785
786    def _update_color(self):
787        self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * 6
788
789    @property
790    def x2(self):
791        """Second X coordinate of the shape.
792
793        :type: int or float
794        """
795        return self._x2
796
797    @x2.setter
798    def x2(self, value):
799        self._x2 = value
800        self._update_position()
801
802    @property
803    def y2(self):
804        """Second Y coordinate of the shape.
805
806        :type: int or float
807        """
808        return self._y2
809
810    @y2.setter
811    def y2(self, value):
812        self._y2 = value
813        self._update_position()
814
815    @property
816    def position(self):
817        """The (x, y, x2, y2) coordinates of the line, as a tuple.
818
819        :Parameters:
820            `x` : int or float
821                X coordinate of the line.
822            `y` : int or float
823                Y coordinate of the line.
824            `x2` : int or float
825                X2 coordinate of the line.
826            `y2` : int or float
827                Y2 coordinate of the line.
828        """
829        return self._x, self._y, self._x2, self._y2
830
831    @position.setter
832    def position(self, values):
833        self._x, self._y, self._x2, self._y2 = values
834        self._update_position()
835
836
837class Rectangle(_ShapeBase):
838    def __init__(self, x, y, width, height, color=(255, 255, 255), batch=None, group=None):
839        """Create a rectangle or square.
840
841        The rectangle's anchor point defaults to the (x, y) coordinates,
842        which are at the bottom left.
843
844        :Parameters:
845            `x` : float
846                The X coordinate of the rectangle.
847            `y` : float
848                The Y coordinate of the rectangle.
849            `width` : float
850                The width of the rectangle.
851            `height` : float
852                The height of the rectangle.
853            `color` : (int, int, int)
854                The RGB color of the rectangle, specified as
855                a tuple of three ints in the range of 0-255.
856            `batch` : `~pyglet.graphics.Batch`
857                Optional batch to add the rectangle to.
858            `group` : `~pyglet.graphics.Group`
859                Optional parent group of the rectangle.
860        """
861        self._x = x
862        self._y = y
863        self._width = width
864        self._height = height
865        self._rotation = 0
866        self._rgb = color
867
868        self._batch = batch or Batch()
869        self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, group)
870        self._vertex_list = self._batch.add(6, GL_TRIANGLES, self._group, 'v2f', 'c4B')
871        self._update_position()
872        self._update_color()
873
874    def _update_position(self):
875        if not self._visible:
876            self._vertex_list.vertices = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
877        elif self._rotation:
878            x1 = -self._anchor_x
879            y1 = -self._anchor_y
880            x2 = x1 + self._width
881            y2 = y1 + self._height
882            x = self._x
883            y = self._y
884
885            r = -math.radians(self._rotation)
886            cr = math.cos(r)
887            sr = math.sin(r)
888            ax = x1 * cr - y1 * sr + x
889            ay = x1 * sr + y1 * cr + y
890            bx = x2 * cr - y1 * sr + x
891            by = x2 * sr + y1 * cr + y
892            cx = x2 * cr - y2 * sr + x
893            cy = x2 * sr + y2 * cr + y
894            dx = x1 * cr - y2 * sr + x
895            dy = x1 * sr + y2 * cr + y
896            self._vertex_list.vertices = (ax, ay, bx, by, cx, cy, ax, ay, cx, cy, dx, dy)
897        else:
898            x1 = self._x - self._anchor_x
899            y1 = self._y - self._anchor_y
900            x2 = x1 + self._width
901            y2 = y1 + self._height
902            self._vertex_list.vertices = (x1, y1, x2, y1, x2, y2, x1, y1, x2, y2, x1, y2)
903
904    def _update_color(self):
905        self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * 6
906
907    @property
908    def width(self):
909        """The width of the rectangle.
910
911        :type: float
912        """
913        return self._width
914
915    @width.setter
916    def width(self, value):
917        self._width = value
918        self._update_position()
919
920    @property
921    def height(self):
922        """The height of the rectangle.
923
924        :type: float
925        """
926        return self._height
927
928    @height.setter
929    def height(self, value):
930        self._height = value
931        self._update_position()
932
933    @property
934    def rotation(self):
935        """Clockwise rotation of the rectangle, in degrees.
936
937        The Rectangle will be rotated about its (anchor_x, anchor_y)
938        position.
939
940        :type: float
941        """
942        return self._rotation
943
944    @rotation.setter
945    def rotation(self, rotation):
946        self._rotation = rotation
947        self._update_position()
948
949
950class BorderedRectangle(_ShapeBase):
951    def __init__(self, x, y, width, height, border=1, color=(255, 255, 255),
952                 border_color=(100, 100, 100), batch=None, group=None):
953        """Create a rectangle or square.
954
955        The rectangle's anchor point defaults to the (x, y) coordinates,
956        which are at the bottom left.
957
958        :Parameters:
959            `x` : float
960                The X coordinate of the rectangle.
961            `y` : float
962                The Y coordinate of the rectangle.
963            `width` : float
964                The width of the rectangle.
965            `height` : float
966                The height of the rectangle.
967            `border` : float
968                The thickness of the border.
969            `color` : (int, int, int)
970                The RGB color of the rectangle, specified as
971                a tuple of three ints in the range of 0-255.
972            `border_color` : (int, int, int)
973                The RGB color of the rectangle's border, specified as
974                a tuple of three ints in the range of 0-255.
975            `batch` : `~pyglet.graphics.Batch`
976                Optional batch to add the rectangle to.
977            `group` : `~pyglet.graphics.Group`
978                Optional parent group of the rectangle.
979        """
980        self._x = x
981        self._y = y
982        self._width = width
983        self._height = height
984        self._rotation = 0
985        self._border = border
986        self._rgb = color
987        self._brgb = border_color
988
989        self._batch = batch or Batch()
990        self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, group)
991        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]
992        self._vertex_list = self._batch.add_indexed(8, GL_TRIANGLES, self._group, indices, 'v2f', 'c4B')
993        self._update_position()
994        self._update_color()
995
996    def _update_position(self):
997        if not self._visible:
998            self._vertex_list.vertices = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
999        elif self._rotation:
1000            b = self._border
1001            x = self._x
1002            y = self._y
1003
1004            bx1 = -self._anchor_x
1005            by1 = -self._anchor_y
1006            bx2 = bx1 + self._width
1007            by2 = by1 + self._height
1008            ix1 = bx1 + b
1009            iy1 = by1 + b
1010            ix2 = bx2 - b
1011            iy2 = by2 - b
1012
1013            r = -math.radians(self._rotation)
1014            cr = math.cos(r)
1015            sr = math.sin(r)
1016
1017            bax = bx1 * cr - by1 * sr + x
1018            bay = bx1 * sr + by1 * cr + y
1019            bbx = bx2 * cr - by1 * sr + x
1020            bby = bx2 * sr + by1 * cr + y
1021            bcx = bx2 * cr - by2 * sr + x
1022            bcy = bx2 * sr + by2 * cr + y
1023            bdx = bx1 * cr - by2 * sr + x
1024            bdy = bx1 * sr + by2 * cr + y
1025
1026            iax = ix1 * cr - iy1 * sr + x
1027            iay = ix1 * sr + iy1 * cr + y
1028            ibx = ix2 * cr - iy1 * sr + x
1029            iby = ix2 * sr + iy1 * cr + y
1030            icx = ix2 * cr - iy2 * sr + x
1031            icy = ix2 * sr + iy2 * cr + y
1032            idx = ix1 * cr - iy2 * sr + x
1033            idy = ix1 * sr + iy2 * cr + y
1034
1035            self._vertex_list.vertices[:] = (iax, iay, ibx, iby, icx, icy, idx, idy,
1036                                             bax, bay, bbx, bby, bcx, bcy, bdx, bdy,)
1037        else:
1038            b = self._border
1039            bx1 = self._x - self._anchor_x
1040            by1 = self._y - self._anchor_y
1041            bx2 = bx1 + self._width
1042            by2 = by1 + self._height
1043            ix1 = bx1 + b
1044            iy1 = by1 + b
1045            ix2 = bx2 - b
1046            iy2 = by2 - b
1047            self._vertex_list.vertices[:] = (ix1, iy1, ix2, iy1, ix2, iy2, ix1, iy2,
1048                                             bx1, by1, bx2, by1, bx2, by2, bx1, by2,)
1049
1050    def _update_color(self):
1051        opacity = int(self._opacity)
1052        self._vertex_list.colors[:] = [*self._rgb, opacity] * 4 + [*self._brgb, opacity] * 4
1053
1054    @property
1055    def width(self):
1056        """The width of the rectangle.
1057
1058        :type: float
1059        """
1060        return self._width
1061
1062    @width.setter
1063    def width(self, value):
1064        self._width = value
1065        self._update_position()
1066
1067    @property
1068    def height(self):
1069        """The height of the rectangle.
1070
1071        :type: float
1072        """
1073        return self._height
1074
1075    @height.setter
1076    def height(self, value):
1077        self._height = value
1078        self._update_position()
1079
1080    @property
1081    def rotation(self):
1082        """Clockwise rotation of the rectangle, in degrees.
1083
1084        The Rectangle will be rotated about its (anchor_x, anchor_y)
1085        position.
1086
1087        :type: float
1088        """
1089        return self._rotation
1090
1091    @rotation.setter
1092    def rotation(self, value):
1093        self._rotation = value
1094        self._update_position()
1095
1096    @property
1097    def border_color(self):
1098        """The rectangle's border color.
1099
1100        This property sets the color of the border of a bordered rectangle.
1101
1102        The color is specified as an RGB tuple of integers '(red, green, blue)'.
1103        Each color component must be in the range 0 (dark) to 255 (saturated).
1104
1105        :type: (int, int, int)
1106        """
1107        return self._brgb
1108
1109    @border_color.setter
1110    def border_color(self, values):
1111        self._brgb = list(map(int, values))
1112        self._update_color()
1113
1114
1115class Triangle(_ShapeBase):
1116    def __init__(self, x, y, x2, y2, x3, y3, color=(255, 255, 255), batch=None, group=None):
1117        """Create a triangle.
1118
1119        The triangle's anchor point defaults to the first vertex point.
1120
1121        :Parameters:
1122            `x` : float
1123                The first X coordinate of the triangle.
1124            `y` : float
1125                The first Y coordinate of the triangle.
1126            `x2` : float
1127                The second X coordinate of the triangle.
1128            `y2` : float
1129                The second Y coordinate of the triangle.
1130            `x3` : float
1131                The third X coordinate of the triangle.
1132            `y3` : float
1133                The third Y coordinate of the triangle.
1134            `color` : (int, int, int)
1135                The RGB color of the triangle, specified as
1136                a tuple of three ints in the range of 0-255.
1137            `batch` : `~pyglet.graphics.Batch`
1138                Optional batch to add the triangle to.
1139            `group` : `~pyglet.graphics.Group`
1140                Optional parent group of the triangle.
1141        """
1142        self._x = x
1143        self._y = y
1144        self._x2 = x2
1145        self._y2 = y2
1146        self._x3 = x3
1147        self._y3 = y3
1148        self._rotation = 0
1149
1150        self._rgb = color
1151
1152        self._batch = batch or Batch()
1153        self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, group)
1154        self._vertex_list = self._batch.add(3, GL_TRIANGLES, self._group, 'v2f', 'c4B')
1155        self._update_position()
1156        self._update_color()
1157
1158    def _update_position(self):
1159        if not self._visible:
1160            self._vertex_list.vertices = (0, 0, 0, 0, 0, 0)
1161        else:
1162            anchor_x = self._anchor_x
1163            anchor_y = self._anchor_y
1164            x1 = self._x - anchor_x
1165            y1 = self._y - anchor_y
1166            x2 = self._x2 - anchor_x
1167            y2 = self._y2 - anchor_y
1168            x3 = self._x3 - anchor_x
1169            y3 = self._y3 - anchor_y
1170            self._vertex_list.vertices = (x1, y1, x2, y2, x3, y3)
1171
1172    def _update_color(self):
1173        self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * 3
1174
1175    @property
1176    def x2(self):
1177        """Second X coordinate of the shape.
1178
1179        :type: int or float
1180        """
1181        return self._x2
1182
1183    @x2.setter
1184    def x2(self, value):
1185        self._x2 = value
1186        self._update_position()
1187
1188    @property
1189    def y2(self):
1190        """Second Y coordinate of the shape.
1191
1192        :type: int or float
1193        """
1194        return self._y2
1195
1196    @y2.setter
1197    def y2(self, value):
1198        self._y2 = value
1199        self._update_position()
1200
1201    @property
1202    def x3(self):
1203        """Third X coordinate of the shape.
1204
1205        :type: int or float
1206        """
1207        return self._x3
1208
1209    @x3.setter
1210    def x3(self, value):
1211        self._x3 = value
1212        self._update_position()
1213
1214    @property
1215    def y3(self):
1216        """Third Y coordinate of the shape.
1217
1218        :type: int or float
1219        """
1220        return self._y3
1221
1222    @y3.setter
1223    def y3(self, value):
1224        self._y3 = value
1225        self._update_position()
1226
1227    @property
1228    def position(self):
1229        """The (x, y, x2, y2, x3, y3) coordinates of the triangle, as a tuple.
1230
1231        :Parameters:
1232            `x` : int or float
1233                X coordinate of the triangle.
1234            `y` : int or float
1235                Y coordinate of the triangle.
1236            `x2` : int or float
1237                X2 coordinate of the triangle.
1238            `y2` : int or float
1239                Y2 coordinate of the triangle.
1240            `x3` : int or float
1241                X3 coordinate of the triangle.
1242            `y3` : int or float
1243                Y3 coordinate of the triangle.
1244        """
1245        return self._x, self._y, self._x2, self._y2, self._x3, self._y3
1246
1247    @position.setter
1248    def position(self, values):
1249        self._x, self._y, self._x2, self._y2, self._x3, self._y3 = values
1250        self._update_position()
1251
1252
1253class Star(_ShapeBase):
1254    def __init__(self, x, y, outer_radius, inner_radius, num_spikes, rotation=0,
1255                 color=(255, 255, 255), batch=None, group=None) -> None:
1256        """Create a star.
1257
1258        The star's anchor point (x, y) defaults to the center of the star.
1259
1260        :Parameters:
1261            `x` : float
1262                The X coordinate of the star.
1263            `y` : float
1264                The Y coordinate of the star.
1265            `outer_radius` : float
1266                The desired outer radius of the star.
1267            `inner_radius` : float
1268                The desired inner radius of the star.
1269            `num_spikes` : float
1270                The desired number of spikes of the star.
1271            `rotation` : float
1272                The rotation of the star in degrees. A rotation of 0 degrees
1273                will result in one spike lining up with the X axis in
1274                positive direction.
1275            `color` : (int, int, int)
1276                The RGB color of the star, specified as
1277                a tuple of three ints in the range of 0-255.
1278            `batch` : `~pyglet.graphics.Batch`
1279                Optional batch to add the star to.
1280            `group` : `~pyglet.graphics.Group`
1281                Optional parent group of the star.
1282        """
1283        self._x = x
1284        self._y = y
1285        self._outer_radius = outer_radius
1286        self._inner_radius = inner_radius
1287        self._num_spikes = num_spikes
1288        self._rgb = color
1289        self._rotation = rotation
1290
1291        self._batch = batch or Batch()
1292        self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, group)
1293
1294        self._vertex_list = self._batch.add(self._num_spikes*6, GL_TRIANGLES,
1295                                            self._group, 'v2f', 'c4B')
1296        self._update_position()
1297        self._update_color()
1298
1299    def _update_position(self):
1300        if not self._visible:
1301            vertices = (0, 0) * self._num_spikes * 6
1302        else:
1303            x = self._x + self._anchor_x
1304            y = self._y + self._anchor_y
1305            r_i = self._inner_radius
1306            r_o = self._outer_radius
1307
1308            # get angle covered by each line (= half a spike)
1309            d_theta = math.pi / self._num_spikes
1310
1311            # phase shift rotation
1312            phi = self._rotation / 180 * math.pi
1313
1314            # calculate alternating points on outer and outer circles
1315            points = []
1316            for i in range(self._num_spikes):
1317                points.append((x + (r_o * math.cos(2*i * d_theta + phi)),
1318                               y + (r_o * math.sin(2*i * d_theta + phi))))
1319                points.append((x + (r_i * math.cos((2*i+1) * d_theta + phi)),
1320                               y + (r_i * math.sin((2*i+1) * d_theta + phi))))
1321
1322            # create a list of doubled-up points from the points
1323            vertices = []
1324            for i, point in enumerate(points):
1325                triangle = x, y, *points[i - 1], *point
1326                vertices.extend(triangle)
1327
1328        self._vertex_list.vertices[:] = vertices
1329
1330    def _update_color(self):
1331        self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * self._num_spikes * 6
1332
1333    @property
1334    def outer_radius(self):
1335        """The outer radius of the star."""
1336        return self._outer_radius
1337
1338    @outer_radius.setter
1339    def outer_radius(self, value):
1340        self._outer_radius = value
1341        self._update_position()
1342
1343    @property
1344    def inner_radius(self):
1345        """The inner radius of the star."""
1346        return self._inner_radius
1347
1348    @inner_radius.setter
1349    def inner_radius(self, value):
1350        self._inner_radius = value
1351        self._update_position()
1352
1353    @property
1354    def num_spikes(self):
1355        """Number of spikes of the star."""
1356        return self._num_spikes
1357
1358    @num_spikes.setter
1359    def num_spikes(self, value):
1360        self._num_spikes = value
1361        self._update_position()
1362
1363    @property
1364    def rotation(self):
1365        """Rotation of the star, in degrees.
1366        """
1367        return self._rotation
1368
1369    @rotation.setter
1370    def rotation(self, rotation):
1371        self._rotation = rotation
1372        self._update_position()
1373
1374
1375class Polygon(_ShapeBase):
1376    def __init__(self, *coordinates, color=(255, 255, 255), batch=None, group=None):
1377        """Create a convex polygon.
1378
1379        The polygon's anchor point defaults to the first vertex point.
1380
1381        :Parameters:
1382            `coordinates` : List[[int, int]]
1383                The coordinates for each point in the polygon.
1384            `color` : (int, int, int)
1385                The RGB color of the polygon, specified as
1386                a tuple of three ints in the range of 0-255.
1387            `batch` : `~pyglet.graphics.Batch`
1388                Optional batch to add the polygon to.
1389            `group` : `~pyglet.graphics.Group`
1390                Optional parent group of the polygon.
1391        """
1392
1393        # len(self._coordinates) = the number of vertices and sides in the shape.
1394        self._coordinates = list(coordinates)
1395
1396        self._rotation = 0
1397
1398        self._rgb = color
1399
1400        self._batch = batch or Batch()
1401        self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, group)
1402        self._vertex_list = self._batch.add((len(self._coordinates) - 2) * 3, GL_TRIANGLES, self._group, 'v2f', 'c4B')
1403        self._update_position()
1404        self._update_color()
1405
1406    def _update_position(self):
1407        if not self._visible:
1408            self._vertex_list.vertices = tuple([0] * ((len(self._coordinates) - 2) * 6))
1409        elif self._rotation:
1410            # Adjust all coordinates by the anchor.
1411            anchor_x = self._anchor_x
1412            anchor_y = self._anchor_y
1413            coords = [[x - anchor_x, y - anchor_y] for x, y in self._coordinates]
1414
1415            # Rotate the polygon around its first vertex.
1416            x, y = self._coordinates[0]
1417            r = -math.radians(self._rotation)
1418            cr = math.cos(r)
1419            sr = math.sin(r)
1420
1421            for i, c in enumerate(coords):
1422                c = [c[0] - x, c[1] - y]
1423                c = [c[0] * cr - c[1] * sr + x, c[0] * sr + c[1] * cr + y]
1424                coords[i] = c
1425
1426            # Triangulate the convex polygon.
1427            triangles = []
1428            for n in range(len(coords) - 2):
1429                triangles += [coords[0], coords[n + 1], coords[n + 2]]
1430
1431            # Flattening the list before setting vertices to it.
1432            self._vertex_list.vertices = tuple(value for coordinate in triangles for value in coordinate)
1433
1434        else:
1435            # Adjust all coordinates by the anchor.
1436            anchor_x = self._anchor_x
1437            anchor_y = self._anchor_y
1438            coords = [[x - anchor_x, y - anchor_y] for x, y in self._coordinates]
1439
1440            # Triangulate the convex polygon.
1441            triangles = []
1442            for n in range(len(coords) - 2):
1443                triangles += [coords[0], coords[n + 1], coords[n + 2]]
1444
1445            # Flattening the list before setting vertices to it.
1446            self._vertex_list.vertices = tuple(value for coordinate in triangles for value in coordinate)
1447
1448    def _update_color(self):
1449        self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * ((len(self._coordinates) - 2) * 3)
1450
1451    @property
1452    def x(self):
1453        """X coordinate of the shape.
1454
1455        :type: int or float
1456        """
1457        return self._coordinates[0][0]
1458
1459    @x.setter
1460    def x(self, value):
1461        self._coordinates[0][0] = value
1462        self._update_position()
1463
1464    @property
1465    def y(self):
1466        """Y coordinate of the shape.
1467
1468        :type: int or float
1469        """
1470        return self._coordinates[0][1]
1471
1472    @y.setter
1473    def y(self, value):
1474        self._coordinates[0][1] = value
1475        self._update_position()
1476
1477    @property
1478    def position(self):
1479        """The (x, y) coordinates of the shape, as a tuple.
1480
1481        :Parameters:
1482            `x` : int or float
1483                X coordinate of the shape.
1484            `y` : int or float
1485                Y coordinate of the shape.
1486        """
1487        return self._coordinates[0][0], self._coordinates[0][1]
1488
1489    @position.setter
1490    def position(self, values):
1491        self._coordinates[0][0], self._coordinates[0][1] = values
1492        self._update_position()
1493
1494    @property
1495    def rotation(self):
1496        """Clockwise rotation of the polygon, in degrees.
1497
1498        The Polygon will be rotated about its (anchor_x, anchor_y)
1499        position.
1500
1501        :type: float
1502        """
1503        return self._rotation
1504
1505    @rotation.setter
1506    def rotation(self, rotation):
1507        self._rotation = rotation
1508        self._update_position()
1509
1510
1511__all__ = ('Arc', 'Circle', 'Ellipse', 'Line', 'Rectangle', 'BorderedRectangle', 'Triangle', 'Star', 'Polygon', 'Sector')
1512