Let's start with a simple version of the snowglobe. This version does not require any additional bitmap files. You can set a background image if you want. But if you don't have one and just want to get up and running, just leave it alone and it will use a solid color.

Here's the code:

# SPDX-FileCopyrightText: 2019 Carter Nelson for Adafruit Industries
#
# SPDX-License-Identifier: MIT

from random import randrange
import board
import busio
import displayio
from adafruit_gizmo import tft_gizmo
import adafruit_imageload
import adafruit_lis3dh

#---| User Config |---------------
BACKGROUND = 0xAA0000              # specify color or background BMP file
NUM_FLAKES = 50                    # total number of snowflakes
SNOW_COLOR = 0xFFFFFF              # snow color
SHAKE_THRESHOLD = 27               # shake sensitivity, lower=more sensitive
#---| User Config |---------------

# Accelerometer setup
accelo_i2c = busio.I2C(board.ACCELEROMETER_SCL, board.ACCELEROMETER_SDA)
accelo = adafruit_lis3dh.LIS3DH_I2C(accelo_i2c, address=0x19)

# Create the TFT Gizmo display
display = tft_gizmo.TFT_Gizmo()

# Load background image
try:
    bg_bitmap, bg_palette = adafruit_imageload.load(BACKGROUND,
                                                    bitmap=displayio.Bitmap,
                                                    palette=displayio.Palette)
# Or just use solid color
except (OSError, TypeError, AttributeError):
    BACKGROUND = BACKGROUND if isinstance(BACKGROUND, int) else 0x000000
    bg_bitmap = displayio.Bitmap(display.width, display.height, 1)
    bg_palette = displayio.Palette(1)
    bg_palette[0] = BACKGROUND
background = displayio.TileGrid(bg_bitmap, pixel_shader=bg_palette)

# Shared palette for snow bitmaps
palette = displayio.Palette(2)
palette[0] = 0xADAF00   # transparent color
palette[1] = SNOW_COLOR # snow color
palette.make_transparent(0)

# Snowflake setup
FLAKES = (
    0, 0, 0, 0,    0, 0, 0, 0,    1, 1, 1, 1,
    0, 0, 0, 0,    1, 1, 1, 0,    1, 1, 1, 1,
    0, 1, 1, 0,    1, 1, 1, 0,    1, 1, 1, 1,
    0, 1, 1, 0,    1, 1, 1, 0,    1, 1, 1, 1,
)
flake_sheet = displayio.Bitmap(12, 4, len(palette))
for i, value in enumerate(FLAKES):
    flake_sheet[i] = value
flake_pos = [0.0] * NUM_FLAKES
flakes = displayio.Group()
for _ in range(NUM_FLAKES):
    flakes.append(displayio.TileGrid(flake_sheet, pixel_shader=palette,
                                     width = 1,
                                     height = 1,
                                     tile_width = 4,
                                     tile_height = 4 ) )

# Snowfield setup
snow_depth = [display.height] * display.width
snow_bmp = displayio.Bitmap(display.width, display.height, len(palette))
snow = displayio.TileGrid(snow_bmp, pixel_shader=palette)

# Add everything to display
splash = displayio.Group()
splash.append(background)
splash.append(flakes)
splash.append(snow)
display.root_group = splash

def clear_the_snow():
    #pylint: disable=global-statement, redefined-outer-name
    global flakes, flake_pos, snow_depth
    display.auto_refresh = False
    for flake in flakes:
        # set to a random sprite
        flake[0] = randrange(0, 3)
        # set to a random x location
        flake.x = randrange(0, display.width)
    # set random y locations, off screen to start
    flake_pos = [-1.0*randrange(0, display.height) for _ in range(NUM_FLAKES)]
    # reset snow level
    snow_depth = [display.height] * display.width
    # and snow bitmap
    for i in range(display.width * display.height):
        snow_bmp[i] = 0
    display.auto_refresh = True

