Repository navigation
[Feature] Add the Matrix date/time drop - #118
Conversation
Stamps the date and time at the top of the screen at the start of every minute, holds it there for ten seconds, then peels the string away one character at a time and lets it fall out of frame as scrambled code with fading glyph trails - the digital clock from the films. - js/clock.js: the overlay, plus pure clockText()/clockPhase() helpers so the timing can be unit tested. It is a 2D canvas above the renderer rather than part of the WebGL pipeline: the rain draws from a fixed MSDF atlas, so an arbitrary string has no glyphs to sample. - js/config.js: dateTimeOverlay defaults to "auto", which enables it for a phone-sized installed/fullscreen PWA - the case it was asked for - and leaves desktop browser tabs alone. ?dateTimeOverlay=true/false forces it either way. - js/main.js: starts the overlay alongside the renderer; it survives mode switches since it reads the wall clock, not the renderer config. - tests/clock-phase.test.mjs + tests/matrix-clock.spec.js: phase maths and a browser smoke test for both phases.
🎬 PR Preview Deployed!Your changes are now available for testing: Preview URL: https://ap0ught.github.io/matrix/pr-118/ Test Links:The preview will be updated automatically when you push new commits. Preview deployed from commit ad24ba3 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ad24ba3f01
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| import MultiMonitorManager from "./multi-monitor.js"; | ||
| import { setMultiMonitorManager, setMatrixConfig } from "./fullscreen.js"; | ||
| import { updateFavicon } from "./favicon.js"; | ||
| import DateTimeOverlay from "./clock.js"; |
There was a problem hiding this comment.
Add the clock module to the offline precache
The new static import is not accompanied by a js/clock.js entry in service-worker.js's STATIC_ASSETS. After a fresh install or service-worker upgrade, an offline launch can load the cached HTML and js/main.js but fail while resolving this uncached module, preventing the entire app from starting. Precache the new module alongside the other JavaScript modules.
Useful? React with 👍 / 👎.
| // Below the gallery fortunes (20000), playlist (15000) and mode display (1000). | ||
| zIndex: "100", | ||
| }); | ||
| document.body.appendChild(this.canvas); |
There was a problem hiding this comment.
Keep the overlay inside the fullscreen subtree
When the clock is enabled and the user enters fullscreen through the built-in double-click handler, js/fullscreen.js requests fullscreen on the renderer canvas itself. This overlay is appended as a sibling under document.body, so it remains below the fullscreen top layer and disappears behind the opaque renderer canvas. Request fullscreen on a shared container or otherwise place the overlay within the fullscreen element.
Useful? React with 👍 / 👎.
| // Date/time clock overlay: "auto" | "true" | "false" | ||
| dateTimeOverlay: { key: "dateTimeOverlay", parser: (s) => s }, |
There was a problem hiding this comment.
Propagate the clock option to uniform child displays
For fullscreenUniform multi-monitor sessions, MultiMonitorManager.serializeConfig() builds each child URL from an explicit allowlist in js/multi-monitor.js, and that list does not include this new setting. Consequently, dateTimeOverlay=true on the coordinator becomes the default auto in desktop child windows, where the clock is disabled, so only the coordinator can show it. Add this parameter to the uniform-mode serialization path.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The falling animation currently updates position once per second, trails are initially suppressed, and configuration behavior has unresolved inconsistencies.
Review effort: Balanced
Findings: 5
Open (5)
Use millisecond precision for continuous glyph movement · New Restrict auto overlay behavior to phone-sized viewports · New Skip out-of-range trail copies without breaking the loop · New Serialize dateTimeOverlay in all configuration serializers · New Watch and assert errors from the falling overlay page · New
What changed in this PR
Adds a renderer-independent canvas overlay that displays and drops the current date/time for phone PWAs.
Changes:
- Implements clock formatting, timing, animation, and configuration.
- Integrates the overlay into application startup.
- Adds documentation and automated tests.
| File | Description |
|---|---|
js/clock.js |
Implements the date/time overlay. |
js/config.js |
Adds URL configuration and default behavior. |
js/main.js |
Starts the overlay with the renderer. |
README.md |
Documents the URL option. |
tests/clock-phase.test.mjs |
Tests formatting and phase calculations. |
tests/matrix-clock.spec.js |
Tests browser rendering and enablement. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| const secondsIntoMinute = date.getSeconds(); | ||
| if (secondsIntoMinute < CLOCK_HOLD_SECONDS) { | ||
| return { phase: "hold", progress: 0, secondsIntoMinute }; | ||
| } | ||
| return { | ||
| phase: "fall", | ||
| progress: (secondsIntoMinute - CLOCK_HOLD_SECONDS) / CLOCK_FALL_SECONDS, |
| const installed = matchMedia("(display-mode: standalone)").matches || matchMedia("(display-mode: fullscreen)").matches; | ||
| const touch = matchMedia("(pointer: coarse)").matches || (navigator.maxTouchPoints ?? 0) > 0; | ||
| return installed && touch; |
| for (let t = this.trailLength; t >= 1; t--) { | ||
| const trailY = y - step * t; | ||
| if (trailY < headerY - step) break; |
| // Date/time clock overlay: "auto" | "true" | "false" | ||
| dateTimeOverlay: { key: "dateTimeOverlay", parser: (s) => s }, |
| const falling = await page.context().newPage(); | ||
| await openWithClock(falling, "dateTimeOverlay=true", 32); | ||
|
|
||
| const extent = await paintedExtent(falling); | ||
| expect(extent.pixels, "the clock should paint something").toBeGreaterThan(100); | ||
| // Characters peel off in order, so glyphs are well down the screen by mid-fall... | ||
| expect(extent.bottom).toBeGreaterThan(VIEWPORT.height * 0.4); | ||
| // ...and the header no longer holds the date/time, it holds scrambled glyphs and trails. | ||
| const fallingHeader = await headerBandBrightness(falling); | ||
| expect(fallingHeader).not.toBe(heldHeader); | ||
|
|
||
| await falling.close(); | ||
| watchers.assertNoIssues("clock fall"); |

Summary
Adds the digital clock from the films: the current date and time appears at the top of the screen at the start of every minute, holds for ten seconds, then peels away one character at a time and falls out of frame as scrambled code with fading glyph trails.
Requested for the installed PWA on a phone, so
dateTimeOverlaydefaults to"auto": on when the app is a phone-sized installed or fullscreen PWA, off in a desktop browser tab.?dateTimeOverlay=true/=falseforces it either way.Changes
js/clock.js— the overlay, plus pureclockText()/clockPhase()helpers so the timing logic can be unit tested without a browser. Phases: hold for seconds 0–9 of the minute, fall for seconds 10–59 (progress 0 → 1), nothing drawn in between. Characters leave the header in order, so the string visibly peels; the ones still waiting keep showing the real date/time. Falling glyphs scramble, accelerate (as the rain does rather than sliding linearly), and trail fading copies of themselves.js/config.js—dateTimeOverlaydefault and URL parser.js/main.js— starts the overlay fromstartMatrix(), once. It sits atz-index: 100, below the gallery fortunes/playlist and the mode display, and survives mode switches since it reads the wall clock rather than the renderer config.Why a 2D canvas and not WebGL
The rain renders from a fixed MSDF atlas, so an arbitrary string has no glyphs to sample — plumbing variable text into the rain pipeline would mean generating glyphs at runtime. A
pointer-events: none2D canvas above the renderer keeps it renderer-agnostic (works with webgl, webgpu, three and p5) and costs one small canvas.Testing
tests/clock-phase.test.mjs— the text format (including zero padding) and the phase boundaries: hold below ten seconds, fall from ten, monotonic progress, nothing left to draw at the roll-over.tests/matrix-clock.spec.js— browser smoke test with the clock pinned to a fixed second: in the hold the painted pixels stay in the header band; mid-fall glyphs are well down the screen and the header band no longer matches the hold's;dateTimeOverlay=falseand theautodefault in a normal tab both draw nothing.npm test— 22 passed, 2 skipped (WebGPU unavailable here)npm run test:regression— full version × effect matrixChecked in Chromium at phone viewport sizes, hold and fall frames
npx prettier --check --use-tabs --print-width 160onjs/**andtests/**Tested on Chrome (Linux, desktop and 420×860 phone viewport)
Tested classic variant
Tested on Firefox
Tested on a real phone (needs the deploy first — the installed PWA caches the old build until the next deploy)
Performance validated
Notes
CLOCK_HOLD_SECONDS); the fall takes the remaining fifty. Both are exported so they are easy to retune.