OHello
OHello

Reputation: 97

Javascript Timer with clock graphic

I have been working on a timer that is displaying 3 preset times based on the user's choice. I decided to include a countdown graphic in the shape of a clock which only has one hand and rotates 360 degrees for the full length of time assigned to it.

The problem I have is marrying up the original timer and the new display. I have the "clock" element drawn with canvas and I have a working timer, but I need to know how to take that time and utilise it to get the hand to move around. As it currently stands the hands will draw when I have it functioning, but don't display mostly because I haven't set an interval for the clock but I do have one for the original timer.

Full JS

((d) => {
  let btn = d.getElementById("btn");
  let reset = d.getElementById("reset");
  let countdown = d.getElementById("countdown");
  let btnLength = d.getElementById("btnLength");

  // 25 Minutes
  let short = 1500;
  // 45 Minutes
  let med = 2700;
  // 90 Minutes
  let long = 5400;

  let counter;
  let startTime = short;

  let timerFormat = (s) => {
    return (s - (s %= 60)) / 60 + (9 < s ? ":" : ":0") + s;
  };

  countdown.textContent = timerFormat(startTime);

  let timer = () => {
    startTime--;
    countdown.textContent = timerFormat(startTime);
    if (startTime === 0) clearInterval(counter);
  };

  let start = () => {
    counter = counter || setInterval(timer, 950);
  };

  let stop = () => {
    clearInterval(counter);
    counter = undefined;
  };

  // Changes between Start and Stop button labelling.

  btn.addEventListener("click", () => {
    if (counter) {
      stop();
      btn.textContent = "Start";
    } else {
      start();
      btn.textContent = "Stop";
    }
  });

  // Stops counter, resets to original start time and
  // resets button label to Start.

  reset.onclick = () => {
    stop();
    startTime = short;
    if (btn.textContent === "Stop") btn.textContent = "Start";
    btnLength.textContent = "Short";
    countdown.textContent = timerFormat(startTime);
  };

  // Changes timer legnth and button labelling

  btnLength.addEventListener("click", () => {
    if (startTime === short) {
      startTime = med;
      btnLength.textContent = "Medium";
    } else if (startTime === med) {
      startTime = long;
      btnLength.textContent = "Long";
    } else if (startTime === long) {
      startTime = short;
      btnLength.textContent = "Short";
    }
    countdown.textContent = timerFormat(startTime);
  });

  //Clock

  let c = d.getElementById("canvas");
  let ctx = c.getContext("2d");
  let radius = c.height / 2;
  ctx.translate(radius, radius);
  radius = radius * 0.9;

  showTime = (ctx, radius) => {
    let startTime =
      (startTime * Math.PI) / 30 + (startTime * Math.PI) / (30 * 60);
    hand(ctx, startTime, radius * 0.8, radius * 0.07);
  };

  hand = (ctx, position, length, width) => {
    ctx.beginPath();
    ctx.lineWidth = width;
    ctx.lineCap = "round";
    ctx.moveTo(0, 0);
    ctx.rotate(position);
    ctx.lineTo(0, -length);
    ctx.stroke();
    ctx.rotate(-position);
  };

  clock = () => {
    ctx.arc(0, 0, radius, 0, 2 * Math.PI, true);
    ctx.fillStyle = "#f5afaf";
    ctx.fill();
    showTime();
  };

  clock();
})(document);

Html

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Document</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="../styles/styles.css" />
    <script defer src="../JS/timer.js"></script>
  </head>
  <body>
    <div class="btn__container">
      <div id="countdown"></div>
      <button class="btn" id="btn">Start</button>
      <button class="btn" id="btnLength">Short</button>
      <button class="btn" id="reset">Reset</button>
<div class="clock">
<canvas  id="canvas" width="300" height="300" style="background-color:none"></canvas>
</div>
  </body>
</html>


Upvotes: 2

Views: 424

Answers (1)

Luka Kralj
Luka Kralj

Reputation: 456

The first issue you have is that you only call clock() once - at the end of your script to initialise it. Add it to timer() function:

let timer = () => {
  startTime--;
  countdown.textContent = timerFormat(startTime);
  clock(); // <-- here
  if (startTime === 0) clearInterval(counter);
};

The second issue is that you don't pass ctx and radius when you call showTime():

clock = () => {
  ctx.arc(0, 0, radius, 0, 2 * Math.PI, true);
  ctx.fillStyle = "#f5afaf";
  ctx.fill();
  showTime(ctx, radius); // <-- here
};

The third issue you will have now is Cannot access 'startTime' before initialization. This is because you re-declare startTime in the showTime() function. Update this function to:

showTime = (ctx, radius) => {
  let time = // <-- here
    (startTime * Math.PI) / 30 + (startTime * Math.PI) / (30 * 60);
  hand(ctx, time, radius * 0.8, radius * 0.07);
};

