πŸ”΄ Level 4 β€” 10x L4

1. display.perform() β€” Upgrading to 60fps

display.perform() is the single most impactful call you can make in Limn Engine β€” it replaces the default setInterval game loop entirely by monkey-patching Display.prototype.start so that when you call display.start() immediately after, the engine launches the ani() function driven by requestAnimationFrame, upgrading the loop to run at up to 60fps synced to the monitor's refresh rate.
see more...

Without perform(), Limn runs its loop on setInterval(() => this.updat(), 20) β€” that fires every 20 milliseconds regardless of what the browser is doing, capping the game at roughly 50 frames per second and causing visible tearing because the loop runs out of sync with the browser's own paint cycle. What display.perform() actually does at the code level is overwrite Display.prototype.start with a new function that calls this.interval = ani() instead of the old setInterval β€” meaning it patches the prototype that start() is about to execute, so the change takes effect the moment display.start() runs on the next line. The ani() function receives a high-precision DOMHighResTimeStamp from the browser on every call via requestAnimationFrame, uses it to count how many frames ran in the last second by comparing display.timing = display.time - display.contTime against 1000ms, and when that threshold is crossed it sets display.fps = display.frame and resets the counter β€” giving you a live, accurate frames-per-second reading derived from actual browser timestamps rather than a guessed 1-second interval. At the end of every execution, ani() calls return requestAnimationFrame(ani), which asks the browser to run it again on the next screen paint β€” so the loop is always in sync with the monitor's refresh rate, whether that is 60Hz, 90Hz, or 144Hz, and deltaTime is accurate on every frame because it comes from that same live FPS reading. Additionally, perform() initialises the hidden fake canvas, activates the dual-canvas pipeline, and calls fake.start() automatically β€” so one call does everything.

const display = new Display();

// perform() patches Display.prototype.start β€” must come FIRST
display.perform();

// start() now launches ani() β†’ requestAnimationFrame instead of setInterval
display.start(800, 600);

// Static content β†’ fake canvas (only redrawn on display.once = true)
fake.add(new Component(800, 600, "#0d0d1a", 0, 0, "rect"));

// Dynamic content β†’ main display (redrawn every frame by ani())
const player = new Component(40, 40, "cyan", 400, 300, "rect");
display.add(player);

// update() now receives accurate deltaTime from live browser timestamps
function update(dt) {
    if (display.keys[68]) player.speedX =  250 * dt;
    if (display.keys[65]) player.speedX = -250 * dt;
    else if (!display.keys[68]) player.speedX = 0;
}
⚠️ display.perform() MUST be called before display.start(). It patches the prototype that start() executes β€” calling start() first launches the old setInterval loop and the patch is ignored.
Without perform()With perform()
setInterval at 20ms β€” ~50fps maxrequestAnimationFrame β€” 60–144fps
Loop out of sync with screen paintSynced to monitor refresh rate
deltaTime from a separate 1-second interval β€” staledeltaTime from live frame timestamp β€” accurate
Single canvas β€” all objects redrawn every frameDual canvas β€” static cached, dynamic on top
No fake canvasfake canvas auto-initialised by perform()

2. The fake Canvas & display.once

The fake canvas is a hidden offscreen Display that holds all static content β€” tiles and fake.add() objects β€” and display.once is the flag that controls whether ani() redraws it this frame or skips it, making a 2000-tile world cost the same render time as a 1-tile world on all frames after the initial cache build.
see more...

When perform() runs it creates the fake Display instance, calls fake.start() which injects the fake canvas into the DOM but hides it with display:none, so it exists purely as an offscreen pixel buffer that you draw into but never see directly. Every frame, ani() checks if(display.once) at the top of its render cycle β€” if true it clears the fake canvas, applies the fake camera translation, and iterates through both tileComm[] (the tiles registered by tileFace.show()) and commp[] (the Components registered via fake.add()), drawing each one into the offscreen buffer, then sets display.once = false after 2 frames so every future frame skips the entire fake canvas redraw. The main canvas then calls display.context.drawImage(fake.canvas, 0, 0) β€” a single draw call that stamps the entire cached world onto the visible canvas regardless of how many objects are in it β€” before drawing the dynamic comm[] Components on top. This means 500 tiles cost the same as 1 tile on every frame after the first two, because those 500 draw calls are compressed into one cached bitmap. To force a redraw β€” after tileFace.add(), tileFace.remove(), or any other change to the static world β€” call fake.refresh(), which simply sets display.once = true and triggers exactly one new redraw cycle before the cache locks again.

