Skip to main content

Drawing & Layers

The engine provides a layered rendering system where you can draw shapes, text, images, and custom content. All drawing operations use world coordinates, and the engine handles the scaling and positioning automatically.

Layer System

Layers control the Z-order of your content. Lower numbers draw first (background), higher numbers draw last (foreground).

LayerTypical Usage
0Background / Terrain
1Grid lines / Floor decorations
2Objects / Units / Buildings
3UI Markers / Overlays
tip

You can use any number for a layer. They are sorted automatically at render time.

Shapes

drawRect & drawCircle

Draw basic geometric shapes. You can pass a single object or an array of objects for batch rendering.

PropertyTypeDefaultDescription
x, ynumberRequiredWorld coordinates of the center/origin.
sizenumber1Size in grid units (width/diameter).
layernumber1Rendering layer.
styleobject{}Styling options (see below).
originobject{ mode: "cell", x: 0.5, y: 0.5 }Anchor point.
rotatenumber0Rotation angle in degrees (only for drawRect).

Style Options:

  • fillStyle: Fill color (e.g., "#ff0000", "rgba(0,0,0,0.5)")
  • strokeStyle: Border color
  • lineWidth: Border width in pixels
// Draw a blue square on layer 1
engine.drawRect(
{
x: 5,
y: 5,
size: 1,
style: { fillStyle: "#0077be" },
},
1
);

// Draw a rotated rectangle (45 degrees)
engine.drawRect(
{
x: 8,
y: 5,
size: 1,
rotate: 45, // 45 degrees
style: { fillStyle: "#ff6b6b" },
},
1
);

// Draw a red circle on layer 2
engine.drawCircle(
{
x: 6,
y: 5,
size: 0.8,
style: { fillStyle: "#e63946" },
},
2
);

// Batch drawing (Array)
engine.drawRect(
[
{ x: 10, y: 10 },
{ x: 12, y: 10 },
{ x: 14, y: 10 },
],
1
);

Lines & Paths

drawLine

Draw a straight line between two points. Supports single object or array of objects.

| Property | Type | Description | | from | { x, y } | Start coordinates. | | to | { x, y } | End coordinates. | | style | object | Line style (strokeStyle, lineWidth). |

engine.drawLine({ from: { x: 0, y: 0 }, to: { x: 10, y: 10 } }, { strokeStyle: "#fb8500", lineWidth: 3 }, 1);

drawPath

Draw a continuous line through multiple points. Supports a single path (array of points) or an array of paths.

| Property | Type | Description | | items | Coords[] | Array of points { x, y }. | | style | object | Path style (strokeStyle, lineWidth). |

engine.drawPath(
[
{ x: 0, y: 0 },
{ x: 5, y: 0 },
{ x: 5, y: 5 },
],
{ strokeStyle: "#219ebc", lineWidth: 2 },
1
);

drawGridLines

Draw grid lines at specified intervals. This is useful for creating grid overlays on your map.

PropertyTypeDefaultDescription
cellSizenumberRequiredSize of each grid cell in world units.
lineWidthnumber1Width of grid lines in pixels.
strokeStylestring"black"Color of the grid lines.
layernumber0Rendering layer.
// Draw a basic grid with 1-unit cells
engine.drawGridLines(1);

// Draw a grid with custom styling
engine.drawGridLines(5, 2, "rgba(255, 255, 255, 0.3)", 0);

// Draw multiple grids at different scales
engine.drawGridLines(1, 0.5, "rgba(0, 0, 0, 0.1)", 0); // Fine grid
engine.drawGridLines(5, 1, "rgba(0, 0, 0, 0.3)", 0); // Medium grid
engine.drawGridLines(50, 2, "rgba(0, 0, 0, 0.5)", 0); // Coarse grid

Text & Images

drawText

Render text at a specific world coordinate. Supports single object or array of objects.

PropertyTypeDefaultDescription
coords{ x, y }RequiredPosition in world space.
textstringRequiredThe text content.
styleobject-Font styling options.

Style Options:

  • font: CSS font string (e.g., "12px Arial")
  • fillStyle: Text color
  • textAlign: "left", "center", "right"
  • textBaseline: "top", "middle", "bottom"
engine.drawText({ coords: { x: 5, y: 5 }, text: "Base Camp" }, { font: "14px sans-serif", fillStyle: "white" }, 3);

