Skip to content

Commit 632880b

Browse files
committed
Added rotation code to image so it doesn't expose internals anymore
1 parent 49f9bc6 commit 632880b

File tree

1 file changed

+141
-31
lines changed

1 file changed

+141
-31
lines changed

Lib/turtle.py

Lines changed: 141 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -860,26 +860,140 @@ class TurtleGraphicsError(Exception):
860860
"""Some TurtleGraphics Error
861861
"""
862862

863-
class BaseShapeDrawer(object):
864-
"""Base class that allows for taking over controll of drawing process
865-
for custom turtle shapes
866-
"""
867-
def __init__(self, screen):
868-
self.screen = screen
863+
class TransformableImage(object):
864+
"""Class that handles rotation of image based turtle shape"""
865+
def __init__(self, screen, image):
866+
assert(isinstance(image, TK.PhotoImage))
867+
self._screen = screen
868+
self._originalImage = image
869+
self._rotationCenter = (image.width() / 2, image.height() / 2)
870+
self._tkPhotoImage = image.copy()
871+
self._currentOrientation = (1.0, 0)
872+
self._currentTilt = 0.0
873+
self._transformMatrix = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
874+
self._item = screen._createimage(self._tkPhotoImage)
875+
869876
def clone(self, *args, **kwargs):
870-
return self.__class__(self.screen, *args, **kwargs)
871-
def draw(self, position, orientation, pen_color, fill_color, transform, pen_size):
872-
pass
877+
return self.__class__(self._screen, self._originalImage, *args, **kwargs)
878+
879+
def _transform_coordinates(self, x, y):
880+
m = self._transformMatrix
881+
return (m[0][0] * x + m[0][1] * y + m[0][2],
882+
m[1][0] * x + m[1][1] * y + m[1][2])
883+
884+
def _get_new_bounding_box(self):
885+
w, h = self._originalImage.width(), self._originalImage.height()
886+
c = [self._transform_coordinates(x, y)
887+
for x, y in [(0, 0), (w, 0), (w, h), (0, h)]]
888+
min_x = min(x for x, _ in c)
889+
min_y = min(y for _, y in c)
890+
max_x = max(x for x, _ in c)
891+
max_y = max(y for _, y in c)
892+
return {
893+
"min_x": min_x,
894+
"min_y": min_y,
895+
"max_x": max_x,
896+
"max_y": max_y,
897+
"width": math.ceil(max_x - min_x),
898+
"height": math.ceil(max_y - min_y)
899+
}
900+
901+
def _interpolate_color(self, x, y):
902+
"""Interpolates color based on neighboring pixels."""
903+
x_floor, y_floor = int(x), int(y)
904+
x_ceil, y_ceil = math.ceil(x), math.ceil(y)
905+
906+
if x_floor == x_ceil and y_floor == y_ceil:
907+
return self._originalImage.get(x_floor, y_floor)
908+
909+
if x_floor == x_ceil:
910+
c1 = self._originalImage.get(x_floor, y_floor)
911+
c2 = self._originalImage.get(x_floor, y_ceil)
912+
alpha = y - y_floor
913+
return (
914+
int(c1[0] * (1 - alpha) + c2[0] * alpha),
915+
int(c1[1] * (1 - alpha) + c2[1] * alpha),
916+
int(c1[2] * (1 - alpha) + c2[2] * alpha),
917+
)
918+
919+
if y_floor == y_ceil:
920+
c1 = self._originalImage.get(x_floor, y_floor)
921+
c2 = self._originalImage.get(x_ceil, y_floor)
922+
alpha = x - x_floor
923+
return (
924+
int(c1[0] * (1 - alpha) + c2[0] * alpha),
925+
int(c1[1] * (1 - alpha) + c2[1] * alpha),
926+
int(c1[2] * (1 - alpha) + c2[2] * alpha),
927+
)
928+
929+
c11 = self._originalImage.get(x_floor, y_floor)
930+
c12 = self._originalImage.get(x_floor, y_ceil)
931+
c21 = self._originalImage.get(x_ceil, y_floor)
932+
c22 = self._originalImage.get(x_ceil, y_ceil)
933+
934+
alpha_x = x - x_floor
935+
alpha_y = y - y_floor
936+
937+
c1 = (
938+
int(c11[0] * (1 - alpha_y) + c12[0] * alpha_y),
939+
int(c11[1] * (1 - alpha_y) + c12[1] * alpha_y),
940+
int(c11[2] * (1 - alpha_y) + c12[2] * alpha_y),
941+
)
942+
c2 = (
943+
int(c21[0] * (1 - alpha_y) + c22[0] * alpha_y),
944+
int(c21[1] * (1 - alpha_y) + c22[1] * alpha_y),
945+
int(c21[2] * (1 - alpha_y) + c22[2] * alpha_y),
946+
)
947+
948+
return (
949+
int(c1[0] * (1 - alpha_x) + c2[0] * alpha_x),
950+
int(c1[1] * (1 - alpha_x) + c2[1] * alpha_x),
951+
int(c1[2] * (1 - alpha_x) + c2[2] * alpha_x),
952+
)
953+
954+
def draw(self, position, orientation, tilt):
955+
if (self._currentOrientation != orientation or self._currentTilt != tilt):
956+
angle = math.atan2(orientation[1], orientation[0]) + tilt
957+
cos_theta = math.cos(angle)
958+
sin_theta = math.sin(angle)
959+
x, y = self._rotationCenter
960+
self._transformMatrix = [
961+
[cos_theta, -sin_theta, x * (1 - cos_theta) + y * sin_theta],
962+
[sin_theta, cos_theta, y * (1 - cos_theta) - x * sin_theta],
963+
[0, 0, 1]
964+
]
965+
bounding_box = self._get_new_bounding_box()
966+
offset_x = bounding_box["min_x"] * -1
967+
offset_y = bounding_box["min_y"] * -1
968+
self._tkPhotoImage = TK.PhotoImage(width=bounding_box["width"], height=bounding_box["height"])
969+
970+
for new_y in range(bounding_box["height"]):
971+
for new_x in range(bounding_box["width"]):
972+
original_x, original_y = self._transform_coordinates(new_x - offset_x, new_y - offset_y)
973+
974+
if 0 <= original_x < self._originalImage.width() - 1 and 0 <= original_y < self._originalImage.height() - 1:
975+
rgb = self._interpolate_color(original_x, original_y)
976+
is_transparent = self._originalImage.transparency_get(int(original_x), int(original_y))
977+
self._tkPhotoImage.put("#{:02x}{:02x}{:02x}".format(rgb[0], rgb[1], rgb[2]), (new_x, new_y))
978+
self._tkPhotoImage.transparency_set(new_x, new_y, is_transparent)
979+
elif 0 <= int(original_x) < self._originalImage.width() and 0 <= int(original_y) < self._originalImage.height():
980+
rgb = self._originalImage.get(int(original_x),int(original_y))
981+
is_transparent = self._originalImage.transparency_get(int(original_x), int(original_y))
982+
self._tkPhotoImage.put("#{:02x}{:02x}{:02x}".format(rgb[0], rgb[1], rgb[2]), (new_x, new_y))
983+
self._tkPhotoImage.transparency_set(new_x, new_y, is_transparent)
984+
self._currentOrientation = orientation
985+
self._currentTilt = tilt
986+
self._screen._drawimage(self._item, position, self._tkPhotoImage)
987+
873988
def delete(self):
874-
pass
989+
self._screen._delete(self._item)
875990

876991
class Shape(object):
877992
"""Data structure modeling shapes.
878993
879-
attribute _type is one of "polygon", "image", "compound", "shape_drawer"
994+
attribute _type is one of "polygon", "image", "compound", "transformable_image"
880995
attribute _data is - depending on _type a poygon-tuple,
881-
an image, a list constructed using the addcomponent method or a subclass of
882-
BaseShapeDrawer.
996+
an image or a list constructed using the addcomponent method
883997
"""
884998
def __init__(self, type_, data=None):
885999
self._type = type_
@@ -888,8 +1002,8 @@ def __init__(self, type_, data=None):
8881002
data = tuple(data)
8891003
elif type_ == "image":
8901004
assert(isinstance(data, TK.PhotoImage))
891-
elif type_ == "shape_drawer":
892-
assert(issubclass(data, BaseShapeDrawer))
1005+
elif type_ == "transformable_image":
1006+
assert(isinstance(data, TK.PhotoImage))
8931007
elif type_ == "compound":
8941008
data = []
8951009
else:
@@ -1126,7 +1240,7 @@ def register_shape(self, name, shape=None):
11261240
of pairs of coordinates. Installs the corresponding
11271241
polygon shape
11281242
(4) name is an arbitrary string and shape is a
1129-
(compound or shape_drawer) Shape object. Installs the corresponding
1243+
(compound or transformable_image) Shape object. Installs the corresponding
11301244
shape.
11311245
To use a shape, you have to issue the command shape(shapename).
11321246
@@ -2574,7 +2688,7 @@ def _setshape(self, shapeIndex):
25742688
elif self._type == "compound":
25752689
for item in self._item:
25762690
screen._delete(item)
2577-
elif self._type == "shape_drawer":
2691+
elif self._type == "transformable_image":
25782692
del self._item
25792693

