An animation editor for protogen helmets running NCPlyn's ProtoESP firmware.
Open protoface.html in a browser. That's the entire install — one file, no
build step, no dependencies, no internet. It runs off a USB stick, and you can
hand it to someone who doesn't code.
Not yet tested on hardware. The file format is verified against the firmware source, and every stock animation round-trips through the editor bit-for-bit (98 automated tests). But nobody has yet put a face made here onto a real helmet. If you do, please open an issue and say how it went.
ProtoESP already ships an animator, and it's good — it's in the firmware repo at
ProtoESP-Controller/data/animator.html, and the helmet serves its own copy
over WiFi. Protoface isn't a replacement. It's built around the parts that get
painful once you're making things that move rather than single faces:
| Protoface | ProtoESP's animator | |
|---|---|---|
| Mirror | live, while you draw | button, applied afterward |
| Onion skin | yes | no |
| Undo / redo | 80 steps | no |
| Frame timeline | thumbnails, drag to reorder | numbered list |
| Motion tween (Slide) | yes | no |
| Playback | at real per-frame timings | yes |
| Rotate / flip a panel | yes, mirror-aware | yes |
| Toggle a whole row or column | yes, row spans the screen | yes, per panel |
| Copy-paste between panels | yes, mirror-aware | yes |
| Ear animations | no — passed through untouched | yes, full editor |
| Runs on the helmet over WiFi | no | yes |
Files move freely between the two — same format, no conversion. A reasonable workflow is to draw faces here and use theirs for ears and for live tweaks with the head on.
| Word | Meaning |
|---|---|
| panel (matrix, segment) | One 8×8 square of LEDs. A helmet is several, chained. |
| t0, t1, t2… | Panel numbers, in the order they're wired — not their position on the face. |
| frame | One picture of the whole face, plus how long it stays up. |
| timespan | How long a frame is held, in milliseconds. 1000 = one second. |
| layout | Which panel sits where. Written as a visType string. |
| mirror | Draw one half of the face, the other half follows. |
| onion skin | The previous frame shown faintly behind the current one. |
| MAX7219 | The single-colour panels. Every LED is just on or off. |
| WS2812 | The colour panels. Every LED can be any colour. |
| isMouth | Which panels count as "mouth" — the firmware wobbles these with the mic. |
- Draw by clicking and dragging. Mirror is on, so the other half follows.
- Duplicate the frame, move things slightly. That's animation.
- Onion ghosts the previous frame behind you so you can see how far things moved.
- Hold for sets how long the frame stays up. 100ms is quick, 600ms is a slow blink.
- Play loops it. Drag frames along the bottom strip to reorder.
- Export gives you a
.jsonthe firmware reads directly.
Import opens any existing animation, including the stock ones, so you can
start from happy.json rather than a blank face.
Two behaviours that stay out of your way:
- Changing anything stops playback. Otherwise the playhead keeps advancing while you type a duration, and the number smears across several frames.
- Undo goes back 80 steps (
⌘Z/Ctrl+Z). A whole drag is one step, and so is a whole number typed into a box — undo rewinds a gesture, not a keystroke. It clears when you import a file or change the panel count, since those make earlier steps the wrong shape to restore.
Keys: ⌘Z undo · ⇧⌘Z redo · Space play · ← → frames · D duplicate ·
E eraser · M mirror · O onion · Backspace delete frame.
Select a panel three ways: click one of the t0 t1 t2… chips in the sidebar,
click a panel's t label on the face, or shift-click any of its pixels. Click
again to deselect.
Fill, Clear, Invert, the shift arrows and Slide then affect only that panel, and a Panel box appears with rotate, flip, copy and paste.
These stay mirror-aware, which is the point of them: with Mirror on, rotating a panel clockwise rotates its opposite counter-clockwise, and pasting into one panel pastes a left-right flipped copy into the other. A symmetric face stays symmetric. Turn Mirror off to work on one side alone.
With the cursor over a pixel, R toggles that whole row and C that whole
column. A row runs the full width of its screen — one keypress draws a line
right across a 24-pixel mouth. If the line is already full it clears instead.
Keys: [ ] rotate · ⌘C ⌘V copy and paste the selected panel · R row ·
C column.
Draw a pattern once, then Slide… generates the frames that carry it across the face. Set how far it travels and how many frames it takes.
Wrap around is the useful part: pixels leaving one edge come back on the other, so a scroll loops. Slide by exactly the width of a screen and it closes seamlessly — the frame that would repeat your first one is dropped automatically.
Slide moves each band of panels independently: on a normal face the two mouth
halves scroll as separate 24-pixel strips rather than one 48-pixel one, so
nothing leaps across the middle of the face. Click a panel's small t label
first to slide only that panel.
Either:
- Over WiFi — join the helmet's network (
ProtoWiFi/Proto1234by default), open the page it serves, upload the file. - Over USB — drop the
.jsonintoProtoESP-Controller/data/anims/and upload the filesystem image with PlatformIO.
To make an animation fire on a boop or a head tilt, point aBoop / aTilt /
aUp in data/config.json at its filename.
There's a CRC checksum on the data partition (configCRC.txt) that looks like
it might reject files you add. It doesn't — it covers config.json only
(genCRC-auto.py, fileOp.cpp:142-147). Animations aren't checksummed.
Protoface defaults to an 11-panel face:
| Panels | Part | Size |
|---|---|---|
| t0, t1 | eye | 16×8 |
| t2, t3, t4 | mouth | 24×8 |
| t5 | nose | 8×8 |
| t6, t7, t8 | mouth | 24×8 |
| t9, t10 | eye | 16×8 |
The wiring order above is certain — it's what every stock animation uses.
Where those panels physically sit is a guess, mirroring the arrangement the
ProtoESP repo ships. Read data/visType.txt off your own helmet and paste it
into Layout…, otherwise you may be drawing on the wrong squares.
A layout string is cells separated by ; — t4 is a panel, blush2 is a
blush LED pair, - is a gap, _ starts a new row:
t0;t1;-;-;-;-;-;t9;t10;_;-;-;-;-;t5;-;-;-;-;_;t2;t3;t4;-;-;-;t6;t7;t8
Two things worth knowing:
- The firmware repo is internally inconsistent —
src/main.cppsetsMATRIXESNUM 11while the shippeddata/visType.txtdescribes a 14-panel face. Whatever is flashed on your helmet wins. Both are presets here. main.cppnotes that the visor data line starts at the right cheek, sot0may be on the wearer's right. It makes no difference to a symmetric face; it does to a wink. If your first asymmetric animation comes out backwards, that's why.
Leave it on Monochrome for MAX7219 panels — the file gets fColor and
ppColor full of zeros and the helmet uses visColor from its own config.
For WS2812 panels, switch to RGB. The colour you paint with is written
per-pixel into ppColor, and Colour selected panel sets a whole panel's
fColor. The firmware's order of preference is per-pixel, then per-panel, then
the config default.
Protoface doesn't draw ear animations. Pick one of the firmware's built-in types
(rainbow, white_noise, corner_sabers, none). If you import a file with
hand-made ear frames, they're written back out untouched — editing a face never
destroys someone's ear work.
Verified against ProtoESP-Controller/src/main.cpp.
Each leds string is a panel's 64 pixels as a big-endian 64-bit number:
bit index = row * 8 + col row 0 = top, col 0 = left
so the leftmost hex pair is the bottom row and the rightmost is the top.
From main.cpp:782-804:
byte row = (leds[y] >> i*8) & 0xFF; // i = row
mx.setPoint(i, j + y*8, bitRead(row, j)); // j = colMirroring is panel p ↔ N-1-p, column c ↔ 7-c, same row.
npm i -D playwright && npx playwright install chromium
node test.mjs
Boots the page in headless Chromium, imports all eight stock animations, re-exports them, and asserts every panel of every frame is bit-identical — then checks mirroring, sliding, band shifting, undo granularity, layout validation, RGB round-tripping, timeline reordering and the download.
Protoface is MIT — see LICENSE and NOTICE.
The sample animations in fixtures/anims/ are not. They're unmodified files
from the ProtoESP repo, © NCPlyn, GPL-3.0. See
fixtures/README.md; delete the folder and pass the tests a
path to your own firmware checkout if you'd rather not have them.
Protoface contains no code from ProtoESP. It's an independent implementation that reads and writes the same file format.
The firmware, the format, and the original animator are NCPlyn's work: ProtogenHelmet-ESP32. Protoface only writes files their firmware reads; none of this exists without it.
If you're building a protogen on their work, consider sending them something. They ask, and it's fair.

{ "ears": { "type": "rainbow" }, "visor": { "type": "custom", "isMouth": [false,false,true,true,true,false,true,true,true,false,false], "frames": [{ "timespan": 600, // ms this frame is held "fColor": ["0","0", ...], // per-panel colour, "0" = none "ppColor": [{ "mIndex": 3, // per-pixel colour, panel 3 "data": [[9,"00ff88"]] }], // [pixel index, rrggbb] "leds": ["000c1c3870e0c080", ...], // one 64-bit hex string per panel "ledsBlush": ["0","0", ...] // the LEDs under the eyes }] } }