Главная » Полезные статьи » HTML-верстка » Video + canvas = волшебство
Распечатать статью

Video + canvas = волшебство

Вы уже знакомы с элементами <video> и <canvas>, но известно ли вам, что они были разработаны для совместной работы? Фактически, при объединении этих элементов происходит что-то невероятное! Я продемонстрирую вам несколько очень простых демо с использованием этих двух элементов, которые, я надеюсь, будут полезными в ваших, ребята веб-авторы, будущих крутых проектах. (Эти демо работают во всех современных браузерах, кроме Internet Explorer.)

Для начала, основы

Если вы новичок в HTML5, вы можете быть не знакомы с элементом <video> и с тем, как его использовать. Вот простой пример, который будет использоваться в последующих демо:

 

<video controls loop>
  <source src=video.webm type=video/webm>
  <source src=video.ogg type=video/ogg>
  <source src=video.mp4 type=video/mp4>
</video>

Элемент <video> содержит два атрибута: controls и loop. controls говорит о том, что браузер должен снабдить видео стандартным набором видео-контролов: воспроизвести/пауза, контроль скорости воспроизведения, громкость и т. д. loop указывает, что браузер должен воспроизводить видео сначала, когда оно заканчивается.

Затем, внутри элемента <video> располагаются три дочерних элемента <source>, каждый из которых ссылается на разную кодировку одного и того же видео. Браузер попробует каждый источник по очереди и воспроизведет первый, который поймет.

Посмотрите этот код в действии, воспроизводящий вступление к одному из величайших мультсериалов всех времен.

HTML5 <video>, воспроизведенный в Chrome

(Примечание: все эти демо предполагают, что ваш браузер поддерживает <video>, чего не может IE8 и его более ранние версии. Обычно в качестве альтернативного варианта для таких браузеров используется Flash или что-то подобное, но в данном случае это не сильно подойдет, так как все техники, которые я демонстрирую, основываются на взаимодействии элементов <video> и <canvas>, которое не может быть достигнуто при помощи Flash. Поэтому в этих примерах я не указывал никаких запасных вариантов для браузеров, не поддерживающих <video>. Но, тем не менее, я привожу несколько источников видео, так что все браузеры, поддерживающие <video>, смогут воспроизвести его.)

Теперь, простой пример

Сейчас, когда мы знаем, как воспроизводить видео, давайте смешаем его с <canvas>. Сначала посмотрите демо, а затем ознакомьтесь с кодом. Я подожду.

Воспроизведение видео с помощью <canvas> в полноэкранном режиме

Справились? Круто! Теперь, как это работает? Конечно же для этого необходимы несколько сотен строк JavaScript, правда? Если вы сжульничали и уже подсмотрели исходный код страницы с демо, вы уже поняли, насколько это легко.

Мы начинаем с этого HTML:

<!DOCTYPE html>
<title>Video/Canvas Demo 1</title>

<canvas id=c></canvas>
<video id=v controls loop>
  <source src=video.webm type=video/webm>
  <source src=video.ogg type=video/ogg>

  <source src=video.mp4 type=video/mp4>
</video>

Точно такой же код, как раньше, но теперь мы подмешали в него элемент <canvas>. Однако, он пока еще слегка пустой и бесполезный. Мы допишем скрипт для него чуть позже.

Сейчас давайте добавим сюда немного CSS, чтоб правильно расположить вещи:

<style>

body {
  background: black;
}

#c {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  width: 100%
  height: 100%
}

#v {
  position: absolute;
  top: 50%;
  left: 50%;
  margin: -180px 0 0 -240px;
}
</style>

Этот код всего лишь располагает видео по центру экрана и растягивает <canvas> на полную ширину и высоту окна браузера. Так как <canvas> идет первым в документе, он будет располагаться позади видео, именно там, где нам надо.

А теперь – волшебство!

