JavaScript
Article

Advanced Snap.svg

By Marcello La Rocca

We have seen, in an earlier post, how to get started with Snap.svg. In this post, we are going to take a closer look at the new features mentioned in the first article.

Masking

Let’s start by recalling how to create a drawing surface, a simple shape, and then load an image:

var paper = Snap(800, 600),
    img = paper.image('bigImage.jpg', 10, 10, 300, 300),
    bigCircle = s.circle(150, 150, 100);

The circle covers the center of the image, for now.

It is kind of a pity, though, that you can only have rectangular images. Maybe your designer created nice circular buttons or images. Of course, there are several solutions, but they all leave you with another problem: best case scenario, the designer can give you an image with the outside matching the page’s background, so that it will look circular. However, assuming you have a solid background, if you have to change its color, you’ll have to edit the image. You could use transparency, but you’d either need heavier formats like PNG, or loose quality with GIFs. In a few years, maybe, WebP will be fully supported by all browsers, and that would end the conundrum. Either way, if you need interactivity for your image, you’ll be stuck with a rectangular shape responding to events like mouseenter, mouseout, click, etc.

Having dealt with Flash for a long time in the past, one of the most frustrating things in SVG was not being able to use masks, introduced in SVG 1.1). In Snap, applying a mask to any element, including images, is quite easy:

bigCircle.attr('fill', '#fff'); //This is IMPORTANT

img.attr({
    mask: bigCircle
});

Basically, we just have to assign the mask property to our element. We have to be careful with the element we use as the actual mask. Since the opacity of the final element will be proportional to the level of white in the mask element, we must fill the circle in white if we want a full opacity for our image. While at first this might seem annoying, it opens a lot of possibilities for amazing effects, as we’ll see in the next sections.

You can obviously compose together different shapes to create complex masks. Snap offers some syntactic sugar to help you:

var smallRect = paper.rect(180, 30, 50, 40),
    bigCircle = paper.circle(150, 150, 100),
    mask = paper.mask(bigCircle, smallRect);

mask.attr('fill', 'white');

img.attr({
    mask: mask
});

The Paper.mask() method is equivalent to Paper.g(), and in fact it can be seamlessly replaced by it.

Clipping

Clipping paths restrict the region to which paint can be applied, so any parts of the drawing outside the region bounded by the currently active clipping path are not drawn. A clipping path can be thought of as a mask with visible areas (within the clipping path) have an alpha value of 1 and hidden areas have an alpha value of 0. The one difference is that while areas hidden by masks will nonetheless respond to events, clipped-out areas won’t.

Snap doesn’t have shortcuts for clipping, but you can set the clip, clip-path, and clip-route properties of any element using the attr() method.

Gradients

SVG 1.1 allows the use of gradients to fill shapes. Of course, if we use those shapes to fill a mask, we can leverage the possibility to specify the alpha level of the final drawing by changing the mask’s filling, and create astonishing effects. Snap provides shortcuts to create gradients, that can later be assigned to the fill property of other elements. If we modify the previous code just a little bit, for example:

var gradient = paper.gradient('r()#fff-#000');
mask.attr('fill', gradient);

If you test this code, the final effect won’t be exactly what you expected. That’s because we used the relative radiant gradient type, expressed by the lowercase ‘r’ above. Relative gradients are created separately for each element of a group (as a composite mask). If you prefer having a single gradient for the whole group, you can use the absolute version of the command. 'R()#fff-#000' is an absolute radiant gradient starting with white fill at the center and degrading to black at the borders.

We can get the same result by specifying the SVG gradient for the fill property of any element:

mask.attr('fill', 'L(0, 0, 300, 300)#000-#f00:25-#fff');

In this last example, we have shown a more complex gradient. Besides the different type (absolute linear), this gradient goes from (0, 0) to (300, 300), from black through red at 25% to white.

The gradient() method accepts a string. Further details are explained in Snap’s documentation.

It is also possible to use existing gradients from any svg element in the page:

<svg id="svg-test">
    <defs>
      <linearGradient id="MyGradient">
        <stop offset="5%" stop-color="#F60" />
        <stop offset="95%" stop-color="#FF6" />
      </linearGradient>
    </defs>
  </svg>
paper.circle(50, 50, 50, 50).attr('fill', Snap('#svg-test').select('#MyGradient'));

