def __init__(self, width, height, margin, scale, speed, loop): ''' scale: each cell in the maze will be a square of (scale x scale) pixels in the image. speed: control how often a frame is rendered. loop: the number of loops of the GIF image. delay: the delay between two successive frames. trans_index: which transparent color is used. colormap: a dict that maps the maze to an image. ''' Maze.__init__(self, width, height, margin) self.writer = GIFWriter(width * scale, height * scale, loop) self.scale = scale self.speed = speed self.trans_index = 3 self.delay = 2 self.colormap = {i: i for i in range(4)}
def __init__(self, maze, scale, offsets, depth, palette, loop, filename): """ INPUTS: - `maze`: an instance of the maze class below. - `scale`: each cell in the maze occupies scale*scale pixels in the image. - `offsets`: only useful if this canvas is embedded into another gif image. It's a tuple of 4 integers that specify the (left, right, top, bottom) offsets of this canvas relative to that image. - `filename`: the output file. - `depth`, `palette`, `loop`: the same as they are in the GIFWriter class. """ self.maze = maze self.scale = scale self.writer = GIFWriter(maze.width * scale + offsets[0] + offsets[1], maze.height * scale + offsets[2] + offsets[3], depth, palette, loop) self.offsets = offsets # use a dict to map the cells to the color indices. self.colormap = {i: i for i in range(1 << depth)} self.speed = 10 # output the frame once this number of cells are changed. self.trans_index = 3 # the index of the transparent color in the global color table. self.delay = 5 # delay between successive frames. self.target_file = open(filename, 'wb') self.target_file.write(self.writer.logical_screen_descriptor + self.writer.global_color_table + self.writer.loop_control)
def __init__(self, maze, scale=5, speed=30, loop=0): self.maze = maze self.writer = GIFWriter(maze.width * scale, maze.height * scale, loop) self.scale = scale self.speed = speed self.trans_index = 3 self.delay = 5 # this dict is used for communicating with our LZW encoder. # by modifying it we can color a maze in different ways. self.init_table = {str(c): c for c in range(4)}
def __init__(self, width, height, scale, min_bits, palette, loop): self.width = width self.height = height self.grid = [[0] * height for _ in range(width)] self.scale = scale self.num_changes = 0 # a counter holds how many cells are changed. self.frame_box = None # maintains the region that to be updated. self.writer = GIFWriter(width * scale, height * scale, min_bits, palette, loop) self.colormap = {i: i for i in range(1 << min_bits)} self.speed = 10 # output the frame once this number of cells are changed. self.trans_index = 3 # the index of the transparent color in the global color table. self.delay = 5 # delay between successive frames.
def __init__(self, width, height, margin, scale, speed, loop): ''' scale: size of a cell in pixels. speed: speed of the animation. loop: number of loops. ''' Maze.__init__(self, width, height, margin) self.writer = GIFWriter(width * scale, height * scale, loop) self.scale = scale self.speed = speed self.trans_index = 3 # this table is used for communicating with our LZW encoder. # by modifying it we can color a maze in different ways. self.init_table = {str(c): c for c in range(4)}
def encode_frame(self): if self.frame_box is not None: left, top, right, bottom = self.frame_box else: left, top, right, bottom = 0, 0, self.width - 1, self.height - 1 width = right - left + 1 height = bottom - top + 1 descriptor = GIFWriter.image_descriptor(left * self.scale, top * self.scale, width * self.scale, height * self.scale) def get_frame_pixels(): for i in range(width * height * self.scale * self.scale): y = i // (width * self.scale * self.scale) x = (i % (width * self.scale)) // self.scale val = self.grid[x + left][y + top] c = self.colormap[val] yield c frame = self.writer.LZW_encode(get_frame_pixels()) self.num_changes = 0 self.frame_box = None return descriptor + frame
def encode_frame(self, static=False): """ Encode current maze into one frame. If static is `True` then the graphics control block is not added (so this frame can be used as a static background image). """ # get the bounding box of this frame. if self.maze.frame_box is not None: left, top, right, bottom = self.maze.frame_box else: left, top, right, bottom = 0, 0, self.maze.width - 1, self.maze.height - 1 # then get the image descriptor of this frame. width = right - left + 1 height = bottom - top + 1 descriptor = GIFWriter.image_descriptor(left * self.scale, top * self.scale, width * self.scale, height * self.scale) # A generator that yields the pixels of this frame. This may look a bit unintuitive # because encoding frames will be called thousands of times in an animation # and I don't want to create and destroy a new list each time it's called. def get_frame_pixels(): for i in range(width * height * self.scale * self.scale): y = i // (width * self.scale * self.scale) x = (i % (width * self.scale)) // self.scale val = self.maze.grid[x + left][y + top] c = self.colormap[val] yield c # encode the frame data via the LZW compression. frame = self.writer.LZW_encode(get_frame_pixels()) # reset `num_changes` and `frame_box`. self.maze.num_changes = 0 self.maze.frame_box = None if static: return descriptor + frame else: control = GIFWriter.graphics_control_block(self.delay, self.trans_index) return control + descriptor + frame
def __init__(self, maze, scale, min_bits, palette, loop, filename): """ maze: an instance of the maze class below. scale: each cell in the maze occupies scale*scale pixels in the image. filename: the output file. min_bits, palette, loop: the same as they are in the GIFWriter class. """ self.maze = maze self.scale = scale self.writer = GIFWriter(maze.width * scale, maze.height * scale, min_bits, palette, loop) # use a dict to map the cells to the color indices. self.colormap = {i: i for i in range(1 << min_bits)} self.speed = 10 # output the frame once this number of cells are changed. self.trans_index = 3 # the index of the transparent color in the global color table. self.delay = 5 # delay between successive frames. self.target_file = open(filename, 'wb') self.target_file.write(self.writer.logical_screen_descriptor + self.writer.global_color_table + self.writer.loop_control)
def __init__(self, width, height, margin, scale, speed, loop): ''' scale: size of a cell in pixels. speed: speed of the animation. loop: number of loops. ''' Maze.__init__(self, width, height, margin) self.writer = GIFWriter(width * scale, height * scale, loop) self.scale = scale self.speed = speed self.trans_index = 3 # this table is used for communicating with our LZW encoder. # by modifying it we can color a maze in different ways. self.init_table = {str(c): c for c in range(4)}
class WilsonAnimation(Maze): ''' our animation contains basically two parts: run the algorithm, and write to the file. to write to the file, we will need several attributes: 1. delay: control the delay between frames. 2. trans_index: control which transparent color is used. 3. init_dict: map the maze into an image (to communicate with our LZW encoder) to run the animation, we will need these data structures: 1. self.tree: maintain the cells that have been added to the tree. 2. self.path: maintain the path of the loop erased random walk. ''' def __init__(self, width, height, margin, scale, speed, loop): ''' scale: size of a cell in pixels. speed: speed of the animation. loop: number of loops. ''' Maze.__init__(self, width, height, margin) self.writer = GIFWriter(width * scale, height * scale, loop) self.scale = scale self.speed = speed self.trans_index = 3 # this table is used for communicating with our LZW encoder. # by modifying it we can color a maze in different ways. self.init_table = {str(c): c for c in range(4)} def __call__(self, filename): # here we need to paint the blank background because the region that has not been # covered by any frame will be set to transparent by decoders. # comment this line and watch the result if you don't understand this. self.paint_background() # pad a two-seconds delay, get ready! self.pad_delay_frame(200) # in the wilson algorithm step no cells are 'filled', # hence it's safe to use color 3 as the transparent color. self.make_wison_animation(delay=2, trans_index=3, wall_color=0, tree_color=1, path_color=2) # pad a three-seconds delay to help to see the resulting maze clearly. self.pad_delay_frame(300) # fix a suitable speed for path finding animation. self.set_speed(10) # in the dfs algorithm step the walls are unchanged throughout, # hence it's safe to use color 0 as the transparent color. self.make_dfs_animation(delay=5, trans_index=0, wall_color=0, tree_color=0, path_color=2, fill_color=3) # pad a five-seconds delay to help to see the resulting path clearly. self.pad_delay_frame(500) # finally save the bits stream in 'wb' mode. self.write_to_gif(filename) def make_wison_animation(self, delay, trans_index, **kwargs): ''' animating the Wilson algorithm. ''' self.set_delay(delay) self.set_transparent(trans_index) self.set_colors(**kwargs) # initially the tree only contains the root. self.init_tree(self.start) # for each cell in the maze that is not in the tree yet, # start a loop erased random walk from this cell until the walk hits the tree. for cell in self.cells: if cell not in self.tree: self.loop_erased_random_walk(cell) # there may be some changes that has not been written to the file, write them. self.clear() def init_tree(self, root): self.tree = set([root]) self.mark_cell(root, TREE) def loop_erased_random_walk(self, cell): ''' start a loop erased random walk from this cell until it hits the tree. ''' self.begin_path(cell) current_cell = cell while current_cell not in self.tree: current_cell = self.move_one_step(current_cell) self.refresh_frame() # once the walk meets the tree, add the path to the tree. self.tree = self.tree.union(self.path) self.mark_path(self.path, TREE) def begin_path(self, cell): self.path = [cell] self.mark_cell(cell, PATH) def move_one_step(self, cell): ''' the most fundamental operation in wilson algorithm: choose a random neighbor z of current cell, and move to z. 1. if z already in current path, then a loop is found, erase this loop and start the walk from z again. 2. if z is not in current path, then add z to current path. repeat this procedure until z 'hits' the tree. ''' next_cell = random.choice(self.get_neighbors(cell)) # if next_cell is already in path, then we have found a loop in our path, erase it! if next_cell in self.path: self.erase_loop(next_cell) else: self.add_to_path(next_cell) return next_cell def erase_loop(self, cell): index = self.path.index(cell) # erase the loop self.mark_path(self.path[index:], WALL) # re-mark this cell self.mark_cell(self.path[index], PATH) self.path = self.path[:index + 1] def add_to_path(self, cell): self.mark_cell(cell, PATH) self.mark_wall(self.path[-1], cell, PATH) self.path.append(cell) def make_dfs_animation(self, delay, trans_index, **kwargs): ''' animating the depth first search algorithm. ''' self.set_delay(delay) self.set_transparent(trans_index) self.set_colors(**kwargs) # besides a stack to run the dfs, we need a dict to remember each step. from_to = dict() stack = [(self.start, self.start)] visited = set([self.start]) while stack: parent, child = stack.pop() from_to[parent] = child self.mark_cell(child, FILL) self.mark_wall(parent, child, FILL) if child == self.end: break else: for next_cell in self.get_neighbors(child): if (next_cell not in visited) and (not self.check_wall( child, next_cell)): stack.append((child, next_cell)) visited.add(next_cell) self.refresh_frame() self.clear() # retrieve the path path = [self.start] tmp = self.start while tmp != self.end: tmp = from_to[tmp] path.append(tmp) self.mark_path(path, PATH) # show the path self.refresh_frame() def set_transparent(self, index): self.trans_index = index def set_delay(self, delay): self.delay = delay def set_speed(self, speed): self.speed = speed def set_colors(self, **kwargs): colormap = { 'wall_color': '0', 'tree_color': '1', 'path_color': '2', 'fill_color': '3' } for key, val in kwargs.items(): self.init_table[colormap[key]] = val def pad_delay_frame(self, delay): self.writer.data += self.writer.pad_delay_frame( delay, self.trans_index) def refresh_frame(self): if self.num_changes >= self.speed: self.write_current_frame() def clear(self): ''' if there are remaining changes that has not been rendered, output them. ''' if self.num_changes > 0: self.write_current_frame() def write_current_frame(self): control = self.writer.graphics_control_block(self.delay, self.trans_index) self.writer.data += control + self.encode_frame() def paint_background(self, **kwargs): ''' if no colors are specified then previous self.init_table will be used. this function allows you to insert current frame at the beginning of the file to serve as the background, it does not need the graphics control block. ''' if kwargs: self.set_colors(**kwargs) else: self.writer.data = self.encode_frame() + self.writer.data def encode_frame(self): ''' encode the frame, but not write the result to the stream. ''' if self.frame_box: left, top, right, bottom = self.frame_box else: left, top, right, bottom = 0, 0, self.width - 1, self.height - 1 width = right - left + 1 height = bottom - top + 1 descriptor = self.writer.image_descriptor(left * self.scale, top * self.scale, width * self.scale, height * self.scale) input_data = [0] * width * height * self.scale * self.scale for i in range(len(input_data)): y = i // (width * self.scale * self.scale) x = (i % (width * self.scale)) // self.scale input_data[i] = self.grid[x + left][y + top] self.num_changes = 0 self.frame_box = None return descriptor + self.writer.LZW_encode(input_data, self.init_table) def write_to_gif(self, filename): self.writer.save(filename)
class WilsonAnimation(Maze): ''' our animation contains basically two parts: run the algorithm, and write to the file. to write to the file, we will need several attributes: 1. delay: control the delay between frames. 2. trans_index: control which transparent color is used. 3. init_dict: map the maze into an image (to communicate with our LZW encoder) to run the animation, we will need these data structures: 1. self.tree: maintain the cells that have been added to the tree. 2. self.path: maintain the path of the loop erased random walk. ''' def __init__(self, width, height, margin, scale, speed, loop): ''' scale: size of a cell in pixels. speed: speed of the animation. loop: number of loops. ''' Maze.__init__(self, width, height, margin) self.writer = GIFWriter(width * scale, height * scale, loop) self.scale = scale self.speed = speed self.trans_index = 3 # this table is used for communicating with our LZW encoder. # by modifying it we can color a maze in different ways. self.init_table = {str(c): c for c in range(4)} def __call__(self, filename): # here we need to paint the blank background because the region that has not been # covered by any frame will be set to transparent by decoders. # comment this line and watch the result if you don't understand this. self.paint_background() # pad a two-seconds delay, get ready! self.pad_delay_frame(200) # in the wilson algorithm step no cells are 'filled', # hence it's safe to use color 3 as the transparent color. self.make_wison_animation(delay=2, trans_index=3, wall_color=0, tree_color=1, path_color=2) # pad a three-seconds delay to help to see the resulting maze clearly. self.pad_delay_frame(300) # fix a suitable speed for path finding animation. self.set_speed(10) # in the dfs algorithm step the walls are unchanged throughout, # hence it's safe to use color 0 as the transparent color. self.make_dfs_animation(delay=5, trans_index=0, wall_color=0, tree_color=0, path_color=2, fill_color=3) # pad a five-seconds delay to help to see the resulting path clearly. self.pad_delay_frame(500) # finally save the bits stream in 'wb' mode. self.write_to_gif(filename) def make_wison_animation(self, delay, trans_index, **kwargs): ''' animating the Wilson algorithm. ''' self.set_delay(delay) self.set_transparent(trans_index) self.set_colors(**kwargs) # initially the tree only contains the root. self.init_tree(self.start) # for each cell in the maze that is not in the tree yet, # start a loop erased random walk from this cell until the walk hits the tree. for cell in self.cells: if cell not in self.tree: self.loop_erased_random_walk(cell) # there may be some changes that has not been written to the file, write them. self.clear() def init_tree(self, root): self.tree = set([root]) self.mark_cell(root, TREE) def loop_erased_random_walk(self, cell): ''' start a loop erased random walk from this cell until it hits the tree. ''' self.begin_path(cell) current_cell = cell while current_cell not in self.tree: current_cell = self.move_one_step(current_cell) self.refresh_frame() # once the walk meets the tree, add the path to the tree. self.tree = self.tree.union(self.path) self.mark_path(self.path, TREE) def begin_path(self, cell): self.path = [cell] self.mark_cell(cell, PATH) def move_one_step(self, cell): ''' the most fundamental operation in wilson algorithm: choose a random neighbor z of current cell, and move to z. 1. if z already in current path, then a loop is found, erase this loop and start the walk from z again. 2. if z is not in current path, then add z to current path. repeat this procedure until z 'hits' the tree. ''' next_cell = random.choice(self.get_neighbors(cell)) # if next_cell is already in path, then we have found a loop in our path, erase it! if next_cell in self.path: self.erase_loop(next_cell) else: self.add_to_path(next_cell) return next_cell def erase_loop(self, cell): index = self.path.index(cell) # erase the loop self.mark_path(self.path[index:], WALL) # re-mark this cell self.mark_cell(self.path[index], PATH) self.path = self.path[:index+1] def add_to_path(self, cell): self.mark_cell(cell, PATH) self.mark_wall(self.path[-1], cell, PATH) self.path.append(cell) def make_dfs_animation(self, delay, trans_index, **kwargs): ''' animating the depth first search algorithm. ''' self.set_delay(delay) self.set_transparent(trans_index) self.set_colors(**kwargs) # besides a stack to run the dfs, we need a dict to remember each step. from_to = dict() stack = [(self.start, self.start)] visited = set([self.start]) while stack: parent, child = stack.pop() from_to[parent] = child self.mark_cell(child, FILL) self.mark_wall(parent, child, FILL) if child == self.end: break else: for next_cell in self.get_neighbors(child): if (next_cell not in visited) and (not self.check_wall(child, next_cell)): stack.append((child, next_cell)) visited.add(next_cell) self.refresh_frame() self.clear() # retrieve the path path = [self.start] tmp = self.start while tmp != self.end: tmp = from_to[tmp] path.append(tmp) self.mark_path(path, PATH) # show the path self.refresh_frame() def set_transparent(self, index): self.trans_index = index def set_delay(self, delay): self.delay = delay def set_speed(self, speed): self.speed = speed def set_colors(self, **kwargs): colormap = {'wall_color': '0', 'tree_color': '1', 'path_color': '2', 'fill_color': '3'} for key, val in kwargs.items(): self.init_table[colormap[key]] = val def pad_delay_frame(self, delay): self.writer.data += self.writer.pad_delay_frame(delay, self.trans_index) def refresh_frame(self): if self.num_changes >= self.speed: self.write_current_frame() def clear(self): ''' if there are remaining changes that has not been rendered, output them. ''' if self.num_changes > 0: self.write_current_frame() def write_current_frame(self): control = self.writer.graphics_control_block(self.delay, self.trans_index) self.writer.data += control + self.encode_frame() def paint_background(self, **kwargs): ''' if no colors are specified then previous self.init_table will be used. this function allows you to insert current frame at the beginning of the file to serve as the background, it does not need the graphics control block. ''' if kwargs: self.set_colors(**kwargs) else: self.writer.data = self.encode_frame() + self.writer.data def encode_frame(self): ''' encode the frame, but not write the result to the stream. ''' if self.frame_box: left, top, right, bottom = self.frame_box else: left, top, right, bottom = 0, 0, self.width - 1, self.height - 1 width = right - left + 1 height = bottom - top + 1 descriptor = self.writer.image_descriptor(left * self.scale, top * self.scale, width * self.scale, height * self.scale) input_data = [0] * width * height * self.scale * self.scale for i in range(len(input_data)): y = i // (width * self.scale * self.scale) x = (i % (width * self.scale)) // self.scale input_data[i] = self.grid[x + left][y + top] self.num_changes = 0 self.frame_box = None return descriptor + self.writer.LZW_encode(input_data, self.init_table) def write_to_gif(self, filename): self.writer.save(filename)
class Canvas(object): """ A canvas is built on top of a maze for encoding it into frames. The core part is the `encode_frame` method below. Other methods are either wrappers of this function or simply for setting the parameters of the animation. """ def __init__(self, maze, scale, min_bits, palette, loop, filename): """ INPUTS: - `maze`: an instance of the maze class below. - `scale`: each cell in the maze occupies scale*scale pixels in the image. - `filename`: the output file. - `min_bits`, `palette`, `loop`: the same as they are in the GIFWriter class. """ self.maze = maze self.scale = scale self.writer = GIFWriter(maze.width * scale, maze.height * scale, min_bits, palette, loop) # use a dict to map the cells to the color indices. self.colormap = {i: i for i in range(1 << min_bits)} self.speed = 10 # output the frame once this number of cells are changed. self.trans_index = 3 # the index of the transparent color in the global color table. self.delay = 5 # delay between successive frames. self.target_file = open(filename, 'wb') self.target_file.write(self.writer.logical_screen_descriptor + self.writer.global_color_table + self.writer.loop_control) def encode_frame(self, static=False): """ Encode current maze into one frame. If static is `True` then the graphics control block is not added (so this frame can be used as a static background image). """ # get the bounding box of this frame. if self.maze.frame_box is not None: left, top, right, bottom = self.maze.frame_box else: left, top, right, bottom = 0, 0, self.maze.width - 1, self.maze.height - 1 # then get the image descriptor of this frame. width = right - left + 1 height = bottom - top + 1 descriptor = GIFWriter.image_descriptor(left * self.scale, top * self.scale, width * self.scale, height * self.scale) # A generator that yields the pixels of this frame. This may look a bit unintuitive # because encoding frames will be called thousands of times in an animation # and I don't want to create and destroy a new list each time it's called. def get_frame_pixels(): for i in range(width * height * self.scale * self.scale): y = i // (width * self.scale * self.scale) x = (i % (width * self.scale)) // self.scale val = self.maze.grid[x + left][y + top] c = self.colormap[val] yield c # encode the frame data via the LZW compression. frame = self.writer.LZW_encode(get_frame_pixels()) # reset `num_changes` and `frame_box`. self.maze.num_changes = 0 self.maze.frame_box = None if static: return descriptor + frame else: control = GIFWriter.graphics_control_block(self.delay, self.trans_index) return control + descriptor + frame def paint_background(self, **kwargs): """ Insert current frame at the beginning to use it as the background. This does not require the graphics control block. """ if kwargs: self.set_colors(**kwargs) self.target_file.write(self.encode_frame(static=True)) def refresh_frame(self): """Update a frame in the animation and write it into the file.""" if self.maze.num_changes >= self.speed: self.target_file.write(self.encode_frame(static=False)) def clear_remaining_changes(self): """May be there are some remaining changes when the animation is finished.""" if self.maze.num_changes > 0: self.target_file.write(self.encode_frame(static=False)) def set_colors(self, **kwargs): color_dict = {'wall_color': 0, 'tree_color': 1, 'path_color': 2, 'fill_color': 3} for key, val in kwargs.items(): self.colormap[color_dict[key]] = val def pad_delay_frame(self, delay): self.target_file.write(self.writer.pad_delay_frame(delay, self.trans_index)) def set_control_params(self, speed=30, delay=3, trans_index=5, **kwargs): self.speed = speed self.delay = delay self.trans_index = trans_index self.set_colors(**kwargs) def save(self): self.target_file.write(bytearray([0x3B])) self.target_file.close()
class WilsonAlgoAnimation(Maze): ''' Our animation contains basically two parts: run the algorithms, and write to the GIF file. ''' def __init__(self, width, height, margin, scale, speed, loop): ''' scale: each cell in the maze will be a square of (scale x scale) pixels in the image. speed: control how often a frame is rendered. loop: the number of loops of the GIF image. delay: the delay between two successive frames. trans_index: which transparent color is used. colormap: a dict that maps the maze to an image. ''' Maze.__init__(self, width, height, margin) self.writer = GIFWriter(width * scale, height * scale, loop) self.scale = scale self.speed = speed self.trans_index = 3 self.delay = 2 self.colormap = {i: i for i in range(4)} def run_wilson_algorithm(self, delay, trans_index, **kwargs): ''' Animating Wilson's uniform spanning tree algorithm. ''' self.set_delay(delay) self.set_transparent(trans_index) self.set_colors(**kwargs) # initially the tree only contains the root. self.mark_cell(self.start, TREE) self.tree = set([self.start]) # for each cell that is not in the tree, # start a loop erased random walk from this cell until the walk hits the tree. for cell in self.cells: if cell not in self.tree: self.loop_erased_random_walk(cell) self.clear_remaining_changes() def loop_erased_random_walk(self, cell): ''' Start a loop erased random walk. ''' self.path = [cell] self.mark_cell(cell, PATH) current_cell = cell while current_cell not in self.tree: current_cell = self.move_one_step(current_cell) self.refresh_frame() # once the walk meets the tree, add the path to the tree. self.mark_path(self.path, TREE) self.tree = self.tree.union(self.path) def move_one_step(self, cell): ''' The most fundamental step in Wilson's algorithm: 1. choose a random neighbor z of current cell and move to z. 2. (i) if z is already in current path then a loop is found, erase this loop and continue the walk from z. (ii) if z is not in current path then append it to current path. in both cases current cell is updated to be z. 3. repeat this procedure until z 'hits' the tree. ''' next_cell = random.choice(self.get_neighbors(cell)) if next_cell in self.path: self.erase_loop(next_cell) else: self.add_to_path(next_cell) return next_cell def erase_loop(self, cell): index = self.path.index(cell) # erase the loop self.mark_path(self.path[index:], WALL) # re-mark this cell self.mark_cell(self.path[index], PATH) self.path = self.path[:index+1] def add_to_path(self, cell): self.mark_cell(cell, PATH) self.mark_wall(self.path[-1], cell, PATH) self.path.append(cell) def run_dfs_algorithm(self, delay, trans_index, **kwargs): ''' Animating the depth first search algorithm. ''' self.set_delay(delay) self.set_transparent(trans_index) self.set_colors(**kwargs) # we use a dict to remember each step. from_to = dict() stack = [(self.start, self.start)] visited = set([self.start]) while stack: parent, child = stack.pop() from_to[parent] = child self.mark_cell(child, FILL) self.mark_wall(parent, child, FILL) if child == self.end: break else: for next_cell in self.get_neighbors(child): if (next_cell not in visited) and (not self.check_wall(child, next_cell)): stack.append((child, next_cell)) visited.add(next_cell) self.refresh_frame() self.clear_remaining_changes() # retrieve the path path = [self.start] tmp = self.start while tmp != self.end: tmp = from_to[tmp] path.append(tmp) self.mark_path(path, PATH) # show the path self.refresh_frame() def set_transparent(self, index): self.trans_index = index def set_delay(self, delay): self.delay = delay def set_speed(self, speed): self.speed = speed def set_colors(self, **kwargs): cell_index = {'wall_color': 0, 'tree_color': 1, 'path_color': 2, 'fill_color': 3} for key, val in kwargs.items(): self.colormap[cell_index[key]] = val def pad_delay_frame(self, delay): self.writer.data += self.writer.pad_delay_frame(delay, self.trans_index) def encode_frame(self): ''' Encode current maze into a frame of the GIF file. ''' if self.frame_box: left, top, right, bottom = self.frame_box else: left, top, right, bottom = 0, 0, self.width - 1, self.height - 1 width = right - left + 1 height = bottom - top + 1 descriptor = self.writer.image_descriptor(left * self.scale, top * self.scale, width * self.scale, height * self.scale) # flatten the pixels of the region into a 1D list. input_data = [0] * width * height * self.scale * self.scale for i in range(len(input_data)): y = i // (width * self.scale * self.scale) x = (i % (width * self.scale)) // self.scale value = self.grid[x + left][y + top] # map the value of the cell to the color index. input_data[i] = self.colormap[value] # and don't forget to reset frame_box and num_changes. self.num_changes = 0 self.frame_box = None return descriptor + self.writer.LZW_encode(input_data) def write_current_frame(self): control = self.writer.graphics_control_block(self.delay, self.trans_index) self.writer.data += control + self.encode_frame() def refresh_frame(self): if self.num_changes >= self.speed: self.write_current_frame() def clear_remaining_changes(self): ''' Output (possibly) remaining changes. ''' if self.num_changes > 0: self.write_current_frame() def paint_background(self, **kwargs): ''' If no colors are specified then previous init_table will be used. This function allows you to insert current frame at the beginning of the file to serve as the background, it does not need the graphics control block. ''' if kwargs: self.set_colors(**kwargs) self.writer.data = self.encode_frame() + self.writer.data def write_to_gif(self, filename): self.writer.save(filename)
class Canvas(object): def __init__(self, width, height, scale, min_bits, palette, loop): self.width = width self.height = height self.grid = [[0] * height for _ in range(width)] self.scale = scale self.num_changes = 0 # a counter holds how many cells are changed. self.frame_box = None # maintains the region that to be updated. self.writer = GIFWriter(width * scale, height * scale, min_bits, palette, loop) self.colormap = {i: i for i in range(1 << min_bits)} self.speed = 10 # output the frame once this number of cells are changed. self.trans_index = 3 # the index of the transparent color in the global color table. self.delay = 5 # delay between successive frames. def mark_cell(self, cell, index): """Mark a cell and update `frame_box` and `num_changes`.""" x, y = cell self.grid[x][y] = index if self.frame_box is not None: left, top, right, bottom = self.frame_box self.frame_box = (min(x, left), min(y, top), max(x, right), max(y, bottom)) else: self.frame_box = (x, y, x, y) self.num_changes += 1 def encode_frame(self): if self.frame_box is not None: left, top, right, bottom = self.frame_box else: left, top, right, bottom = 0, 0, self.width - 1, self.height - 1 width = right - left + 1 height = bottom - top + 1 descriptor = GIFWriter.image_descriptor(left * self.scale, top * self.scale, width * self.scale, height * self.scale) def get_frame_pixels(): for i in range(width * height * self.scale * self.scale): y = i // (width * self.scale * self.scale) x = (i % (width * self.scale)) // self.scale val = self.grid[x + left][y + top] c = self.colormap[val] yield c frame = self.writer.LZW_encode(get_frame_pixels()) self.num_changes = 0 self.frame_box = None return descriptor + frame def paint_background(self, **kwargs): """Insert current frame at the beginning to use it as the background. This does not require the graphics control block.""" if kwargs: self.set_colors(**kwargs) self.writer.data = self.encode_frame() + self.writer.data def output_frame(self): """Output current frame to the data stream. This method will not be directly called: it's called by `refresh_frame()` and `clear_remaining_changes()`.""" control = self.writer.graphics_control_block(self.delay, self.trans_index) self.writer.data += control + self.encode_frame() def refresh_frame(self): if self.num_changes >= self.speed: self.output_frame() def clear_remaining_changes(self): if self.num_changes > 0: self.output_frame() def set_colors(self, **kwargs): """`wc` is short for wall color, `tc` is short for tree color, etc.""" color_dict = {'wc': 0, 'tc': 1, 'pc': 2, 'fc': 3} for key, val in kwargs.items(): self.colormap[color_dict[key]] = val def pad_delay_frame(self, delay): self.writer.data += self.writer.pad_delay_frame( delay, self.trans_index) def write_to_gif(self, filename): self.writer.save_gif(filename)
class Animation(object): ''' This class is built on top of the 'Maze' class for encodig the algorithms into GIF images. It needs several further parameters to control the resulting image: 1. scale: the size of the GIF image is (scale) x (size of the maze). 2. loop: the number of loops of the GIF image. 3. delay: the delay between two successive frames. 4. trans_index: control which transparent color is used. 5. init_dict: map the maze to an image (to communicate with our LZW encoder). 6. speed: control how often a frame is rendered. ''' def __init__(self, maze, scale=5, speed=30, loop=0): self.maze = maze self.writer = GIFWriter(maze.width * scale, maze.height * scale, loop) self.scale = scale self.speed = speed self.trans_index = 3 self.delay = 5 # this dict is used for communicating with our LZW encoder. # by modifying it we can color a maze in different ways. self.init_table = {str(c): c for c in range(4)} def set_transparent(self, index): self.trans_index = index def set_delay(self, delay): self.delay = delay def set_speed(self, speed): self.speed = speed def set_colors(self, **kwargs): colormap = { 'wall_color': '0', 'tree_color': '1', 'path_color': '2', 'fill_color': '3' } for key, val in kwargs.items(): self.init_table[colormap[key]] = val def pad_delay_frame(self, delay): self.writer.data += self.writer.pad_delay_frame( delay, self.trans_index) def refresh_frame(self): if self.maze.num_changes >= self.speed: self.write_current_frame() def clear(self): '''output (possibly) remaining changes''' if self.maze.num_changes > 0: self.write_current_frame() def write_current_frame(self): control = self.writer.graphics_control_block(self.delay, self.trans_index) self.writer.data += control + self.encode_frame() def paint_background(self, **kwargs): ''' If no colors are specified then previous init_table will be used. This function allows you to insert current frame at the beginning of the file to serve as the background, it does not need the graphics control block. ''' if kwargs: self.set_colors(**kwargs) self.writer.data = self.encode_frame() + self.writer.data def encode_frame(self): if self.maze.frame_box: left, top, right, bottom = self.maze.frame_box else: left, top, right, bottom = 0, 0, self.maze.width - 1, self.maze.height - 1 width = right - left + 1 height = bottom - top + 1 descriptor = self.writer.image_descriptor(left * self.scale, top * self.scale, width * self.scale, height * self.scale) input_data = [0] * width * height * self.scale * self.scale for i in range(len(input_data)): y = i // (width * self.scale * self.scale) x = (i % (width * self.scale)) // self.scale input_data[i] = self.maze.grid[x + left][y + top] self.maze.num_changes = 0 self.maze.frame_box = None return descriptor + self.writer.LZW_encode(input_data, self.init_table) def write_to_gif(self, filename): self.writer.save(filename)