// After perform() + start(), the ani() order of operations every frame is:
// 1. if(display.once) β†’ clear fake canvas, draw tileComm[] + commp[] into it
// 2. display.once = false after frame 2 β†’ skip fake redraw from now on
// 3. display.context.drawImage(fake.canvas, 0, 0) β€” one call for the whole world
// 4. update(display.deltaTime) β€” your game logic
// 5. draw comm[] dynamic objects (with viewport culling)
// 6. requestAnimationFrame(ani) β†’ schedule next frame

// Force a fake canvas redraw after any static change
display.tileFace.add(1, 5, 3);  // edit a tile
// fake.refresh() is already called inside add() automatically

// Or trigger it manually after fake.add() changes
display.once = true;
display.once = truedisplay.once = false
Fake canvas cleared and redrawn from tileComm[] + commp[]Fake canvas redraw skipped entirely
First 2 frames and after fake.refresh()Every frame after that β€” the majority of the game
Expensive β€” draws every tile one by oneFree β€” one drawImage covers the whole world

3. Syncing Cameras

In perform() mode both canvases have completely independent cameras β€” display.camera for the foreground and fake.camera for the background β€” and you must copy the display camera's position to the fake camera every frame inside update(), or the background will stay frozen at the origin while the foreground scrolls.
see more...

The ani() function applies fake.context.translate(-fake.camera.x, -fake.camera.y) when drawing the fake canvas and display.context.translate(-display.camera.x, -display.camera.y) when drawing the main canvas β€” these are separate transform operations on separate 2D contexts, so neither camera knows about the other. If you only call display.camera.follow(player, true) but never update fake.camera, the foreground (player, enemies) scrolls with the camera but the background (tiles, static objects) stays fixed at world origin, producing an obvious disconnected sliding effect. The fix is one line after your camera.follow() call: fake.camera.x = display.camera.x; fake.camera.y = display.camera.y; β€” this copies the already-updated camera position to the fake camera so both canvases translate by the same amount and the world aligns perfectly. For camera shake, pass opposite values to both cameras for a realistic counter-shake effect β€” the foreground lurches one way and the background lurches the other, which reads as a unified world shake rather than only the top layer moving.

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

    // Then sync fake camera to match β€” AFTER follow() so you copy updated values
    fake.camera.x = display.camera.x;
    fake.camera.y = display.camera.y;

    // Shake both cameras β€” opposite directions for realistic impact feel
    if (explosion) {
        display.camera.shake(10, 8);
        fake.camera.shake(-10, -8); // opposite = world shakes as one unit
        display.camera.shakeRotation(0.07);
    }
}
⚠️ Always sync fake.camera after display.camera.follow() β€” copy the post-follow values, not the pre-follow values.

4. deltaTime Movement

Using dt (deltaTime) as a multiplier on all speed values makes your game frame-rate independent β€” instead of moving N pixels per frame, your objects move N pixels per second, so the game plays identically on a 30fps machine and a 144fps monitor.
see more...

In perform() mode, display.deltaTime is calculated as 1 / display.fps where display.fps is measured from live browser timestamps updated every second β€” so at 60fps deltaTime is approximately 0.0167, at 30fps it is approximately 0.033, and at 144fps it is approximately 0.0069. Multiplying a speed constant by deltaTime converts it from pixels-per-frame to pixels-per-second: 250 * dt moves 250 pixels in one real second at any frame rate, because slower machines get a larger deltaTime that compensates for their fewer frames. The engine passes deltaTime as the first argument to your update(dt) function automatically in perform() mode, so you just accept it in the function signature and use it β€” no setup required. Define all speeds as pixels-per-second constants at the top of your script for readability and easy tuning β€” const PLAYER_SPEED = 280 is much clearer than a raw number buried in movement code.

const PLAYER_SPEED  = 280; // px/sec
const BULLET_SPEED  = 600; // px/sec
const ENEMY_SPEED   = 120; // px/sec

function update(dt) {
    // Frame-independent movement β€” works at any fps
    player.speedX = 0; player.speedY = 0;
    if (display.keys[87]) player.speedY = -PLAYER_SPEED * dt;
    if (display.keys[83]) player.speedY =  PLAYER_SPEED * dt;
    if (display.keys[65]) player.speedX = -PLAYER_SPEED * dt;
    if (display.keys[68]) player.speedX =  PLAYER_SPEED * dt;
}

