Skip to content

Stopwatch

A stopwatch that doesn't drift over time.

This example also demonstrates how state and logic can be separated from it's representation if needed.

import { $, batch, Expression, get, Index, Nest, Overwrite, Show, watchUpdates } from "rvx";
import { useAnimation } from "rvx/async";

export function Example() {
    const stopwatch = new Stopwatch();

    return <div class="column">
        <Overwrite style={{ "font-size": "2rem" }}>
            <Time value={stopwatch.time} />
        </Overwrite>
        <div class="row">
            <Nest watch={stopwatch.running}>
                {running => running
                    ? <button on:click={stopwatch.stop}>Stop</button>
                    : <button on:click={stopwatch.start}>Start</button>
                }
            </Nest>
            <button on:click={stopwatch.lap} disabled={() => !stopwatch.running()}>Lap</button>
            <button on:click={stopwatch.reset}>Reset</button>
        </div>
        <ul>
            <Index each={stopwatch.laps}>
                {(lap, i) => <li><Time value={lap - (stopwatch.laps()[i - 1] ?? 0)} /></li>}
            </Index>
        </ul>
    </div>;
}

class Stopwatch {
    #runningSince = 0;
    #running = $(false);
    #time = $(0);
    #laps = $<number[]>([]);

    constructor() {
        watchUpdates(this.#running, running => {
            if (running) {
                this.#runningSince = performance.now() - this.#time.value;
                useAnimation(() => {
                    this.#time.value = performance.now() - this.#runningSince;
                });
            }
        });
    }

    time = () => this.#time.value;
    running = () => this.#running.value;
    laps = () => this.#laps.value;

    start = () => { this.#running.value = true };
    stop = () => { this.#running.value = false };

    reset = () => batch(() => {
        this.#running.value = false;
        this.#time.value = 0;
        this.#laps.value = [];
    });

    lap = () => {
        this.#laps.update(laps => {
            laps.push(this.#time.value);
        });
    };
}

function Time(props: { value: Expression<number> }) {
    return <code>
        <Show when={() => Math.floor(get(props.value) / 1000 / 60 / 60)}>
            {hours => <>{hours}:</>}
        </Show>
        {() => Math.floor(get(props.value) / 1000 / 60) % 60}
        :
        {() => (Math.floor(get(props.value) / 1000) % 60).toFixed(0).padStart(2, "0")}
        .
        {() => (Math.floor(get(props.value)) % 1000).toFixed(0).padStart(3, "0")}
    </code>;
}