Canvas и Bitmap
2D-поверхность для рисования, запекающаяся в текстуру — способ поместить текст и произвольную векторную графику в спрайты, 3D-материалы и UI-изображения («canvas — это просто текстура»). API повторяет 2D-контекст браузера, но записывает команды вместо немедленной отрисовки: весь буфер растрируется один раз за запекание, на нативном 2D-стеке платформы.
Обзор
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 — валидный источник текстуры где угодноСоздание
new Canvas(width, height, opts?: {
pixelRatio?: number // множитель device-пикселей; по умолчанию 1
})Всё рисование задаётся в логических единицах. width/height — логический размер; запечённая
текстура имеет width × pixelRatio на height × pixelRatio device-пикселей. pixelRatio влияет
только на чёткость, никогда на экранный размер — Sprite на основе Canvas по умолчанию берёт
логический размер как мировой. Используй pixelRatio: 2 (или device.pixelRatio) для чёткого
текста на retina-экранах.
цвета и шрифты — это CSS-строки, разрешаемые платформой. Всё рисование выполняй после
await registerFont(...) для любого своего шрифта — незагруженный шрифт измеряется неверно.
Состояние рисования
Свойства чтения/записи, как в браузере. Установка одного записывает операцию состояния и запоминает значение, чтобы геттер вернул его обратно.
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, поэтому вызовы чейнятся.
c.save(): this // положить состояние (трансформ + состояние рисования)
c.restore(): this // снять его
c.translate(x, y): this
c.scale(sx, sy): this // трансформ — не связан с pixelRatio
c.rotate(rad): this // радианыПути
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Сахар для прямоугольников и текста
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 } // измеряет в ТЕКУЩЕМ шрифте, логические pxmeasureText — единственный синхронный вызов к платформе; всё остальное только записывает.
Изображения: drawImage и Bitmap
drawImage копирует Bitmap — снимок, сделанный через toBitmap(). Он не принимает Texture2D
или другой Canvas.
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()).
canvas.toBitmap(): Bitmap // неизменяемая копия текущих пикселей, независимая от canvasbmp.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) на постоянно растущем рисунке:
const snap = canvas.toBitmap()
canvas.reset()
canvas.drawImage(snap, 0, 0) // одна операция заменяет всю историю; продолжай рисовать поверхЗапекание в текстуры
canvas.texture(): Texture2D // запечь + вернуть 2D-текстуру (создаётся один раз, затем кэшируется)
canvas.update(): this // перерастрировать и перезалить в каждую текстуру, произведённую этим canvastexture() сам вызываешь редко — присваивание canvas туда, где ждут текстуру
(new Sprite({ texture: c }), UIImage, карта Material), запекает его. После перерисовки
динамического canvas (reset() + новые команды) вызови update(), чтобы протолкнуть новые пиксели
всем его потребителям.
canvas.toFile(name?, type?): Promise<File> // закодировать текущие пиксели; png (по умолчанию) или jpeg
canvas.destroy(): void // освободить нативную поверхность + её кэшированную 2D-текстурупосле destroy() сам canvas и любой спрайт/материал, всё ещё сэмплящий его текстуру,
недействительны. Текстура, запечённая в 3D-движок, освобождается при разрушении этого движка, а не
здесь.
reset() и resize()
canvas.reset(): this // сбросить записанный рисунок, чтобы начать новый
canvas.resize(w, h): this // изменить логический размер; вступает в силу при следующем запеканииreset() очищает и записанное состояние — font, fillStyle, textAlign, каждая заданная тобой
настройка исчезает из следующего запекания, которое откатывается к значениям по умолчанию. Геттеры
свойств всё ещё сообщают старые значения (и measureText всё ещё измеряет старым шрифтом), из-за
чего это легко упустить. Переустанавливай font и компанию после каждого reset().
resize() не очищает записанные команды; сочетай с reset(), когда начинаешь заново в новом
размере. Схема «измерить, потом изменить размер» (см. Обзор) работает, потому что до первого
запекания ничего не растрируется.
Подводные камни
// ✗ перерисовка после 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, строки цветов.