drawImage

Draw an image scaled to world units. Supports single object or array of objects.

PropertyTypeDescription
imgHTMLImageElementThe loaded image object.
x, ynumberWorld coordinates.
sizenumberSize in grid units (maintains aspect ratio).
rotatenumberRotation angle in degrees (0 = no rotation, positive = clockwise).
const img = await engine.images.load("/assets/tree.png");
engine.drawImage({ x: 2, y: 3, size: 1.5, img }, 2);

// Draw a rotated image (90 degrees clockwise)
const arrow = await engine.images.load("/assets/arrow.png");
engine.drawImage({ x: 5, y: 3, size: 1, img: arrow, rotate: 90 }, 2);

Advanced

Custom Drawing (addDrawFunction)

For maximum flexibility, you can register a custom drawing function that gets direct access to the canvas context.

engine.addDrawFunction((ctx, coords, config) => {
// coords = Top-left world coordinate of the view
// config = Current engine configuration

ctx.fillStyle = "purple";
ctx.fillRect(100, 100, 50, 50); // Draw in screen pixels
}, 4);

Renderer Hook (onDraw)

The onDraw callback runs after all layers have been drawn but before the debug overlays. It is useful for post-processing effects or drawing UI elements that should always be on top of the map content.

engine.onDraw = (ctx, info) => {
// info contains: { scale, width, height, coords }

// Draw a border around the entire canvas
ctx.strokeStyle = "red";
ctx.lineWidth = 5;
ctx.strokeRect(0, 0, info.width, info.height);
};

Origin & Anchoring

The origin property controls how shapes and images are positioned relative to their x, y coordinates.

  • mode: "cell" (Default): Anchors relative to the grid cell. x: 0.5, y: 0.5 centers the object in the cell.
  • mode: "self": Anchors relative to the object's own size. x: 0.5, y: 0.5 centers the object on the coordinate.

Performance (Culling)

The engine automatically skips drawing objects that are outside the current viewport (plus a small buffer). You can safely pass thousands of objects to the draw methods; only the visible ones will be rendered.

Static Caching (Pre-rendered Content)

For large static datasets (e.g., mini-maps with 100k+ items), the engine provides pre-rendering methods that cache content to an offscreen canvas. This dramatically improves performance when all items need to be visible at once.

When to Use Static Caching

ScenarioUse Static?Why
Mini-map with 100k items✅ YesAll items visible, static content
Main map with 100k items❌ NoOnly viewport visible, culling is enough
Overview map (fixed zoom)✅ YesStatic zoom, all items visible
Dynamic content (units moving)❌ NoContent changes frequently

drawStaticRect

Pre-renders rectangles to an offscreen canvas. Ideal for mini-maps. Supports rotate property for rotated rectangles.

const miniMapItems = items.map((item) => ({
x: item.x,
y: item.y,
size: 0.9,
style: { fillStyle: item.color },
rotate: item.rotation, // Optional rotation in degrees
}));

// "minimap-items" is a unique cache key
miniMap.drawStaticRect(miniMapItems, "minimap-items", 1);

drawStaticCircle

Pre-renders circles to an offscreen canvas.

const markers = items.map((item) => ({
x: item.x,
y: item.y,
size: 0.5,
style: { fillStyle: item.color },
}));

miniMap.drawStaticCircle(markers, "minimap-markers", 2);

drawStaticImage

Pre-renders images to an offscreen canvas. Useful for static terrain or decorations with fixed zoom. Supports rotate property for rotated images.

const terrainTiles = tiles.map((tile) => ({
x: tile.x,
y: tile.y,
size: 1,
img: tileImages[tile.type],
rotate: tile.rotation, // Optional rotation in degrees
}));

engine.drawStaticImage(terrainTiles, "terrain-cache", 0);

clearStaticCache

Clears pre-rendered caches when content changes.

// Clear a specific cache
engine.clearStaticCache("minimap-items");

// Clear all static caches
engine.clearStaticCache();

Cache Keys

The cache key (second parameter) identifies each pre-rendered cache. Using the same key reuses the existing cache; using a different key creates a new one.

// These use separate caches
engine.drawStaticRect(villages, "villages", 1);
engine.drawStaticRect(cities, "cities", 1);

// This reuses the "villages" cache (no re-render)
engine.drawStaticRect(villages, "villages", 1);
Memory Usage

