A scrolling banner loops. The text runs off the left edge and the same text follows it in from the right, with a consistent gap, forever. Done properly nobody can tell where one repetition ends and the next begins.

Done improperly, there is a visible jump at the seam, and users describe it as the banner "glitching." Ours glitched, and only for some messages.

The loop is a width calculation

To draw a continuous loop you need one number: the width of the text plus the gap you want between repetitions. Draw the text at the current offset, then again one period further right, and reset the offset by exactly one period when it exceeds it.

If the width you used is wrong by even a few pixels, the reset does not land where the second copy was drawn, and every loop produces a small jump. The error is invisible while the text scrolls and unmistakable at the seam.

So everything comes down to measuring text, which sounds like a solved problem.

Measuring text is not one number

The measurement API gives you an advance width — how far the drawing position moves. That is not the same as the ink extent, which is how far the glyphs actually reach. For most text the difference is negligible. For italic or heavily sloped faces, a final letter can overhang its advance, so text measured by advance and drawn at that spacing has the last glyph clipped by the next copy.

Then fonts fall back. A message containing Korean, Latin, and a symbol may be rendered from three different font files, chosen per character by the system. Measuring the whole string as one run works — the platform handles the fallback — but measuring pieces and adding them up does not, because the fallback decision can depend on surrounding context.

And then emoji.

Emoji are not characters

An emoji can be several code points. A flag is a pair of regional indicators. A person with a skin tone modifier is a base plus a modifier. Family and profession emoji are sequences joined by zero-width joiners, sometimes five or six code points, rendered as one glyph.

Any code that walks a string by code unit, or by code point, and measures each in isolation, gets a completely different total from what is drawn. A ZWJ sequence measured piecewise gives you the sum of several separate glyph widths; drawn, it produces one.

Our width calculation was doing exactly that, for reasons that had nothing to do with emoji — it was walking the string to handle per-character colouring. So a message with a flag in it measured much wider than it drew, and the loop reset late, producing a visible gap and then a jump.

The fix is to measure the string as a whole, using the same layout path that draws it, and to walk text by grapheme cluster rather than by character anywhere iteration is genuinely needed. The general rule: measure with the thing that draws. Any second implementation of text layout will disagree with the first, and the disagreement will surface on someone else's phone with someone else's language.

Contrast, and the colours that fail at distance

The other half of readability is not code, and the mistakes are consistent.

A phone at arm's length flatters colour combinations that fail completely at ten metres. Red text on a black background looks striking on a desk. Across a dim venue it is a smear, because red is the dimmest primary on most displays and the eye resolves it poorly at low light levels and small angular sizes.

What works at a distance is high luminance contrast, and the safe choices are unglamorous: white, yellow, or cyan on black. Yellow on black is the highest-contrast pairing most displays can produce and it is what road signage uses for a reason.

What fails: any two colours of similar brightness, however different their hue. Blue on black — blue is dim. Thin type in any colour, since stroke weight matters more than size at distance.

Dark background rather than light is also a battery and comfort choice on an OLED panel, and it is what makes the text read as a lit sign rather than a bright rectangle.

Writing for a moving target

Cut it shorter than feels right. Scrolling text is read in fragments, and most viewers catch part of one pass. "Pickup here" beats a polite sentence. A three-word cheer beats an eight-second one.

Front-load the important word. A name, a number, a destination. People look, read what is passing, and look away. Anything at the end of the loop is seen by fewer people.

Match speed to what the reader has to do with it. Fast is energetic and comprehends badly — fine for a repeating cheer where the movement is the message. Slow for anything containing information someone must act on. And set it for a reader who does not already know what it says.

Longer distance needs slower movement and heavier type. Both, not one.

Test from where the reader will stand. Hold the phone at the real distance, look away, glance back. If you cannot read it in one glance, shorten it, slow it, or raise the contrast. Fifteen seconds, and it is the only test worth running.

Prepare in advance. An arrivals hall or a crowded venue is a bad place to be typing. Write the message beforehand and save it. The point of a sign is being ready at the moment the other person looks up.

Situations and what they want

Meeting someone — their name, large, slow, high contrast. This is the case where the message is read once by one person who is scanning a crowd, so nothing else should be on screen.

A cheer at a concert — short, fast, colourful. Comprehension is barely the point; visibility is.

A pickup or delivery — the instruction and nothing else. "Side entrance." "Table 4." A driver reads it from a car, at an angle, in under a second.

A stall or counter — this is the one case where a longer message works, because the reader is close and stationary. Slow it down and let it be a sentence.