// WITHOUT deltaTime β€” frame-dependent (BAD):
// player.speedX = 4; // 240px/s at 60fps, 120px/s at 30fps

// WITH deltaTime β€” frame-independent (GOOD):
// player.speedX = 280 * dt; // always 280px/s at any fps
dt at 60fpsdt at 30fps280 * dt at 60fps280 * dt at 30fps
~0.0167~0.033~4.67 px/frame~9.24 px/frame
Both cases travel 280 pixels per second βœ…

5. clearMargin Optimisation

display.clearMargin is a two-element array that tells the engine how large an area to clear with clearRect at the start of each frame β€” the default is [widthΒ², heightΒ²] which is safe for any rotation or zoom but wasteful for most games that do not rotate the canvas.
see more...

Internally, display.clear() calls ctx.clearRect(0, 0, this.clearMargin[0], this.clearMargin[1]) β€” that rectangle needs to be large enough to erase anything that was drawn in the previous frame, accounting for camera translation and any rotation. When the canvas is rotated, the corners of the visible area can extend beyond the canvas dimensions in world space, which is why the default [width*width, height*height] is set conservatively large β€” it guarantees nothing is left behind even at extreme rotations. However, if your game does not rotate the canvas (most games do not use shakeRotation() constantly), clearing a 640,000-pixel wide area when your canvas is only 800px wide is unnecessary work. Setting display.clearMargin = [800, 600] to match your canvas size, or [worldWidth, worldHeight] for a scrolling world, is a simple one-line optimisation that reduces the per-frame clear operation to the exact area you actually need.

const display = new Display();
display.perform();
display.start(800, 600);

// Default after start() β€” conservatively large for rotation safety
// display.clearMargin = [640000, 360000]  ← wasteful for most games

// Optimised for a fixed-camera game with no canvas rotation
display.clearMargin = [800, 600];

// Optimised for a scrolling world
display.clearMargin = [3000, 2000]; // match your world dimensions

// Keep the large default only if you use shakeRotation heavily

6. Viewport Culling with TCJSgameGameArea

In perform() mode, Limn Engine automatically skips drawing any Component that is outside the visible viewport using a collision check against TCJSgameGameArea β€” a large invisible Component that covers the canvas area β€” so off-screen enemies and objects cost zero draw calls.
see more...

When perform() is active, the ani() loop wraps each dynamic Component's draw call in if(TCJSgameGameArea.crashWith(component.x)) β€” only Components that overlap with TCJSgameGameArea get drawn. TCJSgameGameArea is created at the canvas dimensions plus 100px on each side and positioned at the camera's current origin, so it always covers slightly more than the visible screen as a buffer zone. This means a game with 200 enemies spread across a large world only draws the 8 that are currently on screen β€” the other 192 are moved and checked for logic in your own update loop, but their draw calls are skipped by the engine automatically. You do not need to configure anything β€” this culling is built into the perform() pipeline. The only thing to be aware of is that very large Components (like a 2000px background rectangle) should go on the fake canvas via fake.add() rather than the main canvas, so they are cached rather than culled.

// Culling is automatic in perform() mode β€” nothing to configure
// These 200 enemies all exist and move every frame
// but only the ones on-screen are drawn:
const enemies = [];
for (let i = 0; i < 200; i++) {
    const e = new Component(32, 32, "red",
        Math.random() * 3000,
        Math.random() * 2000, "rect");
    display.add(e);
    enemies.push(e);
}

// TCJSgameGameArea does this check for every component:
// if (TCJSgameGameArea.crashWith(component)) β†’ draw it
// else β†’ skip draw call this frame

7. Custom Particle System from Scratch

When the built-in ParticleSystem is not specialised enough for your use case, you can draw particles directly onto display.context inside update() β€” bypassing the Component system entirely for maximum performance and pixel-level control.
see more...

Adding hundreds of Particles to comm[] means the engine iterates that array each frame β€” a pool-based custom system that draws directly to the context skips the array overhead entirely. The pattern is a class with a pool array, an emit() method that pushes plain objects (not Components) onto it, and an update(ctx) method that loops the pool, applies physics, draws each particle with ctx.arc() or ctx.fillRect(), and splices dead ones out. Because you are drawing directly to the same canvas context that Limn uses, your particles appear in the correct render order as long as you call trail.update(display.context) inside your update() function β€” after the engine has applied the camera transform but before context.restore(). This approach can handle thousands of particles at 60fps where the Component-based system would slow to a crawl.

