# Атласы из JSON — `$.atlas`

Спрайтовый лист обычно не сетка: художник режет картинки как удобно и отдаёт
вместе с ними JSON — где какой кадр и как называются анимации. `$.atlas`
читает этот JSON и делает из него спрайты.

```js
$.ready(() => {
    const hero = $.atlas.load('hero', 'art/hero.json');

    $('#hero').sprite(hero.frame('idle_0')).at(200, 300).appendTo($.world);

    // Тег Aseprite — готовый клип для $.anim.
    $.anim.define('hero', {
        clips: {
            idle: { frames: hero.tagSprites('idle'), fps: 8, loop: true },
            walk: { frames: hero.tagSprites('walk'), fps: 12, loop: true },
        },
    });
    $('.hero').anim('hero').play('idle');
});
```

---

## 1. Загрузка

| Вызов | Что делает |
|---|---|
| `$.atlas.load('hero', 'art/hero.json')` | читает JSON через `$.fs.readJSON`, грузит картинку из данных, режет кадры |
| `$.atlas.load('hero', { data: json, src: 'art/sheet.png' })` | данные уже в памяти (ответ сети, тест); `src` перебивает путь к картинке |
| `$.atlas.get('hero')` | загруженный атлас или `null` |
| `$.atlas.names()` | имена загруженных атласов |
| `$.atlas.unload('hero')` | забыть атлас (спрайты живут в движке) |
| `$.atlas.parse(data)` | разобрать данные без загрузки — для отладки |
| `$.atlas.imagePath(json, data)` | путь к картинке по данным |

Путь к картинке берётся из `meta.image` (Aseprite) или `image`, считается от
каталога JSON. Если поля нет — рядом с JSON подставляется `.png`.

## 2. Форматы

Формат определяется по содержимому, а не по расширению.

| Формат | Как узнать | Особенности |
|---|---|---|
| **Aseprite** (Export Sprite Sheet → JSON) | `frames` — объект, есть `meta` | кадры с `duration`, теги `meta.frameTags` |
| **TexturePacker / LibGDX** | `frames` — массив с `filename` | кадры вида `frame: {x,y,w,h}` |
| **свой простой** | `frames` — объект «имя → `{x,y,w,h}`» | годится для ручных списков; теги можно задать полем `tags` |

Поддержаны обе формы прямоугольника: `{w, h}` и `{width, height}`, а также
кадр без обёртки `frame` (плоский).

## 3. Объект атласа

| Метод | Возвращает |
|---|---|
| `frame(name)` | id спрайта кадра (`-1`, если кадра нет) |
| `frames()` | имена кадров в порядке атласа |
| `info(name)` | `{ name, x, y, w, h, duration }` или `null` |
| `tag(name)` | имена кадров тега; пустой массив, если тега нет |
| `tagSprites(name)` | массив id спрайтов — готовый вход `$.anim.clip` |
| `tagInterval(name)` | длительность кадра тега в мс (`0` — брать из клипа) |
| `tags()` | имена тегов |
| `slice(name, frame?)` | слайс Aseprite: `{frame, x, y, w, h, pivotX, pivotY, pivotLx, pivotLy}` |
| `sliceNames()` | имена слайсов |
| `sliceCount(name)` | сколько ключей (по кадрам) у слайса |
| `size()` | `[ширина, высота]` картинки атласа |
| `image`, `texture`, `format`, `meta` | поля загруженного атласа |

Тег `direction: 'reverse'` из Aseprite разворачивает список кадров, поэтому
`tag('walk')` идёт в правильном порядке.

## 3.1. Слайсы и пивоты Aseprite

Aseprite хранит **слайсы** (`meta.slices`): у каждого ключа прямоугольник и
**пивот**. Это ровно то, что нужно для рамок, точек крепления и вращения частей.

```js
const hero = $.atlas.load('hero', 'art/hero.json');

hero.sliceNames();                     // ['head', 'hand', 'body']
const hand = hero.slice('hand', 2);    // ключ слайса для кадра 2
// { frame, x, y, w, h, pivotX, pivotY, pivotLx, pivotLy }
```

`pivotX`/`pivotY` — как в JSON (**абсолютные**, в координатах спрайта);
`pivotLx`/`pivotLy` — локальные, от левого верхнего угла слайса. Для поворота
нужен именно локальный: `$.mesh.fromSlice` делает пивот **началом координат
части**, поэтому `$.mesh.draw` крутит часть вокруг сустава, а не вокруг угла
картинки (см. [mesh.md](highlevel/mesh) §2.1).

Без `pivot` в ключе пивот считается центром слайса.

Ключи слайса нумеруются **кадрами листа** (число), а не именами: `slice(name,
frame)` берёт последний ключ с `frame <=` указанного. Без аргумента — первый.

## 3.2. Правка в SDK

Атлас в формате «Aseprite JSON с `frames`-объектом» правит Sprite Studio
(SDK, [../SDK.md](SDK) §5): кадры, пивоты (слайсы с именем кадра),
длительности, теги-анимации (`meta.frameTags`, поле `loop` рантайм
игнорирует) и `meta.custom`. Файл пишется в каноническом виде — по строке на
кадр, тег и слайс. Движок следит за `*.atlas.json`
([script.md](highlevel/script)), поэтому сохранение в Studio перезапускает игру.

## 4. Ограничения

* **повёрнутые кадры не поддержаны**: если в JSON у кадра `rotated: true`,
  кадр пропускается с записью в журнал — выгрузите атлас без поворота;
* **trimmed-кадры** импортируются по своему прямоугольнику. Пивот из
  **слайсов** (`meta.slices`) теперь переносится — см. §3.1; если пивот задан
  только у trimmed-кадра и слайсов нет, ставьте его сами (`.pivot(0.5, 1)`);
* **`$.atlas` не кэширует JSON на диск**: повторный `load` тем же именем
  возвращает уже собранный атлас, а `reload` для атласов нет — вызовите
  `unload` и `load` заново;
* **картинка одна на атлас**: многолистовые атласы (несколько PNG в одном
  JSON) не собираются;
* **костей (`bones`) в Aseprite JSON нет**: они есть только в `.ase`, а JSON
  несёт кадры, теги и слайсы. Скелет задаётся через `$.mesh.skeleton` руками, а
  слайсы дают привязку частей и пивоты;
* выгрузка спрайтов и картинки — через `$.resource.free()` и
  `engine.freeTexture()` ([resource.md](highlevel/resource)).

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

```bash
# разбор трёх форматов, теги, обратное направление, путь к картинке, слайсы
build/_deps/quickjs-build/qjs tests/js/atlas_test.mjs
```
