Reputation: 4346
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