JSFiddle - React, Tailwind, and code Playground

HTML

<script>
  function Timer(duration /* in ms */ , props) {
    this.ontick = props && typeof props.ontick === "function" ? props.ontick : function() {};
    this.onend = props && typeof props.onend === "function" ? props.onend : function() {};
    this.interval = props && typeof props.interval === "number" ? props.interval : 1000 / 10;
    this.elapsed = 0;

    var running = false,
      start, end, self = this;

    this.start = function() {
      if (running) return self;

      start = new Date().getTime();
      end = start + duration;
      tick();
      running = true;

      return self;
    };

    function tick() {
      var now = new Date().getTime();
      self.ontick.call(self);
      self.elapsed = now - start;

      if (now < end) setTimeout(tick, self.interval);
      else self.onend.call(self);
    }
  }
</script>

JavaScript

var can = document.createElement("canvas"),
  ctx = can.getContext('2d'),
  sta = -Math.PI / 2,
  dur = 3 * 1000;

can.width = 100;
can.height = 100;
document.body.appendChild(can);
ctx.font = "normal 30px Arial";

var myTimer = new Timer(dur, {
  ontick: function () {
    var pct = this.elapsed / dur,
      sec = Math.ceil((dur - this.elapsed) / 1000),
      wid = ctx.measureText(sec).width;

    ctx.clearRect(0, 0, 100, 100);

    ctx.fillStyle = "#777";
    ctx.arc(50, 50, 50, 0, 2 * Math.PI);
    ctx.fill();

    ctx.fillStyle = "#f00";
    ctx.beginPath();
    ctx.moveTo(50, 50);
    ctx.arc(50, 50, 50, sta, sta + 2 * Math.PI * pct);
    ctx.fill();

    ctx.fillStyle = "#111";
    ctx.fillText(sec, 50 - wid / 2 + 1, 61);
    ctx.fillStyle = "#eee";
    ctx.fillText(sec, 50 - wid / 2, 60);
  },
  onend: function () {
    this.ontick();
  },
  interval: 1000 / 60
}).start();

var timer2 = new Timer(dur);
timer2.ontick = function () {
  console.log(this.elapsed);
};
timer2.start();