Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page.

## Unreleased

### Fixes
- Fixed several `arcade.create_text_sprite()` bugs:
- Sprites with the same text shared one image in the texture atlas, so creating a second one (for example in another color or size) changed the first one and drew the second stretched. Each sprite now gets its own image.
- `anchor_x="center"` cut off the left half of the text, and `anchor_x="right"` made an empty sprite.
- The sprite was placed too high. It now covers the same area as an `arcade.Text` with the same arguments at the same position.
- An empty string raised `ValueError`. It now makes a transparent sprite, as documented.

### New Features
- Added `arcade.sweep_sprite(sprite, dx, dy, sprite_list)`, which checks the whole path of a moving sprite and returns a `SweepInfo` for the first sprite it would hit (the `sprite`, how far along the move as a `fraction` and `distance`, and the surface `normal`), or `None`. Fast sprites can't pass through thin walls this way. A sprite that already overlaps one is an immediate hit. Added the `sprite_bullets_sweep` example comparing it with a plain collision check.

Expand Down
26 changes: 20 additions & 6 deletions arcade/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
Drawing text with pyglet label
"""

import math
from pathlib import Path
from typing import Any
from uuid import uuid4

import pyglet
from pyglet.enums import Style, Weight
Expand Down Expand Up @@ -913,12 +915,23 @@ def create_text_sprite(
multiline=multiline,
)

# Where the text is with its anchor at (0, 0), as a Text object would be
left = text_object.left
bottom = text_object.bottom
# At least 1 pixel, so an empty string still makes a (transparent) texture
size = (
int(text_object.right - text_object.left),
int(text_object.top - text_object.bottom),
max(1, math.ceil(text_object.right - left)),
max(1, math.ceil(text_object.top - bottom)),
)
text_object.y = -text_object.bottom
texture = arcade.Texture.create_empty(text, size)

# Draw it into the texture with its bottom left corner at (0, 0)
text_object.x = -left
text_object.y = -bottom

# Each sprite needs its own image in the atlas. A name based on the text
# would make sprites with the same text, but different colors or sizes,
# share one.
texture = arcade.Texture.create_empty(f"create_text_sprite_{uuid4().hex}", size)

if not texture_atlas:
texture_atlas = arcade.get_window().ctx.default_atlas
Expand All @@ -927,10 +940,11 @@ def create_text_sprite(
fbo.clear(color=background_color or arcade.color.TRANSPARENT_BLACK)
text_object.draw()

# Place the sprite where the Text object was drawn
return arcade.Sprite(
texture,
center_x=text_object.right - (size[0] / 2),
center_y=text_object.top,
center_x=left + size[0] / 2,
center_y=bottom + size[1] / 2,
)


Expand Down
75 changes: 75 additions & 0 deletions tests/unit/text/test_text_sprite.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,78 @@ def test_create(window, color):
assert isinstance(sprite, arcade.Sprite)
assert sprite.width == pytest.approx(75, rel=10)
assert sprite.height == pytest.approx(20, rel=5)


def _ink(image):
"""Total alpha of an image, as a measure of how much was drawn"""
return sum(value * count for value, count in enumerate(image.getchannel("A").histogram()))


def _sprite_bounds(sprite):
return sprite.left, sprite.right, sprite.bottom, sprite.top


def _text_bounds(text):
return text.left, text.right, text.bottom, text.top


@pytest.mark.parametrize(
"kwargs",
[
{},
{"anchor_x": "center"},
{"anchor_x": "right"},
{"font_size": 40},
{"multiline": True, "width": 120},
{"multiline": True, "width": 90, "align": "center", "anchor_x": "center"},
],
)
def test_position_matches_text(window, kwargs):
"""The sprite covers the same area as a Text object at the same position"""
text = "Two lines of text gj"
sprite = arcade.create_text_sprite(text, **kwargs)
text_object = arcade.Text(text, 0, 0, anchor_y="baseline", **kwargs)
assert _sprite_bounds(sprite) == pytest.approx(_text_bounds(text_object), abs=1)


@pytest.mark.parametrize("anchor_x", ["left", "center", "right"])
def test_anchor_draws_all_text(window, anchor_x):
"""Every anchor puts all of the text inside the texture"""
atlas = window.ctx.default_atlas
# Read each image right away, so this doesn't depend on the sprites
# having separate images (tested below)
anchored = arcade.create_text_sprite("Hello World", font_size=20, anchor_x=anchor_x)
actual = _ink(atlas.read_texture_image_from_atlas(anchored.texture))
left = arcade.create_text_sprite("Hello World", font_size=20)
expected = _ink(atlas.read_texture_image_from_atlas(left.texture))
assert expected > 0
assert actual == pytest.approx(expected, rel=0.01)


def test_same_text_different_style(window):
"""Sprites with the same text keep their own images"""
atlas = window.ctx.default_atlas
small_red = arcade.create_text_sprite("Same", color=arcade.color.RED, font_size=20)
red_image = atlas.read_texture_image_from_atlas(small_red.texture)

big_blue = arcade.create_text_sprite("Same", color=arcade.color.BLUE, font_size=40)

# Each texture has an atlas region of its own size
for sprite in (small_red, big_blue):
region = atlas.get_texture_region_info(sprite.texture.atlas_name)
assert (region.width, region.height) == sprite.texture.size
assert big_blue.texture.size != small_red.texture.size

# Creating the second sprite didn't change the first one's image
assert atlas.read_texture_image_from_atlas(small_red.texture).tobytes() == red_image.tobytes()
red, _green, blue, _alpha = red_image.getextrema()
assert red[1] > 200 and blue[1] == 0


def test_empty_text(window):
"""An empty string makes a transparent sprite instead of failing"""
sprite = arcade.create_text_sprite("")
assert sprite.texture.width >= 1
assert sprite.texture.height >= 1
image = window.ctx.default_atlas.read_texture_image_from_atlas(sprite.texture)
assert _ink(image) == 0
Loading