Skip to content
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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
115 changes: 103 additions & 12 deletions pgzero/actor.py
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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Copy link
Owner

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

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))
Copy link
Owner

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

The 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):
Expand Down
123 changes: 121 additions & 2 deletions test/test_actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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)
Copy link
Owner

Choose a reason for hiding this comment

The 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.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The 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)