Patterns

Patterns allow to fill shapes by repeating occurrences of another svg shape, gradient, or image. Snap offers the Element.toPattern() method (formely, pattern(), now deprecated) that creates a pattern out of any Snap element.

Creating a pattern and filling an element with it is pretty straightforward:

var p = paper.path("M10-5-10,15M15,0,0,15M0-5-20,15").attr({
                      fill: "none",
                      stroke: "#bada55",
                      strokeWidth: 5
                  }).toPattern(0, 0, 10, 10),
    c = paper.circle(200, 200, 100).attr({
                                            fill: p
                                        });

If, instead, we would like to combine gradients and patterns, that’s a different story, and a slightly more complicated one!
As an example, let’s see how to create a mask that combines a radiant gradient and a pattern similar to the one above:

//assuming the shapes bigCircle and smallRect have already been defined, as well as 'paper'
var mask = paper.g(bigCircle, smallRect),
    gradient = paper.gradient("R()#fff-#000"),
    pattern = paper.path("M10-5-10,15M15,0,0,15M0-5-20,15").attr({
        fill: "none",
        stroke: "#bada55",
        strokeWidth: 5
    }).toPattern(0, 0, 10, 10);

mask.attr('fill', pattern); //we need to set this before calling clone!
mask.attr({
    mask: mask.clone()      //makes a deep copy of current mask
});

img.attr({
    mask: mask
});

We basically have to create a two level map. The final map used on our image, which we fill with a gradient, has a map itself filled with a gradient. The result is quite impressive! Turns out this was also a good opportunity to introduce you the clone() method, which does what you would imagine – creates a deep copy of the element it is called on.

Animations

Animations are one of Snap.svg’s best crafted features. There are several ways to handle animations, with slightly different behaviors.

Element.animate()

We will start with the simplest animation method, Element.animate(). This method allows users to animate any number of an element’s properties, all in sync. The initial value for the property is, of course, its current value, while the final one is specified in the first argument to animate(). Besides the properties to be changed, it is possible to pass the duration of the animation, its ease, and a callback that will be called once the animation is completed.

An example will make everything clearer:

bigCircle.animate({r: 10}, 2000);

This will simply shrink the big circle in our mask to a smaller radius over the course of two seconds.

Set.animate()

You can animate elements in a group (set) independently. But, what if you want to animate the whole set synchronously? Easy! You can use Set.animate(). This will apply the same transformation to all the elements in the set, ensuring synchronicity among the various animations, and enhancing performance by gathering all the changes together.

mask.animate({'opacity': 0.1}, 1000);

You can also animate each element in a set independently, but synchronously. Set.animate() accepts a variable number of arguments, so that you can pass an array with the arguments for each sub-element you need to animate:

var set = mask.selectAll('circle');  //Create a set containing all the circle elements in mask's subtree (1 element)
paper.selectAll('rect')                //Select all the rect in the drawing surface (2 elements)
        .forEach(function(e) {set.push(e);}); //Add each of those rectangles to the set previously defined
set.animate([{r: 10}, 500], [{x: 20}, 1500, mina.easein], [{x: 20}, 1500, mina.easein]); //Animate the three elements in the set

Assuming you have correctly followed our example code so far (try it on CodePen), running the code above in your browser’s console you’ll see how the three elements gets animated in sync, but independently. The code above was also a chance to introduce sets (as the results of the select() and selectAll() methods), and a few useful methods defined on them.

Another way to create a set is by passing an array of elements to the Snap constructor method:

var set2 = Snap([bigCircle, smallRect]);

Snap.animate()

You can animate any numeric property, but animate() won’t work on other types, for example it will mess up your text elements if you try to animate their text attribute. Yet there is another way to gain such an effect, i.e. the third way to call animate() in Snap.

By calling the animate method of the Snap object it is possible to specify in further details the actions that will be executed at each step of the animation. This helps both grouping together complex animation and running them in sync (although the Set.animate() method would be the right way to deal with this problem), and to animate complex, non numeric properties.

For instance, let’s create and animate a text element:

var labelEl = paper.text(300, 150, "TEST"),
    labels = ["TEST", "TETT","TEUT","TEVT","TEXT","TES-","TE--","T---", "----", "C---", "CH--", "CHE-", "CHEC-", "CHECK"];
