Introducing Sled, a Rust Library for Creating Spatial LED Strip Lighting Effects

I am excited to announce the stable 0.1 release of spatial_led, my open-source rust library that lets you use LED strips in a totally new way.

What Makes Spatial LED Different?

Typically, when we think of individually addressable LED strips, we see them as an array of color values, like this:

Index Color
0 #5C3F4F
1 #59464C
2 #55544A
3 #48584E
4 #3F5865

The beauty of this setup is in its simplicity. To change the color of the 3rd LED in a strip, we'd write something like this:

leds[2] = Rgb::new(1.0, 0.0, 0.0);

The idea of Spatial LED (abbreviated as Sled), is to consider the physical position of each LED in 2d space, and then expose that information in a way that is valuable to a person designing lighting effects.

Under the hood, a Sled configuration looks a little more like this:

Index Color Segment Position Angle Distance
0 #5C3F4F 0
1 #59464C 0
2 #55544A 0
3 #48584E 0
4 #3F5865 0
150 #293136 1
151 #333C43 1
152 #3A464C 1
153 #434F55 1
154 #4D5960 1

Of course, properties like Position and Angle aren't super easily indexible, so lets talk about the API.

A Highly-Ergonomic API

A Sled struct is most easily constructed by passing in some data via a configuration file.

use spatial_led::<Sled>;
fn main()  {
    let mut sled = Sled::new("/path/to/config.yap").unwrap();
}

Our config.yap file will look something like this:

center: (0.0, 0.5)
density: 30.0
--segments--
(-2, 0) --> (0.5, -1) --> (3.5, 0) -->
(2, 2) --> (-2, 2) --> (-2, 0)

