GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Dot Matrix Display: 9 Canvas LED Sign Scenes

How does an LED sign turn a letter into dots? 01 lights only the 1 cells of a 5x7 table, 03 leaves an afterglow on dark dots, and 09 lights one row at a time

A Dot Matrix Display Is Not Writing Letters on a Screen but a Calculation That Turns Each Letter Into a 5 by 7 Table of Dots and Lights It Cell by Cell: Building One Starts by Deciding the Glyph Table and the Flowing Column Index

Auto-plays · click a tile to jump to its section · all nine in one zip

One frame carries the nine scenes. A 480x300 canvas holds two or three dot grid boards, and one board is usually 64 columns by 7 rows with a dot pitch of PITCH 7px. No library, WebGL or CDN is used, and all nine files contain zero random numbers, date calls, fetch requests or outside URLs, so motion comes from the elapsed time on the scene clock alone.

One function argument decides the brightness of a dot

board takes the board position, the pitch, the radius ratio and an lvAt(c, r) callback, and the brightness of a lit dot comes from the single value between 0 and 1 that lvAt returns. It paints the dark base dot first, adds a faint halo ring and the dot when brightness passes 0.03, and rounds dots above 0.85 with one small highlight. The callback appears 23 times across the nine scenes, and the only thing that changes between them is the arithmetic inside lvAt.

One letter becomes five 7-bit integers

The glyph table holds 45 characters: uppercase letters, digits, ! . - : % + > * and the space, each written as seven strings like '01110'. colsOf flattens a phrase into a list of columns where one column is a 7-bit integer with bit 0 on the top row, one blank column sits between letters, and the trailing blank column is dropped.

    // Flatten a phrase into a list of columns - one column is a 7-bit integer (bit 0 is the top row). The blank column between letters stays in the list and the trailing one is dropped
    function colsOf(s) {
        const out = [];
        for (let i = 0; i < s.length; i += 1) {
            const rows = glyphRows(s.charAt(i));
            for (let c = 0; c < GLYPH_W; c += 1) {
                let v = 0;
                for (let r = 0; r < GLYPH_H; r += 1) {
                    if (rows[r].charAt(c) === '1') {
                        v |= 1 << r;
                    }
                }
                out.push(v);
            }
            if (i < s.length - 1) {
                out.push(0);
            }
        }
        return out;
    }

An auto demo that runs on the timetable alone

Scenes move with no input at all. The first board starts START_S ahead so the page opens mid-scroll, and after LAP_S the scene repeats from the beginning. Time carried through one frame is capped at DT_MAX 0.05 seconds.

Dot Matrix Display: 9 Scenes — a 5x7 Dot Font, a Column Scroll Without a Seam, Afterglow on Dark Dots, a Row Roll Between Phrases, Brightness Levels, Dot Pitch and Radius, Measuring Width to Hold or Scroll, a Color Ramp by Column and a Row Scan Drive

Why the nine scenes sit in this order, and what the render measured

Scene 01 comes first because the glyph table is what the rest stand on, 02 to 05 cover how dots switch as time passes, and 06 to 08 are board-level decisions about dot shape, width and color. Scene 09 closes by explaining why a real sign reads as one picture.

Scene Cumulative area Moving frames Mean intensity
01 5x7 font 14.1% 22/23 73.8
02 Column scroll 37.4% 23/23 105.8
03 Afterglow 24.0% 23/23 80.8
04 Row roll 20.4% 7/23 104.6
05 Brightness levels 9.7% 12/23 43.0
06 Dot pitch 28.3% 23/23 110.8
07 Width measure 12.0% 23/23 118.7
08 Color ramp 28.1% 23/23 114.5
09 Row scan 19.9% 23/23 58.2

Measured after rendering, 02 covers the widest area at 37.4% and 05 the narrowest at 9.7%, while mean intensity peaks at 118.7 in 07 and bottoms out at 43.0 in 05. Scene 04 holds still for 1.6 of the 2.16 seconds each phrase gets, so it moved in only 7/23 frames, and 05 has a rest between the brightening and the dimming span, which leaves it at 12/23.

01A 5x7 Dot Font Stamped on a Grid

The smallest way to draw a short phrase in dots with no font file. OPEN 24H is stamped one letter per 1.0 second, and inside a letter the first 84% of the time reads the 35 table cells in column order, while the left board draws square dots and the right board round dots from the same table. lvOf returns 0 when the column position inside a letter reaches GLYPH_W, which is the blank column between letters.

