Photoshop Layer Mask Implemented with CSS Custom Filters

My last post talked about how you can create a Photoshop-like soft blur effect with built-in filters.  As I started reading more about how you would actually create that soft blur effect in Photoshop though, I realized that often you don’t want the soft blur to apply to people’s faces.  You want to apply a layer mask which essentially punches through the soft blur around the central subject’s face to reveal the crisp underlying photo on the bottom layer.

There is no built-in CSS filter to achieve this, but building a CSS Custom Filter to achieve this effect is actually quite straightforward.  And, it turns out you can use the effect to create much more than cheesy ’80s photos.

Let me show you first what great things you can do with this custom filter.  Make sure you’re viewing this page with Chrome with CSS shaders enabled.  To do so, navigate to chrome://flags and search for “Enable CSS Shaders”.  Click on “Enable” and restart your browser.

Now, see that grass above?  Move your mouse over the grass and you should be in for a very nice surprise.  Just in case you’re not seeing the effect, here’s a video:

Ok, so what’s going on here? We have two overlapping DIVs, each with its own background image.  Then, we’re using a CSS Custom Filter I’ve developed, called ‘Circular Mask’ to punch a transparent hole in the top DIV when you hover over it.  The image of the crocodile starts to poke through the middle of the image of the grass.  I’m just demonstrating the technique with two images, but this can be applied to any content at all.

The video below is the circular mask filter being used in CSS Filter Lab.  The white circle that you can see in the video is the white background showing through all the text and images.

To understand how this was all done, first let me show you the HTML & CSS.  Then I’ll show you how I built the custom filter.

HTML

The HTML is quite simple:

<div class="layer-container aligncenter">
    <div id="layer1" class="layer"></div>
    <div id="layer2" class="layer"></div>
</div>

Just a container DIV containing two other DIVs representing the two layers.  Each DIV is of class ‘layer’ so I can apply shared CSS attributes to all layers.  Then each layer has its own ID so I can give them unique background images and apply the custom filters to the top layer.

CSS

And here is the CSS:

.layer-container { 
    /* Position relative so absolutely positioned children will be relative to the container */
    position: relative; 

    /* Center the container */
    width: 800px; 
    height: 600px;
    margin-left: auto;
    margin-right: auto; 
}

.layer { 
    background-repeat: no-repeat;
    background-size:contain;   
    position: absolute;
    top: 0; left: 0;
    width: 800px;
    height: 600px;
}

#layer1 {
    background-image: url(http://blattchat.com/demos/circular_mask/croc.png);   
}

#layer2 {
    background-image: url(http://blattchat.com/demos/circular_mask/grass.jpg);

    /* Give the layer a default filter config so the transition works smoothly */
    -webkit-filter: custom(
        url(http://blattchat.com/demos/circular_mask/shaders/circular_mask.vs)             
            mix(url(http://blattchat.com/demos/circular_mask/shaders/circular_mask.fs) multiply source-atop), 
        50 50 border-box, 
        radius 0.32, center 0.52 0.45, opacity 1.0, feather 0.2);
    -webkit-transition: -webkit-filter 3s;
}

#layer2:hover {
    /* Punch a 27% opaque hole with soft edges in layer2 */
    -webkit-filter: custom(
        url(http://blattchat.com/demos/circular_mask/shaders/circular_mask.vs)
            mix(url(http://blattchat.com/demos/circular_mask/shaders/circular_mask.fs) multiply source-atop), 
        50 50 border-box, 
        radius 0.32, center 0.52 0.45, opacity 0.27, feather 0.2);
}

The outer DIV is relatively positioned so that the absolutely positioned child DIVs (layer1 & layer2) are positioned within the container.

Note that by default, layer2 has the circular mask filter applied, but with opacity set to 1.0, or fully opaque.  This has no effect on the image itself, but it allows for a smooth transition when the mouse moves over the DIV.

Let’s take a look at the :hover custom filter declaration in more detail:

-webkit-filter: custom(
    url(http://blattchat.com/demos/circular_mask/shaders/circular_mask.vs)
        mix(url(http://blattchat.com/demos/circular_mask/shaders/circular_mask.fs) multiply source-atop), 
    50 50 border-box, 
    radius 0.32, center 0.52 0.45, opacity 0.27, feather 0.2);

We’ve declared that the vertex shader (shaders/circular_mask.vs) should use a 50×50 mesh.  The fragment shader can be found at ‘shaders/circular_mask.fs’.  The custom filter, which consists of the two shaders, has the following parameters:

  • radius – Radius of the circle.
  • center – Position of the circle
  • opacity – Opacity of the circle.  Everything outside of the circle will inherit its opacity from surrounding CSS.
  • feather – Size of extra margin around the circle over which opacity will range from opacity set as filter parameter, to fully opaque (1.0).

All of the above measurements treat the element to which the custom filter is applied as ranging from 0.0 to 1.0 in both X and Y directions.  Thus, setting the center to (0.5, 0.5) will position the circle in the middle of the element.

Circular Mask Filter Implementation

The vertex shader here is very simple.

precision mediump float;

uniform mat4 u_projectionMatrix;

// Built-in attributes

attribute vec4 a_position;
attribute vec2 a_texCoord;

// Varyings
varying vec2 v_uv;

void main()
{
    v_uv = a_texCoord;
    gl_Position = u_projectionMatrix * a_position;
}

It doesn’t warp the image at all, but it does create what is called a “varying” that is passed to the fragment shader.  The vertex shader is called once for each mesh point in each triangle  in the 50×50 mesh.  When the shader is called, a_texCoord is a built-in attribute which is set to the position of the current mesh point being processed. a_texCoord ranges from 0.0 to 1.0.

a_texCoord is assigned to v_uv which is a varying.  v_uv will be interpolated throughout the mesh triangle as the fragment shader is called once for every pixel.  In this way, the fragment shader knows which pixel it is dealing with and can act accordingly.

The fragment shader is where all the action occurs in this filter:

precision mediump float;

uniform float radius;
uniform vec2 center;
uniform float opacity;
uniform float feather;

// Varyings
varying vec2 v_uv;

// Main

void main()
{
    float featherStep = smoothstep(radius, radius+feather, length(v_uv - center));
    float transparency = opacity + (1.0 - opacity) * featherStep;
    css_ColorMatrix = mat4(1.0, 0.0, 0.0, 0.0,
                       0.0, 1.0, 0.0, 0.0,
                        0.0, 0.0, 1.0, 0.0,
                        0.0, 0.0, 0.0, transparency);    
}

The uniforms above declare the center, radius, opacity and feather parameters.

We then calculate a transparency value based on the currently interpolated v_uv varying.  If the current v_uv is inside the circle, its transparency is set to the opacity parameter passed into the filter.  Outside of the circle, we uniformly make our way back to full opacity over the feather distance.  This gives the circle a soft edge.

Then we use that calculated transparency in the css_ColorMatrix, which is multiplied against the current pixel.  Since everything but the transparency is an identity matrix, the colors all remain the same.  Only the opacity changes.

And that’s it.  If you look back, this was actually pretty straightforward to implement.  Let me know if you have any thoughts or questions, or if you have any ideas for other interesting custom filters to implement.

You can find all of the source to this filter and a demo that uses it on GitHub.

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

Leave a Reply

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