5

I needed a way to add a "stroke" (outline) and drop-shadow effect to a transparent PNG image based on its alpha mask, and the only solution I could find was using custom SVG filters. (Note: The web app for which I need these effects is for my own private use, so it's ok that this solution isn't compatible with legacy browsers. Moving on...)

I had never used SVG before, but it was pretty simple to create stroke and drop-shadow filters individually. Unfortunately, I could not find a way to create a combined effect without actually copying-and-pasting the filters into a new one, as shown in the code below:

<svg width="0" height="0" xmlns="http://www.w3.org/2000/svg">

    <!-- drop shadow -->
    <filter id="drop-shadow">
        <feGaussianBlur in="SourceAlpha" stdDeviation="4" />
        <feOffset result="m_offsetBlurred" dx="12" dy="12" />
        <feFlood result="m_floodTrans50" flood-color="rgba(0,0,0,0.5)" />
        <feComposite result="m_offsetBlurredTrans50" in="m_floodTrans50" in2="m_offsetBlurred" operator="in" />
        <feMerge>
            <feMergeNode in="m_offsetBlurredTrans50" />
            <feMergeNode in="SourceGraphic" />
        </feMerge>
    </filter>


    <!-- outer stroke -->
    <filter id="outer-stroke">
        <!-- create rectangle of the desired color -->
        <feFlood result="m_floodRect" flood-color="black" />

        <!-- create copy of png's alpha mask and expand -->
        <feMorphology result="m_expandedMask" in="SourceAlpha" operator="dilate" radius="1" />

        <!-- "cut out" a section of the flood fill matching the expanded copy -->
        <feComposite result="m_expandedColored" in="m_floodRect" in2="m_expandedMask" operator="in" />

        <!-- blend it behind the original shape to create the outline effect -->
        <feBlend in="SourceGraphic" in2="m_expandedColored" mode="normal" />
    </filter>


    <!-- drop shadow & outer stroke (must copy & paste the 2 filters above, which violates the DRY principle) -->
    <filter id="outer-stroke-drop-shadow">
        <!-- create rectangle of the desired color -->
        <feFlood result="m_floodRect" flood-color="black" />

        <!-- create copy of png's alpha mask and expand -->
        <feMorphology result="m_expandedMask" in="SourceAlpha" operator="dilate" radius="1" />

        <!-- "cut out" a section of the flood fill matching the expanded copy -->
        <feComposite result="m_expandedColored" in="m_floodRect" in2="m_expandedMask" operator="in" />

        <!-- blend it behind the original shape to create the outline effect -->
        <feBlend result="m_stroked" in="SourceGraphic" in2="m_expandedColored" mode="normal" />

        <!-- add drop shadow -->
        <feGaussianBlur result="m_blurred" in="SourceAlpha" stdDeviation="4" />
        <feOffset result="m_offsetBlurred" in="m_blurred" dx="12" dy="12" />
        <feFlood result="m_floodTrans50" flood-color="rgba(0,0,0,0.5)" />
        <feComposite result="m_offsetBlurredTrans50" in="m_floodTrans50" in2="m_offsetBlurred" operator="in" />
        <feMerge>
            <feMergeNode in="m_offsetBlurredTrans50" />
            <feMergeNode in="m_stroked" />
        </feMerge>
    </filter>
</svg>


<style>
    .fx_drop_shadow              { filter: url('#drop-shadow'); }
    .fx_outer_stroke             { filter: url('#outer-stroke'); }
    .fx_outer_stroke_drop_shadow { filter: url('#outer-stroke-drop-shadow'); }
</style>


<div>
    <img src="gfx/odd_shape.png" />
    <img src="gfx/odd_shape.png" class="fx_drop_shadow" />
    <img src="gfx/odd_shape.png" class="fx_outer_stroke" />
    <img src="gfx/odd_shape.png" class="fx_outer_stroke_drop_shadow" />
</div>

Here is how the above code will render in an HTML5 document:

SVG filters applied to a PNG image

And here is the original PNG graphic (odd_shape.png):

enter image description here

Question 1: How can I reuse the first 2 filters (drop-shadow and outer-stroke) so I can simply apply them in the combined filter (outer-stroke-drop-shadow) instead of having to copy and paste them.

Question 2: Is it possible to parameterize the custom filters so that I can specify things such as the stroke color, or the transparency of the drop shadow? This would make them even more reusable.


Thanks.

etherice
  • 1,761
  • 15
  • 25
  • For the re-use, you may be able to use "xml include" based methods like the ones referenced here: http://stackoverflow.com/questions/5121052/can-we-import-xml-file-into-another-xml-file . For the parameterization, some type of preprocessor (think LESS or Compass) might do the trick, although a quick google did not show any existing svg preprocessors. Does your project use a build / task runner, like grunt? – gregtzar Feb 13 '14 at 19:52
  • If you use javascript you can clone your base filter, then add/remove the various needed elements to the clone, give it a new id, and apply it. – Francis Hemsher Feb 13 '14 at 22:30

