๐ŸŸข Level 1 โ€” Beginner L1

1. Creating a Display

The Display class is the foundation of every Limn Engine game โ€” it creates the HTML5 canvas, attaches it to the page, listens for keyboard and mouse input, and starts the game loop that drives everything.
see more...

To start a game you create one Display instance and call display.start(width, height) on it. Under the hood, start() sets the canvas dimensions, inserts the canvas into the page, registers all keyboard and touch event listeners, and fires a setInterval that calls the internal updat() method every 20 milliseconds to keep the game running. Inside that loop the engine clears the canvas, applies the camera transform, calls your update() function, and then draws every registered Component โ€” so all your game logic and rendering happens automatically once you define function update(). The important rule is that your Display instance must be named display โ€” the engine references that variable name internally across many classes, and any other name will break it.

<!DOCTYPE html>
<html>
<head><title>My First Limn Game</title></head>
<body>
<script src="epic.js"></script>
<script>
const display = new Display();
display.start(800, 600); // width, height

display.backgroundColor("#0d0d2a"); // optional background colour

function update() {
    // Your game logic runs here every frame
}
</script>
</body>
</html>
โš ๏ธ Your Display instance must be named display โ€” the engine uses this variable name internally across Component, Camera, TileMap, and many other classes.
Method / PropertyParametersPurpose
new Display()NoneCreate the engine โ€” canvas, input, loop, camera are all set up
display.start(w, h, node)width, height, node=document.bodyCreate the canvas at that size and start the game loop
display.backgroundColor(color)CSS color stringSet the canvas background colour
display.stop()NonePause the game loop
display.fullScreen()NoneEnter fullscreen mode

2. Adding & Moving Components

A Component is any visible object in your game โ€” the player, an enemy, a coin, a wall โ€” and it needs to be registered with display.add() before Limn Engine will draw or update it each frame.
see more...

You create a Component by passing its width, height, colour, starting X position, starting Y position, and an optional type string to the constructor. The type can be "rect" (a filled rectangle), "image" (a loaded image file), or "text" (drawn text) โ€” if you leave it blank it defaults to "rect". Once created, the Component does nothing until you call display.add(player), which pushes it into the internal comm[] array so the engine draws and moves it every single frame automatically. To move a Component you set its speedX and speedY properties โ€” the engine adds those values to the Component's x and y position on every frame, so setting player.speedX = 3 means the player moves 3 pixels to the right per frame without you needing to write player.x += 3 anywhere.

// Component(width, height, color, x, y, type)
const player = new Component(50, 50, "blue", 100, 200, "rect");
display.add(player); // now the engine draws and moves it every frame

// Teleport instantly
player.x = 400;
player.y = 300;

// Move continuously โ€” engine applies this every frame
player.speedX = 3;  // moves 3px right per frame
player.speedY = 2;  // moves 2px down per frame

// Stop all movement
player.stopMove();
๐Ÿ’ก The canvas origin (0,0) is the top-left corner. X increases going right. Y increases going down.
PropertyTypePurpose
x / ynumberWorld position โ€” the top-left corner of the Component
width / heightnumberSize in pixels
speedX / speedYnumberVelocity added to x/y every frame automatically
colorstringFill colour (rect mode) or image path (image mode)
angleradiansRotation angle โ€” engine applies it on draw

The three Component types:

  1. "rect" โ€” draws a filled colour rectangle (default if omitted)
  2. "image" โ€” draws a loaded image; pass the file path as the color argument
  3. "text" โ€” draws text; use Tctxt for more control

3. Keyboard Input & deltaTime

Limn Engine fills the display.keys[] array with true while a key is held and false when it is released, letting you read any key by its numeric keyCode inside update() โ€” and multiplying by dt (deltaTime) makes your movement the same speed on every machine.
see more...

The engine attaches keydown and keyup listeners when display.start() is called, so display.keys[keyCode] is always up to date by the time your update() function runs. When you check display.keys[39] for the right arrow key, that value is true on every frame the key is physically held down and automatically switches to false the moment the key is released. Without deltaTime, setting player.speedX = 4 moves the player 4 pixels per frame โ€” which is 240px/sec at 60fps but only 200px/sec at 50fps, meaning slower machines play the game in slow motion. Multiplying by dt converts your speed into pixels per second: 250 * dt always travels 250 pixels in one real-world second regardless of frame rate, making your game fair and consistent across all devices. Note that dt is only accurate and non-zero when you use display.perform() โ€” in the default loop deltaTime is updated once per second from a separate interval, so for frame-rate-independent movement you should use perform().

