# 캔버스

> 게임 화면에 텍스트 상자, 패널, 진행 바, 이미지, 지면에 붙는 원, 화살표가 달린 경로를 그립니다. 런타임이 매 프레임 직접 그리며 게임 상태를 바꾸지 않으므로 멀티플레이 게임에서도 안전합니다. Python, HTTP, 공유 메모리 직접 쓰기를 모두 지원합니다.

출처: https://war3ai.com/ko/docs/canvas/

외부 프로그램이 게임 화면에 **텍스트 상자, 패널, 진행 바, 이미지, 지면의 원, 화살표가 달린 지면 경로**를 그릴 수 있으며, 런타임이 매 프레임 직접 그립니다. 나만의 HUD, 보조선, 안내, 교육용 표시, 방송 정보판을 만들기에 알맞습니다.

## 캔버스와 JASS 화면 함수, 어느 쪽을 쓸까

| | 캔버스(이 페이지) | [JASS 화면 함수](https://war3ai.com/ko/docs/jass/) |
|---|---|---|
| 그리는 주체 | 런타임이 직접 그림 | 게임 자체(떠다니는 텍스트, 특수 효과, 패널, 초상화 대사……) |
| 멀티플레이 게임 | **안전**: 이 컴퓨터의 화면에만 그리며, 게임 오브젝트를 만들지 않고 게임 상태도 바꾸지 않음 | 싱글플레이 게임만 가능 |
| 스타일 | 자유로움: CJK 글꼴, 둥근 모서리, 반투명, 테두리, 임의의 색, 로컬 이미지 | 게임 기본 스타일 |
| 대상 따라가기 | 유닛, 월드 좌표, 화면 위치를 따라감. 지면의 원은 지형 기복에 맞춰 붙음 | 함수마다 다름 |
| 비용 | 실측 프레임당 0.2 ~ 0.35 ms(요소 9개) | 호출 1회에 약 13 ms |

두 방법은 함께 쓸 수 있습니다. 게임 고유 스타일의 효과는 JASS로, 직접 디자인한 패널, 보조선, 안내는 캔버스로 그리세요.

## Python

```python
c = g.canvas                               # 처음 쓸 때 런타임이 그리기 훅을 설치(약 0.1초)
c.text("title", "안녕하세요, 캔버스입니다", screen=(40, 110), color=(255, 220, 80),
       bg=(0, 0, 0, 170), border="#C49C40", size=22, bold=True)
c.panel("status", "동료 · 빛나", ["기분: 기쁨", "처치: 12"], screen=(16, 330))
c.bar("hp", 0.62, unit=hero, lift=260, width=100, height=12, color=(80, 220, 80), text="62%")   # 유닛을 따라감
c.text("tag", "보스가 궁극기를 쓰려고 합니다!", 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)          # 지면의 위험 지역
c.circle("aura", hero, 450, color=(80, 200, 255, 220))                                         # 유닛을 따라가는 원
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는 자기가 그린 것만 지움
c.expire("tag", 5)                         # 5초 뒤 저절로 사라짐
with c.batch(): ...                        # 여러 개를 한 번에 바꾸고, 공유 메모리는 한 번만 씀
c.stats()                                  # drawnFrames가 늘고 있으면 = 실제로 그리는 중
```

각 요소는 `key` 하나로 식별합니다. 같은 key로 다시 그리면 갱신됩니다.

**클릭 가능**: 텍스트 상자와 패널에 `clickable=True`를 붙이면(호버 색은 `hover=`로 지정) 클릭했을 때 이벤트 스트림에 `ui.click`이 하나 들어오고, `ev.key`가 바로 그 key입니다. 그 위를 누른 클릭은 게임에 전달되지 않습니다. 바로 쓸 수 있는 버튼, 선택 카드, 단축키, 지면 클릭은 [UI와 입력](https://war3ai.com/ko/docs/ui-input/)을 참고하세요.

**위치**(요소마다 하나 지정):

- `screen=(x, y)`: 화면 픽셀. 음수는 오른쪽 / 아래쪽에서부터 거꾸로 셉니다. `center=True`이면 중심 기준으로 정렬합니다.
- `frac=(0.5, 0.1)`: 화면 비율.
- `world=(x, y)`: 월드 좌표.
- `unit=유닛`: 유닛을 따라갑니다. 월드와 유닛 위의 텍스트, 진행 바는 아래쪽 가운데를 그 지점에 맞추며, `lift`만큼 위로 올립니다.

월드와 유닛 위의 요소는 기본적으로 하단 조작 패널과 상단 낮밤 시계를 피합니다(`over_ui=True`이면 그 위를 덮음). **색**은 `(r, g, b)`, `(r, g, b, a)`, `"#RRGGBB"`, `"#RRGGBBAA"`로 쓸 수 있습니다.

| 메서드 | 그리는 것 | 자주 쓰는 인자 |
|---|---|---|
| `text(key, 텍스트, ...)` | 텍스트 상자. 여러 줄은 `\n` | `color`, `bg` 배경색(없으면 투명), `border`, `size`, `bold`, `shadow`, `width`(이 너비에서 줄바꿈), `radius` 둥근 모서리 |
| `panel(key, 제목, [줄...], ...)` | 패널(어두운 반투명 배경, 금색 테두리) | `text`와 같음 |
| `bar(key, 0..1, ...)` | 진행 바: 체력, 쿨다운, 시전 바 | `width`, `height`, `color`, `bg`, `border`, `text` |
| `image(key, 경로, ...)` | 로컬 이미지(png / jpg / bmp / gif) | `width`, `height`(없으면 원본 크기) |
| `circle(key, 유닛 또는 지점, 반지름, ...)` | 지면의 원, 지형에 붙음 | `color` 선 색, `fill` 채우기(투명도 포함), `width` 선 굵기 |
| `path(key, [지점...], ...)` | 지면의 꺾은선 | `color`, `width`, `arrow` 끝 화살표. 지점은 좌표나 유닛 |

## HTTP(모든 언어)

Farsight 백엔드(로컬에서만 수신 대기):

```http
POST /api/instances/20/canvas
{"set": [
   {"key": "banner", "kind": "text", "text": "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        지금 그려진 요소 목록 + 그린 프레임 수
```

`kind`는 Python의 메서드 이름이고, 인자 이름도 같습니다. 유닛은 스냅샷의 주소 `addr`로 씁니다.

## 공유 메모리에 직접 쓰기

Python과 Farsight를 거치지 않을 수도 있습니다. 먼저 시맨틱 명령 `canvas_enable`(W3P 연산 코드 73)을 한 번 보내면, 런타임이 공유 메모리 블록 `Local\War3Canvas_<pid>`를 만듭니다. 구성은 헤더 64바이트 + 항목 256개 × 112바이트 + 64 KB 텍스트 / 지점 풀입니다. seqlock 방식으로 씁니다(시퀀스 번호를 홀수로 → 항목과 풀 쓰기 → 시퀀스 번호를 짝수로). 런타임은 매 프레임 한 번 읽고, 반쯤 쓰인 상태를 읽으면 이전 프레임 내용을 그대로 쓰며, 그린 프레임 수, 요소 수, 예외 횟수를 되써 줍니다. Python 참조 구현은 `sdk/python/w3canvas.py`이며, 구조체 정의는 프로토콜 헤더 파일에 있습니다. [W3P 프로토콜](https://war3ai.com/ko/docs/protocol/)을 참고하세요.

## 여러 프로그램이 동시에 그릴 때

모드, Farsight, MCP, 게이트웨이가 같은 게임에 동시에 그릴 수 있지만, 캔버스는 하나뿐입니다. 규칙은 **각 프로그램은 자기 요소만 건드린다**입니다.

- 쓰기 전에 이름 있는 락을 먼저 잡고, 기존 요소를 읽어 다른 프로그램의 것은 남기고 자기 것만 바꾼 뒤 다시 씁니다.
- 요소마다 누가 그렸는지(프로세스 ID + 프로세스 안 일련번호)를 기록합니다. 그린 프로그램이 종료되면 다음에 누군가 쓸 때 함께 정리되고, 그 프로그램의 버튼도 더 이상 클릭을 가로채지 않습니다.
- 요소 번호는 공용 카운터에서 할당하므로 겹치지 않습니다.

Python SDK는 이미 이렇게 동작하며, `clear()`도 자기 것만 지웁니다. 공유 메모리에 직접 쓴다면 이 규칙을 따르세요. 그러지 않으면 다른 프로그램의 것을 덮어써 버립니다. 레이아웃 세부 사항은 [W3P 프로토콜](https://war3ai.com/ko/docs/protocol/)을 참고하세요.

## 실측과 주의 사항

- 2026-09-25 실측(1920×1080, 2배속): 요소 9개에 프레임당 0.27 ~ 0.34 ms, 약 63프레임/초, 예외 0회. 9개를 쓰는 데 6 ms. 영웅이 움직일 때 유닛을 따라가는 원, 텍스트, 체력 바가 모두 잘 따라갑니다. 내용이 바뀔 때만 텍스처를 다시 그리고, 위치만 옮길 때는 다시 그리지 않습니다.
- 게임 UI를 그린 뒤, 마우스 포인터를 그리기 전에 그립니다. 게임 자체의 체력 바, 유닛, UI 위를 덮고, 마우스 포인터는 그 위에 그려집니다. 하단 조작 패널과 상단 낮밤 시계는 피하지만, **맵 자체의 패널(오른쪽 위의 리더보드, 카운트다운)은 피하지 않습니다** — 직접 만든 패널은 오른쪽 위에 두지 마세요.
- 게임 중이 아닐 때(메인 메뉴, 결과 화면)는 월드 좌표와 유닛에 붙은 요소는 그리지 않고, 화면 위치에 둔 요소는 그대로 그립니다.
- 지면의 원은 원주 위 64개 점을 각각 지면에 투영한 것이라, 지형에 높낮이가 있으면 모양도 따라 굴곡집니다 — 이것이 맞습니다. 실제 지면 위에 그리기 때문입니다.
- 처음 열 때 훅 설치와 글꼴 워밍업에 약 1초가 걸리며, 그동안 텍스트 계열 요소는 그리지 않고 원과 선은 그대로 그립니다.
- 그리는 중 예외가 한 번이라도 나면 이번 세션에서는 더 이상 그리지 않습니다(머리 위 말풍선과 같은 보호 장치). `stats()`의 `faults`가 1이 됩니다.
- 텍스트, 이미지 경로, 지점을 합쳐 64 KB, 요소는 최대 256개입니다. 이미지 경로는 게임 프로세스가 읽을 수 있는 로컬 경로여야 합니다.

[AI 동료](https://war3ai.com/ko/docs/companion/)의 상태 패널도 캔버스로 그린 것입니다. 체력 바, 지금 하는 일, 기분, 처치 수와 치유 횟수를 보여 줍니다.
