Flashing-content warning: this project deals with strobing images. Our demo media uses low-contrast, small flashes.
Inspiration
About 3% of people with epilepsy have photosensitive epilepsy: flashing lights or patterns can trigger seizures. Games and videos are full of flashes (explosions, lightning, muzzle flashes, strobing effects), and most content has no warning. In 1997, one flashing scene in a Pokémon episode sent hundreds of children in Japan to hospital, most with no history of seizures.
Existing tools mostly work inside one website, like YouTube, or analyze video files offline. Nothing protects everything on your screen, including games. We wanted people who are sensitive to flashing to be able to just play.
What it does
CalmFrame is a Windows app that watches the whole screen and detects flashing that breaks the WCAG 2.3.1 flash thresholds (more than 3 flashes per second over a large enough area, or saturated red flashes). When it finds some, it covers the screen with a smoothed copy in which the flashing becomes a gentle change, then fades back out when the flashing stops.
- Works across apps: games in borderless-windowed mode, video players, browsers
- Runs fully offline: no frame ever leaves your computer
- A dashboard shows the status (Protected / Smoothing / Protection off), a live flashes-per-second graph against the WCAG limit, and a log of every smoothed event
- A sensitivity slider, an "Ignore movement" switch, a tray icon, and a global kill switch (Ctrl+Alt+Shift+C)
- Its own interface never flashes. The raw preview is kept small enough to stay under the hazard area by design.
How we built it
- Capture: a small C++ module uses Windows Desktop Duplication to copy each new screen frame into a shared GPU texture, which Electron hands to a transparent overlay window at 1920×1080, 60 fps. Chromium's screen capture is the automatic fallback.
- Detection (TypeScript, pure and fully unit-tested): each frame is converted on the GPU to linear-light luminance on a 96×54 grid. A per-cell tracker counts brightness reversals, and the WCAG area rule checks whether enough of a 10° visual field is flashing together.
- Mitigation (WebGL2): a per-pixel running average whose strength is solved from the detected flash, plus dimming when smoothing alone can't get under the threshold. On our 10 Hz test strobe, the swing left after smoothing is 0.070 to 0.074, under the WCAG 0.10 threshold.
- Measured, not guessed: from captured frame to protected frame drawn, 20 ms median, 28 ms at the 95th percentile, from the app's own log.
- Validated against IRIS: our validator runs the same detector code on video files and compares it second by second with the community Python port of EA's IRIS analyzer. In IRIS-compatible mode, CalmFrame agrees on 110 of 110 seconds of our synthetic test set. In WCAG mode it agrees on 90.9%, and every difference is an expected difference between the two standards' area rules.
- 298 unit tests, and a strobe test page that starts paused, uses low contrast, and stops itself after 5 seconds.
- [TODO after filming] Slow-motion measurement at 240 fps: at 10 Hz, X flashes reach the viewer with protection off vs. Y with it on.
Built with Coder
Everything that doesn't need the Windows desktop (the detector core, its tests, and the IRIS validator) was built and tested in our team's Coder workspace. We logged every test run there in docs/CODER-LOG.md as we went: 10 runs overnight, ending at 298 passing tests on main.
Built with Entire
Every Claude Code session was captured with Entire. Each commit links to the prompts and session that produced it, and each build phase ran as its own Trail with the spec's checklist in its body. When we switched laptops between phases, entire search gave the next session the decisions the previous one made, instead of re-deriving them.
Challenges we ran into
- Scrolling looked like flashing. In live testing, scrolling YouTube thumbnails made the whole screen smooth: light and dark content passing over a grid cell looks like a flash to that cell. WCAG talks about flashes occurring concurrently, so we only count reversals when a whole region brightens (or darkens) together in the same frame. Movement makes neighbouring cells change in opposite directions, so it no longer triggers.
- Capture speed. Chromium's capture managed about 50 fps at a reduced 960×540, which made the smoothed copy soft. We wrote a native Desktop Duplication module to reach 60 fps at full resolution.
- The first flashes. Any overlay reacts after the screen does, so the first one or two flashes pass before protection lands. We measured the delay, made the trigger point a user setting, and protection always lands before the WCAG threshold is crossed.
Accomplishments that we're proud of
- System-wide protection that works in games, not just one website
- Real measurements: latency, residual flash after smoothing, and agreement with IRIS
- Careful safety practice throughout: low-contrast test media, no flashing UI, and honest wording
What we learned
How the WCAG flash rules are defined, why linear light matters, how to keep a real-time pipeline on the GPU, and how much a spec, small commits and measured numbers help a team move fast across two laptops.
What's next
- Slow-motion measurements on more real game footage
- Detecting hazardous static patterns (stripes, checkerboards), which IRIS covers and we don't yet
- Smoothing only the flashing region, so the rest of the screen stays sharp
- A plugin for game engines, so games can smooth their own flashes before they are drawn, with zero delay
- Multi-monitor and macOS support
Impact and limitations
CalmFrame reduces exposure to hazardous flashing. It is not a medical device and does not prevent seizures. Known limits: the first one or two flashes pass before protection lands; exclusive-fullscreen games may sit above the overlay (use borderless mode); DRM-protected video captures as black; only the primary display is protected.