Пиксель-арт в браузере без размытия: image-rendering, целочисленное масштабирование и devicePixelRatio

Коротко

Пиксель-арт размывается, когда браузер увеличивает картинку со сглаживанием или на дробный коэффициент. Чтобы пиксели остались квадратными, нужны три вещи: imageSmoothingEnabled = false в контексте Canvas2D (заново после каждого изменения размера холста), image-rendering: pixelated на элементе canvas и целочисленный масштаб, посчитанный в физических пикселях с учётом devicePixelRatio. Ниже — формулы, готовая функция подгонки холста и таблица масштабов для популярных базовых разрешений.

Содержание

  1. Где именно появляется размытие
  2. Правило 1: отключить сглаживание в Canvas2D
  3. Правило 2: image-rendering для самого холста
  4. Правило 3: целочисленный масштаб
  5. Правило 4: считать масштаб в физических пикселях
  6. Рисовать в низком разрешении или сразу в большом
  7. Рабочий сценарий: игра 320×180 на любом экране
  8. Когда вам это не подойдёт

Где именно появляется размытие

Размытие появляется в трёх местах, и каждое нужно закрыть отдельно. Первое — рисование спрайта с увеличением через drawImage: по умолчанию контекст сглаживает картинку. Второе — растягивание всего холста через CSS: если размер элемента на странице больше его внутреннего растра, браузер масштабирует его по правилу image-rendering, а значение по умолчанию auto оставляет алгоритм на усмотрение браузера (MDN приводит пример: Gecko использует билинейную интерполяцию). Третье — дробный коэффициент: при увеличении в 4,5 раза одни пиксели исходника становятся шириной 4 экранных, другие — 5, и даже без сглаживания картинка выглядит неровной.

Три источника размытия и что их чинит
ГдеЧто видноЧто чинит
drawImage с масштабомМягкие края у спрайтаctx.imageSmoothingEnabled = false
CSS-размер холста больше растраМыло на всём кадреimage-rendering: pixelated
Дробный масштаб (×4,5)Пиксели разной ширины, «дрожь» при движенииЦелый масштаб, посчитанный в физических пикселях

Правило 1: отключить сглаживание в Canvas2D

Сглаживание в Canvas2D отключается свойством контекста imageSmoothingEnabled. По MDN значение по умолчанию — true, и при увеличении картинки алгоритм по умолчанию размывает пиксели; для игр с пиксель-артом свойство рекомендуют ставить в false (MDN, проверено: ноябрь 2026).

Главная ловушка — сброс. По стандарту HTML установка размеров растра холста (свойства width и height) сначала сбрасывает контекст рисования в состояние по умолчанию, а imageSmoothingEnabled входит в это состояние (HTML Standard, раздел о canvas, проверено: ноябрь 2026). Поэтому код «один раз выключил при старте» ломается при первом же изменении размера окна.

function applyPixelSettings(ctx) {
  // вызывать после КАЖДОГО изменения canvas.width или canvas.height
  ctx.imageSmoothingEnabled = false;
}

canvas.width = 1920;   // контекст сброшен, сглаживание снова включено
canvas.height = 1080;
applyPixelSettings(ctx);

Правило 2: image-rendering для самого холста

Для элемента canvas, который растягивается стилями, нужно CSS-свойство image-rendering. MDN описывает два подходящих значения (проверено: ноябрь 2026):

  • crisp-edges — масштабирование алгоритмом вроде ближайшего соседа, без размытия и смешивания цветов; предназначено для пиксель-арта и штриховых рисунков.
  • pixelated — ближайший сосед до ближайшего целого кратного исходного размера, а оставшаяся часть до итогового размера догоняется плавной интерполяцией. Так сохраняется пиксельный вид без артефактов, когда итоговый размер не кратен исходному.
canvas.game {
  image-rendering: pixelated;
}

Свойство действует только тогда, когда картинка действительно масштабируется: если CSS-размер холста совпадает с размером его растра в физических пикселях, ничего не меняется. Это и есть идеальная цель — масштабировать самим, целым коэффициентом, а pixelated держать как страховку.

Правило 3: целочисленный масштаб

Целочисленный масштаб — это наибольшее целое число, при котором базовый кадр игры помещается в доступную область. Оставшееся место заполняется полями.

