Motivation
I spent New Year’s Eve with my girlfriend. Due to Covid19, we couldn’t invite any one else and I collected activity ideas for the two of us. She got quite excited about this firework in Lua.
In a previous blog post, I explained pixels.lua. We implemented it to run the Sense HAT 8×8 LED matrix natively with Lua. Let’s get started to implement some firework (or 花火/hanabi as it is called in Japanese).
Unlike last time, I didn’t implement the program beforehand on my own. We created the design on-the-fly together.
Goal
Implementation steps
Previous code as dependency
First, we reuse the code of the previous blog post and require
it. rpi_sensehat_pixels.lua is simply the previous code provided as a library by exporting its functionality with return
.
package.path = package.path .. ';?.lua'
local hat = require('rpi_sensehat_pixels')
We extend the search path for the working directory and the functionality of rpi_sensehat_pixels.lua
is now accessible via the hat
variable.
set_pixel
In the previous blog post, we have seen that setting pixel in the LED matrix corresponds to writing a file. We now write a function set_pixel
which takes a LED ID (int ∈ {1, 2, …, 64}) and some color and sets 1 of 64 Sense HAT LEDs to the given color (a hexadecimal string of length 6).
local set_pixel = function (led_id, color)
local fd = io.open(hat.sense_hat_device_path(), "w")
fd:seek("set", 2 * (led_id - 1))
fd:write(hat.color_to_bin(color))
fd:close()
end
xy_to_id
We really don’t want to deal with LED IDs, but define a zero-based two-dimensional coordinate system (x,y ∈ {0, 1, …, 7}). Because LED ID 1 is the LED at the top left, but we define x=0 y=0
to be at the bottom left, he have some offset of 57:
local xy_to_id = function (x, y)
return 57 + x - 8 * y
end
point_is_on_circle
There are different ways to implement the circle of a firework, but we reminded ourselves of the circle equation, we learned in high school. Is given point (x, y) on the circle with radius r? The second equation generalizes the equation for a non-origin center (\(x_c\), \(y_c\)).
Now, since we have a discrete system (there is no LED pixel between two LED pixels in consecutive lines), we don’t want to use the equation, but an inequality instead. We define a threshold of 1. Any LED matrix with an Euclidean distance ≤ 1 shall be highlighted. Be aware than inequality ≤ 1 would cover the entire interior disk. So we define a big circle with radius r and a small circle r-1. If the pixel is within the big circle, but not the small one, we return true (i.e. we say “the point is on the circle”).
local point_is_on_circle = function (cx, cy, r, x, y)
local in_big_circle = (x - cx) * (x - cx) + (y - cy) * (y - cy) - r * r <= 1
local in_small_circle = (x - cx) * (x - cx) + (y - cy) * (y - cy) - (r-1) * (r-1) <= 1
return in_big_circle and not in_small_circle
end
Use draw_explosion
But how can we use point_is_on_circle
? The idea is to iterate over all LEDs and determine whether the given pixel is on the circle. If so, then we draw the corresponding circle. This is design of draw_explosion
. We take a color, some center (cx, cy), and time t. At time t=0, we draw a single point and at any later time t (max is 4), we draw a circle of radius t.
local draw_explosion = function (color, cx, cy, t)
if t == 0 then
set_pixel(xy_to_id(cx, cy), color)
end
if t > 0 and t <= 4 then
for x = 0, 7 do
for y = 0, 7 do
if point_is_on_circle(cx, cy, t, x, y) then
set_pixel(xy_to_id(x, y), color)
end
end
end
end
end
A rising rocket
Drawing a rising rocket is trivial. At time t and with explosion center (cx, cy), we set pixel y=t if y<cy:
for t = 0, (cy - 1) do
set_pixel(xy_to_id(cx, t), color)
hat.clear_pixels()
end
In the final implementation, I delayed the execution of the next loop iteration a little bit.
Drawing a firework
Thus, we have all part, we draw a rising rocket and then let the rocket explode by showing a circle. But where? Well, we pick a random location. We use math.random(a, b) which returns a random integer in {a, a+1, …, b}. We recognized that y≤1 looks aesthetically less pleasing and thus ensured that y is at least 2. Also the color is picked randomly from the table.
local draw_firework = function ()
hat.clear_pixels()
local cx = math.random(8) - 1
local cy = math.random(6) + 1
local color = colortable[math.random(8) - 1]
for t = 0, (cy - 1) do
set_pixel(xy_to_id(cx, t), color)
hat.sleep(0.1)
hat.clear_pixels()
end
for t = 0, 4 do
draw_explosion(color, cx, cy, t)
hat.clear_pixels()
end
end
The recording called draw_firework
13 times consecutively.
Complete source code
You can find the complete code on gist.github.com.
Depending on your preference, you might want to change the timing delays of the firework (hat.sleep
calls).
One advanced goal would be to let multiple rockets rise at the same time and explode at different times. This is why we implemented set_pixel
instead of using set_pixels
from last time. The latter set all pixels and thus clears pixels which are not related to the firework we draw. In the end, I think a more sophisticated design for time handling suffices to implement this usecase. We did not have time for it and switched to preparing cheese fondue.
All the best in 2021!