Language

Get in touch

Android app · since 2025

ClearLap

Android stopwatch with lap times, built as one offline web page in a Cordova shell.

Released
Runtime permissions
None
Ads
None
Price
Free
  • HTML
  • CSS
  • JavaScript
  • Apache Cordova (Android)
  • Web Audio API
  • Screen Wake Lock API
  • Vibration API
Google Play play.google.com

About

ClearLap is a stopwatch with laps, best and worst lap highlighting, click sounds, vibration where the WebView supports it, and a full-screen dark layout. It has no ads, no accounts, no network use and no plugins: the whole app is one HTML file of about 8 KB with inline CSS and JavaScript.

The engineering work is in doing the basics correctly inside a WebView: time that does not drift when frames are dropped or the app is in the background, laps that always add up to the total, a screen wake lock requested where the WebView supports it, and feedback sounds synthesized without audio files.

ClearLap · stopwatch

what was solved

LIVEsince 2025

Timing core

in the product

What it givesTime stays exact when the phone hides the app, pausing never adds or loses time, laps add up to the total, and the digits do not jump around.

Problem
A stopwatch in a WebView cannot trust timers or frame callbacks to count time: they are throttled, skipped or paused when the app is in the background.
Solution

Time is always computed from timestamps; the frame loop only draws. Pause, resume and laps are arithmetic on stored timestamps.

Stack
  • JavaScript
  • requestAnimationFrame
Tasks · 5
Interval counters drift and stop while the app is hiddenElapsed time is Date.now() minus a start timestamp, recomputed on every frame

tick() never increments a counter. It sets elapsed = Date.now() - startTs and redraws. If frames are dropped or the WebView suspends requestAnimationFrame in the background, the next frame shows the correct time because nothing was accumulated.

Pause and resume must not lose or add timeOn resume, rebase the start: startTs = Date.now() - elapsed

Pause cancels the animation frame and keeps elapsed as is. Resume moves startTs back by the already elapsed amount, so the formula in tick() continues from the same value without a separate 'paused duration' variable.

Reset cancels the frame loop, zeroes the state and disables the Lap button; doLap() also returns early when the watch is not running.

Lap durations must add up exactly to the total timeEach lap is elapsed minus the previous lap mark, so laps telescope to the total

doLap() stores the lap number, its duration (elapsed - lapElapsed) and the running total, then moves lapElapsed to elapsed. Because every lap starts where the previous one ended, the sum of lap durations equals the total by construction, with no rounding gap.

The current lap counter under the main time uses the same mark: elapsed - lapElapsed.

Best and worst lap marking is meaningless with one lapCompute min and max over all laps on each render; highlight only when there are 2+

renderLaps() rebuilds the list newest first, finds the shortest and longest durations and adds best/worst classes only when more than one lap exists.

Digits jump sideways as the time changesMonospace font plus font-variant-numeric: tabular-nums on every time display

All time fields use fixed-width digits, so the display at 16vw does not shift as centiseconds change. fmt() always prints mm:ss.cc with zero padding.

Device behaviour without plugins

in the product

What it givesThe screen stays on during long sessions, the app fills the whole screen, and taps give a beep and a buzz, without ever disturbing the timing.

Problem
Screen-on, full screen, sound and haptics usually come from Cordova plugins, and browsers allow some of them only after a user gesture.
Solution

Use web platform APIs from the WebView where they exist, request them at the right moment, and make every one of them optional so a missing API never breaks timing.

Stack
  • Screen Wake Lock API
  • Fullscreen API
  • Web Audio API
  • Vibration API
  • Cordova config.xml
Tasks · 4
Screen turns off during a long timing sessionRequest a screen wake lock where the WebView supports it, and again whenever the page becomes visible

The system releases a wake lock when the page is hidden. requestWakeLock() runs when the page loads and on every visibilitychange to visible, and the release listener clears the reference. The call is skipped when navigator.wakeLock is absent and errors are swallowed, so the stopwatch works the same either way.

Full screen can only be requested from a user gestureRequest it on the first touch, with a webkit fallback, then drop the listener

A one-time touchstart listener calls requestFullscreen or webkitRequestFullscreen and removes itself. config.xml also sets the Cordova Fullscreen preference for the Android build.

Click sounds without audio files and without clicks or popsSynthesize short sine beeps with Web Audio and an exponential gain fade-out

beep() creates an oscillator and gain node per press and ramps gain exponentially to 0.001 over the beep length, which avoids the pop of an abrupt stop. Start, lap and reset use different pitches and lengths (880, 1100 and 440 Hz).

The AudioContext is created lazily inside the first button handler, which satisfies the autoplay rule that audio must start from a user gesture.

Feedback failures must never interrupt timingSound and vibration run before state changes, only where the API exists, and inside try/catch

beep() and vibe() catch every exception, and vibe() calls navigator.vibrate only where it is available. Start, lap and reset request different vibration lengths (25, 15 and 40 ms).

config.xml declares no plugins and the page makes no network calls, so the timing core depends only on the WebView.

Numbers

from the product
  • Runs on Android 5.0 and newer
  • App code about 8 KB One HTML file, no frameworks.
  • Precision 1/100 s