skip to Main Content

I am making a feature on an app which allows the user to perform breathing exercises to do this I am using CSS animations. My issue is that I want to display exhale and inhale when an animation of a circle is shrinking and enlarging but my animation does start from the same point each time so this causes issues with the word that is displayed.

function toggleDropdown() {
  var dropdownContent = document.getElementById("dropdown-content");
  dropdownContent.classList.toggle("show");
}

var timer;

function selectExercise(exerciseId) {
  // Hide all exercises
  var exercises = document.getElementsByClassName("exercise");
  for (var i = 0; i < exercises.length; i++) {
    exercises[i].style.display = "none";
  }

  // Reset animation for all circles
  var circles = document.getElementsByClassName("circle");
  for (var i = 0; i < circles.length; i++) {
    circles[i].style.animationDuration = "0ms";
    circles[i].innerHTML = "Inhale";
    circles[i].classList.remove("reset-animation");
    void circles[i].offsetWidth; // Trigger reflow to apply the class immediately
    circles[i].classList.add("reset-animation");
  }


  // Show selected exercise
  var selectedExercise = document.getElementById(exerciseId);
  selectedExercise.style.display = "block";

  var dropdownContent = document.getElementById("dropdown-content");
  dropdownContent.classList.remove("show");

  clearInterval(timer);

  var timerElement = document.getElementById("timer" + exerciseId.slice(-1));
  timerElement.innerHTML = "";
}

function startAnimation(circleId, duration, totalCycles, timerId) {
  var circle = document.getElementById(circleId);
  var cycles = 0;
  var remainingTime = duration * totalCycles;
  var inhaleTime = duration / 2;
  var exhaleTime = duration / 2;

  clearInterval(timer);

  animate();

  function animate() {
    circle.innerHTML = "Inhale";
    setTimeout(function() {
      circle.innerHTML = "Exhale";
      setTimeout(function() {
        circle.innerHTML = "Inhale";
        cycles++;
        if (cycles < totalCycles) {
          animate(); // Start the next cycle
        } else {
          circle.style.animationDuration = "0ms"; // Stop the animation
        }
      }, inhaleTime);
    }, exhaleTime);
  }

  circle.style.animationDuration = duration + "ms";

  var timerElement = document.getElementById(timerId);
  timerElement.innerHTML = "Time left: " + (remainingTime / 1000) + " seconds";

  timer = setInterval(function() {
    remainingTime -= 1000;
    if (remainingTime <= 0) {
      clearInterval(timer);
      timerElement.innerHTML = "Time left: 0 seconds";
    } else {
      timerElement.innerHTML = "Time left: " + (remainingTime / 1000) + " seconds";
    }
  }, 1000);
}

selectExercise('exercise1', 4000);
body {
  background: #005eb8;
  margin: 0;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
  font-family: "Helvetica Neue", Arial, sans-serif;
}

.container {
  max-width: 350px;
  margin: 0 auto;
  border-radius: 5px;
  overflow: hidden;
  background: #ffffff;
  padding: 20px;
}

h1 {
  margin-bottom: 20px;
  text-align: center;
  color: #003087;
}

.dropdown {
  position: relative;
}

.button {
  width: 100%;
  padding: 10px;
  margin-bottom: 1px;
  color: #ffffff;
  background: #0072ce;
  border: none;
  cursor: pointer;
  border-radius: 5px;
}

.dropdown-content {
  display: none;
  position: absolute;
  background-color: #ffffff;
  width: 100%;
  border-radius: 5px;
  box-shadow: 0 0 15px rgba(0, 0, 0, 0.15);
  z-index: 1;
}

.dropdown-content a {
  display: block;
  padding: 10px;
  color: #003087;
  text-decoration: none;
}

.dropdown:hover .dropdown-content {
  display: block;
}

.exercise {
  display: none;
  padding: 2px;
  background-color: #ffffff;
  border-radius: 5px;
  margin-top: 0px;
  text-align: center;
  color: #003087;
}

.exercise h2 {
  margin-top: 10px;
  margin-bottom: 10px;
}

.exercise p {
  margin-top: 10px;
  margin-bottom: 20px;
}

.circle-wrapper {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  margin-bottom: 20px;
}

