• Ambilight для тэга video

    В некоторых топовых моделях телевизоров Philips есть такая прикольная штука, как Ambilight. По сути, это светодиодная подсветка телевизора, которая меняет цвет в зависимости от цвета картинки. Смотреть кино на таком телевизоре — одно удовольствие.

    На флэше уже есть реализации такой подсветки, ну а чем мы — фронтовики — хуже? Дабы в очередной раз разобраться, на что способны современные браузеры, на свет появился очередной эксперимент:

    Ambilight для тэга <video> (Firefox 3.5, Opera 10.5, Safari 4, Google Chrome 4)

    Далее рассмотрим, как это было сделано.

    Алгоритм

    Прежде, чем начать что-то писать, нужно составить алгоритм, по которому будет работать наша подсветка.

    Настоящая подсветка в телевизоре работает примерно так. На задней панели располагается ряд ярких светодиодов, которые светятся разными цветами. Причём цвет диода примерно соответствует цвету области изображения, напротив которой он находится. Когда картинка меняется, светодиод плавно меняет свой цвет на другой.

    Исходя из этого описания, нам нужно проделать следующее: определить цвет каждого диода для текущего кадра и отрисовать его свечение. Что ж, приступим.

    Определяем цвет диода

    Для удобства предположим, что в нашем «телевизоре» всего по 5 светодиодов с каждой стороны. Соответственно, нужно взять фрагмент кадра, разделить его на области по количеству диодов и найти усреднённый цвет в каждой области — это и будут цвета подсветки:

    get-color

    Чтобы получить изображение текущего видео-кадра, достаточно отрисовать его в <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);
    

    Получим что-то вроде этого:

    gradient

    Маска

    Маску мы нарисуем в фотошопе. Есть замечательный фильтр Lightning Effects (Filters→Render→ Lightning Effects...), который позволяет создавать источники света. Заливаем слой белым цветом и вызываем этот фильтр примерно с такими настройками:

    lightning

    Получим вот такое световое пятно:

    spot

    Меняем режим наложения на Lighten, дублируем, крутим, меняем масштаб, играемся с прозрачностью, правим уровни и получаем вот такой результат:
    spot-grid

    Так как изображение чёрно-белое, из него очень легко получить маску, где белый цвет будет прозрачным. И если эту маску наложить поверх градиента, то получим вполне себе симпатичное свечение:

    result

    Но самое главное — мы легко сможем менять внешний вид и интенсивность свечения, не прибегая к программированию.

    Свечение для левой стороны готово, осталось проделать то же самое для правой стороны, добавить плавную смену подсветок и написать контроллер, который с определённым интервалом будет эту подсветку обновлять. Расписывать это — долго и нудно, проще посмотреть исходник.

    UPD: как показал эксперимент, далеко не у всех нормально работает HD-видео (изначально размер ролика был 1280×544), снижение разрешения до 592×256 решило проблему.

    Метки: , , ,
  • 60 комментариев

    1. Sap1enS
      4 марта 2010

      Очень круто. Странно только, у меня почему-то свечение очень-очень тусклое в FF (3.5.8), а в Хроме все работает так как задумано.

    2. Михаил
      4 марта 2010

      че-то я ни в хроме 5, ни в фф 3.6 ничего не увидел

    3. 4 марта 2010

      Очень впечатляет :)

      Сергей, только не забывайте, что за вами следит все мировое флеш-сообщество, готовое инкриминировать вам убийство флеша :) Шучу.

      Кстати, на флеше не видел этого эффекта. Если вы встречали, буду благодарен за ссылку.
      Разве что на нашумевшем сайте http://www.cinema.philips.com
      Но тут явно просто под телеком еще одно пререндеренное видео. Видна на нем характерная гранулярность flv.
      Флеш умеет очень чисто работать с пикселями и подобными эффектами, но там явно не оно.

    4. derek
      4 марта 2010

      Как всегда очень круто. Хочу такую функцию в VLC :)

    5. Михаил
      4 марта 2010

      все норм, токо бледновато и из-за моника почти не видно было

    6. 4 марта 2010

      по идее цвет должен менятся у свечения или нет?
      и да, видео очень лагало, хотя проц вообще был не нагружен

    7. 4 марта 2010

      тормоза страшные :) а так бы добавить подсветку вверх и вниз. реализация интересна!

    8. Сергей Чикуенок
      4 марта 2010

      Подсветку сделал более яркой, теперь все должны увидеть.

      Михаил, у вас какая ОС?

      PS: лучше смотреть в Сафари/Хроме или Опере, в Файерфоксе действительно есть проблемы с лагами, пока не придумал, как побороть

    9. 4 марта 2010

      И всё же video — не тег, а элемент. ;-)

    10. morozov
      4 марта 2010

      Сергей. В chrome нереально у меня тормозит. win7. 2 ядра забиваются полностью.

    11. vlad43
      4 марта 2010

      Странно…
      В Safari 4.0.4 пишет «Your browser doens’t support tag»
      В FF 3.5 эффект видно, но тормоза ужасные
      В хроме (3.0.195) видео идет, а эффекта нет. Надо обновить, пожалуй :)

    12. vlad43
      4 марта 2010

      p.s.
      обновился хром, проблема осталась — видео прет, амбилайта нет.
      такое чувство, что он сначала грузит видео, и ему не до того :)) Поставил на паузу, помотал туда -сюда, и вдруг амбилайт появился. странно, вобщем :)

    13. Melo
      4 марта 2010

      Это круто. Но опять же лаги в фаерфоксе, в хроме почему-то не загружается сразу подсветка, только после обновления. В принципе такую подсветку можно применить и для простой картинки?

    14. Дмитрий
      4 марта 2010

      Так нигде и не увидел эффекта. Пробовал Опера 10.50, Firefox 3.6, Chrome 4 (Windows 7 x64)

    15. Сергей Чикуенок
      4 марта 2010

      Попробуйте сейчас посмотреть — всё ещё тормозит?

    16. morozov
      4 марта 2010

      А сейчас нормуль в chrome

    17. UtK
      4 марта 2010

      Ооо. Эффект появился. Шикарно. Даже не ожидал, что такое в принципе возможно :)

    18. Сергей Чикуенок
      4 марта 2010

      Добавил UPD к статье. Похоже, не у всех нормально работает HD-видео.

    19. 4 марта 2010

      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]

    20. Сергей Чикуенок
      4 марта 2010

      В каком случае выдаёт такую ошибку?

    21. Леонид
      4 марта 2010

      Рома, по поводу амбилайта на флеше. Студия «Цэтис» делала сайт «Глюкоза Продакшн» и реализовала амбилайт на флеше http://www.cetis.ru/portfolio/clients/glukoza/glukoza/ или на сайте http://www.glukoza-production.ru/
      Сергей, супер! Но в FF 3.5.8 — почему то цвет фона темнее маски световых пятент.

    22. Сергей Чикуенок
      4 марта 2010

      Да, действительно, маски были светлее. Подправил.

    23. 4 марта 2010

      Напомнило прошлогоднюю статью: «Add some ambiance to your videos», но там конечно попроще.

    24. 4 марта 2010

      Очень круто. И отдельное спасибо за описание реализации!
      Смотрел в ff-3.6/linux

    25. Vii
      4 марта 2010

      Мего-круто!

      PS: лучше смотреть в Сафари/Хроме

      В google-chrome-dev 5.0.335.0 (Linux) видео сильно лагает (Core2Duo), а firefox-kde 3.6 подсветки не видно практически.

    26. 4 марта 2010

      эх… в свежей опере 10.50 не показывает :(

    27. 4 марта 2010

      странно, в первый раз не показывало, красиво!

    28. Сергей Чикуенок
      4 марта 2010

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

    29. 4 марта 2010

      А так и задумано что свет не сразу меняется с картинкой, а с задержкой? Мне кажется на каком нибудь экшене это будет только мешать, а не добавлять атмосферы…

    30. Сергей Чикуенок
      4 марта 2010

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

      Пока это больше как proof of concept, для продакшн-версии нужно много чего улучшить. Как сделаю — выложу в открытый доступ.

    31. gh
      4 марта 2010

      Не работает в Сафари…

    32. 4 марта 2010

      Спасибо, очень познавательная статья. Буду знать теперь, что можно получить доступ к данным video.

    33. kandasoft
      4 марта 2010

      Остается только сказать «Вау!».
      Все работает более чем отлично.

    34. Михаил
      4 марта 2010

      винХП

    35. 5 марта 2010

      Тормоза сильные:( FF 3.5, win xp, проц 1.8 ГГц.

    36. homm
      5 марта 2010

      Все, у кого вариант Сергея идет слишком медленно, попробуйте этот:

      http://www.stratero.ru/homm/ambilight/ambilight.htm

      Dr.Death, Вы тоже попробуйте, изменения отображаются без задержки :)

    37. warp.
      5 марта 2010

      this is awesome, congrats!

    38. Вадим
      9 марта 2010

      Рома, на сайте turbofilm.ru реализован ambilight на флеше.

    39. 9 марта 2010

      Уиии! Я и не знал, что если video на canvas отрисовать, можно его куски получать :)
      Оно летает в Chrome 5 под Linux на… Eee PC 900!

      P.S. Наткнулся на этот знакомый сайт по ссылке из твиттера. Да из чьего! Paul Irish, ведущий yayQuery :) http://twitter.com/paul_irish/status/10224940395

    40. 11 марта 2010

      Особенно радует уход от флеша )

      Что касается самого эмбилайта, то у филипса есть отличная приставка: ставится сзади и по краям монитора, превращая его в топовый телевизор ) Всем советую, раз эффект уже даже в вебе эмулируют.
      http://www.ambx.com/

    41. 11 марта 2010

      Серёга, новая версия не тормозит и маска лучше.

    42. GreLI
      12 марта 2010

      Начинаются настоящие испытания трафика: уже на ajaxian написали.

    43. Сергей Чикуенок
      12 марта 2010

      Не, настоящие испытания начались, когда на reddit написали. 5000 заходов с утра только с одного сайта, из-за чего мне хостинг отрубили :)

    44. 12 марта 2010

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

    45. Сергей Чикуенок
      12 марта 2010

      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.

    46. homm
      12 марта 2010

      Сергей, если вызывать перерисовку 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, мне кажется это тоже должно помочь.

    47. Сергей Чикуенок
      12 марта 2010

      Как я уже написал, я работаю над новой версией эмбилайта, которая будет работать быстрее и лучше

    48. vhanla
      13 марта 2010

      Очень впечатляет. Поздравления

    49. 15 марта 2010

      Nice effect !

    50. 23 марта 2010

      А не проще вывести просто картинку. По моему, это будет экономичнее.

    51. 20 мая 2010

      очень интересная статья, впечатляет

    52. 3 июня 2010

      ха прикольно получилось!!!спасибо!! будем дальше экспериментировать!!

    53. 4 ноября 2010

      У меня тоже получилось! Спасибо за урок.

    54. 10 ноября 2010
    55. 3 февраля 2011

      hmm, this is kinda intriguing

    56. Ldev
      13 февраля 2011

      Сергей, зачем в исходниках неиспольуемые функции getTime, animationLoop, addAnimation и removeAnimation? Также меня беспокоит строка 191 (var from = i * w * block_width;), она не нужна и немножко вводит в заблуждение.

    57. Eric
      14 июля 2011

      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?

    58. Сергей Чикуенок
      14 июля 2011

      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 or opacity: 0 то canvas elements. Or you can store ambilight state in <video> element with jQuery data() method and skip lights draw in ambilightLoop() function depending on ambilight visibility state

    59. 2 апреля 2012

      Doesn’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?

    60. Paul
      14 сентября 2012

      Добрый день.
      Не получается прикрутить, причину вижу в

      опасити. При каждом старте проигрывания создается эта строка вместо opacity: 1