Timer
Programmable timer web app – e.g. for gym workouts or stretching sessions
I have a mild habit of building my own version of things that surely already exist. This time it’s a timer – one that counts down, beeps, and reads activity names out loud. But it’s also “programmable”: you create a sequence of activities – for stretching and exercise sessions, say, where you hold body positions for set times – and structure them freely, with breaks in between or as repeatable sequences.
Timer programs are encoded in a declarative language, where a sample program could look like so:1
0:10 Get ready
4x
0:30 Work out
0:15* Rest
3x
0:45 Stretch
0:15 Relax
0:10* Change position
0:45 Cool down
In words: after starting the timer, you have 10 seconds to get ready; then you do your 30-second workout 4 times in a row, with a 15-second break in between (which, however, is skipped on the last loop iteration, denoted by the *); then you do a stretch-relax-reposition sequence 3 times; and eventually, you have a final 45-second cool down before the program ends.
As with many such side projects, my timer app started out fairly minimal. But I kept enjoying it, and kept adding little tweaks and features over time. At first, I only shared it with a few friends, who started to use it and gave good feedback on what to improve (one asked me to make the beep volume adjustable, after the default nearly launched them off their yoga mat). Now I’m making it public, on the off chance others enjoy it too. You’ll find the source code on GitHub, or you can just use the app here.
Tech Talk
Under the hood, the timer is a single-page app written in TypeScript and React. The app is all static, so there is no backend or server. Programs are fully encoded in the URL, so you can store them as bookmarks, and share or transfer them via URL or QR code. That may not appeal to the masses, but I personally find it pretty handy, and it was rather simple to implement. Considering the typical lengths of such programs, the size limits of URLs and QR codes should also be plenty for most scenarios.
This is the URL of the aforementioned demo program:
https://timer.jotaen.net/#sports/1:1kwe:U3BvcnRzIQowOjEwIEdldCByZWFkeQo0eAogIDA6MzAgV29yayBvdXQKICAwOjE1KiBSZXN0CjN4CiAgMDo0NSBTdHJldGNoCiAgMDoxNSBSZWxheAogIDA6MTAqIENoYW5nZSBwb3NpdGlvbgowOjQ1IENvb2wgZG93bg==
You can break up the URL into these parts:
https://timer.jotaen.net/#– the base URL, delimited by a hash (so everything that follows is frontend-only)sports– human-readable, URL-safe slug, as a mere convenience to help you make sense of the URL when managing it manually/– artificial delimiter to visually offset the slug1– the encoding version (to facilitate compatibility, should the program format evolve):– delimiter1kwe– checksum:– delimiterU3Bvc…– the base64-encoded program (including the title)
The programs themselves are written in a plain-text notation. While I’ll honestly admit that I do have a certain fondness for plain-text notations, the decision here was mainly a practical one: a decent UI would have been quite a bit of extra work, especially to optimise it for mobile. Also, the plain-text format happens to mirror the visual layout of a GUI fairly closely.
The app settings are stored in local storage, so they’ll stay on your specific device, and won’t be transferred via the URL.
Implementation Challenges
Most of the timer app was straightforward to build. But three parts had a way of looking trivial right up until they weren’t.
First and foremost are the browsers’ audio APIs, which the timer app uses for reading out activity names and creating the “beep” sound effect for the countdown. The APIs themselves seem simple enough, but I ran into a few subtle (yet significant) intricacies in practice. For example, on mobile, the “beep” countdown would get ignored, seemingly at random. It took me a while to find the culprit: certain browsers silently mute the oscillator audio API unless the sound is triggered by a direct user interaction. So a “beep” tied to a click or touch gesture is fine – but emitting one on a plain timeout or interval is blocked. Solution: emit an inaudible beep via a global event handler on the very first user interaction, to basically “approve” the oscillator instance. Independent of that, “beeping” can also be restricted on mobile if the device ringtone is muted (which doesn’t apply to other audio APIs, though). Needless to say, it just fails silently, so you can neither gracefully handle this nor really tell what’s going on in the first place.
The voice side was no kinder: loading the list of available voices happens asynchronously for some reason. A voiceschanged event is supposed to let you handle that nicely – except that it doesn’t fire reliably, so you need to put an additional polling mechanism in place. Certain privacy-focussed browsers (looking at you, Brave on iOS) also appear to have bugs in their anti-fingerprinting implementations, so calls to the voice APIs can throw errors even though they aren’t supposed to. All that, unfortunately, leads to overly defensive code that’s riddled with seemingly unmotivated checks and try/catch blocks.
The background animation also cost me some time (the faint blurry bubbles which are floating around). It serves no purpose whatsoever, which is rather the point – but I like the aesthetics and the visual depth it creates. I first implemented this naively with some CSS-positioned <div> boxes and a CSS blur() filter, but I quickly noticed how GPU-intensive this rendering approach is. That isn’t only a question of honour, but actually has real consequences on mobile, where it would drain the battery for no good reason. In the end, I had to recreate the background effect with a full-sized, JS-powered <canvas> element and tweak the parameters manually to find a good middle ground between aesthetics and efficiency.
Last but not least, I dedicated extra care to the styling of the main timer (XX:XX). Micro-optimising the kerning and padding around the center colon was the easier exercise; getting the timer to always maximize its font size across every viewport size and orientation was the finicky part. (I partly have to blame that on myself, however, because I challenged myself to do it in pure CSS.) I eventually ended up with a – by my standards – quite sophisticated CSS declaration, which combines clamping and container queries, spiced with some obscure magic numbers2:
.main-timer {
font-size: max(
3em,
min(
calc(100cqw / 5 / 0.6),
calc(100cqh / (2 * 0.6))
)
);
}
Closing Thoughts
So there it is: another app that surely already existed, now existing again in my own particular flavour. The main thing I’ll take away development-wise is that certain browser APIs can still feel like the wild west of the early-2000s web. Except that, by now, we really ought to have consistent, reliable standards. Except that, apparently, we don’t.
-
The main purpose of this sample program is to demonstrate the timer’s abilities, not so much to represent a realistic workout scenario. ↩︎
-
The magic numbers aren’t actually that obscure:
0.6is a font-specific sizing factor,5is the length of the string (XX:XX), and3em/2control the lower and upper boundaries. ↩︎