.circle {
  width: 200px;
  height: 200px;
  background-color: #4BC0C0;
  border-radius: 50%;
  display: flex;
  justify-content: center;
  align-items: center;
  color: #fff;
  font-size: 24px;
  font-weight: bold;
  animation-name: breathe;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
  animation-fill-mode: forwards;
  margin-bottom: 5px;
}

@keyframes breathe {
  0% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.2);
  }
  100% {
    transform: scale(1);
  }
}

.start-button {
  padding: 10px;
  color: #ffffff;
  background: #0072ce;
  width: 100%;
  border: none;
  cursor: pointer;
  border-radius: 5px;
  font-weight: bold;
  margin-top: 5px;
}

.start-button:hover {
  background: #003087;
}
<div class="container">
  <h1>Breathing Exercises</h1>

  <div class="dropdown">
    <button class="button" onclick="toggleDropdown()">Select Exercise</button>
    <div id="dropdown-content" class="dropdown-content">
      <a href="#" onclick="selectExercise('exercise1')">Exercise 1</a>
      <a href="#" onclick="selectExercise('exercise2')">Exercise 2</a>
      <a href="#" onclick="selectExercise('exercise3')">Exercise 3</a>
      <a href="#" onclick="selectExercise('exercise4')">Exercise 4</a>
    </div>
  </div>

  <div id="exercise1" class="exercise">
    <h2>Exercise 1</h2>
    <p>This is the description for Exercise 1.</p>
    <div class="circle-wrapper">
      <div id="circle1" class="circle"></div>
    </div>
    <div id="timer1" class="timer"></div>
    <button class="start-button" onclick="startAnimation('circle1', 10000, 5, 'timer1')">Start</button>
  </div>

  <div id="exercise2" class="exercise" style="display: none;">
    <h2>Exercise 2</h2>
    <p>This is the description for Exercise 2.</p>
    <div class="circle-wrapper">
      <div id="circle2" class="circle"></div>
    </div>
    <div id="timer2" class="timer"></div>
    <button class="start-button" onclick="startAnimation('circle2', 6000, 3, 'timer2')">Start</button>
  </div>

  <div id="exercise3" class="exercise" style="display: none;">
    <h2>Exercise 3</h2>
    <p>This is the description for Exercise 3.</p>
    <div class="circle-wrapper">
      <div id="circle3" class="circle"></div>
    </div>
    <div id="timer3" class="timer"></div>
    <button class="start-button" onclick="startAnimation('circle3', 8000, 2, 'timer3')">Start</button>
  </div>

  <div id="exercise4" class="exercise" style="display: none;">
    <h2>Exercise 4</h2>
    <p>This is the description for Exercise 4.</p>
    <div class="circle-wrapper">
      <div id="circle4" class="circle"></div>
    </div>
    <div id="timer4" class="timer"></div>
    <button class="start-button" onclick="startAnimation('circle4', 4000, 10, 'timer4')">Start</button>
  </div>
</div>

I have tried different methods for resetting the animation each time a new exercise is selected however I’ve been unsuccessful.

2

