Incorrect height value after transition in eventListener

62 Views Asked by At

        var container    = document.querySelector(".container");
        var bar          = document.querySelector(".bar");    
        var barL         = document.getElementsByClassName("barLayer");
        var value        = document.getElementsByClassName("value");
        var compare      = document.getElementsByClassName("compare"); // array vallue to show than code can't get the correct result of that bar's height   
        var pixelStep    = 4;   // cooficient to expand bar height. if  val=100 => bar.height = 400px
        var barNum       = 12;  // number of bars
        var spaceBetween = 100; // space between bars
        var intervalTime = 10;  // ms reading transition changes
        var delayTime    = 50;  // ms after drawing previous bar.

        const data       = [ 
        { brand : "SAMSUNG",  val : 40,  color : "#c00"}, 
        { brand : "APPLE",    val : 46,  color : "#0c0"}, 
        { brand : "HUAWEI",   val : 43,  color : "#f84"}, 
        { brand : "ASUS",     val : 71,  color : "#248"},    
        { brand : "XIAOMI",   val : 53,  color : "#0cc"},
        { brand : "OPPO",     val : 100, color : "#cc0"},
        { brand : "VIVO",     val : 66,  color : "#c0c"}, 
        { brand : "MOTOROLA", val : 86,  color : "#ccc"},
        { brand : "LENOVO",   val : 61,  color : "#c40"},
        { brand : "LG",       val : 93,  color : "#333"},
        { brand : "NOKIA",    val : 83,  color : "#088"}, 
        { brand : "OTHERS",   val : 51,  color : "#06c"} ]; 

        sortFunction(data, "val");  //sort Array

        clone(bar, container); // clone bar for (barNum-1) times

        for(let i = 0; i < data.length; i++){ barL[i].style.backgroundColor = data[i].color; } // colorize every bar with corresponding array color value 

        //---------------

      var myInterval = [];

        function anim(j){  // draw bars one after the other in sequence

          const computedStyleObj = getComputedStyle(barL[j]);
          value[j].textContent   = Math.round(parseInt(computedStyleObj.height)/pixelStep) + "K";

          compare[j].textContent = data[j].val;  

          barL[j].style.height   = value[j].style.bottom = (data[j].val * pixelStep) + "px"  //transiton value of height 
        //console.log("j : ", data[j].val + " - " + parseInt(computedStyleObj.height));
        barL[j].addEventListener("transitionEnd", () =>{clearInterval(myInterval[j]);});        // when transition ends, clear interval
        barL[j].addEventListener("webkitTransitionEnd",  () =>{clearInterval(myInterval[j]);});
      }

      for (let i = 0; i < data.length; i++) {
        setTimeout(function() {
        myInterval[i] = setInterval(function() {anim(i);}, intervalTime);   // after delayTime ms start the next bar animation
      }, i * delayTime);
      }
        //----------------

        function clone(item, container) {           // clone an item in a container function
          for(let i = 0; i < barNum-1 ; i++){
            var cln = item.cloneNode(true);
            container.appendChild(cln);
            cln.style.left = ((i+1) * spaceBetween ) + "px";
          }
        }

        function sortFunction(arr, key) {   // sort an array of objects by given key
          arr.sort(function(a, b){
            let x = a[key];
            let y = b[key];
            if (x < y) {return -1;}
            if (x > y) {return 1;}
            return 0;
          });
          data.reverse();  // reverse for descending array 
        }
        :root {
          --barWidth     : 80px;
          --color        : #aaa;
          --contentWidth : 1200px;
        }

        * {
          margin     : 0;
          padding    : 0;
          box-sizing : border-box;
        }

        body {
          width           : 100%;
          display         : flex;
          align-content   : center;       
          justify-content : center;
          flex-direction  : column;
          background      : var(--color);
          font-family     : 'Roboto Mono', monospace;
        }

        .container {
          position    : relative;
          width       : var(--contentWidth);
          height      : 500px;
          border      : 1px solid #0005;
          background  : #fff4;
          margin      : 10px auto;
        }

        .bar {
          position   : absolute;
          width      : var(--barWidth);
          margin     : 10px;
          display    : inline-block;
          border     : 0px solid #0005;
          bottom     : 0px;
        }

        .barLayer {
          position   : absolute;
          width      : var(--barWidth);;
          height     : 0px;
          bottom     : 0;
          border     : 0px solid #0005;          
        }

        .value, .compare {
          position    : absolute;
          width       : var(--barWidth);
          height      : calc(var(--barWidth)/2);
          bottom      : 0px;
          font-size   : calc(var(--barWidth)/4);
          text-align  : center;
          border      : 0px solid #fff;
          line-height : calc(var(--barWidth)/2);   
        } 

        .barLayer, .value { 
          transition : all 1s linear;
        }
      <div class="container">
        <div class="bar">     
          <div class="barLayer"></div>
          <div class="value"></div>
          <div class="compare"></div>
        </div>
      </div>  

