Animation showing on the progress bar is not correctly halted

I am encountering a problem with handling an animation of a progress bar in my code. The issue arises when I pause the animation, as it seems to slightly backtrack instead of pausing at the correct position upon clicking a button. Can someone help identify what's wrong in my code?

It's important to note that this is just a simplified version and I prefer not to mess with class removal since in the actual code, I need to restart the animation under certain conditions.

const { useState, useEffect } = React;


const App = () => {
  const totalTime = 10;
  const [timeLeft, setTimeLeft] = useState(totalTime);
  const [isTimerAnimated, setIsTimerAnimated] = useState(true);

  useEffect(() => {
    const interval = setInterval(() => {
      if (timeLeft > 0 && isTimerAnimated) {
        setTimeLeft((prevTimeLeft) => prevTimeLeft - 1);
      }
    }, 1000);

    return () => clearInterval(interval);
  }, [timeLeft, isTimerAnimated]);

 const handleAnimationState = () => {
    if (isTimerAnimated) {
      setIsTimerAnimated(false);
    } else {
      setIsTimerAnimated(true);
      setTimeLeft(totalTime);
    }
  };

  return (
    <div>
      <div className="progress-bar">
        <div
          className={`${isTimerAnimated ? "animated" : ""}`}
          style={{
            transform: `scaleX(${timeLeft ? timeLeft / totalTime : 0})`,
            animationDuration: `${totalTime}s`,
          }}
        />
      </div>
      <button onClick={handleAnimationState}>{isTimerAnimated ? "Pause the animation" : "Restart the animation"}</button>
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));
@keyframes timerAnimation {
  from {
    transform: scaleX(1);
  }
  to {
    transform: scaleX(0);
  }
}

.progress-bar {
  height: 10px;
  width: 300px;
  background-color: #fdb913;
  border-radius: 10px;
  overflow: hidden;
  margin-top: 2rem;
  margin-bottom: 3rem;
}

.progress-bar div {
  background-color: grey;
  height: 100%;
  animation-play-state: paused;
  animation-fill-mode: forwards;
  transform-origin: left center;
}

.progress-bar div.animated {
  animation: timerAnimation linear;
  animation-play-state: running;
}
<div id="root">
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js"></script>

</div>

Answer №1

This code snippet showcases a vanilla JS implementation of requestAnimationFrame to demonstrate its usage in creating a specific animation effect. The browser works towards synchronizing animations with the display refresh rate, as explained in detail on the following link: https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame

class AnimatedBar {
  constructor(elem, totalTime){
    this.elem = elem
    this.progressState = 100
    this.currentTime = totalTime
    this.totalTime = totalTime
    this.lastTimeStamp = null
    this.playing = false
    this.animationFrameRequest = null
    
    PlayButton.onclick = () => this.play()
    RestartButton.onclick = () => this.restart()
  }
  pause(){
    window.cancelAnimationFrame(this.animationFrameRequest)
    this.playing = false
    this.lastTimeStamp = null
    PlayButton.textContent = 'Play'    
    PlayButton.onclick = () => this.play()
  }
  play(){
    if(this.lastTimeStamp){
      //only change animation state if there is a previous timestamp to go from
      const elapsedTime = Date.now() - this.lastTimeStamp
      this.currentTime -= elapsedTime
      
      if(this.currentTime <= 0){
        this.currentTime = 0
        this.playing = false
        this.lastTimeStamp = null
        
        PlayButton.textContent = 'Restart'        
        PlayButton.onclick = () => this.restart()
      }
      
      this.progressState = (this.currentTime/this.totalTime) * 100
      
      this.elem.style.width = `${this.progressState}%`
    } else {    
      this.playing = true
      PlayButton.textContent = 'Pause'
      PlayButton.onclick = () => this.pause()
    }
    
    this.lastTimeStamp = Date.now()
    
    if(this.playing){
      this.animationFrameRequest = window.requestAnimationFrame(() => this.play())
    }
  }
  restart(){
    this.pause()
    this.currentTime = this.totalTime
    this.play()
  }
}


