> ## Documentation Index
> Fetch the complete documentation index at: https://docs.spurdoverse.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Render

> Draw lines, shapes, text, and images on the screen overlay.

```ts theme={null}
import Render from "@native/render";
```

The `Render` module draws primitives on the screen overlay. Everything is drawn on top of the game in screen-space pixel coordinates.

<Warning>
  **All draw calls must happen inside the `on("render", ...)` callback.**
  Drawing from `"tick"`, timers, or other events will cause flickering or
  invisible output - the overlay only accepts commands during the render
  frame. See [Events → render](/api/globals#render) for details.
</Warning>

<Note>
  **Pro tip:** All position arguments accept both [Vector2](/api/types/vector2) instances and plain `{ x, y }` objects.
  All color arguments accept [Color](/api/types/color) instances and packed hex integers (e.g. `0xFF0000FF`). If a color is omitted, white is used.
</Note>

## Overview

<CardGroup cols={2}>
  <Card title="Screen size" icon="display" href="#screen-size">
    Get the current overlay dimensions.
  </Card>

  <Card title="Rectangles & Circles" icon="shapes" href="#rectangles--circles">
    Draw bounding boxes, health bars, and FOV rings.
  </Card>

  <Card title="Lines & Polygons" icon="draw-polygon" href="#lines--polygons">
    Connect points, draw custom radar shapes and crosshairs.
  </Card>

  <Card title="Text" icon="font" href="#text">
    Render custom strings and player names.
  </Card>

  <Card title="Images" icon="image" href="#images">
    Draw loaded textures with optional rounding.
  </Card>
</CardGroup>

***

## Screen size

### Render.getScreenSize

<br />

```ts theme={null}
Render.getScreenSize(): Vector2
```

Returns the current overlay dimensions in pixels as a
[Vector2](/api/types/vector2) (`x` = width, `y` = height).

Call this once per frame - the size can change if the user resizes the window
or switches resolution.

```ts theme={null}
on("render", () => {
    const { x: screenWidth, y: screenHeight } = Render.getScreenSize();
    const center = { x: screenWidth / 2, y: screenHeight / 2 };
    Render.circleFilled(center, 3, Color.white());
});
```

***

## Rectangles & Circles

Instead of listing separate functions for outlines and filled shapes, we group them here. The parameters are nearly identical, with the exception of `thickness`.

### Rectangles

```ts theme={null}
Render.rect(min: Vector2, max: Vector2, color?: Color, thickness?: number, rounding?: number): void
Render.rectFilled(min: Vector2, max: Vector2, color?: Color, rounding?: number): void
```

<ParamField path="min" type="Vector2" required>
  Top-left corner of the rectangle in screen coordinates.
</ParamField>

<ParamField path="max" type="Vector2" required>
  Bottom-right corner of the rectangle in screen coordinates.
</ParamField>

<ParamField path="color" type="Color" default="Color.white()">
  Outline or fill color.
</ParamField>

<ParamField path="thickness" type="number" default="1.0">
  Border width in pixels. **(Ignored in `rectFilled`)**
</ParamField>

<ParamField path="rounding" type="number" default="0.0">
  Corner radius in pixels. Use for UI backgrounds or modern-looking ESP boxes.
</ParamField>

<Tabs>
  <Tab title="Box ESP (Outline)">
    ```ts theme={null}
    import Entities from "@native/entities";

    function getBoundingBox(entity) {
        const origin = entity.m_pGameSceneNode.m_vecAbsOrigin;
        const collision = entity.m_pCollision;
        if (!collision) return null;

        const mins = collision.m_vecMins;
        const maxs = collision.m_vecMaxs;

        let screenMins = new Vector2(Infinity, Infinity);
        let screenMaxs = new Vector2(-Infinity, -Infinity);

        for (let i = 0; i < 8; i++) {
            const world = new Vector(
                origin.x + (i & 1 ? maxs.x : mins.x),
                origin.y + (i & 2 ? maxs.y : mins.y),
                origin.z + (i & 4 ? maxs.z : mins.z),
            );
            const { success, screen } = Math.worldToScreen(world);
            if (!success) return null;

            screenMins.x = Math.min(screenMins.x, screen.x);
            screenMins.y = Math.min(screenMins.y, screen.y);
            screenMaxs.x = Math.max(screenMaxs.x, screen.x);
            screenMaxs.y = Math.max(screenMaxs.y, screen.y);
        }

        return { mins: screenMins, maxs: screenMaxs };
    }

    on("render", () => {
        for (const enemy of Entities.getPlayers({ skipLocal: true })) {
            if (enemy.m_iHealth <= 0) continue;

            const box = getBoundingBox(enemy);
            if (!box) continue;

            Render.rect(box.mins, box.maxs, Color.red(), 1.5);
        }
    });
    ```
  </Tab>

  <Tab title="Health Bar (Filled)">
    ```ts theme={null}
    // Assuming screen position (top-left of the bar) is already calculated
    const barWidth = 60;
    const barHeight = 4;
    const fillWidth = (enemy.m_iHealth / 100) * barWidth;

    const barMin = screen;
    const barMax = new Vector2(screen.x + barWidth, screen.y + barHeight);

    // 1. Draw dark background
    Render.rectFilled(barMin, barMax, new Color(0, 0, 0, 150));

    // 2. Draw green fill on top
    const fillMax = new Vector2(screen.x + fillWidth, screen.y + barHeight);
    Render.rectFilled(barMin, fillMax, Color.green());
    ```
  </Tab>
</Tabs>

***

### Circles

```ts theme={null}
Render.circle(center: Vector2, radius: number, color?: Color, thickness?: number): void
Render.circleFilled(center: Vector2, radius: number, color?: Color): void
```

<ParamField path="center" type="Vector2" required>
  The center point of the circle on the screen.
</ParamField>

<ParamField path="radius" type="number" required>
  The radius in pixels.
</ParamField>

<ParamField path="color" type="Color" default="Color.white()">
  Outline or fill color.
</ParamField>

<ParamField path="thickness" type="number" default="1.0">
  Line width for the outline. **(Ignored in `circleFilled`)**
</ParamField>

<CodeGroup>
  ```ts FOV Ring (Outline) theme={null}
  on("render", () => {
      const { x: screenWidth, y: screenHeight } = Render.getScreenSize();
      const center = { x: screenWidth / 2, y: screenHeight / 2 };
      const fovRadius = 150;

      Render.circle(center, fovRadius, Color.white().withAlpha(100), 1);
  });
  ```

  ```ts Head Dot (Filled) theme={null}
  import Entities from "@native/entities";

  on("render", () => {
      for (const enemy of Entities.getPlayers({ skipLocal: true })) {
          if (enemy.m_iHealth <= 0) continue;

          const pos = enemy.m_pGameSceneNode.m_vecAbsOrigin;
          const head = new Vector(pos.x, pos.y, pos.z + 72);
          const { success, screen } = Math.worldToScreen(head);
          if (success) {
              Render.circleFilled(screen, 4, Color.red());
          }
      }
  });
  ```
</CodeGroup>

***

## Lines & Polygons

<AccordionGroup>
  <Accordion title="Render.line" icon="minus">
    Draws a single straight line between two points.

    ```ts theme={null}
    Render.line(start: Vector2, end: Vector2, color?: Color, thickness?: number): void
    ```

    <ParamField path="start" type="Vector2" required>Starting point.</ParamField>
    <ParamField path="end" type="Vector2" required>Ending point.</ParamField>

    **Example:** Draw a snapline from the bottom center of the screen to an enemy.

    ```ts theme={null}
    import Entities from "@native/entities";

    on("render", () => {
        const { x: screenWidth, y: screenHeight } = Render.getScreenSize();
        const bottomCenter = { x: screenWidth / 2, y: screenHeight };

        for (const enemy of Entities.getPlayers({ skipLocal: true })) {
            if (enemy.m_iHealth <= 0) continue;

            const { success, screen } = Math.worldToScreen(enemy.m_pGameSceneNode.m_vecAbsOrigin);
            if (success) {
                Render.line(bottomCenter, screen, Color.yellow(), 1);
            }
        }
    });
    ```
  </Accordion>

  <Accordion title="Render.triangle" icon="play">
    Draws a triangle outline using three specific vertices.

    ```ts theme={null}
    Render.triangle(p1: Vector2, p2: Vector2, p3: Vector2, color?: Color, thickness?: number): void
    ```

    **Example:** Draw a directional arrow indicating where an off-screen enemy is.

    ```ts theme={null}
    const tip = center.add({ x: 0, y: -15 });
    const left = center.add({ x: -8, y: 5 });
    const right = center.add({ x: 8, y: 5 });

    Render.triangle(tip, left, right, Color.white(), 1.5);
    ```
  </Accordion>

  <Accordion title="Render.polyline" icon="draw-polygon">
    Draws a series of connected line segments through an array of points. Best used for custom complex shapes like radar blips or star polygons.

    ```ts theme={null}
    Render.polyline(points: Vector2[], color?: Color, thickness?: number, closed?: boolean): void
    ```

    <ParamField path="points" type="Vector2[]" required>Array of vertices.</ParamField>
    <ParamField path="closed" type="boolean" default="false">If true, automatically draws a line from the last point back to the first point.</ParamField>

    **Example:** Draw a diamond shape.

    ```ts theme={null}
    const points = [
        center.add({ x: 0, y: -20 }),
        center.add({ x: 15, y: 0 }),
        center.add({ x: 0, y: 20 }),
        center.add({ x: -15, y: 0 }),
    ];
    // 'closed: true' completes the diamond automatically
    Render.polyline(points, Color.white(), 1, true);
    ```
  </Accordion>
</AccordionGroup>

***

## Text

### Render.text

<br />

```ts theme={null}
Render.text(pos: Vector2, text: string, color?: Color, fontSize?: number, fontName?: string): void
```

Draws a text string on the screen. The position defines the **top-left** corner of the rendered text block.

<ParamField path="pos" type="Vector2" required>
  Top-left coordinate for the text.
</ParamField>

<ParamField path="text" type="string" required>
  The string to render.
</ParamField>

<ParamField path="color" type="Color" default="Color.white()">
  Text color.
</ParamField>

<ParamField path="fontSize" type="number" default="14">
  Font size in pixels.
</ParamField>

<ParamField path="fontName" type="string" default="&#x22;segoe&#x22;">
  Font name. See [available fonts](#available-fonts).
</ParamField>

```ts theme={null}
import Entities from "@native/entities";

// getBoundingBox defined above - see Box ESP example

on("render", () => {
    for (const enemy of Entities.getPlayers({ skipLocal: true })) {
        if (enemy.m_iHealth <= 0) continue;

        const box = getBoundingBox(enemy);
        if (!box) continue;

        // center the name above the bounding box
        const name = enemy.controller?.m_iszPlayerName ?? "unknown";
        const textSize = Render.calcTextSize(name);
        const centerX = (box.mins.x + box.maxs.x) / 2 - textSize.x / 2;
        Render.text(new Vector2(centerX, box.mins.y - 16), name, Color.white());
    }
});
```

***

### Render.calcTextSize

<br />

```ts theme={null}
Render.calcTextSize(text: string, fontSize?: number, fontName?: string): Vector2
```

Returns the pixel dimensions of a text string without drawing it.
Use this to center or align text precisely.

<Note>
  The measurement is exact on every call, including the first one, and costs
  nothing worth measuring - it walks the font's glyph table directly and caches
  the result. It is safe to call in `render` for every entity on screen.
</Note>

<ParamField path="text" type="string" required>
  The string to measure.
</ParamField>

<ParamField path="fontSize" type="number" default="14">
  Font size in pixels. Must match the size you pass to `Render.text`.
</ParamField>

<ParamField path="fontName" type="string" default="&#x22;segoe&#x22;">
  Font name. Must match the font you pass to `Render.text`. See [available fonts](#available-fonts).
</ParamField>

<Tip>
  Pass the **same** `fontSize` and `fontName` you will pass to `Render.text` -
  measuring with one and drawing with another is the only way to get a mismatch.
  Both default to `14` and `"segoe"`.
</Tip>

```ts theme={null}
const text = "Hello World";
const size = Render.calcTextSize(text, 18);
console.log(`Text is ${size.x}px wide, ${size.y}px tall`);

// measure with a specific font
const size2 = Render.calcTextSize(text, 18, "verdana");

// center text on screen
on("render", () => {
    const screen = Render.getScreenSize();
    const textSize = Render.calcTextSize(text, 18, "segoe-semibold");
    const pos = new Vector2(
        screen.x / 2 - textSize.x / 2,
        screen.y / 2 - textSize.y / 2
    );
    Render.text(pos, text, Color.white(), 18, "segoe-semibold");
});
```

***

<h3 id="available-fonts">
  Available fonts
</h3>

| Name | Description |
| :- | :- |
| `"segoe"` | Segoe UI Regular (default) |
| `"segoe-semibold"` | Segoe UI Semibold |
| `"segoe-bold"` | Segoe UI Bold |
| `"dewi"` | RF Dewi Expanded |
| `"dewi-semibold"` | RF Dewi Semibold |
| `"tahoma"` | Tahoma |
| `"verdana"` | Verdana |
| `"arial"` | Arial |
| `"weapons"` | Weapon icons |

***

## Images

Draw loaded [Texture](/api/types/texture) objects.

<Warning>
  Always check `texture.isReady` before drawing. Textures load asynchronously from the disk. Attempting to draw a texture that hasn't finished loading is a silent no-op.
</Warning>

```ts theme={null}
Render.image(texture: Texture, min: Vector2, max: Vector2, color?: Color): void
Render.imageRounded(texture: Texture, min: Vector2, max: Vector2, rounding?: number, color?: Color): void
```

<ParamField path="texture" type="Texture" required>
  A previously loaded texture instance.
</ParamField>

<ParamField path="min" type="Vector2" required>
  Top-left corner of the image rectangle.
</ParamField>

<ParamField path="max" type="Vector2" required>
  Bottom-right corner of the image rectangle.
</ParamField>

<ParamField path="rounding" type="number" default="0.0">
  Corner radius in pixels. *(Only available in `imageRounded`)*
</ParamField>

<ParamField path="color" type="Color" default="Color.white()">
  Acts as a tint mask. Using `Color.white()` draws the image in its original colors.
</ParamField>

```ts theme={null}
// Do this ONCE outside your render loop
const avatar = new Texture("path/to/avatar.png");

// Inside your tick callback:
on("render", () => {
    if (!avatar.isReady) return;

    // Draw a 48x48 avatar in the top-left corner as a circle
    Render.imageRounded(avatar, new Vector2(10, 10), new Vector2(58, 58), 24, Color.white());
});
```

***

## Cookbook: Custom Crosshairs

A quick example showing how to combine basic primitives to build custom UI elements.

<Frame>
  <img src="https://mintlify.s3.us-west-1.amazonaws.com/spurdo/images/crosshair-example.png" alt="A custom drawn crosshair" />
</Frame>

```ts theme={null}
on("render", () => {
    const { x: screenWidth, y: screenHeight } = Render.getScreenSize();
    const cx = screenWidth / 2;
    const cy = screenHeight / 2;
    const gap = 4;
    const size = 10;
    const col = Color.green();

    // Center dot
    Render.circleFilled({ x: cx, y: cy }, 1, col);

    // 4 radiating lines
    Render.line({ x: cx - gap - size, y: cy }, { x: cx - gap, y: cy }, col);
    Render.line({ x: cx + gap, y: cy }, { x: cx + gap + size, y: cy }, col);
    Render.line({ x: cx, y: cy - gap - size }, { x: cx, y: cy - gap }, col);
    Render.line({ x: cx, y: cy + gap }, { x: cx, y: cy + gap + size }, col);
});
```