A 5x7 Dot Font Stamped on a Grid
Open in a new tab

01 · A 5x7 Dot Font Stamped on a Grid — Measured area 14.124% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

GLYPH_WGLYPH_HglyphRows
    function lvOf(c, r, first, idx, n) {
        const s = Math.floor(c / ADV);
        const cc = c % ADV;
        if (cc >= GLYPH_W) {
            return 0;
        }
        const k = first + s;
        if (k > idx) {
            return 0;
        }
        if (glyphRows(MSG.charAt(k))[r].charAt(cc) !== '1') {
            return 0;
        }
        if (k < idx) {
            return 1;
        }
        const cell = cc * GLYPH_H + r;
        return cell < n ? (cell === n - 1 ? 1 : 0.9) : 0;
    }

02Scrolling by Whole Columns Without a Seam

The basic motion of a sign where one notice line flows without stopping. The start column index rises by SCROLL_SPEED 24 columns per second, and Math.floor keeps it whole so dots do not smear, while GAP_COLS 13 blank columns follow the 107 columns of SALE 30% OFF TODAY to make a 120-column loop. At the point where the end hands over to the start, 13 blank columns run in a row; set the gap to 0 and there are none, so the closing Y and the opening S touch.

Scrolling by Whole Columns Without a Seam
Open in a new tab

02 · Scrolling by Whole Columns Without a Seam — Measured area 37.431% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SCROLL_SPEEDGAP_COLScolAt
    // Loop length in columns - gap blank columns follow the phrase columns
    function loopLen(gap) { return cols.length + gap; }
    // Whether cell c is lit from start column i - read from the circular list
    function colAt(i, c, r, gap) { return loopBit(cols, gap, i + c - LEAD, r); }
    function headCol() { return Math.floor(clock() * SCROLL_SPEED); }
    // Board cell of the first phrase column - -1 when outside the visible range
    function seamCol(gap) {
        const n = loopLen(gap);
        const c = (((LEAD - headCol()) % n) + n) % n;
        return c < VIEW_COLS ? c : -1;
    }

03The Afterglow Left on a Dot That Switched Off

Afterimage handling that makes motion look smooth to the eye. Each dot keeps its own brightness, jumps straight to 1 when it should be lit and is multiplied down by a decay once it goes dark; the top board multiplies so that DECAY 0.55 applies per TICK_S 0.05 seconds of elapsed time, and the middle board multiplies 0.55 on each frame of a 144 Hz screen. One column behind the head the leftover brightness is 0.608 on the time basis and 0.028 on the frame basis, so the tail above 0.1 splits into 4 columns versus 0.

The Afterglow Left on a Dot That Switched Off
Open in a new tab

03 · The Afterglow Left on a Dot That Switched Off — Measured area 24.035% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

DECAYlevelfadeStep
    // Brightness left on a dot k columns behind the head - time basis and frame basis
    function tailT(k) { return Math.pow(DECAY, k / SCROLL_SPEED / TICK_S); }
    function tailF(k) { return Math.pow(DECAY, k / SCROLL_SPEED * FRAME_HZ); }
    function tailLen(f) {
        let n = 0;
        while (n < 60 && f(n + 1) > 0.1) {
            n += 1;
        }
        return n;
    }

04A Row-by-Row Roll When the Phrase Changes

A swap method for a notice board or a waiting-number board that cycles several messages. The 7 rows of the old phrase, one blank row and the 7 rows of the new phrase stack into a 15-row dummy, and a 7-row window rolls up one row every ROLL_STEP 0.07 seconds so 8 rows travel; whole rows mean dots do not smear, and the window stops exactly on the new phrase. The middle board swaps in one cut at swapAt, and the bottom board pushes by fractional rows so dots smear.

A Row-by-Row Roll When the Phrase Changes
Open in a new tab

04 · A Row-by-Row Roll When the Phrase Changes — Measured area 20.402% · moving frames 7/23Original by GODRICH · SCSS and React ship in the zip · See the password

