DEV Community

Cover image for 🎨 Limn Engine Rendering Pipeline — From CSS Background to Dynamic Components
Kehinde Owolabi
Kehinde Owolabi

Posted on

🎨 Limn Engine Rendering Pipeline — From CSS Background to Dynamic Components

🎨 Limn Engine Rendering Pipeline — From CSS Background to Dynamic Components

How Limn Engine Draws Your Game, Layer by Layer

Ever wondered what happens when Limn Engine renders a frame? Behind the simple display.start() lies a carefully designed rendering pipeline that layers everything from CSS backgrounds to dynamic game objects.

This article breaks down every layer of Limn Engine's rendering process — from the bottom (CSS background) to the top (dynamic components).


The Complete Render Order

Limn Engine renders in this exact order, from back to front:

┌─────────────────────────────────────────────────────────────────┐
│  LAYER 0: CSS Background                                      │
│  ↓                                                             │
│  LAYER 1: display.backgroundColor / lgradient / rgradient      │
│  ↓                                                             │
│  LAYER 2: fake.bgComm (Background Component)                   │
│  ↓                                                             │
│  LAYER 3: tileComm (TileMap Layers)                           │
│  ↓                                                             │
│  LAYER 4: commp (Fake Canvas Components)                      │
│  ↓                                                             │
│  LAYER 5: comm (Main Display Components)                      │
│  ↓                                                             │
│  LAYER 6: UI with .fixed() (HUD on top of everything)         │
└─────────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Layer 0: CSS Background (Bottom Layer)

This is the lowest layer. It's the actual CSS background of the canvas element.

display.canvas.style.backgroundColor = "black";
Enter fullscreen mode Exit fullscreen mode

Why it matters: If all other layers fail or are transparent, this is what shows. It's your safety net.

Limn Engine wraps this with convenience methods:

display.backgroundColor("#0d0d2a");     // Solid color
display.lgradient("top", "blue", "black"); // Linear gradient
display.rgradient("white", "navy");      // Radial gradient
Enter fullscreen mode Exit fullscreen mode

When to use: For simple backgrounds or as a fallback. This layer is fast because it's pure CSS.


Layer 1: Display Background Methods

These are CSS-based backgrounds applied to the canvas.

Method Purpose
display.backgroundColor(color) Solid CSS color
display.lgradient(dir, c1, c2) Linear gradient
display.rgradient(c1, c2) Radial gradient
// Solid background
display.backgroundColor("lightblue");

// Gradient sky
display.lgradient("top", "royalblue", "darkblue");
Enter fullscreen mode Exit fullscreen mode

What's happening: These set this.canvas.style.background to the appropriate CSS value. The browser handles the rendering.

When to use: When you need a simple, fast background without using the canvas drawing context.


Layer 2: fake.bgComm (Background Component)

This is the first canvas-drawn layer. fake.bgComm is a special Component that renders behind everything on the fake canvas.

fake.bgComm = new Component(1600, 1200, null, 0, 0, "image");
fake.bgComm.setImage("sky.png");
Enter fullscreen mode Exit fullscreen mode

In the Render Loop

// Inside ani()
if(display.once){
    fake.context.save();
    fake.context.translate(-fake.camera.x, -fake.camera.y);

    // RENDER BACKGROUND FIRST
    if(fake.bgComm){
        fake.bgComm.update(fake.context);
    }

    // THEN render tiles
    tileComm.forEach(...);
    // THEN render other fake objects
    commp.forEach(...);

    fake.context.restore();
}
Enter fullscreen mode Exit fullscreen mode

What's happening: This renders once (or when fake.refresh() is called) to the offscreen fake canvas. It then gets composited with other layers.

When to use: For skyboxes, parallax backgrounds, or any large static background image.


Layer 3: tileComm (TileMap Layers)

Tilemaps render on top of the background but behind everything else.

display.tile = [
    new Component(64, 64, "#2d6a2d", 0, 0), // grass
    new Component(64, 64, "#654321", 0, 0)  // dirt
];

display.map = [
    [1,1,1,1,1],
    [1,0,2,0,1],
    [1,1,1,1,1]
];

display.tileMap();
display.tileFace.show();
Enter fullscreen mode Exit fullscreen mode

In the Render Loop

// Render tiles AFTER background
tileComm.forEach(component => {
    component.x.update(fake.context);
});
Enter fullscreen mode Exit fullscreen mode

What's happening: Tiles are rendered as Components on the fake canvas. They can have colors, images, and even physics.

When to use: For ground, walls, platforms, and any grid-based level elements.


Layer 4: commp (Fake Canvas Components)

These are static Components on the fake canvas that render AFTER tiles.

fake.add(tree, 0);      // Adds a tree on top of tiles
fake.add(decoration, 0); // Adds decorations
Enter fullscreen mode Exit fullscreen mode

In the Render Loop

// Render static components AFTER tiles
commp.forEach(component => {
    if (component.scene == fake.scene) {
        component.x.bUpdate(fake.context);
    }
});
Enter fullscreen mode Exit fullscreen mode

