Timed Animation

Hello PB Community,

TLDR… skip to the second paragraph.

I’m currently working on a sunrise alarm clock to get myself some experience with PB before the holiday lighting season. Initially I was using my raspberry Pi and the neopixel library to drive the two 8x32 LED panels I’ll be using for the light-up portion of the alarm function. The animations I created worked fine during initial testing running all the python scripts direct from the terminal. I developed a web interface for scheduling the alarms and was able to use it to kick off animations on the LED panel, the problem I ran into was I was not able to stop an animation once it begins executing. Rather than continuing to try to recreate the wheel and create my own API, I did some research and invested in a PB. So far this has worked great and resolved my initial issues. I use my web interface and Pi to make API calls to the PB to start up animations, most importantly, I can also stop them (turn off the alarm). At this point you are probably wondering what the issue is, mainly I think I bit off a bit more than I could chew with my first animation, but I’d like to get this project knocked out so I can move onto experimenting/learning with PB on some strip lights for the holidays.

So onto the nuts and bolts of why we are here. I caught the Tasks postings Scruffynerf had been posting for a while and stumbled on the sunrise animations. I ended up poaching one of them for my own purposes, however I need to make some tweaks to step through the animation over a set interval of time. This animation will act as the wake-up light, with each frame of the animation, the panel will get a bit brighter, eventually pausing on the last frame of the animation. I want this timer to be configurable so the user can choose the period the animation unfolds over. For example, let’s say there are 10 frames, at T0, you would see Frame 1. When Tn<T<Tn+30, where n is a value between 1-30 in minutes selected by the user, you would see frame 10. When T>Tn+30 the panel would turn back off. So far I’ve been able to change how fast the animation plays but haven’t been able to do it in at any sort of predictable rate. I’ve definitely not figured out how to make the animation pause on the last frame. Anyone willing to help a newb with a timed animation?

Side note, thanks to @jeff for the original animation I used as the basis of my code. I’ll paste my most recent (non-working) edits for reference.

var hueSun = .10, hueEdges = 0.01
var sharpness = 3, spread = .8
var location = 0.5 // the centre of the sunrise
export var t1  // main time cycling variable, from 0 to 1
var sunRadius = 0.5
var rimToWhite = 0.01
var interval
var step = 0

export function inputNumberWakeTimer(WakeTime){
  interval = (clamp(WakeTime,1,30) * 60000)/96  //96 is a WAG for the number of frames
  return interval
}

// over-simplified, given that the HSV colorspace almost allows for a reasonable sunset color gradient.  Ideally, this would be a proper gradient calculation
// this will be fed a value from 0 to 0.5.  at 0, it should return hueSun ,and at 0.5 (or more, error correction), it should return hueEdges
function sunsetGradient(distFromSun) {
  var returnHue = hueSun
  if (distFromSun <=0) returnHue = hueSun
  else if (distFromSun >=0.4) returnHue = hueEdges
  else {
    // safely have a value of cDist between 0 and 0.4.  calculate slope of higher-lower/horizontal, plus lower, to give line between higher and lower, over the given horizontal distance.
	  returnHue = ((hueEdges-hueSun)/0.4)*distFromSun + hueSun
  }
  return returnHue
}

// Only here for the v2
function hypot(a, b) { return sqrt(a * a + b * b) }

//https://www.gizma.com/easing/
function easeOutQuad(t, b, c, d) {
	t /= d
	return -c * t * (t-2) + b
}

// time loops every 65.54*interval seconds, and interval 0.015 = 1 second apprx.
export function beforeRender(delta) {
  step += delta
  if (step > interval){
    t0 = clamp(1.5 * time(.2) - .25, 0, 1)
    t1 = easeOutQuad(t0, 0, 1, 1)
    sunRadius = .3 + t0 / 3
  }
}

export function render2D(index, x, y) {
  x -= .5; y = 4 - y - t1
  var cDist = hypot(x, y - 2 * t1)
  var distFromSun = clamp((cDist-sunRadius)/2,0, 1)
  var sat, hue
 
  var va = spread * pow((1/spread)-cDist, sharpness) + t1
  va = clamp(va, 0, .5) //clamp the value
 
  // calculate white (desaturated) centre of sun
  sat = (cDist<(sunRadius-rimToWhite)) ?.5/(sunRadius-rimToWhite)*cDist + .5 : 1
    
  // calculate gradient,as a functin of the distance from the sun centre.
  hue = sunsetGradient(distFromSun)
    
  hsv(hue, sqrt(sat), va * va * va)
}

export function render3D(index, x, y, z) {
   render2D(index,x,y)
}
export function render(index) {
   render2D(index, .5, index / pixelCount)
}

Here’s where I landed after a bunch of searching the forums and trial and error. I ended up poaching the hue gradient, otherwise re-drafted most of the code. I set the hue and intensity of each pixel based on its distance from the center point, then move this center point based on elapsed time. I think this could easily be adapted to track the sun based on the time of day, or the sun’s position in the sky.

var width = 16
var height = 32
var numPixels = width * height
var canvasValues = array(numPixels) //make a "canvas" of brightness values
var canvasHues = array(numPixels) //likewise for hues
var canvasSats = array(numPixels) //likewise for sats
var syi = -1.2 //Starting location of sun core in y direction "rises" to positive values
var syf = .1 //Final location of sun core in y direction
var sx = .5 //Starting location of sun core in x direction, centered on panel
var sy = syi //Current sun location in y direction
var r = .01  //Radius of sun core from 0-1
var hueSun = .10, hueEdges = 0.01
var t1 = 0