<script>
document.addEventListener('DOMContentLoaded',
function(){
  var v = document.getElementById('v');
  var canvas = document.getElementById('c');
  var context = canvas.getContext('2d');

  var cw = Math.floor(canvas.clientWidth / 100);
  var ch = Math.floor(canvas.clientHeight / 100);
  canvas.width = cw;
  canvas.height = ch;

  v.addEventListener('play', function(){
    draw(this,context,cw,ch);
  },false);

},false);

function draw(v,c,w,h) {
  if(v.paused || v.ended) return false;
  c.drawImage(v,0,0,w,h);
  setTimeout(draw,20,v,c,w,h);
}
</script>

Подождите минутку. Просто глубоко вздохните и впитайте его. Такой короткий, такой сладкий, такой милый…

Теперь давайте рассмотрим его шаг за шагом.

  var v = document.getElementById('v');
  var canvas = document.getElementById('c');
  var context = canvas.getContext('2d');

  var cw = Math.floor(canvas.clientWidth / 100);
  var ch = Math.floor(canvas.clientHeight / 100);
  canvas.width = cw;
  canvas.height = ch;

Эта часть простая. Я получаю доступ к элементам <video> и <canvas> на странице и доступ к 2D-контексту элемента <canvas>, для того, чтобы я мог рисовать на нем. Затем я быстренько делаю расчет, чтоб определить, ширину и высоту области <canvas>. Сам элемент <canvas> уже растянут по размеру экрана с помощью CSS, так что это сделает каждый пиксель области для рисования равным примерно 100х100 пикселей на экране.

Последний кусочек требует небольшого разъяснения, если вы не знакомы с <canvas>. Как правило, отображаемый размер и размер области для рисования элемента <canvas> одинаковые. В этом случае нарисованная линия длинной 50 пикселей будет отображена как линия длинной 50 пикселей. Но это не обязательно должно быть так – вы можете установить размер поверхности для рисования с помощью свойств width и height в самом элементе <canvas>, а затем изменить размер отображаемого <canvas> с помощью CSS. Тогда браузер автоматически увеличит или уменьшит размер рисунка, чтоб область для рисования соответствовала отображаемому размеру. В этом случае я устанавливаю очень маленький размер поверхности для рисования <canvas> – на большинстве экранов он будет примерно 10 пикселей в ширину и 7 пикселей в высоту – а затем растягиваю отображаемый размер с помощью CSS таким образом, что каждый нарисованный мной пиксель будет увеличиваться браузером в 100 раз. Именно это является причиной такого клевого визуального эффекта в демо.

<v.addEventListener('play', function(){
  draw(v,context,cw,ch);
},false);

Еще одна простая часть. Здесь я навешиваю немного кода на событие play элемента <video>. Это событие срабатывает, как только пользователь нажимает кнопку «воспроизвести», чтобы начать просмотр видео. Все, что я делаю – это вызываю функцию draw ( ) с соответствующими параметрами: сам <video>, контекст рисования <canvas> и ширина и высота <canvas>.

function draw(v,c,w,h){
  if(v.paused || v.ended) return false;
  c.drawImage(v,0,0,w,h);
  setTimeout(draw,20,v,c,w,h);
}

Первая строчка просто немедленно останавливает выполнение функции, как только пользователь нажимает на паузу или останавливает видео, для того, чтобы не нагружать процессор, когда ничего не происходит. Третья строчка снова вызывает функцию draw ( ), что дает браузеру возможность выполнить другие задачи, например, обновить видео. Я задаю задержку 20 миллисекунд, так что мы получим примерно 50 кадров в секунду, чего более чем достаточно.

Вторая строчка – это именно то место, где происходят чудеса: она рисует текущий кадр видео прямо на <canvas>. Да, это действительно так просто, как кажется. Просто установите элемент <video>, координаты x, y, ширину и высоту прямоугольника, который вы хотите нарисовать на <canvas>. В этом случае он заполняет весь <canvas>, но вы можете сделать его меньшим (или большим!), если хотите.

