Sunday, May 25, 2014

One SVG Filter to Rule Them All


I have my SVG-based Polymer custom element in happy shape at this point. The <x-pizza> element is in nice code shape and, more importantly, building pizzas for hungry customers:



It is composed of several different SVG files, each loaded with <polymer-ajax> for simplicity and ease of dynamic manipulation. It took some tinkering, but I finally have the drop shadow for the individual toppings animating on mouse over:



The drop shadow pain was due to vagaries of SVG, but it proved that the <polymer-ajax> solution to loading other SVG assets (pizza toppings in this case) into a primary SVG worked and could still be effectively manipulated. I would like to build on that idea tonight by adding drop shadows to each of my toppings—not just the current pepperonis.

I could work through each topping in turn with Inkscape, defining a second layer in each that holds the topping a little higher and with a drop shadow, but that seems like tedious work. Hopefully a little Dart can solve the problem without any Inkscape time.

I start by pulling the drop shadow filter definition out of the only topping SVG that currently has it, pepperoni.svg, and placing it in the background SVG of the blank pizza:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   version="1.1"
   width="300"
   height="300">
  <defs>
    <filter
       color-interpolation-filters="sRGB"
       height="2.0"
       width="1.5"
       y="-0.2"
       id="topping-drop-shadow">
      <!-- lots and lots of filters... --> 
    </filter>
  </defs>
  <path
     d="m 300,150 a 150,150 0 1 1 -300,0 150,150 0 1 1 300,0 z"
     id="path3755"
     style="fill:#c8b7b7;fill-opacity:1;stroke:none" />
  <path
     d="m 280,150 a 130,130 0 1 1 -260,0 130,130 0 1 1 260,0 z"
     id="path3759"
     style="fill:#c83737;fill-opacity:1;stroke:none" />
  <path
     d="m 270,150 a 120,120 0 1 1 -240,0 120,120 0 1 1 240,0 z"
     id="path3761"
     style="fill:#ffe6d5;fill-opacity:1;stroke:none" />
</svg>
The only change that I make to the filter is the ID, which I now call topping-drop-shadow for code maintainability and readability. Since I append toppings to this base SVG image, the hope is that the drop shadow filter will continue to work with the topping SVG assets. Starting with pepperoni.svg, I give it a try:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   version="1.1"
   width="32"
   height="32">
  <g id="down" style="display: inline">
    <path
       d="M 22,12 A 10,10 0 1 1 2,12 10,10 0 1 1 22,12 z"
       transform="translate(2.8577037,1.6074583)"
       style="fill:#d40000;fill-opacity:1;stroke:#000000;stroke-width:1.5;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
  </g>
  <g id="up" style="display: none">
    <path
       d="M 22,12 A 10,10 0 1 1 2,12 10,10 0 1 1 22,12 z"
       style="fill:#d40000;fill-opacity:1;stroke:#000000;stroke-width:1.5;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#topping-drop-shadow)" />
  </g>
</svg>
Happily that does still work.

Since it works on one, the hopefully the filter can work on all of my elements. While I am at it, I would like to switch from hiding/showing layers in my SVG to translating a single layer in each topping SVG. So, I remove the group layers in pepperoni.svg which now becomes:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   version="1.1"
   width="32"
   height="32">
  <path
      d="M 22,12 A 10,10 0 1 1 2,12 10,10 0 1 1 22,12 z"
      style="fill:#d40000;fill-opacity:1;stroke:#000000;stroke-width:1.5;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
</svg>
That is much, much smaller without the filter definition and the layers.

Now I can eliminate the layer code that had supported the pepperoni SVG:
  _svgPepperoni() {
    var svg = _svgImage('pepperoni');
    return svg
     ..onMouseOver.listen((e){
          svg.query('#up').style.display = '';
          svg.query('#down').style.display = 'none';
        })
      ..onMouseLeave.listen((e){
          svg.query('#up').style.display = 'none';
          svg.query('#down').style.display = '';
      });
  }
Instead, the pepperoni now uses the same function as all of the other toppings:
  _svgPepperoni()   => _svgImage('pepperoni');
  _svgSausage()     => _svgImage('sausage');
  _svgGreenPepper() => _svgImage('green_pepper');
Finally, the _svgImage() function now does translation and filter application when mouse events occur:
  _svgImage(basename) {
    var svg = new GElement()
      ..append(new SvgElement.svg(svgContent['$basename.svg']));

    var path = svg.query('path');
    var style = path.getAttribute('style');

    return svg
      ..onMouseOver.listen((e){
          path
            ..setAttribute('style', '$style;filter:url(#topping-drop-shadow)')
            ..setAttribute('transform', 'translate(-2.8577037,-1.6074583)');
        })
      ..onMouseLeave.listen((e){
          path
            ..setAttribute('style', style)
            ..setAttribute('transform', 'translate(0,0)');
      });
  }
And that does the trick. I now get drop shadows for all of my toppings:



Well, that might not be 100% working. I still need to figure out how best to handle the drawing area clipping, but this is getting very close to good enough. I will pick back up with the clipping tomorrow.


Day #74

No comments:

Post a Comment