Search code examples
javascriptjquerycssanimation

Skill cooldown animation effect similar to games like WoW in css/js?


I'm trying to make an effect where when a skill icon is clicked, it goes on "cooldown" similar to when a skill goes on cooldown in a typical MMO like WoW.

I found the below code for achieving such an effect, but it's not documented and I don't very much understand it but want to make changes to it.

html:

<table>
<tr>
    <td>
        <div class="skill"></div>
    </td>
    <td>
        <div class="skill"></div>
    </td>
    <td>
        <div class="skill"></div>
    </td>
    <td>
        <div class="skill"></div>
    </td>
    <td>
        <div class="skill"></div>
    </td>
</tr>
</table>

css:

.skill {
    margin: 0px;
    padding: 0px;
    position: relative;
    border: 1px solid #36393E;
    border-radius: 5%;
    width: 44px;
    height: 44px;
    overflow: hidden;
    background-color: transparent;
    background-image: url("skill.png");
    background-repeat: no-repeat;
    background-size: 100%;
}

.cooldown {
    position: absolute;
    opacity: 0.8;
    top: 0px;
    left: 0px;
    height: 100%;
    width: 100%;
}

js:

function cooldown(container, percent) {
    var div = $(container);
    div.empty();
    
    var total = 100;
    if(percent < total) {
        var data = [percent, total - percent];
        var width = div.width();
        var height = div.height();
        var cx = width / 2;
        var cy = height / 2;
        var r = cx * Math.SQRT2;
        var colors = [null, '#AAA'];
        var svgns = "http://www.w3.org/2000/svg";
        var chart = document.createElementNS(svgns, "svg:svg");
        chart.setAttribute("width", width);
        chart.setAttribute("height", height);
        chart.setAttribute("viewBox", "0 0 " + width + " " + height);
        
        var angles = []
        for(var i = 0; i < data.length; i++) angles[i] = data[i] / total * Math.PI * 2;
        startangle = 0;
        for(var i = 0; i < data.length; i++) {
            var endangle = startangle + angles[i];
            
            var x1 = cx + r * Math.sin(startangle);
            var y1 = cy - r * Math.cos(startangle);
            var x2 = cx + r * Math.sin(endangle);
            var y2 = cy - r * Math.cos(endangle);
            
            var big = 0;
            if (endangle - startangle > Math.PI) big = 1;
            
            var path = document.createElementNS(svgns, "path");
            
            var d = "M " + cx + "," + cy + " L " + x1 + "," + y1 +
                " A " + r + "," + r + " 0 " + big + " 1 " +
                x2 + "," + y2 + " Z";
            
            path.setAttribute("d", d);
            if(colors[i]) {
                path.setAttribute("fill", colors[i]);
            } else {
                path.setAttribute("opacity", 0);
            }
            chart.appendChild(path);
            startangle = endangle;
        }   
        
        chart.setAttribute('overflow', 'hidden');
        div.append(chart);
    }
}

$(window).ready(function() {
    $('.skill').each(function() {
        var skill = $(this);
        var div = $('<div />').appendTo(skill).addClass('cooldown');
        var radius = div.parent().width() / 2;
        
        skill.click(function() {
            $({pct: 0}).animate({pct: 100}, {
                duration: 5000,
                step: function (curLeft) { cooldown(div, curLeft); }
            });
        });
    });
});

I managed to make one change to it that I wanted which was to add text with the cooldown time, like so: (haven't done updating of it in js yet but I think that's something I can do myself)

<div class="skill"><div class="text">5</div></div>

.skill .text {
    font-size: 14px;
    color: #E1E5EB;
    text-shadow: 1px 1px 1px #000000;
    line-height: 44px;
    text-align: center;
    background-color: transparent;
}

The other changes that I need help with are:

  • Filling counter-clockwise instead of clockwise.

  • Flip the opacity of filled/unfilled portions (currently the filled portion is dark and unfilled is light but I want the opposite)


Solution

  • Another approach would be to use the conic-gradient together with some CSS variables. This way we don't need to build an SVG.

    PS: There are comments in the JS to explain how it works.

    const SECOND_IN_MS = 1000;
    const UPDATE_INTERVAL = SECOND_IN_MS / 60; // Update 60 times per second (60 FPS)
    const SKILL_CLASS = 'skill';
    const DISABLED_CLASS = 'disabled';
    
    // Cooldowns per skill in milliseconds
    const COOLDOWN_MAP = new Map([
      ['run', 1000],
      ['jump', 2000],
      ['crawl', 3000],
      ['slide', 4000],
      ['tumble', 5000],
    ]);
    
    // Get skills table from the DOM
    const skillsTable = document.querySelector('.skills-table');
    
    // Activate clicked skill
    const activateSkill = (event) => {
      const {target} = event;
      
      // Exit if we click on anything that isn't a skill
      if(!target.classList.contains(SKILL_CLASS)) return;
      
      target.classList.add(DISABLED_CLASS);
      target.style = '--time-left: 100%';
      
      // Get cooldown time
      const skill = target.dataset.skill;
      let time = COOLDOWN_MAP.get(skill) - UPDATE_INTERVAL;
      
      // Update remaining cooldown
      const intervalID = setInterval(() => {
        // Pass remaining time in percentage to CSS
        const passedTime = time / COOLDOWN_MAP.get(skill) * 100;
        target.style = `--time-left: ${passedTime}%`;
        
        // Display time left
        target.textContent = (time / SECOND_IN_MS).toFixed(2);
        time -= UPDATE_INTERVAL;
        
        // Stop timer when there is no time left
        if(time < 0) {
          target.textContent = '';
          target.style = '';
          target.classList.remove(DISABLED_CLASS);
          
          clearInterval(intervalID);
        }
      }, UPDATE_INTERVAL);
    }
    
    // Add click handler to the table
    skillsTable.addEventListener('click', activateSkill, false);
    .skill {
      position: relative;
      border: 1px solid #36393E;
      border-radius: 5%;
      width: 44px;
      height: 44px;
      overflow: hidden;
      cursor: pointer;
    }
    
    /* Prevents you from clicked the button multiple times */
    .skill.disabled {
      pointer-events: none;
    }
    
    /* Makes sure we click the skill not anything in it */
    .skill > * {
      pointer-events: none;
    }
    
    .skill::before {
      content: "";
      background: conic-gradient(
        rgba(0, 0, 0, 0.7) var(--time-left),
        rgba(0, 0, 0, 0.1) var(--time-left));
      position: absolute;
      opacity: 0.8;
      top: 0;
      left: 0p;
      height: 100%;
      width: 100%;
    }
    <table class='skills-table'>
      <tr>
        <td>
          <!-- data-skill refers to the COOLDOWN_MAP in JS -->
          <div class="skill" data-skill='run'></div>
        </td>
        <td>
          <!-- data-skill refers to the COOLDOWN_MAP in JS -->
          <div class="skill" data-skill='jump'></div>
        </td>
        <td>
          <!-- data-skill refers to the COOLDOWN_MAP in JS -->
          <div class="skill" data-skill='crawl'></div>
        </td>
        <td>
          <!-- data-skill refers to the COOLDOWN_MAP in JS -->
          <div class="skill" data-skill='slide'></div>
        </td>
        <td>
          <!-- data-skill refers to the COOLDOWN_MAP in JS -->
          <div class="skill" data-skill='tumble'></div>
        </td>
      </tr>
    </table>