Emoting on 8x8
Skill srfinch17/peckworks-skills-laboratory/skills/emoting-on-8x8
A workshop where skills for LLM agents are engineered, not just written: developed test-first against baseline agent behavior, hardened by adversarial review, and required to earn their keep with a logged field-win record.
npx -y skills add srfinch17/peckworks-skills-laboratory --skill emoting-on-8x8Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 28 days oldThe repository was created 28 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Design legible expressions and custom animations for the ESP32-S3 8x8 LED matrix (Claude's expression channel, matrix_express / matrix_animate / the CANNED library in mcp_server/expressions.ts). Use whenever creating, redesigning, or debugging a glyph or animation that has to read on the physical 64-pixel panel, including when an expression "doesn't read," looks like one flat color, flickers, or freezes the board.
SKILL.md
9.5 KB, as published. Nobody here has run it
Emoting on the 8×8 panel
The matrix is Claude's expression window. The hard part isn't drawing; it's that a 64-pixel panel running at brightness 5 can show almost no detail and almost no color nuance. Everything here exists to make a human identify the thing at a glance (the silhouette test). You can't see the LEDs; the user is your eyes; fire, ask, iterate.
First decide: expression or firmware?
Two build paths; pick before you start, because they deploy completely differently:
- Frame-based expression: a glyph/loop you author pixel-by-pixel and push as frames.
Design it HERE with
matrix_animate,save_asto ship. Zero deploy, instantly reversible. Use for emotes, status, wait/idle bits; anything ≤24 hand-drawn frames. - Firmware animation: a generative real-time mode (fire, particles, snow, anything
parametric or endless). That's a new
anim_*.inoand needs a flash. Switch to theadd-animationskill for the 6-step wiring; then come back here for the legibility craft (brightness bands, silhouette, color choice) while you design the look.
Rule of thumb: you author the pixels → expression; the board computes them → firmware. The three tiers below are all within the expression path.
The iron rule: iterate live, promote winners
There are three tiers, with very different costs. Don't prototype in the expensive one.
| Tier | How | Cost |
|---|---|---|
| On-the-fly | matrix_animate (raw frames) | zero (instant, primary path) |
| Saved | matrix_animate + save_as → JSON on disk | zero rebuild/restart (read at runtime) |
| Canned | edit mcp_server/expressions.ts → tsc | rebuild + Claude Code restart |
Always design with matrix_animate. Get it right live, then either save_as it or,
for core vocabulary, paste the final frames into CANNED. Editing CANNED to prototype
forces the slow restart cycle. Trick: to preview a CANNED edit without restarting,
re-send the identical frames via matrix_animate; the running MCP server keeps serving
old dist until CC restarts, so matrix_express won't match source until then.
Mind the brightness-5 thresholds (the #1 cause of "it looks wrong")
The panel usually runs at bri 5. FastLED then scales each channel by 6/256, so a channel only lights if its value ≥ ~43, leaving just 5 visible levels. Two consequences bite constantly:
- Hues collapse. red→orange→yellow become nearly one color (orange dies between
them). For 3 distinct warm shades use white-hot / yellow / red (add the blue
channel for the hottest). For red+orange, push orange green up to ~
#ff8c00so it clears red. - Near-equal colors merge. A cyan window
#bdf4ffis invisible against a white hull#ffffff(both ≈ all-channels-max). Use a real blue#2060ffso it separates.
Rule: pick colors that differ in which channels are lit, not in subtle hue. For a brightness gradient, pin levels to band centers so each is distinct on hardware:
| Want level | Channel value |
|---|---|
| 1 (dim) | 64 |
| 2 | 107 |
| 3 | 149 |
| 4 | 192 |
| 5 (bright) | 235 |
If a hue genuinely won't separate, say so (it needs brightness > ~15) and let the user
choose rather than shipping mud. (See docs/LED_BRIGHTNESS.md, CLAUDE.md.)
Static glyphs: downsample, don't freehand
Hand-drawn 8×8 icons look like blobs. Instead: take a real reference icon, picture it
solid (flood-filled), and shrink it to 8×8 keeping the silhouette. One bold subject,
≤3 colors, dark background, no 1-pixel details, no text beyond ~2 chars. Found a great
starter set already in data/sketch.html (the paint app's STARTERS: rocket, heart,
star, etc.); reuse those shapes.
The fast path when the user hands you a reference image (this nailed the Claude-mascot alien in minutes): don't eyeball it; run a throwaway Python script.
- Mask on the subject's color, not alpha. Exported PNGs usually have a solid opaque background (often black), so alpha-masking selects the whole frame; threshold on the fill color instead.
- Crop to the mask's bounding box, then box-average the binary mask into 8×8.
- Print BOTH a thresholded
#/.map AND a 0 to 9 coverage map. The coverage numbers let you hand-judge the borderline cells (eyes, legs) the threshold flips; then snap the key features by hand (e.g. force the eyes dark even if the average filled them).
Lock the static resting pose (proportions AND color) to the user's eye BEFORE adding any motion. Animating a pose that doesn't yet read just multiplies the iterations; nailing the static frame first is why the motion then worked first try.
Animation: motion and contrast carry it, not detail
- Motion makes the silhouette. A "busy/working" indicator reads far better as a comet traveling a clear path than as discrete state-hops. Give it directional travel.
- Bursts/explosions flicker: don't morph a shape. An expanding firework (each frame a different concentric ring) reads as disjoint flicker. Switch to a steady-motion field: falling confetti, drifting mist, a sweep. Celebration reads through color + continuous motion, no fragile silhouette needed. Make it loop seamlessly (shift every row by 1 each frame over N frames).
- Attention-grab = photo-negative blink, not on/off flash. Alternate the glyph with
its inverse (every lit cell off, every off cell lit), ~3 blinks, then settle on the
solid glyph (last frame,
loop: 1). Reads as a deliberate "look here," far calmer and clearer than blanking the panel. - Animate a character by moving its negative space. The dark holes (eyes) are the cheapest expression on 64 px: slide the 2 eye-pixels left/right = "looking around"; remove them (eyes→solid) = a blink. No new pixels, no silhouette risk.
- Translate the whole silhouette ±1px; never deform it (keep a blank margin row) so the shape has room to move. A 1px vertical bob + a blink reads as "alive/working"; a 1px horizontal sway + eye-darts reads as "idle/playful." So give sibling animations of the same character different motion axes and they read as genuinely different moods.
- Fake brightness gradients with one hue at fixed steps. Bake the dim values into the hex (per the band table) rather than relying on FastLED dimming, so trails/glows survive bri 5.
Generate complex animations with a script; don't hand-place pixels
For shimmer/particle/gradient effects (e.g. a Frostbite-style sparkle: an icy mist field
plus points fading on sine-bell curves), write a throwaway Python script that emits the
{colors, frames} JSON for matrix_animate. Hand-placing 24×64 pixels is error-prone; a
generator nails timing, loops cleanly via modular time, and lets you re-roll. Quantize
the generator's brightness to the band-center values above so 3+ shades stay visible;
the first instinct (linear scaling) drops dim levels below threshold and collapses to one
color. Mirror the firmware's real effects when one exists (anim_frostbite.ino,
anim_fireworks.ino).
Keep payloads light; heavy frames can crash the board
/api/display/frames parses the whole payload with an elastic JSON doc on top of the
request body (a near-max 24-frame, full-panel animation) is a ~20 KB transient heap spike.
On a tight heap this trips the firmware's low-heap auto-restart (esp32_matrix_webserver.ino)
and the board freezes. PSRAM must be Enabled (Tools → PSRAM); that's the real fix and
the usual culprit. Even so, prefer few frames and sparse lit pixels; reach for 24
full-panel frames only when the effect needs it. If you must reproduce a crash to debug,
do it with the Serial Monitor open and watch the [heap] free=… line; never blindly
re-fire a known crasher.
Workflow checklist
- Design live with
matrix_animate(start light; pick colors per the band table). - Fire and ask: the user is the eyes. One change per state; no spam.
- Iterate on their feedback (motion? contrast? pacing? does it read at a glance?).
- Promote the winner:
save_asfor one-offs, or paste intoCANNED+tscfor core vocabulary (then remind the user a CC restart is needed formatrix_expressto serve it). - Record what the user likes/dislikes in auto-memory.
Live-preview gotcha: the
UserPromptSubmithook fires a wait spinner on every user message, so whatever you're previewing is overwritten the instant the user types; expect to re-fire ("show again") between rounds. It's the hook doing its job, not a bug.
The long game (display-agnostic): the principles here (thinking in a semantic message, the at-a-glance/silhouette test, iterate-live-promote-winners, motion-carries- meaning) transfer to any renderer; only the brightness-band/FastLED/downsample specifics are 8×8. The presence protocol's "one message, many renderers" is where this is headed. Extract a device-agnostic parent skill when a second renderer earns its own emote-design work (not before) (one example can't tell you the right seams).
Spec: docs/superpowers/specs/2026-06-11-claude-expression-display.md.