clear:both;

Всё о xHTML, CSS и JavaScript

video + canvas = магия

Комментариев: 0

Вызов такси

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

Сначала основы.

Если вы только начинаете работать с 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> содержит 2 атрибута: @controls и @loop. @controls (средства управления) говорит браузеру дать видео со стандартным набором средств управления: просмотр/пауза, перемотка, звук, и т. д. @loop (цикл) говорит браузеру повторить видео еще один раз, когда оно закончиться.

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

Посмотреть этот код в действии: проиграем начало одного из величайших анимационных сериалов всех времен.

HTML5 Просмотр видео в Chrome

HTML5 Просмотр видео в Chrome

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

Рассмотрим простой пример

Теперь, когда мы знаем, как воспроизвести видео, давайте добавим немного <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>

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

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

<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;

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

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

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

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

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 кадров / с, что более чем достаточно. Вторая строка, в которой работает магия - она перетаскивает текущий кадр из видео прямо на холст. Да, это именно так просто, как кажется. Просто переносит элемент видео и X, Y, ширина и высота прямоугольника на холсте такие, как вы хотите. В этом случае, видео заполнит весь холст, но если вы хотите, то сможете сделать меньше (или больше!).

Я использую здесь еще одну хитрость. Помните, что полотно действительно крошечное? Видео будет не менее чем в 20 раз больше, чем холст на большинстве экранов, так как же мы можем разместить его на такой крошечный холст? Функция DrawImage () выполняет следующее: она автоматически масштабирует все, что вы укажите в первом аргументе, так чтобы заполнить прямоугольник, который вы укажете. Это означает, что нам больше не придется беспокоиться об усреднении цвета пикселей (или экстраполяции, если вы вписываете небольшое видео в большой прямоугольник), потому что браузер будет делать это для нас. Я буду использовать этот трюк далее, так что следите за этим.

И ... вот оно что! Весь пример делается в 20 строк легко читаемого кода JavaScript, мгновенно производится отличный эффект фона для любого видео, которое вы хотите воспроизвести. Вы можете тривиально изменить размер "пикселей" на холсте путем корректировки линии, которые устанавливают CW и CH переменных.

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

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

Видео сделано градацией серого манипуляцией с  холстом Видео сделано градацией серого манипуляцией с холстом

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;
    // First, draw it into the backing canvas
    bc.drawImage(v,0,0,w,h);
    // Grab the pixel data from the backing canvas
    var idata = bc.getImageData(0,0,w,h);
    var data = idata.data;
    // Loop through the pixels, turning them grayscale
    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;
    // Draw the pixels onto the visible canvas
    c.putImageData(idata,0,0);
    // Start over!
    setTimeout(function(){ draw(v,c,bc,w,h); }, 0);
}

Этот скрипт занимает немного дольше времени, потому что мы фактически делаем некоторые работы. Но это все еще очень просто!

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);

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

Во-первых, я создаю второй холст и вытягиваю его из контекста. Это "резервный холст", который я буду использовать для выполнения промежуточных операций перед заливкой конечного результата в видимой холст в разметке. Резервный холст не нужно даже добавлять в документ. Он может просто висеть здесь, в моем сценарии. Эта стратегия будет использоваться много позже в примерах, и это весьма полезно в целом, так что примите ее к сведению.

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

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

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

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

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

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

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

Теперь, небольшое примечание об объекте ImageData. Он возвращает пиксели специальным образом для того, чтобы ими можно было легко манипулировать. Если у вас есть, скажем, холст 100 × 100 пикселей, он содержит в общей сложности 10000 пикселей. Массив ImageData для него будет иметь 40000 элементов, так как пиксели разбиты по компонентам и перечислены последовательно. Каждая группа из четырех элементов в массиве 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);

... А потом засунуть все это в видимой холст! Нет необходимости производить какие-то сложные действия! Просто берем пиксели, манипулируем ими, и засовываем их обратно. Вот так легко!

Финальное замечание: в режиме реального времени манипуляции пикселями в полноэкранном видео является одним из тех редких мест, где микро-оптимизация на самом деле важна. Вы можете увидеть их влияние на мой код. Первоначально, я не брал пиксельные данные из объекта ImageData, и просто писал "VAR R = idata.data [i];" и так далее каждый раз, что означало несколько дополнительных запросов в каждой итерации цикла. Также я первоначально разделил яркость на 8 и округлил значение, что несколько медленнее, чем битовый сдвиг на 3 позиции. В обычном коде, такого рода вещи совершенно незначительны, но когда вы делаете их несколько миллионов раз в секунду (видео размером 480 × 360, и, следовательно, содержит около 200000 пикселей, каждый из которых индивидуально обрабатываются примерно 100 раз в секунду), эти крошечные задержки складываются в заметное отставание.

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

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

Видео с выдавливанием при манипуляции с холстом

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

function draw(v,c,bc,cw,ch) {
    if(v.paused || v.ended) return false;
    // First, draw it into the backing canvas
    bc.drawImage(v,0,0,cw,ch);
    // Grab the pixel data from the backing canvas
    var idata = bc.getImageData(0,0,cw,ch);
    var data = idata.data;
    var w = idata.width;
    var limit = data.length
    // Loop through the subpixels, convoluting each using an edge-detection matrix.
    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];
    }
    // Draw the pixels onto the visible canvas
    c.putImageData(idata,0,0);
    // Start over!
    setTimeout(draw,20,v,c,bc,cw,ch);
}

Теперь следующий шаг.

function draw(v,c,bc,cw,ch) {
    if(v.paused || v.ended) return false;
    // First, draw it into the backing canvas
    bc.drawImage(v,0,0,cw,ch);
    // Grab the pixel data from the backing canvas
    var idata = bc.getImageData(0,0,cw,ch);
    var data = idata.data;

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

var w = idata.width;

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

Если они будут использовать "буфер высокого разрешения (high-resolution backing store)", это будет означать лучшее качество изображения, поскольку размер артефактов сглаживания (зубчатых краев на диагональных линиях) станет намного меньше и менее заметным. Это также означает, что вместо 100 × 100 пиксельного куска холста с 40000 значениями, вы получите объект ImageData.data c 160000 значений. Запрашивая у ImageData его ширину и высоту, мы гарантируем, что мы перебор пиксельных данных будет совершен правильно, независимо от того, используетли браузер низкое или высокое разрешение буфера для него.

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

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 объекта в видимом холсте, и создайте еще один вызов функции в 20 миллисекунд. Также, как и в предыдущем примере.

Заключение

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

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

Данная статья является переводом статьи Tab Atkins Jnr video + canvas = magic, размещенной на [html5doctor]:(http://html5doctor.com). На этом сайте размещен перевод вышеуказанной статьи с разрешения html5doctor.

Перевод Яша Плюшев

21 марта 2011 18:14

Из разделов: JavaScript , HTML , HTML5


Комментариев: 0

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