eieio.games

by nolen royalty

Teleyegraph: convert blinks to morse code

Who says face tracking isn't useful?

Feb 13, 2024

I made a little tool. It’s called telEyegraph. It converts blinks to morse code.

me, blinking out a dril tweet

I built it because I’m fascinated with controlling things with your eyes and as a test of the React and CSS skills that I spent January learning.

You can play with it here or check out the code on github.

How does morse code work?

Before we talk about how TelEyegraph works let’s talk about the basics of morse code.

Characters in morse code are described using “dits” and “dahs” - you may know these as “dots” and “dashes.” An “S” is written as ”…” (dit dit dit) and an O as ”- - -” (dah dah dah).

Morse code is sequence of signals that are “on” or “off.” Being “on” typically means sending a sound. In TelEyegraph it means your eyes being closed. Being off means silence (or your eyes being open).

Dits and dahs are distinguished by time - a dah is three times longer than a dit. A dit’s length isn’t fixed; different operators may choose different dit speeds (if you’re new to morse code - and I suspect you are - you probably want to use a slow dit speed!).

a guide to morse code from wikipedia. Each letter of the english alphabet is shown, along with the dits and dahs that represent it.

a guide to morse code from wikipedia

To separate dits, dahs, characters, and words we use spaces. One space is represented as being “off” for 1 dit. Dits and dahs within a character are separated by one space, characters within a word are separated by four spaces, and words are separated by seven spaces.

So interpreting morse code is something like:

  1. Agree on a dit speed.
  2. When you’ve been on for one dit add a . to the current signal
  3. When you’ve been on for three dits convert that . to a -
  4. When you’ve been off for one dit append your . or - to the current character
  5. When you’ve been off for four dits convert your current set of signals (e.g - -) to a character (M) and add it to the current word.
  6. When you’ve been off for seven dits, add your current word to the sentence.

Not too bad! How does that translate to React?

Oops I barely know React

Feel free to skip or skim this if you don’t want to hear about my React struggles

Ok, well, I did not translate this to React particularly well. But that’s largely because I was about 3 weeks into Josh W. Comeau’s lovely React course when I made TelEyegraph.

Morse code is all about consuming a long series of short events and updating your state in response to it. React is all about re-rendering your UI in response to state updates. And I struggled to reconcile these two things - to consume events on each video frame without re-rendering the entire site on each video frame (there are a lot of frames in a second - that’s a lot of re-rendering!).

My naive React implementation looked something like:

