Back to Projects

    Ocula — Webcam Eye-Controlled Cursor for Assistive Input

    Ocula is a free, open-source eye-controlled cursor that runs entirely in a browser tab. It uses a laptop webcam and MediaPipe Face Mesh iris landmarks to estimate gaze, maps that to screen coordinates through a five-point calibration, and replaces the mouse button with dwell-to-click. It ships as one MIT-licensed HTML file.

    2026
    Creator
    10 Technologies
    MediaPipe Face Mesh 0.4MediaPipe Camera Utils 0.3WebAssemblyMediaDevices getUserMediaWeb Speech API (SpeechSynthesis)Vanilla JavaScriptHTML5CSS custom propertiesrequestAnimationFrameGitHub Pages
    Ocula — Webcam Eye-Controlled Cursor for Assistive Input

    Verify it yourself

    • Source repositoryMIT licensed. The entire application is a single index.html file.
    • Live demoServed from GitHub Pages. Requires camera permission and a five-point calibration.

    Introduction

    Ocula started from a narrow question: how much assistive pointing can you get out of hardware people already own? The README frames the target as a 2019-era laptop with a plain webcam, and the answer it claims is enough for a board of large tiles. Not enough to type freely, and the README says so plainly.

    The Challenge

    Gaze input is a solved problem if you can pay for it. Dedicated eye trackers sit in the price band the project README records as USD 200 to 3000 and up, and they typically want drivers, a dongle and a specific desktop OS. That combination excludes exactly the people it should serve: someone with ALS whose speech and hand control are going at the same time, someone recovering from an arm injury for six weeks, a family in a country where the tracker plus the software costs more than the laptop. The economics matter more than the accuracy here. A rougher tool that costs nothing reaches people a clinical-grade tracker never will, because the acquisition cost is a browser tab.

    The Solution

    I built Ocula as a single HTML file that loads MediaPipe Face Mesh in the browser, extracts iris position relative to each eye socket, and learns a mapping from that signal to screen coordinates during a five-point calibration. During tracking, the mapped position is low-pass filtered into a visible cursor, hit-tested against clickable elements, and a dwell timer on the hovered element stands in for the mouse button. The included demo is an AAC communication board whose tiles carry a phrase and speak it through the browser speech synthesiser when activated, with the phrase also shown on screen for anyone who cannot rely on audio. Everything runs client side. The page does pull the MediaPipe libraries and their WebAssembly assets from a public CDN and its web fonts from Google Fonts, but camera frames and landmarks never leave the tab, because there is no backend to send them to.

    Technical Deep Dive

    1

    Gaze signal. Face Mesh runs with refineLandmarks set to true, which is what exposes the iris landmarks (indices 468 and 473). For each eye I take the outer and inner corners, the top and bottom lids, and the iris centre, then express iris position as an offset from the eye centre normalised by eye width and height. Normalising by socket size is what makes the signal roughly invariant to how far the user is sitting from the camera. The two per-eye vectors are averaged, and x is negated because the video preview is mirrored.

    2

    Calibration as a least-squares fit, not a lookup. Five targets sit at the four corners inset to 10 and 15 percent plus the centre. Each target samples for 1500 ms, discarding the first 600 ms so the eye has time to settle, then commits the median of the samples rather than the mean so a single blink or saccade cannot drag the point. The five gaze-to-screen pairs feed an affine solve (screenX = a·gx + b·gy + c, same for Y) built from 3x3 normal equations and Cramer's rule. A determinant below 1e-10 means the gaze samples were degenerate, and the app says calibration failed instead of producing a cursor that lies.

    3

    Smoothing versus latency. Raw per-frame gaze is far too jittery to dwell on a target. I used a single-pole exponential filter with alpha 0.20, so each new frame contributes 20 percent and 80 percent carries over from the previous smoothed position. That trades responsiveness for stability, which is the correct direction for dwell input: the user is holding still on purpose, and overshoot costs a misfire.

    4

    Dwell that tolerates drift. A naive dwell timer resets the moment the cursor moves one pixel, which never completes on a noisy signal. Ocula anchors the dwell at the position where the target was first entered and only resets if the cursor wanders more than 55 pixels from that anchor or moves onto a different element. Completion fires the click, then a 600 ms cooldown suppresses the next dwell so the user does not double-fire a tile they are still looking at.

    5

    Hit testing without the cursor in the way. The gaze cursor is a DOM element, so document.elementFromPoint would return the cursor itself. The update loop hides it, samples the element under the point, and restores it in the same frame. Activation calls the element's own click() method, meaning the gaze layer works with ordinary click handlers and needs no special component API.

    6

    State machine and failure modes. The app moves through welcome, loading, calibrate, tracking and error states stamped onto a body data attribute that CSS keys off. Distinct errors are surfaced separately: no camera API in the browser, MediaPipe failing to load, NotAllowedError when permission is denied, and calibration that was too noisy to fit. If no face has been seen for 700 ms the status indicator flips and a warning appears, because silently freezing the cursor is the worst thing an assistive tool can do.

    7

    Calibration invalidation. A window resize changes the coordinate space the affine map was fitted in, so the resize handler discards the fitted function and prompts a recalibration rather than continuing with a stale mapping. Recalibration is also reachable by dwelling on a control or pressing R, without reloading the page.

    Key Features

    Five-point webcam calibration

    Five on-screen targets with a filling progress ring, median-sampled per point, fitted to an affine gaze-to-screen transform. The fit is rejected outright if fewer than three usable points were captured or the system is degenerate.

    Dwell-to-click with adjustable speed

    Dwell time cycles between 0.5, 0.8 and 1.2 seconds from a control on the page. The default is 0.8 s. A progress ring on the cursor shows the fill so the user always knows how close the click is.

    AAC communication board demo

    Large gaze-clickable tiles for phrases such as Yes, No, Thank you, Help me, Water and Call family. Activating a tile speaks its phrase through SpeechSynthesis and prints it on screen.

    Loss-of-tracking feedback

    A status dot and text report Tracking or No face, and a warning banner appears when no landmarks have arrived for 700 ms, so the user can tell the difference between a bad estimate and a lost face.

    Client-side by construction

    There is no backend in the repository. Camera frames, landmarks, calibration data and cursor coordinates exist only in page memory, and none of it is persisted or transmitted.

    One file, MIT licensed

    The entire application is index.html with inline CSS and JS. Clone it, open it, or serve it with python3 -m http.server. There is no build step and no dependency install.

    Results & Impact

    • The project is public and live: an MIT-licensed repository and a GitHub Pages build serving the same single file at revolutionarybukhari.github.io/ocula. It carries 1 star and 0 forks as of July 2026.
    • The end-to-end path works without any install: allow the camera, complete a five-point calibration, and gaze plus dwell drives an AAC board that speaks aloud. No account, no download, no hardware beyond the built-in webcam.
    • Because activation dispatches a genuine DOM click, the gaze layer is not tied to the demo board. Any element marked gaze-clickable in the page participates, which is what makes it forkable into someone else's board.
    • The README documents its own limits rather than hiding them: accuracy is stated there as roughly 3 to 5 degrees of visual angle, head pose is not compensated, and the project is explicitly labelled a research and educational prototype, not a medical device.

    Lessons Learned

    "The hard part of webcam gaze is not detection, it is the mapping. MediaPipe hands you stable iris landmarks in a few lines. Everything that determines whether the thing is usable lives in calibration sampling, the median commit, the affine fit and the smoothing constant."

    "Dwell design is where accessibility tools succeed or fail. Anchor tolerance, cooldown and a visible progress ring are not polish; without them the same gaze signal produces either misfires or clicks that never complete, and the user has no way to tell which."

    "Refusing to produce output is a feature. Rejecting a degenerate calibration fit and forcing recalibration after a resize is less pleasant than always showing a cursor, and much better than a cursor the user cannot trust."

    "Constraining the scope to one HTML file forced honesty about the stack. There was nowhere to hide a build pipeline, so the reviewable surface is 36 KB of source anyone can read before pointing a camera at their face, which is the right posture for a tool that asks for camera permission."

    Conclusion

    Ocula is a small, honest demonstration that browser-only assistive input is good enough for a real task today. If you are evaluating whether an idea like this is buildable on commodity hardware, the fastest answer is to open the demo, calibrate, and judge the cursor yourself.

    Frequently asked questions

    Is Ocula open source, and can I use it in a clinical or commercial setting?
    Yes. The repository is public under the MIT licence, which permits personal, educational, clinical and commercial use including modification and redistribution. It is a research and educational prototype, not a certified medical device, so it should not be treated as clinical equipment on its own.
    What does it cost to run?
    Nothing beyond the device you already have. There is no backend, no API key and no account. It is a static HTML file, so hosting is free on GitHub Pages or any static host. The only running cost is the user's own laptop and webcam.
    How accurate is webcam gaze compared with a dedicated eye tracker?
    The project README states roughly 3 to 5 degrees of visual angle, which is why the demo uses large tiles rather than a keyboard. It has not been benchmarked against clinical trackers. It is designed for a handful of big targets, not precise pointing or free text entry.
    Does any video or face data leave my machine?
    No. Face detection, iris tracking, calibration and smoothing all run inside the browser tab, and the repository contains no server component. The page does fetch the MediaPipe libraries, their WebAssembly assets and its web fonts from public CDNs at load, but no camera frame, landmark or coordinate is uploaded anywhere.
    How does it handle head movement?
    It does not compensate for head pose yet, and the README says so plainly. The gaze estimate comes from iris position within the eye socket, so moving your head shifts the estimate. The fix in practice is recalibration, available from a control or the R key without a reload.
    Can I put the gaze layer on my own interface?
    Yes. Activation calls the target element's native click() method, so any element carrying the gaze-clickable class participates and your existing click handlers fire unchanged. Swapping the demo AAC board for your own tiles is an edit to one array in index.html.
    Which browsers and cameras does it work with?
    Any browser exposing getUserMedia with WASM SIMD support. The README lists testing on Chrome 120+, Edge 120+, Safari 17+ and Firefox 121+ on desktop. Mobile runs it but accuracy is poor. Low light, strong reflections on glasses and fish-eye lenses all degrade the signal.

    Interested in a Similar Project?

    Let's discuss how I can help bring your ideas to life.

    Get in Touch

    Let's Create a Revolution