πŸ”΅ Level 2 β€” Intermediate L2

1. Physics & Gravity

Enabling physics on a Component tells Limn Engine to apply gravity to it automatically every frame. The engine accumulates downward speed into a gravitySpeed value that compounds over time, creating realistic freefall motion.
πŸ“– Full explanation & examples

When player.physics = true, the engine's internal move() method adds this.gravity to this.gravitySpeed on every frame, then adds the total gravitySpeed to the Component's Y position. This causes downward acceleration to build up naturally over time β€” after 10 frames of gravity 0.4, the object falls at 4px per frame and continues accelerating.

The gravity property controls the pull strength per frame. A value of 0.4 adds 0.4 to gravitySpeed every frame. The bounce property (0–1) controls energy retained on impact β€” 0 means dead stop, 1 means perfect bounce, and values like 0.6 produce realistic decaying bounces.

⚠️ You must call player.hitBottom() inside update() β€” the engine applies gravity but does not automatically stop the player at any surface.
const player = new Component(40, 56, "blue", 100, 50, "rect");
display.add(player);

player.physics = true;  // enable gravity accumulation
player.gravity  = 0.4;  // added to gravitySpeed each frame
player.bounce   = 0.2;  // 20% energy kept on floor impact

function update() {
    player.hitBottom(); // clamp to canvas floor and bounce

    // Left/right movement still works normally alongside physics
    if (display.keys[39]) player.speedX =  4;
    if (display.keys[37]) player.speedX = -4;
    else if (!display.keys[39]) player.speedX = 0;
}
PropertyTypePurpose
physicsbooleanEnable gravity accumulation in move()
gravitynumberAmount added to gravitySpeed per frame
gravitySpeednumberAccumulated downward speed β€” set negative to jump
bounce0–1Fraction of speed kept on floor impact

2. hitBottom() and move.hitObject()

hitBottom() clamps a physics Component to the canvas floor and reverses its gravitySpeed by the bounce factor. move.hitObject() does the same thing but uses another Component as the landing surface instead of the canvas edge.
πŸ“– Full explanation & examples

hitBottom() reads display.canvas.height - this.height as the floor Y position. The moment the Component's Y exceeds that, it snaps the Y back to the floor and multiplies gravitySpeed by -this.bounce β€” the negative sign flips velocity upward, and the bounce factor scales how much speed survives, producing gradually diminishing bounces until the Component settles at rest.

move.hitObject(id, otherid) works identically but uses the top edge of another Component as the floor. It reads otherid.y as the landing surface and only triggers if the two Components also overlap on the X axis (checked via crashWith), making it suitable for platforms at different heights.

You can combine both in the same update loop β€” hitBottom() catches the canvas edge as a safety net and move.hitObject() handles specific platforms.

const player = new Component(36, 48, "cyan", 100, 0, "rect");
const platform = new Component(200, 20, "#555", 200, 300, "rect");
display.add(player);
display.add(platform);

player.physics = true;
player.gravity  = 0.5;
player.bounce   = 0.05;

function update() {
    // Land on the platform
    move.hitObject(player, platform);

    // Also stop at the canvas floor (safety net)
    player.hitBottom();
}
MethodParametersPurpose
hitBottom()optional groundYClamp to canvas floor and bounce
move.hitObject(id, floor)Component, floor ComponentTreat another Component's top edge as landing surface

3. move.glideX / glideY / glideTo

The glide functions ease a Component from its current position to a target coordinate over a specified duration using a cubic ease-out curve. The movement is fast at the start and decelerates smoothly as it approaches the target, requiring no manual lerp code from you.
πŸ“– Full explanation & examples

Internally, move.glideX() records the Component's starting X and the current timestamp from performance.now(), then launches its own requestAnimationFrame loop that calculates elapsed time on every frame, divides it by the total duration to get a 0–1 progress value, feeds that through 1 - Math.pow(1 - progress, 3) to produce cubic ease-out, and applies the result to the Component's X position.

This runs independently of your game loop β€” you call it once and walk away. The Component will arrive at the target position precisely at the end of the duration regardless of frame rate.

