В некоторых топовых моделях телевизоров 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, дублируем, крутим, меняем масштаб, играемся с прозрачностью, правим уровни и получаем вот такой результат:
Так как изображение чёрно-белое, из него очень легко получить маску, где белый цвет будет прозрачным. И если эту маску наложить поверх градиента, то получим вполне себе симпатичное свечение:
Но самое главное — мы легко сможем менять внешний вид и интенсивность свечения, не прибегая к программированию.
Свечение для левой стороны готово, осталось проделать то же самое для правой стороны, добавить плавную смену подсветок и написать контроллер, который с определённым интервалом будет эту подсветку обновлять. Расписывать это — долго и нудно, проще посмотреть исходник.