Julie's Document Website / Delphi Menu Engine / Rendering Elements
Publish Date
2025-02-09 15:00:00 +0200 UTC
Author
Julie

Rendering Elements

Turning DOM elements into display entities.

Table Of Contents

Introduction

This document describes the process of turning DOM elements into render objects and how those objects are rendered into display entities.

The rendering is part of the delphirender module.

Note: The document uses a lot of pseudo code with syntax similar to TypeScript, but breaks it occasionally for simplicity or brevity.

Data Structure

At the heart of the renderer is the data structure which more or less maps render objects to their DOM elements.

This uses a generic base class called RenderObject which does very little.

1
2
3
4
5
6
7
8
abstract class RenderObject {
  position: Vector2f
  size: Vector2f
  depth: float

  abstract spawn(): void
  abstract kill(): void
}

Aside from some utility functions and methods, that is all the class declares. Almost all of the heavy lifting is up to its inheritors. This class is extended by another render object class, whose purpose is to make handling render objects that only use 1 display entity easier:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
  abstract class SingleEntityRenderObject<T extends Display> : RenderObject {
    entity: T?

    abstract spawnEntity(w: World, l: Location): T

    protected configure(entity: T, trans: Transformation): void {
      // Left for child classes to override
    }

    spawn(): void {
      // Bunch of stuff covered later
    }

    kill(): void {
      // Bunch of stuff covered later
    }
  }

The SingleEntityRenderObject is extended by several other classes:

BoxRenderObject

This is a simple render object that is used to render a single rectangle.

When spawned, this render object sets the text display entity's content to be a single 0 character with text opacity 24. Because any text opacity under 26 stops the text from even being rendered... for some reason.

TextRenderObject
These are themselves actually an abstract class, divided into StringRenderObject and ComponentRenderObject, but they behave almost identically.
ItemRenderObject
Responsible for rendering itemstacks.

And lastly, there is the ElementRenderObject:

1
2
3
4
5
6
7
class ElementRenderObject: RenderObject {
  boxes: BoxRenderObject[]
  children: List<RenderObject>

  style: FullStyle
  comp: ComputedStyleSet
}

They store the background (padding), border and outline as BoxRenderObjects in their boxes array, along with a list of child elements. Element render objects also store a ComputedStyleSet which is the exact same style set as stored in the CSS object model. Meaning that any changes to the CSS object model are easily accessible in the render object. And the style: FullStyle contains the fully computed style values.

The boxes array in element render objects is initialized to always be length 3, with each box being set to particular a part of the element:

  1. Background (padding)
  2. Border
  3. Padding

Each of these boxes also has their depth calculated by iterating over each box and multiplying its index by MICRO_LAYER_DEPTH and adding the element render object's depth to it.

DOM to Render Tree Mapping

This section describes how the DOM is mapped to the render tree. Since this mapping process is handled differently per DOM Element and Node, I'll describe the conversion cases.