class TrailSystem {
    constructor() { this.pool = []; }

    emit(x, y, color = "#7fffb2") {
        this.pool.push({ x, y, color, alpha: 1, size: 5, vx: (Math.random()-0.5)*2, vy:-1 });
    }

    update(ctx) {
        for (let i = this.pool.length - 1; i >= 0; i--) {
            const p = this.pool[i];
            p.alpha -= 0.035;
            p.size  -= 0.1;
            p.x += p.vx;
            p.y += p.vy;
            if (p.alpha <= 0 || p.size <= 0) { this.pool.splice(i, 1); continue; }
            ctx.save();
            ctx.globalAlpha = p.alpha;
            ctx.fillStyle = p.color;
            ctx.beginPath();
            ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
            ctx.fill();
            ctx.restore();
        }
    }
}

const trail = new TrailSystem();

function update(dt) {
    trail.emit(player.x + 20, player.y + 20);
    trail.update(display.context); // draws into the same context Limn uses
}

8. Extending the Engine

Limn Engine is plain unminified JavaScript, which means you can add methods to any class via prototype extension β€” adding a health system, a hitbox visualiser, or a custom physics layer to every Component in your game with zero modification to epic.js itself.
see more...

JavaScript prototype extension works by adding a function to ClassName.prototype after the class has been defined β€” because epic.js defines Component as a regular ES6 class, any method you attach to Component.prototype in your own script (loaded after epic.js) is immediately available on every existing and future Component instance. This is the cleanest way to add game-specific behaviour β€” it reads exactly like a built-in engine method, it survives engine version updates since you never touch epic.js, and it lets you keep all your game logic in a separate file. You can also patch existing engine methods by saving the original and calling it inside a wrapper β€” the pattern is called "method chaining via prototype" and is safe as long as you save the original reference before overwriting it.

// Add a full health system to every Component
Component.prototype.setHealth = function(max) {
    this.hp = max;
    this.maxHp = max;
    this.invincible = 0; // invincibility frames counter
};

Component.prototype.damage = function(amount) {
    if (this.invincible > 0) return false; // immune
    this.hp = Math.max(0, this.hp - amount);
    this.invincible = 30; // 30 frames of invincibility
    return this.hp === 0;  // returns true if dead
};

Component.prototype.tickInvincible = function() {
    if (this.invincible > 0) this.invincible--;
};

Component.prototype.drawHealthBar = function(ctx) {
    const pct = this.hp / this.maxHp;
    const barW = this.width;
    ctx.fillStyle = "#333";
    ctx.fillRect(this.x, this.y - 10, barW, 5);
    ctx.fillStyle = pct > 0.5 ? "#00cc44" : pct > 0.25 ? "#ffaa00" : "#cc0000";
    ctx.fillRect(this.x, this.y - 10, barW * pct, 5);
};

// Usage
player.setHealth(100);
enemy.setHealth(40);

function update() {
    player.tickInvincible();
    if (player.crashWith(enemy)) {
        const died = enemy.damage(25);
        if (died) enemy.destroy();
    }
    player.drawHealthBar(display.context);
    enemy.drawHealthBar(display.context);
}

9. SoundManager

SoundManager is Limn's full audio pipeline β€” it manages a named dictionary of preloaded sounds, tracks current background music, applies separate volume levels for SFX and music scaled by a master volume, and provides mute/unmute toggling, all accessible via the shortcut move.sound.*.
see more...

When you call soundManager.load("jump", "audio/jump.wav") it creates a new Sound instance and stores it in the internal sounds dictionary under the key "jump" β€” so soundManager.play("jump") can look it up, determine the correct volume (sfx for keys not prefixed with "music_", music volume for those that are), multiply by masterVolume, and call sound.play(finalVolume) in one consistent operation. playMusic(name) stops whatever music was previously playing by calling currentMusic.stop(), then retrieves and plays the new music sound β€” giving you seamless music transitions with one call. The move.sound object is a thin wrapper that checks if(window.soundManager) before each call, so you can use the shorthand syntax without worrying about null reference errors if SoundManager was not initialised. The best practice is to create and load all sounds before calling display.start() so they preload during any loading screen or intro, then use move.sound.* in gameplay for brevity.