ROLL_STEProwOffsetswapAt
    // Rows rolled since the roll began - an integer from 0 to ROLL_ROWS
    function rowOffset(u) {
        return clamp(Math.floor((u - HOLD_S) / ROLL_STEP), 0, ROLL_ROWS);
    }
    // Cut swap - changes all at once halfway through the roll
    function swapAt(u) { return u >= HOLD_S + (ROLL_ROWS / 2) * ROLL_STEP; }
    // Column c of dummy row s - rows 0 to 6 are the old phrase, 7 is the blank row, 8 to 14 the new phrase
    function stackBit(pi, s, c) {
        if (s === GLYPH_H || s < 0 || s > GLYPH_H + GLYPH_H) {
            return 0;
        }
        const old = s < GLYPH_H;
        const cols = PH_COLS[old ? pi : (pi + 1) % PHRASES.length];
        const start = Math.floor((VIEW_COLS - cols.length) / 2);
        return bitOf(cols, c - start, old ? s : s - GLYPH_H - 1);
    }

05Fading Through a Few Brightness Levels

A yardstick for choosing between coarse and smooth brightness like cheap hardware. The phrase brightens through an incoming smear band RAMP_W 22 columns wide and dims in the same order, and quantize rounds the continuous brightness of each dot to the nearest step through stepOf. Drawing the same fade side by side in 2 levels, LEVELS 4 levels and 16 levels shows the band breaking into stair steps as levels get fewer.

Fading Through a Few Brightness Levels
Open in a new tab

05 · Fading Through a Few Brightness Levels — Measured area 9.708% · moving frames 12/23Original by GODRICH · SCSS and React ship in the zip · See the password

LEVELSquantizestepOf
    // Which step - 0 to n - 1
    function stepOf(v, n) { return Math.round(clamp(v, 0, 1) * (n - 1)); }
    // To the nearest step brightness - n steps means 0, 1/(n-1), up to 1
    function quantize(v, n) { return n <= 1 ? 0 : stepOf(v, n) / (n - 1); }
    // Continuous brightness of column c - the incoming smear band minus the outgoing smear band
    function fadeAt(c) {
        const xin = lerp(0, VIEW_COLS + RAMP_W, clamp((sceneT - IN_T0) / (IN_T1 - IN_T0), 0, 1));
        const xout = lerp(0, VIEW_COLS + RAMP_W, clamp((sceneT - OUT_T0) / (OUT_T1 - OUT_T0), 0, 1));
        return clamp((xin - c) / RAMP_W, 0, 1) - clamp((xout - c) / RAMP_W, 0, 1);
    }
    function lv(c, r, n) {
        return bitOf(cols, c - START_COL, r) === 1 ? quantize(fadeAt(c), n) : 0;
    }

06The Ratio of Dot Pitch to Radius

A grid setting that keeps the dot feel across screen sizes. DOT_FILL, the radius as a share of the PITCH 7px between dot centers, is a value with no unit: at 0.4 the radius is 2.8px with 1.4px of air between dots, and thin 0.2 dots sit at a 1.4px radius with 4.2px of air. On the bottom board the ratio swings between 0.18 and 0.5, where 0.5 leaves zero air and neighbors touch, while softEdge lowers the edge ring as the ratio grows.

The Ratio of Dot Pitch to Radius
Open in a new tab

06 · The Ratio of Dot Pitch to Radius — Measured area 28.342% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

PITCHDOT_FILLsoftEdge
    // Edge softening - a larger radius ratio overlaps neighbors, so lower the ring multiplier. 0 gives a hard dot
    function softEdge(fill) { return clamp(0.5 / fill * 0.92, 1.05, 1.9); }
    // The swinging board's current ratio - one period runs FILL_LO to FILL_HI and back
    function fillNow() { return lerp(FILL_LO, FILL_HI, 0.5 - 0.5 * Math.cos(TAU * clock() / FILL_PERIOD)); }
    function lvAt(c, r) {
        return loopBit(cols, GAP_COLS, Math.floor(clock() * SCROLL_SPEED) + c - LEAD, r);
    }
    function gapPx(fill) { return PITCH - 2 * PITCH * fill; }

07Measuring Width to Hold or Scroll

A rule that picks the easier reading mode for a board whose phrase length changes each time. When the count from colCount runs past VIEW_COLS 64 the phrase scrolls, and otherwise it holds in the center of the board; the short OPEN 24H is 47 columns, leaving 8 columns on one side and 9 on the other. Every 6.5 seconds the board hangs the short phrase and the 209-column WELCOME TO THE DOT MATRIX SIGN 5X7 in turn, and the long one flows.

Measuring Width to Hold or Scroll
Open in a new tab

07 · Measuring Width to Hold or Scroll — Measured area 12.04% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

