Continuous Clock with Python Turtle (Source Code)

In a previous project you animated a clock. Improve the clock by making all hands move continuously.

Source Code:

import turtle
import datetime
import math

screen = turtle.Screen()
screen.title('Continuous Clock - PythonTurtle.Academy')
screen.bgcolor('sky blue')
screen.setup(1000,1000)
screen.setworldcoordinates(-1000,-1000,1000,1000)
screen.tracer(0,0)


class clock:
    def __init__(self,hour,minute,second):
        self.hour, self.minute, self.second = hour, minute, second
        self.microsecond = 0
        self.face = turtle.Turtle()
        self.hand = turtle.Turtle()
        self.face.hideturtle()
        self.hand.hideturtle()

    def draw(self):
        self.draw_face()
        self.draw_hand()
        
    def draw_face(self):
        self.face.clear()
        self.face.up()
        self.face.goto(0,-700)
        self.face.pensize(4)
        self.face.down()
        self.face.fillcolor('white')
        self.face.begin_fill()
        self.face.circle(700,steps=100)
        self.face.end_fill()
        self.face.up()
        self.face.goto(0,0)
        self.face.dot(10)
        self.face.pensize(2)
        for angle in range(0,360,6):
            self.face.up()
            self.face.goto(0,0)
            self.face.seth(90-angle)
            self.face.fd(620)
            self.face.down()
            self.face.fd(30)
            
        self.face.pensize(3)
        for angle in range(0,360,30):
            self.face.up()
            self.face.goto(0,0)
            self.face.seth(90-angle)
            self.face.fd(600)
            self.face.down()
            self.face.fd(50)
            
        self.face.pensize(4)
        for angle in range(0,360,90):
            self.face.up()
            self.face.goto(0,0)
            self.face.seth(90-angle)
            self.face.fd(580)
            self.face.down()
            self.face.fd(70)
        
    def draw_hand(self):    
        self.hand.clear()       
        self.hand.up()
        self.hand.goto(0,0)
        self.hand.seth(90-math.floor(((self.hour%12)*60*60*1000000+self.minute*60*1000000+self.second*1000000+self.microsecond)/3600000000*30))
        self.hand.down()
        self.hand.color('black')
        self.hand.pensize(6)
        self.hand.fd(300)

        self.hand.up()
        self.hand.goto(0,0)
        self.hand.seth(90-math.floor((self.minute*60*1000000+self.second*1000000+self.microsecond)/60000000*6))
        self.hand.down()
        self.hand.color('black')
        self.hand.pensize(4)
        self.hand.fd(400)

        self.hand.up()
        self.hand.color('red')
        self.hand.goto(0,0)
        self.hand.dot(5)
        self.hand.seth(90-(self.second*1000000+self.microsecond)/1000000*6)
        self.hand.down()
        self.hand.pensize(2)
        self.hand.fd(570)

def animate():
    global c
    d = datetime.datetime.now()
    c.hour, c.minute, c.second, c.microsecond = d.hour, d.minute, d.second, d.microsecond
    c.draw_hand()
    screen.update()
    screen.ontimer(animate,100)
    
d = datetime.datetime.now()
c = clock(d.hour,d.minute,d.second)
c.draw_face()
screen.update()
animate()

Clock with Python Turtle (Source Code)

Define a ‘clock’ class with Python and use the datetime library to draw an animated clock shown.

Source Code:

import turtle
import datetime
screen = turtle.Screen()
screen.title('Clock - PythonTurtle.Academy')
screen.setup(1000,1000)
screen.setworldcoordinates(-1000,-1000,1000,1000)
screen.tracer(0,0)
screen.bgcolor('sky blue')

