Smooth Speed Slider?

This is my speed slider:

export function sliderSpeed(_v) {
  fps = .01 + (3.99 * _v)
  // When slider is full left, _v == 0, speed is .01. 
  // When slider is full right, _v == 1, speed is 4.
}

When I use it, my pattern jumps all over the place, and ends up on a random colour as the starting point for the new speed.

I’m assuming this is because fps ends up at some new random number when the slider stops moving.

Is there any way to implement a speed control that doesn’t do this? so that I could speed up/slow down the pattern without it jumping all over?

Or would this be a new feature request - ie when the time base in time() is changed, it continues with the same number as previously, just increments it faster/slower?

Thanks.

So unsure that’s the slider’s fault…

I mean yes the time base is changing… So whatever value assigned by time(x) is changed because you’ve changed the X…

So how to smooth that out…

Example:

Slider populates “speed”

y = x // before the speed change... Record X
x = time(speed)  // X varies between 0 and 1... 

// Put a new value as a throttle...
// Throttle is 2%, let's say...
If ( abs(y-x) > ( .02 * y) ) {
     If ( (y-x) > 0) {
         y = y * .98;
      } else {
         y = y * 1.02;
     }
  } Else {
    Y = X
  }

Use Y as your value. It’ll track X, unless the change is sudden… then it’ll attempt to find a new sync. Ideally, you could make the throttle based on the speed variable… Then it would likely behave even better.

All code is off the top of my head. Errors may occur, my mind is buggy. When I find some time to sit with a PB, I’ll post real code. (For instance, to clamp Y to not go beyond 0…1). It also might depends on if it’s a sawtooth, a wave or what…
Good problem to ponder.

I’ve actually got a slightly better way now. Basically involving the delta of Y and X, and adjusting that slowly. Might actually use X Y and Z to do it right… I’ll put a demo up tomorrow.

Alternatively, you could throttle/adjust speed. I might try both and see which feel more responsive.

Hey @Nick_W

I believe ZRanger1 [edit: Nope! Wizard] made the pattern in the library called “Example: Smooth Speed Slider” which does this. It’s compact enough that I’ll include it here:

Example: Smooth Speed Slider
/*
This pattern is based on the default scrolling rainbow and has a slider to control speed.
It uses an accumulation based on delta instead of time() so that changes in speed do not cause the animation to jump.
The downsides is slightly more code, and this pattern won't be synchronized with other Pixelblazes through firestorm.
*/

var speedRange = 1/1000 // this scales the milliseconds back to a usable range. shown here, the max rate is 1Hz
var speed = speedRange // controlled by slider

export function sliderSpeed(s) {
  speed = s*s * speedRange // square it to give better control at lower values, then scale it
}

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

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

Here’s another approach I use often, which is what I think Scruffy was going to narrow in on with the “delta of” concept:

Example: Time Offsets
var duration = 2, t1Offset = time(duration / 65.535)

export function sliderSpeed(_v) {
  var oldDuration = duration
  duration = 10 / (1 + 9 * _v)  // Period from 1 to 10 seconds
  t1Offset += time(oldDuration / 65.535) - time(duration / 65.535) + 1
  t1Offset %= 1
} 

export function beforeRender(delta) {
  t1 = (time(duration / 65.535) + t1Offset + 1) % 1
}

export function render(index) {
  hsv(0, 1, index == floor(t1 * pixelCount))
}

actually, my original idea had some issues (but I like the above solution), but when I saw @zranger1 's solution, which didn’t use time() at all, I realized there was a solution which did, and doesn’t need the delta:

export var speed, x, y, z
export function sliderSpeed(v) {
  speed = v*32700
}

export function beforeRender() {
  x = time(speed)  // pulled out only for debug
  y = (x + y)%1 // replace x with time(speed) directly for less variables
  z = wave(y)
  // 
}

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

This works with wave(), triangle(), and anything else you want to use.
Kudos to @zranger for the idea to use mod and thus wrap, which makes it behave as a 0…1 (which means you can treat it like a time() itself).

Sliding the slider doesn’t cause any jump (unless you slide it to very fast/small, which could be limited in the slider)

I still think there is a solution where we can make it ‘accelerate’ (or decelerate) from speed A to speed B smoothly, but that’s for another day.

I just added a version of the above to the matrix arrayless KITT, to stop it from jumping around when you adjust the speed slider, and it’s absolutely an improvement.

Thanks @Nick_W for asking about this!

1 Like

I can’t take credit for this excellent idea – I’m pretty sure it was @wizard who posted the Smooth Speed Slider example.

1 Like

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