VIEW_COLScolCountholdOrScroll
    // One column counter decides both branches - past the board width it scrolls, otherwise it holds
    function colCount(cols) { return cols.length; }
    function holdOrScroll(cols) { return colCount(cols) > VIEW_COLS ? 'scroll' : 'hold'; }
    // The short phrase sits once in the board center with no loop, the long phrase reads from the circular list
    function lvAt(c, r) {
        if (curIdx() === 0) {
            return bitOf(SHORT_COLS, c - Math.floor((VIEW_COLS - SHORT_COLS.length) / 2), r);
        }
        return loopBit(LONG_COLS, GAP_COLS, headCol() + c - LEAD, r);
    }

08Dots Whose Color Follows Column Position

A method for a station board or a waiting display that signals speed or state by color. Color belongs to the board column rather than the letter, so as the phrase flows past, the same letter reads red, then amber, then lime green, and colorAt blends linearly between the three RAMP_STOPS with lerpRgb so there is no sudden break. The middle board's zoneAt cuts into three zones with no blending, and the strip below reads the rgb of one column picked by sampleCol on a 5-second sweep.

Dots Whose Color Follows Column Position
Open in a new tab

08 · Dots Whose Color Follows Column Position — Measured area 28.085% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

RAMP_STOPScolorAtlerpRgb
    // Blend two colors by ratio k - each of the three channels linearly
    function lerpRgb(a, b, k) { return [lerp(a[0], b[0], k), lerp(a[1], b[1], k), lerp(a[2], b[2], k)]; }
    // Color at board position u (0 to 1) - interpolated between two stops
    function colorAt(u) {
        const x = clamp(u, 0, 1);
        for (let i = 1; i < RAMP_STOPS.length; i += 1) {
            if (x <= RAMP_STOPS[i].at) {
                const a = RAMP_STOPS[i - 1];
                const b = RAMP_STOPS[i];
                return lerpRgb(a.c, b.c, (x - a.at) / (b.at - a.at));
            }
        }
        return RAMP_STOPS[RAMP_STOPS.length - 1].c;
    }
    // Cut into three zones with no interpolation - the stop colors as they are
    function zoneAt(u) {
        const i = Math.min(RAMP_STOPS.length - 1, Math.floor(clamp(u, 0, 0.999) * RAMP_STOPS.length));
        return RAMP_STOPS[i].c;
    }

09A Row Scan That Lights One Row at a Time

A teaching scene that shows by eye why a cheap drive method looks like one picture. The board lights one row at a time at 7x strength, SCAN_HZ counts how many times per second the seven rows go around, and persistence of vision is imitated with an exponential average over EYE_S 0.09 seconds. At 60 sweeps one row swings 16% in brightness and the board merges into one picture, at SCAN_LOW 10 it swings 94% and rows flicker, and the bottom board turns at SLOW_HZ 1.5 so the lit row sweeps from top to bottom.

A Row Scan That Lights One Row at a Time
Open in a new tab

09 · A Row Scan That Lights One Row at a Time — Measured area 19.942% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SCAN_HZrowIndexpersist
    // One tick - light one row at 7x strength and blend into the eye average. The mean brightness matches lighting the whole frame
    function stepEye(lv, ncols, row, t, h) {
        const e = Math.exp(-h / EYE_S);
        for (let c = 0; c < ncols; c += 1) {
            for (let r = 0; r < GLYPH_H; r += 1) {
                const i = c * GLYPH_H + r;
                const on = r === row ? 7 * targetOf(t, c, r) : 0;
                lv[i] = lv[i] * e + on * (1 - e);
            }
        }
    }

When You Draw a Dot Matrix Display in Code, How Many Values Turn One Phrase Into Moving Dots?

A phrase comes down to one list of columns and one start column index, and each scene adds one value that bends dot brightness. That is why all nine scenes share colsOf, loopBit runs in 02, 03, 06, 07, 08 and 09, and bitOf in 04, 05, 07 and 09.

The values one dot carries and what they do

Value Where it comes from What it does
cols colsOf The phrase flattened into a list of 7-bit integer columns
SCROLL_SPEED 24 in 02, 03 and 07, 13 in 06, 12 in 08, 9 in 09 Columns per second added to the start column index
GAP_COLS 13 Blank columns appended after the phrase to make a loop
lvAt(c, r) Different per scene The 0-to-1 brightness handed to board
DECAY, LEVELS, SCAN_HZ 03, 05, 09 Afterglow multiplier, level count, row scan rate