function update(dt) {
    // Reset every frame so the player stops when no key is held
    player.speedX = 0;
    player.speedY = 0;

    // Arrow keys
    if (display.keys[37]) player.speedX = -250 * dt; // โ†
    if (display.keys[39]) player.speedX =  250 * dt; // โ†’
    if (display.keys[38]) player.speedY = -250 * dt; // โ†‘
    if (display.keys[40]) player.speedY =  250 * dt; // โ†“

    // WASD โ€” same thing, different codes
    if (display.keys[65]) player.speedX = -250 * dt; // A
    if (display.keys[68]) player.speedX =  250 * dt; // D
    if (display.keys[87]) player.speedY = -250 * dt; // W
    if (display.keys[83]) player.speedY =  250 * dt; // S

    // Single press โ€” reset the key so it only fires once per press
    if (display.keys[32]) {
        display.keys[32] = false; // Space โ€” fire once
        shoot();
    }
}
KeyCodeKeyCode
โ† Left Arrow37A65
โ†‘ Up Arrow38W87
โ†’ Right Arrow39D68
โ†“ Down Arrow40S83
Space32Enter13
Escape27Shift16
Z90X88

4. Boundaries with move.bound()

move.bound() prevents a Component from walking off any edge of the canvas by clamping its position to the canvas boundaries every frame โ€” one line of code that replaces four manual if-checks.
see more...

Without boundary checking, a player that walks past the right edge of the canvas will simply disappear โ€” the engine still updates and draws it, it is just off-screen and unreachable. move.bound(id) reads the canvas width and height from display.canvas.width and display.canvas.height and clamps the Component's x and y so it can never go past any of the four sides, accounting for the Component's own width and height so the whole rectangle stays visible. If you need the player to be bounded within a custom region rather than the full canvas โ€” for example locked between two walls โ€” use move.boundTo() which takes explicit left, right, top, and bottom values, and you can pass false for any side you want to leave open. Both methods modify position directly, so call them after setting speed inside update() for correct results every frame.

function update(dt) {
    if (display.keys[39]) player.speedX =  4;
    if (display.keys[37]) player.speedX = -4;
    else if (!display.keys[39]) player.speedX = 0;

    // Clamp to all four canvas edges
    move.bound(player);

    // OR clamp to custom boundaries โ€” pass false to leave a side open
    move.boundTo(player, 50, 750, 0, 580);
    // left=50, right=750, top=0, bottom=580

    // Leave top and bottom open (only clamp horizontally)
    move.boundTo(player, 0, 800, false, false);
}
MethodParametersPurpose
move.bound(id)ComponentClamp to all four canvas edges โ€” accounts for Component size
move.boundTo(id,l,r,t,b)Component, left, right, top, bottom (or false)Clamp to custom pixel boundaries per side

5. Rectangle Collision with crashWith()

crashWith(other) is Limn Engine's standard collision detection method โ€” it checks whether the bounding rectangles of two Components overlap and returns true if they do, giving you the foundation for hit detection, pickups, and obstacle collision in a single call.
see more...

The engine uses AABB (Axis-Aligned Bounding Box) collision โ€” it compares the left, right, top, and bottom edges of both Components and returns true the moment any overlap is detected. The implementation is also rotation-aware: it rotates the other Component's centre point into this Component's local angle space before doing the edge comparison, so collision still works correctly even when your player or enemy is rotated. You call it inside update() on every frame so the check runs continuously โ€” the moment the player rectangle touches the coin rectangle, the condition becomes true and you can respond to it. For round objects like balls and circular enemies, the separate enableCircleCollision() and crashWithCircle() methods give more accurate results than AABB โ€” but crashWith() is the right starting point for most beginner games.

