# Спрайт: пивот и nine-slice

Две настройки, которых не хватало любому 2D-движку: **точка вращения** (пивот)
и **резиновая рамка** (nine-slice). Обе — свойства узла, обе работают с обычными
спрайтами, обе не требуют отдельных тегов.

```js
$.ready(() => {
    // Персонаж вращается вокруг ног, а не вокруг пуза.
    $('<player>', { id: 'hero', src: 'art/hero.png' })
        .size(48, 64).pivot(0.5, 1).at(200, 300).appendTo($.world);

    // Панель-рамка: углы целые, края тянутся под любой размер.
    $('<sprite>', { id: 'panel', src: 'ui/panel.png' })
        .slice({ left: 12, right: 12, top: 12, bottom: 12 })
        .size(420, 240).at(400, 200).appendTo($.ui);
});
```

---

## 1. Пивот

Пивот — точка узла, вокруг которой идут **вращение** и **масштаб**. По
умолчанию это центр (0.5, 0.5), то есть прежнее поведение движка.

| Вызов | Что делает |
|---|---|
| `.pivot(x, y)` | задать: доли 0..1 — доля размера узла, больше 1 — пиксели от левого верхнего угла |
| `.pivot()` | текущий пивот: `{ x, y }` |
| `.pivot(0.5, 0.5)` | вернуть вращение вокруг центра |
| `.pivotAt(x, y)` | пивот в мировых координатах: доли считаются от размера узла |

Типичные значения:

| Пивот | Точка вращения | Кому |
|---|---|---|
| `(0.5, 0.5)` | центр | снаряды, монеты, по умолчанию всё |
| `(0.5, 1)` | низ по центру | персонажи, стоящие на земле |
| `(0.5, 0)` | верх по центру | маятники, подвешенные объекты |
| `(0, 0.5)` | середина левого края | двери, ставни |
| `(0, 0)` | левый верхний угол | стрелки, «шпильки» на карте |

Как это работает внутри. Спрайт в C вращается вокруг центра своего
прямоугольника, поэтому движок сдвигает прямоугольник так, чтобы **пивот
оказался в точке узла**. Из этого следует простое правило: **координаты узла —
это координаты пивота**, а не центра картинки. Узел с `.pivot(0.5, 1).at(200, 300)`
стоит «ногами» в (200, 300).

Пивот не влияет на `.scale()` по осям? Влияет: масштаб идёт от пивота, поэтому
`.pivot(0.5, 1).scale(1, 1.4)` вытягивает персонажа вверх, оставив ноги на месте.

Габарит отсечения по камере считается от прямоугольника вокруг точки узла —
если пивот вынесен далеко за пределы спрайта, край может отсечься раньше
времени. Это осознанный компромисс: отсечение по габариту не может знать о
произвольном пивоте, а вынос пивота за спрайт — редкость.

## 2. Nine-slice

`.slice(insets)` режет спрайт на **девять частей** и растягивает их под размер
узла: четыре угла остаются целыми, края тянутся в одном направлении, центр
заполняет остальное. Так делаются окна интерфейса, рамки, панели диалогов и
«резиновые» кнопки без отдельной вёрстки.

```js
$('#panel').slice({ left: 12, right: 12, top: 12, bottom: 12 });
$('#frame').slice(8);                 // со всех сторон по 8
$('#panel').size(500, 300);           // края растянутся, углы — нет
$('#panel').slice(null);              // выключить
```

| Поле | Тип | Смысл |
|---|---|---|
| `left` / `right` / `top` / `bottom` | число | ширина полосы с этой стороны |

Толщина полос: **0..1 — доля стороны** исходного спрайта, **больше 1 —
пиксели**. `{ left: 0.25 }` возьмёт четверть ширины, `{ left: 12 }` — 12
пикселей.

Правила поведения:

* если места меньше суммы полос, полосы ужимаются пропорционально, чтобы части
  не налезали друг на друга;
* части режутся из текстуры по `src` (или из спрайта, поставленного `.sprite()`)
  и **кэшируются**: повторные кадры не создают новых спрайтов;
* nine-slice требует картинку, а не цвет: без `src` часть нарисовать не из чего,
  и в журнал уйдёт подсказка;
* поворот применяется к каждой части вокруг её центра — для рамок это не
  заметно, но для сильно повёрнутых «резиновых» панелей используйте обычный
  спрайт.

`.slice()` без аргумента возвращает текущие инсеты или `null`.

## 3. Проверка

```bash
# nine-slice и пивот проверяются агентским тестом: кадры с пивотом
# различаются, а возврат к центру даёт побитово тот же кадр
python3 tests/agent/highlevel_sprite_test.py
```
