Smooth Speed Slider?

I have tried all 3 of these ‘smooth speed’ sliders.

The first two (delta and time offset) work well.

@Scruffynerf 's version seems the simplest, but I can’t get it to work at all.
I’ll play with it a bit more…

A few tips to making mine work (based on my own playing with it)

  1. the value passed to speed needs to be relatively high. If your slider variable is low, multiply it by a lot (10000?) inside the time()
    Too fast a cycle and it’ll add too fast. You want a long cycle, for tiny increases to y.

[ added - this was my goof: it ONLY works while it’s tiny, as the time value gets bigger, it breaks. I didn’t test this enough, I failed hard here.]

  1. you have to put the main addition inside of before_render. It won’t work inside the slider (only called when slider moves?) or main render (called too often), as it needs to keep adding the value time(speed) to y (%1), and then you can treat y as if it was a smooth running time() so wrap it in a wave/triangle/etc

I’ll definitely be using this in the future. I didn’t realize how many patterns don’t like their sliders adjusted. I mean they work, but they feel jarring when you adjust them.

Still researching bezier curve easing (which would be awesome) as that could make transitions even smoother, but way more work for very little payoff in 99% of the cases.

1 Like

This is my version:

//Smooth Speed Nick version
export var speed // controlled by slider

export function sliderSpeed(s) {
  speed = 1 / (1 + 99 * s)
  w = time(speed)
}

var t1
export function beforeRender() {
  x = time(speed)
  t1 = (t1 + (x-w)) % 1 // accumulate time in t1, and wrap it using modulus math to keep it between 0-1
  w = x
}

export function render(index) {
  h = t1 + index/pixelCount
  s = 1
  v = 1
  hsv(h, s, v)
}
1 Like

I still don’t understand this.
Say x (time(speed)) is 0.25 for example, on first loop, y = (0.25 +0) % 1 ie 0.25.
On the next loop, x is now 0.25001, so y = (0.25001 + 0.25) % 1 ie 0.50001
Next loop, x is 0.25002, so y = (0.25002 + 0.50001) % 1 ie 0.75003
etc.

This is my problem - you are adding y to itself, so, y is not a smooth running time. Am I missing something?

Ok, upon review, I dun goofed. By adding time() as I did in very small increments, it worked, but as values increased, it sped up incorrectly, which was entirely wrong. Now Fixed. My bad for not fully testing, I didn’t pay attention when the slider was too high to notice.

Thanks @jeff for suggestion of visualising one pixel, rather than the spectrum.

Correct code:

export var speed, y, z, h  // exports for debugging
var tuning = 500 // scaling factor 

export function sliderSpeed(v) {
  speed = v / tuning 
  // 0..1 speed divided by tuning for scale (SMALL increments)
  // with the above tuning, speed goes from 0 to .0002, very small
}

export function beforeRender() {
  y = (speed + y)%1
  // y is now incremented slightly by speed before each render cycle, 
  // and stays within 0..1
  z = wave(y) 
  // because y is really a smooth sawtooth, wave() works as expected.
}

export function render(index) {
  h = z + index/pixelCount - 1 // color cycles
  // h = 1
  // ^ uncomment to make solid color
  s = 1
  v = (index == floor(z*pixelCount))  
  // only light one moving pixel, based on z (only one will be 1, rest will be 0)
  hsv(h, s, v)
}

the extra (and unneeded) time() causes the problem. I still want to use time() and will play with that more. @wizard’s [since Zranger disclaimed the code] original version is very similar, but also adds in delta to the increment value, so that it better handles framerate differences.

2 Likes

@Nick_W,
Nice work! It looks like you are maintaining the phase, and incrementing by the corrected delta. That would work as well if you move the phase detection stuff to the slider, less math for beforeRender.

Here’s my take on it:

/*
The time(interval) function will return very different values for even small changes to interval. This
can cause animations to jump around if the interval used is changed e.g. through a slider.

This pattern shows a way to correct for that by measuring the phase difference when making changes to the interval.

The benefit of this method is that the interval can be changed without causing "jumps" in animation.
Since it still uses time() instead of delta, it can be synchronized with other Pixelblaze devices on the network, 
as long as they share the same interval and phase state.

*/

export var interval = .1 //aka "speed" to be controlled via a slider
export var phase = 0 //keep track of a phase correction factor

export function sliderConsistentSpeed(v) {
  var p1 = time(interval) //measure the current interval's value
  interval = v*v + .001
  var p2 = time(interval) //measure the new interval's value
  //calculate the phase difference between these
  phase = (1 + phase + p1 - p2) % 1
}

function phaseTime(interval, phase) {
  return (time(interval) + phase) % 1
}

export function beforeRender(delta) {
  t1 = phaseTime(interval, phase)
}

export function render(index) {
  h = t1 + index/pixelCount
  s = 1
  v = 1
  hsv(h, s, v)
}
2 Likes

Here I was saying to myself: it’s a phase difference, as you change the wavelength, there’s got to be a good way to do that… And you posted it as I was refreshing myself on the math. Nice.

What about this:

//Smooth Speed Nick version II
export var speed
export function sliderSpeed(s) {
  speed = 1 / (1 + 99 * s)
}

var ts, p, prev_i
function smoothTime(interval) {
  /*
  time() function to smooth out changes in interval
  ts is time smooth, p is phase, prev_i is previous interval
  */
  x = time(prev_i)
  ts = (1+ ts + x - p) % 1 // accumulate time in ts, and wrap it using modulus math to keep it between 0-1
  p = (interval == prev_i) ? time(prev_i) : time(interval)
  prev_i = interval
  return ts
}

