Design, CG Graphics & Inspiration
Ambilight for the “video” tag

Ambilight for the “video” tag

Some high-end Philips TV sets have a cool feature, Ambilight. Basically, it is LED lighting that changes its color dynamically, depending on the television picture’s color. It is such a pleasure to watch movies on an Ambilight-enabled TV!

There are some implementations of such lighting in Adobe Flash. Why can’t we, web masters, do the same thing using scripts? It was another opportunity for me to check out what state-of-the-art web browsers can do, so I’ve made the following thing:

Ambilight for the <video> tag (Firefox 3.5, Opera 10.5, Safari 4, Google Chrome 4)

Let’s take a look at how it was done

Algorithm

Before writing the script for Ambient-like lighting, we need to make an algorithm for that.

The real Ambient lighting works as follows: There are many super-bright LEDs on the TV set’s back panel, and they emit light of different colors. The color emitted by each LED approximates the color of the television picture area in its neighborhood, so when the picture changes, the LEDs smoothly change their colors too.

Therefore, we should do the following: determine each LED’s color for the current picture frame and paint the lighting. Let’s start, shall we.

Determining the LED color

To make the task easier, we’ll assume that there are only 5 LEDs on each side of our “TV set”. So we take a part of the picture frame, divide it into several areas (each corresponding to one LED) and find each area’s average color, that is, the lighting colors.

To get the current picture frame, we just need to draw it in <canvas> using the drawImage() method:

var canvas = document.createElement('canvas'),
video = document.getElementsByTagName('video')[0],
ctx = canvas.getContext('2d');

// setting the canvas size (remember to do it!)
canvas.width = video.width;
canvas.height = video.height;

// drawing the picture frame
ctx.drawImage(video, 0, 0, video.width, video.height);

We’ve got the current frame, and now we need to check the color of each pixel at the picture’s side. To do that, we use the getImageData() method:

/** Width of the analyzed area */
var block_width = 50;

var pixels = ctx.getImageData(0, 0, block_width, canvas.height);

The pixels object has the data property, which contains the colors of all pixels, though in a somewhat odd format: an array of RGBA components of all pixels. For example, the first 4 elements of the data array give us the color and transparency of the first pixel; the next 4 elements, the same of the second pixel; and so on:

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

We should divide all the pixels into 5 groups (one group per LED) and analyze each group, one after another:

function getMidColors() {
var width = canvas.width,
height = canvas.height,
lamps = 5, // number of LEDs
block_width = 50, // width of the analyzed area
block_height = Math.ceil(height / lamps), // height of the analyzed block
pxl = block_width * block_height * 4, // number of RGBA components in one area
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; }
In this function, we are simply going block by block and calculating the average color for each of them, using the calcMidColor() function. We don't have to use sophisticated formulas to calculate the average color since it's enough to get the arithmetic mean for each color component:

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; }
We've got the LED colors, but they are too dim. Keep in mind that the LEDs must be very bright, otherwise we don't get sufficient light intensity. So we must crank up the brightness of those colors, and do the same for the saturation (to add depth to the lighting). The HSV (hue, saturation, value) color model is very handy for that purpose — just multiply the last two components by some factor, and you have what you need. However, our colors are stored as RGB, so first we must convert them to HSV, then increase brightness and saturation, and then convert everything back to RGB (you can easily google the formulas for RGB>HSV and HSV>RGB conversion):

function adjustColor(color) {
color = rgb2hsv(color);
color[1] = Math.min(100, color[1] * 1.4); // saturation
color[2] = Math.min(100, color[2] * 2.7); // brightness
return hsv2rgb(color);
}

Drawing the lighting

LEDs are omnidirectional light sources. It is best to use radial gradients to draw them, one gradient per LED. But to get really good visuals, we'd have to do a lot of very complex calculations as we'd need to consider each LED's position and diameter, light attenuation, mixing of adjacent colors, and so on. So let's cheat a little bit: draw a linear gradient, and then overlay a special mask to make a realistic lighting.

It's easy to draw a gradient: first make it using createLinearGradient(), then add colors using addColorStop() and draw it:

// creating a new canvas for lighting
var light_canvas = document.createElement('canvas'),
light_ctx = light_canvas.getContext('2d');

light_canvas.width = 200;
light_canvas.height = 200;

var midcolors = getMidColors(), // getting average colors

grd = ctx.createLinearGradient(0, 0, 0, canvas.height); // gradient

for (var i = 0, il = midcolors.length; i < il; i++) { grd.addColorStop(i / il, 'rgb(' + adjustColor(midcolors[i]).join(',') + ')'); } // drawing gradient light_ctx.fillStyle = grd; light_ctx.fillRect(0, 0, light_canvas.width, light_canvas.height);
The result looks like that:

Mask

To make the mask, we'll use Adobe Photoshop. There is a wonderful filter, Lightning Effects (Filters→Render→ Lightning Effects...), which lets you to create light sources. We flood the layer with white color and then call the filter with settings like that:

We've made this light spot:

Now we change the blending mode to Lighten, duplicate, rotate, change the scale, play a little bit with transparence, adjust the levels, and here's what we've got:

The picture is black-and-white, so it's very easy to make a mask with transparent white color. By laying the mask over the gradient, we get a cute enough lighting:

But the most important thing is that we can easily change the lighting's appearance and intensity even without using more programming.

The left-side lighting is ready — now we must do the same for the right side, add smooth changing of lighting, and code a controller to update the lighting at some interval. Describing the whole process would be too long and tedious, so better take a look at the source code.

As some people reported that the Ambient-like lighting didn't work well the initial HD video (1280 by 544 pixels), I reduced the video's resolution to 592 by 256 pixels, and now everything should be fine.

  • Splashnology Editors,
  • March 10, 2010

SHARE THIS POST

This post has been written by the team here at Splashnology.com

Subscribe for the hottest posts

Subscribe to our email newsletter for useful tips and freebies.

  • Hello, great plugin!! Can this work for embedded videos or only videos hosted on your own server??

  • Hello, great plugin!! Can this work for embedded videos or only videos hosted on your own server??