I want to clear "interval" after transition end (drawing bar with transition animation) not to busy CPU in vain, but I can't get the correct height value.

You can see the result using snipped link below.

Top is wrong value, bottom is the data from array. I tried some arithmetic methods, but want to use "transitionend" eventListener. Thanks! :)

1

There are 1 best solutions below

2
Sheraff On

TL;DR: for animation stuff, use requestAnimationFrame

Usually for animation related things, we prefer using requestAnimationFrame than intervals or timeout. You can think of requestAnimationFrame as a setTimeout that will trigger on the next frame. So I'm proposing this as a replacement for your setInterval.

As for the measure of "how far along is this bar" to animate the numbers, I think you'll get a good enough approximation by just watching how much time has elapsed and how long the transition is supposed to last. This will not be precise by any means, but measuring the elements with getComputedStyle or getBoundingClientRect on every frame would be a disaster for fluidity. So this is why I'm using timestamps in watchAnimation.

And for the rest, I didn't change much. Just re-wrote your code because (sorry but) it was a bit of a mess, and way too many things that didn't have anything to do with the problem at hand.

const container = document.querySelector('.container')
const bar = document.querySelector('.bar')

const values = [100, 80, 63, 20]
const DURATION = 1000 //ms

values.forEach((value, i) => {
    initItems(value, i)
    staggeredStart(value, i)
    watchAnimation(value, i)
})

function initItems(value, i) {
    let item = container.children.item(i)
    if(!item) {
        item = bar.cloneNode(true)
        container.appendChild(item)
    }
    // other things to initialize items
}

function staggeredStart(value, i) {
    const item = container.children.item(i)
    setTimeout(() => {
        item.classList.add('animate')
        item.addEventListener('transitionend', () => {
            item.classList.remove('animate')
        }, {once: true, passive: true})
        item.style.setProperty('--value', `${100 - value}%`)
    }, i * 50)
}

function watchAnimation(value, i) {
    const item = container.children.item(i)
    const span = item.querySelector('.value')
    span.textContent = '0'
    let requestAnimationFrameId
    let startTime

    function onStart(event) {
        startTime = event.timeStamp
        onFrame()
    }
    
    function onEnd() {
        cancelAnimationFrame(requestAnimationFrameId)
        span.textContent = value
    }
    
    function onFrame() {
        requestAnimationFrameId = requestAnimationFrame((time) => {
            const delta = time - startTime
            const ratio = Math.min(Math.max(0, delta / DURATION), 1)
            span.textContent = Math.round(value * ratio)
            onFrame()
        })
    }

    item.addEventListener('transitionstart', onStart, {once: true, passive: true})
    item.addEventListener('transitionend', onEnd, {once: true, passive: true})
}
* {
    box-sizing: border-box;
}

.container {
    height: 500px;
    border: 1px solid #0005;
    overflow: hidden;
    display: flex;

    --bar-width: 80px;
    --space-for-value: 40px;
}

.bar {
    --value: 100%;
    position: relative;
    width: var(--bar-width);
    height: calc(100% - var(--space-for-value));
    top: var(--space-for-value);
    transform: translateY(var(--value));
}

.bar:not(:first-child) {
    margin-left: 20px;
}

.bar.animate {
    transition: transform 1s linear;
}

.layer {
    position: absolute;
    width: 100%;
    height: 100%;
    bottom: 0;
    background: red;
}

.value {
    position: absolute;
    width: 100%;
    height: var(--space-for-value);
    bottom: 100%;
    text-align: center;
    line-height: var(--space-for-value);
}
<div class="container">
    <div class="bar">
        <div class="layer"></div>
        <div class="value"></div>
    </div>
</div>