Where:

  • center is a 2d point to which all pre-calculated angles will be relative to, and
  • density is the number of LEDs per meter (or whatever unit of measurement you're using)

Once we've provided all that information, the constructor will map out the shape of our LED strips in 2D space and precalculate all that information you see in the table above.

The official documentation provides several examples, but let me give a taste of what this library enables you to do.

Set Colors By Distance

To set all LEDs 2 Units away from the center to red:

sled.set_at_dist(2.0, Rgb::new(1.0, 0.0, 0.0));
// or relative to any other point using:
sled.set_at_dist_from(DISTANCE, POSITION, COLOR)
Set at Distance Under the hood, the Sled is just performing an intersection test between each line segment and a circle formed by the radius. It then colors the LEDs at each point of intersection.

Similar method like set_at_angle(angle, color) or set_at_dir(direction, color), work by running a simple intersection test between line segments and a ray.

Color Mapping and Modulation

Imagine if we declare a function that would take an input spatial property (like distance, position, angle, etc) and map it to an output color.

A mathematical notation for a mapping of 2D direction from the reference point to an RGB color might look like this:

Expressing that in Sled is super easy.

sled.map_by_dir_from(Vec2::new(2.0, 1.0), |dir| {
	let red = (dir.x + 1.0) * 0.5;
	let green = (dir.y + 1.0) * 0.5;
	Rgb::new(red, green, 0.5)
});
Map by Direction

Where a mapping function replaces the old color with a new one given some color rule function, a modulation function lets you operate on the old color value to produce a modified color.
Here's one way you might apply that idea:

sled.modulate_segment(3, |led| led.color * 0.25)?;
Modulate Segment

Output

It's worth noting that Sled doesn't interact directly with the GPIO pins of whatever hardware you're running this on; it is solely responsible for color computation. Whenever you need your colors back in a simple one dimensional array like we talked about earlier, you can call one use one of Sled's several output methods.

let colors = sled.colors();

for color in colors {
    println!(
        "{}, {}, {}",
        color.red, color.green, color.blue
    );
}

Extra Features for Time-driven Effects

If you're using this library, odds are you'll want to use it to design some cool time-driven effects rather than just static images. For that reason, some optional extra tools are thrown into the library for your convenience.

Drivers

A Driver is used to encapsulate all logic you might need to drive an effect into one structure. There's a lot of convenience to this design pattern, including the ease of swapping between different effects.

fn main() {
    let sled = Sled::new("path/to/config.yap").unwrap();
    
    let mut driver = Driver::new();
    driver.set_startup_commands(startup); // startup is a function
    driver.set_draw_commands(draw); // same here
    driver.mount(sled);

    let mut scheduler = Scheduler::new(240.0); // 240hz
    scheduler.loop_forever(|| {
        driver.step();
        let colors = driver.colors();
        // display our colors somehow
    });
}

Our startup and draw commands could be defined as follows:
Startup

#[startup_commands]
fn startup(buffers: &mut BufferContainer) -> SledResult {
    let colors = buffers.create_buffer::<Rgb>("colors");
    colors.extend([
        Rgb::new(1.0, 0.0, 0.0),
        Rgb::new(0.0, 1.0, 0.0),
        Rgb::new(0.0, 0.0, 1.0),
    ]);
    Ok(())
}
#[draw_commands]
fn draw(
    sled: &mut Sled,
    buffers: &BufferContainer,
    time_info: &TimeInfo
) -> SledResult {
    let elapsed = time_info.elapsed.as_secs_f32();
    let colors = buffers.get_buffer::<Rgb>("colors")?;
    let num_colors = colors.len();
    // clear our canvas each frame
    sled.set_all(Rgb::new(0.0, 0.0, 0.0));

    for i in 0..num_colors {
        let alpha = i as f32 / num_colors as f32;
        let angle = elapsed + (std::f32::consts::TAU * alpha);
        sled.set_at_angle(angle, colors[i]);
    }
    Ok(())
}

Again, consult the documentation for more details, but it's worth explaining what all that BufferContainer yap is about.

A buffer, under the hood, is just an array of values. You can create them for any type that you might need and store them in your driver for use in your effects. The benefit of storing our red, green, and blue colors in the example above (rather than just hard-coding them into our effect) is the ability drivers give us to modify buffer values from outside our driver.

So if we had some super cool dashboard to control our effects and the user wanted to change the color of our red spinning LED at runtime, we could do something like this:

driver
    .buffers_mut()
    .set_buffer_item::<Rgb>("colors", 0, user_requested_color)
    .unwrap(); // will error if there's no buffer of Rgb values called 'colors'.

Showcase

Here are a handful of cool effects that I've whipped up using this library. I'm rendering these using ratatui so you can see from a top-down perspective what's going on. The room shape is also pretty unusual, but again that's just so you better see how Sled interacts with the shape of the room.

Scan

Simulates a straight line sweeping through the room at random angles, whose color gradually progresses through the rainbow over time.

Click here if the video player isn't loading.

Embers

Animates a rigid multi-fractal noise pattern across the room, mapping the noise output to a color ramp.

Click here if the video player isn't loading.

Ripples

Simulates growing rings of color at random points in your room.

Click here if the video player isn't loading.

Warpspeed

Simulates stars zooming past you, almost like you're traveling in a space ship at light speed. All directions are relative to the center point declared in your config file.

Click here if the video player isn't loading.

The Future

I'm super happy with this release, but there's still a handful of improvements I'd like to make in future editions.

Further, I'd love to hear from all of you. This is my first time publishing an open source library and I'm sure there's plenty more I can learn from the experience. I'd love to see where you all run into friction using Sled, or any gripes you might have with the design.

  • Is no no_std support a total dealbreaker for this space?
  • Would you rather colors be represented as a (u8, u8, u8) instead of using palette?
  • Are Filters[1] even that useful of a feature?

Feel free to open up a discussion or pull request on the GitHub repository for this project. I'd love to hear from you all!


  1. See the example here titled "Set all LEDs within the overlapping areas of two different circles to blue".↩︎