Calculating Rate of Change for Interpolation at a given ease

I am trying to generate a speed graph of a d3.tween for a given ease. For example, if I have the following element

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1536 720">
<rect x="0" y="360" width="50" height="50" fill="blue"></rect>
</svg>

I can get a value graph (y at a given t) of a tween for the above like this.

const valueData = [];
const end = 100;
d3.select('rect')
    .transition()
    .duration(1000)
    .ease(d3.easeLinear)
    .tween('allTween', function() {
        const interpolate = d3.interpolateNumber(0, end);
        return function(t) {
            const val = interpolate(t);
            element.attr('x', `${val}`);
            valueData.push({ x: t, y: d3.easeLinear(t) * end });
        }
    })

But I was wondering if it is possible to calculate the rate of change (speed graph). I tried like this which did not work. If the calculation works out perfectly, I will have a constant rate of change of linearEase only for each t.

const valueData = [];
const speedData = [];
const start = 0;
const end = 100;
d3.select('rect')
    .transition()
    .duration(1000)
    .ease(d3.easeLinear)
    .tween('allTween', function() {
        const interpolate = d3.interpolateNumber(start, end);
        let prevVal = start;
        let prevTime = performance.now()
        return function(t) {
            const val = interpolate(t);
            const currentTime = performance.now();
            const speed = (val - prevVal) / (currentTime - prevTime) ;
            prevVal = val;
            prevTime = currentTime;   

            element.attr('x', `${val}`);
            valueData.push({ x: t, y: d3.easeLinear(t) * end });
            speedData.push({ x: t, y: speed });
        }
    })

@Fil
Thank you very much in advance.

Browser timings like performance.now() aren’t exact (on purpose). If you want to produce a representative derivative you’ll have to use the previous and current t as basis for your calculations.

Have a look at Eases On Edge / Fabian Iwand | Observable for an example.

You might also find some inspiration here:

@mootari thanks for this. Following your advice, I tried like this which still did not work. I must have done something wrong as the calculation produces varying rates of speed for linearEase

const valueData = [];
const speedData = [];
const start = 0;
const end = 100;
d3.select('rect')
    .transition()
    .duration(1000)
    .ease(d3.easeLinear)
    .tween('allTween', function() {
        const interpolate = d3.interpolateNumber(start, end);

        return function(t) {
            const val = interpolate(t);

            element.attr('x', `${val}`);
            valueData.push({ x: t, y: d3.easeLinear(t) * end });
            speedData.push({ x: t, y: d3.easeLinear(t) * end });

            speedData.forEach(
                (a, i, r) => {
                    if (i != 0) {
                        a.speed = (r[i].y - r[i - 1].y) / (r[i].x - r[i - 1].x)
                    }
                }
            )


            console.log(speedData.filter((a, i) => i != 0));
        }
    })

Can you share a link to your notebook?

here’s a notebook that shows an easing and the differences in t

Sorry @mootari it took me a while. My end goal is to visualize ease and to be able to see both value and speed. In After Effects, you can see both value and speed graph for any given AE ease and I am trying to find a way to see if it is possible to achieve the same for a given d3 ease. Since Ease makes or breaks an animation, I want to be able to see how the speed changes over time when I am assigning ease (now, that I can also assign CSS easings, thanks to @Fil ).

I have done something like this and here is a relevant notebook.

To obtain the speed data, I have rounded the derivative calculation and I am not 100% sure if that was the correct approach. But, without rounding, the derivative values were extremely weird. I used linear as a test case for derivative and the speed should be equally same for all t. Without rounding, I could not achieve it. If you can please let me know if rounding was the correct approach for derivative calculation, I should be good (I like to think that value calculations are okay and they probably match to this)

I’m still not sure why you need to calculate the derivative on the fly, but I would suggest something like the following for your demo:

values = {
  const values = [];
  const ease = d3.easeBounceInOut;  
  
  await d3.select("rect")
    .attr("x", 0)
    .transition()
    .attr("x", 100)
    .duration(1000)
    .ease(t => {
      const t2 = ease(t);
      values.push({x: t, y: t2});
      return t2;
    })
    .end();
  
  return values;
}

and then

Plot.plot({
  marks: [
    Plot.line(values, {x: "x", y: "y"}),
    Plot.line(
      values.map(({x, y}, i, arr) => ({x, y: y - arr[i - 1]?.y})),
     {x: "x", y: "y", stroke: "red"}
  ),
  ]
})

You may also want to give your HTML cell a name (e.g. “demo”) and then change your values cell to use

d3.select(demo).select("rect")

instead of

d3.select("rect")

Global selectors should be avoided in Observable notebooks.

Nice idea by the way to use a Voronoi diagram as a curvature comb in the Power BI example!

1 Like

Thanks, @mootari for looking into it.

In my opinion, the speed graph provides more insight into easing than the value graph. Therefore, I prioritize ensuring the accuracy of the speed graph. Let’s take the QuadOut easing function as an example. While the value graph depicts the change in position, the speed graph effectively illustrates the sharp deceleration of the animation property change over time, which aligns with the intended easing effect. Unless the author is familiar with the specific animation easing equation and constructs a chart accordingly beforehand, it is difficult to determine the intended easing effect before assigning the ease.

Regarding the implementation, I am not familiar with Plot, and since Observable is not my production environment, I prefer to utilize d3.js and a different production environment for my solution.

I’m not sure what you mean by that. You’re measuring during a live transition, with all the noise that is introduced by the browser. The shorter the transition, the fewer samples you have and the noisier the resulting data gets.

Maybe this is closer to what you have in mind:

data = d3.range(0, 1, 1/1000) // 1000 samples
  .map((t, i, arr) => ({ offset: t, value: d3.easeCubicInOut(t) }))
  .map((d, i, arr) => ({ ...d, change: !i ? 0 : (d.value - arr[i - 1].value) }))

The Plot part is irrelevant, I simply use it to visualize the data:

2 Likes

Here’s my take:


With regard to Mootari’s point that

This is not exclusive to Observable; rather, it’s quite generally the case that selectors of greater specificity will be more performant and less error prone. The need for this grows with the complexity of the environment.

More generally, Observable encourages good programming practices that will serve you well in other environments. You should not dismiss these practices because references on D3 written several years ago don’t use these techniques.

1 Like

@mcmcclur May thanks; after referring to your notebook, I can see that my work produces an identical speed grpah to yours. So coming back to my original question on this “Should I round the values of the derivatives of the live interpolation data” - The answer is probably yes, cause by doing this it generated an identical speedGraph to yours.

I will move on now to CSS Easing. I have longed for a speedGraph for so many years for easing (and I could not think of bringing one to life till I started with d3. d3 is super awesome, to say the least) and I like to think I am directionally correct with my approach to chart live interpolation data both for position and speed.

@mootari just so I understand, so you are asking me to perform this task with data that is not based on interpolation (as I have originally done. By “interpolation”, I meant data generated out of live transition). So, create static data first based on the easing function and chart based on that.

I feel we may be using different definitions of “interpolation”.

Your example samples values during a live transition. These samples aren’t evenly spaced because the transition steps don’t occur at perfectly even intervals. Unless your easings are somehow guided by factors that you don’t know beforehand, there is no benefit in doing it this way.

Instead you’d pick the sample size (e.g. 1000 as in my example), and then calculate the eased values, and from those calculate the rate of change for every sample.

Visualizing that rate of change gives you your speed graph, at a precision of your own choosing.

1 Like