// Create globally before display.start() for early preloading
window.soundManager = new SoundManager();

soundManager.load("jump",     "audio/jump.wav");
soundManager.load("shoot",    "audio/shoot.wav", { volume: 0.7 });
soundManager.load("explode",  "audio/explode.wav");
soundManager.load("music_bg", "audio/background.mp3", { loop: true });

soundManager.masterVolume = 0.8;
soundManager.sfxVolume    = 1.0;
soundManager.musicVolume  = 0.5;

soundManager.playMusic("music_bg");

function update() {
    if (display.keys[32]) {
        display.keys[32] = false;
        move.sound.play("shoot"); // uses move.sound shorthand
    }
    if (playerDied) {
        move.sound.stopMusic();
        move.sound.play("explode");
    }
}
MethodPurpose
soundManager.load(name,src,opts)Preload a sound into the dictionary
soundManager.play(name)Play β€” auto sfx/music volume + masterVolume
soundManager.playMusic(name)Stop current music, start new track
soundManager.stopMusic()Stop current background music
soundManager.mute()Silence all audio
soundManager.unmute()Restore audio
masterVolume / sfxVolume / musicVolume0–1 multipliers

10. AnimatedSprite Deep-dive

AnimatedSprite extends Sprite with a named animation clip system β€” each clip stores its own start frame, end frame, speed, and loop flag, and playAnimation(name) switches between them without restarting the current clip if the name has not changed, enabling smooth state-machine-style character animation.
see more...

Under the hood, addAnimation() stores each clip as an object in this.animations[name] with its start, end, speed, loop, and internal timer β€” the updateAnimation() override reads from this.animations[this.currentAnim] rather than the base Sprite's global frameCount, so each clip plays its own range of frames at its own speed. playAnimation(name) checks if (this.currentAnim === name) return as its first line β€” this is the critical guard that makes calling it every frame safe, because without that check calling hero.playAnimation("run") 60 times per second would reset the frame counter on every frame and the animation would be stuck on frame 0 permanently. When a non-looping animation finishes it sets this.paused = true and holds on the last frame β€” your code can read hero.paused to detect completion and transition to the next state, for example going from "attack" back to "idle" when the attack animation finishes.

const hero = new AnimatedSprite("hero_sheet.png", 64, 64, 200, 300);

// addAnimation(name, startFrame, endFrame, speed, loop)
hero.addAnimation("idle",   0,  3, 10, true);  // frames 0–3, loops
hero.addAnimation("run",    4, 11,  5, true);  // frames 4–11, loops
hero.addAnimation("jump",  12, 15,  4, false); // frames 12–15, one-shot
hero.addAnimation("attack",16, 19,  3, false); // frames 16–19, one-shot
hero.addAnimation("die",   20, 27,  6, false); // frames 20–27, one-shot
display.add(hero);

let state = "idle";
function update() {
    const moving = display.keys[68] || display.keys[65];
    const onGround = hero.gravitySpeed >= 0;

    if (state === "idle" && moving)        state = "run";
    if (state === "run"  && !moving)       state = "idle";
    if (display.keys[87] && onGround)      state = "jump";
    if (display.keys[90] && state !== "attack") state = "attack";

    // playAnimation only resets if state changed β€” safe to call every frame
    hero.playAnimation(state);

    // Transition back from one-shot animations
    if (hero.paused && (state === "attack" || state === "jump")) {
        hero.paused = false;
        state = moving ? "run" : "idle";
    }

    hero.updateAnimation(); // advance the frame counter
}

11. Performance Checklist

Everything a 10x Limn developer verifies before shipping β€” covering the loop, the two canvases, object lifecycle, and audio.
see more...
CheckWhy it matters
βœ… display.perform() before start()requestAnimationFrame loop, accurate deltaTime, fake canvas activated
βœ… Static objects on fake, dynamic on displayStatic world cached in one bitmap β€” zero per-frame re-draw cost
βœ… Sync fake.camera after camera.follow()Background and foreground stay visually aligned when scrolling
βœ… All speeds multiplied by dtIdentical game speed at 30fps, 60fps, and 144fps
βœ… display.clearMargin set to canvas or world sizeAvoid clearing a 640,000px area when 800px is enough
βœ… ps.update() called every frameDead particles removed from comm[] β€” prevents unbounded memory growth
βœ… destroy() on off-screen bullets and killed enemiesRemoves from comm[] β€” reduces loop iteration count permanently
βœ… hide() for temporarily invisible objectsSkips draw call without destroying β€” use for respawning objects
βœ… Scenes used for menus and game-overNo create/destroy cost on state change β€” just flip display.scene
βœ… All sounds preloaded before display.start()No mid-game stutter on first play of an unloaded sound
βœ… Reverse iteration when splicing arrays mid-loopForward iteration + splice skips elements β€” reverse is safe
βœ… Large background rects on fake, not displayAvoids TCJSgameGameArea culling incorrectly excluding large rects