3 Answers3

5

Here is a complete solution that works in both browsers I tested (Firefox and Chrome)...

Solution for Question 1: None of the browsers I tested support specifying more than one filter in the filter property, so the best (and perhaps only) way to combine user-defined filters is using the technique suggested by Michael Mullany: apply them sequentially in nested <g> elements, creating the filter graph as desired.

Solution for Question 2: The W3C has a working draft for SVG Parameters and the draft includes a polyfill script for using and testing the proposed feature. Parameters are declared via param() functional attribute value (e.g., param(shadowColor) black) and defined through a query-string-like interface (e.g., foo.svg?shadowColor=red) or through child elements of the <object> container (e.g., <param name="shadowColor" value="red"/>).

Demo code for both solutions is provided below, along with a screenshot from Firefox.


in mypage.html:
<object type="image/svg+xml" data="filters.svg?osColor=lime&dsAlpha=0.4"></object>
<object type="image/svg+xml" data="filters.svg?osColor=white&osWidth=4&dsAlpha=0.8&dsBlurSigma=8&dsOffsetX=32"></object>


in filters.svg:
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="300px" height="320px" viewBox="0 0 300 320">
    <defs>
        <filter id="dropShadow" width="150%">
            <feGaussianBlur in="SourceAlpha" stdDeviation="param(dsBlurSigma) 4" />
            <feOffset result="m_offsetBlurred" dx="param(dsOffsetX) 12" dy="param(dsOffsetY) 12" />
            <feComponentTransfer result="m_offsetBlurredTranslucent" in="m_offsetBlurred">
                <feFuncA type="linear" slope="param(dsAlpha) 0.5" />
            </feComponentTransfer>
            <feMerge>
                <feMergeNode in="m_offsetBlurredTranslucent" />
                <feMergeNode in="SourceGraphic" />
            </feMerge>
        </filter>
        <filter id="outerStroke" width="150%">
            <feFlood result="m_floodRect" flood-color="param(osColor) black" />
            <feMorphology result="m_expandedMask" in="SourceAlpha" operator="dilate" radius="param(osWidth) 1" />
            <feComposite result="m_expandedColored" in="m_floodRect" in2="m_expandedMask" operator="in" />
            <feBlend in="SourceGraphic" in2="m_expandedColored" mode="normal" />
        </filter>
    </defs>

    <!-- combine stroke & drop shadow -->
    <g style='filter:url(#dropShadow);' width='300' height='320'>
        <g style='filter:url(#outerStroke);'>
            <image width='240' height='280' xlink:href="gfx/odd_shape.png"></image>
        </g>
    </g>

    <!-- use polyfill from http://dev.w3.org/SVG/modules/param/master/SVGParamPrimer.html -->
    <script type="text/ecmascript" xlink:href="http://dev.w3.org/SVG/modules/param/master/param.js" />
</svg>

The result:

enter image description here

etherice
  • 1,761
  • 15
  • 25
2

The SVG 1.1 filter spec includes the ability to include another filter by reference, but only IE10+ (and Firefox - thanks Robert!) support this capability. You can combine filters by applying them at different levels of element nesting aka using wrapper group elements. Although it's not particularly elegant.

There is no ability to parameterize an SVG filter per se either (although of course, you can do anything you want via JavaScript). The spec includes the ability to use the stroke and fill of an object as filter inputs, but these are only supported in Firefox and IE10+ today (no Chrome, no Safari).

Michael Mullany
  • 30,283
  • 6
  • 81
  • 105
  • Firefox supports including by reference too. – Robert Longson Feb 14 '14 at 09:31
  • The filter spec does have the ability to reference another filter element to "copy" its child elements, but it is defined such that it only works if the other filter element has no child elements of its own. – Erik Dahlström Feb 14 '14 at 10:09
  • Thanks. +1 for your help. Please see my answer for the complete solution I found, including parameterizing the filters. – etherice Feb 15 '14 at 16:44
2

Answer to question 1:

The Filter Effects spec does allow you to have multiple effects in a linear list, for example:

filter: url(#outer-stroke) drop-shadow(5px 5px 10px black);

or even:

filter: url(#outer-stroke) url(#drop-shadow);

Whether this is implemented yet is another question however. In Chrome you can currently only combine the shorthand syntax in this way. The effects are applied in the order you specify them in.

Answer to question 2:

If you use the drop-shadow shorthand you can specify the shadow color as rgba, which gives you opacity.

Erik Dahlström
  • 59,452
  • 12
  • 120
  • 139
  • Thanks. +1 for your help. Unfortunately, none of the browsers I tested allowed me to specify multiple filters in a single `filter` property. However, I was able to nest `` elements and create the filter chain that way. Please see my answer for the complete solution I found, including parameterizing the filters. – etherice Feb 15 '14 at 16:46