move.glideTo() simply calls both glideX() and glideY() simultaneously with the same duration, so both axes ease in sync. Glide is ideal for UI animations, cutscenes, enemy patrol paths, and any movement that needs to feel polished rather than instant.

const box = new Component(50, 50, "#7fffb2", 0, 200, "rect");
display.add(box);

// On any keypress, glide the box to a new position
window.addEventListener("keydown", () => {
    move.glideX(box, 2000, 700);    // ease to x=700 over 2 seconds
    move.glideY(box, 1500, 400);    // ease to y=400 over 1.5 seconds

    // Or both axes together in perfect sync
    move.glideTo(box, 2000, 700, 400);
});
MethodParametersEasing
move.glideX(id, ms, x)Component, duration ms, target xCubic ease-out
move.glideY(id, ms, y)Component, duration ms, target yCubic ease-out
move.glideTo(id, ms, x, y)Component, duration ms, x, yBoth axes in sync

4. move.project β€” Projectile Motion

move.project() launches a Component as a physics projectile. You specify velocity, launch angle in degrees, and gravity value. The engine calculates the correct X and Y velocity components and handles the arc, including bouncing when the object hits the floor.
πŸ“– Full explanation & examples

The function converts the angle from degrees to radians, then uses Math.cos(angle) and Math.sin(angle) to split the velocity into horizontal and vertical components, assigning them to speedX and speedY.

It then starts its own requestAnimationFrame loop that adds the gravity value to speedY on every frame to simulate the arc. When the projectile reaches the floor (either the canvas bottom or a custom ground Y you pass as the fifth argument), it applies the Component's bounce factor and reverses the vertical velocity, continuing until the bounce speed rounds to zero and the loop stops.

Angle 0Β° launches horizontally to the right, 90Β° launches straight up, and 45Β° gives the classic arc β€” angles above 90Β° launch backward and upward.

const ball = new Component(20, 20, "orange", 100, 450, "rect");
display.add(ball);
ball.bounce = 0.5;

window.addEventListener("dblclick", () => {
    // move.project(id, velocity, angleΒ°, gravity, customGround?)
    move.project(ball, 12, 45, 0.3);
    // Launches at 45Β° with velocity 12 β€” arcs and bounces on canvas floor

    // Custom ground height β€” bounce on a platform at y=400
    move.project(ball, 12, 45, 0.3, 400);
});
ParameterTypePurpose
velocitynumberLaunch speed in pixels per frame
angledegrees0Β° = right, 90Β° = up, 45Β° = classic arc
gravitynumberAdded to speedY every frame β€” controls arc steepness
groundnumber (optional)Custom floor Y β€” defaults to display.canvas.height

5. Camera Follow

display.camera.follow(target) translates the canvas context so the view tracks a Component. This keeps the player centred on screen as they move through a world larger than the canvas, with an optional smooth lerp mode that eases the camera behind the player rather than snapping to it instantly.
πŸ“– Full explanation & examples

Internally, the camera stores an x and y offset and passes it to context.translate(-camera.x, -camera.y) at the start of every frame, shifting the entire canvas coordinate system so that the target Component appears at the centre of the screen.

With smooth = false (the default), the camera sets its position exactly to centre on the target each frame β€” perfect for lock-on behaviour. With smooth = true, the camera uses a 10% lerp: camera.x += (target.x - centreX) * 0.1, so it drifts toward the player position rather than snapping, giving the professional trailing-camera feel used in most 2D platformers.

The camera also clamps its position to camera.worldWidth and camera.worldHeight so it never shows empty space past the edges of your world.

display.camera.worldWidth  = 3000; // level is 3000px wide
display.camera.worldHeight = 1000; // level is 1000px tall

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

    // Smooth follow β€” lerps 10% of the gap each frame (recommended)
    display.camera.follow(player, true);

    // Hard follow β€” snaps to player exactly
    // display.camera.follow(player);
}

6. Camera Zoom

display.camera.setZoom(amount) scales the entire canvas context by a multiplier each frame. Values above 1 zoom in, values below 1 zoom out β€” and it must be called inside update() every frame because the context transform resets between frames.
πŸ“– Full explanation & examples