The last thing you need to do is call finish the path when you draw the hand:

hand = (ctx, position, length, width) => {
  ctx.beginPath();
  ctx.lineWidth = width;
  ctx.lineCap = "round";
  ctx.moveTo(0, 0);
  ctx.rotate(position);
  ctx.lineTo(0, -length);
  ctx.stroke();
  ctx.rotate(-position);
  ctx.closePath(); // <-- here
};

Now the hand moves with the timer. However, if I may, I wanted to suggest a few improvements. Currently, you move the canvas to get the center of your clock at coordinates (0,0), and then you move the canvas two more times every time you draw the hand. I suggest you leave the canvas alone and rather specify an offset that tells you where the center of the clock is.

Now you will need to specify the exact coordinates to the tip of the hand, which can be done with some quick trigonometry (see full example below).

Last thing, the starting position of your hand is somewhere in the bottom right. Perhaps a good position for time 0 would be on top. If you want to change it you can add some ratio of PI to the angle calculated in showTime() (e.g. add + Math.PI to have time 0 at the bottom.

See the full example below:

((d) => {
    let btn = d.getElementById("btn");
    let reset = d.getElementById("reset");
    let countdown = d.getElementById("countdown");
    let btnLength = d.getElementById("btnLength");
  
    // 25 Minutes
    let short = 1500;
    // 45 Minutes
    let med = 2700;
    // 90 Minutes
    let long = 5400;
  
    let counter;
    let startTime = short;
  
    let timerFormat = (s) => {
      return (s - (s %= 60)) / 60 + (9 < s ? ":" : ":0") + s;
    };
  
    countdown.textContent = timerFormat(startTime);
  
    let timer = () => {
      startTime--;
      countdown.textContent = timerFormat(startTime);
      clock(); // <-- here
      if (startTime === 0) clearInterval(counter);
    };
  
    let start = () => {
      counter = counter || setInterval(timer, 950);
    };
  
    let stop = () => {
      clearInterval(counter);
      counter = undefined;
    };
  
    // Changes between Start and Stop button labelling.
  
    btn.addEventListener("click", () => {
      if (counter) {
        stop();
        btn.textContent = "Start";
      } else {
        start();
        btn.textContent = "Stop";
      }
    });
  
    // Stops counter, resets to original start time and
    // resets button label to Start.
  
    reset.onclick = () => {
      stop();
      startTime = short;
      if (btn.textContent === "Stop") btn.textContent = "Start";
      btnLength.textContent = "Short";
      countdown.textContent = timerFormat(startTime);
    };
  
    // Changes timer length and button labelling
  
    btnLength.addEventListener("click", () => {
      if (startTime === short) {
        startTime = med;
        btnLength.textContent = "Medium";
      } else if (startTime === med) {
        startTime = long;
        btnLength.textContent = "Long";
      } else if (startTime === long) {
        startTime = short;
        btnLength.textContent = "Short";
      }
      countdown.textContent = timerFormat(startTime);
    });
  
    //Clock
  
    let c = d.getElementById("canvas");
    let ctx = c.getContext("2d");
    let offset = c.height / 2;
    let radius = offset * 0.9;
  
    showTime = (ctx, radius) => {
      let second = startTime % 60;
      let angle = (2*Math.PI) - (2 * Math.PI * second) / 60; // Add + Math.PI; to put time 0 in the bottom

      hand(ctx, angle, radius * 0.8, radius * 0.07); 
    };
  
    hand = (ctx, angle, length, width) => {
      // Calculate coordinates of the tip of the hand.
      x = offset + Math.round(- length * Math.sin(angle));
      y = offset + Math.round(- length * Math.cos(angle));

      ctx.beginPath();
      ctx.lineWidth = width;
      ctx.lineCap = "round";
      ctx.moveTo(offset, offset);
      ctx.lineTo(x, y);
      ctx.stroke();
      ctx.closePath();
    };
  
    clock = () => {
      ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
      ctx.arc(offset, offset, radius, 0, 2 * Math.PI, true);
      ctx.fillStyle = "#f5afaf";
      ctx.fill();
      showTime(ctx, radius); 
    };
  
    clock();
  })(document);
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Document</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    
    <script defer src="../Desktop/timer.js"></script>
  </head>
  <body>
    <div class="btn__container">
      <div id="countdown"></div>
      <button class="btn" id="btn">Start</button>
      <button class="btn" id="btnLength">Short</button>
      <button class="btn" id="reset">Reset</button>
      </div>
<div class="clock">
<canvas  id="canvas" width="300" height="300" style="background-color:none"></canvas>
</div>
  </body>
</html>

Upvotes: 1

Related Questions