масштаб = max(1, floor(min(доступная_ширина / базовая_ширина,
                           доступная_высота / базовая_высота)))

итоговый размер = базовый размер × масштаб
поля            = (доступный размер − итоговый размер) / 2

Пример для базы 320×180 и экрана 1366×768: 1366 / 320 = 4,27, 768 / 180 = 4,27, масштаб 4, кадр 1280×720, поля 43 пикселя по бокам и 24 сверху и снизу.

Целые масштабы для базовых разрешений 16:9 (наш расчёт по формуле выше)
База1280×7201920×10802560×14403840×2160
320×180×4 без полей×6 без полей×8 без полей×12 без полей
384×216×3 (1152×648)×5 без полей×6 (2304×1296)×10 без полей
480×270×2 (960×540)×4 без полей×5 (2400×1350)×8 без полей
640×360×2 без полей×3 без полей×4 без полей×6 без полей

Из таблицы видно, почему базу 320×180 и 640×360 выбирают часто: они делят все четыре популярных разрешения без остатка. База 384×216 даёт больше игрового поля, но на 1280×720 и 2560×1440 оставляет поля.

Правило 4: считать масштаб в физических пикселях

Масштаб нужно считать в физических пикселях экрана, а не в CSS-пикселях. window.devicePixelRatio — отношение физических пикселей к CSS-пикселям: 1 соответствует классическому экрану 96 DPI, 2 — HiDPI/Retina; значение меняется при масштабировании страницы и при переносе окна на другой монитор (MDN, проверено: ноябрь 2026). На ноутбуках с масштабом системы 125% или 150% оно равно 1,25 или 1,5.

Почему это важно, видно на примере: окно 1100×650 CSS-пикселей, devicePixelRatio = 1,5, база 320×180.

Расчёт в CSS-пикселях (ошибка):
  floor(min(1100/320, 650/180)) = floor(min(3,44; 3,61)) = 3
  холст 960×540 CSS-пикселей = 1440×810 физических
  1440 / 320 = 4,5  → пиксели исходника шириной то 4, то 5 точек

Расчёт в физических пикселях (верно):
  доступно 1650×975 физических
  floor(min(1650/320, 975/180)) = floor(min(5,16; 5,42)) = 5
  растр холста 1600×900, CSS-размер 1600/1,5 × 900/1,5 ≈ 1066,67×600

Готовая функция подгонки, которая собирает все четыре правила:

const BASE_W = 320;
const BASE_H = 180;

function fitCanvas(canvas, ctx) {
  const dpr = window.devicePixelRatio || 1;
  const availW = Math.floor(window.innerWidth * dpr);
  const availH = Math.floor(window.innerHeight * dpr);

  const scale = Math.max(1, Math.floor(Math.min(availW / BASE_W, availH / BASE_H)));

  canvas.width = BASE_W * scale;            // растр в физических пикселях
  canvas.height = BASE_H * scale;
  canvas.style.width = (canvas.width / dpr) + "px";   // размер на странице
  canvas.style.height = (canvas.height / dpr) + "px";

  ctx.imageSmoothingEnabled = false;        // контекст только что сброшен
  return scale;
}

window.addEventListener("resize", () => fitCanvas(canvas, ctx));

Для отслеживания смены devicePixelRatio без изменения размера окна (перенос на другой монитор, масштаб страницы) MDN предлагает медиазапрос matchMedia("(resolution: " + dpr + "dppx)") с подпиской на событие change. CSS-размер холста при этом может быть дробным; проверяйте результат на реальных экранах с масштабом 125% и 150%, потому что округление дробных CSS-размеров до физических точек остаётся за браузером.

Рисовать в низком разрешении или сразу в большом

Есть два рабочих способа, и выбирать их нужно по тому, как движется камера. Первый — рисовать весь кадр во внеэкранный холст 320×180, а затем одним вызовом drawImage растягивать его на видимый холст в целое число раз. Второй — рисовать сразу на большом холсте, умножая координаты на масштаб и округляя их до целых.

Два способа вывода пиксель-арта
Буфер в базовом разрешенииРисование сразу в итоговом размере
Пиксели всегда одного размераДаТолько если координаты округлены и спрайты не вращаются
Плавная камера с шагом меньше игрового пикселяНет, камера «шагает» по 1 пикселю базыДа
Текст интерфейса в высоком разрешенииНужен отдельный слой поверхМожно рисовать на том же холсте
СтоимостьОдин лишний drawImage за кадрБез лишнего прохода

