Робота з Canvas

Canvas - контейнер для програмної реалізації графіки на веб-сторінці.

На відміну від формату SVG графіка на canvas виконується лише програмно, за допомогою JS.

На canvas можна реалізувати як 2D, так і 3D-графіку, можна включити підтримку OpenGL (WebGL) і навіть створювати ігри.

Основні примітиви

Вставте на веб-сторінку тег <canvas>:

<canvas></canvas>

Всередину можна вставити контент для старих браузерів (хоча наразі це навряд чи актуально):

canvas { width: 400px; height: 300px; border: 1px solid #aaa;}

<canvas id="img1">
  <img src="canvas_error.png" alt="Canvas error">
  Ваш браузер не підтримує роботу з динамічною графікою.
</canvas>

<canvas id="img2">
  <!-- Статична картинка, графік, гіфка, відео, SVG ... -->
</canvas>

Для початку малювання потрібно взяти контекст полотна, тобто, вказати - по чому ми малюємо. Полотно - контейнер для малювання всередині тега canvas.

По-замовчуванню розмір полотна 300 x 150 px, і малюнок на ньому буде маштабуватися до розміру canvas. Тому бажано задати розмір полотна.

Цим способом довільне малювання по віконному додатку відбувається у більшості мов програмування: C++, Java, Delphi і т.п. (використовується API операційної системи).

let canvas = document.querySelector('canvas');
let ctx = canvas.getContext('2d');
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;

Раджу використовувати загальноприйняту назву змінної ctx (скорочення від context).

Якщо потрібна підтримка старих браузерів - можна виконати перевірку:

let ctx = null;
if (canvas.getContext){
  ctx = canvas.getContext('2d');
} else {
  // браузер не підтримує canvas
}

Далі - алгоритм малювання примітивів.

Уявіть себе художником. Алгоритм дій:
1. берете в руку червоний олівець;
2. зафарбовуєте фон;
3. берете в руку синій олівець;
4. ставите його в точку [3см, 5см];
5. малюєте лінію до точки [8см, 7см];
6. ...

Подібним чином працює і алгоритм малювання по canvas:

ctx.fillStyle = "red"; // color
ctx.fillRect(10, 10, 200, 150); // x, y, width, height
ctx.fillStyle = "rgba(0,200,0,0.5)";
ctx.ellipse(140, 100, 50, 80);
ctx.fill();
ctx.strokeRect(50, 50, 200, 150); // x, y, width, height
ctx.clearRect(80, 80, 30, 20); // x, y, width, height
ctx.fillStyle = "rgb(128,128,256)";
ctx.beginPath();
ctx.moveTo(140, 120);
ctx.lineTo(260, 160);
ctx.lineTo(160, 200);
ctx.closePath();
ctx.fill();
ctx.arc(50, 50, 40, 0, Math.PI * 2); // cx, cy, r, aStart, aFinish
ctx.arc(50, 50, 40, 0, (Math.PI / 180) * 45, true);
// cx, cy, r, aStart, aFinish, anticlockwise
// ctx.rect(x, y, width, height);
// ctx.quadraticCurveTo(x1, y1, x, y);
// ctx.bezierCurveTo(x1, y1, x2, y2, x, y);
// ctx.arcTo()

Пікселі і проценти

В багатьох випадках зручніше використовувати не пікселі, а проценти: потрібно для адаптивності, маштабування і т.п.

Як правило, за 100% беруть більший розмір блока, екрана.

Примітиви

Задаємо колір лінії та колір зафарбовування:

ctx.strokeStyle = "green";
ctx.fillStyle = "red";

// інші формати кольору:
ctx.fillStyle = "#f00";
ctx.fillStyle = "rgb(255,0,0)";
ctx.fillStyle = "rgba(255,0,0,1)";

Задаємо тип ліній:

ctx.lineWidth = 10;
ctx.lineCap = 'round';
ctx.lineJoin = 'miter';
ctx.setLineDash([5, 15, 3, 10]);

Прямокутники (рамка, зафарбований, очистка області):

ctx.strokeRect(50,50,50,50);
ctx.fillRect(25,25,100,100);
ctx.clearRect(45,45,60,60);

Текст:

ctx.font = "italic 30px Arial";
ctx.fillStyle = "red";
ctx.fillText("Звичайний текст", 20, 50);
ctx.strokeStyle = "green";
ctx.strokeText("Текст у вигляді обводки", 20, 100);

Вставимо растрове зображення:

var cat = new Image();
cat.addEventListener("load", function(){
  ctx.drawImage(cat, 50, 50); // image, x, y
  ctx.drawImage(cat, 150, 150, 200, 120); // image, x, y, scaleX, scaleY
}, false);
cat.src = 'images/cat.jpg';

Path.

ctx.beginPath();
ctx.moveTo(30, 20);
ctx.lineTo(100, 80);
ctx.lineTo(150, 30);
ctx.closePath();
ctx.strokeStyle = "red";
ctx.stroke();

Path як об'єкт.

var p = new Path2D("M 15 30 h 50 v 80 h -20 z");
var r = Path2D(p); // copy from previous object p
var rec = new Path2D();
rec.rect(10, 10, 50, 50);
var crc = new Path2D();
// arc(x, y, radius, startAngle, endAngle, anticlockwise)
crc.arc(100, 35, 25, 0, Math.PI * 2);
ctx.stroke(rec);
ctx.strokeStyle = "rgb(100,200,255)";
ctx.stroke(p);
// ctx.arcTo(x1, y1, x2, y2, radius)
// ctx.quadraticCurveTo(x1, y1, x2, y2)
// ctx.bezierCurveTo(x1, y1, x2, y2, x3, y3)

Стек станів, збереження та відновлення станів.

Під час малювання можна використовувати стек станів - зберігати та відновлювати стани (кольори, прозорість, тип ліній і т.п.).

Кожне збереження записує стан у стек, кожне відновлення стану зчитує стан з кінця стеку (останній запис).

function sets(fill, stroke, width, alpha){
  ctx.fillStyle = fill;
  ctx.strokeStyle = stroke;
  ctx.lineWidth = width;
  ctx.globalAlpha = alpha;
}
function drawRect(x){
  ctx.beginPath();
  ctx.rect(x, x / 2, 100, 100);
  ctx.closePath();
  ctx.fill();
  ctx.stroke();
}
ctx.fillStyle = "red";
ctx.save();
drawRect(10);
sets('green', 'navy', 3, 0.3);
ctx.save();
drawRect(60);
sets('blue', 'orange', 10, 0.8);
drawRect(110);
ctx.restore();
drawRect(160);
ctx.restore();
drawRect(210);

Трансформації

Функція translate умовно зсуває координати полотна на задані значення.

Прямокутник буде намальовано не в нульових координатах, а в [100, 100]:

ctx.translate(100, 100); // x, y
ctx.fillRect(0, 0, 50, 50);

Обертання відбувається відносно нульової точки координат, обертається також все полотно:

ctx.fillStyle = "blue";
ctx.fillRect(30, 20, 80, 50);
ctx.rotate((Math.PI/180)*30);
ctx.fillStyle = "red";
ctx.fillRect(30, 20, 80, 50);

Щоб фігура прокрутилася довкола свого центру - зсунемо канву на початок координат прямокутника і ще на половину його розмірів:

ctx.fillStyle = "blue";
ctx.fillRect(30, 20, 80, 50);
ctx.translate(70, 45); // 30 + 80/2, 20 + 50/2
ctx.rotate((Math.PI/180)*30);
ctx.fillStyle = "red";
ctx.fillRect(30, 20, 80, 50);
ctx.translate(-70, -45);

В кінці оберту не забудьте повернути канву на початкове місце.

Приклад маштабування. Маштабуванням можна віддзеркалювати об'єкти по горизонталі, вертикалі чи одночасно по обом вісям, задаючи від'ємні значення.

ctx.fillStyle = "blue";
ctx.fillRect(10, 10, 50, 50);
ctx.fillStyle = "red";
ctx.globalAlpha = 0.5;
ctx.scale(2, 3);
ctx.fillRect(10, 10, 50, 50);
ctx.scale(-1, -1);
ctx.fillText("Hello", -100, -100);

Градієнтна заливка

Градієнт створюється у вигляді окремого об'єкта. До створеного об'єкта додаються стоп-точки.

На відміну від SVG-формату, canvas-градієнт створюється на саме полотно, а не на фігури:

var grad = ctx.createLinearGradient(20, 20, 150, 100);
grad.addColorStop(0, 'red');
grad.addColorStop(0.5, 'green');
grad.addColorStop(1, '#00f');
ctx.fillStyle = grad;
ctx.fillRect(40, 40, 100, 100);

Зручно буде створити функцію для створення градієнтів:

ctx.linearGrad = function(x1, y1, x2, y2, stops){
  var grad = this.createLinearGradient(x1, y1, x2, y2);
  for (var i = 0; i < stops.length; i++){
    grad.addColorStop(stops[i][0], stops[i],[1]);
  }
  this.fillStyle = grad;
}
ctx.linearGrad(20, 20, 150, 100, [
  [0, 'red'],
  [0.5, 'green'],
  [1, '#00f']
]);
ctx.fillRect(40, 40, 100, 100);

Радіальний градієнт задається дещо складніше:

// context.createRadialGradient(x0, y0, r0, x1, y1, r1);
var rgrad = ctx.createRadialGradient(20, 20, 10, 20, 20, 100);
rgrad.addColorStop(0, 'yellow');
rgrad.addColorStop(1, 'blue');
ctx.fillStyle = rgrad;
ctx.fillRect(0, 0, 300, 200);

Анімація

На відміну від CSS- та SVG-анімації, анімація canvas відбувається покадрово.

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

Проміжок часу між кадрами можна задати вручну за допомогою таймінгових функцій, або задати автоматичне перемальовування за допомогою функції window.requestAnimationFrame(callback).

В останньому випадку розрахунки координат елементів зазвичай розраховують в залежності від поточного часу.

Анімація реалізується досить складними алгоритмами, приклад алгоритмів можна дослідити на сторінці https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Basic_animations.

Практика

Бізнес-графіка:

  1. Згенеруйте 7 випадкових чисел в межах [5..100].
    Напишіть юніт-тест, щоб визначити, чи не генеруються числа менше 5 чи більше 100.
    За даними точками побудуйте графік на canvas.
    Графік повинен бути респонсивним.
  2. Додайте hover-ефект: при наведенні на точку графіка відобразіть над нею числове значення.
  3. Зробіть анімацію: при завантаженні сторінки лінія графіка на 0, за 1 секунду точки підіймаються вгору на свої місця.
  4. Анімація повинна спрацьовувати при завантаженні сторінки, але якщо графік не повністю видно на екрані - то при проскролі до нього.
  5. Дано 5 значень: [8, 14, 17, 22, 31]. Побудуйте кругову діаграму.
  6. Побудуйте кільцеву діаграму використовуючи дані з попереднього завдання.

Ефекти, розваги:

  1. Використайте невелике зображення котика.
  2. При наведенні на нього мишки застосуйте до нього "фільтр портретів Fallout".
  3. При кліку на кнопку завантажте ще одне зображення такого ж розміру і об'єднайте два зображення.
  4. Зробіть, щоб перехід від одного зображення до другого був плавний, зліва направо.
  5. При кліку на котика зробіть анімацію вибуха за допомогою спрайта.
  6. Намалюйте декілька примітивів довільної форми. При наведенні на примітив у нього повинен з'являтися контур, а курсор повинен приймати вигляд "pointer".
  7. Намалюйте "дорогу" - декілька послідовних ліній безьє. Намалюйте машинку примітивами і зробіть, щоб вона їхала по дорозі (колеса мають торкатися дороги). Опціонально: на спусках машинка має пришвидшуватися, на підйомах - сповільнюаватися.
  8. Зробіть щоб довкола мишки весь час кружляв рій точок з ефектом інерції.