new AnimatedBar(ProgressBar, 10000)
<div style="width: 100px; height: 16px; background: grey; border-radius: 8px; overflow: hidden; margin-bottom: 8px;">
  <div id="ProgressBar" style="width: 100%; height: 100%; background: yellow;"></div>
</div>

<button id="PlayButton">Start</button>
<button id="RestartButton">Restart</button>

Answer №2

To improve the responsiveness of your progress bar, I made two key changes to the code. First, I adjusted the timeLeft state to track milliseconds (totalTime * 1000) instead of seconds. Second, I modified the interval to run every 100 milliseconds (setInterval callback) instead of every second.

const { useState, useEffect } = React;

const App = () => {
  const totalTime = 10; // Total time in seconds
  const [timeLeft, setTimeLeft] = useState(totalTime * 1000);
  const [isTimerAnimated, setIsTimerAnimated] = useState(true);
  const [progress, setProgress] = useState(1);

  useEffect(() => {
    let interval;
    if (isTimerAnimated && timeLeft > 0) {
      interval = setInterval(() => {
        setTimeLeft(prevTimeLeft => prevTimeLeft - 100);
        setProgress(timeLeft / (totalTime * 1000));
      }, 100);
    } else if (timeLeft <= 0) {
      setIsTimerAnimated(false);
    }
    return () => clearInterval(interval);
  }, [timeLeft, isTimerAnimated]);

  const handleAnimationState = () => {
    if (isTimerAnimated) {
      setIsTimerAnimated(false);
    } else {
      setIsTimerAnimated(true);
      setTimeLeft(totalTime * 1000);
      setProgress(1);
    }
  };

  return React.createElement("div", {},
    React.createElement("div", {className: "progress-bar"},
      React.createElement("div", {
        className: "progress",
        style: {
          transform: `scaleX(${progress})`,
        }
      })
    ),
    React.createElement("button", {onClick: handleAnimationState},
      isTimerAnimated ? "Pause the animation" : "Restart the animation"
    )
  );
};

ReactDOM.render(React.createElement(App), document.getElementById("root"));
.progress-bar {
      height: 10px;
      width: 300px;
      background-color: #fdb913;
      border-radius: 10px;
      overflow: hidden;
      margin-top: 2rem;
      margin-bottom: 3rem;
    }

    .progress-bar div {
      background-color: grey;
      height: 100%;
      transform-origin: left center;
    }
<div id="root"></div>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js"></script>

Answer №3

If you want to avoid scaling and work with the width instead, ensuring it stays between 0 and 100, try this approach.

Measure time in milliseconds for smaller progress changes rather than sudden steps. Then relate your millisecond time to the progress percentage using this formula:

y(progress) = (x(ms elapsed) / totalTime(s) * 1000 ) * 100

const { useState, useEffect } = React;

const App = () => {
  const totalTime = 4;
  const [progress, setProgress] = useState(0);
  const [elapsedMS, setElapsedMS] = useState(0);
  const [isPaused, setIsPaused] = useState(false)
  
  useEffect(() => {
    setInterval(() => {
      if(!isPaused) {
         setElapsedMS(p => p + 1)
      }
    }, 1)
  }, [])


  useEffect(() => {
     if(isPaused) setProgress((elapsedMS / (totalTime * 1000)) * 100)
  }, [elapsedMS])

  const handleAnimationState = () => {
    setIsPaused(p => !p)
  }

  

return (
     <div>
        <div className="progress-bar">
           <div
             className={`${isPaused ? "animated" : ""}`}
             style={{
                width: `${100 - progress}%`,
             }}
           />
        </div>
        <button onClick={handleAnimationState}>{isPaused ? "Pause the animation" : "Restart the animation"}</button>
    </div>
  );

};

ReactDOM.render(<App />, document.getElementById("root"));
@keyframes timerAnimation {
  from {
    transform: scaleX(1);
  }
  to {
    transform: scaleX(0);
  }
}

