Source code

'''
Tree picture generation

Tests:
'''
import math
import random
import tkinter


def gencolours(x, min_col=0, max_col=255):
    ''' Colour generation - generate html colour strings using random numbers '''
    colours = []
    for _x in range(x):
        r = random.randint(min_col, max_col) 
        g = random.randint(min_col, max_col) 
        b = random.randint(min_col, max_col)
        colours.append(f'#{r:02x}{g:02x}{b:02x}')
    return colours


class TreeBranch:
    ''' Branch object of a tree. '''
    

    def __init__(self, length, angle, parent=None, level=1, fromtopofparent=0):
        # calculate position of branch from top of parent.. as parent 'grows' from the bottom, this is an invariant quantity
        self.fromtopofparent = fromtopofparent
        self.length = length
        self.angle = angle
        self.branches = []
        self.xcoords = []
        self.ycoords = []
        self.level = level
        self.parent = parent
        self.maxbranches = 2  # maximum number of sub-branches that can come off one branch per season
        self.lengthinpix = 15  # conversion of lengths to pixels
        self.maxnewbranchlength = 1  # max new branch size
        self.minnewbranchlength = 1  # min new branch size
        self.maximumangle = 60  # maximum angle the tree branch can droop 
        

    def lengthen(self):
        ''' Increase the length of all existing branches each growing season. '''
        self.length = self.length + 1
        for branch in self.branches:
            branch.lengthen()


    def growtree(self):
        ''' Grow the tree. Add branches. '''
        for branch in self.branches:
           branch.growtree()    
           branch.addbranches()
        if self.level == 1:
            self.addbranches()
        
        
    def addbranches(self):
        ''' Add branches to the tree. '''
        numbranches = random.randint(0, self.maxbranches)
        while numbranches > 0:
            new_tree_branch = TreeBranch(random.uniform(self.minnewbranchlength, self.maxnewbranchlength), 
                                         random.uniform(-self.maximumangle, self.maximumangle),
                                         parent=self, level=self.level+1, fromtopofparent=random.uniform(0, self.length) )
            self.branches.append(new_tree_branch)
            numbranches = numbranches - 1        
     
     
    def printtreexytocanvus(self, canvus, seasons):
        ''' Print the tree to the canvas. '''
        to_pix = self.lengthinpix
        if self.level == 1:
            self.xcoords = (float(canvus["width"]) / 2, 
                            float(canvus["width"]) / 2)
            self.ycoords = (float(canvus["height"]), 
                            float(canvus["height"]) - (self.length * to_pix))
        else: 
            p_length = self.parent.length
            p_x = self.parent.xcoords[0]
            p_y = self.parent.ycoords[0]
            pi_2_180 = (2 * math.pi / 180)
            self.xcoords = (p_x + to_pix * (p_length - self.fromtopofparent) * math.sin(self.parent.angle * pi_2_180), 
                            p_x + to_pix * (p_length - self.fromtopofparent) * math.sin(self.parent.angle * pi_2_180) + 
                                to_pix * self.length * math.sin(self.angle * pi_2_180))
            self.ycoords = (p_y - to_pix * (p_length - self.fromtopofparent) * math.cos(self.parent.angle * pi_2_180), 
                            p_y - to_pix * (p_length - self.fromtopofparent) * math.cos(self.parent.angle * pi_2_180) - 
                                to_pix * self.length * math.cos(self.angle * pi_2_180))
        canvus.create_line(self.xcoords[0], self.ycoords[0], self.xcoords[1], self.ycoords[1], 
                           fill=canvus.master.colours[self.level], width=(seasons - self.level + 1) / 2)
        for branch in self.branches:
            branch.printtreexytocanvus(canvus, seasons)


class TreeApplication(tkinter.Frame):  
    ''' Tkinter application for showing the tree. '''

    def __init__(self, master=None, width=800, height=800, maxseasons=10, newtreelength=3):
        super().__init__(master, width=width, height=height)
        
        self.colours = []
        self.maxseasons = maxseasons
        self.newtreelength = newtreelength  # size of the stump
        self.grid()
        self.quitButton = tkinter.Button(self, text='Quit', command=self.master.destroy)
        self.quitButton.grid(row=0, column=0)
        self.clearButton = tkinter.Button(self, text='Clear', command=self.clearlines)
        self.clearButton.grid(row=1, column=0)
        self.genButton = tkinter.Button(self, text='Generate', command=self.gentree)
        self.genButton.grid(row=2, column=0)
        screenbackground = '#FFFFFF'  # For dark mode, try, '#000066'
        self.canv = tkinter.Canvas(self, width=width, height=height, background=screenbackground)
        self.gentree()
        self.canv.grid(column=1, row=0, rowspan=20)

    def clearlines(self):
        ''' Clear all lines on the canvas. '''
        objlist = self.canv.find_all()
        for obj in objlist:
            self.canv.delete(obj)

    def gentree(self):
        ''' Generate the tree '''
        self.clearlines()
        newtree = TreeBranch(self.newtreelength, 0)
        newtree.addbranches()
        # min_col=10 here stops the branch colour clashing with the canvas
        # If in dark mode, flip to max_col=245
        self.colours = gencolours(self.maxseasons + 5, min_col=10)  
        for _season in range(self.maxseasons):
            newtree.lengthen()
            newtree.growtree()
        newtree.printtreexytocanvus(self.canv, self.maxseasons)
             
if __name__ == "__main__":
    # run as python3 tree.py
    root = tkinter.Tk()
    app = TreeApplication(root)
    app.master.title("Tree generation")
    root.mainloop()