A tiny desktop app that opens a browser window and plays a configurable playlist of moving-ball patterns: horizontal / vertical / diagonal sweep, bounce, circle (CW/CCW), infinity ∞ (horizontal and vertical, CW/CCW). Background color, ball color, global ball size, global speed, edge-linger duration (a vision-training pulse at the extreme of each linear sweep), per-item repeat count, and direction are all editable in the UI and persist to a JSON file in the OS user config directory.
Double-click the appropriate artifact for your OS:
| OS | File | First-launch notes |
|---|---|---|
| Linux | Zoetrope-VERSION-linux-amd64.deb |
sudo apt install ./Zoetrope-*.deb, then launch from the apps menu. |
| Windows | Zoetrope.exe |
SmartScreen will warn (unsigned). Click More info → Run anyway. |
| macOS | Zoetrope.app |
Right-click → Open the first time (unsigned). |
On launch, the app:
- Picks a free port on
127.0.0.1. - Reads or creates
config.jsonin the OS user config directory (~/.config/zoetrope,%APPDATA%\zoetrope,~/Library/Application Support/zoetrope). - Opens the UI in a dedicated, chromeless app-mode window using an installed Chromium-family browser (Chrome / Edge / Brave / Chromium). If none is found it falls back to a normal tab in your default browser.
Quit the app by closing its window — the server notices the window is gone and shuts itself down shortly after.
- Transport bar (bottom)
|<Start of playlist<Start of current pattern- Play / Pause
>Next pattern- Speed multiplier (
0.25×–4×) - Gear icon — toggle the editor panel
- Editor panel (right) — global controls at top (background color, ball size, speed on a 0–10 scale — higher is faster; the dial sets the ball's on-screen speed, so every pattern moves at the same pace at a given setting); below them, a Library picker that selects the active playlist + buttons to create / rename / duplicate / delete playlists; then add / reorder / delete pattern items in the active playlist; per-item color / repeats / direction / angle. Click Save to write to the config file; Revert discards unsaved edits.
- Keyboard shortcuts — Space (play/pause), ← (start of pattern), → (next pattern), Home (start of playlist).
| Pattern | Cycle = one full… | Direction (cw/ccw) |
angleDeg |
|---|---|---|---|
| h-sweep | left → right → left | — | — |
| v-sweep | top → bottom → top | — | — |
| diag-ulbr | upper-left ↔ bottom-right (↘) | — | — |
| diag-urbl | upper-right ↔ bottom-left (↙) | — | — |
| circle | revolution | yes | — |
| serpentine | raster-scan back-and-forth + interleaved return | yes | — (see cornerRadius, startCorner, lanes) |
| lightbulbs | serpentine raster where each turn loops around a bulb | — | — (see lanes, bulbSize) |
| fig8-h | figure 8 from two tangent circles (∞ orientation) | yes | — |
| fig8-v | figure 8 from two tangent circles (8 orientation) | yes | — |
| infinity-h | trace of ∞ (lobes side by side, Lissajous) | yes | — |
| infinity-v | trace of vertical 8 (lobes stacked, Lissajous) | yes | — |
| bounce | max(w, h) pixels of travel |
— | yes (initial heading) |
| position-sequence | ordered walk through named gaze targets (8-point grid + center) | — | — |
speed (0–10 scale, global; higher = faster) sets the ball's on-screen
pixel speed — each pattern's cycle is normalized by its path length so
they all move at the same pace at a given setting (calibrated to the
circle; exact on a 16:9 viewport). ballSize (pixels, global) applies to
every item. repeats (per item) is how many cycles play before the
playlist advances. A per-item speed (0–10) overrides the global dial for
that one item; leave it blank to follow the global speed. The speed
multiplier in the transport bar scales the effective speed at runtime
without persisting.
The serpentine pattern is a closed-loop raster scan. The ball sweeps
across lanes horizontal lanes (2–8, default 3), dropping into the next
lane at each end via a U-turn, until it reaches the bottom ("Turn") —
then serpentines back up through the gaps between the down-lanes,
returning to Start to close the loop. Per-item fields: cornerRadius
(0–1, how round the U-turns are; 0 = sharp, 1 = full half-circle),
startCorner (tl or tr), lanes (integer), and direction
(cw runs the loop forward, ccw reverses it).
The lightbulbs pattern shares the serpentine's interleaved closed-loop
raster, but every turn balloons into a near-full circular loop — a "bulb"
the ball traces before continuing down the thin lane. Per-item fields:
lanes (2–8, default 3) and bulbSize (0–1, how large the bulbs are).
lingerSec (global, default 0) is a vision-training aid for the linear
patterns (h-sweep, v-sweep, diag-ulbr, diag-urbl). When > 0, the ball
stops at each extreme of a sweep, grows to 2× then shrinks back over
lingerSec seconds before reversing direction — peripheral feedback so
the viewer can keep their eyes moving "off-screen" and pick the ball
back up smoothly when it returns. Cycle time extends by 2 × lingerSec
so the on-screen motion pace stays consistent regardless of dwell.
position-sequence is a separate engine from the continuous patterns
above. Each item has an ordered list of steps, each referencing one of
nine named gaze targets on a 3×3 grid:
up-l up up-r
lateral-l center lateral-r
down-l down down-r
Per-item dwellSec (default 1.5s) is the time the ball holds at each
position. transitSec (default 0.8s) is the smooth-pursuit time between
positions (cosine ease-in-out); set it to 0 for an instant jump (saccade
training) instead of smooth pursuit. The global speed knob scales the
whole sequence: speed = 2 honors the configured dwell/transit; higher =
faster.
Toggle Show position labels in the editor to overlay grid labels —
useful while learning the positions, off for live sessions. The optional
per-item name field overrides the pattern's default label in the editor
and the now-playing strip.
Two IEMT-category playlists ship in the default config: "IEMT · Identity (draft)" (16 steps with returns to center between each cardinal/diagonal) and "IEMT · Emotion (draft)" (continuous 8-position round). Both are starting drafts — the exact step orderings are placeholders rather than canonical clinical sequences.
The editor library holds any number of named playlists, each filed under
a free-form Category (e.g. Continuous, IEMT, EMDR). The Library
picker switches which one plays — the engine resets to the first item of
the chosen playlist. The default config ships:
Default(Continuous) — the eight continuous sweep / circle / infinity / bounce patterns, active on first launch.IEMT · Identity (draft)(IEMT)IEMT · Emotion (draft)(IEMT)EMDR · Horizontal (draft)(EMDR) — a sustained horizontal bilateral set; Loop is off, so it plays one set of passes and stops (re-engage play for the next set).Saccades (draft)(Saccades) — horizontal + vertical saccade drills using instanttransitSec: 0jumps between the lateral / vertical targets.Anti-saccade (draft)(Saccades) — the same jump motion; the practitioner cues the patient to look away from the dot.Smooth Pursuit (draft)(Pursuit) — a curated continuous-tracking set (circle, infinity, figure-8).Fixation (draft)(Fixation) — nine single-step holds (center + the eight eccentric targets); the ball sits still at one position. Navigate with next/back and pause to hold indefinitely;dwellSecsets the auto-advance time. A steady target for brainspotting / gaze-holding-endurance work.
New playlists default to the currently-active playlist's category;
+ New category… in the Category dropdown lets you start a fresh
grouping (e.g. add an EMDR set). Renames are name-collision-safe — a
duplicate name gets (2), (3) appended automatically.
Each playlist has a Loop toggle (next to the picker). With Loop on (the default) the playlist cycles back to the first item after the last; with it off the playlist rewinds to its first item and stops there when it reaches the end — re-engage play to run it again. The toggle applies to built-in playlists too.
Zoetrope is a configurable gaze-target animation tool — a piece of software that moves a ball on a screen. It is not a treatment or a substitute for clinical care. The included IEMT presets and any other named modality (saccade training, anti-saccade, brainspotting fixation, EMDR, etc.) are starting templates for use with a qualified practitioner of that modality. Nothing in this README or the in-app copy is a therapeutic claim.
Requires Go 1.25+, zip, and (for the Debian package) dpkg-deb.
The version string lives in the top-level VERSION file (e.g. 0.1.0,
no leading v). It's //go:embed-ed into the binary (main.go), so the
running app reports it at startup and on GET /version. build.sh
reads the same file to name the artifacts and stamp the macOS Info.plist
and the .deb control file — one source, no overrides.
go run . # run locally; reports the VERSION file's value
./build.sh # cross-compile every artifact into dist/
./install.sh # build (if needed) + install the .deb on this machine (Linux)To cut a release: bump VERSION, commit, tag vX.Y.Z, rebuild.
.
├── main.go entry point, listener, idle-shutdown, signal handling
├── config.go JSON load/save, defaults, atomic writes, builtin reconcile
├── server.go HTTP routes, embedded asset serving
├── update.go opt-in GitHub update check (GET /update/check)
├── browser.go app-mode window launch, with default-browser tab fallback
├── build.sh cross-compile every artifact into dist/
├── install.sh build + install the .deb on this machine (Linux)
├── web/ embedded UI (HTML/CSS/JS)
│ ├── app.js rAF animation loop, transport, editor wiring
│ ├── patterns.js SoT for the animation kinematics
│ └── editor.js config editor sidebar (playlists, items, globals)
└── build/ Info.plist, .desktop entry, lipo + icon helpers
Zoetrope is free software, licensed under the GNU Affero General Public License v3.0 — see LICENSE.
It carries no third-party dependencies: the binary is pure Go standard
library (no go.sum), and the embedded UI ships no bundled fonts, scripts, or
CDN assets. There are therefore no third-party licenses to include.