Split-flap message board

HTML

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" 
          content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="./main.css"/>
    <script src="./main.js"></script>
  </head>
  <body>
    <div>
      <textarea id="message" 
                rows="6" cols="80"
                maxlength="2000">The apparition
of these faces in the crowd:
Petals on a wet, black bough.</textarea>
    </div>
    <div>
      <button id="run" type="button">Run</button>
    </div>
    <br/>
    <div id="display-container"></div>
  </body>
</html>

CSS

:root {
  /* these are overridden by our JS code */
  --msg-font-size: 16px;
  --glyph-split-gap: 2px;
}

body {
  font-family: helvetica;
}

#display-container {
  font-size: var(--msg-font-size);
  display: none;
  background-color: #121212;
  width: fit-content;
  border: solid rgba(90,90,90,0.7) 2px;
  border-radius: 3px;
}

.display-line {
  display: flex;
  flex-flow: row wrap;
  justify-content: flex-start;
  align-content: flex-start;
  width: fit-content;
  gap: 5px;
  padding: 3px;
}

.display-cell {
  display: flex;
  justify-content: center;
  align-items: center;
  border: solid rgba(15,15,15,0.5) 1px;
  box-shadow: inset  2px  2px 2px rgba(35,35,35,0.7),
              inset -2px -2px 2px rgba(70,70,70,0.3);
  position: relative;
  background: #1c1c1c;
  width: 1em;
  height: 1.2em;
}

/* draws a horizontal line through 
   the middle of each display cell,
   as an alternative to using the
   `.glyph-split-gap` class        
.display-cell::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 1px;
  background: black;
  transform: translateY(-50%);
  z-index: 0;
}
*/

.glyph-top {
  background: linear-gradient(
    to bottom,
    white 50%,
    transparent 50%
  );
  background-clip: text;
  color: transparent;
  z-index: 10;
}

.glyph-bottom {
  background: linear-gradient(
    to bottom,
    transparent 50%,
    white 50%
  );
  background-clip: text;
  color: transparent;
  position: absolute;
  z-index: 10;
}

/* see the JS code for how this is used */
.glyph-split-gap {
  top: var(--glyph-split-gap);
}

JavaScript

/*
  Split-flap display emulation.
  https://en.wikipedia.org/wiki/Split-flap_display

  Fakes a flap flipping by displaying the top half of
  a letter slightly before the lower half.

  Inspired by these two examples:

   - https://gist.github.com/veltman/f2b2a06d4ffa62f4d39d5ebac5ceeef0
   - https://github.com/paulcuth/departure-board

  See https://github.com/northcoder-repo/split-flap-demo

*/

const msgFontSize = '20px';

/* gap between bottom and    */
/* top halves of glyphs      */
/* set to 'none' for no gap, */
/* or a valid CSS value      */
/* ('2px', '5%', etc.)       */
const glyphSplitGap = '1px';

// speed of transitions (in milliseconds):
const flapPause = 60; // top half of glyph to bottom half
const glyphPause = 180; // pause between glyph sequence
const cascade = 90; // speed of flow from one cell to next

const maxMessageLength = 500;

// show a gap between top and 
// bottom halves of glyphs:
const letterGap = true;

let message = '';
// any char in the message which is not in the `characters`
// list must be replaced by a character which is in the list,
// otherwise the search for a match would never end.
const replacementChar = '☒';

// All the valid characters which can be displayed:
const characters = ' ' // space character (should be first in list!)
    + '1234567890' 
    + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' // uppercase only!
    + ',.?;:/\'"!&()-’'
    //+ '@#$%^_[{]}=+|~*' // less commmonly used
    + replacementChar; // Do not remove (must be last in list!)

// ------------------- s e t - u p ------------------------- //

// build a rectangular display grid matching the shape
// of the message to be displayed (as wide as the longest
// line in the message).

function createDisplayLine(lineDiv, charsPerLine) {
  for (let i = 0; i < charsPerLine; i++) {
    const glyphDiv = document.createElement("div");
    glyphDiv.classList.add("display-cell");

    const top =...