---
title: "Векторная графика: линии и круги"
description: "vector.Path, Fill, Stroke: UI без картинок. Круги, линии и рамки HUD, кулдаун-дуга по углу, кэш пути и хитбоксы примитивов в играх на Ebitengine."
course: "Ebitengine: 2D-игры на Go"
url: "https://golangmaster.ru/courses/ebitengine/lessons/19-primitivy/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Векторная графика: линии и круги

*Курс «Ebitengine: 2D-игры на Go», урок 19 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/ebitengine/lessons/19-primitivy/*

Спрайты — не единственный способ рисовать: сетка уровня, рамки HUD и индикаторы перезарядки не требуют картинок и живут в векторной графике. В Ebitengine за неё отвечает пакет vector: готовые функции для кругов, линий и прямоугольников плюс тип vector.Path для фигур, собранных из сегментов. Мы нарисуем HUD из примитивов, полый круг кулдауна по углу и линию трассы, посчитаем цену кадра и закроем графику примитивы для хитбоксов — без единого файла ассетов.

У игры есть спрайты и текст, но между ними дыра: полоса здоровья, рамка экрана, сетка в редакторе и круг перезарядки не тянут на полноценные ассеты — ради них не заводят ни атлас, ни художника. Именно эту дыру закрывает пакет vector из поставки Ebitengine: векторная графика примитивов рождается прямо в коде, меняется константой и весит ноль байт. В этом уроке мы соберём из линий и кругов боевой HUD, поймём, когда примитив честнее спрайта, посчитаем цену каждого вызова и прикрутим к примитивам хитбоксы — геометрию, которую игра проверяет так же легко, как рисует. К концу урока прототип сможет выглядеть собранным ещё до появления настоящего арта.

### Пакет vector: готовые функции

Пакет vector открывается пятью функциями быстрого рисования: FillCircle заливает круг, StrokeCircle обводит окружность, StrokeLine проводит линию заданной ширины, FillRect и StrokeRect заливают и обводят прямоугольники. Общая схема аргументов одинаковая: холст, координаты в float32, цвет и последний флаг antialias — сглаживание краёв. Включённый флаг даёт мягкие края ценой дополнительных вычислений, выключенный рисует резко и дешевле: для статичного UI сглаживание включают, для сотен объектов на кадр — выключают. Координаты — пиксели холста, и круг задаётся центром плюс радиусом, а не прямоугольником: геометрия круга живёт прямо в аргументах. Массовость — главный аргумент в пользу готовых функций: сетка из сотни линий или облако частиц из кругов собираются циклом без единой картинки, и правка цвета — одна константа в коде, а не перерисовка ассета.

Имена честно отражают историю пакета: DrawFilledCircle из старых статей с версии 2.9 помечена устаревшей и стала алиасом FillCircle — в новом коде используйте актуальные имена, чтобы не тащить предупреждения и не путать читателя. Цвет — обычный color.Color из image/color, и прозрачность через альфу работает так же, как у спрайтов: полупрозрачный прямоугольник поверх экрана — это FillRect с альфой, а не отдельная картинка затемнения. Именно так собирается оверлей паузы из урока про сцены: затемнение плюс надпись — и никакой картинки не понадобилось.

### HUD: рамка, здоровье и кулдаун

```go
func drawHUD(screen *ebiten.Image, g *Game) {
        // рамка экрана: контур прямоугольника шириной 2
        vector.StrokeRect(screen, 8, 8, 624, 464, 2,
                color.RGBA{0x80, 0x80, 0x80, 0xff}, false)

        // полоса здоровья: ширина зависит от hp
        w := float32(g.hp) / float32(g.maxHP) * 120
        vector.FillRect(screen, 16, 16, w, 14,
                color.RGBA{0xff, 0x40, 0x40, 0xff}, false)

        // контур индикатора перезарядки
        vector.StrokeCircle(screen, 600, 40, 16, 2,
                color.RGBA{0xff, 0xff, 0xff, 0xff}, false)
        // заливка растёт по мере готовности
        r := float32(12) * (1 - g.cooldown/g.cooldownMax)
        vector.FillCircle(screen, 600, 40, r,
                color.RGBA{0x40, 0x80, 0xff, 0xff}, true)
}
```

Рамка — один вызов StrokeRect с шириной линии два: ни картинки, ни четырёх вручную поставленных полосок. Полоса здоровья — FillRect, чья ширина считается из отношения hp к максимуму: ноль здоровья даёт пустую полосу, полный запас — сто двадцать пикселей. Индикатор перезарядки собран из двух кругов: StrokeCircle рисует постоянный контур, а радиус заливки FillCircle растёт по мере готовности — от нуля до почти полного радиуса. Заливка со сглаживанием ради мягкого края, рамка без него: на статичном HUD лишние вычисления не нужны.

**Вопрос:** Какая функция рисует залитый круг в актуальном API пакета vector?

- DrawFilledCircle — классическое имя из старых примеров
- ✅ FillCircle: DrawFilledCircle устарела с версии 2.9
- StrokeCircle с нулевой шириной линии
- FillPath с параметром круга

DrawFilledCircle ещё работает, но помечена устаревшей: с версии 2.9 пакет переименовал функции, и актуальное имя заливки круга — FillCircle. Обводка круга в том же переименовании стала StrokeCircle, поэтому смешивать старые и новые имена в одном проекте не стоит.

### Path: фигуры из сегментов

Когда готовых фигур мало, включается vector.Path — собираемая фигура. Метод MoveTo ставит перо в точку, LineTo тянет сегмент до следующей, Arc рисует дугу по центру, радиусу, стартовому и конечному углу, Close замыкает контур. Готовый путь отдаётся в FillPath — заливку — или StrokePath — обводку — вместе с опциями: StrokeOptions задаёт ширину линии Width и стили концов и стыков, DrawPathOptions — сглаживание AntiAlias и цвет через ColorScale. Путь — просто список инструкций: его можно строить заново каждый кадр, а можно собрать один раз и переиспользовать, и от этого выбора зависит производительность следующего раздела. Кроме линий у пера есть кривые: QuadTo тянет квадратичную кривую через одну контрольную точку, CubicTo — кубическую через две, и на них строятся плавные траектории и органические контуры; в играх курса хватает линий и дуг, но знать про кривые стоит уже потому, что они объясняют, почему дуги в Path сегментированы.

Круг из готовой функции и круг из Path — разные вещи по цене: FillCircle внутри строит путь с дугой на две сотни сегментов, но делает это в оптимизированном виде с переиспользованием вершин. Свой Path нужен там, где фигура нестандартна: дуга перезарядки по углу, ломаная трассы, многоугольник препятствия. Для прямых и прямоугольников Path избыточен — готовые функции короче, быстрее и читаются в одну строку. Ещё один плюс собранного пути — переносимость: один и тот же Path можно обвести на любом холсте — на экране, на offscreen-изображении для предрендера или на карте, куда смотрит камера, — геометрия не знает, куда её рисуют, и это делает пути удобными заготовками для слоёв интерфейса.

### Кулдаун-дуга и линия трассы

```go
// кулдаун-дуга: прогресс виден по углу
var ring vector.Path
start := float32(-math.Pi / 2) // старт сверху
end := start + 2*math.Pi*(1-g.cooldown/g.cooldownMax)
ring.Arc(600, 40, 16, start, end, vector.Clockwise)
vector.StrokePath(screen, &ring,
        &vector.StrokeOptions{Width: 3},
        &vector.DrawPathOptions{AntiAlias: true})

// линия трассы выстрела
vector.StrokeLine(screen, g.px, g.py, g.tx, g.ty, 2,
        color.RGBA{0xff, 0xff, 0x00, 0xff}, true)
```

Дуга перезарядки — Path с одним вызовом Arc: центр, радиус, стартовый угол сверху и конечный угол, привязанный к прогрессу, направление по часовой. StrokePath обводит путь линией шириной три со сглаживанием — получается полый индикатор, который отрастает по кругу без единого кадра анимации. Трасса выстрела — готовая StrokeLine: две точки, ширина два, жёлтый цвет. Путь ring строится заново каждый кадр, потому что угол меняется; статичные фигуры так рисовать дорого.

### Где примитивы бьют спрайты

| Задача | Инструмент | Почему примитив |
| --- | --- | --- |
| Сетка уровня в редакторе | StrokeLine в цикле по шагу | спрайтам нужен тайл под каждую длину |
| Рамки и панели HUD | StrokeRect и FillRect | цвет меняется константой, атлас не растянешь |
| Индикаторы прогресса | Arc и StrokePath | угол считается кодом, а не режется из картинки |
| Прототип физики | FillCircle по точкам тел | коллизии видно сразу, до отрисовки спрайтов |
| Снаряды и пузыри | FillCircle | десяток заготовок заменяет одна функция |
| Финальный арт игры | спрайты и атласы | чистой геометрии не хватает выразительности |

### Какой кусок кольца обведён при прогрессе перезарядки 0.25?

```go
// g.cooldown / g.cooldownMax == 0.25
start := float32(-math.Pi / 2)
end := start + 2*math.Pi*(1-g.cooldown/g.cooldownMax)
ring.Arc(600, 40, 16, start, end, vector.Clockwise)
vector.StrokePath(screen, &ring, ...)
```

**Что выведет программа?**

- ✅ Три четверти круга: заряд почти готов
- Четверть круга: обведено ровно 25 процентов
- Полный круг: дуга не зависит от прогресса
- Ничего: отрицательный стартовый угол стирает дугу

Дуга покрывает готовую часть: единица минус прогресс даёт три четверти оборота от верхней точки по часовой. Если кольцо должно показывать оставшееся время, множитель меняют на сам прогресс — это вопрос дизайна, а не движка. Главное: угол — арифметика, а не раскадровка спрайта.

### Цена: строить путь каждый кадр дорого

Примитивы дёшевы только на первый взгляд: каждый вызов FillCircle внутри собирает путь из дуги на десятки сегментов, триангулирует его и отдаёт видеокарте. Десяток кругов на HUD — незаметно, тысяча частиц-кругов в каждом кадре — уже ощутимо, и тут начинается экономика. Фигуры, которые не меняются между кадрами, обязаны кэшироваться: vector.Path собирается один раз и хранится в поле структуры, а Draw рисует готовый путь вызовом FillPath или StrokePath — геометрия не пересчитывается, платится только вывод. Если фигура всё-таки поменялась, Reset() чистит путь, и он собирается заново — редкое событие, а не каждый тик.

Второй рычаг — сглаживание: флаг antialias и AntiAlias в опциях включают дополнительную работу на каждый пиксель края, и на сотнях объектов разница в кадрах видна без замеров. Правило: сглаживание — крупному UI и фигурам на переднем плане, без сглаживания — массовым объектам и отладочным слоям. Полную карту таких цен и способы замера соберёт [урок про производительность](/courses/ebitengine/lessons/20-proizvoditelnost/): примитивы стоят там в одном ряду с DrawImage и текстом, и кэш путей — первый пункт их оптимизации. Практический ориентир такой: HUD и отладочные слои живут без кэша спокойно, а всё, что рисуется циклом по десяткам объектов, обязано либо кэшировать пути, либо переходить на готовые функции, либо уходить в предрендер — один раз нарисованная в offscreen-картинку сетка дешевле векторной в разы.

**Вопрос:** Что делает последний аргумент bool в вызове vector.StrokeLine?

- Замыкает линию в контур
- ✅ Включает сглаживание antialias
- Задаёт ширину линии в пикселях
- Переводит координаты в доли экрана

Готовые функции пакета заканчиваются флагом сглаживания: включённый даёт мягкие края за счёт дополнительных вычислений, выключенный рисует резко и дешевле. Ширина линии — отдельный аргумент перед цветом, а замыкание контура — метод Close у Path.

> **Живое место: путь растёт без Reset.** Кэш пути работает, пока путь не мутирует. Типовая ошибка — держать один Path и в каждом кадре докидывать в него новые LineTo, не начав с Reset: путь накапливает сегменты, на первом кадре линия одна, на сотом — сто, и StrokePath честно обводит все сто, съедая кадр целиком. Симптом узнаваемый: игра тормозит тем сильнее, чем дольше работает, а на отдельном кадре в отладке всё чисто. Дисциплина: путь, собираемый каждый кадр, живёт локально в Draw и умирает; кэшированный — собирается один раз и после Reset, а не дописывается с конца.

### Хитбоксы на примитивах: круг против прямоугольника

Примитивы — не только картинка, но и геометрия, которую удобно проверять на пересечения. Круг с кругом проверяется расстоянием: сумма радиусов больше дистанции между центрами — столкнулись, а корень из квадрата расстояния извлекать не обязательно, достаточно сравнить квадраты. Круг с прямоугольником проверяется ближайшей точкой: координаты центра зажимаются в границы прямоугольника, и если зажатая точка ближе радиуса — пересечение есть. Углы обрабатываются сами собой: ближайшая точка может совпасть с углом прямоугольника, и формула это покрывает без отдельных веток.

Такая геометрия ложится прямо поверх [AABB-коллизий](/courses/ebitengine/lessons/09-kollizii-aabb/): там мир проверялся прямоугольниками, здесь к ним добавляются круги — снаряды, зоны взрыва, радиусы подбора. Практика прототипа физики: тела рисуются FillCircle по тем же координатам, по которым проверяются пересечения, поэтому логика и картинка не разъезжаются, а любая ошибка в движении видна на первом же кадре. Круговой хитбокс ещё и удобнее для геймплея: он не имеет углов, игрок прощает ему промахи на волосок, и вращение спрайта не ломает форму проверки — повёрнутый прямоугольник потребовал бы отдельной математики, круг от поворота не меняется. По этой формуле стоит устроить быструю проверку широким кругом перед точной: сначала грубый фильтр по расстоянию центров, потом детальная проверка — это тот же принцип грубого и точного прохода, на котором стоят все коллизии в играх.

### Правила примитивов одним списком

- Готовые функции FillCircle, StrokeCircle, StrokeLine, FillRect, StrokeRect закрывают HUD и сетки
- DrawFilledCircle и другие старые имена устарели с версии 2.9: используйте актуальные
- Path собирается из MoveTo, LineTo, Arc и Close и отдаётся в FillPath или StrokePath
- Флаг antialias — крупному UI, массовые объекты рисуются без сглаживания
- Меняющаяся фигура строится заново каждый кадр, статичная кэшируется в поле структуры
- Мутирующий кэш-путь начинается с Reset, иначе сегменты копятся и кадр дорожает
- Хитбоксы: квадраты расстояний для кругов, зажим ближайшей точки для круга с прямоугольником
- Прототип физики рисуется теми же координатами, что проверяются на пересечение

### Соберите путь из сегментов

Восстановите два метода: постановка пера и сегмент линии.

```go
var path vector.Path
path.___(x0, y0) // перо в стартовую точку
path.___(x1, y1) // сегмент до второй точки
vector.StrokePath(screen, &path,
        &vector.StrokeOptions{Width: 2},
        &vector.DrawPathOptions{})
```

Подсказка: Путь начинается с переноса пера без рисования, продолжается отрезками.

Правильные ответы в пропусках: `MoveTo`, `LineTo`

> **Про песочницу: математика без окна.** Рисование примитивов требует окна и проверяется в локальном Go, но геометрия урока — чистые вычисления: зажим координат, квадраты расстояний, углы дуги. Тематическая программа в песочнице сайта гоняет проверку круг-прямоугольник на десятках пар точек и печатает прогресс дуги по шагам — та же арифметика, что в circleHitsRect и кулдауне, только без холста.

### Проверка круг-прямоугольник

Соберите функцию пересечения круга с прямоугольником.

Правильный порядок строк:

1. `func circleHitsRect(cx, cy, r float32, rc Rect) bool {`
2. `// ближайшая к центру точка прямоугольника`
3. `px := max(rc.x, min(cx, rc.x+rc.w))`
4. `py := max(rc.y, min(cy, rc.y+rc.h))`
5. `dx, dy := cx-px, cy-py`
6. `return dx*dx+dy*dy <= r*r`
7. `}`

### Примитивы и итоговый проект

В мини-аркаде, к которой курс идёт, примитивы займут место по силе: рамка HUD и полоса здоровья — StrokeRect и FillRect из этого урока, кулдаун способности — дуга Arc, отладочные коллизии при доводке — StrokeCircle вокруг каждого хитбокса, включаемые одной константой. Отладочный слой из примитивов — приём профессиональных студий: спрайты могут врать про границы тела, круги и прямоугольники вокруг тел — никогда, и расхождение физики с картинкой ловится мгновенно, до воспроизведения бага игроком. Тот же слой помогает настройке: радиус подбора, длина луча, зона взрыва — всё это числа в коде, но включённый слой превращает их в видимые окружности, и балансировка из гадания становится настройкой по линейке.

Каркас [итогового проекта](/courses/ebitengine/lessons/25-itogovyj-proekt/) выигрывает и от того, что примитивы не добавляют ассетов: игре не нужны файлы, чтобы выглядеть собранной на ранних этапах. Стиль из чистой геометрии — честный минимум: рамка, круг, линия, текст из урока про шрифты. Этот минимум показывает механику в чистом виде, пока художественные ассеты дозревают в очереди на замену, и в прототипной фазе выглядит аккуратнее случайных заглушек.

Урок закрыл дыру между спрайтами и текстом: игра умеет рисовать геометрию — от рамки HUD до дуги перезарядки — не тратя ни одного файла ассетов. Пять готовых функций покрывают типовой UI, Path собирает нестандартные фигуры, кэш путей бережёт кадр, а хитбоксы на кругах и прямоугольниках делают прототип физики честным. Приёмы урока складываются в рабочий набор: статичный HUD без кэша, динамичные фигуры на готовых функциях, отладочный слой на одной константе. Дальше курс считает кадры: урок про производительность покажет, как замерять цену этих вызовов и где примитивам придётся уступить атласам. Домашнее задание соберёт HUD вашего проекта из примитивов — и он будет выглядеть готовым раньше, чем появятся настоящие спрайты.

### Термины урока

- **vector.Path** — Собираемая фигура: MoveTo, LineTo, Arc и Close. Отдаётся в FillPath для заливки или StrokePath для обводки с опциями.
- **FillCircle и StrokeCircle** — Залитый и полый круг: центр, радиус, цвет; у обводки ещё ширина линии. Последний аргумент — флаг сглаживания.
- **StrokeOptions** — Опции обводки: Width — толщина линии, LineCap и LineJoin — форма концов и стыков. Передаются в StrokePath.
- **Кэш пути** — Статичная фигура собирается в Path один раз и хранится в поле структуры; Draw только рисует готовый путь. После мутации — Reset.
- **antialias** — Сглаживание краёв: мягкие края за дополнительные вычисления. Включается флагом в готовых функциях или полем AntiAlias в DrawPathOptions.
- **Хитбокс круг-прямоугольник** — Центр круга зажимается в границы прямоугольника; если ближайшая точка ближе радиуса — пересечение есть. Углы покрываются автоматически.

## Коротко: что запомнить

- Пакет vector рисует UI без картинок: пять готовых функций для кругов, линий и прямоугольников.
- DrawFilledCircle устарела с версии 2.9 — актуальные имена начинаются с Fill и Stroke.
- Path собирает нестандартные фигуры из MoveTo, LineTo и Arc и отдаётся в FillPath или StrokePath.
- Сглаживание — последний аргумент готовых функций и AntiAlias в опциях: оно не бесплатное.
- Меняющиеся фигуры строятся заново, статичные кэшируются в поле структуры с Reset при мутации.
- Кулдаун-дуга — Arc по углу прогресса; трасса выстрела — StrokeLine по двум точкам.
- Хитбоксы примитивов: квадраты расстояний для кругов, зажим ближайшей точки для прямоугольников.

## Домашнее задание

Соберите HUD проекта из примитивов: рамку экрана через StrokeRect, полосу здоровья через FillRect с шириной от hp, круг перезарядки способности через Arc и StrokePath с углом от прогресса. Добавьте отладочный слой на клавишу F3: StrokeCircle вокруг каждого хитбокса и StrokeLine между парами сталкивающихся тел — и проверьте, что круги сидят точно на физике. Реализуйте circleHitsRect для снарядов и зон подбора, сравните с прямоугольными проверками из урока про коллизии: расхождение на углах нормально, круг честнее. Заметите подтормаживание — кэшируйте статичные пути в полях структуры и сверьте TPS до и после. В конце включите и выключите antialias на массовом объекте: разница в кадрах должна быть видна на сотнях кругов.

## Вопросы и ответы по уроку

### Почему координаты примитивов float32, а не float64?

Пакет целится в видеокарту, где float32 — родной формат вершин: конверсия на границе уже ничего не экономит. Для игровых координат тридцати двух бит хватает с запасом, а переход из float64 в float32 проще делать один раз, на границе пакета.

### Чем FillCircle отличается от Path с дугой на два пи?

Результат один: готовая функция внутри строит ровно такой путь, но в оптимизированном виде с переиспользованием вершин. Практическое правило: стандартная фигура — готовая функция, нестандартная или составная — собственный Path.

### Можно ли примитивами нарисовать весь прототип игры?

Да, и это рабочий приём: круги-игроки, прямоугольники-стены, линии-лучи проверяют механику до появления арта. Прототип на примитивах живёт днями, а не часами, потому что правка цвета и формы — константа в коде, а не перерисовка ассета.