Здесь я использую еще один фокус. Помните, насколько <canvas> маленький? Видео будет как минимум в 20 раз превышать размер <canvas> на большинстве экранов, так как же нам нарисовать его на таком маленьком <canvas>? С этим справится функция drawImage ( ) – она автоматически масштабирует все, что вы укажете в первом аргументе, чтобы заполнить заданный прямоугольник. Это значит, что нам, авторам, не нужно беспокоиться об усреднении цветов пикселей (или экстраполировании, если вы рисуете маленькое видео в большом прямоугольнике), потому что браузер сделает это за нас. Я буду еще использовать этот прием в дальнейшем, так что будьте внимательны.

И… это все! Все демо сделано с помощью 20 строчек легкочитаемого JavaScript-кода, который незамедлительно производит отличный фоновый эффект для любого воспроизводимого вами видео. Вы можете просто менять размер «пикселей» на <canvas>, меняя строчки, которые задают переменные cw и ch.

Непосредственное манипулирование пикселями видео

Последнее демо было классное, но оно просто позволяло сделать всю тяжелую работу на браузеру. Браузер уменьшил видео, нарисовал его на <canvas>, а затем увеличил пиксели <canvas> и все это автоматически. Давайте испытаем собственные силы и сделаем что-то самостоятельно! Посмотрите демо, чтоб увидеть в действии, как я сделал видео в серых тонах.

Видео, сделанное в серых тонах с помощью манипуляции <canvas>

HTML для этой страницы практически идентичный:

<video id=v controls loop>
  <source src=video.webm type=video/webm>
  <source src=video.ogg type=video/ogg>
  <source src=video.mp4 type=video/mp4>
</video>
<canvas id=c></canvas>

Здесь ничего нового, так что давайте перейдем к скрипту.

document.addEventListener('DOMContentLoaded', function(){
  var v = document.getElementById('v');
  var canvas = document.getElementById('c');
  var context = canvas.getContext('2d');
  var back = document.createElement('canvas');
  var backcontext = back.getContext('2d');

  var cw,ch;

  v.addEventListener('play', function(){
    cw = v.clientWidth;
    ch = v.clientHeight;
    canvas.width = cw;
    canvas.height = ch;
    back.width = cw;
    back.height = ch;
    draw(v,context,backcontext,cw.ch);
  },false);

},false);

function draw(v,c,bc,w,h) {
  if(v.paused || v.ended) return false;
  // Сначала рисуем его на запасном <canvas>
  bc.drawImage(v,0,0,w,h);
  // Берем данные пикселей запасного <canvas>
  var idata = bc.getImageData(0,0,w,h);
  var data = idata.data;
  // Проходимся по пикселям, делая их в серых тонах
  for(var i = 0; i < data.length; i+4) {
    var r = data[i];
    var g = data[i+1];
    var b = data[i+2];
    var brightness = (3*r+4*g+b)>>>3;
    data[i] = brightness;
    data[i+1] = brightness;
    data[i+2] = brightness;
  }
  idata.data = data;
  // Рисуем пиксели на видимом <canvas>
  c.putImageData(idata,0,0);
  // Начинаем!
  setTimeout(function(){ draw(v,c,bc,w,h); }, 0);
}

На этот раз скрипт получился немного длиннее, потому что мы действительно выполнили некоторую работу. Но он все равно очень простой!

