Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Controls an animation using a progress bar

Tags:

d3.js

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>
like image 666
yavg Avatar asked Dec 10 '25 01:12

yavg


1 Answers

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:

  1. if T is less than the delay, then you are still waiting for the transition to start. The shape that you're animating still has all attributes set to the begin values and you only need to make the delay shorter
  2. if T is more than the delay, but less then delay+transition duration, then the shape is already in the transition phase. In this case, you need to determine how far in translation you are at time T and interpolate the attribute values (so the values will be something between begin and destiny), also the transition duration needs to be shortened
  3. if T is more than the delay, then the transition is already completed. In this case, the attribute values will be set to destiny values

This 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)


You can test it in the snippet below.

Here are few notes regarding its functionality:
  • after clicking on the progressbar, the animation will be shown at given time (the animation will be paused, press play to start it)
  • also, after pressing play, the animation starts from the time corresponding to the progress bar (and since the progress bar counts only whole seconds, that implies that if you pause & play the animation, it may seem to jump back a little)

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>
like image 99
mcernak Avatar answered Dec 12 '25 06:12

mcernak



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!