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