ClearLap
Android stopwatch with lap times, built as one offline web page in a Cordova shell.
- Released
- Runtime permissions
- None
- Ads
- None
- Price
- Free
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
Timing core
in the productWhat 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
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 productWhat 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 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