Get to know Fabric.js. Part 2

    This is the second part of a series of articles about the open Javascript canvas library Fabric.js.

    In the first part of this series, we introduced the most basic aspects of the canvas fabric.js library . We learned how Fabric can be useful, examined its object model and hierarchy of objects; saw that there are both simple shapes (rectangle, triangle, circle), and complex (SVG). Learned how to perform simple operations on these objects.

    Well, we’ve figured out the basics, let's get down to more interesting things!


    Animation


    Any self-respecting canvas library nowadays includes animation tools. Fabric is no exception. After all, we have a powerful object model and flexible graphical capabilities. It would be a sin not to be able to set this in motion.

    You probably remember how to change the attribute of an object. We just call the method set, passing the appropriate value:

    rect.set('angle', 45);
    

    You can animate an object by the same principle and with the same ease. Each object in Fabric has a method animate(inheriting from fabric.Object) that ... animates this object.

    rect.animate('angle', 45, {
      onChange: canvas.renderAll.bind(canvas)
    });
    

    The first argument is the attribute we want to change. The second argument is the final value of this attribute. For example, if the rectangle is at an angle of -15 °, and we indicate “45”, then the angle will gradually change from -15 ° to 45 °. Well, the last argument is an optional object for more detailed settings (duration, calls, easing, etc.), by the

    animateway, it has very useful functionality - support for relative values. For example, if you want to move the object 100px to the right, then this is very simple:

    rect.animate('left', '+=100', { onChange: canvas.renderAll.bind(canvas) });
    

    According to the same principle, to rotate an object 5 degrees counterclockwise:

    rect.animate('angle', '-=5', { onChange: canvas.renderAll.bind(canvas) });
    

    You probably noticed that we constantly indicate the call of "onChange". Isn't the 3rd argument optional? Yes exactly. The fact is that just this invocation canvas.renderAllfor each frame of the animation allows you to see the animation itself! The method animateonly changes the value of the attribute within the specified time, and according to a certain algorithm (easing). rect.animate('angle', 45)changes the value of the angle, without redrawing the screen after each change. And redrawing the screen is needed in order to see the animation.

    Well, why animatenot redraw the screen automatically? Due to performance. After all, hundreds or even thousands of objects can be on the canvas. It would be pretty awful if each of the objects redraws the screen when changing. In this case, it is better to use for examplerequestAnimationFrameto constantly draw the canvas without calling renderAllfor each object. However, in most cases, you will most likely use it canvas.renderAllas an “onChange” call.

    Returning to the options for animation, what exactly can we change?

    • from : Allows you to change the initial attribute value for the animation (if you do not want to use the current one).
    • duration : The duration of the animation. The default is 500 (ms).
    • onComplete : Function to call at the end of the animation.
    • easing : The easing function.


    All these options are more or less obvious, except probably easing. Let's take a closer look.

    By default, they animateuse a linear function to soften the animation. If this option is not suitable, Fabric has a large set of popular easing functions (accessible through the object fabric.util.ease). For example, like this, you can move the object to the right, while springing at the end:

    rect.animate('left', 500, {
      onChange: canvas.renderAll.bind(canvas),
      duration: 1000,
      easing: fabric.util.ease.easeOutBounce
    });
    

    Notice what we use fabric.util.ease.easeOutBounceas a mitigation option. There are other popular features - easeInCubic, easeOutCubic, easeInElastic, easeOutElastic, easeInBounce, easeOutExpo,, etc.

    That's basically all you need to know about animation. Now you can easily do interesting things - change the angle of an object to make it rotate; animate left / top to move it; animate width / height to increase / decrease; animate opacity to appear / disappear; etc.

    Image filters


    In the first part of this series, we learned how to work with images in Fabric. As you probably remember, a fabric.Imageconstructor is used for this , passing an element to it . There is also a method fabric.Image.fromURLby which you can create an object directly from a URL string. And of course, these fabric.Imageobjects can be thrown onto the canvas where they will be displayed like everything else.

    Working with images is fun, and with image filters - even more fun!

    Fabric already has several filters, and also allows you to easily define your own. Some filters from Fabric are probably familiar to you - removal of a white background, conversion to black and white, negative or brightness. And some are less popular - gradient transparency, sepia, noise.

    So how do you apply a filter to an image? Eachfabric.Imagethe object has a “filters” attribute, which is simply an array of filters. Each element in this array is either one of the existing ones in Fabric or its own filter.

    Well, for example, let's make the picture black and white:

    fabric.Image.fromURL('pug.jpg', function(img) {
      // добавляем фильтр
      img.filters.push(new fabric.Image.filters.Grayscale());
      // применяем фильтры и перерисовываем канвас после применения
      img.applyFilters(canvas.renderAll.bind(canvas));
      // добавляем изображения на холст
      canvas.add(img);
    });
    



    And here you can make sepia:

    fabric.Image.fromURL('pug.jpg', function(img) {
      img.filters.push(new fabric.Image.filters.Sepia());
      img.applyFilters(canvas.renderAll.bind(canvas));
      canvas.add(img);
    });
    



    With the attribute «filters» can do everything the same as with the usual array - remove the filter (using the pop, splice, or shift), add a filter (using push, splice, unshift), or even to combine multiple filters. When called applyFilters, all filters in the array are applied to the picture in turn. Here, for example, let's create a picture with increased brightness and with a sepia effect:

    fabric.Image.fromURL('pug.jpg', function(img) {
      img.filters.push(
        new fabric.Image.filters.Sepia(),
        new fabric.Image.filters.Brightness({ brightness: 100 }));
      img.applyFilters(canvas.renderAll.bind(canvas));
      canvas.add(img);
    });
    



    Notice that we passed the { brightness: 100 }object to the Brightness filter. This is because some filters do not need anything extra, and some (for example grayscale, invert, sepia) need to specify certain parameters. For a brightness filter, this is the brightness value itself (0-255). For a noise filter, this is the noise value (0-1000). And the filter remove the white background ("remove white"), has a threshold (threshold) and distance (distance).

    Well, sorted out the filters; it's time to create your own!

    A sample for creating filters will be quite simple. We need to create a "class", and write a method applyTo. Optionally, we can give the filter a toJSONmethod (support for JSON serialization), and / or initialize(if the filter has additional parameters).

    fabric.Image.filters.Redify = fabric.util.createClass({
      type: 'Redify',
      applyTo: function(canvasEl) {
        var context = canvasEl.getContext('2d'),
            imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height),
            data = imageData.data;
        for (var i = 0, len = data.length; i < len; i += 4) {
          data[i + 1] = 0;
          data[i + 2] = 0;
        }
        context.putImageData(imageData, 0, 0);
      }
    });
    fabric.Image.filters.Redify.fromObject = function(object) {
      return new fabric.Image.filters.Redify(object);
    };
    




    Without delving deeply into the details of the code, it is worth noting that the most important thing happens in the loop, where we change the green (data [i + 1]) and blue (data [i + 2]) components of each pixel to 0, essentially deleting them. The red component remains untouched, which makes the entire image red. As you can see, the applyTomethod receives a canvas element, which is an image. Having such a canvas, we can go through all the pixels of the image ( getImageData().data) changing them as we please.

    Colors


    No matter what you prefer working with — hex, RGB, or RGBA color formats — Fabric simplifies the tedious operations and transfers from one format to another. Let's look at a few ways to define color in Fabric:

    new fabric.Color('#f55');
    new fabric.Color('#123123');
    new fabric.Color('356735');
    new fabric.Color('rgb(100,0,100)');
    new fabric.Color('rgba(10, 20, 30, 0.5)');
    

    Format translation is very simple. toHex()translates color to hex. toRgb()- in RGB, and toRgba()- in RGB with alpha channel (transparency).

    new fabric.Color('#f55').toRgb(); // "rgb(255,85,85)"
    new fabric.Color('rgb(100,100,100)').toHex(); // "646464"
    new fabric.Color('fff').toHex(); // "FFFFFF"
    

    By the way, you can do more than just translation. You can "overlay" the colors one on top of the other, or make them a black and white version.

    var redish = new fabric.Color('#f55');
    var greenish = new fabric.Color('#5f5');
    redish.overlayWith(greenish).toHex(); // "AAAA55"
    redish.toGrayscale().toHex(); // "A1A1A1"
    

    Gradients


    An even more expressive way to work with colors is to use gradients. Gradients allows you to smoothly mix one color with another, opening up the possibility of quite amazing effects.

    Fabric supports them using a method setGradientthat is present on all objects. Invoking setGradient('fill', ...)it is almost like setting the fill value of an object, only a gradient is used instead of color.

    var circle = new fabric.Circle({
      left: 100,
      top: 100,
      radius: 50
    });
    circle.setGradient('fill', {
      x1: 0,
      y1: 0,
      x2: 0,
      y2: circle.height,
      colorStops: {
        0: '#000',
        1: '#fff'
      }
    });
    



    In this example, we create a circle at 100,100, with a radius of 50px. Then we set the gradient to it, running along the entire height of the object, from black to white.

    As you can see, the method receives a configuration object, which may contain 2 pairs of coordinates (x1, y1 and x2, y2), and a colorStops object. Coordinates indicate where the gradient begins and where it ends. colorStops indicate what colors it consists of. You can define as many colors as you like; the main thing is that their positions are in the range from 0 to 1 (for example, 0, 0.1, 0.3, 0.5, 0.75, 1). 0 represents the beginning of the gradient, 1 represents its end.

    Here is an example of a red-blue gradient going from left to right:

    circle.setGradient('fill', {
      x1: 0,
      y1: circle.height / 2,
      x2: circle.width,
      y2: circle.height / 2,
      colorStops: {
        0: "red",
        1: "blue"
      }
    });
    



    And here is a 5-step gradient rainbow, with flowers occupying 20% ​​of the entire length:

    circle.setGradient('fill', {
      x1: 0,
      y1: circle.height / 2,
      x2: circle.width,
      y2: circle.height / 2,
      colorStops: {
        0: "red",
        0.2: "orange",
        0.4: "yellow",
        0.6: "green",
        0.8: "blue",
        1: "purple"
      }
    });
    



    Can you come up with something interesting?

    Text


    What if you need to display not only pictures and vector shapes on canvas, but also text? Fabric can do it too! Meet me fabric.Text.

    Before talking about the text, it is worth noting why we generally provide support for working with text. After all, canvas has built-in methods fillText and strokeText.

    Firstly, in order to be able to work with text as objects. Built-in canvas methods - as usual - allow you to display text at a very low level. But fabric.Textafter creating an object of type , we can work with it like with any other object on the canvas - move it, scale it, change attributes, etc.

    The second reason is to have richer functionality than what canvas gives us. Some things that are in Fabric but not in native methods:

    • Multi-line . Native methods allow you to write only one line, ignoring line breaks.
    • Text alignment . Left, center, right. Useful when working with multi-line text.
    • Text background . The background is displayed only under the text itself, depending on the alignment.
    • The scenery of the text . Underscore, overline, strikeout.
    • The row height . Useful when working with multi-line text.


    Well then, let's look at the ubiquitous "hello world"?

    var text = new fabric.Text('hello world', { left: 100, top: 100 });
    canvas.add(text);
    

    That's all! To display text, you just need to add a type object fabric.Textto the canvas, indicating the desired position. The first parameter is needed - this is actually the line of text itself. The second argument is an optional configuration, as usual; You can specify left, top, fill, opacity, etc.

    In addition to the usual attributes, text objects certainly have their own, related to the text. In short, about these attributes:

    fontFamily


    The default is Times New Roman. Allows you to change the font family for text.

    var comicSansText = new fabric.Text("I'm in Comic Sans", {
      fontFamily: 'Comic Sans'
    });
    



    fontSize



    Controls the size of the text. Note that unlike other objects in Fabric, we cannot resize text using width / height. Instead, fontSize is used, and of course scaleX / scaleY.

    var text40 = new fabric.Text("I'm at fontSize 40", {
      fontSize: 40
    });
    var text20 = new fabric.Text("I'm at fontSize 20", {
      fontSize: 20
    });
    



    fontWeight



    Allows you to make the text fatter or thinner. Just like in CSS, you can use either words (“normal”, “bold”) or numerical values ​​(100, 200, 400, 600, 800). It is important to understand that for a certain thickness you need to have an appropriate font. If the font does not have a “bold” (bold) option, for example, then bold text may not be displayed.

    var normalText = new fabric.Text("I'm a normal text", {
      fontWeight: 'normal'
    });
    var boldText = new fabric.Text("I'm a bold text", {
      fontWeight: 'bold'
    });
    



    textDecoration



    Allows you to add strikethrough, underlining, or underlining to text. Again, this declaration works just like in CSS. However, Fabric can do a bit more, allowing you to use these decorations together (such as underlining and strikethrough) by simply listing them with a space.

    var underlineText = new fabric.Text("I'm an underlined text", {
      textDecoration: 'underline'
    });
    var strokeThroughText = new fabric.Text("I'm a stroke-through text", {
      textDecoration: 'line-through'
    });
    var overlineText = new fabric.Text("I'm an overline text", {
      textDecoration: 'overline'
    });
    



    shadow



    Shadow for the text. It consists of 4 components: color, horizontal indentation, vertical indentation, and blur size. This should all be familiar if you've worked with shadows in CSS before. By changing these 4 options, you can achieve many interesting effects.

    var shadowText1 = new fabric.Text("I'm a text with shadow", {
      shadow: 'rgba(0,0,0,0.3) 5px 5px 5px'
    });
    var shadowText2 = new fabric.Text("And another shadow", {
      shadow: 'rgba(0,0,0,0.2) 0 0 5px'
    });
    var shadowText3 = new fabric.Text("Lorem ipsum dolor sit", {
      shadow: 'green -5px -5px 3px'
    });
    



    fontStyle



    Text style. There can be only one of two: “normal” or “italic”. Again, it works the same as in CSS.

    var italicText = new fabric.Text("A very fancy italic text", {
      fontStyle: 'italic',
      fontFamily: 'Delicious'
    });
    var anotherItalicText = new fabric.Text("another italic text", {
      fontStyle: 'italic',
      fontFamily: 'Hoefler Text'
    });
    



    stroke and strokeWidth



    By combining stroke (the color of the outer stroke) and strokeWidth (the width of the outer stroke), pretty interesting effects can be achieved. Here are a couple of examples:

    var textWithStroke = new fabric.Text("Text with a stroke", {
      stroke: '#ff1318',
      strokeWidth: 1
    });
    var loremIpsumDolor = new fabric.Text("Lorem ipsum dolor", {
      fontFamily: 'Impact',
      stroke: '#c3bfbf',
      strokeWidth: 3
    });
    



    textAlign



    Alignment is useful when working with multi-line text. In a single-line text, alignment is not visible, because the width of the text object itself is the same as the length of the line.

    Possible values ​​are “left”, “center”, “right”, and “justify”

    var text = 'this is\na multiline\ntext\naligned right!';
    var alignedRightText = new fabric.Text(text, {
      textAlign: 'right'
    });
    



    lineHeight



    Another attribute most likely familiar from CSS is lineHeight (line height). Allows you to change the distance between lines in multi-line text. Here is an example of text with lineHeight 3, and the second with lineHeight 1.

    var lineHeight3 = new fabric.Text('Lorem ipsum ...', {
      lineHeight: 3
    });
    var lineHeight1 = new fabric.Text('Lorem ipsum ...', {
      lineHeight: 1
    });
    



    textBackgroundColor



    Finally, you can give the text a background using textBackgroundColor. Note that the background is filled only under the text itself, and not on the entire "box". To paint over the entire text object, you can use the attribute "backgroundColor". You can also see that the background depends on the alignment of the text and lineHeight. If lineHeight is very large, the background will only be visible under the text.

    var text = 'this is\na multiline\ntext\nwith\ncustom lineheight\n&background';
    var textWithBackground = new fabric.Text(text, {
      textBackgroundColor: 'rgb(0,200,0)'
    });
    



    Events


    Events are an indispensable tool for creating complex applications. For ease of use, and more detailed configuration, Fabric has an extensive event system; starting from low-level mouse events, and up to high-level events of objects.

    Events allow us to “catch” various moments when something happens on the canvas. Want to know when the mouse was clicked? We follow the mouse: down event. How about when an object was added to the canvas? There is an “object: added” for this. Well, what about redrawing the canvas? We use "after: render".

    The event API is very simple, and similar to what you are most likely used to in jQuery, Underscore.js, or other popular JS libraries. There is a method onfor initializing an event listener, and there is a method offfor deleting it.

    Let's look at an example:

    var canvas = new fabric.Canvas('...');
    canvas.on('mouse:down', function(options) {
      console.log(options.e.clientX, options.e.clientY);
    });
    

    We added a “mouse: down” event listener to the canvas object, and specified a handler that would record the coordinates of where this event occurred. Thus, we can see exactly where the click on the canvas occurred. The event handler receives an options object, with two parameters: e- the original event, and target- the Fabric object on the canvas, if it is found. The first parameter is always present, but targetonly if a click has occurred on the object. And of course, it is targettransmitted only to the developers of those events where it makes sense. For example, for “mouse: down” but not for “after: render” (since this event does not “have” any objects, it simply means that the canvas was redrawn).

    canvas.on('mouse:down', function(options) {
      if (options.target) {
        console.log('an object was clicked! ', options.target.type);
      }
    });
    

    This example will output “an object was clicked!” If we click on an object. The type of this object will also be shown.

    What other events are available in Fabric? At the mouse level, we have " mouse: down ", " mouse: move ", and " mouse: up ". Of the general ones, there is " after: render ". There are events regarding the selection of objects: " before: selection: cleared ", " selection: created ", " selection: cleared ". And of course, the events of the objects: " object: modified ", " object: selected ", " object: moving ", " object: scaling ", "".

    It is worth noting that events of the type" object: moving "(or" object: scaling ") occur continuously, while moving or scaling an object, even if it is one pixel. At the same time, events of the type" object: modified "or "Selection: created" occurs only at the end of the action (changing the object, creating a group of objects, etc.).

    In the previous examples, we attached the listener to the canvas object ( canvas.on('mouse:down', ...)). As you might guess, this means that events only apply to that the canvas to which we attached them.If you have several canvases on the page, you can give them knowledgeable listeners. Events on one canvas do not apply to other canvases.

    For convenience, Fabric allows you to add listeners directly to Fabric objects!

    var rect = new fabric.Rect({ width: 100, height: 50, fill: 'green' });
    rect.on('selected', function() {
      console.log('selected a rectangle');
    });
    var circle = new fabric.Circle({ radius: 75, fill: 'blue' });
    circle.on('selected', function() {
      console.log('selected a circle');
    });
    

    In this example, the listeners “join” directly to the rectangle and the circle. Instead of “object: selected”, we use the “selected” event. By the same principle, you can use the event "modified" ("object: modified" when "hang" on the canvas), "rotating" (analogue of "object: rotating"), etc.

    You can get acquainted with the events closer and directly in real time in this demo .

    On this, the second part came to an end. So many new things, but that's not all! In the 3rd part, we will consider groups of objects, serialization / deserialization of the canvas and the JSON format, SVG parser, as well as the creation of subclasses.


    Also popular now: