@@ -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
876991class 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
25942708class 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