Правило для обоих способов одно: координаты спрайтов и камеры перед рисованием округляются (Math.round), иначе соседние кадры дают смещение на доли пикселя, и при imageSmoothingEnabled = false спрайт начинает «дрожать» на 1 пиксель при медленном движении.

Рабочий сценарий: игра 320×180 на любом экране

Задача: платформер с базой 320×180 должен выглядеть одинаково чётко на мониторе 1920×1080, ноутбуке 1366×768 и ноутбуке с масштабом системы 150%. Проверяемый результат — на скриншоте, увеличенном в 4 раза, каждый пиксель исходника состоит из одинакового квадрата экранных точек.

Чек-лист

  1. Выберите базу по таблице выше: 320×180, если важны все четыре разрешения без полей.
  2. Сделайте фон страницы того же цвета, что и поля, чтобы поля читались как рамка, а не как ошибка.
  3. Подключите fitCanvas при старте, на resize и на смену devicePixelRatio.
  4. Добавьте image-rendering: pixelated холсту как страховку от дробного остатка.
  5. Округляйте координаты спрайтов и камеры перед каждым drawImage.
  6. Проверьте три экрана: devicePixelRatio 1, 1,25 и 1,5 (в Chrome DevTools это можно эмулировать в режиме устройства) и масштаб страницы 110%.
  7. Проверьте шрифт интерфейса: пиксельный шрифт рисуйте тем же масштабом, обычный — отдельным слоем в полном разрешении.

Когда вам это не подойдёт

Целочисленное масштабирование нужно только для пиксель-арта и только там, где поля по краям приемлемы. Случаи, когда лучше выбрать другое:

  • Рисованная HD-графика или вектор. Сглаживание таким картинкам полезно; отключение даст ступенчатые края.
  • Спрайты вращаются или плавно масштабируются. Ближайший сосед при повороте даёт рваные края; для таких объектов оставляют сглаживание или готовят повёрнутые кадры заранее.
  • Портретный телефон. При ширине 390 CSS-пикселей база 320×180 помещается только в масштабе ×1 (с учётом devicePixelRatio — немного больше), и игра занимает узкую полосу. Для телефона нужна отдельная вертикальная база или дробный масштаб с pixelated.
  • Отрисовка через WebGL или WebGPU. CSS-правила те же, но сглаживание текстур задаётся фильтром самой текстуры (режим ближайшего соседа), а не свойством Canvas2D.

Мы разрабатываем J2DRU — браузерный 2D игровой движок; он в разработке и публично пока не открыт, заявку можно оставить на странице раннего доступа (подача заявки не гарантирует приглашение).

Источники (проверено: ноябрь 2026): MDN — image-rendering CSS property; MDN — CanvasRenderingContext2D: imageSmoothingEnabled property; MDN — Window: devicePixelRatio property; WHATWG HTML Standard — раздел The canvas element (установка размеров растра и состояние контекста). Таблица масштабов и примеры — наш расчёт по формулам статьи.

Читайте также:

Частые вопросы

Почему мой пиксель-арт размыт, хотя картинка чёткая?

По умолчанию браузер сглаживает изображение при увеличении: в Canvas2D свойство imageSmoothingEnabled равно true, а CSS-свойство image-rendering по умолчанию auto. Оба нужно переключить на режим ближайшего соседа.

Чем pixelated отличается от crisp-edges?

По описанию MDN, crisp-edges масштабирует методом ближайшего соседа без сглаживания, а pixelated увеличивает картинку ближайшим соседом до ближайшего целого кратного и только остаток догоняет сглаживанием. Для пиксель-арта чаще выбирают pixelated.

Почему после изменения размера холста сглаживание снова включилось?

По стандарту HTML изменение width или height элемента canvas сбрасывает контекст рисования в состояние по умолчанию, а imageSmoothingEnabled входит в это состояние. Выставляйте false заново после каждого изменения размера.

Как считать масштаб на ноутбуке с масштабом экрана 125% или 150%?

Считайте целый масштаб в физических пикселях: доступную ширину в CSS-пикселях умножьте на devicePixelRatio, разделите на базовую ширину игры и округлите вниз. Размер холста в CSS-пикселях получается делением обратно на devicePixelRatio.