Code and Stuff

Jan 30, 2013

Animating SVG Filters applied to HTML elements

Animating CSS3 filters is possible. While the ones defined directly in CSS can be animated with CSS3 transitions, SVG filters require another approach. We can use SVG Animations. It is not as trivial as the transitions but still quite simple.

Setup

To animate SVG filters we simply have to put an animate tag as child of the element we want to animate. Animations can be timed, concatenate and even started with javascript.

Here an example on how to animate the intercept of all the RGB transfer functions of the feComponentTransfer filter from -1 to 1 and back repeatedly.

<svg width="0" height="0" xmlns="http://www.w3.org/2000/svg">
  <filter id="myfilter">
    <feComponentTransfer>
      <feFuncR type="linear" slope="1" intercept="-1">
        <animate attributeName="intercept" attributeType="XML" 
            begin="0s" dur="4s" repeatCount="indefinite" 
            values="-1;1;-1"/>
      </feFuncR>
      <feFuncG type="linear" slope="1" intercept="-1">
        <animate attributeName="intercept" attributeType="XML" 
            begin="0s" dur="4s" repeatCount="indefinite" 
            values="-1;1;-1"/>
      </feFuncG>
      <feFuncB type="linear" slope="1" intercept="-1">
        <animate attributeName="intercept" attributeType="XML" 
            begin="0s" dur="4s" repeatCount="indefinite" 
            values="-1;1;-1"/>
      </feFuncB>
    </feComponentTransfer>
  </filter>
</svg>
As per usual the filter is applied to the dom element with some CSS:
<style type="text/css">
#someElement {
   filter: url(#myfilter);
   -webkit-filter: url(#myfilter);
}
</style>

Triggering animations on hover (Firefox)

Unfortunately setting the begin attribute to mouseover or click does not work. This probably because the filter is not applied to an SVG image but an HTML DOM element.

We will, therefore, need some Javascript to simulate a CSS3 Transition for an SVG filter. Looks like there is some problems with SVG filter animations in chrome, so this only works properly with firefox.

JQuery has the very handy hover method to add the two event handlers. As implemented the hover transitions must be very short (0.5s or so). A cleaner implementation should check the actual position of the one animation to start the other. This would allow smoother transition with no jumps when the transition happens before the previous one ends.

$(function() { // on DOM ready register the events
  $("#theItem").hover(function(event){
    // stop the out event
    $("#myfilter animate").get(1).endElement();
    // start the in one
    $("#myfilter animate").get(0).beginElement();
  }, function(event){
    $("#myfilter animate").get(0).endElement();
    $("#myfilter animate").get(1).beginElement();
  });
});
The filter will need this time two animation with undefined begin and end times. An animated saturation filter would look like this:
<svg width="0" height="0" xmlns="http://www.w3.org/2000/svg">
  <filter id="myfilter">
    <feColorMatrix type="saturate" values="0">
      <animate attributeName="values" attributeType="XML" 
                  begin="indefinite" end="indefinite" dur="0.5s" 
                  fill="freeze" from="0" to="1"/>
      <animate attributeName="values" attributeType="XML" 
                  begin="indefinite" end="indefinite" dur="0.5s" 
                  fill="freeze" from="1" to="0"/>
    </feColorMatrix>
  </filter>
</svg>
On firefox moving the mouse over the image should add color to it.

Chrome Vs. Firefox

With CSS3 SVG Filters firefox seams to work better. It appears to implement more stuff. I found three things where Chrome appears to have some problems:
  • Animating some of the filters, for instance the ColorMatrix's values attribute (but I'm not sure if it has to)
  • Support for some common attributes of SVG filters (result, X, Y, Width, Height)
  • Strangely, when called while handling some specific DOM event, the beginElement function will run the animation only the first time

Jan 17, 2013

HTML5 Range Input Hack (Firefox)

UPDATE: Firefox 23 added support for the input range

Firefox does not implement the input type range yet. So here's a quick hack that creates a scrollbar for them. The code is implemented using JQuery. I'm sure there are plugins for JQuery that do this, but wanted to try my idea.

WARNING: Did not test it much and the range input needs all attributes (min, max, step)

The idea is pretty simple. For each input element with type range do the following:

  1. Create a div of same size as the input and set its overflow attribute to scroll
  2. Add a horizontally bigger element to this div
  3. Add a scroll event listener to the div that will:
    1. Compute the percentual of scroll
    2. Scale that to the min, max and step values of the input range
    3. Set the range value to the scale value
    4. inform the input it changed
  4. Make the input invisible by setting its opacity to 0 and position it somewhere far away (left:-5000, top:-5000). Do NOT set the display value to none!