function useProcessFrame(signalState, updateSignalState, video) {
    const requestRef = React.useRef();

    React.useEffect(() => {
        const processFrame = () => {
            const eyesClosed = determineEyesClosed(video);
            if (eyesClosed) {
                updateSignalState("on");
            } else {
                updateSignalState("off");
            }

            if (signalState.on > DIT_LENGTH) {
                addDit();
            } else if (signalState.off > DIT_LENGTH) {
                addSpace();
            }
            requestRef.current = window.requestAnimationFrame(processFrame);
        }

        requestRef.current = window.requestAnimationFrame(processFrame);
        // Cleanup
        return () => window.cancelAnimationFrame(requestRef.current)
    }, [updateSignalState, signalState]
}

The basic idea here is: every frame, check whether the user is closing their eyes and potentially add a dit or a space to our current state. This is pseudo-codey and doesn’t actually work in React (for one, signalState won’t be updated immediately so our if statements that check its value won’t follow at the right time). But let’s ignore that for now.

The fundamental problem here is that this loop requires access to the current signalState - but the rules of React say that if signalState is a state variable (the type of thing that can force your UI to update when it changes), procesFrame needs to be redeclared every time signalState changes. “Redeclaring” here means canceling the current requestAnimationFrame (using the cleanup function that I return), redefining processFrame, and re-registering it with requestAnimationFrame. And we’d need to do that on every frame - 60 times a second!

I worked around this by using refs 1 - React’s way of declaring state that can be mutated without triggering a state update. I use a ton of refs. Too many. I store all of the state about whether a user is blinking in refs and then have a single state variable called consume that I flip to true when it’s time for the UI to update (when we add a new dit or space). The UI notices that consume is true, updates its state, and then flips consume to false.

1

I’m used to the idea of a ref from OCaml and it’s been cool to see that (and other OCaml-y things) pop up in React.

This…works. It’s ugly and not very React-y, but I didn’t know how else to handle the problem.

This was frustrating to me because I know how I’d like to solve this in a functional way - I want a state machine that consumes a sequence of updates (“ons” and “offs”) and potentially updates its state in response. I just didn’t know how to make that happen in React.

Immediately after finishing TelEyegraph I went back to Josh’s Class and learned about useReducer, which is React’s way of creating a state machine that receives updates and updates its state in response. I kicked myself a little for not finding it, but hey - I’m never going to forget that useReducer exists.

Visualizing Morse Code

One fun challenge with TelEyegraph was figuring out how to help folks understand how Morse code works, since I figured people wouldn’t come in knowing much.

Fortunately Morse code is pretty straightforward - we use a simple series of rules to move from “creating a dit or dah” to adding a character to adding a word.

I chose to visualize these transitions by fading things in and out. As a character is added to a word, the dits and dahs of that character fade out and the character to be added fades in. The same thing happens when adding a word to a paragraph.

Dits transform into dahs, which fade out as characters fade in

I also chose to add a telegraph button that you could to manually send signals, since it’s probably easier to focus on visualizations when your eyes are open.

I’m pretty happy with how all of this turned out! It certainly gave me a better understanding of Morse code.

Why React?

I’d never used React before - why’d I decide to go learn it?

Well, at the start of this year I realized that I wasn’t very happy with the tools that I was using. I had tons of energy to work on things like flappy dird where I was using technology that I liked, but found myself dragging my feet every time I went back to making a game in Godot.

My initial reaction to this was to try to push through. Sometimes you don’t want to work and still have to! But I eventually decided that that wasn’t the right attitude. I’m building games and toys because it’s fun and I like doing it. I’m at my best when I’m excited about my work. And when my tools are causing me to not like my work, that’s a problem.

I made a list of things that Godot didn’t have that I thought I really needed to be happy as a developer. Things like:

  • A mature ecosystem with a good package manager
  • First class functions 2
  • A well supported code formatter that auto-formats on save
  • A type system that I like
  • The ability to work without a mouse (and a pleasant development experience on a laptop)
2

These apparently exist in Godot 4, but Godot 4 web exports are broken on Macs. The fact that Godot devs don’t treat this as a regression is insane to me.

Javascript (with TypeScript) checks all of those boxes. I enjoyed using it for stranger video - and really enjoyed using CSS to lay things out instead of dragging things with my mouse. And so I figured I’d give it a try.

I started with React because I figured it was a good choice for some of what I wanted to make, that it’d be helpful for understanding other tools built in response to React, that it’d be a good way to get used to writing Javascript, and because I knew of a good class on it. The fact that Josh also had a class on CSS which I also needed to learn certainly helped 3.

3

Josh’s classes are pricey but great. I have no regrets about buying them.

So far I’m really happy with my decisions here. I like using React and I’m excited to build some other stuff with it. I’m excited to learn about the broader Javascript ecosystem and to take a look at tools more purpose built for game building. And more than anything I’m excited that I chose to replace the tools that I didn’t like.

Wrapping Up

I’d like to write more about the experience of picking better tools but I’ll save that for a separate blog. I’ve got all sorts of dumb stuff that I’m ready to go build now - both webapps and broader nonsense. Last night I started working on “pong running in Apple Calendar via iOS shortcuts.” We’ll see how that goes.

Till next time!

Thanks for reading!

Keep up with me on my socials 👆

Or sub to my newsletter here! 👇