document.addEventLister('DOMContentLoaded', function(){
  var v = document.getElementById('v');
  var canvas = document.getElementById('c');
  var context = canvas.getContext('2d');
  var back = document.createElement('canvas');
  var backcontext = back.getContext('2d');

  var cw,ch;

  v.addEventListener('play', function(){
    cw = v.clientWidth;
    ch = v.clientHeight;
    canvas.width = cw;
    canvas.height = ch;
    back.width = cw;
    back.height = ch;
    draw(v,context,backcontext,cw,ch);
  },false);

Это почти то же, что у меня было раньше, но с двумя существенными отличиями.

Во-первых, я создаю второй <canvas> и также получаю доступ к его контексту. Это «запасной <canvas>«, который я буду использовать для выполнения промежуточных операций, прежде чем нарисовать финальный результат на видимом <canvas>, который присутствует в разметке. Запасной <canvas> даже не нужно добавлять в документ. Он может просто висеть здесь в моем скрипте. Такая стратегия будет использоваться во многих последующих примерах. Она весьма полезна в принципе, так что возьмите ее себе на заметку.

Во-вторых, вместо того, чтобы сразу менять размер <canvas>, я жду, пока видео воспроизведется, потому что элемент <video> скорей всего не успевает загрузить свое видео, когда запускается событие DOMContentLoaded, так что до тех пор он использует стандартный размер элемента. А когда он готов воспроизводить, ему уже известен размер видео и его размер устанавливается соответственно. Таким образом, мы можем устанавливать размеры <canvas> равными размерам видео.

function draw(v,c,bc,w,h) {
  if(v.paused || v.ended) return false;
  bc.drawImage(v,0,0,w,h);

Также, как и в первом демо, функция draw ( ) сначала проверяет, должна ли она остановиться, а затем просто рисует видео на <canvas>. Заметьте, что я рисую его на запасном <canvas>, который, опять же, просто сидит в моем скрипте и не отображается в документе. Отображаемый <canvas> зарезервирован для показа версии в серых тонах, так что я использую запасной <canvas> для загрузки исходных данных видео.

  var idata = bc.getImageData(0,0,w,h);
  var data = idata.data;

Вот первое новшество. Вы можете нарисовать что-то на <canvas> с помощью обычной функции рисования <canvas> или drawImage ( ), а можете просто непосредственно манипулировать пикселями через объект ImageData. getImageData ( ) возвращает пиксели с прямоугольника <canvas>. В этом случае я просто получаю его полностью.

Внимание! Если вы попытаетесь запустить демо на локальной машине, то скорей всего столкнетесь с проблемой. Элемент <canvas> отслеживает, откуда берутся данные внутри него, и, если он знает, что вы получили что-то с другого сайта (например, если элемент <video>, который вы нарисовали на <canvas>, указывает на файл с другого домена), он «испортит» <canvas>. Вы не можете использовать пиксели из испорченного <canvas>. К сожалению, file: url считаются «кросс-доменными» в этом случае, так что у вас не получится воспроизвести это на вашем компьютере. Вы можете запустить веб-сервер на вашем компьютере и просмотреть страницу с локального узла или загрузить его на какой-нибудь другой сервер, которым вы управляете.

  for(var i = 0; i < data.length; i+=4) {
    var r = data[i];
    var g = data[i+1];
    var b = data[i+2];

Теперь вкратце об объекте ImageData. Он возвращает пиксели особым способом, для того, чтоб ими было легко манипулировать. Если ваш <canvas>, скажем, размером 100х100 пикселей, в нем содержится 10 000 пикселей. Тогда диапазон ImageData для него будет содержать 40 000 элементов, потому что пиксели разбиваются по компонентам и располагаются последовательно. Каждая группа из четырех элементов в диапазоне ImageData соответствует красному, зеленому, синему и альфа каналам для этого пикселя. Чтоб пройтись по пикселям, просто каждый раз увеличивайте ваш счетчик в 4 раза, как я делаю здесь. Тогда каждый канал – это целое число от 0 до 255.

var brightness = (3*r+4*g+b)>>>3;
  data[i] = brightness;
  data[i+1] = brightness;
  data[i+2] = brightness;

Сейчас немного о математических преобразователях RGB-значения пикселя в отдельное значение «яркости». Когда наши глаза открываются, они наиболее сильно реагируют на зеленый свет, меньше на красный и еще меньше на синий. Поэтому я соответственно взвесил каналы, прежде чем вычислил среднее. Затем мы просто скармливаем это отдельное значение обратно всем трем каналам. Как нам всем наверное известно, когда красное, зеленое и синее значение цвета равны, получается серый. (В течении всего этого процесса я совершенно игнорирую четвертый компонент каждой группы – альфа канал, потому что он всегда равен 255.)

idata.data = data;

Запихиваем диапазон измененных пикселей обратно в объект ImageData

c.putImageData(idata,0,0);

… а затем запихиваем все это в отображаемый <canvas>! Нам вообще не нужно никакое сложное рисование! Просто берем пиксели, манипулируем с ними и запихиваем их обратно. Так просто!

Заключительное примечание: манипуляция пикселями всего видео в режиме реального времени – это один из тех редких случаев, когда микро-оптимизация действительно имеет значение. Вы можете увидеть ее эффект в моем коде здесь. Изначально я не вытягивал пиксели из объекта ImageData, а просто писал «var r = idata.data[i];» и так далее каждый раз, что означало несколько дополнительных подстановок свойств в каждом цикле. Я также сначала просто делил яркость на 8 и основывался на этом значении, что немного медленнее, чем разделение на 3 группы. В обычном коде такие моменты совершенно незначительны, но если вы повторяете их несколько миллионов раз в секунду (видео 480х360, то есть содержит около 200 000 пикселей, каждый из которых индивидуально обрабатывается 100 раз в секунду), такие маленькие задержки накапливаются в значительный лаг.

Более продвинутая манипуляция пикселями

Вы также можете оперировать более чем одним пикселем одновременно, получая чудесные комплексные эффекты. Как было замечено в конце предыдущего раздела, исполнение играет здесь важную роль, но вы удивитесь, что можно получить, применив немного фантазии. Как видно в демо, в этом примере я буду создавать эффект тиснения, для которого вам необходимо использовать несколько вводных пикселей вместе, чтоб подсчитать значение каждого полученного пикселя.

Видео с эффектом тиснения, достигнутым с помощью манипуляции пикселями

Вот код. HTML и большая часть начального кода идентичны предыдущему примеру, так что я опустил все, кроме функции draw ( ):

function draw(v,c,bc,cw,ch) {
  if(v.paused || v.ended) return false;
  // Сначала рисуем его на запасном <canvas>
  bc.drawImage(v,0,0,cw,ch);
  // Берем пиксели с запасного <canvas>
  var idata = bc.getImageData(0,0,cw,ch);
  var data = idata.data;
  var w = idata.data;
  var limit = data.length
  // Проходимся по подпикселям, поворачивая каждый, используя матрицу обнаружения краев.
  for(var i = 0; i < limit; i++) {
    if( i%4 == 3 ) continue;
    data[i[ = 127 +2*data[i] - data[i + 4] - data[i + w*4];
  }
  // Рисуем пиксели на отображаемом <canvas>
  с.putImageData(idata,0,0);
  // Начинаем!
  setTimeout(draw,20,v,c,bc,cw,ch);
}

А теперь шаг за шагом.

function draw(v,c,bc,cw,ch) {
  if(v.paused || v.ended) return false;
  // Сначала рисуем его на запасном <canvas>
  bc.drawImage(v,0,0,cw,ch);
  // Берем пиксели с запасного <canvas>
  var idata = bc.getImageData(0,0,cw,ch);
  var data = idata.data;

Также, как и в последем примере. Проверяем, нужно ли нам остановиться, а затем рисуем видео на запасном <canvas> и берем пиксели с него.

  var w = idata.data;

Значимость этой строки требует некоторого пояснения. Я уже передал ширину <canvas> в функцию (в виде переменной cw), так зачем мне опять менять его ширину здесь? Что ж, я просто обманул вас раньше, когда рассказывал, каким большим будет массив пикселей. Браузер может связать один пиксель <canvas> с одним пикселем ImageData, но браузеры имеют возможность использовать более высокое разрешение в данных изображения, отображая каждый пиксель <canvas> как блок пикселей ImageData размером 2х2, или 3х3 или, возможно, даже больше!

Если они будут использовать «резервное хранилище высокого разрешения», как я его назвал, это означает лучшее отображение, такие как артифакты алиазинга (зубчатые границы диагональных линий) уменьшатся и станут не такими заметными. Это также означает, что вместо <canvas> размером 100х100 пикселей, который выдает вам объект ImageData.data с 40 000 цифр, у вас будет 160 000 цифр. Запрашивая у ImageData его ширину и высоту, мы убеждаемся, что правильно прошлись по пикселям, в не зависимости, использует браузер высокое разрешение или низкое.

Очень важно использовать это правильно всякий раз, когда вам нужны ширина и высота данных, которые вы получили как объект ImageData. Если слишком много людей станет забывать об этом и просто использовать ширину и высоту <canvas>, браузерам придется всегда использовать низкое разрешение, чтоб справиться с подобными кривыми скриптами!

  var limit = data.length;
    for(var i = 0; i < limit; i++) {
      if( i%4 == 3 ) continue;
      data[i] = 127 + 2*data[i] - data[i + 4] - data[i + w*4];
    }

Я беру длинну данных и сохраняю ее в переменной, так что мне не придется тратить ресурсы за доступ к свойству в каждой отдельной итерации. (Помните, микро-оптимизация важна, когда вы манипулируете с видео в режиме раельного времени!) Затем я просто прохожусь по пикселям, как и раньше. Если пиксель оказывается из альфа-канала (каждый четвертый пиксель в диапазоне), я могу его просто пропустить – я не хочу менять прозрачность. Другими словами, я использую немного математики, чтоб найти разницу между текущим цветовым каналом пикселя и подобными каналами пикселей ниже и справа, а затем просто соединяю эту разницу со «средним» серым значением 127. Это приводит к эффекту, когда области с пикселями одинакового цвета закрашиваются в умеренный серый, а границы, на которых цвет резко меняется, становятся либо светлыми, либо темными.

Здесь еще одна оптимизация. Так как я сравниваю только текущие пиксели с пикселями, которые  «далеко впереди» в данных, которых я еще не видел, я могу просто сохранять измененные данные прямо в исходных данных, потому что текущие пиксели уже не понадобятся. Это значит, что мне не придется аккумулировать огромный диапазон для получения результата, прежде чем преобразовать данные обратно в объект ImageData.

  c.putImageData(idata,0,0);
    setTimeout(draw,20,v,c,bc,cw,ch);

И, наконец, рисуем измененный объект ImageData на отображаемом <canvas> и устанавливаем еще один вызов функции через 20 миллисекунд. Это так же, как и в предыдущем примере.

В завершение

Итак, сегодня мы изучили основы объединения элементов <canvas> и <video>. Демо были очень простыми, но они проиллюстрировали все основные техники, которые вам нужны для создания чего-то даже более крутого самостоятельно:

  1. Вы можете рисовать видео непосредственно на <canvas>.
  2. Когда вы рисуете на <canvas>, браузер автоматически масштабирует изображение при необходимости.
  3. Когда вы отображаете <canvas>, браузер снова автоматически масштабирует его, если визуальный размер отличается от размера в резервном хранилище.
  4. Вы можете манипулировать <canvas> на уровне пикселей, просто взяв ImageData, изменив его и нарисовав опять.

Во второй части этой статьи [скоро!] я расскажу про другие интересные приложения, полученные с помощью совмещения <video> c <canvas>, в том числе про преобразователь видео в ASCII в режиме реального времени!

Источник:  css-live.ru

Вы можете оставить комментарий, или обратную ссылку на Ваш сайт.

Оставить комментарий

Похожие статьи