function makeRange(dummy, element) {
  if ($.browser.mozilla) {
    var width = $(element).width();

    var minimum = +$(element).attr("min");
    var maximum = +$(element).attr("max");
    var step = $(element).attr("step");
    var size = maximum - minimum;
    var value = $(element).val();

    // 1 and 2
    var newDiv = $('<div style="overflow:scroll; display:inline-block;width:' + width + 'px; height:15px;">\
                    <div style="height:0px;width:' + (width * 4) + 'px">&nbsp;</div></div>');
    newDiv.insertBefore($(element));

    // scroll to initial value
    newDiv.scrollLeft(width * 3 *((value - minimum) / size));
    
    // 3
    newDiv.scroll(function(e){

      // 3.1
      var width = $(this).children().first().width() - $(this).width();
      var value = this.scrollLeft / width;

      // 3.2
      var size = maximum - minimum;
      var fval = minimum + size * value;
      var ival = Math.floor(fval / step) * step;
      ival = ival.toFixed(Math.ceil(Math.abs(Math.log(step))));
      
      // 3.3
      $(element).val(ival);
      
      // 3.4
      $(element).trigger('change');
    });

    // 4
    $(element).css("position","absolute");
    $(element).css("opacity","0");
    $(element).css("left", "-5000px");
    $(element).css("top", "-5000px");
  }
}

// call this when the Document is loaded
$(function(){
    // for each input element of type range 
    $("input[type=range]").each(makeRange);
});
If this method is used in a page where ranges are create dynamically in javascript, make sure to run it only on the newly created range. Otherwise you will end up with duplicate scrollbars on existing ranges.

Jan 16, 2013

Miniature Effect with single SVG Filter (Firefox)

In a previous attempt to create a minuature filter for firefox I used multiple DIVs each showing a different level of blur. This works fine, especially if Webkit support is needed, but it is a little complicated to implement when needed multiple times.

When firefox support alone is ok, the whole effect can be implemented in one single SVG filter. We just have to combine the correct ones.

This new implementation uses merging of multiple filter steps. Currently this only works in Firefox. The filter is:

<svg width="0" height="0" xmlns="http://www.w3.org/2000/svg">
  <filter id="svgFilter3153195357290276084">
    <feGaussianBlur y="0" height="100%" in="SourceGraphic" stdDeviation="8" result="b1"/>
    <feGaussianBlur y="5%" height="90%" in="SourceGraphic" stdDeviation="7" result="b2"/>
    <feGaussianBlur y="10%" height="80%" in="SourceGraphic" stdDeviation="6" result="b3"/>
    <feGaussianBlur y="15%" height="70%" in="SourceGraphic" stdDeviation="5" result="b4"/>
    <feGaussianBlur y="20%" height="60%" in="SourceGraphic" stdDeviation="4" result="b5"/>
    <feGaussianBlur y="25%" height="50%" in="SourceGraphic" stdDeviation="3" result="b6"/>
    <feGaussianBlur y="30%" height="40%" in="SourceGraphic" stdDeviation="2" result="b7"/>
    <feGaussianBlur y="35%" height="30%" in="SourceGraphic" stdDeviation="1" result="b8"/>
    <feGaussianBlur y="40%" height="20%" in="SourceGraphic" stdDeviation="0" result="b9"/>
    <!-- the center needs to be unblurred (identity component transfer) -->
    <feComponentTransfer x="0" y="45%" width="100%" height="10%" in="SourceGraphic" result="a"/>

    <feMerge x="0" y="0" width="100%" height="100%" >
      <feMergeNode in="SourceGraphic"/>
      <feMergeNode in="b1"/>
      <feMergeNode in="b2"/>
      <feMergeNode in="b3"/>
      <feMergeNode in="b4"/>
      <feMergeNode in="b5"/>
      <feMergeNode in="b6"/>
      <feMergeNode in="b7"/>
      <feMergeNode in="b8"/>
      <feMergeNode in="b9"/>
      <feMergeNode in="a"/>
    </feMerge>
  </filter>
</svg>

In the first part of the filter a set of 8 gaussian blurs, of always smaller section of the source image, are created. In the second one, they are composed with the simple alpha composition.

As in the original post the filter is applied using CSS with something like:

img, video {
   filter: url(#filterId);
}
This time there is no need for a container DIV nor a positioning one. The filter can be applied to any DOM element.