Convert CSS cubic-bezier timing value to d3 custom ease

I am trying to figure out how can I create custom ease for d3 animation.

To elaborate, in CSS, animation-timing function is controlled by cubic-bezier value.
In my element, I have three rect- red, green, and blue.

red rect is moved with an explicit cubic-bezier value animation-timing-function: cubic-bezier(0.42, 0, 1, 1);.

For green rect I have explicitly calculated the keyFrame time% and progress using the following, given a cubic-bezier value. By doing this, I am explicitly telling the green rect to move as per the calculation. Also, by doing this, I can convert a CSS cubic-bezier value to the penner’s equation `(t,b,c,d) form.

 // create percentage container
  const pct = [];

  for (let i = 0; i <= 100; i++) {
    pct.push(i / 100);
  }

  //cubic-bezier
  //const cubicBezCurvVal = "0.42, 0, 1, 1"

  //split bezier curve value
  var cleanVal = cubicBezCurvVal.split(",");
  //clean space with map -retunrns new array with the function, original array unchnaged
  var cleanVal = cleanVal.map((x) => parseFloat(x.replace(/ /g, "")));

  //p0
  const p0 = {
    x: 0,
    y: 0
  };
  //p3
  const p3 = {
    x: 1,
    y: 1
  };
  //p1
  const p1 = {
    x: cleanVal[0],
    y: cleanVal[1]
  };
  //p2
  const p2 = {
    x: cleanVal[2],
    y: cleanVal[3]
  };

  const x0 = p0.x; //=0
  const y0 = p0.y; //=0

  const x1 = p1.x;
  const y1 = p1.y;

  const x2 = p2.x;
  const y2 = p2.y;

  const x3 = p3.x; //=1
  const y3 = p3.y; //=1

  /*given a time percentage, calculates the x-axis of the cubic bezier graph, i.e. time elpased% */
  const x = (t) =>
    Math.pow(1 - t, 3) * x0 +
    3 * Math.pow(1 - t, 2) * t * x1 +
    3 * (1 - t) * Math.pow(t, 2) * x2 +
    Math.pow(t, 3) * x3;

  /*given a time percentage, calculates the y-axis of the cubic bezier graph, i.e. progres% */
  const y = (t) =>
    Math.pow(1 - t, 3) * y0 +
    3 * Math.pow(1 - t, 2) * t * y1 +
    3 * (1 - t) * Math.pow(t, 2) * y2 +
    Math.pow(t, 3) * y3;

  //penner's easing equation p=f(t)
  const c = width - 50; // c of t,b,c,d of penner's equation
  const b = 0; // b of t,b,c,d of penner's equation

  //create container
  const time = []; //to collect values of x(t), i.e. time elapsed %
  const progress = []; //to collect values of y(t), i.e. progress %

  //get the time first --- goes into keyframe---not dependent on progress,i.e. y(t)
  pct.forEach((a) => {
    time.push(x(a));
  });

  //get the progress for each time --- goes into progress --- not dependent on time x(t)
  pct.forEach((a) => {
    progress.push(y(a) * c + b);
  });

I also have blue rect and I was wondering how can translate the same cubic-bezier value into a custom -ease function for d3?

I referred to custom bounce and tried this which did not work unfortunately.

function progress1(t) {
    //p0
    const p0 = {
      x: 0,
      y: 0
    };
    //p3
    const p3 = {
      x: 1,
      y: 1
    };
    //p1
    const p1 = {
      x: cleanVal[0],
      y: cleanVal[1]
    };
    //p2
    const p2 = {
      x: cleanVal[2],
      y: cleanVal[3]
    };

    const x0 = p0.x; ///0
    const y0 = p0.y; ///0

    const x1 = p1.x;
    const y1 = p1.y;

    const x2 = p2.x;
    const y2 = p2.y;

    const x3 = p3.x; ////1
    const y3 = p3.y; ////1

    const progress =
      Math.pow(1 - t, 3) * y0 +
      3 * Math.pow(1 - t, 2) * t * y1 +
      3 * (1 - t) * Math.pow(t, 2) * y2 +
      Math.pow(t, 3) * y3;

    return t >= 1 ? 1 : progress;
  }

If you take a closer look at the end of your progress1() function you’ll notice that const progress does not reference any of the x values.

There is a rather popular JS library, bezier-easing, which provides precisely what you are looking for. We can import it like this:

bezierEasing = require('bezier-easing@2.1.0/dist/bezier-easing.min.js')

and create a custom easing function:

easeBezier = bezierEasing(0.42, 0, 1, 1)

To verify that this works as intended I suggest that you plot the values, for example with:

Plot.line(
  d3.range(0, 1, .01).map(t => ({x: t, y: easeBezier(t)})),
  {x: 'x', y: 'y'}
).plot()

which produces

If you need more complex easings without wanting to use multistep transitions, you might also be interested in

It provides a helper to rescale time offsets. I realize the documentation is sparse, so if you have any questions don’t hesitate to ask!

@mootari thanks for this, how can I use this library in d3.

Yes, I understood that and I thought I was passing at y=f(x) but that is not the case. The way I am passing it on CSS, is a parametric form where both x and y are functions of t (i.e. x=f(t) and y=f(t)) of cubic-bezier curve whereas I probably need an equation of cubic-bezier in explicit form where y=f(x)…

Also, I don’t think (I could be wrong) if it gets resolved, it can’t be done using d3.transition, rather it needs to use d3.attrTween. I came across this.

So my best guess is, as long as an explicit form of cubic-bezier can be utilized, it might be worth taking a shot by using attrTween with a custom interpolator fed with explicit equation.

You import the offset helper:

import {weightedOffset} from 'https://observablehq.com/@mootari/stitched-easings'

then create your own easing function. Here is an (unoptimized) example that you can experiment with:

function myEase(t) {
  const mix = (a, b, t) => a * (1 - t) + b * t;
  const steps = [
    // We use mix() here to scale and offset each ease vertically.
    [.3, t => mix(0, .5, d3.easeCubicIn(t))],
    [.7, t => mix(.5, 1, d3.easeBounceInOut(t))],
  ];
  const weights = steps.map(s => s[0]);
  const eases = steps.map(s => s[1]);
  
  const [i, tLocal] = weightedOffset(t, weights);
  return eases[i](tLocal);
}

If you plot it:

Plot.line(
  d3.range(0, 1, .01).map(t => ({t, v: myEase(t)})),
  {x: 't', y: 'v'}
).plot({y: {domain: [0, 1]}})

it produces the following output:

That sounds reasonable to me, although I don’t have much experience with either the evaluation of bezier curves or d3-transition.

Note that you can find the documentation on GitHub and Observable.

An alternative implementation of this easing is available at Cubic Bezier Easing (#3) / D3 / Observable — it could be merged into d3-ease (with a bit of clean-up work).

2 Likes

That would be amazing

This is super awesome @Fil ; I tried out with different css easing; produces identical results to different css easing. Can’t thank you enough.