Each static cache creates an offscreen canvas sized to fit all items. For 100k items spread across a large world, this can consume significant memory. Use static caching only when the performance benefit justifies it.

How It Works

  1. First render: All items are drawn to an offscreen canvas (OffscreenCanvas or HTMLCanvasElement)
  2. Subsequent renders: Only the visible portion is copied (blitted) to the main canvas
  3. Zoom changes: Cache is automatically rebuilt when scale changes

When to Use

The performance benefit becomes most noticeable during dragging. Each drag movement triggers a re-render, and without static caching, this means redrawing all visible items on every frame—causing noticeable lag when you have tens of thousands of items.

With static caching, dragging remains smooth because only a single drawImage call copies the pre-rendered content during each frame.

We recommend using static caching when:

  • You have 50k–100k+ items visible at once (e.g., mini-maps, overview maps)
  • Dragging/panning is enabled on that canvas

If your canvas doesn't support drag interactions, or only a small portion of items are visible at a time, the regular drawRect/drawCircle/drawImage methods with automatic culling are sufficient.

tip

Static caching is most effective when:

  • Zoom level is fixed (like a mini-map)
  • Content doesn't change frequently
  • All or most items are visible at once

For scrollable maps where only a small portion is visible, the regular drawRect/drawCircle/drawImage methods with automatic culling are more efficient.

Example: Mini-map with 100k Items

// Mini-map uses static caching (all 100k items visible)
const miniMapRects = allItems.map((item) => ({
x: item.x,
y: item.y,
size: 0.9,
style: { fillStyle: item.color },
}));
miniMap.drawStaticRect(miniMapRects, "minimap-items", 1);

// When items change, clear and redraw
function updateMiniMap() {
miniMap.clearStaticCache("minimap-items");
miniMap.clearLayer(1);
miniMap.drawStaticRect(updatedItems, "minimap-items", 1);
miniMap.render();
}

Clearing Layers

When your scene content changes dynamically (e.g., objects change color, get added or removed), you need to clear the layer before redrawing. Without clearing, new draw calls accumulate on top of existing ones.

clearLayer(layer)

Clears all draw callbacks from a specific layer.

// Clear layer 1 before redrawing
engine.clearLayer(1);
engine.drawRect(updatedRects, 1);
engine.render();

clearAll()

Clears all draw callbacks from all layers. Useful for complete scene reset.

// Reset everything
engine.clearAll();
// Redraw from scratch
engine.drawRect(background, 0);
engine.drawImage(units, 1);
engine.render();

When to Clear?

ScenarioClear Needed?Example
Camera pan/zoom❌ NoUser drags the map
Object color changes✅ YesSeat selection in cinema
Object added/removed✅ YesPlacing a tower
Object position changes✅ YesMoving a unit
Loading new level✅ YesGame level transition
tip

If your scene is static (objects don't change), you only need to call drawX() once at startup. The engine will re-render the same layer content when the camera moves.

If your scene is dynamic (objects change state), use clearLayer() + drawX() + render() pattern.

Static Scene Example (Map):

// Draw once at startup
engine.drawImage(mapTiles, 0);
engine.drawImage(buildings, 1);
engine.render();

// Camera changes only need render()
engine.onCoordsChange = () => {
engine.render(); // No clear needed, objects are the same
};

Dynamic Scene Example (Cinema Seats):

function redraw() {
engine.clearLayer(1); // Clear old seats
engine.drawRect(
seats.map((s) => ({
x: s.x,
y: s.y,
size: 0.9,
style: { fillStyle: s.selected ? "blue" : "green" },
})),
1
);
engine.render();
}

engine.onClick = (coords) => {
const seat = findSeat(coords.snapped.x, coords.snapped.y);
if (seat) {
seat.selected = !seat.selected;
redraw(); // Clear + Draw + Render
}
};

Rendering

The engine uses a passive rendering approach. It does not run a continuous loop (like requestAnimationFrame) unless you implement one. You must explicitly call render() to update the canvas when you modify the scene.

render()

Draws the current state of all layers to the canvas.

engine.drawRect({ x: 10, y: 10 });
engine.render(); // Must be called to see the rectangle

Automatic Renders: The engine automatically calls render() when:

  • The camera is panned or zoomed.
  • The viewport is resized.

For all other changes (adding shapes, changing config, loading images), you must call render() manually.