Reading it in the opposite direction

The ASCII art edition reads a scene's pixels to pick letters, while this one lights dots from the 1 cells of a glyph table, so the direction of reading is reversed. Rolling a single number is covered in the number rolling edition, and this one stays with the calculation where dots gather into a phrase.

Flatten the Phrase Into Columns of the Glyph Table, Raise the Start Column With Time and Paint Each Dot While Its Brightness Falls: the Order in Which One Dot Matrix Display Frame Is Drawn

The five steps one frame takes

  1. colsOf flattens the phrase into a list of column integers. One letter is 5 columns and one blank column sits between letters.
  2. The start column index comes from time. Whole-column motion is Math.floor(clock * SCROLL_SPEED).
  3. Each board cell (c, r) reads whether it is lit through loopBit(cols, gap, start + c - LEAD, r). LEAD 4 makes the first phrase column begin at the fourth board cell.
  4. Scenes 03, 05 and 09 keep a brightness per dot and multiply it, cut it into steps or blend it into the eye average.
  5. board paints the base dot, the halo, the dot and the highlight, in that order.

Read colsOf first and the rest shows up as layers stacked on that one list.

The start column index goes around a circular list

The phrase columns are followed by gap blank columns to close a loop, and the index is taken modulo the loop length, so however large the start column grows, the same phrase keeps coming around.

    // Circular list - gap blank columns follow the phrase columns to close one loop. The index is taken modulo the loop length
    function loopBit(cols, gap, c, r) {
        const n = cols.length + gap;
        const k = ((c % n) + n) % n;
        return k < cols.length ? (cols[k] >> r) & 1 : 0;
    }

Putting It on a Real Screen Like a Dot Matrix Display Simulator: Building Without a Library, Writing Phrases as an Array and When the Phrase Changes

One canvas, no library

A single index.html holds the canvas, the pause button and the status line, and dots are painted directly with roundRect, arc and fillRect. This code is written for someone putting a dot grid board on a web page rather than someone shopping for an LED module, so lifting out the scene clock and board alone turns it into a store sign or a notice strip.

Write phrases as an array and measure to pick the mode

Scene 04 keeps four phrases in the PHRASES array — OPEN 24H, SALE 30%, NEW MENU and THANK YOU — and hangs one per segment. When phrases are added or removed, LAP_S follows as the product of PERIOD and the phrase count, so the repeat length adjusts itself. Phrases of mixed length split into hold and scroll through the single holdOrScroll from scene 07.

Gather the moment a phrase changes in one place

Scene 04's slot derives the phrase index and the elapsed time inside that segment in one function, and the status sentence switches to the next phrase name the moment the rolled rows reach ROLL_ROWS. A waiting-number board only needs the number swapped into this structure, and keeping the phrase-change moment in the scene clock alone stops the board and the spoken sentence from drifting apart.

Where a Dot Matrix Display Breaks: a Trap of Moving by Fractional Columns So Dots Smear, a Seam Where the End Touches the Start and Cannot Be Read and an Afterglow That Slows Down Because It Relies on the Frame Gap

Moving by fractional columns smears dots

The middle board of 02 moves at the same speed in fractional columns. At start column 30.5, 159 of the 203 lit dots sit at mid brightness, while whole-column motion lights 120 dots fully and leaves 0 in between.

A seam where the end touches the start cannot be read

With a gap of 13 columns the loop seam runs 13 blank columns in a row, and with 0 it runs none. The blank columns between words inside the phrase number 7, so shrinking the gap to word spacing can make the end and the start read like one word.

An afterglow that leans on the frame gap changes with the screen

The middle board of 03 multiplies 0.55 on each frame, so on a 144 Hz screen the brightness one column back falls to 0.028 and the tail is 0 columns, while the elapsed-time side keeps 4 columns on the same screen. Tie the decay to frames and the tail length differs from device to device with the refresh rate.

Forcing a phrase that does not fit into one mode

Holding the 209-column long phrase still leaves the 64-column board showing 31%, with 139 of 421 lit dots visible and 145 columns hidden. Scrolling the 47-column short phrase instead leaves the board's columns 41% empty over 7 seconds, against 27% when held, with 8 and 9 columns to either side.

Letters missing from the glyph table turn blank

glyphRows maps any letter not in the table to the space glyph. The table holds 45 characters — uppercase letters, digits, seven symbols and the space — so lowercase text and other scripts come out as blank columns; write phrases in capitals or add the letters to the table.

