Эта статья — сборник мелких проблем, с которыми вы можете столкнуться при работе с WebGL, но которые слишком малы для отдельной статьи.
В браузере есть по сути две функции для создания скриншота:
Старая — canvas.toDataURL
и новая, более удобная — canvas.toBlob
Кажется, что сделать скриншот просто — достаточно добавить такой код:
<canvas id="c"></canvas>
+<button id="screenshot" type="button">Сохранить...</button>
const elem = document.querySelector('#screenshot');
elem.addEventListener('click', () => {
canvas.toBlob((blob) => {
saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`);
});
});
const saveBlob = (function() {
const a = document.createElement('a');
document.body.appendChild(a);
a.style.display = 'none';
return function saveData(blob, fileName) {
const url = window.URL.createObjectURL(blob);
a.href = url;
a.download = fileName;
a.click();
};
}());
Вот пример из статьи про анимацию с этим кодом и немного CSS для кнопки:
Когда я попробовал — получил вот такой скриншот:
Да, это просто пустое изображение.
Возможно, у вас сработает (зависит от браузера/ОС), но обычно не работает.
Проблема в том, что для производительности и совместимости браузер по умолчанию очищает буфер рисования WebGL-канваса после отрисовки.
Есть три решения:
вызвать функцию рендера прямо перед захватом
Код, который мы использовали, был функцией drawScene
. Лучше сделать так, чтобы эта функция не меняла состояние, и тогда можно вызывать её для захвата.
elem.addEventListener('click', () => {
+ drawScene();
canvas.toBlob((blob) => {
saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`);
});
});
вызвать код захвата внутри рендер-цикла
В этом случае мы просто ставим флаг, что хотим сделать захват, а в рендер-цикле реально делаем захват:
let needCapture = false;
elem.addEventListener('click', () => {
needCapture = true;
});
а в рендер-цикле (например, в drawScene
), после отрисовки:
function drawScene(time) {
...
+ if (needCapture) {
+ needCapture = false;
+ canvas.toBlob((blob) => {
+ saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`);
+ });
+ }
...
}
Установить preserveDrawingBuffer: true
при создании WebGL-контекста
const gl = someCanvas.getContext('webgl2', {preserveDrawingBuffer: true});
Это заставит WebGL не очищать канвас после композитинга с остальной страницей, но может помешать некоторым оптимизациям.
Я бы выбрал вариант №1. Для этого лучше разделить код, который обновляет состояние, и код, который рисует.
var then = 0;
- requestAnimationFrame(drawScene);
+ requestAnimationFrame(renderLoop);
+ function renderLoop(now) {
+ // Переводим в секунды
+ now *= 0.001;
+ // Разница со временем предыдущего кадра
+ var deltaTime = now - then;
+ // Запоминаем время
+ then = now;
+ // Каждый кадр увеличиваем вращение
+ rotation[1] += rotationSpeed * deltaTime;
+ drawScene();
+ // Следующий кадр
+ requestAnimationFrame(renderLoop);
+ }
// Рисуем сцену
+ function drawScene() {
- function drawScene(now) {
- // Переводим в секунды
- now *= 0.001;
- // Разница со временем предыдущего кадра
- var deltaTime = now - then;
- // Запоминаем время
- then = now;
-
- // Каждый кадр увеличиваем вращение
- rotation[1] += rotationSpeed * deltaTime;
webglUtils.resizeCanvasToDisplaySize(gl.canvas);
...
- // Следующий кадр
- requestAnimationFrame(drawScene);
}
Теперь можно просто вызвать drawScene
перед захватом:
elem.addEventListener('click', () => {
+ drawScene();
canvas.toBlob((blob) => {
saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`);
});
});
Теперь всё должно работать.
Если посмотреть на полученное изображение, фон будет прозрачным. Подробнее — в этой статье.
Допустим, вы хотите дать пользователю рисовать анимированным объектом. Нужно передать preserveDrawingBuffer: true
при создании WebGL-контекста. Это не даст браузеру очищать канвас.
Возьмём последний пример из статьи про анимацию:
var canvas = document.querySelector("#canvas");
-var gl = canvas.getContext("webgl2");
+var gl = canvas.getContext("webgl2", {preserveDrawingBuffer: true});
и изменим вызов gl.clear
, чтобы очищался только depth-буфер:
-// Очищаем канвас.
-gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+// Очищаем только depth-буфер.
+gl.clear(gl.DEPTH_BUFFER_BIT);
Обратите внимание: если делать полноценную программу для рисования, это не решение, так как браузер всё равно очистит канвас при изменении его размера. Мы меняем размер в зависимости от размера отображения, а он меняется при изменении окна, загрузке файла (например, в другой вкладке), появлении статус-бара, повороте телефона и т.д.
Если делать настоящее приложение для рисования — рендерьте в текстуру.
Если вы делаете полноэкранное WebGL-приложение, то всё просто. Но часто хочется, чтобы канвас был частью страницы, и чтобы при клике по нему он принимал ввод с клавиатуры. По умолчанию канвас не получает ввод с клавиатуры. Чтобы это исправить, задайте ему tabindex
0 или больше. Например:
<canvas tabindex="0"></canvas>
Появляется новая проблема: любой элемент с tabindex
получает обводку при фокусе. Чтобы убрать её, добавьте CSS:
canvas:focus {
outline:none;
}
Для примера — три канваса:
<canvas id="c1"></canvas>
<canvas id="c2" tabindex="0"></canvas>
<canvas id="c3" tabindex="1"></canvas>
и CSS только для последнего:
#c3:focus {
outline: none;
}
Навесим одинаковые обработчики на все:
document.querySelectorAll('canvas').forEach((canvas) => {
const ctx = canvas.getContext('2d');
function draw(str) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(str, canvas.width / 2, canvas.height / 2);
}
draw(canvas.id);
canvas.addEventListener('focus', () => {
draw('has focus press a key');
});
canvas.addEventListener('blur', () => {
draw('lost focus');
});
canvas.addEventListener('keydown', (e) => {
draw(`keyCode: ${e.keyCode}`);
});
});
Обратите внимание: первый канвас не может принимать ввод с клавиатуры. Второй может, но получает обводку. У третьего применены оба решения.
Часто спрашивают, как сделать WebGL-анимацию фоном веб-страницы.
Есть два очевидных способа:
position
канваса как fixed
:#canvas {
position: fixed;
left: 0;
top: 0;
z-index: -1;
...
}
и установить z-index
в -1.
Небольшой недостаток этого решения: ваш JavaScript должен интегрироваться со страницей, и если у вас сложная страница, нужно убедиться, что JavaScript в WebGL-коде не конфликтует с JavaScript, который делает другие вещи на странице.
iframe
Это решение используется на главной странице этого сайта.
В вашей веб-странице просто вставьте iframe, например:
<iframe id="background" src="background.html"></iframe>
<div>
Ваш контент здесь.
</div>
Затем стилизуйте iframe, чтобы он заполнял окно и был на фоне — в основном тот же код, что мы использовали выше для канваса, но также нужно установить border
в none
, так как iframe по умолчанию имеют границу:
#background {
position: fixed;
width: 100vw;
height: 100vh;
left: 0;
top: 0;
z-index: -1;
border: none;
pointer-events: none;
}