setZoom() calls display.context.scale(amount, amount) on the live canvas context, which multiplies all subsequent draw coordinates by that factor β€” a zoom of 2.0 means every pixel of your game world is drawn at twice the size, effectively doubling the apparent canvas magnification.

Because Limn's render loop calls context.save() and context.restore() every frame, the scale transform is reset at the end of each frame β€” which is why you must call setZoom() inside update() on every single frame to maintain the zoom level continuously.

⚠️ Call setZoom() every frame β€” the context scale resets at the end of each frame and will return to 1.0 if you stop calling it.
let zoomLevel = 1.0;

function update(dt) {
    display.camera.follow(player, true);

    // Zoom in with + key, out with - key
    if (display.keys[187]) zoomLevel = Math.min(zoomLevel + 0.01, 2.5); // +
    if (display.keys[189]) zoomLevel = Math.max(zoomLevel - 0.01, 0.5); // -

    // Must be called every frame to maintain the zoom
    display.camera.setZoom(zoomLevel);
}

7. Sprite & AnimatedSprite

Sprite animates a horizontal spritesheet by cycling through evenly spaced frames at a controlled speed. AnimatedSprite extends it with named animation clips so you can define separate frame ranges for idle, run, jump, attack, and die β€” switching between them by name.
πŸ“– Full explanation & examples

A spritesheet is a single image file where every animation frame is placed side by side in a horizontal row β€” frame 0 is leftmost, frame 1 is next, and so on. Sprite stores the frame width, total frame count, and a speed (frames-per-update value). Each call to updateAnimation() increments an internal timer, and when that timer reaches the speed threshold, it advances currentFrame by 1 and resets the timer β€” the update() method then uses currentFrame * frameWidth as the X source offset for drawImage, cutting out exactly the right slice of the spritesheet.

AnimatedSprite adds an animations dictionary where each entry stores a start frame, end frame, speed, and loop flag. Calling playAnimation("run") switches the active clip and resets the frame counter only if the name is different from the current clip, preventing the animation from restarting if you call it every frame. One-shot animations like jump or die set paused = true when they reach their last frame, which you can check to know when the animation finished.

// Basic Sprite β€” Sprite(src, frameW, frameH, frameCount, frameSpeed, x, y)
const explosion = new Sprite("explode.png", 64, 64, 8, 4, 300, 200);
display.add(explosion);
// updateAnimation() is called automatically inside update() each frame

// AnimatedSprite with named clips
const hero = new AnimatedSprite("hero_sheet.png", 64, 64, 200, 300);
hero.addAnimation("idle",   0,  3, 10, true);  // loop
hero.addAnimation("run",    4, 11,  5, true);  // loop
hero.addAnimation("jump",  12, 15,  4, false); // one-shot β€” pauses on last frame
hero.addAnimation("attack",16, 19,  3, false); // one-shot
display.add(hero);

let attacking = false;
function update() {
    if (display.keys[68]) hero.playAnimation("run");
    else hero.playAnimation("idle");

    if (display.keys[90] && !attacking) { // Z key
        hero.playAnimation("attack");
        attacking = true;
        setTimeout(() => attacking = false, 300);
    }

    hero.updateAnimation(); // must call every frame to advance frames
    // hero.paused is true when a one-shot animation finishes
}

8. TileMap Layers with addMap()

The TileMap system stores multiple map layouts via addMap() β€” each one is an independent 2D array registered as a numbered layer β€” and show(layer) switches the entire tilemap display to that layer, exactly like display.scene switches which Components are visible.
see more...

When you call display.tileMap(), the engine wraps your display.map into this.map[0] β€” the first layer. Calling display.tileFace.addMap(anotherArray) pushes a second map into this.map[1], a third into this.map[2], and so on, but none of them are visible yet. When you call display.tileFace.show(0), the engine clears tileComm[] completely, rebuilds it from layer 0's map array, and sets fake.scene = 0 β€” so ani() only draws tiles from layer 0 into the fake canvas. Calling display.tileFace.show(1) does the same thing for layer 1 β€” it wipes tileComm[] and rebuilds it entirely from layer 1's data, replacing layer 0 completely. This means addMap() is not a layering system β€” it is a map switching system. Think of it like levels or rooms: layer 0 is the grassland level, layer 1 is the dungeon level, layer 2 is the snow level β€” calling show(n) swaps the entire world to that map, the same way display.scene = n swaps which Components are active. This makes it perfect for level progression, room transitions, and alternate map states β€” the same tile templates are shared across all layers, so a tile ID of 2 means the same Component in every map.

