Sand blows away¶

How does it work¶
The basic mechanics of the animation are:
every location of the image is covered by a grain of sand
all grains are black while not moving
every grain waits for a random time before it starts to move
the movement starts slowly in a random direction
a grain of sand has a color as soon as it starts to move
every frame, the movement gets accelerated to the right
To make the animated GIF smaller, the image was shrunk and cropped.
The Obi-Wan Kenobi Series by Disney has a much better animation that inspired me to write my own.
Prerequisites¶
This script requires two libraries: OpenCV for displaying the live animation and imageio for exporting animated GIFs.
pip install opencv-python
pip install imagio
The Script¶
import random
import numpy as np
import cv2
import time
import imageio
BLACK = (0, 0, 0)
BEIGE = (128, 192, 255)
GRAIN_SIZE = 3 # smaller looks better but also slower
MAXX = 1200
MAXY = 550
class GrainOfSand:
"""
generates sand coordinates
"""
def __init__(self, x, y, xtarget):
self._x = x
self._y = y
self.ymod = random.randint(-30, 30) / 10
self.xtarget = xtarget
self.xspeed = random.randint(-20, 20) / 10
self.xgrowth = 1.1 # multiplier for xspeed
self.delay = max(0, int(random.gauss(65, 20)))
# randomize color a little
self.color = list(BEIGE)
self.color[0] += random.randint(-50, 50)
self.color[1] += random.randint(-50, 50)
self.color[2] += random.randint(-50, 0)
@property
def x(self):
return int(self._x)
@property
def y(self):
return int(self._y)
@property
def finished(self):
return self._x >= self.xtarget or self._y < 0 or self._y >= MAXY
@property
def moving(self):
return self.delay == 0
def update(self):
if self.delay > 0:
self.delay -= 1 # waiting time before grain starts to move
else:
self._x += self.xspeed
self._y += self.ymod
if self.xspeed < 1.0: # redirect moves from left to right
self.xspeed += 0.3
else:
self.xspeed *= self.xgrowth
def create_grains():
grains = []
for x in range(300, 900, GRAIN_SIZE):
for y in range(200, 350, GRAIN_SIZE):
grains.append(GrainOfSand(x, y, MAXX-2))
return grains
def draw_grain(frame, x, y, color):
if x >= MAXX - 1: return
frame[y:y+GRAIN_SIZE, x:x+GRAIN_SIZE] = color
def move_grains(frame, grains):
for g in grains:
g.update()
# draw static grains
for g in grains:
if not g.moving:
draw_grain(frame, g.x, g.y, BLACK)
# draw moving grains
for g in grains:
if g.moving:
draw_grain(frame, g.x, g.y, g.color)
return [g for g in grains if not g.finished]
background = np.zeros((MAXY, MAXX, 3), np.uint8)
text = cv2.imread('desert_python.png')
background[200:350, 300:853] = text
grains = create_grains()
frames = []
while True:
# display frames
frame = background.copy()
grains = move_grains(frame, grains)
cv2.imshow('frame', frame)
# shrink frame for using it on the web
rgb = cv2.cvtColor(frame[100:450:2, 200:-200:2], cv2.COLOR_BGR2RGB)
frames.append(rgb)
key = chr(cv2.waitKey(1) & 0xFF)
if key == 'q':
break
#time.sleep(0.03)
cv2.destroyAllWindows()
imageio.mimsave('sand_animation.gif', frames[::2], fps=20)