Snap.animate(0, 13, function (val) {
    labelEl.attr({
        text: labels[Math.floor(val)]
    });
}, 1000);

Event Handling

Going back to the initial comparison between masks and images, you could obtain the same effect we have shown in the previous section with an animated gif (sort of). If, however, you want to reproduce this behavior in response to user interaction, the improvement using SVG is even more relevant. You can still find a way to make it work using multiple gifs, but, besides loosing flexibility, you won’t be able to get the same quality with as little effort:

img.click(function(evt) {
    this.minified = !this.minified;
    bigCircle.animate({
        r: !this.minified ? 100 : 10
    }, 1500);
});

Click handlers can be later removed using the Element.unclick() method.

Among other events that can be handled similarly there are dblclick, mousedown and mouseup, mousemove, mouseout and mouseover, and a number of mobile-oriented events, like touchstart and touchend.

For those of our readers used to jQuery or D3 interfaces, there in no on() method in Snap to manually handle other events. If you need a custom behavior that goes beyond the handlers offered by Snap, you can retrieve the node property for any element, which in turn contains a reference to the associated DOM element, and (possibly after wrapping it in jQuery) you can add handlers and properties to it directly:

img.node.onclick = function () {
    img.attr("opacity", 0.1);
};

Drag and Drop

Snap makes particularly easy activating drag and drop for any element, group or set using the Element.drag() method. If you don’t need any custom behavior, you can call it without any arguments:

labelEl.drag();   //handle drag and drop for you

However, if you need some special behavior, you can pass custom callbacks and contexts for the onmove, ondragstart, ondragend events. Be aware that you can’t omit the onmove callback if you want to pass the next ones.

Adding a drag handler will not hide the click event, that will be fired after the ondragend one, unless explicitly prevented.

Load Existing SVG

One of the strongest points of this great library is that it supports reuse of existing SVG code. You can “inject” it as a string, or even better you can read an existing file, and then change it.

You can try it yourself. Download and save into your project’s root this nice svg drawing. Next load it into your page, change its style or structure as we like, even before adding it to our DOM tree, add event handlers, etc.

Snap.load('ringing-phone.svg', function (phone) {
    // Note that we traverse and change attr before SVG is even added to the page (improving performance)
    phone.selectAll("path[fill='#ff0000']").attr({fill: "#00ff00"});
    var g = phone.select("g");
    paper.append(g);    //Now we add the SVG element to the page
});

Note: Because of the same-origin policy in browsers, you’ll need to run the example in a local server to test the load method.

Performance Improvements

One way to improve performance when manipulating the DOM is using DocumentFragments. Fragments are minimal containers for DOM nodes. Introduced a few years ago, they allow you to inexpensively manipulate entire subtrees, and then clone and add a whole subtree with n nodes to our page with 2 method calls instead of n. The actual difference is explained in details on John Resig’s blog.

Snap allows for native use of fragments as well, with two methods:

  1. Snap.parse(svg) takes a single argument, a string with SVG code, parses it and returns a fragment that can be later appended to any drawing surface.

  2. Snap.fragment(varargs) takes a variable number of elements or strings, and creates a single fragment containing all the elements provided.

Especially for large svg drawings, fragments can lead to a huge performance saving, when used appropriately.

Conclusion

This concludes our article on advanced Snap.svg. Now readers should have a clear idea of what they can do with this library, and how to do it. If you are interested in learning a little more, Snap documentation is a good place to start.

A couple of useful links:

  • rguego

    Good Post.

    i try to set event in another context and not work :(, do you have an example than can help me?

    var r = s.rect(150,150, 25,25);

    r.hover(
    function (mouseEvent, mouseX,mouseY) { console.log(‘hover in’);},
    function(mouseEvent,mouseX, mouseY) {console.log(‘hover out’);}
    );

    above code work rigth, after hover mouse over rect i can see the messages in the console

    but i want to separate the evenst code

    var events = {

    hoverIn: function (mouseEvent, mouseX,mouseY) { console.log(‘hover in’);}
    hoverOut: function(mouseEvent,mouseX, mouseY) {console.log(‘hover out’);}
    };

    r.hover(events.hoverIn, events.hoverOut); // not work :(

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in JavaScript, once a week, for free.