'Calculate transform scale [ transform: scale(x, y) ] as the function of elapsed time

I have a container that is expanded and collapsed on click of chevron icon. The code to collapse/expand the container is in the function transformAnimation. The code of transformAnimation is similar to the code on MDN web docs for requestAnimationFrame. The code to animate (scale) the container has been developed on the guidelines of this article on Building performant expand & collapse animations on Chrome Developers website.

I am not able to figure out how to calculate yScale value (which is nothing but the css function scaleY() for collapse/expand animation) as a function of the time elapsed since the start of the animation.

To elaborate what I mean, let's assume that the container is in expanded state. In this state the yScale value of the container is 6. Now when user clicks on the toggle button, in the transformAnimation function for each animation frame, i.e, execution of the requestAnimationFrame callback step function, the value of yScale should decrease from 6 (the expanded state) to 1 (the collapsed state) in the exact duration that I want the animation to run for. So, basically I want to achieve something similar to css property transition-duration: 2s, where I can control the duration.

In the present state, the code to calculate yScale is not working as expected.

const dragExpandableContainer = document.querySelector('.drag-expandable-container');
const dragExpandableContents = document.querySelector('.drag-expandable__contents');
const resizeableControlEl = document.querySelector('.drag-expandable__resize-control');
const content = document.querySelector(`.content`);
const toggleEl = document.querySelector(`.toggle`);

const collapsedHeight = calculateCollapsedHeight();

/* This height is used as the basis for calculating all the scales for the component.
 * It acts as proxy for collapsed state.
 */
dragExpandableContainer.style.height = `${collapsedHeight}px`;

// Apply iniial transform to expand
dragExpandableContainer.style.transform = `scale(1, 10)`;

// Apply iniial reverse transform on the contents 
dragExpandableContents.style.transform = `scale(1, calc(1/10))`;

let isOpen = true;

const togglePopup = () => {
  if (isOpen) {
    collapsedAnimation();
    toggleEl.classList.remove('toggle-open');
    isOpen = false;
  } else {
    expandAnimation();
    toggleEl.classList.add('toggle-open');
    isOpen = true
  };
};

function calculateCollapsedHeight() {
  const collapsedHeight = content.offsetHeight + resizeableControlEl.offsetHeight;
  return collapsedHeight;
}

const calculateCollapsedScale = function() {
  const collapsedHeight = calculateCollapsedHeight();
  const expandedHeight = dragExpandableContainer.getBoundingClientRect().height;

  return {
    /* Since we are not dealing with scaling on X axis, we keep it 1.
     * It can be inverse to if required */
    x: 1,
    y: expandedHeight / collapsedHeight,
  };
};

const calculateExpandScale = function() {
  const collapsedHeight = calculateCollapsedHeight();
  const expandedHeight = 100;

  return {
    x: 1,
    y: expandedHeight / collapsedHeight,
  };
};

function expandAnimation() {
  const {
    x,
    y
  } = calculateExpandScale();

  transformAnimation('expand', {
    x,
    y
  });
}

function collapsedAnimation() {
  const {
    x,
    y
  } = calculateCollapsedScale();

  transformAnimation('collapse', {
    x,
    y
  });
}

function transformAnimation(animationType, scale) {
  let start, previousTimeStamp;
  let done = false;

  function step(timestamp) {
    if (start === undefined) {
      start = timestamp;
    }
    const elapsed = timestamp - start;

    if (previousTimeStamp !== timestamp) {
      const count = Math.min(0.1 * elapsed, 200);
      //console.log('count', count);
      let yScale;
      
      if (animationType === 'expand') {
        yScale = (scale.y / 100) * count;
      } else yScale = scale.y - (scale.y / 100) * count;
      //console.log('yScale', yScale);
      if (yScale < 1) yScale = 1;
      
      dragExpandableContainer.style.transform = `scale(${scale.x}, ${yScale})`;

      const inverseXScale = 1;
      const inverseYScale = 1 / yScale;
      
      dragExpandableContents.style.transform = `scale(${inverseXScale}, ${inverseYScale})`;

      if (count === 200) done = true;

      //console.log('elapsed', elapsed);
      if (elapsed < 1000) {
        // Stop the animation after 2 seconds
        previousTimeStamp = timestamp;
        if (!done) requestAnimationFrame(step);
      }
    }
  }
  requestAnimationFrame(step);
}
.drag-expandable-container {
  position: absolute;
  bottom: 0px;
  display: block;
  overflow: hidden;
  width: 100%;
  background-color: #f3f7f7;
  transform-origin: bottom left;
}

.drag-expandable__contents {
  transform-origin: top left;
}

.toggle {
  position: absolute;
  top: 2px;
  right: 15px;
  height: 10px;
  width: 10px;
  transition: transform 0.2s linear;
}

.toggle-open {
  transform: rotate(180deg);
}

.drag-expandable__resize-control {
  background-color: #e7eeef;
}

.burger-icon {
  width: 12px;
  margin: 0 auto;
  padding: 2px 0;
}

.burger-icon__line {
  height: 1px;
  background-color: #738F93;
  margin: 2px 0;
}

.drag-expandable__resize-control:hover {
  border-top: 1px solid #4caf50;
  cursor: ns-resize;
}
<!DOCTYPE html>
<html>

<head>
  <link rel="stylesheet" href="css.css">
</head>

<body>
  <div class="drag-expandable-container">
    <div class="drag-expandable__contents">
      <div class="drag-expandable__resize-control">
        <div class="burger-icon">
          <div class="burger-icon__line"></div>
          <div class="burger-icon__line"></div>
          <div class="burger-icon__line"></div>
        </div>
      </div>
      <div class="content" />
      <div>
        <div class="toggle toggle-open" onclick="togglePopup()">
          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M416 352c-8.188 0-16.38-3.125-22.62-9.375L224 173.3l-169.4 169.4c-12.5 12.5-32.75 12.5-45.25 0s-12.5-32.75 0-45.25l192-192c12.5-12.5 32.75-12.5 45.25 0l192 192c12.5 12.5 12.5 32.75 0 45.25C432.4 348.9 424.2 352 416 352z"/></svg>
        </div>
      </div>
    </div>
  </div>
</body>
<script type="text/javascript" src="js.js"></script>

</html>


Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source