Monday, May 26, 2014

Clipping in SVG


I cannot help but think I still have several, fundamental gaps in my SVG knowledge. This is OK. Well… mostly OK.

My primary focus nowadays is Polymer, not SVG. As I ready to produce the screencasts for the “Extras” edition of Patterns in Polymer I am trying to gain some understanding of SVG so that I can produce an <x-pizza> custom element that enables hungry folks to build their own pizzas:



I am almost certainly over-thinking the SVG component of this, but I hate to recommend an approach that proves difficult or impossible to maintain. All the more so if the recommendation is going into video. I am going to have a hard enough time producing videos that will withstand 6 months of Polymer development without worrying about messing up something as old as SVG.

So tonight, I take one more try at getting the individual toppings to popup and generate a drop shadow without getting clipped:



Those toppings are loaded from SVG assets and inserted directly into the base pizza SVG, <svg> tag and all. This seems to work just fine save for the clipping when I translate the topping SVG inside the base SVG. My first fix try is to set overflow=visible. I start with the generic Dart method that takes the previously loaded asset to create a new topping:
  _svgImage(basename) {
    var svg = new GElement()
      ..append(new SvgElement.svg(svgContent['$basename.svg']));

    svg.setAttribute('overflow', 'visible');
    svg.query('svg').setAttribute('overflow', 'visible');

    var path = svg.query('path');
    path.setAttribute('overflow', 'visible');
    var style = path.getAttribute('style');

    return svg
      ..onMouseOver.listen((e){ /* ... */ })
      ..onMouseLeave.listen((e){ /* ... */ });
  }
And, although that attribute is seen in the resulting DOM, it has no effect on the clipping of my topping elements:



And, unfortunately, I find similar results when I try to set the clipping area:
    // ...
    svg
      ..setAttribute('overflow', 'visible')
      ..setAttribute('clip', 'rect(-50px, 50px, 50px, -50px)');

    svg.query('svg')
      ..setAttribute('overflow', 'visible')
      ..setAttribute('clip', 'rect(-50px, 50px, 50px, -50px)');

    var path = svg.query('path')
      ..setAttribute('overflow', 'visible')
      ..setAttribute('clip', 'rect(-50px, 50px, 50px, -50px)');
    // ...
For what it is worth, setting the clip property on the SVG asset does have an effect. If I make it small (20 pixels on a 32 square pixel image):

    // ...
    svg.query('svg')
      ..setAttribute('overflow', 'visible')
      ..setAttribute('clip', 'rect(-50px, 20px, 20px, -50px)');
    // ...
Then the clipping area is noticeably decreased:



But there does not seem to be a way to set the clipping area to negative values so that it expands outside the original image. Bother.

In the end, I give up. Well, I get it working, but in a completely unsatisfactory manner. I create the SVG images with the drawings shifted slightly to the top-left corner. Then, in the _svgImage() method, I make the rest position of the toppings shifted down and to the right (i.e. more toward the center of the image):
  _svgImage(basename) {
    var svg = new GElement()
      ..append(new SvgElement.svg(svgContent['$basename.svg']));

    svg.query('svg')
      ..setAttribute('overflow', 'visible')
      ..setAttribute('clip', 'rect(-50px, 50px, 50px, -50px)');

    var path = svg.query('path')
      ..setAttribute('transform', 'translate(2.8577037,1.6074583)');
    var style = path.getAttribute('style');

    return svg
      ..onMouseOver.listen((e){
          path
            ..setAttribute('style', '$style;filter:url(#topping-drop-shadow)')
            ..setAttribute('transform', 'translate(0,0)');
        })
      ..onMouseLeave.listen((e){
          path
            ..setAttribute('style', style)
            ..setAttribute('transform', 'translate(2.8577037,1.6074583)');
      });
  }
The mouse-enter and mouse-leave stream listeners now have to adjust accordingly.

Which does the trick:



Not wanting to turn this into an SVG chain, I will probably call a halt to clipping investigation there. The solution is not at all satisfactory, but it works. And if any viewers know how to do it right, they ought to be able to mentally correct this approach without much effort.


Day #75

2 comments:

  1. I think that your problem is caused by the lack of a viewBox. I don't see the "viewBox" attribute defined in your svg root.
    The svg viewBox is sometimes poorly understood, but you can think of it as simply the ORIGINAL SIZE OF YOUR SVG DRAWING.

    If your SVG image is a 50x50 circle, then your viewBox is:
    viewBox="0 0 50 50"

    The viewBox is NOT the size of the SVG image in the browser.
    The height and the weight attributes of the SVG element define the size of the displayed SVG.
    Inkscape saves those two values (using the document page size), BUT does not save the viewBox.
    You have to manually add the viewBox attribute. The numbers should be (if in pixel) the exact dimension of the entire drawing.
    You can easily find the values looking at the valus displayed in the top toolbar of Inkscape when you "select All.." objects and groups.
    Effects are included in the selection, so the total area of the drawing is correctly showed as 4 numbers: x-offset, y-offset, width, height. The offsets are different than 0 when the page size is too small to contains some elements above or at the left of the page rectangle.
    The important thing is that the viewBox must always be equal to the drawing bounding box, including ALL elements.

    NOTE: Changing those values would ALWAYS stretch, scale or crop the original SVG image! In fact the viewBox attribute is sometimes used to get a slice, zoom, scroll or pan effect on the original SVG image! The preserveAspectRatio attribute is used to force the original aspect ratio when doing those deformations, but by default is better to use only the original dimensions in the viewBox.
    In my opinion the lack of a defined viewBox is the culprit of the strange behaviour of your code. Usually the document size and the viewBox are of the same size, so Inkscape saves only the former (Illustrator, for example, saves also the viewbox). But when document size and real drawing dimensions are not equal, then the viewBox attribute is needed.

    You can look at this screenshot I made to understand better how to fix:
    http://oi58.tinypic.com/15gd0tu.jpg

    ReplyDelete
    Replies
    1. Awesome sauce. I'll give it whirl tonight. That does look like it'll solve my problems -- I was not at all aware of the viewBox setting. Thanks!

      Delete