Answers


  1. This can be done quite easily using a css transition, manipulating a css custom property for the transition time duration, and adding/removing a class.

    const root = document.documentElement;
    
    function toggleDropdown() {
      var dropdownContent = document.getElementById("dropdown-content");
      dropdownContent.classList.toggle("show");
    }
    
    var timer;
    
    function selectExercise(exerciseId) {
      // Hide all exercises
      var exercises = document.getElementsByClassName("exercise");
      for (var i = 0; i < exercises.length; i++) {
        exercises[i].style.display = "none";
      }
    
      // Reset animation for all circles
      var circles = document.getElementsByClassName("circle");
      for (var i = 0; i < circles.length; i++) {
        root.style.setProperty('--transition-duration', "0");
        circles[i].innerHTML = "Ready";
        //void circles[i].offsetWidth; // Trigger reflow to apply the class immediately
        circles[i].classList.remove("inhale");
      }
    
    
      // Show selected exercise
      var selectedExercise = document.getElementById(exerciseId);
      selectedExercise.style.display = "block";
    
      var dropdownContent = document.getElementById("dropdown-content");
      dropdownContent.classList.remove("show");
    
      clearInterval(timer);
    
      var timerElement = document.getElementById("timer" + exerciseId.slice(-1));
      timerElement.innerHTML = "";
    }
    
    function startAnimation(circleId, duration, totalCycles, timerId) {
      var circle = document.getElementById(circleId);
      var cycles = 0;
      var remainingTime = duration * totalCycles;
      var inhaleTime = duration / 2;
      var exhaleTime = duration / 2;
      root.style.setProperty('--transition-duration', `${inhaleTime}ms`);
    
      clearInterval(timer);
    
      animate();
    
      function animate() {
        circle.innerHTML = "Inhale";
        circle.classList.add('inhale')
        setTimeout(function() {
          circle.innerHTML = "Exhale";
          circle.classList.remove('inhale')
          setTimeout(function() {
            circle.innerHTML = "Inhale";
            circle.classList.add('inhale')
    
            cycles++;
            if (cycles < totalCycles) {
              animate(); // Start the next cycle
            } else {
              //circle.style.animationDuration = "0ms"; // Stop the animation
            }
          }, inhaleTime);
        }, exhaleTime);
      }
    
      // circle.style.animationDuration = duration + "ms";
    
      var timerElement = document.getElementById(timerId);
      timerElement.innerHTML = "Time left: " + (remainingTime / 1000) + " seconds";
    
      timer = setInterval(function() {
        remainingTime -= 1000;
        if (remainingTime <= 0) {
          clearInterval(timer);
          timerElement.innerHTML = "Time left: 0 seconds";
        } else {
          timerElement.innerHTML = "Time left: " + (remainingTime / 1000) + " seconds";
        }
      }, 1000);
    }
    
    selectExercise('exercise1', 4000);
    :root {
      --transition-duration: 0;
    }
    
    body {
      background: #005eb8;
      margin: 0;
      padding: 0;
      display: flex;
      align-items: center;
      justify-content: center;
      height: 100vh;
      font-family: "Helvetica Neue", Arial, sans-serif;
    }
    
    .container {
      max-width: 350px;
      margin: 0 auto;
      border-radius: 5px;
      overflow: hidden;
      background: #ffffff;
      padding: 20px;
    }
    
    h1 {
      margin-bottom: 20px;
      text-align: center;
      color: #003087;
    }
    
    .dropdown {
      position: relative;
    }
    
    .button {
      width: 100%;
      padding: 10px;
      margin-bottom: 1px;
      color: #ffffff;
      background: #0072ce;
      border: none;
      cursor: pointer;
      border-radius: 5px;
    }
    
    .dropdown-content {
      display: none;
      position: absolute;
      background-color: #ffffff;
      width: 100%;
      border-radius: 5px;
      box-shadow: 0 0 15px rgba(0, 0, 0, 0.15);
      z-index: 1;
    }
    
    .dropdown-content a {
      display: block;
      padding: 10px;
      color: #003087;
      text-decoration: none;
    }
    
    .dropdown:hover .dropdown-content {
      display: block;
    }
    
    .exercise {
      display: none;
      padding: 2px;
      background-color: #ffffff;
      border-radius: 5px;
      margin-top: 0px;
      text-align: center;
      color: #003087;
    }
    
    .exercise h2 {
      margin-top: 10px;
      margin-bottom: 10px;
    }
    
    .exercise p {
      margin-top: 10px;
      margin-bottom: 20px;
    }
    
    .circle-wrapper {
      display: flex;
      justify-content: center;
      align-items: center;
      width: 100%;
      margin-bottom: 20px;
    }
    
    .circle {
      width: 200px;
      height: 200px;
      background-color: #4BC0C0;
      border-radius: 50%;
      display: flex;
      justify-content: center;
      align-items: center;
      color: #fff;
      font-size: 24px;
      font-weight: bold;
      /* animation-name: breathe;
      animation-timing-function: ease-in-out;
      animation-iteration-count: infinite;
      animation-fill-mode: forwards; */
      margin-bottom: 5px;
      transform: scale(1.0);
      transition: all var(--transition-duration);
    }
    
    .circle.inhale {
      transform: scale(1.2);
    }
    
    .circle.exhale {}
    
    .start-button {
      padding: 10px;
      color: #ffffff;
      background: #0072ce;
      width: 100%;
      border: none;
      cursor: pointer;
      border-radius: 5px;
      font-weight: bold;
      margin-top: 5px;
    }
    
    .start-button:hover {
      background: #003087;
    }
    <div class="container">
      <h1>Breathing Exercises</h1>
    
      <div class="dropdown">
        <button class="button" onclick="toggleDropdown()">Select Exercise</button>
        <div id="dropdown-content" class="dropdown-content">
          <a href="#" onclick="selectExercise('exercise1')">Exercise 1</a>
          <a href="#" onclick="selectExercise('exercise2')">Exercise 2</a>
          <a href="#" onclick="selectExercise('exercise3')">Exercise 3</a>
          <a href="#" onclick="selectExercise('exercise4')">Exercise 4</a>
        </div>
      </div>
    
      <div id="exercise1" class="exercise">
        <h2>Exercise 1</h2>
        <p>This is the description for Exercise 1.</p>
        <div class="circle-wrapper">
          <div id="circle1" class="circle"></div>
        </div>
        <div id="timer1" class="timer"></div>
        <button class="start-button" onclick="startAnimation('circle1', 10000, 5, 'timer1')">Start</button>
      </div>
    
      <div id="exercise2" class="exercise" style="display: none;">
        <h2>Exercise 2</h2>
        <p>This is the description for Exercise 2.</p>
        <div class="circle-wrapper">
          <div id="circle2" class="circle"></div>
        </div>
        <div id="timer2" class="timer"></div>
        <button class="start-button" onclick="startAnimation('circle2', 6000, 3, 'timer2')">Start</button>
      </div>
    
      <div id="exercise3" class="exercise" style="display: none;">
        <h2>Exercise 3</h2>
        <p>This is the description for Exercise 3.</p>
        <div class="circle-wrapper">
          <div id="circle3" class="circle"></div>
        </div>
        <div id="timer3" class="timer"></div>
        <button class="start-button" onclick="startAnimation('circle3', 8000, 2, 'timer3')">Start</button>
      </div>
    
      <div id="exercise4" class="exercise" style="display: none;">
        <h2>Exercise 4</h2>
        <p>This is the description for Exercise 4.</p>
        <div class="circle-wrapper">
          <div id="circle4" class="circle"></div>
        </div>
        <div id="timer4" class="timer"></div>
        <button class="start-button" onclick="startAnimation('circle4', 4000, 10, 'timer4')">Start</button>
      </div>
    </div>

    https://css-tricks.com/updating-a-css-variable-with-javascript/

    Login or Signup to reply.
  2. I would do this by listening to the animationiteration event, and toggling your text when it fires.

    const circle = document.querySelector(".circle");
    
    const inhale = () => {
      circle.innerHTML = "Inhale";
    };
    const exhale = () => {
      circle.innerHTML = "Exhale";
    };
    
    const animate = () => {
      inhale();
      circle.style.animationPlayState = "running";
      circle.addEventListener("animationiteration", (ev) => {
        if (ev.elapsedTime %4 === 0) {
          inhale();
        } else {
          exhale();
        }
      }, false);
    };
    
    document.getElementById('animate').addEventListener('click', animate);
    .circle-wrapper {
      display: flex;
      justify-content: center;
      align-items: center;
      width: 100%;
      height: 100%;
      margin-bottom: 20px;
    }
    
    .circle {
      width: 200px;
      height: 200px;
      background-color: #4BC0C0;
      border-radius: 50%;
      display: flex;
      justify-content: center;
      align-items: center;
      color: #fff;
      font-size: 24px;
      font-weight: bold;
      animation-name: breathe;
      animation-timing-function: ease-in-out;
      animation-iteration-count: infinite;
      animation-direction: alternate;
      animation-duration: 2s;
      animation-play-state: paused;
      animation-fill-mode: forwards;
      margin-bottom: 5px;
    }
    
    .circle span {
      margin: 33% auto;
      display: inline-block;
      visibility: hidden;
    }
    
    @keyframes breathe {
      0% {
        transform: scale(1);
      }
      100% {
        transform: scale(1.25);
      }
    }
    <div class="circle-wrapper">
    
        <div class="circle"></div>
    
    </div>
    
    
    <button id="animate">animate</button>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search