12. Open-World Demo

A complete 10x example combining perform(), layered tilemap on fake canvas, camera sync, deltaTime movement, custom particle trail, and fixed HUD β€” all patterns working together in a scrolling 3000Γ—2000 world.
see more...
<script src="epic.js"></script>
<script>
const display = new Display();
display.perform(); // requestAnimationFrame, dual canvas, accurate deltaTime
display.start(800, 600);
display.backgroundColor("#0a0a10");
display.camera.worldWidth  = 3000;
display.camera.worldHeight = 2000;
display.clearMargin = [3000, 2000]; // match world size

// ── FAKE CANVAS β€” static world (two layers) ──────────────────────
display.tile = [
    new Component(64, 64, "#1a3a1a", 0, 0), // 1 = grass
    new Component(64, 64, "#2244aa", 0, 0), // 2 = water
    new Component(64, 64, "#555555", 0, 0), // 3 = stone
    new Component(64, 64, "#228B22", 0, 0), // 4 = tree decoration
];
display.map = [
    [1,1,1,2,2,1,1,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
    [1,1,1,2,2,1,1,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
    [1,1,1,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
    [1,1,1,1,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
];
display.tileMap();
display.tileFace.show(0); // layer 0 = ground

// Layer 1 β€” decoration trees on top
display.tileFace.addMap([
    [0,4,0,0,0,4,0,0,0,4,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [4,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,4,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
]);
display.tileFace.show(1); // layer 1 = decorations composited on top

// ── DISPLAY CANVAS β€” dynamic objects ─────────────────────────────
const player = new Component(36, 36, "cyan", 400, 200, "rect");
display.add(player);

const ps = new ParticleSystem(display);

const hud = new Tctxt("14px","Arial","white",12,20,
    "left",false,"alphabetic","rgba(0,0,0,0.6)",8,4);
hud.setText("WASD to explore");
display.add(hud);

const SPEED = 260;

function update(dt) {
    ps.update();

    player.speedX = 0; player.speedY = 0;
    if (display.keys[87]) player.speedY = -SPEED * dt;
    if (display.keys[83]) player.speedY =  SPEED * dt;
    if (display.keys[65]) player.speedX = -SPEED * dt;
    if (display.keys[68]) player.speedX =  SPEED * dt;

    // Particle trail
    move.particles.sparkle(ps, player.x + 18, player.y + 18);

    // Camera + sync
    display.camera.follow(player, true);
    fake.camera.x = display.camera.x;
    fake.camera.y = display.camera.y;

    // HUD
    hud.setText("X:" + Math.floor(player.x) + "  Y:" + Math.floor(player.y));
    hud.fixed();
}
</script>

βœ… WASD moves through a 3000Γ—2000 two-layer tiled world. Sparkle trail follows the player. HUD shows live coordinates. Camera clamps to world bounds. All static content cached in fake canvas.

13. Quick Reference

see more...
FeatureCode
60fps loopdisplay.perform(); // before start()
Static contentfake.add(obj); fake.tileMap(); tileFace.show(n);
Force cache refreshfake.refresh(); // or display.once = true;
Tilemap layerstileFace.addMap(arr); tileFace.show(1); tileFace.show(2);
Sync camerasfake.camera.x = display.camera.x; // after follow()
deltaTimefunction update(dt) { speed = 280 * dt; }
clearMargindisplay.clearMargin = [800, 600];
Extend engineComponent.prototype.myFn = function() { ... }
SoundManagerwindow.soundManager = new SoundManager(); move.sound.play("jump");
AnimatedSpritehero.addAnimation("run",4,11,5,true); hero.updateAnimation();
destroyobj.destroy() β€” removes from comm[] and commp[] permanently
Culling (auto)TCJSgameGameArea.crashWith() skips off-screen draw calls automatically

You've reached the top. β†’ Full API Reference