Tuesday, May 27, 2014

Mouse Events and SVG Paths

I keep promising myself that I am going to stop this, but… there's a minor SVG problem that I'd like to solve tonight.

As an aside, I am likely to keep pushing through some of these smaller problems as I work through the remaining Dart for Hipsters errata. I cannot risk offending the writing gods by breaking this chain o’ learning, but I also need my primary focus to be on that book. Anyhow…

The problem tonight is that mouse over and leave events are causing some weird behavior with the SVG toppings in my <x-pizza> pizza-building Polymer element:

The problem occurs with mouse-overs. When I first approach the pepperoni topping:

I have not yet triggered the mouse-over event at this point. When I do, the pepperoni pops up:

The problem is that the pointer is no longer over the path that draws the SVG pepperoni so that even the slightest movement at this point will trigger a mouseleave. This mouseleave triggers an event the moves the pepperoni back in its original location, which causes the pepperoni to shift back under the pointer.

In other words, the pepperoni jumps back and forth until the pointer is in a location that is over the raised and rest pepperoni. It is a very annoying effect, especially with SVG elements that are not as solid as the pepperoni:

So this is a problem that I would very much like to eliminate. Of course, this is SVG, so it leads me into yet another unexpected tour of things that don't quite work like you expect them to work. For example, my initial suspicion for this problem fell on the combination of path, SVG container, and group element that hold the various toppings. It seemed plausible that the path was generating the jittering mouse events, but that I could ignore them and only pay attention to mouse events on the group or SVG element. But simple debugging in the mouse handlers:
  _svgImage(basename) {
    var svg = new GElement()
      ..append(new SvgElement.svg(svgContent['$basename.svg']));
    // ...
    return svg
          print('[mouseover] ${e.target}');
          // transforms here...
        // ...
Tells me that only [mouseover] path events are being generated. There are no events coming from the SVG topping asset or the group element that contains both. Only the path—the circle or spiral shape—generates events, which just ain't gonna work.

The solution to this turns out to be a bit of obscure SVG chicanery and some CSS chicanery. The obscure SVG solution is to disable events from the path by setting the pointer-events attribute to none. The CSS part of the solution is to create an SVG circle that does generate events, but to make it completely transparent:
  _svgImage(basename) {
    var svg = new GElement()
          new CircleElement()
            ..setAttribute('r', '15')
            ..setAttribute('cx', '15')
            ..setAttribute('cy', '15')
            ..setAttribute('style', 'opacity:0')
      ..append(new SvgElement.svg(svgContent['$basename.svg']));

      ..setAttribute('overflow', 'visible')
      ..setAttribute('clip', 'rect(-50px, 50px, 50px, -50px)')
      ..setAttribute('pointer-events', 'none');

    // Listen for events as usual, but now from the 100% transparent
    // circle
    return svg
      ..onMouseOver.listen((e){ /* ... */ })
      ..onMouseOut.listen((e){ /* ... */ });
That does the trick nicely. The 100% transparent circle SVG generates the same mouse-over and mouse-leave events that had been created by the path. The Dart streams still see those events and react exactly as before—only now there is no jumping since the thing that generates events does not move because of those events. And, since pointer-events is set to none, there are no events whatsoever from the SVG asset or the path that it contains.

That seems a fine stopping point for tonight. May the writing gods look upon me with favor—especially since I may revisit SVG clipping area tomorrow.

Day #76


  1. This comment has been removed by the author.

  2. I don't think that the mouseover and mouseout events are the right ones for your needs. Have you tried the mouseenter and mouseleave events? Those do not have the problem of rising multiple events from all group descendants. They rise only one event for the entire group. (Some old browsers do not support them, but there are many polyfills if you need that kind of compatibility.)

    1. I started with mouse enter / leave and only switched to over / out during a bout of frustration. Both seem to behave the same — at least in Dart. This is Chrome 34 (well Dartium, really) so it's not *that* old :-\