Oblina Labs

Canvas: 2D графіка

Як програмно малювати фігури та створювати анімацію у браузері

Більшість елементів вебсторінки браузер створює з HTML і CSS. Але іноді потрібно малювати графіку програмно: будувати фігури, змінювати зображення в кожному кадрі або створювати інтерактивні візуалізації. Для таких завдань у браузері є елемент <canvas>.

У цій статті ми розберемо, як працює Canvas і його двовимірна система координат. За допомогою TypeScript створимо анімацію, у якій із центра Canvas з’являються лінії з випадковими кінцевими точками.

Ми навчимося малювати лінії та кола, зберігати дані про намальовані об’єкти, створювати цикл анімації через requestAnimationFrame() і керувати ним за допомогою кнопок запуску, паузи та очищення.

Спробуйте самі

Натисніть Запуск, щоб почати анімацію. Із центра Canvas поступово з’являтимуться лінії з випадковими кінцевими точками.

Використовуйте кнопку Пауза, щоб зупинити появу нових ліній, не очищаючи поточне зображення. Кнопка Очистити видаляє всі лінії та повертає візуалізацію до початкового стану.

Інтерактивний приклад

Генератор випадкових ліній

Запустіть анімацію, поставте її на паузу або очистьте Canvas.

Одночасно на Canvas зберігається не більше 50 ліній. Коли з’являється нова лінія, найстаріша зникає.

Canvas і 2D-контекст

<canvas> — це HTML-елемент для програмного малювання графіки. Сам по собі він створює лише порожню прямокутну область:

<canvas id="canvas-demo" width="400" height="300"></canvas>

Атрибути width і height визначають внутрішній розмір області малювання. У нашому прикладі Canvas має ширину 400 і висоту 300 пікселів.

Щоб малювати всередині цієї області, потрібно знайти Canvas і отримати його 2D-контекст:

const canvas = document.getElementById("canvas-demo");

if (!(canvas instanceof HTMLCanvasElement)) {
  throw new Error("Canvas не знайдено");
}

const context = canvas.getContext("2d");

if (!context) {
  throw new Error("Не вдалося отримати 2D-контекст");
}

canvas можна уявити як поверхню, а context — як набір інструментів для малювання. Через контекст ми створюватимемо контури, задаватимемо кольори, очищатимемо зображення та малюватимемо нові кадри.

Перевірки потрібні тому, що елемент або 2D-контекст можуть бути не знайдені. Після перевірок TypeScript знає, що ми працюємо саме з HTMLCanvasElement і CanvasRenderingContext2D.

Canvas зберігає намальовані пікселі, а не окремі об’єкти. Він не пам’ятає, де знаходиться конкретна лінія або коло. Тому для зміни зображення ми зберігатимемо дані окремо, очищатимемо Canvas і малюватимемо актуальний стан заново.

Координати та малювання лінії

Початок координат Canvas розташований у верхньому лівому куті — у точці (0, 0). Координата x збільшується вправо, а y — вниз.

Усі лінії нашої анімації починаються в центрі Canvas. Щоб знайти його, ділимо ширину та висоту навпіл:

const centerX = canvas.width / 2;
const centerY = canvas.height / 2;

Для Canvas розміром 400 × 300 центр має координати (200, 150). Значення обчислюються, а не записуються вручну, тому залишаться правильними після зміни внутрішнього розміру Canvas.

function drawLine(
  canvas: HTMLCanvasElement,
  context: CanvasRenderingContext2D,
  line: Line,
) {
  const centerX = canvas.width / 2;
  const centerY = canvas.height / 2;

  context.beginPath();
  context.moveTo(centerX, centerY);
  context.lineTo(line.endX, line.endY);

  context.strokeStyle = line.color;
  context.lineWidth = 1.5;
  context.lineCap = "round";
  context.stroke();
}

beginPath() починає новий контур, moveTo() переміщує його початкову точку в центр, а lineTo() додає відрізок до кінцевих координат. Метод stroke() відображає готовий контур.

Перед малюванням ми задаємо колір, товщину та округлені кінці лінії через strokeStyle, lineWidth і lineCap. Функція отримує готовий об’єкт line, тому відповідає лише за його відображення.

Випадкові координати та кольори

Усі лінії починаються в центрі, але закінчуються в різних точках. Math.random() повертає випадкове число від 0 до 1. Множення на ширину або висоту перетворює його на координату в межах Canvas.

const lineColors = [
  "#5f4bb6",
  "#c94f6d",
  "#d0782a",
  "#26877b",
  "#3d6fa3",
  "#718c24",
];

const colorIndex = Math.floor(Math.random() * lineColors.length);

const line: Line = {
  endX: Math.random() * canvas.width,
  endY: Math.random() * canvas.height,
  color: lineColors[colorIndex],
};