25802694
self._type = screen._shapes[shapeIndex]._type
@@ -2587,8 +2701,8 @@ def _setshape(self, shapeIndex):
25872701
elif self._type == "compound":
25882702
self._item = [screen._createpoly() for item in
25892703
screen._shapes[shapeIndex]._data]
2590-
elif self._type == "shape_drawer":
2591-
self._item = screen._shapes[self.shapeIndex]._data(screen)
2704+
elif self._type == "transformable_image":
2705+
self._item = TransformableImage(screen, screen._shapes[self.shapeIndex]._data)
25922706

25932707

25942708
class RawTurtle(TPen, TNavigator):
@@ -2876,8 +2990,8 @@ def clone(self):
28762990
elif ttype == "compound":
28772991
q.turtle._item = [screen._createpoly() for item in
28782992
screen._shapes[self.turtle.shapeIndex]._data]
2879-
elif ttype == "shape_drawer":
2880-
q.turtle._item = screen._shapes[self.turtle.shapeIndex]._data(screen)
2993+
elif ttype == "transformable_image":
2994+
q.turtle._item = TransformableImage(screen, screen._shapes[self.turtle.shapeIndex]._data)
28812995
q.currentLineItem = screen._createline()
28822996
q._update()
28832997
return q
@@ -3134,10 +3248,8 @@ def _drawturtle(self):
31343248
poly = self._polytrafo(self._getshapepoly(poly, True))
31353249
screen._drawpoly(item, poly, fill=self._cc(fc),
31363250
outline=self._cc(oc), width=self._outlinewidth, top=True)
3137-
elif ttype == "shape_drawer":
3138-
titem.draw(self._position, self._orient, pen_color = self._pencolor,
3139-
fill_color = self._fillcolor, transform = self._shapetrafo,
3140-
pen_size = self._pensize)
3251+
elif ttype == "transformable_image":
3252+
titem.draw(self._position, self._orient, self._tilt)
31413253
else:
31423254
if self._hidden_from_screen:
31433255
return
@@ -3194,11 +3306,9 @@ def stamp(self):
31943306
poly = self._polytrafo(self._getshapepoly(poly, True))
31953307
screen._drawpoly(item, poly, fill=self._cc(fc),
31963308
outline=self._cc(oc), width=self._outlinewidth, top=True)
3197-
elif ttype == "shape_drawer":
3309+
elif ttype == "transformable_image":
31983310
stitem = self.turtle._item.clone()
3199-
stitem.draw(self._position, self._orient, pen_color = self._pencolor,
3200-
fill_color = self._fillcolor, transform = self._shapetrafo,
3201-
pen_size = self._pensize)
3311+
stitem.draw(self._position, self._orient, self._tilt)
32023312
self.stampItems.append(stitem)
32033313
self.undobuffer.push(("stamp", stitem))
32043314
return stitem
@@ -3210,7 +3320,7 @@ def _clearstamp(self, stampid):
32103320
if isinstance(stampid, tuple):
32113321
for subitem in stampid:
32123322
self.screen._delete(subitem)
3213-
elif not isinstance(stampid, BaseShapeDrawer):
3323+
elif not isinstance(stampid, TransformableImage):
32143324
self.screen._delete(stampid)
32153325
self.stampItems.remove(stampid)
32163326

@@ -3224,7 +3334,7 @@ def _clearstamp(self, stampid):
32243334
if index <= buf.ptr:
32253335
buf.ptr = (buf.ptr - 1) % buf.bufsize
32263336
buf.buffer.insert((buf.ptr+1)%buf.bufsize, [None])
3227-
if isinstance(stampid, BaseShapeDrawer):
3337+
if isinstance(stampid, TransformableImage):
32283338
stampid.delete()
32293339

32303340
def clearstamp(self, stampid):

0 commit comments

Comments
 (0)