What's happening: These are "fake" Components — they exist on the offscreen canvas and render once. They're perfect for decorations, static obstacles, and anything that doesn't move.

When to use: For trees, rocks, decorations, and any static element that sits on top of the ground.


Layer 5: comm (Main Display Components)

These are dynamic Components on the main display that render EVERY frame.

const player = new Component(40, 40, "blue", 100, 100);
display.add(player);

const enemy = new Component(35, 35, "red", 400, 300);
display.add(enemy);
Enter fullscreen mode Exit fullscreen mode

In the Render Loop

comm.forEach(component => {
    if (component.scene == display.scene) {
        if (component.x.angularMovement) {
            component.x.moveAngle();
        } else {
            component.x.move();
        }

        if (TCJSgameGameArea.crashWith(component.x)) {
            component.x.update(display.context);
        }
    }
});
Enter fullscreen mode Exit fullscreen mode

What's happening: These Components are drawn every frame on the main display. They move, animate, and interact with the player.

When to use: For the player, enemies, bullets, and any moving object.


Layer 6: UI with .fixed() (Top Layer)

UI elements that stay locked to the screen position, even when the camera moves.

const healthBar = new Component(200, 20, "green", 20, 20);
const scoreText = new Tctxt("24px", "Arial", "white", 20, 50);
display.add(healthBar);
display.add(scoreText);

function update() {
    healthBar.fixed(); // Stays at screen position (20,20)
    scoreText.fixed(); // Stays at screen position (20,50)
}
Enter fullscreen mode Exit fullscreen mode

What's happening: .fixed() adds the current camera offset to the Component's anchor position, so it appears at the same screen coordinates regardless of camera movement.

When to use: For all UI — health bars, score displays, menus, and dialogue boxes.


The Complete Render Pipeline Visualization

┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  LAYER 6:  UI with .fixed()              [TOP]         │    │
│  │            (Health bars, score, menus)                   │    │
│  └─────────────────────────────────────────────────────────┘    │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  LAYER 5:  comm (Main Display)                         │    │
│  │            (Player, enemies, bullets - dynamic)         │    │
│  └─────────────────────────────────────────────────────────┘    │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  LAYER 4:  commp (Fake Canvas)                         │    │
│  │            (Trees, decorations - static)                │    │
│  └─────────────────────────────────────────────────────────┘    │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  LAYER 3:  tileComm (TileMap)                          │    │
│  │            (Ground, walls, platforms)                   │    │
│  └─────────────────────────────────────────────────────────┘    │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  LAYER 2:  fake.bgComm (Background Component)          │    │
│  │            (Sky, clouds, mountains)                     │    │
│  └─────────────────────────────────────────────────────────┘    │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  LAYER 1:  CSS Background Methods                      │    │
│  │            (backgroundColor, lgradient, rgradient)      │    │
│  └─────────────────────────────────────────────────────────┘    │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  LAYER 0:  CSS Background (canvas.style.background)   │    │
│  │            (Fallback - bottom layer)                   │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Performance Impact of Each Layer

Layer Renders Once Renders Every Frame Performance Impact
CSS Background ✅ Browser handles Minimal Zero
display.backgroundColor/gradient ✅ Browser handles Minimal Zero
fake.bgComm ✅ Once (cached) ✅ Pasted as image Low
tileComm ✅ Once (cached) ✅ Pasted as image Low
commp ✅ Once (cached) ✅ Pasted as image Low
comm ❌ No (dynamic) ✅ Every frame Medium
UI with .fixed() ❌ No (dynamic) ✅ Every frame Medium

💡 Performance Secret: The fake canvas cache means Layers 2-4 render ONCE and then just get pasted as an image. This is why Limn can run at 60 FPS on a Toshiba with 4GB RAM.


How to Use This Knowledge

For Better Performance

If you have... Use... Why
Static backgrounds fake.bgComm Renders once, cached forever
Static decorations fake.add() Renders once, cached forever
Moving objects display.add() Renders every frame (required)
UI elements .fixed() Stays on screen, camera-proof

For Better Organization

// Clear separation of layers

// 1. Background
fake.bgComm = new Component(1600, 1200, null, 0, 0, "image");
fake.bgComm.setImage("sky.png");

// 2. Tilemap
const tilemap = new TileMap(display, level, tiles, 1600, 1200);
tilemap.show();

// 3. Static decorations
fake.add(tree);
fake.add(rock);

// 4. Dynamic objects
display.add(player);
display.add(enemy);

// 5. UI
const scoreText = new Tctxt(...);
display.add(scoreText);
Enter fullscreen mode Exit fullscreen mode

The One-Line Summary

"Limn Engine renders from CSS background up to dynamic objects, with three layers cached on the fake canvas for maximum performance and three layers drawn every frame for maximum flexibility."


Draw your game into existence — one layer at a time. 🎨

Top comments (0)