A perception game in the spirit of eyeball: how accurately can you read the number behind a visual encoding? Each round is a single click. Encodings are ordered by the Cleveland–McGill graphical-perception hierarchy (position is read most accurately, color/area least), so playing doubles as a hands-on demo of that research.
No build step, no dependencies. Just open index.html:
open index.html # macOSOr serve the folder (python3 -m http.server) and visit it — either works,
because the scripts are plain classic <script> tags, not ES modules.
| Mode | What you do | Rank |
|---|---|---|
| Position | Click where a value falls on a number line | 1 |
| Length | Click the height of a bar (shared or floating baseline) | 2 |
| Angle | Click to make a pie / donut slice the right share | 4 |
| Area | Click to size a circle's area to a value | 5 |
| Color | Read a swatch (hue / saturation / lightness), click its place on the scale | 6 |
"Mixed" draws a random encoding each round. Stats (best / average / streak /
rounds) persist in localStorage.
index.html markup + stats bar + mode tabs + SVG stage
styles.css minimalist styling
config.js your logging endpoint URL (empty = logging off)
src/util.js helpers, color maps, scoring, challenge registry (window.CE)
src/logger.js fire-and-forget result logging (CE.log)
src/challenges.js one object per encoding
src/game.js round lifecycle, pointer wiring, stats persistence
backend/apps-script.gs optional Google Sheet backend
backend/cloudflare/ optional Cloudflare Worker + D1 backend
Register one object in src/challenges.js:
CE.register({
id: "myencoding",
name: "My encoding",
hierRank: 3, // Cleveland–McGill rank, used for tab order
blurb: "One-line description shown under the chart.",
generate() { // make a round
const range = CE.niceRange(); // { min, max, unit }
return { range, target: /* the true value */ };
},
prompt: (rd) => `Click to ... <b>${CE.fmt(rd.target, rd.range)}</b>`,
setup(svg, round) { // draw the static scene into the SVG
// ...append elements with CE.el(tag, attrs, text)...
return {
valueAt(pt) { /* pointer {x,y} in SVG units -> value (clamped) */ },
preview(value) { /* draw the hover ghost */ },
commit(guess, target) { /* draw final guess + true value markers */ },
};
},
});The engine handles pointer→SVG coordinate conversion (CE.svgPoint), hover
preview, click-to-commit, scoring (CE.accuracy), and stats. The SVG viewBox
is 0 0 900 520.
Set hideTarget: true on the round (as the Color encoding does) when the player
must read the value rather than produce it — the number is then withheld
until the reveal.
Per-player accuracy is always tracked locally (the breakdown panel). To also
collect results across players, the game fires one anonymous event per round
to an endpoint you configure. It's fire-and-forget (sendBeacon / no-cors
fetch): if no endpoint is set, or the network fails, the game is unaffected.
Each event: ts, session, mode, encoding, variant, aligned, donut, accuracy, error, guess, target, rangeMin, rangeMax, unit. session is a random id in
localStorage — nothing personal. Worth a one-line disclosure to players that
anonymous results are recorded.
- Create a new Google Sheet.
- Extensions → Apps Script. Delete the stub, paste the contents of
backend/apps-script.gs, and save. - Deploy → New deployment → type: Web app. Set Execute as: Me, Who has access: Anyone. Deploy and authorize.
- Copy the Web app URL (ends in
/exec). Opening it should say "perceptron logger is running". - Paste that URL into
config.jsasendpoint.
Play a round — a row appears in the sheet's events tab. Aggregate with a pivot
table (rows = encoding, value = AVERAGE of accuracy, plus COUNT) to
reproduce the Cleveland–McGill ranking from real players. You can also break it
down by variant (color hue/saturation/lightness) or aligned (shared vs
floating baseline).
If you'd rather not expose a personal account, or you want to aggregate with real
SQL, there's a ready-made Cloudflare Worker + D1 backend in
backend/cloudflare/ — see its README for the deploy
steps. It runs on its own free Cloudflare account and the client needs no
changes (same endpoint / token in config.js).
Any endpoint that accepts a POST works (Supabase, Formspree, Val Town, …) — just have it read the JSON body and store it; the client side needs no changes.
- Scatter-plot position (2-D click), line slope, patterns, line weight.
- Unaligned bar comparison ("how many times taller is A than B?").
- Per-encoding score breakdown / a Cleveland-McGill "your personal ranking".
- Shareable result card and daily seed.