I am using these libraries
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
I am making an animation that I try to synchronize with a progress bar. My animation would last a total of 15 seconds, and would have more elements.
For this example, I am making an animation where I have a rectangle and a circle.
var rectangle = vis.append("rect")
var circle=vis.append("circle");
For each element I define its animation properties in a json specifying a starting point (.begin) and an end point (.destiny), also a delay (.delay) then the transition is generated with the definition of those properties for each of them. an example of this json is this:
circleValues={
"delay":4000,
"duration":3000,
"begin": {
"cx": 250,
"r":20,
"fill": "blue"
},
"destiny": {
"cx": 0,
"fill": "orane"
}
}
So for the case of the circle, I will generate an animation that will start with these properties:
//circleValues.begin
"begin": {
"cx": 250,
"r":20,
"fill": "blue"
}
and a transition will be generated where in the end it will end with these properties:
//circleValues.destiny
"destiny": {
"cx": 0,
"fill": "orange"
}
The total animation will be in the course of 15 seconds, in the case of the circle and according to the properties that I defined, it means that it trasntion will start in 4 seconds, for that it will have a delay of 4000 (circleValues.delay) and will last (circleValues.duration) seconds.
Then I have a function called initAnimation() where I execute this transition for each element in the style:
function initAnimation(percentage){
//circle properties
circle.styles(circleValues.begin)
.transition()
.delay(circleValues.delay)
.duration(circleValues.duration)
.ease(d3.easeLinear())
.styles(circleValues.destiny)
}
I want to control the animation using a progress bar in this case a progressbar that I define using:
//15 seconds, min value is 0 seconds, max value is 15 seconds, step=1
<input id="progressbar" type="range" min="0" max="15" step="1" value="0" oninput="setPercentValue(+event.target.value)">
and I make the progress bar automatically move every second with the following code, I also have 2 buttons to stop animation and resume animation.
var duration=15000; //15 seconds duration animation
var second=0; // current value of progress var
var interval; // instance of setInterval
function pause(){
clearInterval(interval);
}
function play(){
moveProgressBar();
}
function moveProgressBar(){
interval= setInterval(()=>{
console.log(second);
second++;
document.getElementById("progressbar").value=second;
if(second==15){
clearInterval(interval);
}
},1000)
}
and I execute all my code with the invocation of these 2 lines.
initAnimation(0);
moveProgressBar();
The problem is that I don't know how to make it so that according to the selected time of my progress bar, it corresponds to the current moment in which each element of my animation should be. I would like that depending on the value you select when moving the progress bar, it corresponds to the behaviors that I defined for each of my elements, even if I pause or resume the animation. This is beyond my knowledge at d3.js.
How can I do it?
d3.select("#visualization").append('svg');
var vis = d3.select("svg").attr("width", 800).attr("height", 150).style("border", "1px solid red");
var duration=15000; //15 seconds duration animation
var second=0; // current value of progress var
var interval; // instance of setInterval
function pause(){
clearInterval(interval);
}
function play(){
moveProgressBar();
}
function moveProgressBar(){
interval= setInterval(()=>{
second++;
document.getElementById("progressbar").value=second;
if(second==15){
clearInterval(interval);
}
},1000)
}
function setPercentValue(percentage) {
second=percentage;
rectangle.interrupt();
circle.interrupt();
initAnimation(percentage);
}
var rectValues={
"delay":2000,
"duration":5000,
"begin": {
"x": 0,
"y": 0,
"height": 70,
"width": 100,
"opacity": 1,
"fill": "red"
},
"destiny": {
"x": 250,
"y": 1,
"height": 100,
"width": 120,
"opacity": 0.8,
"fill": "green"
}
}
var rectangle = vis.append("rect")
var circle=vis.append("circle");
var circleValues={
"delay":4000,
"duration":3000,
"begin": {
"cx": 250,
"r":20,
"fill": "blue"
},
"destiny": {
"cx": 0,
"fill": "orange"
}
}
function initAnimation(percentage){
//rectangle properties
rectangle.styles(rectValues.begin)
.transition()
.delay(rectValues.delay)
.duration(rectValues.duration)
.ease(t => d3.easeLinear(percentage + t * (1 - percentage)))
.styles(rectValues.destiny)
//circle properties
circle.styles(circleValues.begin)
.transition()
.delay(circleValues.delay)
.duration(circleValues.duration)
.ease(t => d3.easeLinear(percentage + t * (1 - percentage)))
.styles(circleValues.destiny)
}
initAnimation(0);
moveProgressBar();
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
<div id="visualization"></div>
<!--<input type="range" min="0" max="1" step="0.1" value="0" oninput="setPercentValue(+event.target.value)">-->
<input id="progressbar" type="range" min="0" max="15" step="1" value="0" oninput="setPercentValue(+event.target.value)">
<button onclick="pause()">
pause
</button>
<button onclick="play()">
play
</button>
As you've specified in your post, your animations are composed of a delay followed by a transition.
If you want to display the result of such animation at some fixed time T (as determined by the progressbar value), you need to evaluate the following conditions:
begin values and you only need to make the delay shorterbegin and destiny), also the transition duration needs to be shorteneddestiny valuesThis logic is encapsulated in the function interpolateValuesAtTime below. This function, in addition to calculating new delay,duration,begin and destiny values, it also creates a new easing function, which calculates how the easing should continue from time T onward (details about how the modified easing functions works are described in detail on this page)
const progressbar = document.getElementById('progressbar');
d3.select("#visualization").append('svg');
const vis = d3.select("svg").attr("width", 800).attr("height", 150).style("border", "1px solid red");
const rectangle = vis.append("rect")
const circle = vis.append("circle");
let second = 0;
let interval;
const rectValues = {
"delay": 2000,
"duration": 5000,
"begin": {
"x": 0,
"y": 0,
"height": 70,
"width": 100,
"opacity": 1,
"fill": "red"
},
"destiny": {
"x": 250,
"y": 1,
"height": 100,
"width": 120,
"opacity": 0.8,
"fill": "green"
}
};
const circleValues = {
"delay": 4000,
"duration": 3000,
"begin": {
"cx": 250,
"r": 20,
"fill": "blue"
},
"destiny": {
"cx": 0,
"fill": "orange"
}
}
function resumedEasingFunction(easingFunction, progress) {
return function (xAfterResume) {
const xOriginal = d3.scaleLinear()
.domain([0, 1])
.range([progress, 1])
(xAfterResume);
return d3.scaleLinear()
.domain([easingFunction(progress), 1])
.range([0, 1])
(easingFunction(xOriginal));
};
}
function interpolateValuesAtTime(values, easingFunction, time) {
const interpolatedValues = JSON.parse(JSON.stringify(values));;
let progress;
if (values.delay >= time) {
//the initial delay has not yet elapsed
interpolatedValues.delay = values.delay - time;
progress = 0;
} else if (values.delay + values.duration >= time) {
//the animation is running
interpolatedValues.delay = 0;
interpolatedValues.duration = values.delay + values.duration - time;
progress = (values.duration - interpolatedValues.duration) / values.duration;
for (let key in values.begin) {
const startValue = values.begin[key];
if (key in values.destiny) {
const endValue = values.destiny[key];
const interpolator = d3.interpolate(startValue, endValue);
interpolatedValues.begin[key] = interpolator(progress);
}
}
} else {
//the animation has already ended
interpolatedValues.delay = 0;
interpolatedValues.duration = 0;
progress = 1;
for (let key in values.destiny) {
interpolatedValues.begin[key] = values.destiny[key];
}
}
interpolatedValues.easingFunction = resumedEasingFunction(easingFunction, progress);
return interpolatedValues;
}
function play() {
startAnimation(progressbar.value * 1000);
startMovingProgressbar();
}
function pause() {
stopMovingProgressBar();
pauseAnimation();
}
function updateTimeBasedOnProgressbar() {
stopMovingProgressBar();
second = progressbar.value;
startAnimation(1000 * second);
pause();
}
function stopMovingProgressBar(){
clearInterval(interval);
}
function startMovingProgressbar() {
clearInterval(interval);
interval = setInterval(() => {
second++;
progressbar.value = second;
if (second == 15) {
clearInterval(interval);
}
}, 1000)
}
function pauseAnimation() {
pauseShape(rectangle);
pauseShape(circle);
}
function pauseShape(shape) {
shape.transition()
.duration(0)
}
function startAnimation(miliseconds) {
//rectangle properties
startShapeAnimation(rectangle, rectValues, miliseconds);
//circle properties
startShapeAnimation(circle, circleValues, miliseconds);
}
function startShapeAnimation(shape, shapeValues, miliseconds) {
const valuesAtTime = interpolateValuesAtTime(shapeValues, d3.easeLinear, miliseconds);
shape.attrs(valuesAtTime.begin)
.transition()
.duration(0)
.attrs(valuesAtTime.begin)
.transition()
.delay(valuesAtTime.delay)
.duration(valuesAtTime.duration)
.ease(valuesAtTime.easingFunction)
.attrs(valuesAtTime.destiny);
}
updateTimeBasedOnProgressbar();
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
</head>
<body>
<div id="visualization"></div>
<input id="progressbar" type="range" min="0" max="15" step="1" value="0" oninput="updateTimeBasedOnProgressbar()">
<button onclick="pause()">pause</button>
<button onclick="play()">play</button>
</script>
</body>
</html>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With