TetrArcade/arcade/draw_commands.py

1343 lines
48 KiB
Python

"""
This module contains commands for basic graphics drawing commands.
(Drawing primitives.)
Many of these commands are slow, because they load everything to the
graphics card each time a shape is drawn. For faster drawing, see the
Buffered Draw Commands.
"""
# pylint: disable=too-many-arguments, too-many-locals, too-few-public-methods
import PIL.Image
import PIL.ImageOps
import PIL.ImageDraw
import numpy as np
import pyglet.gl as gl
from typing import List
from arcade.window_commands import get_projection
from arcade.window_commands import get_window
from arcade.arcade_types import Color
from arcade.arcade_types import PointList
from arcade import shader
from arcade.earclip import earclip
from arcade.utils import *
line_vertex_shader = '''
#version 330
uniform mat4 Projection;
in vec2 in_vert;
in vec4 in_color;
out vec4 v_color;
void main() {
gl_Position = Projection * vec4(in_vert, 0.0, 1.0);
v_color = in_color;
}
'''
line_fragment_shader = '''
#version 330
in vec4 v_color;
out vec4 f_color;
void main() {
f_color = v_color;
}
'''
def get_four_byte_color(color: Color) -> Color:
"""
Given a RGB list, it will return RGBA.
Given a RGBA list, it will return the same RGBA.
:param Color color: Three or four byte tuple
:returns: return: Four byte RGBA tuple
"""
if len(color) == 4:
return color
elif len(color) == 3:
return color[0], color[1], color[2], 255
else:
raise ValueError("This isn't a 3 or 4 byte color")
def get_four_float_color(color: Color) -> (float, float, float, float):
"""
Given a 3 or 4 RGB/RGBA color where each color goes 0-255, this
returns a RGBA list where each item is a scaled float from 0 to 1.
:param Color color: Three or four byte tuple
:return: Four floats as a RGBA tuple
"""
if len(color) == 4:
return color[0] / 255, color[1] / 255, color[2] / 255, color[3] / 255
elif len(color) == 3:
return color[0] / 255, color[1] / 255, color[2] / 255, 1.0
else:
raise ValueError("This isn't a 3 or 4 byte color")
def make_transparent_color(color: Color, transparency: float):
"""
Given a RGB color, along with an alpha, returns a RGBA color tuple.
:param Color color: Three or four byte RGBA color
:param float transparency: Transparency
"""
return color[0], color[1], color[2], transparency
def rotate_point(x: float, y: float, cx: float, cy: float,
angle: float) -> (float, float):
"""
Rotate a point around a center.
:param x: x value of the point you want to rotate
:param y: y value of the point you want to rotate
:param cx: x value of the center point you want to rotate around
:param cy: y value of the center point you want to rotate around
:param angle: Angle, in degrees, to rotate
:return: Return rotated (x, y) pair
:rtype: (float, float)
"""
temp_x = x - cx
temp_y = y - cy
# now apply rotation
rotated_x = temp_x * math.cos(math.radians(angle)) - temp_y * math.sin(math.radians(angle))
rotated_y = temp_x * math.sin(math.radians(angle)) + temp_y * math.cos(math.radians(angle))
# translate back
rounding_precision = 2
x = round(rotated_x + cx, rounding_precision)
y = round(rotated_y + cy, rounding_precision)
return x, y
class Texture:
"""
Class that represents a texture.
Usually created by the ``load_texture`` or ``load_textures`` commands.
Attributes:
:name:
:image:
:scale:
:width: Width of the texture image in pixels
:height: Height of the texture image in pixels
"""
def __init__(self, name, image=None):
self.name = name
self.texture = None
self.image = image
self.scale = 1
if image:
self.width = image.width
self.height = image.height
else:
self.width = 0
self.height = 0
self._sprite = None
self._sprite = None
def draw(self, center_x: float, center_y: float, width: float,
height: float, angle: float = 0,
alpha: int = 255, transparent: bool = True,
repeat_count_x=1, repeat_count_y=1):
"""
Args:
center_x:
center_y:
width:
height:
angle:
alpha: Currently unused.
transparent: Currently unused.
repeat_count_x: Currently unused.
repeat_count_y: Currently unused.
Returns:
"""
from arcade.sprite import Sprite
from arcade.sprite_list import SpriteList
if self._sprite is None:
self._sprite = Sprite()
self._sprite._texture = self
self._sprite.textures = [self]
self._sprite_list = SpriteList()
self._sprite_list.append(self._sprite)
self._sprite.center_x = center_x
self._sprite.center_y = center_y
self._sprite.width = width
self._sprite.height = height
self._sprite.angle = angle
self._sprite.alpha = alpha
self._sprite_list.draw()
def load_textures(file_name: str,
image_location_list: PointList,
mirrored: bool = False,
flipped: bool = False,
scale: float = 1) -> List['Texture']:
"""
Load a set of textures off of a single image file.
Note, if the code is to load only part of the image, the given x, y
coordinates will start with the origin (0, 0) in the upper left of the
image. When drawing, Arcade uses (0, 0)
in the lower left corner when drawing. Be careful about this reversal.
For a longer explanation of why computers sometimes start in the upper
left, see:
http://programarcadegames.com/index.php?chapter=introduction_to_graphics&lang=en#section_5
:param str file_name: Name of the file.
:param PointList image_location_list: List of image locations. Each location should be
a list of four floats. ``[x, y, width, height]``.
:param bool mirrored: If set to true, the image is mirrored left to right.
:param bool flipped: If set to true, the image is flipped upside down.
:param float scale: Scale factor to apply on each new texture.
:Returns: List of textures loaded.
:Raises: ValueError
"""
# See if we already loaded this texture file, and we can just use a cached version.
cache_file_name = "{}".format(file_name)
if cache_file_name in load_texture.texture_cache:
texture = load_texture.texture_cache[cache_file_name]
source_image = texture.image
else:
source_image = PIL.Image.open(file_name)
result = Texture(cache_file_name, source_image)
load_texture.texture_cache[cache_file_name] = result
source_image_width, source_image_height = source_image.size
texture_info_list = []
for image_location in image_location_list:
x, y, width, height = image_location
if width <= 0:
raise ValueError("Texture has a width of {}, must be > 0."
.format(width))
if x > source_image_width:
raise ValueError("Can't load texture starting at an x of {} "
"when the image is only {} across."
.format(x, source_image_width))
if y > source_image_height:
raise ValueError("Can't load texture starting at an y of {} "
"when the image is only {} high."
.format(y, source_image_height))
if x + width > source_image_width:
raise ValueError("Can't load texture ending at an x of {} "
"when the image is only {} wide."
.format(x + width, source_image_width))
if y + height > source_image_height:
raise ValueError("Can't load texture ending at an y of {} "
"when the image is only {} high."
.format(y + height, source_image_height))
# See if we already loaded this texture, and we can just use a cached version.
cache_name = "{}{}{}{}{}{}{}{}".format(file_name, x, y, width, height, scale, flipped, mirrored)
if cache_name in load_texture.texture_cache:
result = load_texture.texture_cache[cache_name]
else:
image = source_image.crop((x, y, x + width, y + height))
# image = _trim_image(image)
if mirrored:
image = PIL.ImageOps.mirror(image)
if flipped:
image = PIL.ImageOps.flip(image)
result = Texture(cache_name, image)
load_texture.texture_cache[cache_name] = result
result.scale = scale
texture_info_list.append(result)
return texture_info_list
def load_texture(file_name: str, x: float = 0, y: float = 0,
width: float = 0, height: float = 0,
mirrored: bool = False,
flipped: bool = False,
scale: float = 1) -> Texture:
"""
Load image from disk and create a texture.
Note, if the code is to load only part of the image, the given x, y
coordinates will start with the origin (0, 0) in the upper left of the
image. When drawing, Arcade uses (0, 0)
in the lower left corner when drawing. Be careful about this reversal.
For a longer explanation of why computers sometimes start in the upper
left, see:
http://programarcadegames.com/index.php?chapter=introduction_to_graphics&lang=en#section_5
:param str file_name: Name of the file to that holds the texture.
:param float x: X position of the crop area of the texture.
:param float y: Y position of the crop area of the texture.
:param float width: Width of the crop area of the texture.
:param float height: Height of the crop area of the texture.
:param bool mirrored: True if we mirror the image across the y axis
:param bool flipped: True if we flip the image across the x axis
:param float scale: Scale factor to apply on the new texture.
:Returns: The new texture.
:raises: None
"""
# See if we already loaded this texture, and we can just use a cached version.
cache_name = "{}{}{}{}{}{}{}{}".format(file_name, x, y, width, height, scale, flipped, mirrored)
if cache_name in load_texture.texture_cache:
return load_texture.texture_cache[cache_name]
# See if we already loaded this texture file, and we can just use a cached version.
cache_file_name = "{}".format(file_name)
if cache_file_name in load_texture.texture_cache:
texture = load_texture.texture_cache[cache_file_name]
source_image = texture.image
else:
source_image = PIL.Image.open(file_name)
result = Texture(cache_file_name, source_image)
load_texture.texture_cache[cache_file_name] = result
source_image_width, source_image_height = source_image.size
if x != 0 or y != 0 or width != 0 or height != 0:
if x > source_image_width:
raise ValueError("Can't load texture starting at an x of {} "
"when the image is only {} across."
.format(x, source_image_width))
if y > source_image_height:
raise ValueError("Can't load texture starting at an y of {} "
"when the image is only {} high."
.format(y, source_image_height))
if x + width > source_image_width:
raise ValueError("Can't load texture ending at an x of {} "
"when the image is only {} wide."
.format(x + width, source_image_width))
if y + height > source_image_height:
raise ValueError("Can't load texture ending at an y of {} "
"when the image is only {} high."
.format(y + height, source_image_height))
image = source_image.crop((x, y, x + width, y + height))
else:
image = source_image
# image = _trim_image(image)
if mirrored:
image = PIL.ImageOps.mirror(image)
if flipped:
image = PIL.ImageOps.flip(image)
result = Texture(cache_name, image)
load_texture.texture_cache[cache_name] = result
result.scale = scale
return result
def make_circle_texture(diameter: int, color: Color) -> Texture:
"""
Return a Texture of a circle with given diameter and color
:param int diameter: Diameter of the circle and dimensions of the square Texture returned
:param Color color: Color of the circle
:Returns: A Texture object
:Raises: None
"""
bg_color = (0, 0, 0, 0) # fully transparent
img = PIL.Image.new("RGBA", (diameter, diameter), bg_color)
draw = PIL.ImageDraw.Draw(img)
draw.ellipse((0, 0, diameter - 1, diameter - 1), fill=color)
name = "{}:{}:{}".format("circle_texture", diameter, color) # name must be unique for caching
return Texture(name, img)
def make_soft_circle_texture(diameter: int, color: Color, center_alpha: int = 255, outer_alpha: int = 0) -> Texture:
"""
Return a Texture of a circle with given diameter, color, and alpha values at its center and edges
Args:
:diameter (int): Diameter of the circle and dimensions of the square Texture returned
:color (Color): Color of the circle
:center_alpha (int): alpha value of circle at its center
:outer_alpha (int): alpha value of circle at its edge
Returns:
A Texture object
Raises:
None
"""
# TODO: create a rectangle and circle (and triangle? and arbitrary poly where client passes
# in list of points?) particle?
bg_color = (0, 0, 0, 0) # fully transparent
img = PIL.Image.new("RGBA", (diameter, diameter), bg_color)
draw = PIL.ImageDraw.Draw(img)
max_radius = int(diameter // 2)
center = max_radius # for readability
for radius in range(max_radius, 0, -1):
alpha = int(lerp(center_alpha, outer_alpha, radius / max_radius))
clr = (color[0], color[1], color[2], alpha)
draw.ellipse((center - radius, center - radius, center + radius - 1, center + radius - 1), fill=clr)
name = "{}:{}:{}:{}:{}".format("soft_circle_texture", diameter, color, center_alpha,
outer_alpha) # name must be unique for caching
return Texture(name, img)
def make_soft_square_texture(size: int, color: Color, center_alpha: int = 255, outer_alpha: int = 0) -> Texture:
"""
Return a Texture of a circle with given diameter and color, fading out at the edges.
Args:
:diameter (int): Diameter of the circle and dimensions of the square Texture returned
:color (Color): Color of the circle
Returns:
The new texture.
Raises:
None
"""
bg_color = (0, 0, 0, 0) # fully transparent
img = PIL.Image.new("RGBA", (size, size), bg_color)
draw = PIL.ImageDraw.Draw(img)
half_size = int(size // 2)
for cur_size in range(0, half_size):
alpha = int(lerp(outer_alpha, center_alpha, cur_size / half_size))
clr = (color[0], color[1], color[2], alpha)
# draw.ellipse((center-radius, center-radius, center+radius, center+radius), fill=clr)
draw.rectangle((cur_size, cur_size, size - cur_size, size - cur_size), clr, None)
name = "{}:{}:{}:{}".format("gradientsquare", size, color, center_alpha,
outer_alpha) # name must be unique for caching
return Texture(name, img)
def _lerp_color(start_color: Color, end_color: Color, u: float) -> Color:
return (
int(lerp(start_color[0], end_color[0], u)),
int(lerp(start_color[1], end_color[1], u)),
int(lerp(start_color[2], end_color[2], u))
)
load_texture.texture_cache = dict()
# --- END TEXTURE FUNCTIONS # # #
def trim_image(image: PIL.Image) -> PIL.Image:
"""
Returns an image with extra whitespace cropped out.
"""
bbox = image.getbbox()
return image.crop(bbox)
# --- BEGIN ARC FUNCTIONS # # #
def draw_arc_filled(center_x: float, center_y: float,
width: float, height: float,
color: Color,
start_angle: float, end_angle: float,
tilt_angle: float = 0,
num_segments: int = 128):
"""
Draw a filled in arc. Useful for drawing pie-wedges, or Pac-Man.
:param float center_x: x position that is the center of the arc.
:param float center_y: y position that is the center of the arc.
:param float width: width of the arc.
:param float height: height of the arc.
:param Color color: color, specified in a list of 3 or 4 bytes in RGB or
RGBA format.
:param float start_angle: start angle of the arc in degrees.
:param float end_angle: end angle of the arc in degrees.
:param float tilt_angle: angle the arc is tilted.
:param float num_segments: Number of line segments used to draw arc.
"""
unrotated_point_list = [[0, 0]]
start_segment = int(start_angle / 360 * num_segments)
end_segment = int(end_angle / 360 * num_segments)
for segment in range(start_segment, end_segment + 1):
theta = 2.0 * 3.1415926 * segment / num_segments
x = width * math.cos(theta)
y = height * math.sin(theta)
unrotated_point_list.append((x, y))
if tilt_angle == 0:
uncentered_point_list = unrotated_point_list
else:
uncentered_point_list = []
for point in unrotated_point_list:
uncentered_point_list.append(rotate_point(point[0], point[1], 0, 0, tilt_angle))
point_list = []
for point in uncentered_point_list:
point_list.append((point[0] + center_x, point[1] + center_y))
_generic_draw_line_strip(point_list, color, gl.GL_TRIANGLE_FAN)
def draw_arc_outline(center_x: float, center_y: float, width: float,
height: float, color: Color,
start_angle: float, end_angle: float,
border_width: float = 1, tilt_angle: float = 0,
num_segments: int = 128):
"""
Draw the outside edge of an arc. Useful for drawing curved lines.
:param float center_x: x position that is the center of the arc.
:param float center_y: y position that is the center of the arc.
:param float width: width of the arc.
:param float height: height of the arc.
:param Color color: color, specified in a list of 3 or 4 bytes in RGB or
RGBA format.
:param float start_angle: start angle of the arc in degrees.
:param float end_angle: end angle of the arc in degrees.
:param float border_width: width of line in pixels.
:param float tilt_angle: angle the arc is tilted.
:param int num_segments: float of triangle segments that make up this
circle. Higher is better quality, but slower render time.
"""
unrotated_point_list = []
start_segment = int(start_angle / 360 * num_segments)
end_segment = int(end_angle / 360 * num_segments)
inside_width = width - border_width / 2
outside_width = width + border_width / 2
inside_height = height - border_width / 2
outside_height = height + border_width / 2
for segment in range(start_segment, end_segment + 1):
theta = 2.0 * math.pi * segment / num_segments
x1 = inside_width * math.cos(theta)
y1 = inside_height * math.sin(theta)
x2 = outside_width * math.cos(theta)
y2 = outside_height * math.sin(theta)
unrotated_point_list.append((x1, y1))
unrotated_point_list.append((x2, y2))
if tilt_angle == 0:
uncentered_point_list = unrotated_point_list
else:
uncentered_point_list = []
for point in unrotated_point_list:
uncentered_point_list.append(rotate_point(point[0], point[1], 0, 0, tilt_angle))
point_list = []
for point in uncentered_point_list:
point_list.append((point[0] + center_x, point[1] + center_y))
_generic_draw_line_strip(point_list, color, gl.GL_TRIANGLE_STRIP)
# --- END ARC FUNCTIONS # # #
# --- BEGIN PARABOLA FUNCTIONS # # #
def draw_parabola_filled(start_x: float, start_y: float, end_x: float,
height: float, color: Color,
tilt_angle: float = 0):
"""
Draws a filled in parabola.
:param float start_x: The starting x position of the parabola
:param float start_y: The starting y position of the parabola
:param float end_x: The ending x position of the parabola
:param float height: The height of the parabola
:param Color color: The color of the parabola
:param float tilt_angle: The angle of the tilt of the parabola
"""
center_x = (start_x + end_x) / 2
center_y = start_y + height
start_angle = 0
end_angle = 180
width = (start_x - end_x)
draw_arc_filled(center_x, center_y, width, height, color,
start_angle, end_angle, tilt_angle)
def draw_parabola_outline(start_x: float, start_y: float, end_x: float,
height: float, color: Color,
border_width: float = 1, tilt_angle: float = 0):
"""
Draws the outline of a parabola.
:param float start_x: The starting x position of the parabola
:param float start_y: The starting y position of the parabola
:param float end_x: The ending x position of the parabola
:param float height: The height of the parabola
:param Color color: The color of the parabola
:param float border_width: The width of the parabola
:param float tilt_angle: The angle of the tilt of the parabola
"""
center_x = (start_x + end_x) / 2
center_y = start_y + height
start_angle = 0
end_angle = 180
width = (start_x - end_x)
draw_arc_outline(center_x, center_y, width, height, color,
start_angle, end_angle, border_width, tilt_angle)
# --- END PARABOLA FUNCTIONS # # #
# --- BEGIN CIRCLE FUNCTIONS # # #
def draw_circle_filled(center_x: float, center_y: float, radius: float,
color: Color,
num_segments: int = 128):
"""
Draw a filled-in circle.
:param float center_x: x position that is the center of the circle.
:param float center_y: y position that is the center of the circle.
:param float radius: width of the circle.
:param Color color: color, specified in a list of 3 or 4 bytes in RGB or
RGBA format.
:param int num_segments: float of triangle segments that make up this
circle. Higher is better quality, but slower render time.
"""
width = radius * 2
height = radius * 2
draw_ellipse_filled(center_x, center_y, width, height, color, num_segments=num_segments)
def draw_circle_outline(center_x: float, center_y: float, radius: float,
color: Color, border_width: float = 1,
num_segments: int = 128):
"""
Draw the outline of a circle.
:param float center_x: x position that is the center of the circle.
:param float center_y: y position that is the center of the circle.
:param float radius: width of the circle.
:param Color color: color, specified in a list of 3 or 4 bytes in RGB or
RGBA format.
:param float border_width: Width of the circle outline in pixels.
:param int num_segments: Int of triangle segments that make up this
circle. Higher is better quality, but slower render time.
"""
width = radius * 2
height = radius * 2
draw_ellipse_outline(center_x, center_y, width, height,
color, border_width, num_segments=num_segments)
# --- END CIRCLE FUNCTIONS # # #
# --- BEGIN ELLIPSE FUNCTIONS # # #
def draw_ellipse_filled(center_x: float, center_y: float,
width: float, height: float, color: Color,
tilt_angle: float = 0, num_segments: int = 128):
"""
Draw a filled in ellipse.
:param float center_x: x position that is the center of the circle.
:param float center_y: y position that is the center of the circle.
:param float height: height of the ellipse.
:param float width: width of the ellipse.
:param Color color: color, specified in a list of 3 or 4 bytes in RGB or
RGBA format.
:param float tilt_angle: Angle in degrees to tilt the ellipse.
:param int num_segments: float of triangle segments that make up this
circle. Higher is better quality, but slower render time.
"""
unrotated_point_list = []
for segment in range(num_segments):
theta = 2.0 * 3.1415926 * segment / num_segments
x = (width / 2) * math.cos(theta)
y = (height / 2) * math.sin(theta)
unrotated_point_list.append((x, y))
if tilt_angle == 0:
uncentered_point_list = unrotated_point_list
else:
uncentered_point_list = []
for point in unrotated_point_list:
uncentered_point_list.append(rotate_point(point[0], point[1], 0, 0, tilt_angle))
point_list = []
for point in uncentered_point_list:
point_list.append((point[0] + center_x, point[1] + center_y))
_generic_draw_line_strip(point_list, color, gl.GL_TRIANGLE_FAN)
def draw_ellipse_outline(center_x: float, center_y: float, width: float,
height: float, color: Color,
border_width: float = 1, tilt_angle: float = 0,
num_segments=128):
"""
Draw the outline of an ellipse.
:param float center_x: x position that is the center of the circle.
:param float center_y: y position that is the center of the circle.
:param float height: height of the ellipse.
:param float width: width of the ellipse.
:param Color color: color, specified in a list of 3 or 4 bytes in RGB or
RGBA format.
:param float border_width: Width of the circle outline in pixels.
:param float tilt_angle: Angle in degrees to tilt the ellipse.
:param float num_segments: Number of line segments used to make the ellipse
"""
if border_width == 1:
unrotated_point_list = []
for segment in range(num_segments):
theta = 2.0 * 3.1415926 * segment / num_segments
x = (width / 2) * math.cos(theta)
y = (height / 2) * math.sin(theta)
unrotated_point_list.append((x, y))
if tilt_angle == 0:
uncentered_point_list = unrotated_point_list
else:
uncentered_point_list = []
for point in unrotated_point_list:
uncentered_point_list.append(rotate_point(point[0], point[1], 0, 0, tilt_angle))
point_list = []
for point in uncentered_point_list:
point_list.append((point[0] + center_x, point[1] + center_y))
_generic_draw_line_strip(point_list, color, gl.GL_LINE_LOOP)
else:
unrotated_point_list = []
start_segment = 0
end_segment = num_segments
inside_width = (width / 2) - border_width / 2
outside_width = (width / 2) + border_width / 2
inside_height = (height / 2) - border_width / 2
outside_height = (height / 2) + border_width / 2
for segment in range(start_segment, end_segment + 1):
theta = 2.0 * math.pi * segment / num_segments
x1 = inside_width * math.cos(theta)
y1 = inside_height * math.sin(theta)
x2 = outside_width * math.cos(theta)
y2 = outside_height * math.sin(theta)
unrotated_point_list.append((x1, y1))
unrotated_point_list.append((x2, y2))
if tilt_angle == 0:
uncentered_point_list = unrotated_point_list
else:
uncentered_point_list = []
for point in unrotated_point_list:
uncentered_point_list.append(rotate_point(point[0], point[1], 0, 0, tilt_angle))
point_list = []
for point in uncentered_point_list:
point_list.append((point[0] + center_x, point[1] + center_y))
_generic_draw_line_strip(point_list, color, gl.GL_TRIANGLE_STRIP)
# --- END ELLIPSE FUNCTIONS # # #
# --- BEGIN LINE FUNCTIONS # # #
def _generic_draw_line_strip(point_list: PointList,
color: Color,
mode: int = gl.GL_LINE_STRIP):
"""
Draw a line strip. A line strip is a set of continuously connected
line segments.
:param point_list: List of points making up the line. Each point is
in a list. So it is a list of lists.
:param Color color: color, specified in a list of 3 or 4 bytes in RGB or
RGBA format.
"""
program = shader.program(
vertex_shader=line_vertex_shader,
fragment_shader=line_fragment_shader,
)
buffer_type = np.dtype([('vertex', '2f4'), ('color', '4B')])
data = np.zeros(len(point_list), dtype=buffer_type)
data['vertex'] = point_list
color = get_four_byte_color(color)
data['color'] = color
vbo = shader.buffer(data.tobytes())
vbo_desc = shader.BufferDescription(
vbo,
'2f 4B',
('in_vert', 'in_color'),
normalized=['in_color']
)
vao_content = [vbo_desc]
vao = shader.vertex_array(program, vao_content)
with vao:
program['Projection'] = get_projection().flatten()
vao.render(mode=mode)
def draw_line_strip(point_list: PointList,
color: Color, line_width: float = 1):
"""
Draw a multi-point line.
:param PointList point_list: List of x, y points that make up this strip
:param Color color: Color of line strip
:param float line_width: Width of the line
"""
if line_width == 1:
_generic_draw_line_strip(point_list, color, gl.GL_LINE_STRIP)
else:
triangle_point_list = []
# This needs a lot of improvement
last_point = None
for point in point_list:
if last_point is not None:
points = _get_points_for_thick_line(last_point[0], last_point[1], point[0], point[1], line_width)
reordered_points = points[1], points[0], points[2], points[3]
triangle_point_list.extend(reordered_points)
last_point = point
_generic_draw_line_strip(triangle_point_list, color, gl.GL_TRIANGLE_STRIP)
def _get_points_for_thick_line(start_x: float, start_y:
float, end_x: float, end_y: float,
line_width: float):
vector_x = start_x - end_x
vector_y = start_y - end_y
perpendicular_x = vector_y
perpendicular_y = -vector_x
length = math.sqrt(vector_x * vector_x + vector_y * vector_y)
normal_x = perpendicular_x / length
normal_y = perpendicular_y / length
r1_x = start_x + normal_x * line_width / 2
r1_y = start_y + normal_y * line_width / 2
r2_x = start_x - normal_x * line_width / 2
r2_y = start_y - normal_y * line_width / 2
r3_x = end_x + normal_x * line_width / 2
r3_y = end_y + normal_y * line_width / 2
r4_x = end_x - normal_x * line_width / 2
r4_y = end_y - normal_y * line_width / 2
points = (r1_x, r1_y), (r2_x, r2_y), (r4_x, r4_y), (r3_x, r3_y)
return points
def draw_line(start_x: float, start_y: float, end_x: float, end_y: float,
color: Color, line_width: float = 1):
"""
Draw a line.
:param float start_x: x position of line starting point.
:param float start_y: y position of line starting point.
:param float end_x: x position of line ending point.
:param float end_y: y position of line ending point.
:param Color color: color, specified in a list of 3 or 4 bytes in RGB or
RGBA format.
:param float line_width: Width of the line in pixels.
"""
# points = (start_x, start_y), (end_x, end_y)
points = _get_points_for_thick_line(start_x, start_y, end_x, end_y, line_width)
triangle_point_list = points[1], points[0], points[2], points[3]
_generic_draw_line_strip(triangle_point_list, color, gl.GL_TRIANGLE_STRIP)
def draw_lines(point_list: PointList,
color: Color,
line_width: float = 1):
"""
Draw a set of lines.
Draw a line between each pair of points specified.
:param PointList point_list: List of points making up the lines. Each point is
in a list. So it is a list of lists.
:param Color color: color, specified in a list of 3 or 4 bytes in RGB or
RGBA format.
:param float line_width: Width of the line in pixels.
"""
triangle_point_list = []
last_point = None
for point in point_list:
if last_point is not None:
points = _get_points_for_thick_line(last_point[0], last_point[1], point[0], point[1], line_width)
reordered_points = points[1], points[0], points[2], points[0], points[2], points[3]
triangle_point_list.extend(reordered_points)
_generic_draw_line_strip(triangle_point_list, color, gl.GL_TRIANGLES)
last_point = None
else:
last_point = point
# --- BEGIN POINT FUNCTIONS # # #
def draw_point(x: float, y: float, color: Color, size: float):
"""
Draw a point.
:param float x: x position of point.
:param float y: y position of point.
:param Color color: color, specified in a list of 3 or 4 bytes in RGB or
RGBA format.
:param float size: Size of the point in pixels.
"""
draw_rectangle_filled(x, y, size / 2, size / 2, color)
def _get_points_for_points(point_list, size):
new_point_list = []
hs = size / 2
for point in point_list:
x = point[0]
y = point[1]
new_point_list.append((x - hs, y - hs))
new_point_list.append((x + hs, y - hs))
new_point_list.append((x + hs, y + hs))
new_point_list.append((x + hs, y + hs))
new_point_list.append((x - hs, y - hs))
new_point_list.append((x - hs, y + hs))
return new_point_list
def draw_points(point_list: PointList,
color: Color, size: float = 1):
"""
Draw a set of points.
:param PointList point_list: List of points Each point is
in a list. So it is a list of lists.
:param Color color: color, specified in a list of 3 or 4 bytes in RGB or
RGBA format.
:param float size: Size of the point in pixels.
"""
new_point_list = _get_points_for_points(point_list, size)
_generic_draw_line_strip(new_point_list, color, gl.GL_TRIANGLES)
# --- END POINT FUNCTIONS # # #
# --- BEGIN POLYGON FUNCTIONS # # #
def draw_polygon_filled(point_list: PointList,
color: Color):
"""
Draw a polygon that is filled in.
Args:
:point_list: List of points making up the lines. Each point is
in a list. So it is a list of lists.
:color: color, specified in a list of 3 or 4 bytes in RGB or
RGBA format.
Returns:
None
Raises:
None
"""
triangle_points = earclip(point_list)
flattened_list = [i for g in triangle_points for i in g]
_generic_draw_line_strip(flattened_list, color, gl.GL_TRIANGLES)
def draw_polygon_outline(point_list: PointList,
color: Color, line_width: float = 1):
"""
Draw a polygon outline. Also known as a "line loop."
:param PointList point_list: List of points making up the lines. Each point is
in a list. So it is a list of lists.
:param Color color: color, specified in a list of 3 or 4 bytes in RGB or
RGBA format.
:param int line_width: Width of the line in pixels.
"""
new_point_list = [point for point in point_list]
new_point_list.append(point_list[0])
triangle_point_list = []
# This needs a lot of improvement
last_point = None
for point in new_point_list:
if last_point is not None:
points = _get_points_for_thick_line(last_point[0], last_point[1], point[0], point[1], line_width)
reordered_points = points[1], points[0], points[2], points[3]
triangle_point_list.extend(reordered_points)
last_point = point
points = _get_points_for_thick_line(new_point_list[0][0], new_point_list[0][1], new_point_list[1][0],
new_point_list[1][1], line_width)
triangle_point_list.append(points[1])
_generic_draw_line_strip(triangle_point_list, color, gl.GL_TRIANGLE_STRIP)
def draw_triangle_filled(x1: float, y1: float,
x2: float, y2: float,
x3: float, y3: float, color: Color):
"""
Draw a filled in triangle.
:param float x1: x value of first coordinate.
:param float y1: y value of first coordinate.
:param float x2: x value of second coordinate.
:param float y2: y value of second coordinate.
:param float x3: x value of third coordinate.
:param float y3: y value of third coordinate.
:param Color color: Color of triangle.
"""
first_point = (x1, y1)
second_point = (x2, y2)
third_point = (x3, y3)
point_list = (first_point, second_point, third_point)
_generic_draw_line_strip(point_list, color, gl.GL_TRIANGLES)
def draw_triangle_outline(x1: float, y1: float,
x2: float, y2: float,
x3: float, y3: float,
color: Color,
border_width: float = 1):
"""
Draw a the outline of a triangle.
:param float x1: x value of first coordinate.
:param float y1: y value of first coordinate.
:param float x2: x value of second coordinate.
:param float y2: y value of second coordinate.
:param float x3: x value of third coordinate.
:param float y3: y value of third coordinate.
:param Color color: Color of triangle.
:param float border_width: Width of the border in pixels. Defaults to 1.
"""
first_point = [x1, y1]
second_point = [x2, y2]
third_point = [x3, y3]
point_list = (first_point, second_point, third_point)
draw_polygon_outline(point_list, color, border_width)
# --- END POLYGON FUNCTIONS # # #
# --- BEGIN RECTANGLE FUNCTIONS # # #
def draw_lrtb_rectangle_outline(left: float, right: float, top: float,
bottom: float, color: Color,
border_width: float = 1):
"""
Draw a rectangle by specifying left, right, top, and bottom edges.
:param float left: The x coordinate of the left edge of the rectangle.
:param float right: The x coordinate of the right edge of the rectangle.
:param float top: The y coordinate of the top of the rectangle.
:param float bottom: The y coordinate of the rectangle bottom.
:param Color color: The color of the rectangle.
:param float border_width: The width of the border in pixels. Defaults to one.
:Raises AttributeError: Raised if left > right or top < bottom.
"""
if left > right:
raise AttributeError("Left coordinate must be less than or equal to "
"the right coordinate")
if bottom > top:
raise AttributeError("Bottom coordinate must be less than or equal to "
"the top coordinate")
center_x = (left + right) / 2
center_y = (top + bottom) / 2
width = right - left
height = top - bottom
draw_rectangle_outline(center_x, center_y, width, height, color,
border_width)
def draw_xywh_rectangle_outline(bottom_left_x: float, bottom_left_y: float,
width: float, height: float,
color: Color,
border_width: float = 1):
"""
Draw a rectangle extending from bottom left to top right
:param float bottom_left_x: The x coordinate of the left edge of the rectangle.
:param float bottom_left_y: The y coordinate of the bottom of the rectangle.
:param float width: The width of the rectangle.
:param float height: The height of the rectangle.
:param Color color: The color of the rectangle.
:param float border_width: The width of the border in pixels. Defaults to one.
"""
center_x = bottom_left_x + (width / 2)
center_y = bottom_left_y + (height / 2)
draw_rectangle_outline(center_x, center_y, width, height, color,
border_width)
def draw_rectangle_outline(center_x: float, center_y: float, width: float,
height: float, color: Color,
border_width: float = 1, tilt_angle: float = 0):
"""
Draw a rectangle outline.
:param float center_x: x coordinate of top left rectangle point.
:param float center_y: y coordinate of top left rectangle point.
:param float width: width of the rectangle.
:param float height: height of the rectangle.
:param Color color: color, specified in a list of 3 or 4 bytes in RGB or
RGBA format.
:param float border_width: width of the lines, in pixels.
:param float tilt_angle: rotation of the rectangle. Defaults to zero.
"""
i_lb = center_x - width / 2 + border_width / 2, center_y - height / 2 + border_width / 2
i_rb = center_x + width / 2 - border_width / 2, center_y - height / 2 + border_width / 2
i_rt = center_x + width / 2 - border_width / 2, center_y + height / 2 - border_width / 2
i_lt = center_x - width / 2 + border_width / 2, center_y + height / 2 - border_width / 2
o_lb = center_x - width / 2 - border_width / 2, center_y - height / 2 - border_width / 2
o_rb = center_x + width / 2 + border_width / 2, center_y - height / 2 - border_width / 2
o_rt = center_x + width / 2 + border_width / 2, center_y + height / 2 + border_width / 2
o_lt = center_x - width / 2 - border_width / 2, center_y + height / 2 + border_width / 2
point_list = o_lt, i_lt, o_rt, i_rt, o_rb, i_rb, o_lb, i_lb, o_lt, i_lt
if tilt_angle != 0:
point_list_2 = []
for point in point_list:
new_point = rotate_point(point[0], point[1], center_x, center_y, tilt_angle)
point_list_2.append(new_point)
point_list = point_list_2
_generic_draw_line_strip(point_list, color, gl.GL_TRIANGLE_STRIP)
def draw_lrtb_rectangle_filled(left: float, right: float, top: float,
bottom: float, color: Color):
"""
Draw a rectangle by specifying left, right, top, and bottom edges.
:param float left: The x coordinate of the left edge of the rectangle.
:param float right: The x coordinate of the right edge of the rectangle.
:param float top: The y coordinate of the top of the rectangle.
:param float bottom: The y coordinate of the rectangle bottom.
:param Color color: The color of the rectangle.
:Raises AttributeError: Raised if left > right or top < bottom.
"""
if left > right:
raise AttributeError("Left coordinate {} must be less than or equal "
"to the right coordinate {}".format(left, right))
if bottom > top:
raise AttributeError("Bottom coordinate {} must be less than or equal "
"to the top coordinate {}".format(bottom, top))
center_x = (left + right) / 2
center_y = (top + bottom) / 2
width = right - left + 1
height = top - bottom + 1
draw_rectangle_filled(center_x, center_y, width, height, color)
def draw_xywh_rectangle_filled(bottom_left_x: float, bottom_left_y: float,
width: float, height: float,
color: Color):
"""
Draw a filled rectangle extending from bottom left to top right
:param float bottom_left_x: The x coordinate of the left edge of the rectangle.
:param float bottom_left_y: The y coordinate of the bottom of the rectangle.
:param float width: The width of the rectangle.
:param float height: The height of the rectangle.
:param Color color: The color of the rectangle.
"""
center_x = bottom_left_x + (width / 2)
center_y = bottom_left_y + (height / 2)
draw_rectangle_filled(center_x, center_y, width, height, color)
def draw_rectangle_filled(center_x: float, center_y: float, width: float,
height: float, color: Color,
tilt_angle: float = 0):
"""
Draw a filled-in rectangle.
:param float center_x: x coordinate of rectangle center.
:param float center_y: y coordinate of rectangle center.
:param float width: width of the rectangle.
:param float height: height of the rectangle.
:param Color color: color, specified in a list of 3 or 4 bytes in RGB or
RGBA format.
:param float tilt_angle: rotation of the rectangle. Defaults to zero.
"""
p1 = -width // 2 + center_x, -height // 2 + center_y
p2 = width // 2 + center_x, -height // 2 + center_y
p3 = width // 2 + center_x, height // 2 + center_y
p4 = -width // 2 + center_x, height // 2 + center_y
if tilt_angle != 0:
p1 = rotate_point(p1[0], p1[1], center_x, center_y, tilt_angle)
p2 = rotate_point(p2[0], p2[1], center_x, center_y, tilt_angle)
p3 = rotate_point(p3[0], p3[1], center_x, center_y, tilt_angle)
p4 = rotate_point(p4[0], p4[1], center_x, center_y, tilt_angle)
_generic_draw_line_strip((p1, p2, p4, p3), color, gl.GL_TRIANGLE_STRIP)
def draw_texture_rectangle(center_x: float, center_y: float, width: float,
height: float, texture: Texture, angle: float = 0,
alpha: int = 255,
repeat_count_x: int = 1, repeat_count_y: int = 1):
"""
Draw a textured rectangle on-screen.
:param float center_x: x coordinate of rectangle center.
:param float center_y: y coordinate of rectangle center.
:param float width: width of the rectangle.
:param float height: height of the rectangle.
:param int texture: identifier of texture returned from load_texture() call
:param float angle: rotation of the rectangle. Defaults to zero.
:param float alpha: Transparency of image. 0 is fully transparent, 255 (default) is visible
:param int repeat_count_x: Unused for now
:param int repeat_count_y: Unused for now
"""
texture.draw(center_x, center_y, width,
height, angle, alpha,
repeat_count_x, repeat_count_y)
def draw_xywh_rectangle_textured(bottom_left_x: float, bottom_left_y: float,
width: float, height: float,
texture: Texture, angle: float = 0,
alpha: int = 255,
repeat_count_x: int = 1, repeat_count_y: int = 1):
"""
Draw a texture extending from bottom left to top right.
:param float bottom_left_x: The x coordinate of the left edge of the rectangle.
:param float bottom_left_y: The y coordinate of the bottom of the rectangle.
:param float width: The width of the rectangle.
:param float height: The height of the rectangle.
:param int texture: identifier of texture returned from load_texture() call
:param float angle: rotation of the rectangle. Defaults to zero.
:param int alpha: Transparency of image. 0 is fully transparent, 255 (default) is visible
:param int repeat_count_x: Unused for now
:param int repeat_count_y: Unused for now
"""
center_x = bottom_left_x + (width / 2)
center_y = bottom_left_y + (height / 2)
draw_texture_rectangle(center_x, center_y,
width, height,
texture,
angle=angle, alpha=alpha)
def get_pixel(x: int, y: int):
"""
Given an x, y, will return RGB color value of that point.
:param int x: x location
:param int y: y location
:returns: Color
"""
a = (gl.GLubyte * 3)(0)
gl.glReadPixels(x, y, 1, 1, gl.GL_RGB, gl.GL_UNSIGNED_BYTE, a)
red = a[0]
green = a[1]
blue = a[2]
return red, green, blue
def get_image(x: int = 0, y: int = 0, width: int = None, height: int = None):
"""
Get an image from the screen.
:param int x: Start (left) x location
:param int y: Start (top) y location
:param int width: Width of image. Leave blank for grabbing the 'rest' of the image
:param int height: Height of image. Leave blank for grabbing the 'rest' of the image
You can save the image like:
.. highlight:: python
.. code-block:: python
image = get_image()
image.save('screenshot.png', 'PNG')
"""
# Get the dimensions
window = get_window()
if width is None:
width = window.width - x
if height is None:
height = window.height - y
# Create an image buffer
image_buffer = (gl.GLubyte * (4 * width * height))(0)
gl.glReadPixels(x, y, width, height, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE, image_buffer)
image = PIL.Image.frombytes("RGBA", (width, height), image_buffer)
image = PIL.ImageOps.flip(image)
# image.save('glutout.png', 'PNG')
return image