Making a Speedrun Timer: Chapter 1
Tags: vue, webdev, javascript, devjournal
Published: 2023-03-30 18:10:18

## Making the Timer ⏱️

In the previous [post](https://dev.to/kevthedev/making-a-speedrun-timer-prologue-4di), we explained what we're making (a speedrun timer), why we're making it, and how to setup the project. Now we're ready to get to the good stuff! To start, we'll want to get the basic core functionality of our app working. 

### The Stopwatch Component 🏗️
A "speedrun timer" is essentially just a stopwatch with speedrun-related functionality built on top of it, so we can begin by creating a simple stopwatch component with a composable:

```html
// Stopwatch.vue
<script setup>
import { useStopwatch } from '../composables/stopwatch';

const {
  timerTxt,
  onTimerStart,
  onTimerStop,
  onTimerReset
} = useStopwatch();

</script>

<template>
  <div>
    <p>{{ timerTxt }}</p>
    <button @mousedown="onTimerStart">Start</button>
    <button @mousedown="onTimerStop">Stop</button>
    <button @mousedown="onTimerReset">Reset</button>
  </div>
</template>

<style scoped>
button {
  margin: 0 5px;
}
</style>
```
As you can see, there's nothing fancy here, just 3 buttons with event functions and some text to display our timer.

We'll use it directly in `App.vue` for now:

```html
// App.vue
<script setup>
import Stopwatch from './components/Stopwatch.vue';
</script>

<template>
  <div>
    <stopwatch></stopwatch>
  </div>
</template>
```

### The Stopwatch Composable 📝
Now let's build our stopwatch composable. We can begin by declaring some necessary variables. I've commented on their uses in the code below:

```javascript
let [milliseconds, seconds, minutes, hours] = [0, 0, 0, 0]; // state of the values used for the stopwatch display
let timer; // keeps track of an active timer
let paused = false; // keeps track of paused state
let prevTime; // the last time the timer was updated

const timerTxt = ref(getTimeFormatString()); // the timer display text value
```

#### onTimerStart() 🏃
Next, we'll want to create our `onTimerStart` function that will be triggered on a start button press (we'll likely move this to a keypress later, but we'll keep the buttons for now):

```javascript
function onTimerStart() {
  if (timer) {
    return;
  }
  
  paused = false;
  prevTime = new Date();
  timer = setInterval(incrementTimer, 10);
}
```
Breaking this down, first we'll want to detect if the timer is already running. If it is, we can short circuit the function and do nothing. Next we ensure our timer isn't in a paused state, update the time our timer was last ran, and start the timer by calling `setInterval` and telling it to run our function `incrementTimer` every 10 milliseconds.

#### Non-Event Functions 🤓
Speaking of the `incrementTimer` function, let's go ahead and define that function and the other functions it will reference:

```javascript
function incrementTimer() {
  if (paused) {
    return;
  }
  
  const elapsedMilliseconds = ((new Date()) - prevTime);
  setTimeValues(elapsedMilliseconds);
  timerTxt.value = getTimeFormatString();
  prevTime = new Date();
}

function setTimeValues(elapsedMilliseconds) {
  milliseconds += elapsedMilliseconds;
  
  if (milliseconds >= 1000) {
    seconds += (milliseconds - (milliseconds % 1000)) / 1000;
    milliseconds = milliseconds % 1000;
    if (seconds >= 60) {
      minutes += (seconds - (seconds % 60)) / 60;
      seconds = seconds % 60;
      if (minutes >= 60) {
        hours += (minutes - (minutes % 60)) / 60;
        minutes = minutes % 60;
      }
    }
  }
}
  
function getTimeFormatString() {
  const h = hours.toLocaleString('en-US', {
    minimumIntegerDigits: 2,
    useGrouping: false
  });
  const min = minutes.toLocaleString('en-US', {
    minimumIntegerDigits: 2,
    useGrouping: false
  });
  const s = seconds.toLocaleString('en-US', {
    minimumIntegerDigits: 2,
    useGrouping: false
  });
  const ms = milliseconds.toLocaleString('en-US', {
    minimumIntegerDigits: 3,
    useGrouping: false
  });
  
  return `${h}:${min}:${s}.${ms}`;
}
```
We'll examine these functions and their usage below:

##### incrementTimer() ➕
This code was designed to be called at any given moment and increment the timer display based on how much time has elapsed from the previous call to the current one. We can't _guarantee_ that `incrementTimer` will be called every 10 milliseconds as `setInterval` merely places `incrementTimer` into a queue after 10 seconds. The actual execution time of the function can easily vary, making it very apparent why we should measure the time between one call to the next instead!

##### setTimeValues(elapsedMilliseconds) ⌚
This function is designed to take in any amount of elapsed time and increment the time values appropriately. We could have assumed a consistent time frame between each execution (e.g. 1 second/1000 milliseconds), but that would not have been a reliable solution given the single-threaded nature of JavaScript and the potential for the thread to hang, sleep, or freeze. Therefore, we need our timer to pick up where it left off. To do this, I needed the algorithm to account for any amount of time passed (even if it took 5 minutes from one call to the next) and take in that parameter and increment it onto the timer values. Which is why you see code like this: `seconds += (milliseconds - (milliseconds % 1000)) / 1000;` and `milliseconds = milliseconds % 1000;`

For example, if our function was called 5001 milliseconds from the last call, then we know we should be adding 5 seconds and 1 millisecond to our time values: 

`seconds += (5001 - (5001 % 1000)) / 1000; //This equals 5`
`milliseconds = 5001 % 1000; //This equals 1`

##### getTimeFormatString() 📝
This code just formats the timer values into the time display format that we want.

### onTimerStop() 🛑
Moving on to the `onTimerStop` function, things get less complicated. We need to set `paused = true;`, stop the timer, and finally clear the timer.

```javascript
function onTimerStop() {
  paused = true;
  clearInterval(timer);
  timer = null;
}
```

### onTimerReset() 🔃
Last but not least, we'll want to reset our timer. This code is very similar to `onTimerStop`, but we also want to reset our timer values to their default values after stopping and clearing the timer.

```javascript
function onTimerReset() {
  paused = true;
  clearInterval(timer);
  [milliseconds, seconds, minutes, hours] = [0, 0, 0, 0];
  paused = false;
  timerTxt.value = getTimeFormatString();
  timer = null;
}
```

### Full Composable 💯
That'll be everything we need for now so here's the full composable code:
```javascript
// stopwatch.js
import { ref } from "vue";

export function useStopwatch() {
  let [milliseconds, seconds, minutes, hours] = [0, 0, 0, 0];
  let timer;
  let paused = false;
  let prevTime;

  const timerTxt = ref(getTimeFormatString());

  function setTimeValues(elapsedMilliseconds) {
    milliseconds += elapsedMilliseconds;

    if (milliseconds >= 1000) {
      seconds += (milliseconds - (milliseconds % 1000)) / 1000;
      milliseconds = milliseconds % 1000;
      if (seconds >= 60) {
        minutes += (seconds - (seconds % 60)) / 60;
        seconds = seconds % 60;
        if (minutes >= 60) {
          hours += (minutes - (minutes % 60)) / 60;
          minutes = minutes % 60;
        }
      }
    }
  }

  function getTimeFormatString() {
    const h = hours.toLocaleString("en-US", {
      minimumIntegerDigits: 2,
      useGrouping: false,
    });
    const min = minutes.toLocaleString("en-US", {
      minimumIntegerDigits: 2,
      useGrouping: false,
    });
    const s = seconds.toLocaleString("en-US", {
      minimumIntegerDigits: 2,
      useGrouping: false,
    });
    const ms = milliseconds.toLocaleString("en-US", {
      minimumIntegerDigits: 3,
      useGrouping: false,
    });

    return `${h}:${min}:${s}.${ms}`;
  }

  function incrementTimer() {
    if (paused) {
      return;
    }

    const elapsedMilliseconds = new Date() - prevTime;
    setTimeValues(elapsedMilliseconds);
    timerTxt.value = getTimeFormatString();
    prevTime = new Date();
  }

  function onTimerStart() {
    if (timer) {
      return;
    }

    paused = false;
    prevTime = new Date();
    timer = setInterval(incrementTimer, 10);
  }

  function onTimerStop() {
    paused = true;
    clearInterval(timer);
    timer = null;
  }

  function onTimerReset() {
    paused = true;
    clearInterval(timer);
    [milliseconds, seconds, minutes, hours] = [0, 0, 0, 0];
    paused = false;
    timerTxt.value = getTimeFormatString();
    timer = null;
  }

  return {
    timerTxt,
    onTimerStart,
    onTimerStop,
    onTimerReset,
  };
}
```

## Seeing the Code in Action! 🎬
Now to run our code and see it in action! I've put the code for Chapter 1 inside of a [CodeSandbox](https://codesandbox.io) so we can visualize what our code looks like while it's running:

{% embed https://codesandbox.io/embed/modest-joji-tw7oe5?fontsize=14&hidenavigation=1&theme=dark&view=preview %}

## Conclusion 💭
Ta-da! We now have a basic stopwatch timer working. In Chapter 2, we'll enhance the timer a bit and abstract the logic to make the code more portable.

Here's the [GitHub repo](https://github.com/kdevcse/SpeedrunTimerClient) for this project. If you haven't already, be sure to give me a follow on here and other socials to see updates to this series and more!