A realtime Metal-powered exhibit running unattended in four science installations, with a fifth opening in Singapore. The engineering story behind the app at /apps/gravity-well/.
The Gravity Well source is private. This page covers the engineering choices that made the product work at the museum-floor level, not the source code.
Gravity Well is a macOS application I built that turns a coin-funnel sculpture into a real-time visualization of orbital mechanics. An astronomical camera looks down into the well from above. A custom Metal shader pipeline processes each camera frame, detects the glowing balls’ positions, and sends those positions to the full-screen public display. The display draws colored trails as the balls spiral toward the center, making invisible gravitational behavior visible.
That product description is the easy part of the story. The interesting part is what it takes to make a camera-driven, GPU-processed exhibit run unattended on a museum floor for years, with no engineer on site, on hardware ranging from a current Mac mini to five-year-old machines, in four different installations with four different ambient lighting situations.
Where It Runs
Four live installations as of mid-2026, with a fifth opening in Singapore:
- NSSTI’s MESO mobile science lab, Colorado region. The Mobile Earth and Space Observatory is a traveling truck that serves school districts and community events. Gravity Well rides along inside it, which means new lighting, new audience, and new physical setup roughly every week.
- Fiske Planetarium at CU Boulder. Fixed install in a working planetarium. Lower ambient light, more sophisticated audience, longer run times during planetarium hours.
- Clark Planetarium in Salt Lake City. Another fixed install, similar audience profile.
- A science museum in Miami, Florida. Different climate, different visitor density, different operator.
- Singapore. The newest install, opening in Summer 2026.
The same software runs in all of them. The same fastlane-built, Developer-ID-notarized binary. Differences are encoded as configuration the operator can edit through the in-app settings UI, not as separate builds.
The Metal Image-Processing Pipeline
The pipeline is built on top of Alloy, the Swift Metal framework I extracted out of Gravity Well. Each step is a Metal compute shader. The data stays on the GPU end-to-end and never round-trips through main memory until the final ball positions are read out.
ASI astronomical camera (16-bit raw Bayer)
-> Debayer RGGB
-> Grayscale (custom luminance formula tuned to the fluorescing ball colors)
-> Donut mask (excludes the well rim and center hole)
-> Threshold
-> Morphological erosion
-> Connected components blob detection
-> Centroid extraction
-> Color sampling at each centroid
-> Ball positions and colors
The camera delivers frames in realtime. The pipeline runs comfortably under the frame budget on every machine in the install fleet, including the oldest hardware in it. The headroom matters because museum hardware ages in place. The exhibit cannot be the thing that forces a hardware upgrade.
The “tuned luminance formula” deserves a mention. Standard grayscale conversions weight red, green, and blue according to human vision. Balls fluorescing under a UV blacklight are not human vision input. The grayscale step uses a luminance formula picked specifically to maximize contrast between the fluorescing ball colors and the well’s painted background, while preserving enough information for the later color-sampling step to recover each ball’s identity. This is a one-line change in the shader. It is the difference between detection that works and detection that does not.
Reliability With No Engineer On Site
The hardest problem in this product is not the shader pipeline. It is keeping the whole camera-to-display system running for years on hardware that nobody is babying. Several specific failures shaped the software:
Camera disconnections. ASI cameras occasionally drop their USB connection, especially after kernel extension updates or USB hub power events. The app has automatic reconnection logic that detects the drop, polls the USB tree for the camera’s reappearance, and resumes capture without an operator restart. An XPC service boundary isolates the camera driver from the main app so a driver crash cannot take the rest down with it.
Ambient light shifts. Sunlight through a skylight at the wrong time of day can blow out the threshold step. The settings UI exposes exposure, gain, and white balance, plus three live HSV color-analysis plots, so an operator with no image-processing background can re-tune the system in five minutes when the lighting changes. The color-correction page has three modes: passthrough for well-lit spaces, a color palette for manual mapping, and saturation/value adjustment for fine-tuning.
OS updates. macOS major-version upgrades have, more than once, shifted the behavior of XPC, kernel extensions, USB power management, or display chrome. Every release goes through a manual exhibit-floor smoke test before it ships. Releases are deliberately infrequent for that reason. Stability beats feature velocity at this price point.
Power outages. The Mac wakes back up, the launchd job restarts the app, the app reconnects to the camera and the public display. No operator action required.
Operator support. A button in the settings UI packages the runtime state into a single file (configuration, last few seconds of camera frames, log tail) that an operator can email me when something is wrong. Most of the support I do happens by reading a state-export file and replying with the one setting that needs changing. I have not had to drive to a venue.
Custom Distribution
Gravity Well does not ship through the Mac App Store. Five customers do not justify the App Store sandboxing constraints, and the camera XPC service is fundamentally incompatible with the App Store sandbox anyway. The release pipeline is fastlane-automated, builds are signed with my Developer ID, and the resulting .app is notarized by Apple before being packaged into a custom installer. Release infrastructure is shared with the rest of my Swift projects, which means the cost of doing this for one customer is roughly the cost of doing it for several.
Recording Mode
A late addition that paid for itself many times over: a recording mode that captures camera frames to disk during a session and a playback mode that replays those frames through the live detection pipeline. The same code paths run on the recorded data as on a live camera. This is how I debug detection issues without traveling to the venue, how I generate screenshots for marketing material, and how I onboard a new operator at the next install: I send them a recording, they tune their settings against it, and the result transfers to live operation.
What This Proves
The reasons I would point a hiring manager at this case study are concrete:
- Production image processing that holds. The pipeline has been running in some installations for over two years. Five-figure customer commitments. Real users, no babysitting.
- Operator-grade UX on top of the engineering. Every reliability fix exposed itself through the settings UI as a one-button affordance for someone who is not me.
- End-to-end ownership. Camera driver, GPU pipeline, app, public display, release automation, support workflow, and the framework underneath (Alloy) all came from one person.
The interesting work is not the shaders. It is the choices about which shader hyperparameter to expose to the operator, which to bake in, and which to make self-tuning. That is what made this a multi-installation product instead of a one-off demo.
For visitor-facing screenshots and product detail, see the feature page.