LeCodesdocs

Canvas и Bitmap

2D-поверхность для рисования, запекающаяся в текстуру — способ поместить текст и произвольную векторную графику в спрайты, 3D-материалы и UI-изображения («canvas — это просто текстура»). API повторяет 2D-контекст браузера, но записывает команды вместо немедленной отрисовки: весь буфер растрируется один раз за запекание, на нативном 2D-стеке платформы.

Обзор

TypeScript
const c = new Canvas(160, 40, { pixelRatio: 2 })       // логические 160×40, запекается в 320×80
c.font = 'bold 24px Inter'
const w = c.measureText('Alice').width + 24
c.resize(w, 40)
c.fillStyle = '#000a'
c.roundRect(0, 0, w, 40, 12).fill()
c.fillStyle = '#fff'
c.textAlign = 'center'
c.fillText('Alice', w / 2, 27)

const tag = new Sprite({ texture: c })   // Canvas — валидный источник текстуры где угодно

Создание

TypeScript
new Canvas(width, height, opts?: {
  pixelRatio?: number    // множитель device-пикселей; по умолчанию 1
})

Всё рисование задаётся в логических единицах. width/height — логический размер; запечённая текстура имеет width × pixelRatio на height × pixelRatio device-пикселей. pixelRatio влияет только на чёткость, никогда на экранный размер — Sprite на основе Canvas по умолчанию берёт логический размер как мировой. Используй pixelRatio: 2 (или device.pixelRatio) для чёткого текста на retina-экранах.

Note

цвета и шрифты — это CSS-строки, разрешаемые платформой. Всё рисование выполняй после await registerFont(...) для любого своего шрифта — незагруженный шрифт измеряется неверно.

Состояние рисования

Свойства чтения/записи, как в браузере. Установка одного записывает операцию состояния и запоминает значение, чтобы геттер вернул его обратно.

TypeScript
c.fillStyle = '#ff8800'          // любая CSS-строка цвета; по умолчанию '#000000'
c.strokeStyle = '#fff'
c.lineWidth = 2                  // по умолчанию 1
c.lineJoin = 'miter'             // 'miter' | 'round' | 'bevel'
c.lineCap = 'butt'               // 'butt' | 'round' | 'square'
c.globalAlpha = 0.5              // 0..1; по умолчанию 1
c.font = '16px sans-serif'       // CSS-строка шрифта; по умолчанию '10px sans-serif'
c.textAlign = 'start'            // 'left' | 'center' | 'right' | 'start' | 'end'
c.textBaseline = 'alphabetic'    // 'alphabetic' | 'top' | 'middle' | 'bottom' | 'hanging' | 'ideographic'

Трансформы и стек состояний

Все методы рисования возвращают this, поэтому вызовы чейнятся.

TypeScript
c.save(): this                   // положить состояние (трансформ + состояние рисования)
c.restore(): this                // снять его
c.translate(x, y): this
c.scale(sx, sy): this            // трансформ — не связан с pixelRatio
c.rotate(rad): this              // радианы

Пути

TypeScript
c.beginPath(): this
c.moveTo(x, y): this
c.lineTo(x, y): this
c.quadraticCurveTo(cx, cy, x, y): this
c.bezierCurveTo(c1x, c1y, c2x, c2y, x, y): this
c.arc(x, y, r, a0, a1, ccw?): this      // углы в радианах; ccw по умолчанию false
c.rect(x, y, w, h): this
c.roundRect(x, y, w, h, r): this        // один радиус на все углы
c.closePath(): this
c.fill(evenOdd?): this                  // evenOdd по умолчанию false (ненулевое правило обхода)
c.stroke(): this

Сахар для прямоугольников и текста

TypeScript
c.fillRect(x, y, w, h): this
c.strokeRect(x, y, w, h): this
c.clearRect(x, y, w, h): this
c.fillText(text, x, y, maxWidth?): this      // maxWidth 0 = без ограничения (по умолчанию)
c.strokeText(text, x, y, maxWidth?): this
c.measureText(text): { width, ascent, descent }   // измеряет в ТЕКУЩЕМ шрифте, логические px

measureText — единственный синхронный вызов к платформе; всё остальное только записывает.

Изображения: drawImage и Bitmap

drawImage копирует Bitmap — снимок, сделанный через toBitmap(). Он не принимает Texture2D или другой Canvas.

