@@ -112,8 +112,7 @@ def position_tip(self, tip, at_start=False):
112112
113113 def reset_endpoints_based_on_tip (self , tip , at_start ):
114114 if self .get_length () == 0 :
115- # Zero length, put_start_and_end_on wouldn't
116- # work
115+ # Zero length, put_start_and_end_on wouldn't work
117116 return self
118117
119118 if at_start :
@@ -295,8 +294,8 @@ def __init__(self, start, end, angle=TAU / 4, radius=None, **kwargs):
295294 sign = 2
296295 halfdist = np .linalg .norm (np .array (start ) - np .array (end )) / 2
297296 if radius < halfdist :
298- raise ValueError (""" ArcBetweenPoints called with a radius that is
299- smaller than half the distance between the points."" " )
297+ raise ValueError ("ArcBetweenPoints called with a radius that is "
298+ " smaller than half the distance between the points." )
300299 arc_height = radius - math .sqrt (radius ** 2 - halfdist ** 2 )
301300 angle = math .acos ((radius - arc_height ) / radius )* sign
302301
@@ -804,6 +803,45 @@ def __init__(self, n=6, **kwargs):
804803 Polygon .__init__ (self , * vertices , ** kwargs )
805804
806805
806+ class ArcPolygon (VMobject ):
807+ """
808+ The ArcPolygon is what it says, a polygon, but made from arcs.
809+ More versatile than the standard Polygon.
810+ Accepts both Arc and ArcBetweenPoints and is instantiated like this:
811+ ArcPolygon(arc0,arc1,arc2,arcN,**kwargs)
812+ For proper appearance the arcs should be seamlessly connected:
813+ [a,b][b,c][c,a]
814+ If they don't, the gaps will be filled in with straight lines.
815+
816+ ArcPolygon also doubles as a group for the input arcs.
817+ That means these arcs can be drawn and seen separately.
818+ If only the ArcPolygon itself is supposed to be visible,
819+ make the arcs invisible (for example with "stroke_width": 0).
820+ ArcPolygon.arcs can be used get values from these subarcs.
821+ """
822+ def __init__ (self , * arcs , ** kwargs ):
823+ if not all ([isinstance (m , Arc ) or
824+ isinstance (m , ArcBetweenPoints ) for m in arcs ]):
825+ raise Exception ("All ArcPolygon submobjects must be of"
826+ "type Arc/ArcBetweenPoints" )
827+ VMobject .__init__ (self , ** kwargs )
828+ # Adding the arcs like this makes arcpolygon double as a group
829+ self .add (* arcs )
830+ # This adding is also needed for this line and ArcPolygon.arcs
831+ # to return the arcs as their values currently are
832+ self .arcs = [* arcs ]
833+ for arc1 , arc2 in adjacent_pairs (arcs ):
834+ self .append_points (arc1 .points )
835+ line = Line (arc1 .get_end (), arc2 .get_start ())
836+ len_ratio = line .get_length () / arc1 .get_arc_length ()
837+ if math .isnan (len_ratio ) or math .isinf (len_ratio ):
838+ continue
839+ line .insert_n_curves (
840+ int (arc1 .get_num_curves () * len_ratio )
841+ )
842+ self .append_points (line .get_points ())
843+
844+
807845class Triangle (RegularPolygon ):
808846 def __init__ (self , ** kwargs ):
809847 RegularPolygon .__init__ (self , n = 3 , ** kwargs )
@@ -876,3 +914,174 @@ class RoundedRectangle(Rectangle):
876914 def __init__ (self , ** kwargs ):
877915 Rectangle .__init__ (self , ** kwargs )
878916 self .round_corners (self .corner_radius )
917+
918+
919+ class Tiling (VMobject ):
920+ """
921+ The purpose of this class is to create tilings/tesselations.
922+ Tilings can also be seemlessly transformed into each other.
923+ This requires their ranges to be the same, tiles to be oriented and
924+ rotated the correct way, as well as having a vertex setup that
925+ allows proper transformations.
926+
927+ It's instantiated like this:
928+ Tiling(tile_prototype, xOffset, yOffset, xRange, yRange, **kwargs)
929+ The tile prototype can be any Mobject, a VGroup or a function.
930+ The function format is function(x,y), returning a Mobject/VGroup.
931+ Using groups or functions allows the tiling to contain multiple
932+ different tiles or to simplify the following offset functions.
933+
934+ Next are two nested lists that determine how the tiles are arranged.
935+ The functions are typically Mobject.shift and Mobject.rotate.
936+ Each list has to contain sublists with Function/Value pairs.
937+ Example for a simple shift along the X-Axis:
938+ [[Mobject.shift,[1,0,0]]]
939+
940+ Every move within the tiling applies a full sublist.
941+ Example for a shift with simultaneous rotation:
942+ [[Mobject.shift,[1,0,0],Mobject.rotate,np.pi]]
943+
944+ When multiple sublists are passed, they are applied alternating.
945+ Example for alternating shifting and rotating:
946+ [[Mobject.shift,[1,0,0]],[Mobject.rotate,np.pi]]
947+
948+ Last are two ranges: If both ranges are range(-1,1,1),
949+ that would result in a grid of 9 tiles.
950+
951+ Full example:
952+ Tiling(Square(),
953+ [[Mobject.shift,[2.1,0,0]]],
954+ [[Mobject.shift,[0,2.1,0]]],
955+ range(-1,1),
956+ range(-1,1))
957+
958+ A Tiling can be directly drawn like a VGroup.
959+ Tiling.tile_dictionary[x][y] can be used to access individual tiles,
960+ to color them for example.
961+ """
962+ def __init__ (self , tile_prototype , xOffset , yOffset , xRange , yRange , ** kwargs ):
963+ VMobject .__init__ (self , ** kwargs )
964+ # First we add one more to the range,
965+ # so that a -1,1 step 1 range also gives us 3 tiles,
966+ # [-1,0,1] as opposed to 2 [-1,0]
967+ self .xRange = range (xRange .start ,xRange .stop + xRange .step ,xRange .step )
968+ self .yRange = range (yRange .start ,yRange .stop + yRange .step ,yRange .step )
969+
970+ # We need the tiles array for a VGroup, which in turn we need
971+ # to draw the tiling and adjust it.
972+ # Trying to draw the tiling directly will not properly work.
973+ self .tile_dictionary = {}
974+ self .kwargs = kwargs
975+ for x in self .xRange :
976+ self .tile_dictionary [x ]= {}
977+ for y in self .yRange :
978+ if callable (tile_prototype ):
979+ tile = tile_prototype (x ,y ).deepcopy ()
980+ else :
981+ tile = tile_prototype .deepcopy ()
982+ self .transform_tile (x ,xOffset ,tile )
983+ self .transform_tile (y ,yOffset ,tile )
984+ self .add (tile )
985+ self .tile_dictionary [x ][y ]= tile
986+ # TODO: Once the config overhaul is far enough:
987+ # Implement a way to apply kwargs/some dict to all tiles
988+
989+ # This method computes and applies the offsets for the tiles.
990+ # Also multiplies inputs, which requires arrays to be numpy arrays.
991+ def transform_tile (self ,position ,offset ,tile ):
992+ # The number of different offsets the current axis has
993+ offsets_nr = len (offset )
994+ for i in range (offsets_nr ):
995+ for j in range (int (len (offset [i ])/ 2 )):
996+ if position < 0 :
997+ # Magnitude is calculated as the length of a range.
998+ # The range starts at 0, adjusting for the number
999+ # of different offset functions, stops at the target
1000+ # position, and uses the amount of different
1001+ # offset functions as the step.
1002+ magnitude = len (range (- i ,position ,- offsets_nr ))* - 1
1003+ offset [- 1 - i ][0 + j * 2 ](tile ,magnitude * np .array (offset [- 1 - i ][1 + j * 2 ]))
1004+ else :
1005+ magnitude = len (range (i ,position ,offsets_nr ))
1006+ offset [i ][0 + j * 2 ](tile ,magnitude * np .array (offset [i ][1 + j * 2 ]))
1007+
1008+
1009+ class Graph ():
1010+ """
1011+ This class is for visual representation of graphs for graph theory.
1012+ (Not graphs of functions. Same term but entirely different things.)
1013+
1014+ It's instantiated with a dictionary that represents the graph,
1015+ a configuration dictionary for vertex appearance, and one for edges.
1016+ The configuration dictionaries are optional.
1017+ Graph(graph_dictionary, vertex_config=vc, edge_config=ec)
1018+
1019+ The keys for the graph have to be of type int in ascending order,
1020+ with each number denoting a vertex.
1021+ The values are lists with 3 elements. A list of coordinates,
1022+ a list of connected vertices, and a configuration dictionary.
1023+ The coordinates determine the position of the vertex.
1024+ The list of connected vertices determines between which vertices
1025+ edges are. For clarity it's possible to have an edge defined in both
1026+ directions, but it'll be drawn once, from lower to higher number.
1027+ For example if vertex 2 is connected to vertex 0, that's ignored.
1028+ The config dictionary is used to initialize a Circle as the vertex.
1029+ (Or an Annulus if annulus=True is passed to this class.)
1030+ It will override values passed as vertex_config to the Graph.
1031+ Full example:
1032+ g = {0: [[0,0,0], [1, 2], {"color": BLUE}],
1033+ 1: [[1,0,0], [0, 2], {"color": GRAY}],
1034+ 2: [[0,1,0], [0, 1], {"color": PINK}]}
1035+ Graph(g,vertex_config={"radius": 0.2,"fill_opacity": 1},
1036+ edge_config={"stroke_width": 5,"color": RED})
1037+
1038+ An edge is instantiated as an ArcBetweenPoints, and optionally
1039+ individual config dictionaries can also be passed to them.
1040+ Example:
1041+ g = {0: [[0,0,0], [[1,{"angle": 2}], [2,{"color": WHITE}]]...
1042+
1043+ Use Graph.vertices/Graph.edges/Graph.annuli for drawing.
1044+ """
1045+ def __init__ (self , graph , vertex_config = {}, edge_config = {}, ** kwargs ):
1046+ if not all (isinstance (n ,int ) for n in graph .keys ()):
1047+ raise ValueError ("All keys for the Graph dictionary have to be of type int" )
1048+ if not all (all (isinstance (m ,int ) or isinstance (m ,list )
1049+ for m in n [1 ])
1050+ for n in graph .values ()):
1051+ raise ValueError ("Invalid Edge definition in Graph class. Use int or "
1052+ "[int,dict]." )
1053+
1054+ self .graph = graph
1055+ self .vertex_config = vertex_config
1056+ self .edge_config = edge_config
1057+ if kwargs .get ('annulus' , False ):
1058+ self .annulus = True
1059+ else :
1060+ self .annulus = False
1061+ self .make_graph ()
1062+
1063+ def make_graph (self ):
1064+ self .vertices = VGroup ()
1065+ self .edges = VGroup ()
1066+ self .annuli = VGroup ()
1067+ for vertex , attributes in self .graph .items ():
1068+ if self .annulus :
1069+ self .annuli .add (Annulus (** {** self .vertex_config ,
1070+ ** attributes [2 ]}).shift (attributes [0 ]))
1071+ else :
1072+ self .vertices .add (Circle (** {** self .vertex_config ,
1073+ ** attributes [2 ]}).shift (attributes [0 ]))
1074+ for edge_definition in attributes [1 ]:
1075+ if isinstance (edge_definition , int ):
1076+ vertex_number = edge_definition
1077+ edge_kwargs = {}
1078+ elif isinstance (edge_definition , list ):
1079+ vertex_number = edge_definition [0 ]
1080+ edge_kwargs = edge_definition [1 ]
1081+ if vertex < vertex_number :
1082+ edge = ArcBetweenPoints (attributes [0 ],
1083+ self .graph [vertex_number ][0 ],
1084+ ** {"angle" : 0 ,
1085+ ** self .edge_config ,
1086+ ** edge_kwargs })
1087+ self .edges .add (edge )
0 commit comments