It’s Curtains for You!

I recently went over to the cnet.com site and was confronted with an ad that really surprised me.  The whole home page itself became a set of curtains that parted to reveal an ad from IBM.  The texture of the curtains themselves was the home page.  Of course, this was faked out with Flash.  If you look carefully the curtain texture isn’t exactly what was on the page, but it’s close enough that you don’t really notice. But then I thought, I bet I could do that easily with CSS Shaders, and with the actual content as texture to boot.

And so, here are the results:

The filter seemed useful enough that the folks working on the Adobe CSS FilterLab have integrated it into their builds.  Hopefully it will show up shortly on html.adobe.com.  You can use it right now though but just cloning the filter lab locally from https://github.com/adobe/cssfilterlab.

Update 12/05/2012: The curtains custom filter is now integrated into CSS Filter Lab at http://html.adobe.com/webstandards/csscustomfilters/cssfilterlab/.  Be sure to use Google Chrome Canary when testing it out.

Update 03/19/2013: Filters are now supported in regular Chrome.  You don’t need to use Canary.  They are however disabled by default.  You’ll need to go to chrome://flags and enable “Enable CSS Shaders”

You can find the source to the vertex & fragment shaders here:

https://github.com/adobe/cssfilterlab/tree/master/shaders

Here’s a brief explanation of how the shader works.  There’s an eentsy weentsy bit of math here, but really it’s quite straightforward.

The Vertex Shader

precision mediump float;

// Built-in attributes.
attribute vec4 a_position;
attribute vec2 a_texCoord;
attribute vec2 a_meshCoord;

// Built-in uniforms.
uniform mat4 u_projectionMatrix;
uniform vec2 u_textureSize;

// Uniforms passed-in from CSS
uniform mat4 transform;
uniform float numFolds;
uniform float foldSize;
uniform float amount;

// Varyings
varying float v_lighting;
varying float xpos;

// Constants
const float PI = 3.1415629;

// Main
void main()
{
    vec4 pos = a_position;
    pos.x += (pos.x < 0.0 ? -1.0 : 1.0) * amount * (0.5 - abs(pos.x));
    xpos = pos.x;

    float cyclePos = sin(abs(a_position.x) * PI * 4.0 * numFolds);
    pos.z = foldSize * amount * cyclePos * 16.0;

    v_lighting = max(1.0 - amount, 0.5 * (cyclePos + 1.0));
    gl_Position = u_projectionMatrix * transform * pos;
}

The Uniforms are parameters passed in from the CSS and are pretty self-explanatory:

  • transform – Scale, rotate or add some perspective to the content
  • numFolds – Number of folders in each curtain
  • foldSize – Size of the folds
  • amount – 0: curtains are completely closed, 1: curtains are fully open

The Squish

First we squish the content left & right based on the setting of amount:

pos.x += (pos.x < 0.0 ? -1.0 : 1.0) * amount * (0.5 - abs(pos.x));

Then we save off the new x position as a varying so we can use it in the fragment shader:

    xpos = pos.x;

The Folds

Now we create the actual folds:

    float cyclePos = sin(abs(a_position.x) * PI * 4.0 * numFolds);
    pos.z = foldSize * amount * cyclePos * 16.0;

That calculation was based on the fact that a sin wave cycles every 2π radians.

a_position.x has the range [-0.5, 0.5], but we want 2 curtains, so we split it down the middle.

Left curtain: [-0.5, 0.0]
Right curtain: [0.0, 0.5]

Using the absolute value of a_position.x will give us the right x value for each curtain with a range of [0.0, 0.5]

abs(a_position.x)

Multiplying that range x 2 gives us a range of [0.0, 1.0]

abs(a_position.x * 2.0)

Multiplying that by 2π will give us a range of [0.0, 2π]

abs(a_position.x * 4.0 * PI)

Do that once for each fold:

abs(a_position.x * 4.0 * PI * numFolds)

and get the resulting position on the sin wave:

cyclePos = sin(abs(a_position.x * 4.0 * PI * numFolds)

will give us values with a range off [-1.0, 1.0].  This can be scaled appropriately to handle the z offset:

pos.z = foldSize * amount * cyclePos * 16.0;

Lighting/Shading

We know where the folds are going to occur, so we can calculate the shadows in the folds appropriately.  cyclePos, the result of the sin() calculation will have a range of [-1.0, 1.0].  We want the light intensity to have a range of [0.0, 1.0], so we add 1 and divide by 2:

0.5 * (cyclePos + 1.0)

But, we want the shadow to be dependent on how much the curtains are drawn, which is specified with amount.  When amount is 0, we don’t want any shadow. And when amount is 1, we want full shadow.  So, no matter what the the lightIntensity would be, we don’t want it be any less than (1.0 – amount), or:

v_lightIntensity = max(1.0 - amount, 0.5 * (cyuclePos + 1.0))

v_lightIntensity is a varying, so it too will be passed to the fragment shader to actually apply the lighting.

The Fragment Shader

precision mediump float;

// Uniforms passed in from CSS

uniform float amount;

// Varyings

varying float v_lighting;
varying float xpos;

// Main

void main()
{
  float alpha = abs(xpos) <= (amount * 0.5) ? 0.0 : 1.0;

  /* Remove any perspective artifacts */
  if (amount == 1.0) alpha = 0.0;
  css_ColorMatrix = mat4(
       vec4(v_lighting, 0.0, 0.0, 0.0),
        vec4(0.0, v_lighting, 0.0, 0.0),
        vec4(0.0, 0.0, v_lighting, 0.0),
        vec4(0.0, 0.0, 0.0, alpha)
    );
}

We’ve pushed the pixels left and right as appropriate based on amount, but that’s going to still leave us with a big rectangle in the middle.  What we want to do is make that rectangle transparent.  Any pixels to the left of the left most edge of the right curtain and to the right of the right most edge of the left curtain need to be fully transparent.

abs(xpos) will be in the range of [0.0, 0.5] and amount will be in the range of [0.0, 1.0] so we first scale amount appropriately and then compare and set the alpha to 0.0 or 1.0:

  float alpha = abs(xpos) <= (amount * 0.5) ? 0.0 : 1.0;

and then we calculate a color matrix using the lightIntensity calculated in the vertex shader, and the alpha we just calculated:

  css_ColorMatrix = mat4(
       vec4(v_lighting, 0.0, 0.0, 0.0),
        vec4(0.0, v_lighting, 0.0, 0.0),
        vec4(0.0, 0.0, v_lighting, 0.0),
        vec4(0.0, 0.0, 0.0, alpha)
    );

And there you have it!  It stretches your brain a bit, but that’s always a good thing.

Click here for more articles on CSS Shaders.

Tagged , , , , , , . Bookmark the permalink.

One Response to It’s Curtains for You!

  1. agreenblatt says:

    For those following this article, CSS Shaders are now supported in Chrome 25+. You don’t need to use Canary. They are however disabled by default. You need to enable them first by navigating to chrome://flags and enabling “Enable CSS Shaders”. Happy shading!

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>