export function beforeRender() {
  t1 = smoothTime(speed)
}

export function render(index) {
  h = t1 + index/pixelCount
  s = 1
  v = 1
  hsv(h, s, v)
}

The advantages of this approach:

  • No functions in sliderSpeed() so that can remain unchanged
  • smoothTime() is a drop in replacement for time()
  • if variable speed is changed directly (by setVars() say), it still works - no need to use the slider
  • will still sync with other PB’s as it uses time()

So just add the smoothTime() function (plus global vars) to your pattern, change time() to smoothTime() and there you go.

If you use more than one time() function in your pattern, you would have to duplicate smoothTime() also, but that seems a minor issue.

1 Like

Really nice work. I will absolutely use something like this.

You can easily reuse that function for multiples if you pass more than one value into it, like a pointer to an array. I’ll flesh that out later.

Is this line actually needed as written?

In one case those are identical, in the other those are different, so you can always take the different.

For those who don’t understand this line:
The conditional (ternary) operator is the only JavaScript operator that takes three operands: a condition followed by a question mark (? ), then an expression to execute if the condition is truthy followed by a colon (: ), and finally the expression to execute if the condition is falsy.

So if they are equal, then prev_i is the same as interval so then using interval is always correct.

Of course you are right - bit too close to the trees here. I have it down to:

export var phase, prev_i
function smoothTime(interval) {
  /*
  time() function to smooth out changes in interval
  prev_i is previous interval
  */
  p1 = time(prev_i)
  p2 = time(interval)
  prev_i = interval
  phase += p1 - p2
  return (p2 + phase) % 1
}

Might need some testing just to make sure phase doesn’t become some huge number or something. so far it works with my numbers.

1 Like

Question:
How does synchronizing time() between PB’s work? ie will the phase offset between smoothTime() and time() matter?

They will both be changing at the same rate, but would have different values due to the phase offset applied to smoothTime().

I was tinkering with a version where the phase value is reduced to 0 over a short span of time, but I’m not sure it’s necessary.

Answer: @wizard described the process here in this forum comment.

So, if you want to have the same values reported by multiple PB’s for smoothTime() would you need to have phase reduce to 0 over time?

I came up with this:

export var phase, prev_i
function smoothTime(interval, delta) {
  /*
  time() function to smooth out changes in interval
  prev_i is previous interval
  delta is optional, if passed, will reduce phase offset to 0 after a few seconds
  */
  var smooth = 0.0002 * delta //can't be smaller than 0.0001
  phase += (phase>smooth) ? -smooth : (phase<-smooth) ? smooth : -phase
  var p1 = time(prev_i), p2 = time(interval)
  prev_i = interval
  return (p2 + (phase += (p1 - p2))) % 1
}

export function beforeRender(delta) {
  t1 = smoothTime(speed, delta)
}

If you don’t pass delta, smoothTime() works as normal, however if you pass delta, then smoothTime() reduces the phase offset to 0 in some period of time depending on the frame rate.

Or is this a pointless complication…

Not pointless. I can imagine cases where adjusting the slider causes “smart behavior” in it’s change. What that behavior needs to be depends greatly on the context.

It’s a case for easing.

But which easing formula we use depends on the usage.
Here’s a bundle of handy formulas, all of which are easily replaceable.

So making the phase shift reduce via easing, with the actual change being a function would allow this to be infinitely flexible.

Easing formulas above uses these variables:

t: current time (this is based on delta, but accumulated, relative to total duration, in a 0…1 range)
b: beginning value (this is our current/starting phase value)
c: change in value (this is always zero phase, in this use case, as we want to reduce phase smoothly)
d: duration (this is how fast we want to return to zero)

Added:
Excellent Walk thru and simplify of the above for specific cases Improved Easing Functions

Perhaps I can ask you to help with the easing functions, as you seem to have a much better grasp of it than me. How would you work out the t value from delta in the range 0-1?

I did find one bug. If phase is negative (which it can easily be), then we return a negative time value - which is not what we want! So I added a suitably large constant offset value, to ensure the returned value is always in the range 0-1.

var phase, prev_i
function smoothTime(interval, delta) {
  /*
  time() function to smooth out changes in interval
  prev_i is previous interval
  delta is optional, if passed, will reduce phase offset to 0 after a few seconds
  */
  var smooth = 0.0002 * delta //can't be smaller than 0.0001
  phase += (phase>smooth) ? -smooth : (phase<-smooth) ? smooth : -phase
  var p1 = time(prev_i), p2 = time(interval)
  prev_i = interval
  return (p2 + (phase += (p1 - p2)) + 255) % 1
}

This seems to work with all the patterns I have tried it with so far - admittedly not many, and only 1D patterns.

Let’s say we want things to ease within 30 seconds… (Could be faster or slower, but picking a number helps). Call that duration d

So if we get delta, we start accumulation. So push it into a “total time”: tt += delta
If we find the ratio of tt to d, that’s how far into the duration we’ve gone… So let’s say delta is 300 milliseconds or .003 seconds. So we do the math of tt/d or .003/30, that’s t in 0…1
Next round, we get delta and add it again, and now it’s (for instance .003 again delta) so now tt/d aka t is .006/30 etc etc …
(If t > 1, then we are done, we’ve synced or should have. Phase should be zero)

Clear?

Got it, I was missing the duration part.

1 Like

Found a wonderful and simple page about easings…

I’ll convert these into a quick library for PB, as I can see lots of potential uses. Any 0…1 can be ‘eased’ in so many ways, so it could be brightness, hue, movement, or more.

reminder to myself to revisit this and add the best option(s) to the slider example code I just did here