.progress-bar {
  height: 10px;
  width: 300px;
  background-color: #fdb913;
  border-radius: 10px;
  overflow: hidden;
  margin-top: 2rem;
  margin-bottom: 3rem;
}

.progress-bar div {
  background-color: grey;
  height: 100%;
  animation-play-state: paused;
  animation-fill-mode: forwards;
  transform-origin: left center;
}

.progress-bar div.animated {
  animation: timerAnimation linear;
  animation-play-state: running;
}
<div id="root">
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js"></script>

</div>

Answer №4

import React from "react";
import { useState, useEffect, useRef } from "react";
import "./style.css";

const App = () => {
  const totalTime = 10;
  const [timeLeft, setTimeLeft] = useState(totalTime);
  const [isTimerAnimated, setIsTimerAnimated] = useState(true);
  const barRef = useRef(null);

  useEffect(() => {
    const interval = setInterval(() => {
      if (timeLeft > 0 && isTimerAnimated) {
        setTimeLeft((prevTimeLeft) => prevTimeLeft - 1);
      }
    }, 1000);

    return () => clearInterval(interval);
  }, [timeLeft, isTimerAnimated]);

  useEffect(() => {
    if (barRef.current) {
      barRef.current.animate(
        [
          { width: '100%' },
          { width: '0%' }
        ],
        {
          duration: totalTime * 1000,
          fill: 'forwards'
        }
      );
    }
  }, [barRef, totalTime]);

  const handleAnimationState = () => {
    if (isTimerAnimated) {
      barRef.current.getAnimations().forEach(animation => animation.pause());
    } else {
      setTimeLeft(totalTime);
      barRef.current.getAnimations().forEach(animation => animation.play());
    }
    setIsTimerAnimated(!isTimerAnimated);
  };

  return (
    <div>
      <div className="progress-bar">
        <div
          ref={barRef}
          className={`${isTimerAnimated ? "animated" : ""}`}
        />
      </div>
      <button onClick={handleAnimationState}>{isTimerAnimated ? "Pause the animation" : "Restart the animation"}</button>
    </div>
  );
};

export default App;
.progress-bar {
  height: 10px;
  width: 300px;
  background-color: #fdb913;
  border-radius: 10px;
  overflow: hidden;
  margin-top: 2rem;
  margin-bottom: 3rem;
}

.progress-bar div {
  background-color: grey;
  height: 100%;
}

.progress-bar div.animated {
  transition: width 1s linear;
}

The challenge here lies in synchronizing the JavaScript interval with the CSS transition for a seamless animation experience.

Modifications made to the code:

  1. Utilization of useRef() instead of useState() to manage play/pause functionality for the animation, ensuring smooth transitions and synchronization between JS interval and CSS.

  2. [{ width: '100%' }, { width: '0%' }] define keyframes for the animation, transitioning element width from 100% to 0%.

  3. { duration: totalTime * 1000, fill: 'forwards' } specify animation settings: totalTime duration in milliseconds and maintaining final style post-animation completion ({ width: '0%' }).

Explore the Web Animations API further at MDN documentation

Hoping this clarifies things! For more insights, don't hesitate to inquire.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

How to make external links open in a new window within wagtail

