This page documents and demonstrates the <rl‑display>
web component. Its main goal is to provide a modern and aesthetically pleasing TTY-like renderer, suitable for Roguelike games. <rl‑display>
remains true to the web platform, rendering individual glyphs via HTML DOM. This allows for CSS-based styling and the application of Web Animations for smoother visual experience.
All examples on this page are interactive; you can click and edit them as necessary. They are powered by the <x-ample> web component. Additional information:
yield
keyword.Once you link the <rl‑display>
library to your HTML page, you can start using the corresponding HTML tag. Its instance can be created in three ways:
new
operator)document.createElement
callManipulating the display is performed with JS calls, so it makes most sense to create the display using JavaScript as well. Let's create a display and draw some characters inside:
The draw
method is the main device for displaying stuff. Every call displays one character at a position given by two coordinates (x and y). The second argument is an object used to configure the visual properties. It might contain:
ch
– the character being drawnfg
– foreground (text) colorbg
– background colorThese properties are optional. The foreground color defaults to gray and the background is transparent (so you see the black background color of the display itself). Let's fill the display with mid-dots on a random background:
Since <rl‑display>
is an HTML element and uses other HTML elements to show stuff, the primary device for styling is CSS. Some properties, however, are configured via JavaScript. In the example above, we see the cols
and rows
properties being used to retrieve the display's size in characters. We can use these properties to change the size as well:
Primary CSS properties used to adjust the display are:
display
– the default is inline-flex
background-color
– the default is black
color
– the default is gray
font-family
– the default is monospace
Let's try customizing these:
Finally, to specify the size of individual characters, we use two custom CSS properties:
--tile-width
specifies the horizontal pitch
--tile-height
specifies the vertical pitch as well as the font size
What is the resulting size of a <rl‑display>
? The render width (in pixels) can be computed by multiplying --tile-width
by the cols
property (which specifies the number of characters, horizontally). However, game maps are often very large and it is not always possible to fit them on the screen. While you can reduce the font size (and therefore --tile-width
and --tile-height
) to make the display smaller, another approach is feasible as well: show only a subset of the large map.
Rendering a subset can be done in a straightforward manner, by manually computing the set of visible coordinates and drawing using an offset. This approach works, but becomes more difficult once you decide to move your camera. To facilitate easier rendering of smaller map subsets, <rl‑display>
offers another approach: the panning viewport. Let's see how that works.
When you adjust display.cols
and --tile-width
, you are influencing the display's final width indirectly. However, you are free to set the CSS width for the <rl‑display>
element directly. Once you do that, you decouple what the user sees (the viewport, whose size is defined explicitly) and what is drawn on the canvas (individual characters rendered via display.draw
). Most frequently, the viewport is smaller than the canvas, resulting in an overflow (which can be typically observed on a <textarea>
HTML element with fixed size). By default, the viewport is centered over the canvas. This can be changed using the panTo
call:
The arguments passed to panTo
are display coordinates of the canvas character that shall be centered inside the viewport. It is possible to reset the viewport pan back to the canvas center using panToCenter
:
The <rl‑display>
element now behaves similarly to a regular web map. The last step to make the panning process even more fancy is to introduce zooming. Instead of drawing larger characters, we can temporarily scale up the whole display with the scaleTo
method:
Scaling uses a single numeric argument that specifies the final scaling ratio, one being the default zoom (where the characters are drawn at the size specified via --tile-height
). Note that scaling maintains the viewport-to-canvas mapping established by the last panTo
call.
Panning and scaling is animated and therefore asynchronous. These methods are returning Promises, so in order to combine panning and scaling, it can be useful to combine the retured values with Promise.all
:
It is possible to apply various visual effects to glyphs drawn into the display. To do that, we need a way to refer to individual characters – a unique identifier. There are two ways to do that:
id
as a part of an optional (fourth) configuration object passed to the draw()
method.id
as a return value from the draw()
call.Let's test both approaches with the most basic effect, the move()
function:
The move()
method needs an id
and target coordinates (any JS value can be used as an id
). It is asynchronous and returns a Promise.
A more generic fx()
method is provided as well as a set of built-in animations suitable for various effects. To specify a particular animation, use a pre-defined string (we will see these in a moment) or a standard keyframe definition suitable for the Element.prototype.animate() method. These are the strings available:
"fade-in"
"fade-out"
"pulse"
"jump"
"explode"
Let's see them in action:
The value returned from the fx()
method is an instance of the Animation class. You can use it, for example, to wait for the completion (via its finished
property) or to cancel a long-running animation (via its cancel()
method). Let's test this along with a custom animation definition:
The last (third) argument to fx()
is the animation's duration. Timing of various animated activities is explained in more detail in the following chapter.
Many operations performed on a <rl‑display>
are animated and thus happen over a period of time. Durations (and other time-related properties) of these animations are configurable. Custom timing can be specified by providing an optional (last) argument to these methods:
scaleTo()
panTo()
panToCenter()
move()
fx()
We are using the same data type that is used as a last argument to the KeyframeEffect constructor. Two values are possible:
duration
and iterations
)The example below shows different movement speeds specified using both data types:
The second form is more verbose, but can be useful when you need to specify additional properties, such as the iteration count. We can create an infinite animation that runs unless explicitly stopped:
What happens if you draw at a position that already contains something? By default, the <rl‑display>
behaves like your traditional terminal: the original content is overwritten and completely lost. However, the draw()
method allows you to specify a z-index value that corresponds to a drawing layer depth. With this approach,
This means that the display remembers previously drawn characters and once the top one disappears, the one below it gets shown:
Note how the dot below the player character gets hidden and re-appears once the player moves away. The z-index value is specified using the same optional configuration object that is used to specify the id (and defaults to zero).
There are two main ways to remove some previously-drawn content: by-id and by-position. We can leverage the fact that every object drawn has a unique identifier. The delete()
method can be used to remove it:
To remove by position, we need to supply a pair of coordinates to the deleteAt()
method. But because one position can hold many characters (with different z-indexes), we need to supply a z-index as well (as the third argument). This way, the id is no longer necessary:
Web apps often need to handle a large variety of different display devices and their sizes. This might prove difficult given the spatial constraints of the game engine – if the game map needs to be large, visualizing it on a small device is hard. In Part 3, we show a possible approach that limits the viewport and leverages panning to show different parts of the whole map canvas. An alternative way is to pick the character size in a way that the map always fits the requested display size.
The static method computeTileSize
can be used to derive a suitable set of tile dimensions, according to given constraints. To call it, we need:
tileCount
– an pair of [cols, rows] describing the number of characters to fit,area
– a pair of [width, height] (in pixels) of the target available rendering area,aspectRatioRange
– a range of [min, max] number values that limit the target tile aspect ratioIn Part 6, we learned that one position can only display one character (the one with the largest z-index). However, by setting the overlap
property, we can enable rendering all stacked characters together (ordered by their z-index). This might be useful in some scenarios as an additional paiting effect.