display.tile = [
    new Component(64, 64, "green",   0, 0), // tile 1 = grass
    new Component(64, 64, "#333",    0, 0), // tile 2 = dungeon floor
    new Component(64, 64, "white",   0, 0), // tile 3 = snow
];

// Layer 0 β€” grassland level
display.map = [
    [1, 1, 1, 1, 1],
    [1, 0, 0, 1, 1],
    [1, 1, 1, 1, 1],
];
display.tileMap();
display.tileFace.show(0); // display layer 0 β€” grassland active

// Layer 1 β€” dungeon level
display.tileFace.addMap([
    [2, 2, 2, 2, 2],
    [2, 0, 0, 0, 2],
    [2, 2, 2, 2, 2],
]);

// Layer 2 β€” snow level
display.tileFace.addMap([
    [3, 3, 0, 3, 3],
    [3, 0, 0, 0, 3],
    [3, 3, 3, 3, 3],
]);

// Switch to dungeon when player enters a door
function update() {
    if (player.crashWith(dungeonDoor)) {
        display.tileFace.show(1); // wipes layer 0, switches to dungeon
        fake.refresh();
    }
    if (player.crashWith(snowDoor)) {
        display.tileFace.show(2); // switches to snow level
        fake.refresh();
    }
}
πŸ’‘ show(layer) and display.scene follow the same design philosophy β€” one switches the active tilemap, the other switches the active Components. Use them together for complete level transitions.
MethodParametersWhat it actually does
display.tileMap()β€”Creates TileMap β€” stores display.map as layer 0
tileFace.addMap(map2d)2D arrayRegister a new map as the next layer number
tileFace.show(layer)layer numberWipe tileComm[], rebuild from that layer, set fake.scene β€” switches the active map
tileFace.add(id,tx,ty,layer)tileId, gridX, gridY, layerEdit a tile in any layer's stored data
tileFace.remove(tx,ty,layer)gridX, gridY, layerRemove a tile from any layer's stored data

9. Tctxt β€” Styled Text UI

Tctxt is the correct class for any on-screen text in Limn Engine. It gives you font size, font family, colour, alignment, optional background fill with padding, stroke vs fill mode, and baseline setting β€” all in one Component.
πŸ“– Full explanation & examples

The base Component.setText() method can draw text but offers no control over font, alignment, or background β€” it is an internal fallback. Tctxt replaces it for any UI work: its update() method sets ctx.font, ctx.textAlign, and ctx.textBaseline from its constructor arguments before drawing, and it calls its own rect() method first to draw the background rectangle, which uses ctx.measureText() on the current text to size the background exactly to the text width plus padding on every frame.

Call scoreText.fixed() every frame inside update() when the camera is moving β€” this adds the camera offset to the anchor position so the text stays at the same screen coordinates regardless of where the camera is pointing.

const scoreText = new Tctxt(
    "22px",                    // font size
    "Arial",                   // font family
    "white",                   // text colour
    20, 40,                    // x, y screen position
    "left",                    // "left" / "center" / "right"
    false,                     // false=fill text, true=stroke/outline
    "alphabetic",              // text baseline
    "rgba(0,0,0,0.6)",         // background colour β€” null to disable
    14, 6                      // paddingX, paddingY
);
scoreText.setText("Score: 0");
display.add(scoreText);

function update() {
    scoreText.setText("Score: " + score);
    scoreText.fixed(); // lock to screen when camera moves
}

10. Accelerate & Decelerate

move.accelerate() and move.decelerate() give you vehicle-style movement where speed builds up gradually to a maximum and bleeds off smoothly to zero. This produces the satisfying weight and momentum that simple speedX = 4 assignments cannot.
πŸ“– Full explanation & examples