The finished source of all nine scenes sits in the zip card below, and you unpack it by typing the password 7hnz4qjz exactly as shown.

Change the Scroll Speed, the Decay and the Scan Rate and How Much Does the Feel Shift? SCROLL_SPEED, DECAY and SCAN_HZ Compared

Name Value changed Effect
A faster scroll 02 SCROLL_SPEED 24 → 60 One loop shrinks from 5.00 s to 2.00 s and a letter (6 columns) passes in 100 ms instead of 250 ms, so it goes by before it can be read. Lowering it keeps each letter clear but a long phrase makes the loop drag
A longer afterglow 03 DECAY 0.55 → 0.9 The time-based tail stretches from 4 to 26 columns, about four letters wide, and letter shapes blur and get harder to read. Lowering it is crisp but flickers more
Half the scan rate 09 SCAN_HZ 60 → 30 One row's swing doubles from 16% to 32%, yet the whole board's brightness sways only from 2.1% to 4.2%, so it still reads as one picture

What to check before changing a value

Raising the speed in 02 cuts the time the phrase takes to cross the board from 2.67 s to 1.07 s, so a long notice cannot be read in one pass. On the frame basis the same DECAY 0.9 only grows the tail from 0 to 3 columns, so one value reads as two different sizes under the two methods.

The scan rate has a floor

Dropping SCAN_HZ to 12 puts one row's swing at 79% with the whole board swaying 10.3%, and at SCAN_LOW 10 they reach 94% and 12.3%. The board starts to flash as a whole, so the lowest value worth trying as a variant is 30. Going down toward 10 makes the whole board flash, which can trigger seizures in sensitive viewers.

One Frozen Line of a Dot Matrix Display Under Reduced Motion: Accessibility Through a Sentence for the Scene and the Phrase on Show

Under reduced motion only one frame is drawn

When prefers-reduced-motion turns on, the requestAnimationFrame loop is not started; the scene clock is set to REST_S and one frame is drawn. The still frame lands at 5.2 s in 01, 0 s in 02, 3.1 s in 03, 0.8 s in 04, 3.3 s in 05, 2.0 s in 06, 0.6 s in 07, 2.4 s in 08 and 2.6 s in 09, and a change listener re-reads the setting even while the page stays open.

Pausing and restarting from the keyboard

The stage carries role="application" with tabindex 0, so focus makes Enter, Space and P pause, N step the scene clock 0.4 s forward, and R restart from the beginning. The two buttons are pause (aria-pressed) and one step, and pressing the stage toggles pause too.

The phrase on show is announced as a sentence

The canvas carries role="img" with an aria-label, and all nine item descriptions name the phrase on show. The on-screen status line holds values that change nonstop through statusText, but the sentence for screen readers is a separate speakText that names the phrase and the board — the current phrase plus the roll or scroll mode in 04 and 07. The status line itself keeps aria-live off at all times, and pushSpeak writes to the role="status" paragraph only when the sentence changes. During the demo it speaks once, 0.6 s before a board ends, and after a person interacts it speaks only when the phrase or the mode changes, so a stream of values does not flood the reader.

A hidden tab stops the clock

When the tab hides, stopLoop halts the loop, and on return lastTs is cleared so the first frame gap is thrown away and the hidden seconds do not pile into one frame.

Dot Matrix Display FAQ: Building Without a Library, Where a Dot Font Comes From and What You Need Before Putting It on the Web

Can I build a dot matrix display simulator without a library?

You can. The nine scenes run on the canvas 2D calls roundRect, arc and fillRect plus requestAnimationFrame, with no outside scripts.

Where does a 5x7 dot matrix display font come from?

This edition downloads no font file; the GL table writes each letter directly as seven lines like '00000'. To add a letter, write its cells in the same shape and glyphRows picks it up, and anything left unwritten comes out as a space.

What do I need before putting an 8x8-style dot board on the web?

The script lives inside index.html with no fetch and no outside URLs, so there is no server-side work to prepare. Canvas scaling caps devicePixelRatio at 2, so a device at ratio 3 does not grow its pixels 2.25 times over.

Canvas drawing follows the MDN references for Canvas 2D, the animation loop for requestAnimationFrame, a hidden tab for the Page Visibility API and reduced motion for prefers-reduced-motion. A board with values flowing across it continues in the live stock ticker edition.

Enter the archive password

The password is inside this article. You will find it as you read.