Text nodes
Text nodes are easily mapped by simply creating a TextRenderObject instance with the text node's content as the render object's content.
<chat-component> elements
An element render object is created, with it's comp field linked to the DOM element's style set. Then a single text render object is created (Same as in a text node's case) and set as the only child element of the element render object.
<item> elements
A ItemRenderObject is created with the item element's item stack as its content. Then we create an element render object, and set the item render object as its only child.
<input> elements
A StringRenderObject is created with the input element's placeholder or inputted value, depending if the value is unset. Then we create an element render object and set the string render object as its only child.
Elements
An element render object is created. Then we iterate over all children of the DOM element and perform this same mapping on them, with the resulting render objects being added as children to the first element render object.

All render objects have their depth field calculated by taking DOM node's depth (Which is a flat integer) and multiplying it with MACRO_LAYER_DEPTH.

Note: For <item> and <chat-component> render objects, the depths of the item render object and text render object inside the resulting element render object have to be manually set when mapping from the DOM.

Constants

This section will define some constants and try to quickly describe them.

CHAR_PX_SIZE_X and CHAR_PX_SIZE_Y
The size of a single pixel on an unscaled text display. Both constants are equal to 0.025
CH_0_SIZE_X
The width (in pixels) of a text display with the content "0". This constant includes both the character pixels and the 1 pixel of padding the display entity has on both the left and right sides.

The value of this constant is 7.0

CH_0_SIZE_Y
The height (in pixels) of a text display with the content "0". This constant includes both the character pixels and the 1 pixel of padding the display entity has on both the top and bottom.

The value of this constant is 10.0

EMPTY_TD_BLOCK_SIZE_X
The X scale of a text display with the content "0" stretched to fill a whole block.

The value of this constant is 1.0f / (CHAR_PX_SIZE_X * CH_0_SIZE_X) or around 5.714286

EMPTY_TD_BLOCK_SIZE_Y
The Y scale of a text display with the content "0" stretched to fill a whole block.

The value of this constant is 1.0f / (CHAR_PX_SIZE_Y * CH_0_SIZE_Y) or 4.0

BLOCK_OFFSET_X
The distance between a text display entity's visual center and its entity origin point when it's been scaled to EMPTY_TD_BLOCK_SIZE_X and EMPTY_TD_BLOCK_SIZE_Y.

The value of this constant is 0.0717

I won't lie, this is a bad name for this constant.

MICRO_LAYER_DEPTH
The distance between each box inside an element render object on the Z axis.

The value of this constant is 0.0001

Note: This is the smallest I could make this value before Z fighting became a major issue. Luckily, this value is also small enough so it doesn't look like elements are floating above other elements too obviously.

MACRO_LAYER_DEPTH
The distance between each render object on the Z axis.

The value of this constant is MICRO_LAYER_DEPTH * 3, or 0.0003.

Render Screen

The render screen is a plane defined by a position, normal and 4 corners. It is the plane onto which render objects are drawn.

Render screens, although defined by the aforementioned properties, also contain more information. For one, render screens can be rotated to face any direction and be in any orientation possible. Screens can also be scaled.

To accomplish this, they also maintain a left rotation, scale and right rotation values.

It's required to note that the initial size of the screen is not changed when the screen is transformed, rather a scalar is applied to it. This gives us another property that is needed for projection later: screenDimensionScale. This property is the difference between the set screen size and the actual size of the screen in the world.

Rendering Transformations

To render an element, the resulting display entity needs to be transformed in a variety of ways to align it correctly to where it is intended to be, and to ensure is aligned to the screen.

Firstly, I'm going to clarify for anyone that isn't aware, Minecraft provides us with 4 transformations that are stored in a Transformation object. These 4 transformations are, in order:

Translation
This moves, offsets, the entity by a specified amount
Left Rotation
Rotates the entity. Called left rotation because it is applied before scaling, the next step
Scaling
Multiplies the entity's size.
Right Rotation
Rotates the entity. Called right rotation because it is applied after scaling.

Translation

Render objects consider their origin point to be the top left corner of the object's bounding box, display entities, however, are aligned differently. Text Displays are aligned to the bottom middle , while Item displays are aligned to the center middle of the entity.

As such, to align the entity correctly, we first offset the entity along the X axis by half the render object's size. Then, depending on if the entity is an item display or text display, we offset by either half its height, or by its full height, respectively on the Y axis.

And the Z component of the translation becomes equal to the render object's depth value, plus the z-index value, multiplied by 2.9999999e-4.

Text and Box render objects also have to compensate for the visual alignment offset bug display entities have. . To get the compensation value, the scaling step has to be performed first (See next section). After that we use the following function:

1
2
3
  public static float visualCenterOffset(float scaleX) {
    return BLOCK_OFFSET_X * (scaleX / EMPTY_TD_BLOCK_SIZE_X);
  }

The value returned from this function is then subtracted from X translation of the text display.

Scaling

Scaling is handled in a complicated to describe way. As such, I'll describe how its performed per each object type:

ItemRenderObject

Since we know that an item's sprite is, by default, the size of a block in GUI render mode, we can easily figure out the transformation scale by just dividing the intended size by that constant size.

As a side note, item displays are scaled down on the Z axis to prevent them from popping out of the page or from clipping into it. The Z scale is a constant of 0.075. Values less than this tend to mess with the entity's brightness in unexpected ways.

TextRenderObject

First, the object's base text size is measured. Then the intended size of the element is divided by the measured text's size to get the X and Y scale. Z scale remains unchanged.

BoxRenderObject
The scale is gotten by multiplying the object's size x and y sizes with the EMPTY_TD_BLOCK_SIZE_X and EMPTY_TD_BLOCK_SIZE_Y constants respectively.

Projection

Projection is what I choose to call the part of transformation where the display entity is rotated and scaled to be aligned to the screen.

First the X and Y components of the display translation are multiplied by the screenDimensionScale. Then the display scale is multiplied by screen's scale. Then the screen's left and right rotations are applied onto the display translation. And finally, the display's left and right rotations are multiplied by the screen's left and right rotations respectively.

Notes on Transformations

ItemRenderObjects perform the translation and scaling step differently. They execute the same operations, however, they do not use their own render object's size to perform these calculations, they use the parent size.

This is because otherwise, the item display wouldn't scale as intended with CSS changes to the parent ElementRenderObject.


Notes

  1. Not actually the middle. They are slightly offset from the middle, this has caused me so much headache. See Dev Journal, 05-02-2025