Tutorial: Designing independent twinkles

In a different thread, @KRoach was looking for help designing twinkles.

:warning: The code examples below use array literals, e.g. var arr = [0, 1, 2]. At the time of writing, this is only supported in firmware 3.19 beta (September, 2021). Converting these examples to an earlier language version is admittedly a potentially annoying effort left to you.

The task

KRoach is driving modules that split into bundles of fiber optic cable to create a fiber optic star ceiling. As he described it:

Here’s his picture of one module:

Design process

Here’s one approach to this task:

  1. Create a way to identify whether a given LED is a white twinkler, yellow-white twinkler, blue-white twinkler, or should be solid white
  2. Design the dimming twinkle waveform over time
  3. Trigger each twinkler randomly
Notes on the code below

In the interest of making a useful tutorial, I’m preferring inefficient or verbose code that demonstrates a clear process for working out the problem. There are many more concise and clever approaches possible.

It’s such a joy using Pixelblaze v3 with the new v3.19 beta firmware that supports array literals. I apologize in advance, but as mentioned up top, I decided to use them in this code.

The snippets in this tutorial do not clutter the examples with slider controls - the full working example in the attached .epe file at the end adds them.

1. “Mapping” an index to a pixel behavior

If we assume each module is always in the order specified and they’re always connected directly to another module (with no gaps or one-off pixels), a modulus will tell you what kind of pixel each is. I’m going to define some constants for each type, kind of like an enum. This helps make the code more readable.

// Constants
var WHITE_TWINKLE = 0
var YELLOWISH_TWINKLE = 1
var BLUEISH_TWINKLE = 2
var WHITE_CONSTANT = 3

// Array literal: requires firmware 3.19 or greater
var moduleSequence = [ WHITE_TWINKLE,
                       WHITE_TWINKLE,
                       YELLOWISH_TWINKLE,
                       BLUEISH_TWINKLE, 
                       WHITE_CONSTANT ]

var modulePixelCount = moduleSequence.length

function pixelType(index) {
  var moduleIndex = index % modulePixelCount
  return moduleSequence[moduleIndex]
}

If you can’t guarantee the repeating nature of each module, you can make your own array that specifies the pixel type for each pixel in the installation:

// (uses constants defined in example above, and again assumes firmware >=3.19)
var pixelTypes = [WHITE_TWINKLE, WHITE_TWINKLE, YELLOWISH_TWINKLE ] // etc - one entry for each pixels. Must have at least `pixelCount` entries.

// Usage:
export function render(index) {
  var pixelType = pixelTypes[index]
  if (pixelType == YELLOWISH_TWINKLE) {} // do something for this type
}

2. Designing a dimming twinkle function

The goal for this step is to think about how we want the brightness to change over time, and create a function that returns that brightness for a given time into the animation.

It’d be nice to specify the depth (how low it goes) and duration of a twinkle in milliseconds.

You may try this and find you want a more smooth or jagged wandering path, but for starters let’s just define a down-up V-shaped brightness event.

Code:

// Return a brightness between 0 and 1 for a V-shaped twinkle-to-dimmer animation
var twinkleDepth = .7  // How dim to twinkle. .5 = half, 1 = to off, 5 = with a nonzero duration in a full off state
var twinkleDuration = 500  // in milliseconds
var halfTwinkleMs = twinkleDuration / 2  // For convenience below

// Param t: Progress into the twinkle, in milliseconds
function twinkleDown(t) {
  var brightness
  if (t < halfTwinkleMs) {
    brightness = 1 - twinkleDepth / halfTwinkleMs * t
  } else {
    brightness = twinkleDepth / halfTwinkleMs * t + 1 - 2 * twinkleDepth
  }

  return clamp(brightness, 0, 1)
}

3. Trigger each randomly

We can choose to store whether each pixel is twinkling right now, and how far we are into each twinkler’s animation. We can store both facts in a single array that holds a animation progress timer for each pixel. If the progress is zero, it isn’t currently twinkling.

Sometimes it helps to plan out a program in pseudocode:

Once per frame
  For each pixel
    If it's a twinkler
      Advance it's twinkle progress timer if it's actively twinkling (timer is > zero)
      If it's done twinkling, reset the timer to zero
      If its timer is zero, it's not twinkling
        Roll the dice to see if it should start twinkling
          If so, store `delta` (ms since last frame) as its twinkle progress timer

For each pixel
  Look up the hue and saturation value for it's color type, e.g. light blue
  If it's not a twinkler, use full brightness
  Otherwise, look up whether it's currently twinkling
    If so, use `twinkleDown(this pixel's twinkle timer)` to find how dimmed the brightness should be right now
  Set the hue, saturation, and value for this pixel

That pseudocode can be implemented like this:

// Assumes the code blocks above

var twinkleFrequency = .1  // Make bigger or smaller to change how much twinkling occurs
var twinkleTimers = array(pixelCount)  // Stores ms into each animation, per pixel

export function beforeRender(delta) {
  // For each pixel
  for (i = 0; i < pixelCount; i++) {
    if (pixelTypes[i] == WHITE_CONSTANT) continue  // Skip the rest of this `for` loop if it's not a twinkler
    // We only get here if it the current pixel is a twinkler
    if (twinkleTimers[i] > 0) { // If it's actively twinkling (or just finished one)...
       if (twinkleTimers[i] > twinkleDuration) { // And if the twinkle is complete...
         twinkleTimers[i] = 0 // Reset it. In the next frame it might be re-triggered.
       } else { 
         twinkleTimers[i] += delta // Advance its twinkle animation progress by how much time has passed.
       }
    } else {  // If it's not currently twinkling...
      if (random(1000) <= twinkleFrequency * delta) { // Roll the dice. Note: Approximation. Actually a Poisson random variable. 
        twinkleTimers[i] = delta // Start a new twinkle animation
      }
    }
  }
}

// WHITE_TWINKLE, YELLOWISH_TWINKLE, BLUEISH_TWINKLE, WHITE_CONSTANT
var hueForType = [0, .16, .667, 0]
var satForType = [0, .8, .8, 0]

export function render(index) {
  var pixelType = pixelTypes[index]  // Get the type of pixel this is
  var v = 1  // Start by assuming full brightness
  
  // If this pixel is one of the types that should twinkle...
  if (pixelType != WHITE_CONSTANT) {
    // If the twinkle animation is in-progress, IE its timer is nonzero...
    if (twinkleTimers[index]) { 
      v = twinkleDown(twinkleTimers[index]) // Set a dimmed value
    }
  }
  
  var h = hueForType[pixelType]
  var s = satForType[pixelType]
  hsv(h, s, v * v)
}

Final pattern

Twinkle tutorial

Download the entire working Twinkle tutorial.epe code.

4 Likes

Excellent write up. Nice walkthrough and explaining. Definitely adaptable to many other uses, twinkling or otherwise.

Other additions possible:

  • Make it variable for each type on depth, duration: “blue twinkle should go to .3”, “yellow twinkle should go to zero, and stay there for a short bit”. You’ll need to add another array for this for each global variable you want to make type specific, and sometimes pass in the type to other functions so it can figure out the answer.

  • Random duration - each time, figure out how long this particular twinkle is for, either a randomly set time ahead of time, or a random factor to decide when the twinkle is going to end (the first is easier given the way this is coded)

  • Make the default state dimmer and occasionally brighten instead.

  • Make the “constellation” LEDs (which are always on) occasionally do something… Maybe fade out all other stars, so they are the only ones left in the sky (planetarium mode?)

This has been a huge help, much appreciated!