-
Ambilight для тэга video
В некоторых топовых моделях телевизоров Philips есть такая прикольная штука, как Ambilight. По сути, это светодиодная подсветка телевизора, которая меняет цвет в зависимости от цвета картинки. Смотреть кино на таком телевизоре — одно удовольствие.
На флэше уже есть реализации такой подсветки, ну а чем мы — фронтовики — хуже? Дабы в очередной раз разобраться, на что способны современные браузеры, на свет появился очередной эксперимент:
Ambilight для тэга <video> (Firefox 3.5, Opera 10.5, Safari 4, Google Chrome 4)
Далее рассмотрим, как это было сделано.
Алгоритм
Прежде, чем начать что-то писать, нужно составить алгоритм, по которому будет работать наша подсветка.
Настоящая подсветка в телевизоре работает примерно так. На задней панели располагается ряд ярких светодиодов, которые светятся разными цветами. Причём цвет диода примерно соответствует цвету области изображения, напротив которой он находится. Когда картинка меняется, светодиод плавно меняет свой цвет на другой.
Исходя из этого описания, нам нужно проделать следующее: определить цвет каждого диода для текущего кадра и отрисовать его свечение. Что ж, приступим.
Определяем цвет диода
Для удобства предположим, что в нашем «телевизоре» всего по 5 светодиодов с каждой стороны. Соответственно, нужно взять фрагмент кадра, разделить его на области по количеству диодов и найти усреднённый цвет в каждой области — это и будут цвета подсветки:
Чтобы получить изображение текущего видео-кадра, достаточно отрисовать его в
<canvas>
через методdrawImage()
:var canvas = document.createElement('canvas'), video = document.getElementsByTagName('video')[0], ctx = canvas.getContext('2d'); // обязательно выставляем размер холста canvas.width = video.width; canvas.height = video.height; // рисуем кадр ctx.drawImage(video, 0, 0, video.width, video.height);
Текущий кадр получили, теперь нужно узнать, какого цвета пиксели сбоку изображения. Для этого воспользуемся методом
getImageData()
:/** Ширина области, которую будем анализировать */ var block_width = 50; var pixels = ctx.getImageData(0, 0, block_width, canvas.height);
В объекте
pixels
есть свойствоdata
, в котором содержатся цвета всех пикселей. Причём хранятся они в немного необычном формате: это массив RGBA-компонетнов всех пикселей. К примеру, чтобы узнать цвет и прозрачность первого пикселя, нужно взять первые 4 элемента массиваdata
, второго пикселя — следующие 4 и так далее:var pixel1 = { r: pixels.data[0], g: pixels.data[1], b: pixels.data[2], a: pixels.data[3] }; var pixel2 = { r: pixels.data[4], g: pixels.data[5], b: pixels.data[6], a: pixels.data[7] };
Нам нужно разделить все полученные пиксели на 5 групп (по количеству светодиодов, которое мы выбрали ранее) и проанализировать каждую группу по очереди:
function getMidColors() { var width = canvas.width, height = canvas.height, lamps = 5, //количество светодиодов block_width = 50, // ширина анализируемой области block_height = Math.ceil(height / lamps), // высота анализируемого блока pxl = block_width * block_height * 4, // сколько всего RGBA-компонентов в одной области result = [], img_data = ctx.getImageData(0, 0, block_width, h), total = img_data.data.length; for (var i = 0; i < lamps; i++) { var from = i * width * block_width; result.push( calcMidColor(img_data.data, i * pxl, Math.min((i + 1) * pxl, total_pixels - 1)) ); } return result; }
В этой функции мы просто пробегаемся по анализируемым блокам и считаем для них усреднённый цвет с помощью функции
calcMidColor()
. Нам не нужно применять всякие хитрые формулы, чтобы посчитать усреднённый цвет на области исходя из интенсивности цветов в ней, достаточно посчитать среднее арифметическое для каждого цветового компонента:function calcMidColor(data, from, to) { var result = [0, 0, 0]; var total_pixels = (to - from) / 4; for (var i = from; i <= to; i += 4) { result[0] += data[i]; result[1] += data[i + 1]; result[2] += data[i + 2]; } result[0] = Math.round(result[0] / total_pixels); result[1] = Math.round(result[1] / total_pixels); result[2] = Math.round(result[2] / total_pixels); return result; }
Итак, мы получили цвета для светодиодов, но они слишком тусклые: ведь диоды светят очень ярко чтобы добиться достаточного уровня свечения. Нужно увеличить яркость цветов, а также увеличить насыщенность, чтобы добавить глубины свечению. Для этих целей очень удобно пользоваться цветовой моделью HSV — hue, saturation, value, — достаточно домножить два последних компонента на некий коэффициент. Но цвета у нас хранятся в модели RGB, поэтому сначала конвертируем цвет в HSV, увеличиваем яркость и насыщенность, а затем обратно конвертируем в RGB (формулы конвертирования RGB→HSV и обратно легко находятся в интернетах):
function adjustColor(color) { color = rgb2hsv(color); color[1] = Math.min(100, color[1] * 1.4); // насыщенность color[2] = Math.min(100, color[2] * 2.7); // яркость return hsv2rgb(color); }
Рисуем свечение
Светодиоды — это всенаправленные источники света. Для их отображения лучше всего подходят радиальные градиенты: для каждого диода свой градиент. Однако для достижения хорошего визуального результата придётся делать очень много сложных расчётов: нужно учитывать позицию диода, диаметр и затухание свечения, смешивание соседних цветов и так далее. Поэтому мы немного сжульничаем: нарисуем обычный — линейный — градиент, а сверху наложим специальную маску, которая создаст ощущение правдоподобного свечения.
Градиент рисуется просто: сначала создаём его с помощью
createLinearGradient()
, а потом добавляем цвета черезaddColorStop()
и отрисовываем его:// для свечения создаём новый холст var light_canvas = document.createElement('canvas'), light_ctx = light_canvas.getContext('2d'); light_canvas.width = 200; light_canvas.height = 200; var midcolors = getMidColors(), // полчаем усреднённые цвета grd = ctx.createLinearGradient(0, 0, 0, canvas.height); // градиент for (var i = 0, il = midcolors.length; i < il; i++) { grd.addColorStop(i / il, 'rgb(' + adjustColor(midcolors[i]).join(',') + ')'); } // рисуем градиент light_ctx.fillStyle = grd; light_ctx.fillRect(0, 0, light_canvas.width, light_canvas.height);
Получим что-то вроде этого:
Маска
Маску мы нарисуем в фотошопе. Есть замечательный фильтр Lightning Effects (Filters→Render→ Lightning Effects...), который позволяет создавать источники света. Заливаем слой белым цветом и вызываем этот фильтр примерно с такими настройками:
Получим вот такое световое пятно:
Меняем режим наложения на Lighten, дублируем, крутим, меняем масштаб, играемся с прозрачностью, правим уровни и получаем вот такой результат:
Так как изображение чёрно-белое, из него очень легко получить маску, где белый цвет будет прозрачным. И если эту маску наложить поверх градиента, то получим вполне себе симпатичное свечение:
Но самое главное — мы легко сможем менять внешний вид и интенсивность свечения, не прибегая к программированию.
Свечение для левой стороны готово, осталось проделать то же самое для правой стороны, добавить плавную смену подсветок и написать контроллер, который с определённым интервалом будет эту подсветку обновлять. Расписывать это — долго и нудно, проще посмотреть исходник.
UPD: как показал эксперимент, далеко не у всех нормально работает HD-видео (изначально размер ролика был 1280×544), снижение разрешения до 592×256 решило проблему.
60 комментариев
Очень круто. Странно только, у меня почему-то свечение очень-очень тусклое в FF (3.5.8), а в Хроме все работает так как задумано.
че-то я ни в хроме 5, ни в фф 3.6 ничего не увидел
Очень впечатляет 🙂
Сергей, только не забывайте, что за вами следит все мировое флеш-сообщество, готовое инкриминировать вам убийство флеша 🙂 Шучу.
Кстати, на флеше не видел этого эффекта. Если вы встречали, буду благодарен за ссылку.
Разве что на нашумевшем сайте http://www.cinema.philips.com
Но тут явно просто под телеком еще одно пререндеренное видео. Видна на нем характерная гранулярность flv.
Флеш умеет очень чисто работать с пикселями и подобными эффектами, но там явно не оно.
Как всегда очень круто. Хочу такую функцию в VLC 🙂
все норм, токо бледновато и из-за моника почти не видно было
по идее цвет должен менятся у свечения или нет?
и да, видео очень лагало, хотя проц вообще был не нагружен
тормоза страшные 🙂 а так бы добавить подсветку вверх и вниз. реализация интересна!
Подсветку сделал более яркой, теперь все должны увидеть.
Михаил, у вас какая ОС?
PS: лучше смотреть в Сафари/Хроме или Опере, в Файерфоксе действительно есть проблемы с лагами, пока не придумал, как побороть
И всё же video — не тег, а элемент. 😉
Сергей. В chrome нереально у меня тормозит. win7. 2 ядра забиваются полностью.
Странно…
В Safari 4.0.4 пишет «Your browser doens’t support tag»
В FF 3.5 эффект видно, но тормоза ужасные
В хроме (3.0.195) видео идет, а эффекта нет. Надо обновить, пожалуй 🙂
p.s.
обновился хром, проблема осталась — видео прет, амбилайта нет.
такое чувство, что он сначала грузит видео, и ему не до того :)) Поставил на паузу, помотал туда -сюда, и вдруг амбилайт появился. странно, вобщем 🙂
Это круто. Но опять же лаги в фаерфоксе, в хроме почему-то не загружается сразу подсветка, только после обновления. В принципе такую подсветку можно применить и для простой картинки?
Так нигде и не увидел эффекта. Пробовал Опера 10.50, Firefox 3.6, Chrome 4 (Windows 7 x64)
Попробуйте сейчас посмотреть — всё ещё тормозит?
А сейчас нормуль в chrome
Ооо. Эффект появился. Шикарно. Даже не ожидал, что такое в принципе возможно 🙂
Добавил UPD к статье. Похоже, не у всех нормально работает HD-видео.
Firefox 3.6
Ошибка: uncaught exception: [Exception… «Component returned failure code: 0x80040111 (NS_ERROR_NOT_AVAILABLE) [nsIDOMCanvasRenderingContext2D.drawImage]» nsresult: «0x80040111 (NS_ERROR_NOT_AVAILABLE)» location: «JS frame :: http://chikuyonok.ru/ambilight/ambilight.js :: drawLight :: line 360″ data: no]
В каком случае выдаёт такую ошибку?
Рома, по поводу амбилайта на флеше. Студия «Цэтис» делала сайт «Глюкоза Продакшн» и реализовала амбилайт на флеше http://www.cetis.ru/portfolio/clients/glukoza/glukoza/ или на сайте http://www.glukoza-production.ru/
Сергей, супер! Но в FF 3.5.8 — почему то цвет фона темнее маски световых пятент.
Да, действительно, маски были светлее. Подправил.
Напомнило прошлогоднюю статью: «Add some ambiance to your videos», но там конечно попроще.
Очень круто. И отдельное спасибо за описание реализации!
Смотрел в ff-3.6/linux
Мего-круто!
В google-chrome-dev 5.0.335.0 (Linux) видео сильно лагает (Core2Duo), а firefox-kde 3.6 подсветки не видно практически.
эх… в свежей опере 10.50 не показывает 🙁
странно, в первый раз не показывало, красиво!
Проверил код: технически могла возникнуть проблема с не успевшей подгрузиться маской. Поставил дополнительные проверки, теперь должно быть лучше.
А так и задумано что свет не сразу меняется с картинкой, а с задержкой? Мне кажется на каком нибудь экшене это будет только мешать, а не добавлять атмосферы…
Скажем так, это техническое ограничение. Подсветка меняется по таймеру. Можно снизить интервал, но тогда будет выше нагрузка на процессор. Более правильным решением будет смена подсветки в момент смены планов, то есть когда сильно меняется цвет изображения.
Пока это больше как proof of concept, для продакшн-версии нужно много чего улучшить. Как сделаю — выложу в открытый доступ.
Не работает в Сафари…
Спасибо, очень познавательная статья. Буду знать теперь, что можно получить доступ к данным video.
Остается только сказать «Вау!».
Все работает более чем отлично.
винХП
Тормоза сильные:( FF 3.5, win xp, проц 1.8 ГГц.
Все, у кого вариант Сергея идет слишком медленно, попробуйте этот:
http://www.stratero.ru/homm/ambilight/ambilight.htm
Dr.Death, Вы тоже попробуйте, изменения отображаются без задержки 🙂
this is awesome, congrats!
Рома, на сайте turbofilm.ru реализован ambilight на флеше.
Уиии! Я и не знал, что если video на canvas отрисовать, можно его куски получать 🙂
Оно летает в Chrome 5 под Linux на… Eee PC 900!
P.S. Наткнулся на этот знакомый сайт по ссылке из твиттера. Да из чьего! Paul Irish, ведущий yayQuery 🙂 http://twitter.com/paul_irish/status/10224940395
Особенно радует уход от флеша )
Что касается самого эмбилайта, то у филипса есть отличная приставка: ставится сзади и по краям монитора, превращая его в топовый телевизор ) Всем советую, раз эффект уже даже в вебе эмулируют.
http://www.ambx.com/
Серёга, новая версия не тормозит и маска лучше.
Начинаются настоящие испытания трафика: уже на ajaxian написали.
Не, настоящие испытания начались, когда на reddit написали. 5000 заходов с утра только с одного сайта, из-за чего мне хостинг отрубили 🙂
I think you could actually do this more quickly by just using images and scaling them. Take the whole scene, scale it down to width 1 and then scale it up to the required width again and apply your mask.
Cheers,
Jonas
PS: It’s quite non trivial to comment for somebody who doesn’t speak Russian. 😉
Midcolor calculations are very fast: they took about 5 ms for each frame. The most processor-cosuming operations are painting video frame on canvas and changing opacity. I’m working on better ambilight implementation, with more responsive lights change and smaller cpu impact.
Сергей, если вызывать перерисовку 2 раза в секунду, то, конечно, 5 ms не играю роли. В предложенном мной варианте вообще нет анимации, только более частая перерисовка и смешивание происходит не картинок, а цветов. При этом в смешивании участвует большее количество кадров. Надо толко убрать постоянное пересоздание канвы.
Насчет «processor-cosuming operations are painting video frame on canvas»
ctx.drawImage(video, 0, 0, video.width, video.height);
Не лучше ли рисовать только 2 области по краям, вместо всего изображения и рисовать на канве размером block_width*2 × video.height, мне кажется это тоже должно помочь.
Как я уже написал, я работаю над новой версией эмбилайта, которая будет работать быстрее и лучше
Очень впечатляет. Поздравления
Nice effect !
А не проще вывести просто картинку. По моему, это будет экономичнее.
очень интересная статья, впечатляет
ха прикольно получилось!!!спасибо!! будем дальше экспериментировать!!
У меня тоже получилось! Спасибо за урок.
Такая функция на флеше тут.
http://blog.denivip.ru/index.php/2010/11/ambilight-эффект-во-flash-видео-плеере/
hmm, this is kinda intriguing
Сергей, зачем в исходниках неиспольуемые функции getTime, animationLoop, addAnimation и removeAnimation? Также меня беспокоит строка 191 (var from = i * w * block_width;), она не нужна и немножко вводит в заблуждение.
Hey Sergei. Very impressive piece of code! I was wondering if you ever wrote a new version since your last posting?
I’d like to use your work in a personal website I’m making and it integrates very nicely. But I wanted to add a switch or toggle to have users turn the effect off if they want to.
Here is my working example — http://www.eschulist.com/test/index.html The toggle button in the upper right is linked to the ambilight effect and it works but only for a single image, a second later the canvas is redrawn and the lights stay on. Is there any way to stop the ambilight effect other than pause or the end of the video?
Eric,
no, there’s no new ambilight version yet, but I think it will be available in next few months.
You can try to hide ambilight by setting
visibility: hidden
oropacity: 0
то canvas elements. Or you can store ambilight state in<video>
element with jQuerydata()
method and skip lights draw inambilightLoop()
function depending on ambilight visibility stateDoesn’t seem to work on an iPad, any ideas on this?
If divx extension is installed in Google Chrome it doesn’t work either, is there a possibility to choose a primary html player?
Добрый день.
Не получается прикрутить, причину вижу в
опасити. При каждом старте проигрывания создается эта строка вместо opacity: 1