Tutorial: Animating Fireflies with Python Turtle

Fireflies Animation

This tutorial is the 3rd in a series of tutorials that leads to the development of synchronized fireflies. In this tutorial we are going to implement fireflies animation shown in the video above. If you have not done so, please go through part 1 and part 2 of this series.

This tutorial is going to use a simple data structure called list in Python. List contains a collection of items and you can access the items with indexes. You can find a lot more information about list on the Internet. The main idea is to create a list of fireflies each with its own brightness value, position, and target position. So, we are going to create a list of brightness values, a list of current coordinates, and target coordinates. Since fireflies light up at different times in the nature (in most cases), we want to simulate this situation. So, we need individual clock for each of these fireflies. We call this clock as phase. Initially, this phase value will be random number from 0 to 5, assuming 5 is the length of internal cycles for these fireflies. At each tick, all values in the phase list will increment by the amount of timer value.

The following is the model only part of the program. As you can see, the code very similar to the part 2 of this tutorial series, except we have loops that for each update functions. The variable N represent the number of fireflies to simulation. The initialze_fireflies() function initializes the list variables. The code is self-explanatory with the help of comments.

import turtle
import colorsys
import random
import math

screen = turtle.Screen()
screen.setup(1000,1000)
screen.title("Fireflies - 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 = 20 # 20 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
N = 100 # Number of fireflies

# Variables
v = [] # list of brightness values
phase = [] # list of phases
current_xpos = [] # list of current x coordinate
current_ypos = [] # list of current y coordinate
target_xpos = [] # list of target x coordinate
target_ypos = [] # list of raget y coordinate

def initialze_fireflies():
    for i in range(N):
        v.append(V_DARK) # set them DARK first. The update function will update it to the correct value
        phase.append(random.uniform(0,CYCLE)) # phase is random from 0 to CYCLE
        current_xpos.append(random.uniform(-500,500)) # Let them go anywhere on screen
        current_ypos.append(random.uniform(-500,500))
        target_xpos.append(random.uniform(-500,500))
        target_ypos.append(random.uniform(-500,500))

# this function computes brightness based on phase
def compute_brightness(phase):
    if phase < CYCLE-LIGHTUP_TIME:
        temp = V_DARK # dormant period
    elif phase < CYCLE-LIGHTUP_TIME/2: # gradually (linearly) lighting up period
        temp = V_DARK + (V_BRIGHT-V_DARK)*(phase-(CYCLE-LIGHTUP_TIME))/(LIGHTUP_TIME/2)
    else: # gradually (linearly) dimming period
        temp = V_BRIGHT - (V_BRIGHT-V_DARK)*(phase-(CYCLE-LIGHTUP_TIME/2))/(LIGHTUP_TIME/2)
    return temp
               
def update_brightness():
    global phase,v
    for i in range(N):
        phase[i] += TIMER_VALUE/1000 # increase the phase by time passed
        if phase[i] > CYCLE: # phase passed CYCLE
            phase[i] -= CYCLE # make sure phase stays within CYCLE
        v[i] = compute_brightness(phase[i]) # compute the brightness based on phase

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

initialze_fireflies()                
update_states()

Now, let’s add drawing part. We will create a list of turtles and let each turtle represent one firefly. Set the size of fireflies to a much smaller value and reduce the speed to let them move more gracefully. The following is the complete code for firefly animation.

import turtle
import colorsys
import random
import math

screen = turtle.Screen()
screen.setup(1000,1000)
screen.title("Fireflies - 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 = 20 # 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
N = 100 # Number of fireflies

# Variables
fireflies = [] # list of firefly turtles
v = [] # list of brightness values
phase = [] # list of phases
current_xpos = [] # list of current x coordinate
current_ypos = [] # list of current y coordinate
target_xpos = [] # list of target x coordinate
target_ypos = [] # list of raget y coordinate

def initialze_fireflies():
    for i in range(N):
        fireflies.append(turtle.Turtle()) # Add a turtle to the firefly turtle list
        v.append(V_DARK) # set them DARK first. The update function will update it to the correct value
        phase.append(random.uniform(0,CYCLE)) # phase is random from 0 to CYCLE
        current_xpos.append(random.uniform(-500,500)) # Let them go anywhere on screen
        current_ypos.append(random.uniform(-500,500))
        target_xpos.append(random.uniform(-500,500))
        target_ypos.append(random.uniform(-500,500))

    for firefly in fireflies: # initialize these turtles
        firefly.hideturtle()
        firefly.up()
                
# this function computes brightness based on phase
def compute_brightness(phase):
    if phase < CYCLE-LIGHTUP_TIME:
        temp = V_DARK # dormant period
    elif phase < CYCLE-LIGHTUP_TIME/2: # gradually (linearly) lighting up period
        temp = V_DARK + (V_BRIGHT-V_DARK)*(phase-(CYCLE-LIGHTUP_TIME))/(LIGHTUP_TIME/2)
    else: # gradually (linearly) dimming period
        temp = V_BRIGHT - (V_BRIGHT-V_DARK)*(phase-(CYCLE-LIGHTUP_TIME/2))/(LIGHTUP_TIME/2)
    return temp

def update_brightness():
    global phase,v
    for i in range(N):
        phase[i] += TIMER_VALUE/1000 # increase the phase by time passed
        if phase[i] > CYCLE: # phase passed CYCLE
            phase[i] -= CYCLE # make sure phase stays within CYCLE
        v[i] = compute_brightness(phase[i]) # compute the brightness based on phase

def update_position():
    global current_xpos,current_ypos,target_xpos,target_ypos
    for i in range(N):
        # move towards target SPEED/FPS steps
        # figure out angle to target first
        angle_to_target = math.atan2(target_ypos[i]-current_ypos[i],target_xpos[i]-current_xpos[i])
        # compute changes to current position based on the angle and distance to move per 1/FPS second.
        current_xpos[i] += SPEED/FPS*math.cos(angle_to_target)
        current_ypos[i] += SPEED/FPS*math.sin(angle_to_target)
        # check to see if close enough to target.
        dist_to_target_squared = (current_xpos[i]-target_xpos[i])**2 + (current_ypos[i]-target_ypos[i])**2
        if dist_to_target_squared < CLOSE_ENOUGH: # close enough, set new target
            target_xpos[i] = random.randint(-500,500) # target x coordinate, random location
            target_ypos[i] = random.randint(-500,500) # target y coordinate, random location
        
def update_states():
    global should_draw
    update_brightness()
    update_position()
    should_draw = True
    screen.ontimer(update_states,TIMER_VALUE)

def draw():
    global v,fireflies,should_draw,current_xpos,current_ypos
    if should_draw == False: # There is no change. Don't draw and return immediately
        return
    for i in range(N):
        fireflies[i].clear() # clear the current drawing
        color = colorsys.hsv_to_rgb(H_YELLOWGREEN,1,v[i]) # use colorsys to convert HSV to RGB color
        fireflies[i].color(color)
        fireflies[i].goto(current_xpos[i],current_ypos[i])
        fireflies[i].dot(10)
    should_draw = False # just finished drawing, set should_draw to False

screen.bgcolor('black')
initialze_fireflies()                
update_states()
while True:
    draw() # draw forever
    screen.update()

Related Tutorials:

Related Post