TypeScript
c.drawImage(bmp, dx, dy): this                            // весь bitmap в натуральном (логическом) размере
c.drawImage(bmp, dx, dy, dw, dh): this                    // весь bitmap, масштабированный в dw×dh
c.drawImage(bmp, sx, sy, sw, sh, dx, dy, dw, dh): this    // подпрямоугольник, масштабированный в dw×dh

Все координаты логические — включая исходный подпрямоугольник в форме с 9 аргументами. Копирование записывается, как и всё остальное, поэтому держи Bitmap живым до следующего запекания (update() / texture() / toBitmap()).

TypeScript
canvas.toBitmap(): Bitmap    // неизменяемая копия текущих пикселей, независимая от canvas
TypeScript
bmp.width, bmp.height              // логический размер
bmp.pixelWidth, bmp.pixelHeight    // device-пиксели (логический × pixelRatio)
bmp.toFile(name?, type?): Promise<File>   // кодировать: 'image/png' (по умолчанию) или 'image/jpeg'
bmp.destroy(): void                // освобождает нативную поверхность; после этого bitmap непригоден

Паттерн «сплющивания» — держать повторные запекания O(1) на постоянно растущем рисунке:

TypeScript
const snap = canvas.toBitmap()
canvas.reset()
canvas.drawImage(snap, 0, 0)   // одна операция заменяет всю историю; продолжай рисовать поверх

Запекание в текстуры

TypeScript
canvas.texture(): Texture2D    // запечь + вернуть 2D-текстуру (создаётся один раз, затем кэшируется)
canvas.update(): this          // перерастрировать и перезалить в каждую текстуру, произведённую этим canvas

texture() сам вызываешь редко — присваивание canvas туда, где ждут текстуру (new Sprite({ texture: c }), UIImage, карта Material), запекает его. После перерисовки динамического canvas (reset() + новые команды) вызови update(), чтобы протолкнуть новые пиксели всем его потребителям.

TypeScript
canvas.toFile(name?, type?): Promise<File>   // закодировать текущие пиксели; png (по умолчанию) или jpeg
canvas.destroy(): void                       // освободить нативную поверхность + её кэшированную 2D-текстуру
Note

после destroy() сам canvas и любой спрайт/материал, всё ещё сэмплящий его текстуру, недействительны. Текстура, запечённая в 3D-движок, освобождается при разрушении этого движка, а не здесь.

reset() и resize()

TypeScript
canvas.reset(): this           // сбросить записанный рисунок, чтобы начать новый
canvas.resize(w, h): this      // изменить логический размер; вступает в силу при следующем запекании
Note

reset() очищает и записанное состояние — font, fillStyle, textAlign, каждая заданная тобой настройка исчезает из следующего запекания, которое откатывается к значениям по умолчанию. Геттеры свойств всё ещё сообщают старые значения (и measureText всё ещё измеряет старым шрифтом), из-за чего это легко упустить. Переустанавливай font и компанию после каждого reset().

resize() не очищает записанные команды; сочетай с reset(), когда начинаешь заново в новом размере. Схема «измерить, потом изменить размер» (см. Обзор) работает, потому что до первого запекания ничего не растрируется.

Подводные камни

TypeScript
// ✗ перерисовка после reset() без переустановки состояния — текст запекается шрифтом по умолчанию 10px
c.reset(); c.fillText('99', 10, 20)
// ✓ состояние — часть записи; задай его снова
c.reset(); c.font = 'bold 24px Inter'; c.fillStyle = '#fff'; c.fillText('99', 10, 20)

// ✗ перерисовка в расчёте, что живые потребители изменятся — спрайт держит старые пиксели
c.reset(); c.fillRect(0, 0, 40, 40)
// ✓ протолкни новые пиксели в каждую текстуру, произведённую этим canvas
c.reset(); c.fillRect(0, 0, 40, 40); c.update()

// ✗ задавать размер спрайта через pixelRatio — pixelRatio это чёткость, не размер
new Sprite({ texture: c, size: [c.width * c.pixelRatio, c.height * c.pixelRatio] })
// ✓ мировой размер по умолчанию уже равен логическому
new Sprite({ texture: c })

Смотрите также

  • Sprite — Canvas как текстура спрайта.
  • Соглашения — логические px, строки цветов.