Canvas
Draw text boxes, panels, progress bars, images, circles that hug the ground and routes with arrows on the game screen. The runtime draws them itself every frame without changing game state, so it's safe in multiplayer games; use it from Python, over HTTP, or by writing shared memory directly.
External programs can draw text boxes, panels, progress bars, images, circles on the ground and routes on the ground (with arrows) on the game screen, and the runtime draws them itself every frame. It’s a good fit for your own HUD, guide lines, hints, teaching annotations and stream info boards.
Canvas or JASS visual functions?
| Canvas (this page) | JASS visual functions | |
|---|---|---|
| Who draws it | The runtime draws it itself | The game itself (floating text, effects, boards, portrait dialogue…) |
| Multiplayer | Safe: drawn only on your local screen; creates no game objects and doesn’t change game state | Single-player only |
| Style | Anything you like: Chinese fonts, rounded corners, translucency, borders, any color, local images | The game’s native look |
| Following things | Follows units, world coordinates or screen positions; ground circles follow the terrain’s rises and dips | Depends on the function |
| Cost | Measured at 0.2–0.35 ms per frame (9 elements) | About 13 ms per call |
You can use both together: JASS for effects in the game’s native style, the canvas for custom panels, guide lines and hints.
Python
c = g.canvas # on first use the runtime installs its draw hook (~0.1 s)
c.text("title", "Hello, this is the canvas", screen=(40, 110), color=(255, 220, 80),
bg=(0, 0, 0, 170), border="#C49C40", size=22, bold=True)
c.panel("status", "Companion · Sunny", ["Mood: happy", "Kills: 12"], screen=(16, 330))
c.bar("hp", 0.62, unit=hero, lift=260, width=100, height=12, color=(80, 220, 80), text="62%") # follows the unit
c.text("tag", "Boss ultimate incoming!", unit=boss, lift=320, color=(255, 80, 80), size=22, bold=True)
c.circle("danger", (x, y), 300, color=(255, 60, 60), fill=(255, 60, 60, 60), width=3) # danger zone on the ground
c.circle("aura", hero, 450, color=(80, 200, 255, 220)) # circle that follows a unit
c.path("route", [(x1, y1), (x2, y2), (x3, y3)], color=(255, 220, 0), width=5, arrow=True)
c.image("icon", "icon.png", screen=(40, 170), width=64, height=64)
c.remove("danger"); c.hide("tag"); c.clear() # clear only removes what you drew
c.expire("tag", 5) # disappears by itself after 5 seconds
with c.batch(): ... # change many entries at once, with a single shared-memory write
c.stats() # drawnFrames going up = it's really drawing
Each element is identified by a key: drawing the same key again updates it.
Clickable: add clickable=True to a text box or panel (set the hover color with hover=). When it’s clicked, a ui.click arrives in the event stream with ev.key set to that key, and the game never receives that click. For ready-made buttons, choice cards, hotkeys and ground clicks, see UI & input.
Position (give each element one of these):
screen=(x, y): screen pixels; negative values count back from the right / bottom edge;center=Truealigns by the center;frac=(0.5, 0.1): fraction of the screen;world=(x, y): world coordinates;unit=u: follows a unit. Text and progress bars placed in the world or on a unit are anchored by the midpoint of their bottom edge;liftraises them.
By default, elements in the world and on units stay clear of the HUD at the bottom and the day/night clock at the top (over_ui=True draws over them). Colors can be written as (r, g, b), (r, g, b, a), "#RRGGBB" or "#RRGGBBAA".
| Method | What it draws | Common parameters |
|---|---|---|
text(key, text, ...) | Text box; use \n for multiple lines | color, bg background (transparent if omitted), border, size, bold, shadow, width (wraps at this width), radius for rounded corners |
panel(key, title, [lines...], ...) | Panel (dark translucent background, gold border) | Same as text |
bar(key, 0..1, ...) | Progress bar: health, cooldown, cast bar | width, height, color, bg, border, text |
image(key, path, ...) | Local image (png / jpg / bmp / gif) | width, height (original size if omitted) |
circle(key, unit_or_point, radius, ...) | Circle on the ground, hugging the terrain | color line color, fill fill (with alpha), width line width |
path(key, [points...], ...) | Polyline on the ground | color, width, arrow for an arrowhead at the end; points can be coordinates or units |
HTTP (any language)
Farsight backend (listens only on the local machine):
POST /api/instances/20/canvas
{"set": [
{"key": "banner", "kind": "text", "text": "Canvas from HTTP", "frac": [0.5, 0.12], "center": true,
"color": "#FFDC50", "bg": [0, 0, 0, 180]},
{"key": "hp", "kind": "bar", "value": 0.8, "unit": 596125988, "lift": 260, "text": "80%"},
{"key": "zone", "kind": "circle", "center": 596125988, "radius": 600, "color": [255, 200, 0], "width": 4},
{"key": "route", "kind": "path", "points": [[-4587, -9092], [-5387, -8792]], "color": "#50C8FF"}
],
"remove": ["old"], "clear": false}
GET /api/instances/20/canvas which elements are drawn right now + how many frames have been drawn
kind is the Python method name, and the parameter names are the same too; for a unit, pass its addr from the snapshot.
Writing shared memory directly
You can also skip Python and Farsight entirely: first send the semantic command canvas_enable once (W3P opcode 73), and the runtime creates the shared memory block Local\War3Canvas_<pid>: a 64-byte header + 256 entries × 112 bytes + a 64 KB text / point pool. Write it with a seqlock (sequence number goes odd → write the entries and the pool → sequence number goes even). The runtime reads it once per frame; if it reads a half-written update it keeps the previous frame, and it writes back the number of frames drawn, the element count and the fault count. The Python reference implementation is sdk/python/w3canvas.py, and the struct definitions are in the protocol header file; see W3P protocol.
Several programs drawing at once
Mods, Farsight, MCP and the gateway may all draw into the same game at the same time, and there’s only one canvas. The rule is: each program touches only its own elements.
- Before writing, take a named lock, read the existing elements, keep everyone else’s, swap in your own, then write it back;
- Each element records who drew it (process ID + an in-process sequence number). When the program that drew it exits, the element is cleaned up the next time anyone writes, and its buttons stop intercepting clicks;
- Element IDs are allocated from a shared counter, so they never collide.
The Python SDK already does this, and clear() only clears its own elements. If you write the shared memory directly, follow the same rules, or you’ll wipe out other programs’ elements. For layout details, see W3P protocol.
Measurements and caveats
- Measured on 2026-09-25 (1920×1080, 2× speed): 9 elements take 0.27–0.34 ms per frame at about 63 frames per second, with 0 faults; writing 9 entries takes 6 ms; while the hero moves, the circles, text and health bars that follow units keep up. Textures are redrawn only when the content changes; moving an element doesn’t trigger a redraw.
- It’s drawn after the game UI and before the mouse cursor: it covers the game’s own health bars, units and UI, and the cursor is drawn over it. It stays clear of the HUD at the bottom and the day/night clock at the top, but does not avoid the map’s own panels (the leaderboard and countdown in the top-right corner) — don’t put your own panels in the top-right.
- Outside a game (main menu, score screen), elements placed at world coordinates or on units aren’t drawn; elements at screen positions are drawn as usual.
- A ground circle is made by projecting each of 64 points on its circumference onto the ground, so on uneven terrain its shape rises and dips with the ground — that’s correct: it’s drawn on the real ground.
- The first time it’s enabled, it installs the hook and warms up the fonts, which takes about 1 second; during that time text elements aren’t drawn yet, but circles and lines are.
- If a single exception occurs while drawing, nothing more is drawn for the rest of the session (the same protection as speech bubbles), and
faultsinstats()becomes 1. - Text, image paths and points share 64 KB in total, with at most 256 elements; image paths must be local paths the game process can read.
The AI companion’s status panel is drawn with the canvas: health bar, what it’s doing, mood, and kill and heal counts.