-
Notifications
You must be signed in to change notification settings - Fork 193
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixed #36 - Allow assignment to Actor's width and height to scale the Actor #92
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
import pygame | ||
from math import radians, sin, cos, atan2, degrees, sqrt | ||
import collections | ||
|
||
from . import game | ||
from . import loaders | ||
|
@@ -34,6 +35,10 @@ def calculate_anchor(value, dim, total): | |
return float(value) | ||
|
||
|
||
class InvalidScaleException(Exception): | ||
"""The scale parameters where invalid ( scale == 0).""" | ||
pass | ||
|
||
# These are methods (of the same name) on pygame.Rect | ||
SYMBOLIC_POSITIONS = set(( | ||
"topleft", "bottomleft", "topright", "bottomright", | ||
|
@@ -85,6 +90,7 @@ def __init__(self, image, pos=POS_TOPLEFT, anchor=ANCHOR_CENTER, **kwargs): | |
# Initialise it at (0, 0) for size (0, 0). | ||
# We'll move it to the right place and resize it later | ||
|
||
self._scale_x = self._scale_y = 1 | ||
self.image = image | ||
self._init_position(pos, anchor, **kwargs) | ||
|
||
|
@@ -150,11 +156,11 @@ def anchor(self): | |
@anchor.setter | ||
def anchor(self, val): | ||
self._anchor_value = val | ||
self._calc_anchor() | ||
self._calc_anchor(self._orig_surf) | ||
|
||
def _calc_anchor(self): | ||
def _calc_anchor(self, surf): | ||
ax, ay = self._anchor_value | ||
ow, oh = self._orig_surf.get_size() | ||
ow, oh = surf.get_size() | ||
ax = calculate_anchor(ax, 'x', ow) | ||
ay = calculate_anchor(ay, 'y', oh) | ||
self._untransformed_anchor = ax, ay | ||
|
@@ -169,14 +175,9 @@ def angle(self): | |
|
||
@angle.setter | ||
def angle(self, angle): | ||
self._angle = angle | ||
self._surf = pygame.transform.rotate(self._orig_surf, angle) | ||
p = self.pos | ||
self._adjust_scale(self._scale_x, self._scale_y) | ||
self._adjust_angle(angle) | ||
self.width, self.height = self._surf.get_size() | ||
w, h = self._orig_surf.get_size() | ||
ax, ay = self._untransformed_anchor | ||
self._anchor = transform_anchor(ax, ay, w, h, angle) | ||
self.pos = p | ||
|
||
@property | ||
def pos(self): | ||
|
@@ -216,12 +217,102 @@ def image(self): | |
def image(self, image): | ||
self._image_name = image | ||
self._orig_surf = self._surf = loaders.images.load(image) | ||
self._update_pos() | ||
self._adjust_scale(self._scale_x, self._scale_y) | ||
try: | ||
self._adjust_angle(self._angle) | ||
except AttributeError: | ||
self._update_pos() | ||
self._adjust_angle(self._angle) | ||
|
||
self.width, self.height = self._surf.get_size() | ||
|
||
|
||
@property | ||
def scale(self): | ||
return self._scale_x, self._scale_y | ||
|
||
@scale.setter | ||
def scale(self, scale): | ||
if isinstance(scale, collections.Sequence): | ||
x, y = scale[0], scale[1] | ||
else: | ||
x = y = scale | ||
|
||
if self._validate_scale_values(x, y): | ||
self._adjust_scale(x, y) | ||
self._adjust_angle(self._angle) | ||
self.width, self.height = self._surf.get_size() | ||
|
||
@property | ||
def scale_x(self): | ||
return self._scale_x | ||
|
||
@scale_x.setter | ||
def scale_x(self, x): | ||
if self._validate_scale_values(x, self._scale_y): | ||
self._adjust_scale(x, self._scale_y) | ||
self._adjust_angle(self._angle) | ||
self.width, self.height = self._surf.get_size() | ||
|
||
@property | ||
def scale_y(self): | ||
return self._scale_y | ||
|
||
@scale_y.setter | ||
def scale_y(self, y): | ||
if self._validate_scale_values(self._scale_x, y): | ||
self._adjust_scale(self._scale_x, y) | ||
self._adjust_angle(self._angle) | ||
self.width, self.height = self._surf.get_size() | ||
|
||
def _validate_scale_values(self, x, y): | ||
"""Validates the x and y scaling values and raises appropriate exceptions. | ||
|
||
If the new values are the same, then | ||
""" | ||
if not isinstance(x, (int, float)): | ||
raise TypeError('Invalid type of scale values. Expected "int/ float", got "{}".'.format(type(x).__name__)) | ||
|
||
if not isinstance(y, (int, float)): | ||
raise TypeError('Invalid type of scale values. Expected "int/ float", got "{}".'.format(type(y).__name__)) | ||
|
||
if x == 0 or y == 0: | ||
raise InvalidScaleException('Invalid scale values. They should be not equal to 0.') | ||
|
||
if self._scale_x == x and self._scale_y == y: | ||
return False | ||
|
||
return True | ||
|
||
def _adjust_scale(self, x, y): | ||
|
||
self._scale_x = x | ||
self._scale_y = y | ||
|
||
if x == 1.0 and y == 1.0: | ||
self._surf = self._orig_surf | ||
pass | ||
|
||
new_width = int(self._orig_surf.get_size()[0] * abs(x)) | ||
new_height = int(self._orig_surf.get_size()[1] * abs(y)) | ||
|
||
self._surf = pygame.transform.scale(self._orig_surf, (new_width, new_height)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should special-case the no-op case for performance for each of these - a check on whether scale_ == scale_y == 1 is much cheaper than doing the transform, which will copy a surface. Same with rotation and flip. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
self._surf = pygame.transform.flip(self._surf, (x < 0), (y < 0)) | ||
|
||
def _adjust_angle(self, angle): | ||
self._angle = angle | ||
self._surf = pygame.transform.rotate(self._surf, angle) | ||
|
||
p = self.pos | ||
w, h = self._orig_surf.get_size() | ||
ax, ay = self._untransformed_anchor | ||
self._anchor = transform_anchor(ax, ay, w, h, angle) | ||
self.pos = p | ||
|
||
def _update_pos(self): | ||
p = self.pos | ||
self.width, self.height = self._surf.get_size() | ||
self._calc_anchor() | ||
self._calc_anchor(self._surf) | ||
self.pos = p | ||
|
||
def draw(self): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,8 +2,8 @@ | |
|
||
import pygame | ||
|
||
from pgzero.actor import calculate_anchor, Actor | ||
from pgzero.loaders import set_root | ||
from pgzero.actor import calculate_anchor, Actor, InvalidScaleException | ||
from pgzero.loaders import set_root, images | ||
|
||
|
||
TEST_MODULE = "pgzero.actor" | ||
|
@@ -39,6 +39,12 @@ class ActorTest(unittest.TestCase): | |
def setUpClass(self): | ||
set_root(__file__) | ||
|
||
def assertImagesEqual(self, a, b): | ||
adata, bdata = (pygame.image.tostring(i, 'RGB') for i in (a, b)) | ||
|
||
if adata != bdata: | ||
raise AssertionError("Images differ") | ||
|
||
def test_sensible_init_defaults(self): | ||
a = Actor("alien") | ||
|
||
|
@@ -107,3 +113,116 @@ def test_rotation(self): | |
for _ in range(360): | ||
a.angle += 1.0 | ||
self.assertEqual(a.pos, (100.0, 100.0)) | ||
|
||
def test_no_scaling(self): | ||
actor = Actor('alien') | ||
originial_size = (actor.width, actor.height) | ||
|
||
actor.scale = 1 | ||
self.assertEqual((actor.width, actor.height), originial_size) | ||
|
||
def test_scale_horizontal(self): | ||
actor = Actor('alien') | ||
originial_size = (actor.width, actor.height) | ||
|
||
actor.scale_x = 2 | ||
self.assertEqual((actor.width, actor.height), (originial_size[0] * 2, originial_size[1])) | ||
|
||
def test_scale_vertical(self): | ||
actor = Actor('alien') | ||
originial_size = (actor.width, actor.height) | ||
|
||
actor.scale_y = 2 | ||
self.assertEqual((actor.width, actor.height), (originial_size[0], originial_size[1] * 2)) | ||
|
||
def test_scale_down(self): | ||
actor = Actor('alien') | ||
originial_size = (actor.width, actor.height) | ||
|
||
actor.scale = (.5, .5) | ||
self.assertEqual((actor.width, actor.height), (originial_size[0]/2, originial_size[1]/2)) | ||
|
||
def test_scale_different(self): | ||
actor = Actor('alien') | ||
originial_size = (actor.width, actor.height) | ||
|
||
actor.scale = (.5, 3) | ||
self.assertEqual((actor.width, actor.height), (originial_size[0]/2, originial_size[1]*3)) | ||
|
||
def test_scale_from_float(self): | ||
actor1 = Actor('alien') | ||
actor2 = Actor('alien') | ||
|
||
actor1.scale = .5 | ||
actor2.scale = (.5, .5) | ||
|
||
self.assertEqual(actor1.width, actor2.width) | ||
self.assertEqual(actor1.height, actor2.height) | ||
|
||
def test_scaling_on_x_and_y(self): | ||
actor1 = Actor('alien') | ||
actor2 = Actor('alien') | ||
|
||
actor1.scale = .5 | ||
actor2.scale_x = .5 | ||
actor2.scale_y = .5 | ||
|
||
self.assertEqual(actor1.width, actor2.width) | ||
self.assertEqual(actor1.height, actor2.height) | ||
|
||
def test_rotate_and_scale(self): | ||
actor = Actor('alien') | ||
original_size = (actor.width, actor.height) | ||
|
||
actor.angle = 90 | ||
actor.scale = .5 | ||
self.assertEqual(actor.angle, 90) | ||
self.assertEqual((actor.width, actor.height), (original_size[1]/2, original_size[0]/2)) | ||
self.assertEqual(actor.topleft, (-13, 13)) | ||
|
||
def test_exception_invalid_scale_params(self): | ||
actor = Actor('alien') | ||
|
||
with self.assertRaises(InvalidScaleException) as cm: | ||
actor.scale = (0, -2) | ||
self.assertEqual(cm.exception.args[0], 'Invalid scale values. They should be not equal to 0.') | ||
|
||
def test_exception_invalid_types(self): | ||
actor = Actor('alien') | ||
|
||
with self.assertRaises(TypeError) as cm: | ||
actor.scale = ('something', 1) | ||
self.assertEqual(cm.exception.args[0], 'Invalid type of scale values. Expected "int/ float", got "str".') | ||
|
||
def test_horizontal_flip(self): | ||
actor = Actor('alien') | ||
orig = images.load('alien') | ||
exp = pygame.transform.flip(orig, True, False) | ||
|
||
actor.scale = (-1, 1) | ||
self.assertImagesEqual(exp, actor._surf) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A tip is "one assert per test". This means that if a test fails, all of the other tests still run, which gives more information, when refactoring, about what you might have broken. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
|
||
def test_vertical_flip(self): | ||
actor = Actor('alien') | ||
orig = images.load('alien') | ||
|
||
exp = pygame.transform.flip(orig, False, True) | ||
actor.scale = (1, -1) | ||
self.assertImagesEqual(exp, actor._surf) | ||
|
||
def test_flip_both_axes(self): | ||
actor = Actor('alien') | ||
orig = images.load('alien') | ||
|
||
exp = pygame.transform.flip(orig, True, True) | ||
actor.scale = -1 | ||
self.assertImagesEqual(exp, actor._surf) | ||
|
||
def test_flip_and_scale(self): | ||
actor = Actor('alien') | ||
orig = images.load('alien') | ||
|
||
exp = pygame.transform.scale(orig, (orig.get_size()[0]*3, orig.get_size()[1]*2)) | ||
exp = pygame.transform.flip(exp, True, True) | ||
actor.scale = (-3, -2) | ||
self.assertImagesEqual(exp, actor._surf) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd quite like scale to accept a single int/float, as a shortcut for setting both values.
Actually I would expect that is the more common case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done