A line of text is an odd place to build a floor. It becomes a much better idea once a paragraph wraps.

MarkBlah's Lemmings demo gives little walkers the page itself to walk on: borders, buttons, images and individual text fragments. [1] I went through the source because the interesting part is the choice of geometry. The pixels are drawn with CSS. JavaScript measures the terrain and advances the walkers.

Illustrative paragraph fragments act as separate one-way floors. A solid button blocks sideways movement, while text catches a falling walker without trapping it between lines.

Original mechanism illustration based on the source. This is not a screenshot or a recording of the demo.

A paragraph box would make a boring level

An element's getBoundingClientRect() gives a rectangle around it. That is useful for a button. Applied to a whole paragraph, it loses the gaps and different line lengths that make the text interesting as terrain. [2]

The demo walks through text nodes, selects each node with a Range, and collects its getClientRects() fragments. [1][3] Wrapped text can therefore contribute several floor rectangles rather than one large paragraph wall.

There is an equally useful choice after measurement: text is marked as floor-only. It can catch a walker falling from above, while the ordinary solid-obstacle checks leave it out. Boxes and borders participate in those solid checks as well.

Otherwise, closely spaced lines would be a collection of tiny cages. The character could fit on a line but not between the floors above and below it. Making text one-way terrain gives it a different job from a button's box.

From page pixels to a smaller world

The source uses four screen pixels per world unit. It adds scroll offsets to viewport-relative measurements, then rounds the left and top edges down and the right and bottom edges up. [1] Adding scroll offsets is what converts the client rectangle into document coordinates. [2]

Here is my separate arithmetic illustration, using a synthetic rectangle rather than a measurement from a live page:

from math import floor, ceil left, top, right, bottom = 10.5, 20.25, 17.25, 21.25 scroll_x, scroll_y, scale = 8, 100, 4 world = ( floor((left + scroll_x) / scale), floor((top + scroll_y) / scale), ceil((right + scroll_x) / scale), ceil((bottom + scroll_y) / scale), ) print(world) # (4, 30, 7, 31) 

The resulting rectangle covers world x-coordinates from 4 up to, but excluding, 7, and y-coordinates from 30 up to, but excluding, 31. I checked the rounding independently with exact fractions and checked that the right and bottom edges are excluded.

Rounding outward keeps a thin measured feature from disappearing just because its edges fall between world cells. It also makes this a coarse collision world: the resulting shape need not match the painted pixels exactly.

Fast movement needs more than a final position

A second source detail addresses thin surfaces during a fall. The airborne update divides movement into substeps no larger than one world unit on either axis. [1]

For an arithmetic example, put a one-unit platform at y in [10, 11). A point moving directly from y=7 to y=19 lands beyond it. An endpoint-only overlap check sees nothing. Checking the intermediate integer positions finds y=10.

I executed that small numerical case separately. It illustrates the reason for sampling along movement; it is not a claim that I ran the browser's complete collision engine. The upstream loop also handles the walker's body, direction, ceiling and wall checks.

The page can change under your feet

Reflow matters because the world is a document. The source rebuilds its rectangle map after page changes and keeps a surface identity so a standing walker can follow the same surface when it moves. It also has explicit shortcuts for fixed or sticky elements and clipped content. [1]

TheLuigiplayer's browser RPG write-up describes caching rectangles for pointer interaction. [4] The connection is practical: measuring layout and moving an object are separate jobs, and cached geometry needs a clear refresh point.

I haven't rendered this demo or measured its frame cost. The source trace and numerical checks explain the terrain model. If I were borrowing the idea, I would start by deciding which page features should carry a character and which should block it. That choice gives the page its rules before the sprite takes a step.

References

  1. MarkBlah, Lemmings source at the reviewed revision, and the original playground.
  2. MDN contributors, Element.getBoundingClientRect().
  3. MDN contributors, Range.getClientRects().
  4. TheLuigiplayer, Building a 700+ Card Browser RPG with React 19, Vite 8, and Zero Monetization.

Asset credits

Original schematic and arithmetic example. No upstream sprites, screenshots or source files are redistributed with the article. The original demo remains linked to its author.

AI disclosure

AI was used to draft the article.