class clock:
    def __init__(self,hour,minute,second):
        self.hour, self.minute, self.second = hour, minute, second
        self.face = turtle.Turtle()
        self.hand = turtle.Turtle()
        self.face.hideturtle()
        self.hand.hideturtle()

    def draw(self):
        self.draw_face()
        self.draw_hand()
        
    def draw_face(self):
        self.face.clear()
        self.face.up()
        self.face.goto(0,-700)
        self.face.pensize(5)
        self.face.down()
        self.face.fillcolor('white')
        self.face.begin_fill()
        self.face.circle(700)
        self.face.end_fill()
        self.face.up()
        self.face.goto(0,0)
        self.face.dot(10)
        self.face.pensize(2)
        for angle in range(0,360,6):
            self.face.up()
            self.face.goto(0,0)
            self.face.seth(90-angle)
            self.face.fd(620)
            self.face.down()
            self.face.fd(30)
        self.face.pensize(4)
        for angle in range(0,360,30):
            self.face.up()
            self.face.goto(0,0)
            self.face.seth(90-angle)
            self.face.fd(600)
            self.face.down()
            self.face.fd(50)
        
    def draw_hand(self):    
        self.hand.clear()       
        self.hand.up()
        self.hand.goto(0,0)
        self.hand.seth(90-self.hour%12*360//12)
        self.hand.down()
        self.hand.color('black')
        self.hand.pensize(6)
        self.hand.fd(300)

        self.hand.up()
        self.hand.goto(0,0)
        self.hand.seth(90-self.minute*6)
        self.hand.down()
        self.hand.color('black')
        self.hand.pensize(4)
        self.hand.fd(400)

        self.hand.up()
        self.hand.color('red')
        self.hand.goto(0,0)
        self.hand.dot(5)
        self.hand.seth(90-self.second*6)
        self.hand.down()
        self.hand.pensize(2)
        self.hand.fd(600)

def animate():
    global c
    d = datetime.datetime.now()
    c.hour, c.minute, c.second = d.hour, d.minute, d.second
    c.draw_hand()
    screen.update()
    screen.ontimer(animate,1000)
    
d = datetime.datetime.now()
c = clock(d.hour,d.minute,d.second)
c.draw_face()
screen.update()
animate()

Game of Snake with Python Turtle

Develop a Game of Snake with Python Turtle with multiple difficulty levels.

You may need to use features or libraries: List, Random, Keyboard Event, Timer Event, Colorsys.

Game of Snake with Python Turtle

Tutorial: Conway’s Game of Life with Python Turtle

In this tutorial, we are you going show how to implement Conway’s Game of Life simulation with Python Turtle.

Conway’s Game of Life with Python Turtle

Step 1. Drawing The Grid

This step should be straightforward. We just need n horizontal and n vertical lines centered in the screen. The following is the code snippet that draws the grid.

screen=turtle.Screen()
turtle.setup(1000,1000)
turtle.title("Conway's Game of Life - PythonTurtle.Academy")
turtle.hideturtle()
turtle.speed(0)
turtle.tracer(0,0)

n = 50 # nxn grid
def draw_line(x1,y1,x2,y2): # this function draw a line between x1,y1 and x2,y2
    turtle.up()
    turtle.goto(x1,y1)
    turtle.down()
    turtle.goto(x2,y2)
    
def draw_grid(): # this function draws nxn grid
    turtle.pencolor('gray')
    turtle.pensize(3)
    x = -400
    for i in range(n+1):
        draw_line(x,-400,x,400)
        x += 800/n
    y = -400
    for i in range(n+1):
        draw_line(-400,y,400,y)
        y += 800/n

draw_grid()
screen.update()

It should a grid like the following picture:

n x n grid

Step 2: Creating Life

We need data structure to store the lives in the n x n cells. The natural data structure for this purpose is a list of lists. We are going to use value 1 to represent ‘life’ and 0 to represent ‘no life’. The lives in cells will be randomly generated with 1/7 probability of having life. The following is the code snippet that creates and initializes lives:

life = list() # create an empty list
def init_lives():
    for i in range(n):
        liferow = [] # a row of lives
        for j in range(n):
            if random.randint(0,7) == 0: # 1/7 probability of life
                liferow.append(1) # 1 means life
            else:
                liferow.append(0) # 0 means no life
        life.append(liferow) # add a row to the life list -> life is a list of list

Step 3: Displaying Life in Cells

The next task is to draw live cells in the grid. We will create and use a new turtle called lifeturtle to draw the live cells. Because cells can become alive or dead, we need to erase them and redraw in each cycle. However, there is no need to erase the grid. By just clearing the lifeturtle, there is no need to redraw the grid. The following is complete code for the first 3 steps.

import turtle
import random

screen=turtle.Screen()
turtle.setup(1000,1000)
turtle.title("Conway's Game of Life - PythonTurtle.Academy")
turtle.hideturtle()
turtle.speed(0)
turtle.tracer(0,0)

lifeturtle = turtle.Turtle() # turtle for drawing life
lifeturtle.up()
lifeturtle.hideturtle()
lifeturtle.speed(0)
lifeturtle.color('black')

n = 50 # nxn grid
def draw_line(x1,y1,x2,y2): # this function draw a line between x1,y1 and x2,y2
    turtle.up()
    turtle.goto(x1,y1)
    turtle.down()
    turtle.goto(x2,y2)
    
def draw_grid(): # this function draws nxn grid
    turtle.pencolor('gray')
    turtle.pensize(3)
    x = -400
    for i in range(n+1):
        draw_line(x,-400,x,400)
        x += 800/n
    y = -400
    for i in range(n+1):
        draw_line(-400,y,400,y)
        y += 800/n

life = list() # create an empty list
def init_lives():
    for i in range(n):
        liferow = [] # a row of lives
        for j in range(n):
            if random.randint(0,7) == 0: # 1/7 probability of life
                liferow.append(1) # 1 means life
            else:
                liferow.append(0) # 0 means no life
        life.append(liferow) # add a row to the life list -> life is a list of list

def draw_square(x,y,size): # draws a filled square 
    lifeturtle.up()
    lifeturtle.goto(x,y)
    lifeturtle.down()
    lifeturtle.seth(0)
    lifeturtle.begin_fill()
    for i in range(4):
        lifeturtle.fd(size)
        lifeturtle.left(90)
    lifeturtle.end_fill()

def draw_life(x,y): # draws life in (x,y)
    lx = 800/n*x - 400 # converts x,y to screen coordinate 
    ly = 800/n*y - 400
    draw_square(lx+1,ly+1,800/n-2)

def draw_all_life(): # draws all life
    global life
    for i in range(n):
        for j in range(n):
            if life[i][j] == 1: draw_life(i,j) # draw live cells

draw_grid()
init_lives()
draw_all_life()
screen.update()

It should draw a shape like the following image:

Step 4: Updating Life Forever

The next step is to update the life based on the Conway’s Rule:

  1. If a cell with fewer than two or more than three live neighbors dies because of underpopulation or overpopulation.
  2. If a live cell has two or three live neighbors, the cell survives to the next generation.
  3. If a dead cell has exactly three live neighbors, it becomes alive.

We will create a function update_life() that will update the cells based on these rules. Then we will call this function again with the Turtle’s timer event. The following is the complete code for animating the Conway’s Game of Life:

import turtle
import random
import copy

screen=turtle.Screen()
turtle.setup(1000,1000)
turtle.title("Conway's Game of Life - PythonTurtle.Academy")
turtle.hideturtle()
turtle.speed(0)
turtle.tracer(0,0)

lifeturtle = turtle.Turtle() # turtle for drawing life
lifeturtle.up()
lifeturtle.hideturtle()
lifeturtle.speed(0)
lifeturtle.color('black')

n = 50 # nxn grid
def draw_line(x1,y1,x2,y2): # this function draw a line between x1,y1 and x2,y2
    turtle.up()
    turtle.goto(x1,y1)
    turtle.down()
    turtle.goto(x2,y2)
    
def draw_grid(): # this function draws nxn grid
    turtle.pencolor('gray')
    turtle.pensize(3)
    x = -400
    for i in range(n+1):
        draw_line(x,-400,x,400)
        x += 800/n
    y = -400
    for i in range(n+1):
        draw_line(-400,y,400,y)
        y += 800/n

life = list() # create an empty list
def init_lives():
    for i in range(n):
        liferow = [] # a row of lives
        for j in range(n):
            if random.randint(0,7) == 0: # 1/7 probability of life
                liferow.append(1) # 1 means life
            else:
                liferow.append(0) # 0 means no life
        life.append(liferow) # add a row to the life list -> life is a list of list

def draw_square(x,y,size): # draws a filled square 
    lifeturtle.up()
    lifeturtle.goto(x,y)
    lifeturtle.down()
    lifeturtle.seth(0)
    lifeturtle.begin_fill()
    for i in range(4):
        lifeturtle.fd(size)
        lifeturtle.left(90)
    lifeturtle.end_fill()

def draw_life(x,y): # draws life in (x,y)
    lx = 800/n*x - 400 # converts x,y to screen coordinate 
    ly = 800/n*y - 400
    draw_square(lx+1,ly+1,800/n-2)

def draw_all_life(): # draws all life
    global life
    for i in range(n):
        for j in range(n):
            if life[i][j] == 1: draw_life(i,j) # draw live cells

def num_neighbors(x,y): # computes the number of life neighbours for cell[x,y]
    sum = 0
    for i in range(max(x-1,0),min(x+1,n-1)+1):
        for j in range(max(y-1,0),min(y+1,n-1)+1):
            sum += life[i][j]
    return sum - life[x][y]
        
def update_life(): # update life for each cycle
    global life 
    newlife = copy.deepcopy(life) # make a copy of life
    for i in range(n):
        for j in range(n):
            k = num_neighbors(i,j)
            if k < 2 or k > 3:
                newlife[i][j] = 0
            elif k == 3:
                newlife[i][j] = 1
    life = copy.deepcopy(newlife) # copy back to life
    lifeturtle.clear() # clears life in previous cycle
    draw_all_life()
    screen.update() 
    screen.ontimer(update_life,200) # update life every 0.2 second

draw_grid()
init_lives()
update_life()

Related Projects:

Find Different Color Game with Python Turtle

Make a fun game that tests how fast you can identify very small color variation. You can use colorsys library to vary hue, saturation, brightness, or any combination of them. Make the game more difficult by reducing the amount of variation as the level increases. Also, make the size of variation smaller as level increases.

Demo Video of Find Different Color Game

Many Projectiles Animation with Python Turtle

Animate many projectiles shot from a platform all with same speed but different initial angles. Which projectile landed first? Which projectile landed farthest? Which projectile landed latest? Use colorsys library to draw these projectiles in different colors.

Tutorial: Gradually Lighting Up and Moving

This tutorial explains how to implement gradually lighting up and moving animation. This is a continuation of gradually lighting up animation tutorial. So, please make sure you read it before continuing on this one. This is part of a series of tutorials leading up to the implementation of synchronized fireflies.

In this project, the object is not only lighting up it is also moving randomly. To achieve this we must add a few more state variables besides current time and brightness values. We will add current position and target position variables. Target position is set randomly. The object will move towards the target position at a constant speed. When the object reaches or get close enough to the target, we will create a new target repeat the same process.

The following code is the model only part of the project. Since there are two independent states to update, we created two functions: update_brightness() and update_position(). The main update_states() function calls these two functions. Function update_brightness() works exactly the same as before. Function update_position() gets the direction from the current position to the target position and move SPEED*(1/FPS) steps because the the elapsed time for each update_position() call is about 1/FPS second. When the object gets close enough to the target, we will randomly generate a new target. Don’t set threshold for close enough to a very small number. Because of errors in floating point computation along with discrete time in updating the position, the object may never get as close to the tiny distance.

import turtle
import colorsys
import random
import math

screen = turtle.Screen()
screen.setup(1000,1000)
screen.title("Gradually Lighting Up and Moving - PythonTurtle.Academy")
turtle.hideturtle()
turtle.speed(0)
turtle.tracer(0,0)

# Constants
H_YELLOWGREEN = 0.22 # constant: hue value of yellow green color.
V_DARK = 0.1 # constant: brightness value of initial dark state
V_BRIGHT = 1 # constant: brightness value of the brightest state
FPS = 30 # constant: refresh about 30 times per second
TIMER_VALUE = 1000//FPS # the timer value in milliseconds for timer events
CYCLE = 5 # costant: 5 second cycle for firefly to light up
LIGHTUP_TIME = 1 # constant: 1 second light up and dim
SPEED = 100 # 100 units per second
CLOSE_ENOUGH = 16 # if distance squared to target is less than 16, then it is close enough.
                    # make sure that this number is greater than SPEED/FPS squared

# Variables
v = V_DARK # initial brightness state
t = 0 # current time
current_xpos = 0 # current x coordinate
current_ypos = 0 # current y coordinate
target_xpos = random.randint(-300,300) # target x coordinate, random location
target_ypos = random.randint(-300,300) # target y coordinate, random location


def update_brightness():
    global t,v
    if t > CYCLE:
        t -= CYCLE # make sure time stays within CYCLE
    if t < CYCLE-LIGHTUP_TIME:
        v = V_DARK # dormant period
    elif t < CYCLE-LIGHTUP_TIME/2: # gradually (linearly) lighting up period
        v = V_DARK + (V_BRIGHT-V_DARK)*(t-(CYCLE-LIGHTUP_TIME))/(LIGHTUP_TIME/2)
    else: # gradually (linearly) dimming period
        v = V_BRIGHT - (V_BRIGHT-V_DARK)*(t-(CYCLE-LIGHTUP_TIME/2))/(LIGHTUP_TIME/2)

def update_position():
    global current_xpos,current_ypos,target_xpos,target_ypos
    # move towards target SPEED/FPS steps
    # figure out angle to target first
    angle_to_target = math.atan2(target_ypos-current_ypos,target_xpos-current_xpos)
    # compute changes to current position based on the angle and distance to move per 1/FPS second.
    current_xpos += SPEED/FPS*math.cos(angle_to_target)
    current_ypos += SPEED/FPS*math.sin(angle_to_target)
    print(current_xpos,current_ypos,target_xpos,target_ypos)
    # check to see if close enough to target.
    dist_to_target_squared = (current_xpos-target_xpos)**2 + (current_ypos-target_ypos)**2
    if dist_to_target_squared < CLOSE_ENOUGH: # close enough, set new target
        target_xpos = random.randint(-300,300) # target x coordinate, random location
        target_ypos = random.randint(-300,300) # target y coordinate, random location
        

def update_states():
    global t
    t += TIMER_VALUE/1000 # every time this function is called, time increases by this value
    update_brightness()
    update_position()
    screen.ontimer(update_states,TIMER_VALUE)
                
screen.ontimer(update_states,TIMER_VALUE)

After completing the model part, the rest is extremely easy. The following is the complete code for this project. Because the object is constantly moving, the should_draw value is set to True every time update_state() function is called.

import turtle
import colorsys
import random
import math

screen = turtle.Screen()
screen.setup(1000,1000)
screen.title("Gradually Lighting Up and Moving - PythonTurtle.Academy")
turtle.hideturtle()
turtle.speed(0)
turtle.tracer(0,0)

screen.bgcolor('black')
firefly = turtle.Turtle() # turtle for drawing the lighting dot
firefly.hideturtle()
firefly.up()

# Constants
H_YELLOWGREEN = 0.22 # constant: hue value of yellow green color.
V_DARK = 0.1 # constant: brightness value of initial dark state
V_BRIGHT = 1 # constant: brightness value of the brightest state
FPS = 30 # constant: refresh about 30 times per second
TIMER_VALUE = 1000//FPS # the timer value in milliseconds for timer events
CYCLE = 5 # costant: 5 second cycle for firefly to light up
LIGHTUP_TIME = 1 # constant: 1 second light up and dim
SPEED = 100 # 100 units per second
CLOSE_ENOUGH = 16 # if distance squared to target is less than 16, then it is close enough.

# Variables
v = V_DARK # initial brightness state
t = 0 # current time
current_xpos = 0 # current x coordinate
current_ypos = 0 # current y coordinate
target_xpos = random.randint(-300,300) # target x coordinate, random location
target_ypos = random.randint(-300,300) # target y coordinate, random location
should_draw = True # this variable is used to decide if drawing is necessary to save CPU time

def update_brightness():
    global t,v
    if t > CYCLE:
        t -= CYCLE # make sure time stays within CYCLE
    if t < CYCLE-LIGHTUP_TIME:
        v = V_DARK # dormant period
    elif t < CYCLE-LIGHTUP_TIME/2: # gradually (linearly) lighting up period
        v = V_DARK + (V_BRIGHT-V_DARK)*(t-(CYCLE-LIGHTUP_TIME))/(LIGHTUP_TIME/2)
    else: # gradually (linearly) dimming period
        v = V_BRIGHT - (V_BRIGHT-V_DARK)*(t-(CYCLE-LIGHTUP_TIME/2))/(LIGHTUP_TIME/2)

def update_position():
    global current_xpos,current_ypos,target_xpos,target_ypos
    # move towards target SPEED/FPS steps
    # figure out angle to target first
    angle_to_target = math.atan2(target_ypos-current_ypos,target_xpos-current_xpos)
    # compute changes to current position based on the angle and distance to move per 1/FPS second.
    current_xpos += SPEED/FPS*math.cos(angle_to_target)
    current_ypos += SPEED/FPS*math.sin(angle_to_target)
    #print(current_xpos,current_ypos,target_xpos,target_ypos)
    # check to see if close enough to target.
    dist_to_target_squared = (current_xpos-target_xpos)**2 + (current_ypos-target_ypos)**2
    if dist_to_target_squared < CLOSE_ENOUGH: # close enough, set new target
        target_xpos = random.randint(-300,300) # target x coordinate, random location
        target_ypos = random.randint(-300,300) # target y coordinate, random location
        

def update_states():
    global t,should_draw
    t += TIMER_VALUE/1000 # every time this function is called, time increases by this value
    update_brightness()
    update_position()
    should_draw = True
    screen.ontimer(update_states,TIMER_VALUE)

def draw():
    global v,firefly,should_draw,current_xpos,current_ypos
    if should_draw == False: # There is no change. Don't draw and return immediately
        return
    firefly.clear() # clear the current drawing
    color = colorsys.hsv_to_rgb(H_YELLOWGREEN,1,v) # use colorsys to convert HSV to RGB color
    firefly.color(color)
    firefly.goto(current_xpos,current_ypos)
    firefly.dot(200)
    should_draw = False # just finished drawing, set should_draw to False
    
screen.ontimer(update_states,TIMER_VALUE)
while True:
    draw() # draw forever
    screen.update()

Related Tutorials:

Tutorial: Gradually Lighting Up Animation with Python Turtle

In this tutorial we are going present how to develop gradually lighting up animation. This is a series of tutorials leading to the development of synchronized fireflies.

When animating, especially if you want control the speed of animation, you need stick to the principle of model-view separation. This means you have one part of program that controls the state of objects (model), and the other part that draws the objects (view). State of objects may include position, heading, color, and other properties. You will know what it means better by doing more practice problems. The part of program that changes objects’ states is not concerned with drawing of the objects. The drawing is done by separate part of program that checks the states of the objects and render them on the screen. Let’s stop taking and get into the development of this animation.

Let’s develop the model part first. As you can observe the only thing that is changing is the color, more specifically the brightness of the circle. It is much easier to control the brightness of a color with HSV (Hue-Saturation-Value) color model, which can be converted to RGB color for Turtle easily with Python’s colorsys library. We will choose hue value of 0.22, which is greenish yellow color that is similar to the color of fireflies. Let’s suppose the firefly’s lighting up cycle is 5 seconds and it takes 1 second to gradually light up and dim back to the original state. So, between time 0 to 4, the brightness value stays at initial low value; then from time 4.0 to 4.5, the brightness gradually increases to maximum value, and from time 4.5 to 5, the brightness gradually goes down from the maximum brightness to initial dark state.

We will use Turtle’s timer event to update this brightness value. The question is: how frequently should we update this value? To see a smooth animation, we need to draw about 30 times per second: this number is called frames per second (fps). It is possible that your computer may not be able to draw 30 times per second especially when there are many complex objects. In this case, you will see stuttering animation. Coming back to deciding on how frequently we should update the values, since we will use 30 fps, we will update every 1/30 second.

The following is the code that uses timer event to set the brightness value. Most of the code is self-explanatory with the help of comments. There are two global variables: t and v. The variable t is current cycle time (0 – CYCLE) in seconds. Every time, the timer function is called, this value increments by the amount of timer value. Variable v represents the current brightness value, which can be computed easily knowing current time t. You can uncomment print(t,v) to see the pair of values at every update and check if they are correct.

import turtle

screen = turtle.Screen()
screen.setup(1000,1000)
screen.title("Gradually Lighting Up - PythonTurtle.Academy")
turtle.hideturtle()
turtle.speed(0)
turtle.tracer(0,0)

# Constants
H_YELLOWGREEN = 0.22 # constant: hue value of yellow green color.
V_DARK = 0.1 # constant: brightness value of initial dark state
V_BRIGHT = 1 # constant: brightness value of the brightest state
FPS = 30 # constant: refresh about 30 times per second
TIMER_VALUE = 1000//FPS # the timer value in milliseconds for timer events
CYCLE = 5 # costant: 5 second cycle for firefly to light up
LIGHTUP_TIME = 1 # constant: 1 second light up and dim

# Variables
v = V_DARK # initial brightness state
t = 0 # current time

def update_brightness():
    global t,v
    t += TIMER_VALUE/1000 # every time this function is called, time increases by this value
    if t > CYCLE:
        t -= CYCLE # make sure time stays within CYCLE
    if t < CYCLE-LIGHTUP_TIME:
        v = V_DARK # dormant period
    elif t < CYCLE-LIGHTUP_TIME/2: # gradually (linearly) lighting up period
        v = V_DARK + (V_BRIGHT-V_DARK)*(t-(CYCLE-LIGHTUP_TIME))/(LIGHTUP_TIME/2)
    else: # gradually (linearly) dimming period
        v = V_BRIGHT - (V_BRIGHT-V_DARK)*(t-(CYCLE-LIGHTUP_TIME/2))/(LIGHTUP_TIME/2)

    #print(t,v)
    screen.ontimer(update_brightness,TIMER_VALUE)
                
screen.ontimer(update_brightness,TIMER_VALUE)

With model part working correctly, it’s time to add the drawing part. We will make a function that draw a dot at the center of the screen. The question is: when should this function be called? We can make an infinite loop, in which this function is called constantly. The problem is: it will waste a lot of CPU time redrawing the same thing unnecessarily. Only when the state of the object has changed this function will draw the dot. So the solution is to make global variable called should_draw. The update_brightness() function in code above is responsible for setting should_draw to True, when it finds the v has changed.

The following is the complete code for this project. Again, most of the code is self-explanatory with the help of comments. Run this program and see if it works correctly!

import turtle
import colorsys

screen = turtle.Screen()
screen.setup(1000,1000)
screen.title("Gradually Lighting Up - PythonTurtle.Academy")
turtle.hideturtle()
turtle.speed(0)
turtle.tracer(0,0)

screen.bgcolor('black')
firefly = turtle.Turtle() # turtle for drawing the lighting dot
firefly.hideturtle()
firefly.up()

# Constants
H_YELLOWGREEN = 0.22 # constant: hue value of yellow green color.
V_DARK = 0.1 # constant: brightness value of initial dark state
V_BRIGHT = 1 # constant: brightness value of the brightest state
FPS = 30 # constant: refresh about 30 times per second
TIMER_VALUE = 1000//FPS # the timer value in milliseconds for timer events
CYCLE = 5 # costant: 5 second cycle for firefly to light up
LIGHTUP_TIME = 1 # constant: 1 second light up and dim

# Variables
v = V_DARK # initial brightness state
t = 0 # current time
should_draw = True # this variable is used to decide if drawing is necessary to save CPU time

def update_brightness():
    global t,v,should_draw
    t += TIMER_VALUE/1000 # every time this function is called, time increases by this value
    lastv = v # lastv is v before potential change
    if t > CYCLE:
        t -= CYCLE # make sure time stays within CYCLE
    if t < CYCLE-LIGHTUP_TIME:
        v = V_DARK # dormant period
    elif t < CYCLE-LIGHTUP_TIME/2: # gradually (linearly) lighting up period
        v = V_DARK + (V_BRIGHT-V_DARK)*(t-(CYCLE-LIGHTUP_TIME))/(LIGHTUP_TIME/2)
    else: # gradually (linearly) dimming period
        v = V_BRIGHT - (V_BRIGHT-V_DARK)*(t-(CYCLE-LIGHTUP_TIME/2))/(LIGHTUP_TIME/2)

    if v != lastv:
        should_draw = True # v has changed, so set should_draw to True
#    print(t,v)
    screen.ontimer(update_brightness,TIMER_VALUE)

def draw():
    global v,firefly,should_draw
    if should_draw == False: # There is no change. Don't draw and return immediately
        return
    firefly.clear() # clear the current drawing
    color = colorsys.hsv_to_rgb(H_YELLOWGREEN,1,v) # use colorsys to convert HSV to RGB color
    firefly.color(color)
    firefly.dot(400)
    should_draw = False # just finished drawing, set should_draw to False
    
screen.ontimer(update_brightness,TIMER_VALUE)
while True:
    draw() # draw forever
    screen.update()

Related Tutorials: