Skip to content

Repository files navigation

browserfly

Your very own fruit flies that wander over a web page. They walk in runs and pauses, turn, and can be drawn toward the cursor or pushed away from it. The fly is a sprite rendered from NeuroMechFly, a biomechanical model of Drosophila melanogaster, replaying a recorded real walking stride, so the legs move in a true tripod gait without sliding.

Live demo: https://sungsushi.github.io/browserfly/.

One script tag, no dependencies, about 8 KB gzipped (plus a 28 KB sprite sheet, or one 37 KB file with the sheet inlined). The overlay never blocks clicks, never scrolls or shifts the layout, and respects prefers-reduced-motion.

Quick start

Copy two files from dist/ next to your page, fly.min.js and fly-sheet.webp (add fly-sheet.png as a fallback for old browsers), then add one tag:

<script src="fly.min.js" data-fly data-count="2" data-attraction="0.4" defer></script>

The sprite sheet is found relative to the script's own URL, so this works on GitHub Pages project sites served under /<repo>/ too.

Single file

fly.inline.min.js has the sprite sheet embedded, so no image file is needed:

<script src="fly.inline.min.js" data-fly defer></script>

Programmatic

<script src="fly.min.js" defer></script>
<script defer>
  const swarm = Fly.create({ count: 3, attraction: -0.5 });
  // swarm.set({ attraction: 0.8 });  swarm.pause();  swarm.resume();  swarm.destroy();
</script>

Inside an element

Instead of the whole viewport, the flies can live inside one element. The canvas is added to the element (which is made position: relative if it is static):

Fly.create({ container: document.querySelector('#hero'), count: 1 });

Flies that scroll with the page

By default (anchor: 'page') the flies live on the page: they keep their place on the content and scroll away with it, and the ones you've scrolled past keep walking out of sight. New flies appear in the part of the page that's visible, then roam the whole page. With anchor: 'viewport' they stay on screen instead, while the page scrolls under them:

<script src="fly.inline.min.js" data-fly data-anchor="viewport" defer></script>

Only ordinary vertical scrolling of the document is supported (not horizontal scrolling or scrolling inside other elements). The canvas stays the size of the window, so long pages cost nothing extra. With a container, anchor is ignored: a container already scrolls with its content.

fly.esm.js is an ES module build exporting create, version, instances and schema. A module has no currentScript, so pass sprite as a URL (e.g. new URL('fly-sheet.webp', import.meta.url).href), or the flies are drawn procedurally.

Files

File Size (gzip) Use
fly.min.js 7.95 KB the library, minified; loads the sprite sheet next to it
fly-sheet.webp 27.6 KB (not gzipped) the sprite sheet
fly-sheet.png 32.1 KB (not gzipped) fallback if WebP can't be decoded
fly.inline.min.js 37.2 KB the library with the sheet inlined: the only file you need
fly.js 12.2 KB readable debug build (extra checks, debug-overlay labels; used by the demo)
fly.esm.js 10.8 KB ES module build
fly-sheet.json 0.6 KB sheet metadata (already embedded in every bundle)

Options

Options can be passed to Fly.create({...}), changed live with swarm.set({...}), or given as data attributes on the auto-init script tag (kebab-case: data-run-mean="3"). Out-of-range values are clamped and unknown options are ignored, each with one console warning. BL is the fly's body length (size).

Option Data attribute Default Values Description
count data-count 1 integer, 1 – 50 number of flies
size data-size 25 8 – 200 rendered body length (px)
speed data-speed 1.2 0.1 – 20 mean run speed (BL/s); real flies walk ~6, so the default is slow motion
speedJitter data-speed-jitter 0.15 0 – 0.5 SD of the per-run speed multiplier
runMean data-run-mean 2.5 0.1 – 60 mean run duration with no cursor influence (s)
stopProbability data-stop-probability 0.09 0 – 1 chance that a run ends in a stop rather than an immediate reorientation
stopMean data-stop-mean 1.2 0.05 – 30 mean stop duration (s)
stopDistribution data-stop-distribution 'lognormal' 'exp', 'lognormal' stop duration distribution
rotDiffusion data-rot-diffusion 0.5 0 – 10 long-run heading diffusion during runs (rad²/s)
headingTau data-heading-tau 0.3 0.02 – 2 correlation time of the heading noise (s)
turnRate data-turn-rate 5 0.5 – 30 max angular speed while reorienting (rad/s)
attraction data-attraction 0.1 -1 – 1 cursor attraction; negative means repulsion
attractionRange data-attraction-range 219 10 – 5000 cursor falloff length scale (px)
steeringGain data-steering-gain 3 0 – 20 strength of continuous steering during runs (rad/s)
kappaMax data-kappa-max 6 0 – 50 von Mises concentration of reorientation at full attraction
klinokinesis data-klinokinesis 0.5 0 – 0.9 modulation of the run-ending hazard by heading
arrivalRadius data-arrival-radius 1.5 0 – 10 attracted flies stop within this distance of the cursor (BL; 0 disables)
lingerMax data-linger-max 6 0 – 60 max time to linger at the cursor (s)
wallMargin data-wall-margin 1.5 0 – 5 soft wall-avoidance zone (BL)
wallGain data-wall-gain 6 0 – 30 wall steering strength (rad/s)
container — null Element, CSS selector or null Element or CSS selector; null means full-viewport overlay
zIndex data-z-index 2147483000 integer, -2147483648 – 2147483647 overlay z-index
seed data-seed null integer, 0 – 4294967295, or null uint32 seed; null means random
reducedMotion data-reduced-motion 'static' 'static', 'hide', 'ignore' behaviour when prefers-reduced-motion: reduce
sprite data-sprite 'auto' 'auto', 'procedural' or a URL 'auto', 'procedural' or a sprite sheet URL
touchTargetTimeout data-touch-target-timeout 5 0 – 30 how long a tap acts as the cursor on touch devices (s)
debug data-debug false true, false draw the debug overlay
anchor data-anchor 'page' 'viewport', 'page' 'page': flies live on the page and scroll with it; 'viewport': flies stay on screen while the page scrolls (ignored with a container)

The default walking speed is slow motion on purpose: a real fly walks about 6 body lengths per second with its legs stepping at about 11 Hz, which is too fast to see individual legs on a 60 Hz screen.

API

Fly.create(options?) → Swarm
Fly.version: string
Fly.instances: Swarm[]          // the auto-init swarm is Fly.instances[0]
Fly.schema                      // the option schema (defaults, ranges)

swarm.set(partialOptions)       // live update; count changes add or remove flies; a new seed restarts
swarm.pause() / swarm.resume()
swarm.destroy()                 // removes the canvas, listeners and observers; safe to call twice
swarm.debug() → { flies, cursor, bounds, anchor, scroll, fps, renderer, sheetUrl, params, running }   // read-only snapshot

How it behaves

Each fly alternates runs, stops and reorientations. During a run its heading drifts smoothly and is steered toward an attractive cursor (or away from a repulsive one); runs heading toward an attractive cursor last longer, and new headings are biased toward it. Attracted flies stop at the cursor, linger for up to lingerMax seconds, then wander off. Soft walls keep them on screen. The behaviour engine is pure and deterministic given seed, and is verified by statistical unit tests (PLAN.md §4).

The animation frame follows the distance walked (one sprite stride per strideBL body lengths), so the feet don't slide at any speed. The animation runs on one requestAnimationFrame loop, only while the page is visible (and, in container mode, while the container is on screen).

Accessibility

  • The canvas is decorative: aria-hidden="true" and role="presentation", pointer-events: none, never focusable, and it never captures or prevents pointer events.
  • prefers-reduced-motion: reduce is respected, and live changes to it are followed. reducedMotion: 'static' (default) shows the flies standing still, 'hide' shows nothing, and 'ignore' animates anyway.
  • On touch screens, a tap acts as the cursor for touchTargetTimeout seconds.

Browser support

Current Chrome, Edge, Firefox and Safari. The code targets ES2020 (Chrome 80+, Firefox 74+, Safari 13.1+). WebP needs Safari 14+; older browsers use fly-sheet.png, and if no sheet can be loaded the flies are drawn procedurally (with one console warning). Tested with Playwright on Chromium, Firefox and WebKit, including touch emulation, 2× pixel density and reduced motion.

Development

npm install
npx playwright install        # browsers for the e2e tests
npm run build                 # dist/ bundles; prints gzip sizes against the budgets
npm test                      # unit tests (vitest), including the statistical engine tests
npm run test:sweep            # statistical tests under 20 different base seeds
npm run test:e2e              # end-to-end tests in Chromium, Firefox and WebKit
npm run lint
npm run docs                  # regenerate the option table above from src/params.js
npm run serve                 # http://localhost:8080/ (forwards to demo/) — demo with a live parameter panel

The demo page (demo/index.html) has sliders for every option, renderer and anchor toggles, a seed box, a debug overlay and a box listing the options you've changed from the defaults. The debug overlay's per-fly text labels are only drawn by the debug build (fly.js); the minified builds draw the overlay without them. In the demo the quotation's text wraps around the flies (demo/textwrap.js, demo-only, not part of the library): it re-wraps as each fly moves, settled so a paragraph is only re-laid out after a fly has moved a few pixels. ?wrap=off (or the panel) leaves layout to the browser. PLAN.md is the full specification and PROGRESS.md the project log.

Regenerating the sprite sheet

The sheet is built offline by a Python pipeline in pipeline/ (see its README for details and FlyGym API notes). It needs Python 3.12 and about 1 GB of disk for the virtual environment. CI does not run it; its outputs in dist/ are committed.

uv venv -p python3.12 pipeline/.venv
uv pip install -p pipeline/.venv/bin/python -r pipeline/requirements.txt
pipeline/.venv/bin/python pipeline/build_all.py pipeline/out/build --dist   # ~30 s
pipeline/.venv/bin/python pipeline/contact_sheet.py final dist/fly-sheet.json pipeline/out/qa
pipeline/.venv/bin/python -m pytest pipeline/tests
npm run build                 # re-embed the new sheet in the bundles

The pipeline replays a recorded walking stride in NeuroMechFly (FlyGym 2.1.0, MuJoCo 3.9.0), renders it from directly overhead with an orthographic camera, picks one seamlessly looping stride, aligns the frames head-up around the thorax, and packs a 16-frame walk and a 2-frame idle clip into a 64-colour sheet. The wings are a translucent grey-blue so they show on white pages (--wing-rgba changes it). The build is deterministic.

Credits and licences

The sprite is rendered from the NeuroMechFly body model through FlyGym, developed at the Neuroengineering Laboratory (Ramdya lab), EPFL, and released under the Apache License 2.0. The walking kinematics are the recorded clip that ships with FlyGym (flygym_demo.spotlight_data), captured with the Spotlight system, with 3D pose from PoseForge and joint angles from SeqIKPy. The sprite sheet (dist/fly-sheet.*) is a rendering of that model and data; keep this attribution when you redistribute it.

If you use this in research, please cite the NeuroMechFly papers, as FlyGym asks:

  • V. Lobato-Rios, S. T. Ramalingasetty, P. G. Özdil, J. Arreguit, A. J. Ijspeert and P. Ramdya. NeuroMechFly, a neuromechanical model of adult Drosophila melanogaster. Nature Methods 19, 620–627 (2022). doi:10.1038/s41592-022-01466-7
  • S. Wang-Chen, V. A. Stimpfling, T. K. C. Lam, P. G. Özdil, L. Genoud, F. Hurtak and P. Ramdya. NeuroMechFly v2: simulating embodied sensorimotor control in adult Drosophila. Nature Methods 21, 2353–2362 (2024). doi:10.1038/s41592-024-02497-y

and, for the walking data, Wang-Chen et al. (2026), bioRxiv 10.64898/2026.03.11.711180; for the inverse kinematics, Özdil et al. (2026), JOSS, 10.21105/joss.08557.

Licence

browserfly is licensed under the Apache License 2.0, the same licence as FlyGym and NeuroMechFly. See NOTICE for the attribution that must travel with copies of the sprite sheet and the bundles that embed it.

Releases

Packages

Contributors

Languages