smpa01
smpa01

Reputation: 4346

Custom easing in d3

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

 const width = 1536;
  const height = 720;
  const svgns = "http://www.w3.org/2000/svg";
  const svg = d3.select("svg");

  /*create base svg*/
  svg.attr("xmlns", svgns).attr("viewBox", `0 0 ${width} ${height}`);

  /*create style*/
  const style = d3.select("body").append("style").attr("type", "text/css");

  //cubic-bezier(x1, y1, x2, y2) ->x1 and x2 must be between 0 and 1; with negative x1 and x2 provided css reverts to default->0.25, 0.1, 0.25, 1.0
  //cubic-bezier --please change the value here to move red and green rect with the exact same velocity
  //default-0.42, 0, 1, 1
  //easeInSine - 0.12, 0, 0.39, 0; easeOutSine-0.61, 1, 0.88, 1; easeInOutSine-0.37, 0, 0.63, 1 etc.
  //0.61, -0.25, 0.88, 1; 0.61, -0.15, 0.88, -1.25 etc.
  const cubicBezCurvVal = "0.42, 0, 1, 1";

  /*background rect*/
  svg
    .append("rect")
    .attr("class", "vBoxRect")
    .attr("width", `${width}`)
    .attr("height", `${height}`)
    .attr("fill", "#EFEFEF")
    .attr("stroke", "black");

  /*red rect - css animation+animation timing=> explicit cubic-bezier value*/
  svg
    .append("rect")
    .attr("class", "red")
    .attr("x", "0")
    .attr("y", `${height / 2 - 40}`)
    .attr("height", "50")
    .attr("width", "50")
    .attr("fill", "red")
    .style("--dist", `${width - 50 + "px"}`);

  svg
    .append("line")
    .attr("class", "path1")
    .attr("x1", "0")
    .attr("y1", () => {
      return document.querySelector(".red").y.baseVal.value;
    })
    .attr("x2", `${width}`)
    .attr("y2", () => {
      return document.querySelector(".red").y.baseVal.value;
    })
    .attr("stroke", "red")
    .attr("stroke-dasharray", "10");

  const keyFrame1 = `
    .red {
        transform-box: fill-box;
        animation-name: move1;
        animation-duration: 5s;
        animation-iteration-count: 1;
        animation-timing-function: cubic-bezier(${cubicBezCurvVal});
        animation-direction: normal;
        animation-fill-mode: both;
    }
    
    @keyframes move1 {
        0% {
            transform: translateX(0px);
        }
        100% {
            transform: translateX(var(--dist));
        }
    }
    `;
  style["_groups"][0][0].innerHTML = keyFrame1;

  /*green rect - css animation+animation timing=> calculated progress of cubic-bezier value with javascript*/

  svg
    .append("rect")
    .attr("class", "green")
    .attr("x", "0")
    .attr("y", `${height / 2 + 40}`)
    .attr("height", "50")
    .attr("width", "50")
    .attr("fill", "green")
    .style("--dist", `${width - 50 + "px"}`);

  svg
    .append("line")
    .attr("class", "path2")
    .attr("x1", "0")
    .attr("y1", () => {
      return document.querySelector(".green").y.baseVal.value;
    })
    .attr("x2", `${width}`)
    .attr("y2", () => {
      return document.querySelector(".green").y.baseVal.value;
    })
    .attr("stroke", "green")
    .attr("stroke-dasharray", "10");

  /*credit - https://blog.maximeheckel.com/posts/cubic-bezier-from-math-to-motion/*/

  // 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);
  });
  //generate keyFrame string
  var str = "@keyframes move{";

  for (let i = 0; i < time.length; i++) {
    var styleStr = `${time[i] * 100}%{ transform:translateX(${
      progress[i]
    }px);}`;
    str += styleStr;
  }

  const keyFrame2 = `.green {
        transform-box: fill-box;        
        animation-name: move;
        animation-duration: 5s;        
        animation-iteration-count: 1; 
        animation-timing-function: linear;       
        animation-direction: normal;
        animation-fill-mode: both;
    }
    ${str}}
    `;

  style["_groups"][0][0].innerHTML += keyFrame2;

  /*blue rect for d3*/

  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;
  }

  svg
    .append("rect")
    .attr("class", "blue")
    .attr("x", "0")
    .attr("y", `${height / 2 + 120}`)
    .attr("height", "50")
    .attr("width", "50")
    .attr("fill", "blue")
    .each(function (d, i) {
      d3.select(this)
        .transition()
        .duration(5000)
        // .ease(progress1(d))
        .attr("x", `${width - 50}`);
    });

  svg
    .append("line")
    .attr("class", "path3")
    .attr("x1", "0")
    .attr("y1", () => {
      return document.querySelector(".blue").y.baseVal.value;
    })
    .attr("x2", `${width}`)
    .attr("y2", () => {
      return document.querySelector(".blue").y.baseVal.value;
    })
    .attr("stroke", "blue")
    .attr("stroke-dasharray", "10");
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<script type="text/javascript" src="https://d3js.org/d3.v7.min.js"></script>

<body>  
    <svg>
               
    </svg>
</body>
<script src="prod.js"></script>

</html>

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.

// 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 this and came up with following 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;

Upvotes: 1

Views: 172

Answers (0)

Related Questions