move.accelerate(id, accelX, accelY, maxSpeedX, maxSpeedY) adds the acceleration values to the Component's current speedX and speedY every frame it is called, then clamps the result so it cannot exceed the max speed values β€” meaning the Component gets faster and faster up to a ceiling, just like a car accelerating.

move.decelerate(id, decelX, decelY) subtracts from the speed on each call but includes an overshoot guard β€” if speedX would cross zero it is set exactly to zero rather than reversing direction, so the Component glides to a clean stop without any jittery back-and-forth.

The typical pattern is to call accelerate() while a key is held and decelerate() in the else branch β€” the Component smoothly speeds up on keydown, and drifts to a stop when the key is released.

function update(dt) {
    if (display.keys[68]) {
        move.accelerate(player, 0.6, 0, 8, 0); // accelX, accelY, maxX, maxY
    } else if (display.keys[65]) {
        move.accelerate(player, -0.6, 0, 8, 0);
    } else {
        move.decelerate(player, 0.4, 0); // decelX, decelY
    }
    move.bound(player);
}

11. Platform Game Tutorial

This tutorial puts together physics, jumping, accelerated movement, a floor platform, smooth camera follow, and a Tctxt score display into a complete playable platformer using only Level 2 concepts.
πŸ“– Full game code
<script src="epic.js"></script>
<script>
const display = new Display();
display.start(800, 400);
display.backgroundColor("#1a1a2e");
display.camera.worldWidth = 3000;

const player = new Component(36, 48, "cyan", 100, 100, "rect");
player.physics = true;
player.gravity  = 0.5;
player.bounce   = 0.05;
display.add(player);

const floor = new Component(3000, 20, "#444", 0, 380, "rect");
display.add(floor);

// A few platforms at different heights
const p1 = new Component(200, 16, "#666", 400, 300, "rect");
const p2 = new Component(150, 16, "#666", 800, 240, "rect");
display.add(p1);
display.add(p2);

const scoreUI = new Tctxt(
    "18px","Arial","white",14,28,
    "left",false,"alphabetic",
    "rgba(0,0,0,0.5)",10,4
);
scoreUI.setText("Distance: 0");
display.add(scoreUI);

function update(dt) {
    // Horizontal movement with acceleration
    if (display.keys[68] || display.keys[39]) {
        move.accelerate(player, 0.7, 0, 7, 0);
    } else if (display.keys[65] || display.keys[37]) {
        move.accelerate(player, -0.7, 0, 7, 0);
    } else {
        move.decelerate(player, 0.5, 0);
    }

    // Jump β€” only when grounded (gravitySpeed >= 0)
    if ((display.keys[87] || display.keys[38]) && player.gravitySpeed >= 0) {
        player.gravitySpeed = -11;
    }

    // Floor and platform collisions
    move.hitObject(player, floor);
    move.hitObject(player, p1);
    move.hitObject(player, p2);
    player.hitBottom();

    display.camera.follow(player, true);
    scoreUI.setText("Distance: " + Math.floor(player.x));
    scoreUI.fixed();
}
</script>
βœ… WASD or arrow keys to move, W/Up to jump. Multi-platform levels, smooth camera, live distance counter.

12. Quick Reference

πŸ“– See all shortcuts
FeatureCode
Enable physicsplayer.physics=true; player.gravity=0.4;
Floor collisionplayer.hitBottom();
Platform collisionmove.hitObject(player, floor);
Jumpif(player.gravitySpeed >= 0) player.gravitySpeed = -10;
Glide to positionmove.glideTo(obj, 2000, x, y);
Projectile launchmove.project(ball, 12, 45, 0.3);
Camera followdisplay.camera.follow(player, true);
Zoomdisplay.camera.setZoom(1.5); // every frame
AnimatedSpritehero.addAnimation("run",4,11,5,true); hero.playAnimation("run"); hero.updateAnimation();
TileMap layertileFace.addMap(arr); tileFace.show(1);
Acceleratemove.accelerate(obj,0.6,0,8,0);
Deceleratemove.decelerate(obj,0.4,0);

Ready for more? β†’ Level 3: Advanced