function cDist(x, y){
  return hypot(x - sx,y - sy)
}

export function inputNumberWakeTimer(v){ //how long the animation will run in minutes
  minutes = clamp(v, 1, 30) //minutes from 1 - 30
  sec = (minutes * 60) //determine seconds
  return sec; minutes
}

export function inputNumberOffTimer(v){ //how long the lights will remain lit in minutes
  offMinutes = clamp(v, minutes + 1, 120) //minutes from 1 - 120
  offAfter = (offMinutes * 60) //determine seconds
  return offAfter
}

export function toggleEnableSunRise(isEnabled){
  if (isEnabled){
    sunrise = true
    syi = -1.2 //Starting location of sun core in y direction "rises" to positive values
    syf = .1 //Final location of sun core in y direction
  }
  else{
    sunrise = false
    syi = .1 //Starting location of sun core in y direction "sets" to negative values
    syf = -1.3 //Final location of sun core in y direction
  }
}

function sunsetGradient(distFromSun) {
  var returnHue = hueSun
  if (distFromSun <=0) returnHue = hueSun
  else if (distFromSun >=0.9) returnHue = hueEdges
  else {
    // safely have a value of cDist between 0 and 0.9.  calculate slope of higher-lower/horizontal, plus lower, to give line between higher and lower, over the given horizontal distance.
	  returnHue = ((hueEdges-hueSun)/0.9)*distFromSun + hueSun
  }
  return returnHue
}

//find the pixel index within a canvas array
//pixels are packed in rows, then columns of rows
function getIndex(x, y) {
  return floor(x*width) + floor(y*height)*width
}

function distanceToCenter(x, y){
  dindex = getIndex(x, y)
  sDistance = cDist(x, y)
  if (pow(x - sx,2) + pow(y - sy,2) <= (sqrt(r)/8)){ //White center based on distance from x, y to sx, sy
    canvasValues[dindex] = .75
    canvasHues[dindex] = sunsetGradient(sDistance)
    canvasSats[dindex] = 0
  }
  else if ((pow(x - sx,2) + pow(y - sy,2) <= (15 * (sqrt(r)))) & (pow(x - sx,2) + pow(y - sy,2) >= (sqrt(r)/8))){ //use sunsetGradient to determine hue based on distance from x, y to sx, sy
    canvasValues[dindex] = .75
    canvasHues[dindex] = sunsetGradient(sDistance)
    canvasSats[dindex] = 1
  }
  else if ((pow(x - sx,2) + pow(y - sy,2) > (15 * (sqrt(r))))){ //use sunsetGradient to turn off pixels based on distance from x, y to sx, sy
    canvasValues[dindex] = 0
    canvasHues[dindex] = sunsetGradient(sDistance)
    canvasSats[dindex] = 1
  }
}

export function beforeRender(delta) { 
  t1sec = delta / 1000 //Convert delta to seconds to avoid wrapping
  t1 = clamp(t1 + t1sec, 0, offAfter) //keep track of elapsed time, stop at offAfter interval to avoid wrapping
  if (t1 == 0){  //reset y to start position
    sy = syi
  }
  else if(t1 >= sec){  //freeze y at final position
    sy = syf
  }
  else{ //move y from start to final position
    pos = clamp(t1 / sec, 0, 1) //determine how far y will move since last frame, as a percentage
    if (sunrise){
      sy = (pos*abs(syi-syf)) + syi //move y pos % to its final position
    }
    if (!sunrise){
      sy = syi - (pos*abs(syf-syi)) //move y pos % to its final position
    }
  }
}

export function render2D(index, x, y) {
  if (t1 >= offAfter){ //shut off all lights after elapsed interval
    hsv(0, 0, 0)
  }
  else{
    index = getIndex(x, y) //calc this pixel's index in the canvas based on position
    distanceToCenter(x, y) //calc this pixels distance to the center of the sun and set H, S, V based on this value
    h = canvasHues[index]
    v = canvasValues[index]
    s = canvasSats[index]
    hsv(h, s, v*v)
  }
}

//var watcher for troubleshooting
export var sy;
export var offAfter;
export var t1;
export var sec;
export var minutes;
export var pos;
export var sunrise;

For me this really came down to two issues, both I think that may have been avoided with some more complete/consolidated documentation. I found it challenging to track down appropriate documentation since it is spread out across so many sources, the main documentation page, the help info in the built in IDE, the bhencke website, and the forums. I think beginners would really benefit from consolidating some of this information into a single source of truth in addition to rounding out some of the topics.

Specifically, the two issues I struggled with, my ignorance as to the implications of 16.16 floating point math. I have little to no experience with floating point math, and that experience I do have was about a decade back. The fact it kept wrapping was why the animation was not moving in an expected manner for me, I found this called out in a forum post, but not anywhere within the official documentation. Even then, it didn’t dawn on me as the problem until I began outputting variables using the vars watcher. Converting from ms to seconds solved the problem of the animation restarting on me.

Next, I just really didn’t understand how to leverage render2d and render3d. The examples provided define specific x and y coordinates in the render2d/3d functions. This led me to falsely assume that you had to define these somehow in the render function. Once I realized the x, y and z coords are supplied to the code in the render call similar to the pixel ID, things fell into place. Honestly that was my first impression, but the documentation and provided examples actually confused me and led me to an incorrect understanding of how the functions work. Eventually ignoring the provided examples and just playing with things led me to figuring it out.