Recently, I made a change to include target="_blank" in external links by implementing the following code: @hooks.register('after_edit_page') def do_after_page_edit(request, page): if hasattr(page, "body"): soup = BeautifulSoup(page. ...

Importing a JSON file into a React component

I have a JSON file that I am importing in my React project: import React from 'react'; import Test1 from './test1.jsx'; import data from './data.json'; class TestWrapper extends React.Component { render () { return ( ...

What is preventing me from accessing these attributes using "${array[a].name}"?

What is preventing me from reading the properties using ${ array[a].name } with For Of loop? I created an example with an array of objects to help diagnose the problem. const movies = [{ title: "Shrek", year: 2001 }, { title: "Shrek 2", ...

Ways to verify the status within the DataTable?

Checking the condition inside column "data":"selectionAudit[0].assignFromDate" of a datatable to display content based on the conditions. var table4 = $('#auditAndNonAudit').DataTable({ "processing" : true, "scrollY": 100 ...

A guide to updating a component on the same route path in ReactJS using Links in version 6

Currently, I have a sidebar menu that allows users to navigate between different pages on the site. However, I've noticed that if a user clicks on the menu item for the current page they are on (e.g., current path is /user and they click on /user agai ...

Other options besides React SSR

I've been tasked with adding server side rendering to a large project I inherited. The project uses a custom routing system based on a "builder JSON" taken by a main component to dynamically render components based on the route, catering to different ...

Display each new array element on a separate line

let team = [['Sara', 'John', 'Kate']] let newTeam = team.map(function(r) { return r; }) outputs [ [ 'Sara', 'John', 'Kate' ] ] Is there a way to modify it so that each value is r ...

Guide for integrating the shadcn/ui Range Date Picker within a Form

Encountering an issue with using The Range Date Picker within the Form component. Specifically, I am looking to store {from, to} values of the range in an object, however, utilizing an object as a Form field value results in error messages not functioning ...

creating curved lines in three.js

I'm looking for assistance in creating a globe using three.js where I can project arcs representing exports and imports between countries. I have a basic globe set up, but I need guidance on the following: 1. How to use country shape files instead of ...

CSS :hover problem, text appearing unexpectedly

I'm currently working on designing a logo that displays hidden text when hovered over, providing instructions to users. However, I've encountered an issue where the hidden text reveals itself even before the logo is hovered over. This is frustrat ...

Having trouble with my ajax request not functioning correctly

<body style="margin:0px; padding:0px;" > <form method="post" > <input type="text" id="city" name="city" placeholder="Enter city name"> <input type="submit" value="Search City" id="searchid"/> ...

As I scroll past the top or bottom of the page, the body's background color transitions to a new shade

I am encountering an issue with my next.js app where the body background color is set to black. Here is the code in globals.css: body { background-color: black; padding: 0; margin: 0; font-family: 'Titillium Web', sans-serif; } However, ...

I'm looking for recommendations on the best technologies to implement in a location-based mobile app. Any suggestions

Currently working on a server-side website tailored for mobile devices. My main objective is to create a member system where users can log in and share their geolocation data. Once logged in, the user's geolocation information will be stored in my dat ...

Scroll to make the div slide in from the bottom

I'm trying to achieve a similar effect like the one shown in this website (you need to scroll down a bit to see the divs sliding in). Although I'm not very proficient in JS, I was able to create a code that makes the divs fade in from 0 opacity ...

The URL requested exceeds the maximum length limit in asp.net, causing a 414 error

I encountered the issue of receiving an "HTTP Error 414. The request URL is too long." While reading through this article, I learned that it is caused by an excessively lengthy query string: Currently in my web.config, I have set maxQueryStringLength to " ...

Ways to organize JSON information in Angular by date basis?

I am working on a project where I need to organize multiple JSON objects into an array based on their date data, with the date field serving as the key. ...

Dynamic rows in an Angular 2 Material data table

I'm currently working on dynamically adding rows to an Angular 2 Data Table ( https://material.angular.io/components/table/overview) by utilizing a service called "ListService". This service provides me with the columns ("meta.attributes") to be displ ...

The RemoveEventListener function seems to be malfunctioning within Angular2 when implemented with TypeScript

I am currently incorporating three.js into Angular2. The code I am using is quite straightforward, as shown below. this.webGLRenderer.domElement.addEventListener('mousedown', ()=>this.onMouseDown(<MouseEvent>event), false); this.webGLR ...

Double-triggered Redux oidc callback occuring twice

Here is my unique React container: class UniqueApp extends React.Component { componentDidMount() { if (this.props.location && this.props.location.pathname != '/unique-callback') { userManager.getUser().then(respo ...

Implement PHP script to send email upon form submission with POST method in HTML

I am new to PHP and trying to set up mail functionality, but I'm facing a problem where clicking on the submit button opens a new window that displays part of my PHP code. My goal is to send an email from a user's email address to mine with the c ...