Колір вибирається за випадковим індексом із підготовленої палітри. Координати й колір зберігаються разом в об’єкті Line, тому після перемальовування лінія не змінює свій вигляд.

Цикл анімації

Кадр — це одне зображення, показане на екрані. requestAnimationFrame() просить браузер викликати передану функцію перед наступним перемальовуванням.

function animate() {
  if (!isRunning) {
    return;
  }

  const colorIndex = Math.floor(Math.random() * lineColors.length);

  const line: Line = {
    endX: Math.random() * canvas.width,
    endY: Math.random() * canvas.height,
    color: lineColors[colorIndex],
  };

  lines.push(line);

  if (lines.length > 50) {
    lines.shift();
  }

  drawLines(canvas, context, lines);
  requestAnimationFrame(animate);
}

У кожному кадрі функція створює випадкову лінію, додає її до масиву, перемальовує Canvas і запитує наступний кадр.

У requestAnimationFrame() передається animate без круглих дужок. Так браузер отримує саму функцію як callback і викликає її пізніше. Запис animate() запустив би її негайно.

Якщо isRunning дорівнює false, функція завершується через return і не запитує наступний кадр.

Зберігаємо та перемальовуємо лінії

Canvas зберігає лише пікселі, тому дані про лінії потрібно тримати окремо. Тип Line описує кінцеві координати й колір, а масивlines зберігає поточний стан візуалізації.

type Line = {
  endX: number;
  endY: number;
  color: string;
};

const lines: Line[] = [];

Під час кожного кадру нова лінія додається в кінець масиву через push(). Якщо елементів стало більше 50, shift() видаляє найстарішу лінію з початку масиву.

Зміна масиву не прибирає вже намальовані пікселі. Тому після оновлення стану ми очищаємо весь Canvas і заново малюємо актуальні лінії:

function drawLines(
  canvas: HTMLCanvasElement,
  context: CanvasRenderingContext2D,
  lines: Line[],
) {
  context.clearRect(0, 0, canvas.width, canvas.height);

  for (let i = 0; i < lines.length; i++) {
    drawLine(canvas, context, lines[i]);
    drawPoint(context, lines[i]);
  }
}

clearRect() очищає всю область малювання. Після цього цикл проходить по масиву й відображає кожну лінію та точку на її кінці.

Таким чином, масив відповідає за те, що має бути показано, а Canvas — за те, як цей стан виглядає на екрані.

Точки на кінцях ліній

Кінцеву точку кожної лінії позначаємо невеликим колом. Для його побудови використовується метод arc().

function drawPoint(
  context: CanvasRenderingContext2D,
  line: Line,
) {
  context.beginPath();
  context.arc(line.endX, line.endY, 3, 0, 2 * Math.PI);
  context.fillStyle = line.color;
  context.fill();
}

Перші два аргументи arc() задають центр кола, третій — його радіус. Значення від 0 до Math.PI * 2 описують повний оберт, а fill() зафарбовує побудовану фігуру.

Точка використовує ті самі координати й колір, що й лінія, тому переміщується та видаляється разом із нею.

Керування анімацією

Змінна isRunning визначає, чи потрібно створювати наступний кадр. Кнопка Запуск змінює її на true і викликаєanimate(), а кнопка Пауза повертає значенняfalse.

Пауза не очищає Canvas і не змінює масив, тому після повторного запуску анімація продовжується з поточного стану.

Кнопка Очистити зупиняє анімацію, очищає масив через lines.length = 0 і видаляє зображення за допомогою clearRect().

Властивість disabled синхронізує кнопки зі станом програми: під час роботи запуск недоступний, під час паузи недоступна пауза, а порожній Canvas не потрібно очищати повторно.

Коли використовувати Canvas

Canvas підходить для графіки, яку потрібно часто перемальовувати: анімацій, ігор, графічних редакторів, візуалізацій даних та ефектів із великою кількістю об’єктів.

Звичайний вміст сторінки — текст, кнопки, форми й навігацію — краще залишати в HTML. HTML-елементи мають семантику, підтримують клавіатурну навігацію та доступні допоміжним технологіям. Саме тому кнопки нашого playground розташовані поза Canvas.

Для іконок, схем і графіків із невеликою кількістю окремих фігур часто зручнішим буде SVG. Його елементи можна знаходити в DOM, стилізувати через CSS і обробляти окремо.

Canvas варто обирати тоді, коли важливіше швидко перемальовувати все зображення, ніж взаємодіяти з кожною фігурою як з окремим елементом.

Висновок

За допомогою Canvas і CanvasRenderingContext2D ми створили анімацію з випадковими лініями та точками. Дані про них зберігаються в масиві, а під час кожного кадру Canvas очищається й відображає актуальний стан.

Цей приклад поєднує систему координат, callback-функції, requestAnimationFrame() і керування станом. Такий самий принцип — оновити дані та перемалювати кадр — можна використовувати у складніших візуалізаціях, графічних редакторах та іграх.