Difficult-Rocket/libs/pyglet/shapes.py
shenjack f9eeafe322
好活!
readme update

看起来更像 Dear ImGui 一些(looks more like Dear ImGui

and some intersting feature to the button

remove debug

确认一下action

404 修改

writing theme

looks good

better?

a ?

alpha=255 not 256

looks better

try new pyglet first

看起来好一些

sync pyglet

水一手

这波必须得水一手了,要不然commit太少了(确信

丢点正经东西上去

顺手继承一下Options

补充docs

坏了,忘记水commit了(

至少我能早睡了(

这里还能水一点来着(

试试再说

reee

保证能跑(

同步lib not dr 的修改

忘记带上 None了

还是加上一个额外的判断参数吧

刷点commit也不错

先更新一下依赖版本

水commit啦

理论可行,实践开始!

构建参数喜加一

reeeee

更新一下 pyproject 的依赖

fix typing

looks better

水一个(

测试?

sync pyglet to master

A | Try use rust-cache

looks good?

what?

C | sync pyglet

A | 添加了一个 Button Draw Theme

A | Magic Number (确信)

A | 尽量不继承Options

sync pyglet

A | Add theme

A | Add more Theme information

Enhance | Theme

sync pyglet

Add | add unifont

Enhance | use os.walk in font loading

Enhance | Use i18n in font loading

Enhance | doc

sync pyglet

Add | add 3.12 build option to build_rs

Fix | Button position have a z

sync pyglet

A | Logger.py 启动!

sync pyglet

Changed | Bump pyo3 to 0.20.0

add logger.py update

logger!

Add | more logger!

Add | lib-not-dr

some lib-not-dr
2023-11-20 20:12:56 +08:00

1780 lines
58 KiB
Python

"""2D shapes.
This module provides classes for a variety of simplistic 2D shapes,
such as Rectangles, Circles, and Lines. These shapes are made
internally from OpenGL primitives, and provide excellent performance
when drawn as part of a :py:class:`~pyglet.graphics.Batch`.
Convenience methods are provided for positioning, changing color
and opacity, and rotation (where applicable). To create more
complex shapes than what is provided here, the lower level
graphics API is more appropriate.
You can also use the ``in`` operator to check whether a point is
inside a shape.
See the :ref:`guide_graphics` for more details.
A simple example of drawing shapes::
import pyglet
from pyglet import shapes
window = pyglet.window.Window(960, 540)
batch = pyglet.graphics.Batch()
circle = shapes.Circle(700, 150, 100, color=(50, 225, 30), batch=batch)
square = shapes.Rectangle(200, 200, 200, 200, color=(55, 55, 255), batch=batch)
rectangle = shapes.Rectangle(250, 300, 400, 200, color=(255, 22, 20), batch=batch)
rectangle.opacity = 128
rectangle.rotation = 33
line = shapes.Line(100, 100, 100, 200, width=19, batch=batch)
line2 = shapes.Line(150, 150, 444, 111, width=4, color=(200, 20, 20), batch=batch)
star = shapes.Star(800, 400, 60, 40, num_spikes=20, color=(255, 255, 0), batch=batch)
@window.event
def on_draw():
window.clear()
batch.draw()
pyglet.app.run()
.. note:: Some Shapes, such as Lines and Triangles, have multiple coordinates.
If you update the x, y coordinate, this will also affect the secondary
coordinates. This allows you to move the shape without affecting it's
overall dimensions.
.. versionadded:: 1.5.4
"""
import math
from abc import ABC, abstractmethod
import pyglet
from pyglet.gl import GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA
from pyglet.gl import GL_TRIANGLES, GL_LINES, GL_BLEND
from pyglet.gl import glBlendFunc, glEnable, glDisable
from pyglet.graphics import Batch, Group
from pyglet.math import Vec2
vertex_source = """#version 150 core
in vec2 position;
in vec2 translation;
in vec4 colors;
in float rotation;
out vec4 vertex_colors;
uniform WindowBlock
{
mat4 projection;
mat4 view;
} window;
mat4 m_rotation = mat4(1.0);
mat4 m_translate = mat4(1.0);
void main()
{
m_translate[3][0] = translation.x;
m_translate[3][1] = translation.y;
m_rotation[0][0] = cos(-radians(rotation));
m_rotation[0][1] = sin(-radians(rotation));
m_rotation[1][0] = -sin(-radians(rotation));
m_rotation[1][1] = cos(-radians(rotation));
gl_Position = window.projection * window.view * m_translate * m_rotation * vec4(position, 0.0, 1.0);
vertex_colors = colors;
}
"""
fragment_source = """#version 150 core
in vec4 vertex_colors;
out vec4 final_color;
void main()
{
final_color = vertex_colors;
}
"""
def get_default_shader():
return pyglet.gl.current_context.create_program((vertex_source, 'vertex'),
(fragment_source, 'fragment'))
def _rotate_point(center, point, angle):
prev_angle = math.atan2(point[1] - center[1], point[0] - center[0])
now_angle = prev_angle + angle
r = math.dist(point, center)
return center[0] + r * math.cos(now_angle), center[1] + r * math.sin(now_angle)
def _sat(vertices, point):
# Separating Axis Theorem
# return True if point is in the shape
poly = vertices + [vertices[0]]
for i in range(len(poly) - 1):
a, b = poly[i], poly[i + 1]
base = Vec2(a[1] - b[1], b[0] - a[0])
projections = []
for x, y in poly:
vec = Vec2(x, y)
projections.append(base.dot(vec) / abs(base))
point_proj = base.dot(Vec2(*point)) / abs(base)
if point_proj < min(projections) or point_proj > max(projections):
return False
return True
class _ShapeGroup(Group):
"""Shared Shape rendering Group.
The group is automatically coalesced with other shape groups
sharing the same parent group and blend parameters.
"""
def __init__(self, blend_src, blend_dest, program, parent=None):
"""Create a Shape group.
The group is created internally. Usually you do not
need to explicitly create it.
:Parameters:
`blend_src` : int
OpenGL blend source mode; for example,
``GL_SRC_ALPHA``.
`blend_dest` : int
OpenGL blend destination mode; for example,
``GL_ONE_MINUS_SRC_ALPHA``.
`program` : `~pyglet.graphics.shader.ShaderProgram`
The ShaderProgram to use.
`parent` : `~pyglet.graphics.Group`
Optional parent group.
"""
super().__init__(parent=parent)
self.program = program
self.blend_src = blend_src
self.blend_dest = blend_dest
def set_state(self):
self.program.bind()
glEnable(GL_BLEND)
glBlendFunc(self.blend_src, self.blend_dest)
def unset_state(self):
glDisable(GL_BLEND)
self.program.unbind()
def __eq__(self, other):
return (other.__class__ is self.__class__ and
self.program == other.program and
self.parent == other.parent and
self.blend_src == other.blend_src and
self.blend_dest == other.blend_dest)
def __hash__(self):
return hash((self.program, self.parent, self.blend_src, self.blend_dest))
class ShapeBase(ABC):
"""Base class for all shape objects.
A number of default shapes are provided in this module. Curves are
approximated using multiple vertices.
If you need shapes or functionality not provided in this module,
you can write your own custom subclass of `ShapeBase` by using
the provided shapes as reference.
"""
_rgba = (255, 255, 255, 255)
_rotation = 0
_visible = True
_x = 0
_y = 0
_anchor_x = 0
_anchor_y = 0
_batch = None
_group = None
_num_verts = 0
_vertex_list = None
_draw_mode = GL_TRIANGLES
group_class = _ShapeGroup
def __del__(self):
if self._vertex_list is not None:
self._vertex_list.delete()
def __contains__(self, point):
"""Test whether a point is inside a shape."""
raise NotImplementedError(f"The `in` operator is not supported for {self.__class__.__name__}")
def _update_color(self):
"""Send the new colors for each vertex to the GPU.
This method must set the contents of `self._vertex_list.colors`
using a list or tuple that contains the RGBA color components
for each vertex in the shape. This is usually done by repeating
`self._rgba` for each vertex.
"""
self._vertex_list.colors[:] = self._rgba * self._num_verts
def _update_translation(self):
self._vertex_list.translation[:] = (self._x, self._y) * self._num_verts
def _create_vertex_list(self):
"""Build internal vertex list.
This method must create a vertex list and assign it to
`self._vertex_list`. It is advisable to use it
during `__init__` and to then update the vertices accordingly
with `self._update_vertices`.
While it is not mandatory to implement it, some properties (
namely `batch` and `group`) rely on this method to properly
recreate the vertex list.
"""
raise NotImplementedError('_create_vertex_list must be defined in '
'order to use group or batch properties')
@abstractmethod
def _update_vertices(self):
"""
Generate up-to-date vertex positions & send them to the GPU.
This method must set the contents of `self._vertex_list.vertices`
using a list or tuple that contains the new vertex coordinates for
each vertex in the shape. See the `ShapeBase` subclasses in this
module for examples of how to do this.
"""
raise NotImplementedError("_update_vertices must be defined"
"for every ShapeBase subclass")
@property
def rotation(self) -> float:
"""Clockwise rotation of the shape in degrees.
It will be rotated about its (anchor_x, anchor_y) position,
which defaults to the first vertex point of the shape.
For most shapes, this is the lower left corner. The shapes
below default to the points their ``radius`` values are
measured from:
* :py:class:`.Circle`
* :py:class:`.Ellipse`
* :py:class:`.Arc`
* :py:class:`.Sector`
* :py:class:`.Star`
"""
return self._rotation
@rotation.setter
def rotation(self, rotation: float) -> None:
self._rotation = rotation
self._vertex_list.rotation[:] = (rotation,) * self._num_verts
def draw(self):
"""Draw the shape at its current position.
Using this method is not recommended. Instead, add the
shape to a `pyglet.graphics.Batch` for efficient rendering.
"""
self._group.set_state_recursive()
self._vertex_list.draw(self._draw_mode)
self._group.unset_state_recursive()
def delete(self):
"""Force immediate removal of the shape from video memory.
It is recommended to call this whenever you delete a shape,
as the Python garbage collector will not necessarily call the
finalizer as soon as the sprite falls out of scope.
"""
self._vertex_list.delete()
self._vertex_list = None
@property
def x(self):
"""X coordinate of the shape.
:type: int or float
"""
return self._x
@x.setter
def x(self, value):
self._x = value
self._update_translation()
@property
def y(self):
"""Y coordinate of the shape.
:type: int or float
"""
return self._y
@y.setter
def y(self, value):
self._y = value
self._update_translation()
@property
def position(self):
"""The (x, y) coordinates of the shape, as a tuple.
:Parameters:
`x` : int or float
X coordinate of the sprite.
`y` : int or float
Y coordinate of the sprite.
"""
return self._x, self._y
@position.setter
def position(self, values):
self._x, self._y = values
self._update_translation()
@property
def anchor_x(self):
"""The X coordinate of the anchor point
:type: int or float
"""
return self._anchor_x
@anchor_x.setter
def anchor_x(self, value):
self._anchor_x = value
self._update_vertices()
@property
def anchor_y(self):
"""The Y coordinate of the anchor point
:type: int or float
"""
return self._anchor_y
@anchor_y.setter
def anchor_y(self, value):
self._anchor_y = value
self._update_vertices()
@property
def anchor_position(self):
"""The (x, y) coordinates of the anchor point, as a tuple.
:Parameters:
`x` : int or float
X coordinate of the anchor point.
`y` : int or float
Y coordinate of the anchor point.
"""
return self._anchor_x, self._anchor_y
@anchor_position.setter
def anchor_position(self, values):
self._anchor_x, self._anchor_y = values
self._update_vertices()
@property
def color(self):
"""The shape color.
This property sets the color of the shape.
The color is specified as an RGB tuple of integers '(red, green, blue)'.
Each color component must be in the range 0 (dark) to 255 (saturated).
:type: (int, int, int)
"""
return self._rgba
@color.setter
def color(self, values):
r, g, b, *a = values
if a:
self._rgba = r, g, b, a[0]
else:
self._rgba = r, g, b, self._rgba[3]
self._update_color()
@property
def opacity(self):
"""Blend opacity.
This property sets the alpha component of the color of the shape.
With the default blend mode (see the constructor), this allows the
shape to be drawn with fractional opacity, blending with the
background.
An opacity of 255 (the default) has no effect. An opacity of 128
will make the shape appear translucent.
:type: int
"""
return self._rgba[3]
@opacity.setter
def opacity(self, value):
self._rgba = (*self._rgba[:3], value)
self._update_color()
@property
def visible(self):
"""True if the shape will be drawn.
:type: bool
"""
return self._visible
@visible.setter
def visible(self, value):
self._visible = value
self._update_vertices()
@property
def group(self):
"""User assigned :class:`Group` object."""
return self._group.parent
@group.setter
def group(self, group):
if self._group.parent == group:
return
self._group = self.group_class(self._group.blend_src,
self._group.blend_dest,
self._group.program,
group)
self._batch.migrate(self._vertex_list, self._draw_mode, self._group,
self._batch)
@property
def batch(self):
"""User assigned :class:`Batch` object."""
return self._batch
@batch.setter
def batch(self, batch):
if self._batch == batch:
return
if batch is not None and self._batch is not None:
self._batch.migrate(self._vertex_list, self._draw_mode, self._group, batch)
self._batch = batch
else:
self._vertex_list.delete()
self._batch = batch
self._create_vertex_list()
self._update_vertices()
class Arc(ShapeBase):
_draw_mode = GL_LINES
def __init__(self, x, y, radius, segments=None, angle=math.tau, start_angle=0,
closed=False, color=(255, 255, 255, 255), batch=None, group=None):
"""Create an Arc.
The Arc's anchor point (x, y) defaults to its center.
:Parameters:
`x` : float
X coordinate of the circle.
`y` : float
Y coordinate of the circle.
`radius` : float
The desired radius.
`segments` : int
You can optionally specify how many distinct line segments
the arc should be made from. If not specified it will be
automatically calculated using the formula:
`max(14, int(radius / 1.25))`.
`angle` : float
The angle of the arc, in radians. Defaults to tau (pi * 2),
which is a full circle.
`start_angle` : float
The start angle of the arc, in radians. Defaults to 0.
`closed` : bool
If True, the ends of the arc will be connected with a line.
defaults to False.
`color` : (int, int, int, int)
The RGB or RGBA color of the arc, specified as a
tuple of 3 or 4 ints in the range of 0-255. RGB colors
will be treated as having opacity of 255.
`batch` : `~pyglet.graphics.Batch`
Optional batch to add the circle to.
`group` : `~pyglet.graphics.Group`
Optional parent group of the circle.
"""
self._x = x
self._y = y
self._radius = radius
self._segments = segments or max(14, int(radius / 1.25))
self._num_verts = self._segments * 2 + (2 if closed else 0)
# handle both 3 and 4 byte colors
r, g, b, *a = color
self._rgba = r, g, b, a[0] if a else 255
self._angle = angle
self._start_angle = start_angle
self._closed = closed
self._rotation = 0
self._batch = batch or Batch()
program = get_default_shader()
self._group = self.group_class(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group)
self._create_vertex_list()
self._update_vertices()
def _create_vertex_list(self):
self._vertex_list = self._group.program.vertex_list(
self._num_verts, self._draw_mode, self._batch, self._group,
colors=('Bn', self._rgba * self._num_verts),
translation=('f', (self._x, self._y) * self._num_verts))
def _update_vertices(self):
if not self._visible:
vertices = (0, 0) * self._num_verts
else:
x = -self._anchor_x
y = -self._anchor_y
r = self._radius
tau_segs = self._angle / self._segments
start_angle = self._start_angle - math.radians(self._rotation)
# Calculate the outer points of the arc:
points = [(x + (r * math.cos((i * tau_segs) + start_angle)),
y + (r * math.sin((i * tau_segs) + start_angle))) for i in range(self._segments + 1)]
# Create a list of doubled-up points from the points:
vertices = []
for i in range(len(points) - 1):
line_points = *points[i], *points[i + 1]
vertices.extend(line_points)
if self._closed:
chord_points = *points[-1], *points[0]
vertices.extend(chord_points)
self._vertex_list.position[:] = vertices
@property
def angle(self):
"""The angle of the arc.
:type: float
"""
return self._angle
@angle.setter
def angle(self, value):
self._angle = value
self._update_vertices()
@property
def start_angle(self):
"""The start angle of the arc.
:type: float
"""
return self._start_angle
@start_angle.setter
def start_angle(self, angle):
self._start_angle = angle
self._update_vertices()
def draw(self):
"""Draw the shape at its current position.
Using this method is not recommended. Instead, add the
shape to a `pyglet.graphics.Batch` for efficient rendering.
"""
self._vertex_list.draw(self._draw_mode)
class BezierCurve(ShapeBase):
_draw_mode = GL_LINES
def __init__(self, *points, t=1.0, segments=100, color=(255, 255, 255, 255), batch=None, group=None):
"""Create a Bézier curve.
The curve's anchor point (x, y) defaults to its first control point.
:Parameters:
`points` : List[[int, int]]
Control points of the curve.
`t` : float
Draw `100*t` percent of the curve. 0.5 means the curve
is half drawn and 1.0 means draw the whole curve.
`segments` : int
You can optionally specify how many line segments the
curve should be made from.
`color` : (int, int, int, int)
The RGB or RGBA color of the curve, specified as a
tuple of 3 or 4 ints in the range of 0-255. RGB colors
will be treated as having an opacity of 255.
`batch` : `~pyglet.graphics.Batch`
Optional batch to add the curve to.
`group` : `~pyglet.graphics.Group`
Optional parent group of the curve.
"""
self._points = list(points)
self._x, self._y = self._points[0]
self._t = t
self._segments = segments
self._num_verts = self._segments * 2
r, g, b, *a = color
self._rgba = r, g, b, a[0] if a else 255
program = get_default_shader()
self._batch = batch or Batch()
self._group = self.group_class(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group)
self._create_vertex_list()
self._update_vertices()
def _make_curve(self, t):
n = len(self._points) - 1
p = [0, 0]
for i in range(n + 1):
m = math.comb(n, i) * (1 - t) ** (n - i) * t ** i
p[0] += m * self._points[i][0]
p[1] += m * self._points[i][1]
return p
def _create_vertex_list(self):
self._vertex_list = self._group.program.vertex_list(
self._num_verts, self._draw_mode, self._batch, self._group,
colors=('Bn', self._rgba * self._num_verts),
translation=('f', (self._x, self._y) * self._num_verts))
def _update_vertices(self):
if not self._visible:
vertices = (0, 0) * self._num_verts
else:
x = -self._anchor_x
y = -self._anchor_y
# Calculate the points of the curve:
points = [(x + self._make_curve(self._t * t / self._segments)[0],
y + self._make_curve(self._t * t / self._segments)[1]) for t in range(self._segments + 1)]
trans_x, trans_y = points[0]
trans_x += self._anchor_x
trans_y += self._anchor_y
coords = [[x - trans_x, y - trans_y] for x, y in points]
# Create a list of doubled-up points from the points:
vertices = []
for i in range(len(coords) - 1):
line_points = *coords[i], *coords[i + 1]
vertices.extend(line_points)
self._vertex_list.position[:] = vertices
@property
def points(self):
"""Control points of the curve.
:type: List[[int, int]]
"""
return self._points
@points.setter
def points(self, value):
self._points = value
self._update_vertices()
@property
def t(self):
"""Draw `100*t` percent of the curve.
:type: float
"""
return self._t
@t.setter
def t(self, value):
self._t = value
self._update_vertices()
class Circle(ShapeBase):
def __init__(self, x, y, radius, segments=None, color=(255, 255, 255, 255),
batch=None, group=None):
"""Create a circle.
The circle's anchor point (x, y) defaults to the center of the circle.
:Parameters:
`x` : float
X coordinate of the circle.
`y` : float
Y coordinate of the circle.
`radius` : float
The desired radius.
`segments` : int
You can optionally specify how many distinct triangles
the circle should be made from. If not specified it will
be automatically calculated using the formula:
`max(14, int(radius / 1.25))`.
`color` : (int, int, int, int)
The RGB or RGBA color of the circle, specified as a
tuple of 3 or 4 ints in the range of 0-255. RGB colors
will be treated as having an opacity of 255.
`batch` : `~pyglet.graphics.Batch`
Optional batch to add the circle to.
`group` : `~pyglet.graphics.Group`
Optional parent group of the circle.
"""
self._x = x
self._y = y
self._radius = radius
self._segments = segments or max(14, int(radius / 1.25))
self._num_verts = self._segments * 3
r, g, b, *a = color
self._rgba = r, g, b, a[0] if a else 255
program = get_default_shader()
self._batch = batch or Batch()
self._group = self.group_class(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group)
self._create_vertex_list()
self._update_vertices()
def __contains__(self, point):
assert len(point) == 2
return math.dist((self._x - self._anchor_x, self._y - self._anchor_y), point) < self._radius
def _create_vertex_list(self):
self._vertex_list = self._group.program.vertex_list(
self._segments*3, self._draw_mode, self._batch, self._group,
colors=('Bn', self._rgba * self._num_verts),
translation=('f', (self._x, self._y) * self._num_verts))
def _update_vertices(self):
if not self._visible:
vertices = (0, 0) * self._num_verts
else:
x = -self._anchor_x
y = -self._anchor_y
r = self._radius
tau_segs = math.pi * 2 / self._segments
# Calculate the outer points of the circle:
points = [(x + (r * math.cos(i * tau_segs)),
y + (r * math.sin(i * tau_segs))) for i in range(self._segments)]
# Create a list of triangles from the points:
vertices = []
for i, point in enumerate(points):
triangle = x, y, *points[i - 1], *point
vertices.extend(triangle)
self._vertex_list.position[:] = vertices
@property
def radius(self):
"""The radius of the circle.
:type: float
"""
return self._radius
@radius.setter
def radius(self, value):
self._radius = value
self._update_vertices()
class Ellipse(ShapeBase):
def __init__(self, x, y, a, b, segments=None, color=(255, 255, 255, 255),
batch=None, group=None):
"""Create an ellipse.
The ellipse's anchor point (x, y) defaults to the center of the ellipse.
:Parameters:
`x` : float
X coordinate of the ellipse.
`y` : float
Y coordinate of the ellipse.
`a` : float
Semi-major axes of the ellipse.
`b`: float
Semi-minor axes of the ellipse.
`color` : (int, int, int, int)
The RGB or RGBA color of the ellipse, specified as a
tuple of 3 or 4 ints in the range of 0-255. RGB colors
will be treated as having an opacity of 255.
`batch` : `~pyglet.graphics.Batch`
Optional batch to add the circle to.
`group` : `~pyglet.graphics.Group`
Optional parent group of the circle.
"""
self._x = x
self._y = y
self._a = a
self._b = b
# Break with conventions in other _Shape constructors
# because a & b are used as meaningful variable names.
color_r, color_g, color_b, *color_a = color
self._rgba = color_r, color_g, color_b, color_a[0] if color_a else 255
self._rotation = 0
self._segments = segments or int(max(a, b) / 1.25)
self._num_verts = self._segments * 3
program = get_default_shader()
self._batch = batch or Batch()
self._group = self.group_class(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group)
self._create_vertex_list()
self._update_vertices()
def __contains__(self, point):
assert len(point) == 2
point = _rotate_point((self._x, self._y), point, math.radians(self._rotation))
# Since directly testing whether a point is inside an ellipse is more
# complicated, it is more convenient to transform it into a circle.
point = (self._b / self._a * point[0], point[1])
shape_center = (self._b / self._a * (self._x - self._anchor_x), self._y - self._anchor_y)
return math.dist(shape_center, point) < self._b
def _create_vertex_list(self):
self._vertex_list = self._group.program.vertex_list(
self._segments*3, self._draw_mode, self._batch, self._group,
colors=('Bn', self._rgba * self._num_verts),
translation=('f', (self._x, self._y) * self._num_verts))
def _update_vertices(self):
if not self._visible:
vertices = (0, 0) * self._num_verts
else:
x = -self._anchor_x
y = -self._anchor_y
tau_segs = math.pi * 2 / self._segments
# Calculate the points of the ellipse by formula:
points = [(x + self._a * math.cos(i * tau_segs),
y + self._b * math.sin(i * tau_segs)) for i in range(self._segments)]
# Create a list of triangles from the points:
vertices = []
for i, point in enumerate(points):
triangle = x, y, *points[i - 1], *point
vertices.extend(triangle)
self._vertex_list.position[:] = vertices
@property
def a(self):
"""The semi-major axes of the ellipse.
:type: float
"""
return self._a
@a.setter
def a(self, value):
self._a = value
self._update_vertices()
@property
def b(self):
"""The semi-minor axes of the ellipse.
:type: float
"""
return self._b
@b.setter
def b(self, value):
self._b = value
self._update_vertices()
class Sector(ShapeBase):
def __init__(self, x, y, radius, segments=None, angle=math.tau, start_angle=0,
color=(255, 255, 255, 255), batch=None, group=None):
"""Create a Sector of a circle.
The sector's anchor point (x, y) defaults to the center of the circle.
:Parameters:
`x` : float
X coordinate of the sector.
`y` : float
Y coordinate of the sector.
`radius` : float
The desired radius.
`segments` : int
You can optionally specify how many distinct triangles
the sector should be made from. If not specified it will
be automatically calculated using the formula:
`max(14, int(radius / 1.25))`.
`angle` : float
The angle of the sector, in radians. Defaults to tau (pi * 2),
which is a full circle.
`start_angle` : float
The start angle of the sector, in radians. Defaults to 0.
`color` : (int, int, int, int)
The RGB or RGBA color of the circle, specified as a
tuple of 3 or 4 ints in the range of 0-255. RGB colors
will be treated as having an opacity of 255.
`batch` : `~pyglet.graphics.Batch`
Optional batch to add the sector to.
`group` : `~pyglet.graphics.Group`
Optional parent group of the sector.
"""
self._x = x
self._y = y
self._radius = radius
self._segments = segments or max(14, int(radius / 1.25))
self._num_verts = self._segments * 3
r, g, b, *a = color
self._rgba = r, g, b, a[0] if a else 255
self._angle = angle
self._start_angle = start_angle
self._rotation = 0
program = get_default_shader()
self._batch = batch or Batch()
self._group = self.group_class(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group)
self._create_vertex_list()
self._update_vertices()
def __contains__(self, point):
assert len(point) == 2
point = _rotate_point((self._x, self._y), point, math.radians(self._rotation))
angle = math.atan2(point[1] - self._y + self._anchor_y, point[0] - self._x + self._anchor_x)
if angle < 0: angle += 2 * math.pi
if self._start_angle < angle < self._start_angle + self._angle:
return math.dist((self._x - self._anchor_x, self._y - self._anchor_y), point) < self._radius
return False
def _create_vertex_list(self):
self._vertex_list = self._group.program.vertex_list(
self._num_verts, self._draw_mode, self._batch, self._group,
colors=('Bn', self._rgba * self._num_verts),
translation=('f', (self._x, self._y) * self._num_verts))
def _update_vertices(self):
if not self._visible:
vertices = (0, 0) * self._num_verts
else:
x = -self._anchor_x
y = -self._anchor_y
r = self._radius
tau_segs = self._angle / self._segments
start_angle = self._start_angle - math.radians(self._rotation)
# Calculate the outer points of the sector.
points = [(x + (r * math.cos((i * tau_segs) + start_angle)),
y + (r * math.sin((i * tau_segs) + start_angle))) for i in range(self._segments + 1)]
# Create a list of triangles from the points
vertices = []
for i, point in enumerate(points[1:], start=1):
triangle = x, y, *points[i - 1], *point
vertices.extend(triangle)
self._vertex_list.position[:] = vertices
@property
def angle(self):
"""The angle of the sector.
:type: float
"""
return self._angle
@angle.setter
def angle(self, value):
self._angle = value
self._update_vertices()
@property
def start_angle(self):
"""The start angle of the sector.
:type: float
"""
return self._start_angle
@start_angle.setter
def start_angle(self, angle):
self._start_angle = angle
self._update_vertices()
@property
def radius(self):
"""The radius of the sector.
:type: float
"""
return self._radius
@radius.setter
def radius(self, value):
self._radius = value
self._update_vertices()
class Line(ShapeBase):
def __init__(self, x, y, x2, y2, width=1, color=(255, 255, 255, 255),
batch=None, group=None):
"""Create a line.
The line's anchor point defaults to the center of the line's
width on the X axis, and the Y axis.
:Parameters:
`x` : float
The first X coordinate of the line.
`y` : float
The first Y coordinate of the line.
`x2` : float
The second X coordinate of the line.
`y2` : float
The second Y coordinate of the line.
`width` : float
The desired width of the line.
`color` : (int, int, int, int)
The RGB or RGBA color of the line, specified as a
tuple of 3 or 4 ints in the range of 0-255. RGB colors
will be treated as having an opacity of 255.
`batch` : `~pyglet.graphics.Batch`
Optional batch to add the line to.
`group` : `~pyglet.graphics.Group`
Optional parent group of the line.
"""
self._x = x
self._y = y
self._x2 = x2
self._y2 = y2
self._width = width
self._rotation = math.degrees(math.atan2(y2 - y, x2 - x))
self._num_verts = 6
r, g, b, *a = color
self._rgba = r, g, b, a[0] if a else 255
program = get_default_shader()
self._batch = batch or Batch()
self._group = self.group_class(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group)
self._create_vertex_list()
self._update_vertices()
def __contains__(self, point):
assert len(point) == 2
vec_ab = Vec2(self._x2 - self._x, self._y2 - self._y)
vec_ba = Vec2(self._x - self._x2, self._y - self._y2)
vec_ap = Vec2(point[0] - self._x - self._anchor_x, point[1] - self._y + self._anchor_y)
vec_bp = Vec2(point[0] - self._x2 - self._anchor_x, point[1] - self._y2 + self._anchor_y)
if vec_ab.dot(vec_ap) * vec_ba.dot(vec_bp) < 0:
return False
a, b = point[0] + self._anchor_x, point[1] - self._anchor_y
x1, y1, x2, y2 = self._x, self._y, self._x2, self._y2
# The following is the expansion of the determinant of a 3x3 matrix
# used to calculate the area of a triangle.
double_area = abs(a*y1+b*x2+x1*y2-x2*y1-a*y2-b*x1)
h = double_area / math.dist((self._x, self._y), (self._x2, self._y2))
return h < self._width / 2
def _create_vertex_list(self):
self._vertex_list = self._group.program.vertex_list(
6, self._draw_mode, self._batch, self._group,
colors=('Bn', self._rgba * self._num_verts),
translation=('f', (self._x, self._y) * self._num_verts))
def _update_vertices(self):
if not self._visible:
self._vertex_list.position[:] = (0, 0) * self._num_verts
else:
x1 = -self._anchor_x
y1 = self._anchor_y - self._width / 2
x2 = x1 + math.hypot(self._y2 - self._y, self._x2 - self._x)
y2 = y1 + self._width
r = math.atan2(self._y2 - self._y, self._x2 - self._x)
cr = math.cos(r)
sr = math.sin(r)
ax = x1 * cr - y1 * sr
ay = x1 * sr + y1 * cr
bx = x2 * cr - y1 * sr
by = x2 * sr + y1 * cr
cx = x2 * cr - y2 * sr
cy = x2 * sr + y2 * cr
dx = x1 * cr - y2 * sr
dy = x1 * sr + y2 * cr
self._vertex_list.position[:] = (ax, ay, bx, by, cx, cy, ax, ay, cx, cy, dx, dy)
@property
def width(self):
return self._width
@width.setter
def width(self, width):
self._width = width
self._update_vertices()
@property
def x2(self):
"""Second X coordinate of the shape.
:type: int or float
"""
return self._x2
@x2.setter
def x2(self, value):
self._x2 = value
self._update_vertices()
@property
def y2(self):
"""Second Y coordinate of the shape.
:type: int or float
"""
return self._y2
@y2.setter
def y2(self, value):
self._y2 = value
self._update_vertices()
class Rectangle(ShapeBase):
def __init__(self, x, y, width, height, color=(255, 255, 255, 255),
batch=None, group=None):
"""Create a rectangle or square.
The rectangle's anchor point defaults to the (x, y) coordinates,
which are at the bottom left.
:Parameters:
`x` : float
The X coordinate of the rectangle.
`y` : float
The Y coordinate of the rectangle.
`width` : float
The width of the rectangle.
`height` : float
The height of the rectangle.
`color` : (int, int, int, int)
The RGB or RGBA color of the circle, specified as a
tuple of 3 or 4 ints in the range of 0-255. RGB colors
will be treated as having an opacity of 255.
`batch` : `~pyglet.graphics.Batch`
Optional batch to add the rectangle to.
`group` : `~pyglet.graphics.Group`
Optional parent group of the rectangle.
"""
self._x = x
self._y = y
self._width = width
self._height = height
self._rotation = 0
self._num_verts = 6
r, g, b, *a = color
self._rgba = r, g, b, a[0] if a else 255
program = get_default_shader()
self._batch = batch or Batch()
self._group = self.group_class(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group)
self._create_vertex_list()
self._update_vertices()
def __contains__(self, point):
assert len(point) == 2
point = _rotate_point((self._x, self._y), point, math.radians(self._rotation))
x, y = self._x - self._anchor_x, self._y - self._anchor_y
return x < point[0] < x + self._width and y < point[1] < y + self._height
def _create_vertex_list(self):
self._vertex_list = self._group.program.vertex_list(
6, self._draw_mode, self._batch, self._group,
colors=('Bn', self._rgba * self._num_verts),
translation=('f', (self._x, self._y) * self._num_verts))
def _update_vertices(self):
if not self._visible:
self._vertex_list.position[:] = (0, 0) * self._num_verts
else:
x1 = -self._anchor_x
y1 = -self._anchor_y
x2 = x1 + self._width
y2 = y1 + self._height
self._vertex_list.position[:] = x1, y1, x2, y1, x2, y2, x1, y1, x2, y2, x1, y2
@property
def width(self):
"""The width of the rectangle.
:type: float
"""
return self._width
@width.setter
def width(self, value):
self._width = value
self._update_vertices()
@property
def height(self):
"""The height of the rectangle.
:type: float
"""
return self._height
@height.setter
def height(self, value):
self._height = value
self._update_vertices()
class BorderedRectangle(ShapeBase):
def __init__(self, x, y, width, height, border=1, color=(255, 255, 255),
border_color=(100, 100, 100), batch=None, group=None):
"""Create a rectangle or square.
The rectangle's anchor point defaults to the (x, y) coordinates,
which are at the bottom left.
:Parameters:
`x` : float
The X coordinate of the rectangle.
`y` : float
The Y coordinate of the rectangle.
`width` : float
The width of the rectangle.
`height` : float
The height of the rectangle.
`border` : float
The thickness of the border.
`color` : (int, int, int, int)
The RGB or RGBA fill color of the rectangle, specified
as a tuple of 3 or 4 ints in the range of 0-255. RGB
colors will be treated as having an opacity of 255.
`border_color` : (int, int, int, int)
The RGB or RGBA fill color of the border, specified
as a tuple of 3 or 4 ints in the range of 0-255. RGB
colors will be treated as having an opacity of 255.
The alpha values must match if you pass RGBA values to
both this argument and `border_color`. If they do not,
a `ValueError` will be raised informing you of the
ambiguity.
`batch` : `~pyglet.graphics.Batch`
Optional batch to add the rectangle to.
`group` : `~pyglet.graphics.Group`
Optional parent group of the rectangle.
"""
self._x = x
self._y = y
self._width = width
self._height = height
self._rotation = 0
self._border = border
self._num_verts = 8
fill_r, fill_g, fill_b, *fill_a = color
border_r, border_g, border_b, *border_a = border_color
# Start with a default alpha value of 255.
alpha = 255
# Raise Exception if we have conflicting alpha values
if fill_a and border_a and fill_a[0] != border_a[0]:
raise ValueError("When color and border_color are both RGBA values,"
"they must both have the same opacity")
# Choose a value to use if there is no conflict
elif fill_a:
alpha = fill_a[0]
elif border_a:
alpha = border_a[0]
# Although the shape is only allowed one opacity, the alpha is
# stored twice to keep other code concise and reduce cpu usage
# from stitching together sequences.
self._rgba = fill_r, fill_g, fill_b, alpha
self._border_rgba = border_r, border_g, border_b, alpha
program = get_default_shader()
self._batch = batch or Batch()
self._group = self.group_class(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group)
self._create_vertex_list()
self._update_vertices()
def __contains__(self, point):
assert len(point) == 2
point = _rotate_point((self._x, self._y), point, math.radians(self._rotation))
x, y = self._x - self._anchor_x, self._y - self._anchor_y
return x < point[0] < x + self._width and y < point[1] < y + self._height
def _create_vertex_list(self):
indices = [0, 1, 2, 0, 2, 3, 0, 4, 3, 4, 7, 3, 0, 1, 5, 0, 5, 4, 1, 2, 5, 5, 2, 6, 6, 2, 3, 6, 3, 7]
self._vertex_list = self._group.program.vertex_list_indexed(
8, self._draw_mode, indices, self._batch, self._group,
colors=('Bn', self._rgba * 4 + self._border_rgba * 4),
translation=('f', (self._x, self._y) * self._num_verts))
def _update_color(self):
self._vertex_list.colors[:] = self._rgba * 4 + self._border_rgba * 4
def _update_vertices(self):
if not self._visible:
self._vertex_list.position[:] = (0, 0) * self._num_verts
else:
bx1 = -self._anchor_x
by1 = -self._anchor_y
bx2 = bx1 + self._width
by2 = by1 + self._height
b = self._border
ix1 = bx1 + b
iy1 = by1 + b
ix2 = bx2 - b
iy2 = by2 - b
self._vertex_list.position[:] = (ix1, iy1, ix2, iy1, ix2, iy2, ix1, iy2,
bx1, by1, bx2, by1, bx2, by2, bx1, by2)
@property
def border(self):
"""The border width of the rectangle.
:return: float
"""
return self._border
@border.setter
def border(self, width):
self._border = width
self._update_vertices()
@property
def width(self):
"""The width of the rectangle.
:type: float
"""
return self._width
@width.setter
def width(self, value):
self._width = value
self._update_vertices()
@property
def height(self):
"""The height of the rectangle.
:type: float
"""
return self._height
@height.setter
def height(self, value):
self._height = value
self._update_vertices()
@property
def border_color(self):
"""The rectangle's border color.
This property sets the color of the border of a bordered rectangle.
The color is specified as an RGB tuple of integers '(red, green, blue)'
or an RGBA tuple of integers '(red, green, blue, alpha)`. Setting the
alpha on this property will change the alpha of the entire shape,
including both the fill and the border.
Each color component must be in the range 0 (dark) to 255 (saturated).
:type: (int, int, int, int)
"""
return self._border_rgba
@border_color.setter
def border_color(self, values):
r, g, b, *a = values
if a:
alpha = a[0]
else:
alpha = self._rgba[3]
self._border_rgba = r, g, b, alpha
self._rgba = *self._rgba[:3], alpha
self._update_color()
@property
def color(self):
"""The rectangle's fill color.
This property sets the color of the inside of a bordered rectangle.
The color is specified as an RGB tuple of integers '(red, green, blue)'
or an RGBA tuple of integers '(red, green, blue, alpha)`. Setting the
alpha on this property will change the alpha of the entire shape,
including both the fill and the border.
Each color component must be in the range 0 (dark) to 255 (saturated).
:type: (int, int, int, int)
"""
return self._rgba
@color.setter
def color(self, values):
r, g, b, *a = values
if a:
alpha = a[0]
else:
alpha = self._rgba[3]
self._rgba = r, g, b, alpha
self._border_rgba = *self._border_rgba[:3], alpha
self._update_color()
class Triangle(ShapeBase):
def __init__(self, x, y, x2, y2, x3, y3, color=(255, 255, 255, 255),
batch=None, group=None):
"""Create a triangle.
The triangle's anchor point defaults to the first vertex point.
:Parameters:
`x` : float
The first X coordinate of the triangle.
`y` : float
The first Y coordinate of the triangle.
`x2` : float
The second X coordinate of the triangle.
`y2` : float
The second Y coordinate of the triangle.
`x3` : float
The third X coordinate of the triangle.
`y3` : float
The third Y coordinate of the triangle.
`color` : (int, int, int, int)
The RGB or RGBA color of the triangle, specified as a
tuple of 3 or 4 ints in the range of 0-255. RGB colors
will be treated as having an opacity of 255.
`batch` : `~pyglet.graphics.Batch`
Optional batch to add the triangle to.
`group` : `~pyglet.graphics.Group`
Optional parent group of the triangle.
"""
self._x = x
self._y = y
self._x2 = x2
self._y2 = y2
self._x3 = x3
self._y3 = y3
self._rotation = 0
self._num_verts = 3
r, g, b, *a = color
self._rgba = r, g, b, a[0] if a else 255
program = get_default_shader()
self._batch = batch or Batch()
self._group = self.group_class(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group)
self._create_vertex_list()
self._update_vertices()
def __contains__(self, point):
assert len(point) == 2
return _sat([(self._x, self._y), (self._x2, self._y2), (self._x3, self._y3)], point)
def _create_vertex_list(self):
self._vertex_list = self._group.program.vertex_list(
3, self._draw_mode, self._batch, self._group,
colors=('Bn', self._rgba * self._num_verts),
translation=('f', (self._x, self._y) * self._num_verts))
def _update_vertices(self):
if not self._visible:
self._vertex_list.position[:] = (0, 0) * self._num_verts
else:
x1 = -self._anchor_x
y1 = -self._anchor_y
x2 = self._x2 + x1 - self._x
y2 = self._y2 + y1 - self._y
x3 = self._x3 + x1 - self._x
y3 = self._y3 + y1 - self._y
self._vertex_list.position[:] = (x1, y1, x2, y2, x3, y3)
@property
def x2(self):
"""Second X coordinate of the shape.
:type: int or float
"""
return self._x + self._x2
@x2.setter
def x2(self, value):
self._x2 = value
self._update_vertices()
@property
def y2(self):
"""Second Y coordinate of the shape.
:type: int or float
"""
return self._y + self._y2
@y2.setter
def y2(self, value):
self._y2 = value
self._update_vertices()
@property
def x3(self):
"""Third X coordinate of the shape.
:type: int or float
"""
return self._x + self._x3
@x3.setter
def x3(self, value):
self._x3 = value
self._update_vertices()
@property
def y3(self):
"""Third Y coordinate of the shape.
:type: int or float
"""
return self._y + self._y3
@y3.setter
def y3(self, value):
self._y3 = value
self._update_vertices()
class Star(ShapeBase):
def __init__(self, x, y, outer_radius, inner_radius, num_spikes, rotation=0,
color=(255, 255, 255, 255), batch=None, group=None) -> None:
"""Create a star.
The star's anchor point (x, y) defaults to the center of the star.
:Parameters:
`x` : float
The X coordinate of the star.
`y` : float
The Y coordinate of the star.
`outer_radius` : float
The desired outer radius of the star.
`inner_radius` : float
The desired inner radius of the star.
`num_spikes` : float
The desired number of spikes of the star.
`rotation` : float
The rotation of the star in degrees. A rotation of 0 degrees
will result in one spike lining up with the X axis in
positive direction.
`color` : (int, int, int)
The RGB or RGBA color of the star, specified as a
tuple of 3 or 4 ints in the range of 0-255. RGB colors
will be treated as having an opacity of 255.
`batch` : `~pyglet.graphics.Batch`
Optional batch to add the star to.
`group` : `~pyglet.graphics.Group`
Optional parent group of the star.
"""
self._x = x
self._y = y
self._outer_radius = outer_radius
self._inner_radius = inner_radius
self._num_spikes = num_spikes
self._num_verts = num_spikes * 6
self._rotation = rotation
r, g, b, *a = color
self._rgba = r, g, b, a[0] if a else 255
program = get_default_shader()
self._batch = batch or Batch()
self._group = self.group_class(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group)
self._create_vertex_list()
self._update_vertices()
def __contains__(self, point):
assert len(point) == 2
point = _rotate_point((self._x, self._y), point, math.radians(self._rotation))
center = (self._x - self._anchor_x, self._y - self._anchor_y)
radius = (self._outer_radius + self._inner_radius) / 2
return math.dist(center, point) < radius
def _create_vertex_list(self):
self._vertex_list = self._group.program.vertex_list(
self._num_verts, self._draw_mode, self._batch, self._group,
colors=('Bn', self._rgba * self._num_verts),
rotation=('f', (self._rotation,) * self._num_verts),
translation=('f', (self._x, self._y) * self._num_verts))
def _update_vertices(self):
if not self._visible:
vertices = (0, 0) * self._num_verts
else:
x = -self._anchor_x
y = -self._anchor_y
r_i = self._inner_radius
r_o = self._outer_radius
# get angle covered by each line (= half a spike)
d_theta = math.pi / self._num_spikes
# calculate alternating points on outer and outer circles
points = []
for i in range(self._num_spikes):
points.append((x + (r_o * math.cos(2*i * d_theta)),
y + (r_o * math.sin(2*i * d_theta))))
points.append((x + (r_i * math.cos((2*i+1) * d_theta)),
y + (r_i * math.sin((2*i+1) * d_theta))))
# create a list of doubled-up points from the points
vertices = []
for i, point in enumerate(points):
triangle = x, y, *points[i - 1], *point
vertices.extend(triangle)
self._vertex_list.position[:] = vertices
@property
def outer_radius(self):
"""The outer radius of the star."""
return self._outer_radius
@outer_radius.setter
def outer_radius(self, value):
self._outer_radius = value
self._update_vertices()
@property
def inner_radius(self):
"""The inner radius of the star."""
return self._inner_radius
@inner_radius.setter
def inner_radius(self, value):
self._inner_radius = value
self._update_vertices()
@property
def num_spikes(self):
"""Number of spikes of the star."""
return self._num_spikes
@num_spikes.setter
def num_spikes(self, value):
self._num_spikes = value
self._update_vertices()
class Polygon(ShapeBase):
def __init__(self, *coordinates, color=(255, 255, 255, 255), batch=None, group=None):
"""Create a convex polygon.
The polygon's anchor point defaults to the first vertex point.
:Parameters:
`coordinates` : List[[int, int]]
The coordinates for each point in the polygon.
`color` : (int, int, int, int)
The RGB or RGBA color of the polygon, specified as a
tuple of 3 or 4 ints in the range of 0-255. RGB colors
will be treated as having an opacity of 255.
`batch` : `~pyglet.graphics.Batch`
Optional batch to add the polygon to.
`group` : `~pyglet.graphics.Group`
Optional parent group of the polygon.
"""
# len(self._coordinates) = the number of vertices and sides in the shape.
self._rotation = 0
self._coordinates = list(coordinates)
self._x, self._y = self._coordinates[0]
self._num_verts = (len(self._coordinates) - 2) * 3
r, g, b, *a = color
self._rgba = r, g, b, a[0] if a else 255
program = get_default_shader()
self._batch = batch or Batch()
self._group = self.group_class(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group)
self._create_vertex_list()
self._update_vertices()
def __contains__(self, point):
assert len(point) == 2
point = _rotate_point(self._coordinates[0], point, math.radians(self._rotation))
return _sat(self._coordinates, point)
def _create_vertex_list(self):
self._vertex_list = self._group.program.vertex_list(
self._num_verts, self._draw_mode, self._batch, self._group,
colors=('Bn', self._rgba * self._num_verts),
translation=('f', (self._x, self._y) * self._num_verts))
def _update_vertices(self):
if not self._visible:
self._vertex_list.position[:] = (0, 0) * self._num_verts
else:
# Adjust all coordinates by the anchor.
trans_x, trans_y = self._coordinates[0]
trans_x += self._anchor_x
trans_y += self._anchor_y
coords = [[x - trans_x, y - trans_y] for x, y in self._coordinates]
# Triangulate the convex polygon.
triangles = []
for n in range(len(coords) - 2):
triangles += [coords[0], coords[n + 1], coords[n + 2]]
# Flattening the list before setting vertices to it.
self._vertex_list.position[:] = tuple(value for coordinate in triangles for value in coordinate)
__all__ = 'Arc', 'BezierCurve', 'Circle', 'Ellipse', 'Line', 'Rectangle', 'BorderedRectangle', 'Triangle', 'Star', 'Polygon', 'Sector'