const player = new Component(40, 40, "blue",  100, 300, "rect");
const coin   = new Component(20, 20, "gold",  400, 200, "rect");
const wall   = new Component(200, 20, "gray", 300, 350, "rect");
display.add(player);
display.add(coin);
display.add(wall);

let score = 0;

function update(dt) {
    if (display.keys[39]) player.speedX =  4;
    if (display.keys[37]) player.speedX = -4;
    else if (!display.keys[39]) player.speedX = 0;

    // Collect coin on touch
    if (player.crashWith(coin)) {
        score++;
        coin.x = Math.random() * 760;
        coin.y = Math.random() * 560;
    }

    // Stop at wall
    if (player.crashWith(wall)) {
        player.speedX = 0;
    }
}
MethodReturnsHow it works
a.crashWith(b)booleanAABB overlap check โ€” rotation-aware

6. Text with Tctxt

Tctxt is Limn Engine's rich text Component โ€” it extends the base Component class with font size, font family, text alignment, an optional background fill with padding, and a baseline setting, making it the correct tool for any score display, dialogue box, or on-screen UI label.
see more...

The base Component class has a setText() method but it gives you very limited control over appearance. Tctxt solves this by accepting eleven constructor parameters that cover every aspect of canvas text rendering โ€” and because it extends Component you register it with display.add() exactly like any other game object. The background fill draws a filled rectangle behind the text using the paddingX and paddingY values to add space around it, which is what makes HUD elements like score boxes look clean and readable against any background colour. Call scoreText.fixed() inside update() every frame to lock the text to a fixed screen position even when the camera is scrolling โ€” without this call the text scrolls with the world and falls off screen when the player moves far enough.

// Tctxt(size, font, color, x, y, align, stroke, baseline, background, padX, padY)
const scoreText = new Tctxt(
    "22px",                   // font size
    "Arial",                  // font family
    "white",                  // text colour
    20, 36,                   // x, y position on screen
    "left",                   // alignment: "left", "center", or "right"
    false,                    // stroke mode โ€” false = fill, true = outline only
    "alphabetic",             // text baseline
    "rgba(0,0,0,0.6)",        // background fill colour (null to disable)
    14, 6                     // paddingX, paddingY around the text
);
scoreText.setText("Score: 0");
display.add(scoreText);

let score = 0;
function update() {
    score++;
    scoreText.setText("Score: " + Math.floor(score / 60));

    // Keep the score box locked to the screen when the camera moves
    scoreText.fixed();
}
๐Ÿ’ก fixed() works by adding the current camera offset to the Component's stored aX / aY anchor position every frame, keeping it at the same screen coordinates no matter where the camera is. You must call it every frame โ€” not just once.
ParameterExamplePurpose
size"22px"Font size as a CSS string
font"Arial"Font family name
align"left""left", "center", or "right"
strokefalsefalse = filled text, true = outlined text only
background"rgba(0,0,0,0.6)"Background fill โ€” null disables it
padX / padY14, 6Pixel padding around the text inside the background
setText(txt)"Score: 0"Update the displayed string โ€” returns the string

7. Images with setImage()

Any Component can display an image instead of a coloured rectangle โ€” either by passing "image" as the type at construction, or by calling setImage(src) at any point to switch to image mode, with automatic fallback to a red rectangle if the file fails to load.
see more...

When you pass "image" as the type and a file path as the color argument, Limn immediately creates an Image object and starts loading the file โ€” the Component draws the image once loading completes. setImage(src) does the same thing at runtime: it changes the type to "image", starts loading the new file, and if the load fails for any reason (wrong path, network error) it automatically falls back to type = "rect" with color = "red" so you always see something on screen and can diagnose the problem. To switch back from image to a plain colour rectangle at any time, call setColor(color), which clears the image reference and restores rect rendering immediately. The Component's width and height still control the drawn size regardless of the image's natural dimensions โ€” Limn stretches or shrinks the image to fit.

// Method 1 โ€” image at construction
const player = new Component(64, 64, "img/hero.png", 100, 100, "image");
display.add(player);

// Method 2 โ€” switch to image at runtime
const enemy = new Component(40, 40, "red", 300, 200, "rect");
display.add(enemy);
enemy.setImage("img/enemy.png"); // switches to image mode safely

