"""
Code related to working with textures.
"""
import logging
import hashlib
from typing import Dict, Optional, Tuple, List, Type, Union, TYPE_CHECKING
from pathlib import Path
from arcade import hitbox
# from weakref import WeakValueDictionary
import PIL.Image
import PIL.ImageOps
import PIL.ImageDraw
from arcade import (
lerp,
RectList,
Color,
)
from arcade.texture_transforms import (
Transform,
FlipLeftToRightTransform,
FlipTopToBottomTransform,
Rotate90Transform,
Rotate180Transform,
Rotate270Transform,
TransposeTransform,
TransverseTransform,
get_shortest_transform,
)
from arcade.arcade_types import PointList
from arcade.color import TRANSPARENT_BLACK
from arcade.resources import resolve_resource_path
from arcade.cache.hit_box import HitBoxCache
from arcade.cache.image import ImageCache
from arcade.cache import build_cache_name
if TYPE_CHECKING:
from arcade.sprite import Sprite
from arcade.sprite_list import SpriteList
LOG = logging.getLogger(__name__)
[docs]class ImageData:
"""
A class holding the image for a texture
with other metadata such as the hash.
This information is used internally by the
texture atlas to identify unique textures.
If a hash is not provided, it will be calculated.
It's important that all hashes are of the same type.
By default, the hash is calculated using the sha256 algorithm.
The ability to provide a hash directly is mainly there
for ensuring we can load and save texture atlases to disk.
:param PIL.Image.Image image: The image for this texture
:param str hash: The hash of the image
"""
hash_func = "sha256"
def __init__(self, image: PIL.Image.Image, hash: Optional[str] = None):
self.image = image
self.hash = hash or self.calculate_hash(image)
[docs] @classmethod
def calculate_hash(cls, image: PIL.Image.Image) -> str:
"""
Calculates the hash of an image.
The algorithm used is defined by the ``hash_func`` class variable.
"""
hash = hashlib.new(cls.hash_func)
hash.update(image.tobytes())
return hash.hexdigest()
[docs]class Texture:
"""
An arcade.Texture is simply a wrapper for image data as a Pillow image
and the hit box data for this image used in collision detection.
Usually created by the :class:`load_texture` or :class:`load_textures` commands.
:param str name: Globally unique name for this texture.
This is used internally for caching and texture atlas.
:param PIL.Image.Image image: The image for this texture
:param str hit_box_algorithm: One of None, 'None', 'Simple' or 'Detailed'. \
Defaults to 'Simple'. Use 'Simple' for the :data:`PhysicsEngineSimple`, \
:data:`PhysicsEnginePlatformer` \
and 'Detailed' for the :data:`PymunkPhysicsEngine`.
.. figure:: ../images/hit_box_algorithm_none.png
:width: 40%
hit_box_algorithm = "None"
.. figure:: ../images/hit_box_algorithm_simple.png
:width: 55%
hit_box_algorithm = "Simple"
.. figure:: ../images/hit_box_algorithm_detailed.png
:width: 75%
hit_box_algorithm = "Detailed"
:param float hit_box_detail: Float, defaults to 4.5. Used with 'Detailed' to hit box
:param PointList hit_box_points: List of points for the hit box (Optional).
Completely overrides the hit box algorithm.
"""
# cache: WeakValueDictionary[str, "Texture"] = WeakValueDictionary()
cache: Dict[str, "Texture"] = dict()
image_cache = ImageCache()
hit_box_cache = HitBoxCache()
def __init__(
self,
name: str,
image: Union[PIL.Image.Image, ImageData],
hit_box_algorithm: Optional[str] = "default",
hit_box_detail: float = 4.5,
hit_box_points: Optional[PointList] = None,
):
if not isinstance(image, PIL.Image.Image):
raise ValueError("A texture must have an image")
self._name = name
if isinstance(image, PIL.Image.Image):
self._image_data = ImageData(image)
elif isinstance(image, ImageData):
self._image_data = image
else:
raise ValueError("image must be an instance of PIL.Image.Image or ImageData")
# Set the size of the texture since this is immutable
self._size = image.width, image.height
# The order of the texture coordinates when mapping
# to a sprite/quad. This order is changed when the
# texture is flipped or rotated.
self._vertex_order = 0, 1, 2, 3
# List of transforms applied to this texture
self._transforms: List[Type[Transform]] = []
# Internal sprite stuff for drawing
self._sprite: Optional[Sprite] = None
self._sprite_list: Optional[SpriteList] = None
self._hit_box_algorithm: Optional[str] = hit_box_algorithm
if self._hit_box_algorithm is not None:
if not isinstance(self._hit_box_algorithm, str):
raise ValueError(
f"hit_box_algorithm must be a string or None, not {hit_box_algorithm}")
self._hit_box_algorithm = self._hit_box_algorithm.lower()
self._hit_box_detail = hit_box_detail
self._hit_box_points: PointList = hit_box_points or self._calculate_hit_box_points()
@property
def name(self) -> str:
"""
The name of the texture (read only).
:return: str
"""
return self._name
@property
def image(self) -> PIL.Image.Image:
"""
Get or set the image of the texture.
.. warning::
This is an advanced function. Be absolutely sure
you know the consequences of changing the image.
It can cause problems with the texture atlas and
hit box points.
:param PIL.Image.Image image: The image to set
"""
return self._image_data.image
@image.setter
def image(self, image: PIL.Image.Image):
if image.size != self._image_data.image.size:
raise ValueError("New image must be the same size as the old image")
self._image_data.image = image
@property
def image_data(self) -> ImageData:
"""
The image data of the texture (read only).
This is a simple wrapper around the image
containing metadata like hash and is used
to determine the uniqueness of the image
in texture atlases.
:return: ImageData
"""
return self._image_data
@property
def width(self) -> int:
"""Width of the texture in pixels."""
return self._size[0]
@property
def height(self) -> int:
"""Height of the texture in pixels."""
return self._size[1]
@property
def size(self) -> Tuple[int, int]:
"""Width and height as a tuple"""
return self._size
@property
def hit_box_points(self) -> PointList:
"""
Get the hit box points for this texture.
Custom hit box points must be supplied during texture creation
and should ideally not be changed after creation.
:return: PointList
"""
return self._hit_box_points
@property
def hit_box_algorithm(self) -> Optional[str]:
"""
(read only) The algorithm used to calculate the hit box for this texture.
"""
return self._hit_box_algorithm
[docs] @classmethod
def create_filled(cls, name: str, size: Tuple[int, int], color: Color) -> "Texture":
"""
Create a filled texture. This is an alias for :py:meth:`create_empty`.
:param str name: Name of the texture
:param Tuple[int, int] size: Size of the texture
:param Color color: Color of the texture
:return: Texture
"""
return cls.create_empty(name, size, color)
[docs] @classmethod
def create_empty(
cls,
name: str,
size: Tuple[int, int],
color: Color = (0, 0, 0, 0),
) -> "Texture":
"""
Create a texture with all pixels set to transparent black.
The hit box of the returned Texture will be set to a rectangle
with the dimensions in ``size`` because there is no non-blank
pixel data to calculate a hit box.
:param str name: The unique name for this texture
:param Tuple[int,int] size: The xy size of the internal image
This function has multiple uses, including:
- Allocating space in texture atlases
- Generating custom cached textures from component images
The internal image can be altered with Pillow draw commands and
then written/updated to a texture atlas. This works best for
infrequent changes such as generating custom cached sprites.
For frequent texture changes, you should instead render directly
into the texture atlas.
.. warning::
If you plan to alter images using Pillow, read its
documentation thoroughly! Some of the functions can have
unexpected behavior.
For example, if you want to draw one or more images that
contain transparency onto a base image that also contains
transparency, you will likely need to use
`PIL.Image.alpha_composite`_ as part of your solution.
Otherwise, blending may behave in unexpected ways.
This is especially important for customizable characters.
.. _PIL.Image.alpha_composite: https://pillow.readthedocs.io/en/stable/\
reference/Image.html#PIL.Image.alpha_composite
Be careful of your RAM usage when using this function. The
Texture this method returns will have a new internal RGBA
Pillow image which uses 4 bytes for every pixel in it.
This will quickly add up if you create many large Textures.
If you want to create more than one blank texture with the same
dimensions, you can save CPU time and RAM by calling this
function once, then passing the ``image`` attribute of the
resulting Texture object to the class constructor for each
additional blank Texture instance you would like to create.
This can be especially helpful if you are creating multiple
large Textures.
"""
return Texture(
name,
image=PIL.Image.new("RGBA", size, TRANSPARENT_BLACK),
hit_box_algorithm=None,
)
[docs] def remove_from_cache(self) -> None:
"""
Remove this texture from the cache.
:return: None
"""
try:
del self.cache[self._name]
except KeyError:
pass
[docs] def flip_left_to_right(self) -> "Texture":
"""
Flip the texture left to right / horizontally.
This returns a new texture with the same image data, but
has updated hit box data and a transform that will be
applied to the image when it's drawn (GPU side).
:return: Texture
"""
return self._new_texture_transformed(FlipLeftToRightTransform)
[docs] def flip_top_to_bottom(self) -> "Texture":
"""
Flip the texture top to bottom / vertically.
This returns a new texture with the same image data, but
has updated hit box data and a transform that will be
applied to the image when it's drawn (GPU side).
:return: Texture
"""
return self._new_texture_transformed(FlipTopToBottomTransform)
[docs] def flip_diagonally(self) -> "Texture":
"""
Returns a new texture that is flipped diagonally from this texture.
This is an alias for :func:`transpose`.
This returns a new texture with the same image data, but
has updated hit box data and a transform that will be
applied to the image when it's drawn (GPU side).
:return: Texture
"""
return self.transpose()
[docs] def transpose(self) -> "Texture":
"""
Returns a new texture that is transposed from this texture.
This flips the texture diagonally from lower right to upper left.
This returns a new texture with the same image data, but
has updated hit box data and a transform that will be
applied to the image when it's drawn (GPU side).
:return: Texture
"""
return self._new_texture_transformed(TransposeTransform)
[docs] def transverse(self) -> "Texture":
"""
Returns a new texture that is transverse from this texture.
This flips the texture diagonally from lower left to upper right.
This returns a new texture with the same image data, but
has updated hit box data and a transform that will be
applied to the image when it's drawn (GPU side).
:return: Texture
"""
return self._new_texture_transformed(TransverseTransform)
[docs] def rotate(self, count: int) -> "Texture":
"""
Rotate the texture by a given number of 90 degree steps.
This returns a new texture with the same image data, but
has updated hit box data and a transform that will be
applied to the image when it's drawn (GPU side).
:param int count: Number of 90 degree steps to rotate.
:return: Texture
"""
angles = [None, Rotate90Transform, Rotate180Transform, Rotate270Transform]
count = count % 4
transform = angles[count]
if transform is None:
return self
return self._new_texture_transformed(transform)
[docs] def crop(self, x: int, y: int, width: int, height: int) -> "Texture":
"""
Create a new texture from a crop of this texture.
:param int x: X position to start crop
:param int y: Y position to start crop
:param int width: Width of crop
:param int height: Height of crop
:return: Texture
"""
raise NotImplementedError()
def _new_texture_transformed(self, transform: Type[Transform]) -> "Texture":
"""
Create a new texture with the given transform applied.
:param Transform transform: Transform to apply
:return: New texture
"""
points = transform.transform_hit_box_points(self._hit_box_points)
texture = Texture(
name=self._name,
image=self.image_data,
# Not relevant, but copy over the value
hit_box_algorithm=self._hit_box_algorithm,
hit_box_points=points,
)
texture._vertex_order = transform.transform_vertex_order(self._vertex_order)
texture._transforms = get_shortest_transform(texture._vertex_order)
return texture
# ------------------------------------------------------------
# Comparison and hash functions so textures can work with sets
# A texture's uniqueness is simply based on the name
# ------------------------------------------------------------
def __hash__(self) -> int:
return hash(self.name)
def __eq__(self, other) -> bool:
if other is None:
return False
if not isinstance(other, self.__class__):
return False
return self.name == other.name
def __ne__(self, other) -> bool:
if other is None:
return True
if not isinstance(other, self.__class__):
return True
return self.name != other.name
# ------------------------------------------------------------
def _calculate_hit_box_points(self) -> PointList:
"""
Calculate the hit box points for this texture
based on the configured hit box algorithm.
This is usually done on texture creation
or when the hit box points are requested the first time.
"""
# Handle some legacy cases:
# Use bounding algo if not set
algo_name = self._hit_box_algorithm or "bounding"
if algo_name == "none":
algo_name = "bounding"
# Check if we're using default algo
if algo_name == "default":
algo_name = hitbox.default_algorithm.name
# Check if we have cached points
keys = [self._image_data.hash, algo_name, self._hit_box_detail]
points = self.hit_box_cache.get(keys)
if points:
return points
# Calculate points with the selected algorithm
algo = hitbox.get_algorithm(algo_name)
points = algo.calculate(self.image, hit_box_algorithm=self._hit_box_detail)
self.hit_box_cache.put(keys, points)
return points
def _create_cached_sprite(self):
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(capacity=1)
self._sprite_list.append(self._sprite)
[docs] def draw_sized(
self,
center_x: float,
center_y: float,
width: float,
height: float,
angle: float = 0.0,
alpha: int = 255,
):
"""
Draw a texture with a specific width and height.
.. warning:: This is a very slow method of drawing a texture,
and should be used sparingly. The method simply
creates a sprite internally and draws it.
:param float center_x: X position to draw texture
:param float center_y: Y position to draw texture
:param float width: Width to draw texture
:param float height: Height to draw texture
:param float angle: Angle to draw texture
:param int alpha: Alpha value to draw texture
"""
self._create_cached_sprite()
if self._sprite and self._sprite_list:
self._sprite.center_x = center_x
self._sprite.center_y = center_y
self._sprite.height = height
self._sprite.width = width
self._sprite.angle = angle
self._sprite.alpha = alpha
self._sprite_list.draw()
[docs] def draw_scaled(
self,
center_x: float,
center_y: float,
scale: float = 1.0,
angle: float = 0.0,
alpha: int = 255,
):
"""
Draw the texture.
.. warning:: This is a very slow method of drawing a texture,
and should be used sparingly. The method simply
creates a sprite internally and draws it.
:param float center_x: X location of where to draw the texture.
:param float center_y: Y location of where to draw the texture.
:param float scale: Scale to draw rectangle. Defaults to 1.
:param float angle: Angle to rotate the texture by.
:param int alpha: The transparency of the texture `(0-255)`.
"""
self._create_cached_sprite()
if self._sprite and self._sprite_list:
self._sprite.center_x = center_x
self._sprite.center_y = center_y
self._sprite.scale = scale
self._sprite.angle = angle
self._sprite.alpha = alpha
self._sprite_list.draw()
[docs]class SolidColorTexture(Texture):
"""
Used internally in Arcade to make colored textures.
This is not indented to be used by the end user.
This texture variant is mainly here it override the
width and height property to fake texture size for
sprites. The internal texture is always a fixed sized
white texture that is colored by the sprite's color property.
:param str name: Name of the texture
:param int width: Width of the texture
:param int height: Height of the texture
:param img: The pillow image
:param hit_box_points: The hit box points
"""
def __init__(self, name, width, height, image):
# We hardcode hit box points based on the fake width and height
hit_box_points = (
(-width / 2, -height / 2),
(width / 2, -height / 2),
(width / 2, height / 2),
(-width / 2, height / 2)
)
super().__init__(
name,
image=image,
hit_box_algorithm=None,
hit_box_points=hit_box_points,
)
# Override the width and height
self._width = width
self._height = height
@property
def width(self):
return self._width
@property
def height(self):
return self._height
[docs]def load_textures(
file_name: Union[str, Path],
image_location_list: RectList,
mirrored: bool = False,
flipped: bool = False,
hit_box_algorithm: Optional[str] = "Simple",
hit_box_detail: float = 4.5,
) -> List[Texture]:
"""
Load a set of textures from 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.
Be careful with 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 List image_location_list: List of image sub-locations. Each rectangle 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 str hit_box_algorithm: One of None, 'None', 'Simple' (default) or 'Detailed'.
:param float hit_box_detail: Float, defaults to 4.5. Used with 'Detailed' to hit box
:returns: List of :class:`Texture`'s.
:raises: ValueError
"""
LOG.info("load_textures: %s ", file_name)
image = PIL.Image.open(resolve_resource_path(file_name))
texture_sections = []
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 > image.width:
raise ValueError(
"Can't load texture starting at an x of {} "
"when the image is only {} across.".format(x, image.width)
)
if y > image.height:
raise ValueError(
"Can't load texture starting at an y of {} "
"when the image is only {} high.".format(y, image.height)
)
if x + width > image.width:
raise ValueError(
"Can't load texture ending at an x of {} "
"when the image is only {} wide.".format(x + width, image.width)
)
if y + height > image.height:
raise ValueError(
"Can't load texture ending at an y of {} "
"when the image is only {} high.".format(
y + height, image.height,
)
)
# See if we already loaded this texture, and we can just use a cached version.
name = build_cache_name(
file_name, x, y, width, height, flipped, mirrored
)
try:
sub_texture = Texture.cache[name]
except KeyError:
sub_image = image.crop((x, y, x + width, y + height))
if mirrored:
sub_image = PIL.ImageOps.mirror(sub_image)
if flipped:
sub_image = PIL.ImageOps.flip(sub_image)
sub_texture = Texture(
name,
image=sub_image,
hit_box_algorithm=hit_box_algorithm,
hit_box_detail=hit_box_detail,
)
Texture.cache[name] = sub_texture
texture_sections.append(sub_texture)
return texture_sections
[docs]def load_texture(
file_name: Union[str, Path],
x: int = 0,
y: int = 0,
width: int = 0,
height: int = 0,
flipped_horizontally: bool = False,
flipped_vertically: bool = False,
flipped_diagonally: bool = False,
hit_box_algorithm: Optional[str] = "Simple",
hit_box_detail: float = 4.5,
) -> Texture:
"""
Load an 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.
Be careful with 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 flipped_horizontally: Mirror the sprite image. Flip left/right across vertical axis.
:param bool flipped_vertically: Flip the image up/down across the horizontal axis.
:param bool flipped_diagonally: Transpose the image, flip it across the diagonal.
:param bool mirrored: Deprecated.
:param str hit_box_algorithm: One of None, 'None', 'Simple' or 'Detailed'. \
Defaults to 'Simple'. Use 'Simple' for the :data:`PhysicsEngineSimple`, \
:data:`PhysicsEnginePlatformer` \
and 'Detailed' for the :data:`PymunkPhysicsEngine`.
.. figure:: ../images/hit_box_algorithm_none.png
:width: 40%
hit_box_algorithm = "None"
.. figure:: ../images/hit_box_algorithm_simple.png
:width: 55%
hit_box_algorithm = "Simple"
.. figure:: ../images/hit_box_algorithm_detailed.png
:width: 75%
hit_box_algorithm = "Detailed"
:param float hit_box_detail: Float, defaults to 4.5. Used with 'Detailed' to hit box
:returns: New :class:`Texture` object.
:raises: ValueError
"""
LOG.info("load_texture: %s ", file_name)
# First check if we have a cached version of this texture.
name = build_cache_name(
file_name,
x, y,
width, height,
flipped_horizontally, flipped_vertically, flipped_diagonally,
hit_box_algorithm,
)
try:
return Texture.cache[name]
except KeyError:
pass
# See if we already loaded this texture file, and we can just use a cached version.
try:
texture = Texture.cache[str(file_name)]
except KeyError:
file_name = resolve_resource_path(file_name)
texture = Texture(
str(file_name),
PIL.Image.open(file_name).convert("RGBA"),
hit_box_algorithm=hit_box_algorithm,
hit_box_detail=hit_box_detail,
)
Texture.cache[str(file_name)] = texture
if x != 0 or y != 0 or width != 0 or height != 0:
if x > texture.image.width:
raise ValueError(
"Can't load texture starting at an x of {} "
"when the image is only {} across.".format(x, texture.image.width)
)
if y > texture.image.height:
raise ValueError(
"Can't load texture starting at an y of {} "
"when the image is only {} high.".format(y, texture.image.height)
)
if x + width > texture.image.width:
raise ValueError(
"Can't load texture ending at an x of {} "
"when the image is only {} wide.".format(x + width, texture.image.width)
)
if y + height > texture.image.height:
raise ValueError(
"Can't load texture ending at an y of {} "
"when the image is only {} high.".format(
y + height, texture.image.height
)
)
image = texture.image.crop((x, y, x + width, y + height))
else:
image = texture.image
if flipped_diagonally:
image = image.transpose(PIL.Image.Transpose.TRANSPOSE)
if flipped_horizontally:
image = image.transpose(PIL.Image.Transpose.FLIP_LEFT_RIGHT)
if flipped_vertically:
image = image.transpose(PIL.Image.Transpose.FLIP_TOP_BOTTOM)
texture = Texture(
name,
image,
hit_box_algorithm=hit_box_algorithm,
hit_box_detail=hit_box_detail,
)
Texture.cache[name] = texture
return texture
[docs]def cleanup_texture_cache():
"""
This cleans up the cache of textures. Useful when running unit tests so that
the next test starts clean.
"""
Texture.cache = Texture.cache.__class__()
import gc
gc.collect()
[docs]def load_texture_pair(file_name: str, hit_box_algorithm: str = "Simple"):
"""
Load a texture pair, with the second being a mirror image of the first.
Useful when doing animations and the character can face left/right.
:param str file_name: Path to texture
:param str hit_box_algorithm: The hit box algorithm
"""
LOG.info("load_texture_pair: %s ", file_name)
return [
load_texture(file_name, hit_box_algorithm=hit_box_algorithm),
load_texture(
file_name, flipped_horizontally=True, hit_box_algorithm=hit_box_algorithm
),
]
[docs]def load_spritesheet(
file_name: Union[str, Path],
sprite_width: int,
sprite_height: int,
columns: int,
count: int,
margin: int = 0,
hit_box_algorithm: Optional[str] = "Simple",
hit_box_detail: float = 4.5,
) -> List[Texture]:
"""
:param str file_name: Name of the file to that holds the texture.
:param int sprite_width: Width of the sprites in pixels
:param int sprite_height: Height of the sprites in pixels
:param int columns: Number of tiles wide the image is.
:param int count: Number of tiles in the image.
:param int margin: Margin between images
:param str hit_box_algorithm: One of None, 'None', 'Simple' (default) or 'Detailed'.
:param float hit_box_detail: Float, defaults to 4.5. Used with 'Detailed' to hit box
:returns List: List of :class:`Texture` objects.
"""
LOG.info("load_spritesheet: %s ", file_name)
texture_list = []
# If we should pull from local resources, replace with proper path
file_name = resolve_resource_path(file_name)
source_image = PIL.Image.open(file_name).convert("RGBA")
for sprite_no in range(count):
row = sprite_no // columns
column = sprite_no % columns
start_x = (sprite_width + margin) * column
start_y = (sprite_height + margin) * row
image = source_image.crop(
(start_x, start_y, start_x + sprite_width, start_y + sprite_height)
)
texture = Texture(
f"{file_name}-{sprite_no}",
image=image,
hit_box_algorithm=hit_box_algorithm,
hit_box_detail=hit_box_detail,
)
texture_list.append(texture)
return texture_list
[docs]def make_circle_texture(diameter: int, color: Color, name: Optional[str] = None) -> Texture:
"""
Return a Texture of a circle with the given diameter and color.
:param int diameter: Diameter of the circle and dimensions of the square :class:`Texture` returned.
:param Color color: Color of the circle.
:param str name: Custom or pre-chosen name for this texture
:returns: New :class:`Texture` object.
"""
name = name or build_cache_name(
"circle_texture", diameter, color[0], color[1], color[2]
)
bg_color = TRANSPARENT_BLACK # 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)
return Texture(name, img)
[docs]def make_soft_circle_texture(
diameter: int,
color: Color,
center_alpha: int = 255,
outer_alpha: int = 0,
name: Optional[str] = None,
) -> Texture:
"""
Return a :class:`Texture` of a circle with the given diameter and color, fading out at its edges.
:param int diameter: Diameter of the circle and dimensions of the square :class:`Texture` returned.
:param Color color: Color of the circle.
:param int center_alpha: Alpha value of the circle at its center.
:param int outer_alpha: Alpha value of the circle at its edges.
:param str name: Custom or pre-chosen name for this texture
:returns: New :class:`Texture` object.
:rtype: arcade.Texture
"""
# TODO: create a rectangle and circle (and triangle? and arbitrary poly where client passes
# in list of points?) particle?
name = build_cache_name(
"soft_circle_texture",
diameter,
color[0],
color[1],
color[2],
center_alpha,
outer_alpha,
) # name must be unique for caching
bg_color = TRANSPARENT_BLACK
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,
)
return Texture(name, img)
[docs]def make_soft_square_texture(
size: int,
color: Color,
center_alpha: int = 255,
outer_alpha: int = 0,
name: Optional[str] = None,
) -> Texture:
"""
Return a :class:`Texture` of a square with the given diameter and color, fading out at its edges.
:param int size: Diameter of the square and dimensions of the square Texture returned.
:param Color color: Color of the square.
:param int center_alpha: Alpha value of the square at its center.
:param int outer_alpha: Alpha value of the square at its edges.
:param str name: Custom or pre-chosen name for this texture
:returns: New :class:`Texture` object.
"""
# name must be unique for caching
name = name or build_cache_name(
"gradient-square", size, color, center_alpha, outer_alpha
)
bg_color = TRANSPARENT_BLACK
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
)
return Texture(name, img)