Saturday, May 17, 2014

SVG as Images in Polymer.dart


I am dynamically generating the SVG in my <x-pizza> pizza-building Polymer. And for the life of me, I don't know why. But it may have been a lucky guess.

For my fist pass, the shapes were all circles:



The pizza was effectively inlined in the Polymer element's shadow DOM with a bit of JavaScript:
  _updateGraphic: function() {
    this.$['pizza-graphic'].innerHTML = '' +
      '<circle cx="150" cy="150" r="150" fill="tan" />' +
      '<circle cx="150" cy="150" r="140" fill="darkred" />' +
      '<circle cx="150" cy="150" r="135" fill="lightyellow" />';

    this._addWholeToppings();
    this._addFirstHalfToppings();
    this._addSecondHalfToppings();
  },
For now, I am going to leave that in place in my custom element. Instead, I will concentrate on loading an external SVG file for the toppings and adding them to the SVG.

I thought I had the hang of saving SVG from Inkscape, but when I create a 10x10 pixel pepperoni, set the document properties so that the page is 10x10 as well and save:



I always get a matrix transform in the resulting plain (not Inkscape formatted) SVG file:
<svg
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   version="1.1"
   width="10"
   height="10">
  <g transform="translate(0,-1042.3622)">
    <path
       d="m 239.99999,318.07648 a 34.285713,37.142857 0 1 1 -68.57142,0 34.285713,37.142857 0 1 1 68.57142,0 z"
       transform="matrix(0.14380379,0,0,0.13275542,-24.582993,1005.1358)"
       style="fill:#d40000;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
  </g>
</svg>
This may not be a huge deal—this is vector graphic manipulation, after all. Still, I bothers me that my SVG is being translated -1042.3622 pixels and then run through that transform matrix. Heh, I realize that I trying to get pixel perfection in a scalable vector graphic, which is just silly. So I press on for now (but I will revisit at some point).

Anyhow, I copy said pepperoni.svg into my Polymer library's asset directory, then try to use it in my Dart code thusly:
  _svgPepperoni() {
    var svgImage = new ImageElement(src: '/assets/svg_example/pepperoni.svg');
    return new GElement()
      ..append(svgImage);
  }
Which does not work at all—there is no matching constructor. Eventually, I realize that ImageElement is different than an <img> element. In order to load an SVG as an image, I need the XLINK namespace, which requires the setAttributeNS method:
  _svgPepperoni() {
    var svgImage = new ImageElement()
      ..setAttributeNS('http://www.w3.org/1999/xlink', 'href', '/assets/svg_example/pepperoni.svg')
      // ..setAttribute('href', '/assets/svg_example/pepperoni.svg')
      ..setAttribute('width', '10')
      ..setAttribute('height', '10');

    return new GElement()
      ..append(svgImage);
  }
(the commented out, non-namespace version does not work)

That's pretty ugly, but it works:



Ugh. This SVG stuff sure is a pain. I am unsure if Dart has any facilities for making this easier. The current approach is very much a JavaScript-y approach and it feels like it. It is hacky and of the barely-holding-duct-tape variety. Still, it does work, which is a start.


Day #66

1 comment:

  1. Great, please don't give up! We need to arrive to an usable polymer svg interactive element.

    About the issues you rise above:

    1 - Always getting a matrix transform in svg files : yes, I forgot to tell you that you need to delete all layers in Inkscape. Layers are useful for complex graphics, but not needed for simple web components like a button. If you remove all layers and just draw in the "root" page, the transform will disappear from the svg file. You just need to go to the "Layer" menu, and choose to open the "Layers..." dialog. Here you will see a list of all layers (like photoshop), and you just need to delete the default one. Inkscape create a default layer for you when you create a new document, but you don't need it.
    If you have some drawings already in the layer you can cut&paste those from Layer 1 to root, and then delete Layer 1.
    You can check anytime if you are drawing on the "root" layer looking at the current layer switch in the lower status bar of the Inkscape window, where you will read "(root)". Create a new document, delete the Layer 1 and draw your circles directly on the root layer. Then resize page to content as usual, and save. You will find that no transform has been added.
    NOTE: Anyway it's not that the transformation added with the layers changes the position of your path object. The transformation is perfectly neutral to the original object. In other words: the original path object (non transformed) is EQUAL to the saved path object with the saved transformation matrix applied to it. The result of object+transformation is identical to the original object. Only if you delete the transformation then the saved path object will result translated in a different position that the correct one. If you just use the svg file as is, it will just work.

    2 - SVG object in Dart: The Dart API has classes for managing SVG. For example:
    https://api.dartlang.org/apidocs/channels/stable/dartdoc-viewer/dart-dom-svg.SvgElement
    and:
    https://api.dartlang.org/apidocs/channels/stable/dartdoc-viewer/dart-dom-svg.SvgSvgElement

    Here is the function I use to load an svg file in Dart:

    //LOAD AN SVG FILE AND MAKE IT A BUTTON
    void load_svg_button(String svg_file_name) {

    String svg_content HttpRequest.getString(basedir(svg_file_name));
    SvgElement new_button = new SvgElement.svg(svg_content);

    String svg_width = new_button.attributes['width'];
    String svg_height = new_button.attributes['height'];

    // Some svgs use "pt" units which are not properly parsed
    svg_width = svg_width.replaceAll("pt", '');
    svg_height = svg_height.replaceAll("pt", '');

    num width = 50;
    num height = 50;

    new_button.attributes['width'] = '$height';
    new_button.attributes['height'] = '$width';
    new_button.attributes['viewBox'] = '0 0 $svg_width $svg_height';
    new_button.onClick.listen(toolbutton_OnClick);
    new_button.id = 'svg_toolbutton_${++svg_id}';
    querySelector("#toolbar").children.add(new_button);
    }

    ReplyDelete