// Switch back to a colour rectangle
enemy.setColor("purple"); // image cleared, back to rect mode
MethodParametersBehaviour
setImage(src)file path stringLoad an image โ€” falls back to red rect on error
setColor(color)CSS color stringSwitch back to filled rect, clears the image

8. Mouse Input & clicked()

Limn Engine stores the mouse position in display.x and display.y while any mouse button or finger is pressed โ€” setting them to false on release โ€” and clicked() lets you check whether the press landed specifically on a given Component.
see more...

The engine registers both mousedown/mouseup and touchstart/touchend listeners in addEventListeners(), so display.x and display.y work identically on desktop and mobile without any extra code from you. While the mouse is pressed display.x holds the page X coordinate and display.y holds the page Y coordinate; the moment the button is released both become false, which means checking if (display.x) is a reliable test for "is the mouse currently held". clicked() goes further โ€” it checks whether those coordinates fall inside the bounding box of the specific Component you call it on, and it is rotation-aware, so clicking a rotated button still registers correctly. The standard pattern is if (display.x && btn.clicked()) โ€” the first condition confirms a press is active, the second confirms it landed on that Component.

const btn = new Component(160, 50, "#7fffb2", 320, 270, "rect");
display.add(btn);

function update() {
    // Check if mouse is pressed anywhere
    if (display.x) {
        console.log("Mouse at", display.x, display.y);
    }

    // Check if mouse pressed specifically on the button
    if (display.x && btn.clicked()) {
        btn.setColor("lime");
        console.log("Button clicked!");
    } else {
        btn.setColor("#7fffb2");
    }
}
๐Ÿ’ก Works on touchscreen too โ€” touchstart sets display.x and display.y to the finger position, and touchend resets them to false.

9. Sound with the Sound Class

The Sound class wraps the browser's Audio API to give you simple load-and-play audio with volume control, looping, pause, and automatic clone-on-overlap so the same sound file can play multiple times simultaneously without cutting itself off.
see more...

When you create new Sound("jump.wav") the engine immediately creates an Audio element, starts preloading the file, and waits for the canplaythrough event before marking it as ready. Calling play() before the file has finished loading logs a warning and does nothing, so it is safest to create all sounds before calling display.start(). The clone-on-overlap behaviour is what makes sound effects in games work correctly โ€” if a sound is already playing and you call play() again, instead of restarting from the beginning it creates a temporary copy of the sound, plays that copy, and disposes of it automatically, meaning rapid events like bullet fires all get their own audio instance. For looping background music, pass { loop: true } as the options object; for sounds that should stop and restart on each call (like a jump), the default non-loop behaviour handles that without any extra code.

// Create sounds at the top โ€” preloading starts immediately
const jumpSfx  = new Sound("audio/jump.wav");
const hitSfx   = new Sound("audio/hit.wav", { volume: 0.8 });
const bgMusic  = new Sound("audio/music.mp3", { loop: true, volume: 0.5 });

bgMusic.play(); // start background music

function update() {
    // Jump on Space โ€” reset key so it fires only once per press
    if (display.keys[32]) {
        display.keys[32] = false;
        jumpSfx.play(); // clones if already playing โ€” no cut-off
    }

    if (player.crashWith(enemy)) {
        hitSfx.play();
    }
}
MethodPurpose
play(volume?)Play the sound โ€” clones if already playing (overlap safe)
stop()Stop playback and rewind to the beginning
pause()Pause at the current position
setVolume(0โ€“1)Change the volume
setLoop(bool)Enable or disable looping
isPlaying()Returns true if currently playing

10. Game States with display.scene

display.scene is Limn Engine's scene management system โ€” every Component is registered to a scene number, and only Components whose scene number matches display.scene are drawn and updated each frame, letting you switch between a main menu, gameplay, and game-over screen with a single assignment.
see more...

When you call display.add(component, 1) the second argument is the scene number โ€” it defaults to 0 if you leave it out. Every frame the engine's internal loop checks if (component.scene == display.scene) before drawing anything, so Components assigned to scene 1 are completely invisible and inactive when display.scene is 0, and instantly become active the moment you set display.scene = 1. This approach is much more efficient than creating and destroying objects on state change โ€” all your Components exist in memory at all times but only the active scene's objects consume draw calls. The pattern is to build every screen upfront, assign each object to the right scene, start on scene 0, and then change display.scene in response to player actions โ€” a button click to start the game, a collision that triggers game over, or a menu option.