def add_snow(index, amount, steepness=2):
    location = []
    # local steepness check
    for x in range(index - amount, index + amount):
        add = False
        if x == 0:
            # check depth to right
            if snow_depth[x+1] - snow_depth[x] < steepness:
                add = True
        elif x == display.width - 1:
            # check depth to left
            if snow_depth[x-1] - snow_depth[x] < steepness:
                add = True
        elif 0 < x < display.width - 1:
            # check depth to left AND right
            if snow_depth[x-1] - snow_depth[x] < steepness and \
               snow_depth[x+1] - snow_depth[x] < steepness:
                add = True
        if add:
            location.append(x)
    # add where snow is not too steep
    for x in location:
        new_level = snow_depth[x] - 1
        if new_level >= 0:
            snow_depth[x] = new_level
            snow_bmp[x, new_level] = 1

while True:
    clear_the_snow()
    # loop until globe is full of snow
    while snow_depth.count(0) < display.width:
        # check for shake
        if accelo.shake(SHAKE_THRESHOLD, 5, 0):
            break
        # update snowflakes
        for i, flake in enumerate(flakes):
            # speed based on sprite index
            flake_pos[i] += 1 - flake[0] / 3
            # check if snowflake has hit the ground
            if int(flake_pos[i]) >= snow_depth[flake.x]:
                # add snow where it fell
                add_snow(flake.x, flake[0] + 2)
                # reset flake to top
                flake_pos[i] = 0
                # at a new x location
                flake.x = randrange(0, display.width)
            flake.y = int(flake_pos[i])
        display.refresh()

The features you can customize are grouped at the top of the code:

#---| User Config |---------------
BACKGROUND = 0xAA0000              # specify color or background BMP file
NUM_FLAKES = 50                    # total number of snowflakes
SNOW_COLOR = 0xFFFFFF              # snow color
SHAKE_THRESHOLD = 27               # shake sensitivity, lower=more sensitive
#---| User Config |---------------

The code comments describe what they do. Here's a summary:

  • BACKGROUND - the background color OR image filename
  • NUM_FLAKES - the total number of flakes to show
  • SNOW_COLOR - the color of the snow
  • SHAKE_THRESHOLD - shake-to-clear sensitivity

For now, just go ahead and try running it as is with the default values. You should get some nice white snow on a red background.

Change Colors

Now try changing the BACKGROUND and SNOW_COLOR values. Change those lines to something like this:

BACKGROUND = 0x00FF00              # specify color or background BMP file
NUM_FLAKES = 50                    # total number of snowflakes
SNOW_COLOR = 0xFF00FF              # snow color
SHAKE_THRESHOLD = 27               # shake sensitivity, lower=more sensitive

Hey! Yellow snow! On a green background.

Change Background

If you specify a bitmap file instead of a color for BACKGROUND, it will be loaded and used. For the TFT Gizmo, the file needs to be an indexed BMP file that is 240x240 in size. Here's one you can use to try this out:

Download that file and copy it to your CIRCUITPY folder. Then change the background setting line to:

BACKGROUND = "/blinka_dark.bmp"              # specify color or background BMP file

And change the snow color back to white:

SNOW_COLOR = 0xFFFFFF              # snow color

Run that and you should end up with snow falling over a Blinka background.

Change Flakes

You can do a minor amount of flake shape customization. We'll provide a fancier version of the code next that will really let you do this. But if you want, you can play around with these lines of code:

FLAKES = (
    0, 0, 0, 0,    0, 0, 0, 0,    1, 1, 1, 1,
    0, 0, 0, 0,    1, 1, 1, 0,    1, 1, 1, 1,
    0, 1, 1, 0,    1, 1, 1, 0,    1, 1, 1, 1,
    0, 1, 1, 0,    1, 1, 1, 0,    1, 1, 1, 1,
)

See how there are 3 separate 4x4 grids? Those are the flakes, which are each 4 pixels wide by 4 pixels high. A 0 ends up being transparent. A 1 ends up being the snow color. So you can edit those if you want to change the flake shape. But with only 4x4 pixels to work with, it's pretty minimal customization.

Shake To Clear

Don't forget to also try shaking the board to "clear" the snow globe. This will erase all of the accumulated snow and reset all of the snowflakes back to the top. Go ahead - shake it like a polaroid!

This guide was first published on Nov 02, 2019. It was last updated on Dec 11, 2023.

This page (Simple Snow Globe) was last updated on Dec 11, 2023.

Text editor powered by tinymce.