// Scene 0 โ€” main menu
const playBtn = new Component(160, 50, "#7fffb2", 320, 270, "rect");
display.add(playBtn, 0);

// Scene 1 โ€” gameplay
const player = new Component(40, 40, "cyan", 100, 100, "rect");
display.add(player, 1);

// Scene 2 โ€” game over
const gameOverText = new Tctxt("36px","Arial","red",260,280,"center");
gameOverText.setText("GAME OVER");
display.add(gameOverText, 2);

display.scene = 0; // start on the menu

let playerDead = false;

function update() {
    if (display.scene === 0) {
        if (display.x && playBtn.clicked()) {
            display.scene = 1; // start game
        }
    }

    if (display.scene === 1) {
        // gameplay logic here
        if (playerDead) display.scene = 2;
    }
}
๐Ÿ’ก Components default to scene 0 if you call display.add(obj) without a second argument โ€” so beginner games that only have one screen never need to think about scene numbers.

11. Coin Collector โ€” Complete Tutorial

This tutorial builds a complete game from scratch using only Level 1 concepts โ€” movement, collision, a score counter, boundaries, and a Tctxt HUD โ€” to show how the pieces fit together into a working, playable game.
see more...
<script src="epic.js"></script>
<script>
const display = new Display();
display.start(800, 600);
display.backgroundColor("#0d0d2a");

// Player
const player = new Component(40, 40, "cyan", 380, 280, "rect");
display.add(player);

// Five coins at random positions
const coins = [];
for (let i = 0; i < 5; i++) {
    const c = new Component(20, 20, "gold",
        50 + Math.random() * 700,
        50 + Math.random() * 500, "rect");
    display.add(c);
    coins.push(c);
}

// Score HUD โ€” fixed to screen
const scoreText = new Tctxt(
    "22px","Arial","white", 16, 36,
    "left", false, "alphabetic",
    "rgba(0,0,0,0.55)", 12, 6
);
scoreText.setText("Score: 0");
display.add(scoreText);

let score = 0;

function update(dt) {
    // WASD movement with deltaTime
    player.speedX = 0;
    player.speedY = 0;
    if (display.keys[87]) player.speedY = -250 * dt; // W
    if (display.keys[83]) player.speedY =  250 * dt; // S
    if (display.keys[65]) player.speedX = -250 * dt; // A
    if (display.keys[68]) player.speedX =  250 * dt; // D

    // Stay inside canvas
    move.bound(player);

    // Collect coins
    for (let i = 0; i < coins.length; i++) {
        if (player.crashWith(coins[i])) {
            // Respawn at a new random position
            coins[i].x = 50 + Math.random() * 700;
            coins[i].y = 50 + Math.random() * 500;
            score++;
            scoreText.setText("Score: " + score);
        }
    }

    // Keep HUD fixed to screen
    scoreText.fixed();
}
</script>

โœ… WASD to move. Walk into gold squares to collect them โ€” each coin respawns at a new position. The score counter stays fixed to the top-left corner even if a camera is added later.

12. Quick Reference

see more...
What you wantCode
Start the engineconst display = new Display(); display.start(800,600);
Add a coloured boxconst b = new Component(w,h,"red",x,y); display.add(b);
Move with keys (frame-independent)if(display.keys[39]) player.speedX = 250*dt;
Stop movingplayer.stopMove(); or player.speedX = 0;
Keep inside canvasmove.bound(player);
Detect collisionif(a.crashWith(b)) { ... }
Show score textconst t = new Tctxt("20px","Arial","white",10,30); t.setText("0");
Lock HUD to screent.fixed(); // every frame in update()
Display an imageobj.setImage("img/hero.png");
Detect mouse click on objectif(display.x && btn.clicked()) { ... }
Play a soundconst s = new Sound("sfx.wav"); s.play();
Switch game screendisplay.add(obj, 1); display.scene = 1;

Comfortable with these? Move on โ†’ Level 2: Intermediate