📜 ⬆️ ⬇️

We get acquainted with Fabric.js. Part 3

This is the translation of the third part of a series of articles on the open Javascript canvas library Fabric.js.

We covered the main part of the basic material on Fabric in the first and second parts of this series. This article will provide more in-depth material.

Groups


image

The first thing we'll talk about is groups. Object grouping is one of the most powerful tools in Fabric. Why do we need to group objects? By itself, in order to work with a variety of objects as a whole.
')
Remember how we grouped any number of objects with the mouse? Once grouped, they can simultaneously move, scale, twist, and even change the appearance properties - color, transparency, frame, etc.

That is what groups exist for. Each time you see a selection on canvas (as in the image above), Fabric implicitly, within itself, creates groups so that you can work with them programmatically later. This is the main meaning of fabric.Group .

Create a group of 2 objects, a circle and a text:

 var circle = new fabric.Circle({ radius: 100, fill: '#eef', scaleY: 0.5, originX: 'center', originY: 'center' }); var text = new fabric.Text('hello world', { fontSize: 30, originX: 'center', originY: 'center' }); var group = new fabric.Group([ circle, text ], { left: 150, top: 100, angle: -10 }); canvas.add(group); 

First, we created a text object “hello world”. The originX and originY properties have originX originY to 'center' , which will 'center' this object within the group. At initial settings, group members are oriented relative to the upper left corner of the group. Then a circle with a radius of 100px was filled with the color "#eef" and squeezed vertically (compression ratio - 0.5). Next, create a fabric.Group object with two parameters. The first parameter is an array of our 2 objects. The second parameter set the group position 150/100 and the angle -10. Finally, they added it using the canvas.add() method like any other object.

And voila! You see an object on the canvas that looks like an ellipse with an inscription. Note that it is easy to modify it by changing the properties of the group. You can work with this object as a whole.

image

Now we have a group on canvas. Let's change it a little bit.

 group.item(0).setFill('red'); group.item(1).set({ text: 'trololo', fill: 'white' }); canvas.renderAll(); 

What's going on here? We accessed objects within the group using the item() method and changed their properties. The first object is a compressed circle, the second is text. Let's see what happened:

image

The important thing you probably noticed is that the objects of the group are aligned with the center of the group. When we changed the text-object text, it remained centered, even after changing its width. This behavior can be canceled by setting the coordinates of the object (left / top).

Let's create and group 3 circles so that they are located horizontally one after another.

 var circle1 = new fabric.Circle({ radius: 50, fill: 'red', left: 0 }); var circle2 = new fabric.Circle({ radius: 50, fill: 'green', left: 100 }); var circle3 = new fabric.Circle({ radius: 50, fill: 'blue', left: 200 }); var group = new fabric.Group([ circle1, circle2, circle3 ], { left: 200, top: 100 }); canvas.add(group); 


image

Working with groups, you need to pay attention to the state of objects . For example, when forming a group of images, you need to make sure that they are fully loaded. Fortunately, Fabric has a turnkey solution:

 fabric.Image.fromURL('/assets/pug.jpg', function(img) { var img1 = img.scale(0.1).set({ left: 100, top: 100 }); fabric.Image.fromURL('/assets/pug.jpg', function(img) { var img2 = img.scale(0.1).set({ left: 175, top: 175 }); fabric.Image.fromURL('/assets/pug.jpg', function(img) { var img3 = img.scale(0.1).set({ left: 250, top: 250 }); canvas.add(new fabric.Group([ img1, img2, img3], { left: 200, top: 200 })) }); }); }); 

image

There are some more important methods for groups. The getObjects() method, which also works like fabric.Canvas#getObjects() , and returns an array of all the objects in the group. There is a size() method that shows the number of all objects in the group. There is also a contains() method that checks for the presence of a specific object in the group. The previously mentioned item() method, which allows you to take a specific object from a group. The forEachObject() method, which works just like fabric.Canvas#forEachObject , with groups only. And finally, the add() and remove() methods, which respectively add and remove objects from the group.

You can add / remove objects from a group in two ways. With the update position / size of the group and without.
Add a rectangle to the center of the group.

 group.add(new fabric.Rect({ ... originX: 'center', originY: 'center' })); 

Add a rectangle 100px from the center of the group:

 group.add(new fabric.Rect({ ... left: 100, top: 100, originX: 'center', originY: 'center' })); 

Add a rectangle to the center of the group and update the size of the group:

 group.addWithUpdate(new fabric.Rect({ ... left: group.getLeft(), top: group.getTop(), originX: 'center', originY: 'center' })); 

Add a rectangle 100px from the center of the group and update the size of the group:

 group.addWithUpdate(new fabric.Rect({ ... left: group.getLeft() + 100, top: group.getTop() + 100, originX: 'center', originY: 'center' })); 

To create a group of objects that already exist on canvas, you need to clone them and then group them:

 //     2-  . var group = new fabric.Group([ canvas.item(0).clone(), canvas.item(1).clone() ]); //     . canvas.clear().renderAll(); //    canvas. canvas.add(group); 

Serialization


When you start implementing the functionality in which, for example, you want to allow the user to save the contents of the canvas, or you want to translate it to another client, you will need to serialize the canvas . How can I send the contents of the canvas? You can of course export the entire canvas to an image, but uploading the image to the server is cumbersome and inconvenient. It is much easier to translate content into text than just Fabric can so please us.

Methods toObject, toJSON


The basis for serialization in Fabris is the fabric.Canvas#toObject() and fabric.Canvas#toJSON() .
Let's look at an example, serialization of empty canvas.

 var canvas = new fabric.Canvas('c'); JSON.stringify(canvas); // '{"objects":[],"background":"rgba(0, 0, 0, 0)"}' 

We use the ES5 JSON.stringify() method, which calls the toJSON method on an object if this method exists. The canvas object in Fabric has this method, it is equivalent to calling JSON.stringify(canvas.toJSON()) .

Consider a return string that represents an empty canvas. It is in JSON format, and consists of the '' objects '' and '' background '' properties. The '' objects '' property is currently empty, since there is nothing on the canvas, and the '' background '' has its initial transparent value (“rgba (0, 0, 0, 0)”).

Set the canvas to a different background, and see what changes.

 canvas.backgroundColor = 'red'; JSON.stringify(canvas); // '{"objects":[],"background":"red"}' 

As expected, the canvas view now contains a different background. Now let's try to add some objects.

 canvas.add(new fabric.Rect({ left: 50, top: 50, height: 20, width: 20, fill: 'green' })); console.log(JSON.stringify(canvas)); 

... will output to the console:
 '{"objects":[{"type":"rect","left":50,"top":50,"width":20,"height":20,"fill":"green","overlayFill":null,"stroke":null,"strokeWidth":1,"strokeDashArray":null,"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,"selectable":true,"hasControls":true,"hasBorders":true,"hasRotatingPoint":false,"transparentCorners":true,"perPixelTargetFind":false,"rx":0,"ry":0}],"background":"rgba(0, 0, 0, 0)"}' 

Wow! At first glance, a lot of things have changed, but after a closer look, we will see that this new object has become part of the “objects” array serialized in JSON. Please note that its description contains all visual components: coordinates, width, height, fill, etc. If we add another object, say a red circle, and place it behind the rectangle, the result will change accordingly.

 canvas.add(new fabric.Circle({ left: 100, top: 100, radius: 50, fill: 'red' })); console.log(JSON.stringify(canvas)); 

... will output to the console:
 '{"objects":[{() "type":"rect","left":50,"top":50,"width":20,"height":20,"fill":"green","overlayFill":null,"stroke":null,"strokeWidth":1,"strokeDashArray":null,"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,"selectable":true,"hasControls":true,"hasBorders":true,"hasRotatingPoint":false,"transparentCorners":true,"perPixelTargetFind":false,"rx":0,"ry":0},{() "type":"circle","left":100,"top":100,"width":100,"height":100,"fill":"red","overlayFill":null,"stroke":null,"strokeWidth":1,"strokeDashArray":null,"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,"selectable":true,"hasControls":true,"hasBorders":true,"hasRotatingPoint":false,"transparentCorners":true,"perPixelTargetFind":false,"radius":50}],"background":"rgba(0, 0, 0, 0)"}' 

I selected "type":"rect" and "type":"circle" to indicate where these objects begin. It may seem that the string is too long, but it's flowers, compared with the serialization of images. For comparison, let's look at the 1/10 (!) Line canvas.toDataURL('png') method:

 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAyAAAAK8CAYAAAAXo9vkAAAgAElEQVR4Xu3dP4xtBbnG4WPAQOQ2YBCLK1qpoQE1/m+NVlCDwUACicRCEuysrOwkwcJgAglEItRQaWz9HxEaolSKtxCJ0FwMRIj32zqFcjm8e868s2fNWo/Jygl+e397rWetk5xf5pyZd13wPwIECBAgQIAAAQIECBxI4F0H++Qb134R/U2fevC8q+5esGWESBAgAABAgQIEFiOwPL/MC5AlvO0OBMCBAgQIECAAAECJxQQICcE9HYCBAgQIECAAAECBPYXECD7W3klAQIECBAgQIAAAQInFBAgJwT0dgIECBAgQIAAAQIE9hcQIPtbeSUBAgQIECBAgAABAicUECAnBPR2AgQIECBAgAABAgT2FxAg+1t5JQECBAgQIECAAAECJxQQICcE9HYCBAgQIECAAAECBPYXECD7W3klAQIECBAgQIAAAQInFBAgJwTc9+3z49yvmNd+dI7PzPHJOW6Y4wNzXD3HlXNc9pZdb85/vzbHK3P8aY7n5vj1HL+Y43dz417f97O9jgABAgQIECBAgMBSBATIKd2JCY5dWNwyx5fn+PwcV5U/6tXZ99M5fjjHk3Mjd6HifwQIECBAgAABAgQWLSBAirdnouP6WXfvHHfOcU1x9T6rXp4XPTLHA3NTX9jnDV5DgAABAgQIECBA4NACAuSE4hMdl8+Kr83xzTmuO+G61ttfnEXfnuN7c4PfaC21hwABAgQIECBAgMBJBQTIJQpOeFw7b71/jtsvccWh3vbYfNB9c6NfOtQH+hwCBAgQIECAAAECFxMQIMd8No7C4+F5283HfOtZv/ypOYG7hMhZ3wafT4AAAQIECBDYtoAA2fP+H/1Vqwd3f4jf8y1Lfdkunu7xV7OWenucFwECBAgQIEBg3QICZI/7O/Fxx7xs9wf3t36r3D3evciX7L7F7+6rIY8u8uycFAECBAgQIE' 

And still ~ 17000 characters .

At first glance, it is not clear why another fabric.Canvas#toObject method was fabric.Canvas#toObject . It's simple: toObject returns the same view as toJSON , only as an object. For example, take a canvas with content in the form of a green rectangle. canvas.toObject() will output to the console:

 { "background" : "rgba(0, 0, 0, 0)", "objects" : [ { "angle" : 0, "fill" : "green", "flipX" : false, "flipY" : false, "hasBorders" : true, "hasControls" : true, "hasRotatingPoint" : false, "height" : 20, "left" : 50, "opacity" : 1, "overlayFill" : null, "perPixelTargetFind" : false, "scaleX" : 1, "scaleY" : 1, "selectable" : true, "stroke" : null, "strokeDashArray" : null, "strokeWidth" : 1, "top" : 50, "transparentCorners" : true, "type" : "rect", "width" : 20 } ] } 

As you can see, toJSON output is nothing more than a translated toObject line. The toObject method toObject interesting and useful in that it is “smart” and “lazy”. What you see in the array is the result of iterating over all the canvas objects, and delegating the toObject method to toObject . The “class” fabric.Path has its own toObject , which returns an array of '' points ''. And fabric.Image also has this method; it returns the '' src '' property on images. Following the OOP pattern, each object knows how to serialize itself.

This means that when you create your own "class", or you want to change the serialized representation of an object, then you need a toObject method that you can rewrite or extend its functionality.
Let's look at an example:

 var rect = new fabric.Rect(); rect.toObject = function() { return { name: 'trololo' }; }; canvas.add(rect); console.log(JSON.stringify(canvas)); 

... will output to the console:

 '{"objects":[{"name":"trololo"}],"background":"rgba(0, 0, 0, 0)"}' 

As you can see, the array “objects” now has a modified representation of our rectangle. But overwriting this way is often not very useful, in contrast to the expansion of the toObject functional toObject additional properties.

 var rect = new fabric.Rect(); rect.toObject = (function(toObject) { return function() { return fabric.util.object.extend(toObject.call(this), { name: this.name }); }; })(rect.toObject); canvas.add(rect); rect.name = 'trololo'; console.log(JSON.stringify(canvas)); 

... will output to the console:

 '{"objects":[{"type":"rect","left":0,"top":0,"width":0,"height":0,"fill":"rgb(0,0,0)","overlayFill":null,"stroke":null,"strokeWidth":1,"strokeDashArray":null,"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,"selectable":true,"hasControls":true,"hasBorders":true,"hasRotatingPoint":false,"transparentCorners":true,"perPixelTargetFind":false,"rx":0,"ry":0,"name":"trololo"}],"background":"rgba(0, 0, 0, 0)"}' 

We extended the existing toObject method on the object with the additional property '' name ''. It is now present in the result of the method call. It must be remembered that when extending the functionality in this way, the “class” of the object (in this case, fabric.Rect ) must contain the newly added property in the '' stateProperties '' array. Only in this case everything will work correctly.

ToSVG ​​method


Another canvas text view is SVG format. Fabric specializes in SVG parsing and rendering to canvas. This makes it possible to convert from canvas to SVG and vice versa. Add the same rectangle to the canvas and see the toSVG method in action:

 canvas.add(new fabric.Rect({ left: 50, top: 50, height: 20, width: 20, fill: 'green' })); console.log(canvas.toSVG()); 

... will output to the console:

 '<?xml version="1.0" standalone="no" ?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="800" height="700" xml:space="preserve"><desc>Created with Fabric.js 0.9.21</desc><rect x="-10" y="-10" rx="0" ry="0" width="20" height="20" style="stroke: none; stroke-width: 1; stroke-dasharray: ; fill: green; opacity: 1;" transform="translate(50 50)" /></svg>' 

Like toJSON or toObject , toSVG , when called on canvas, delegates its logic to each object, and each of them has its own toSVG method, which is specific to each type of object. If you need to change or extend the toSVG method, you can do it as well as with the toObject method.

The advantage of SVG views in comparison with toObject/toJSON is that you can coordinate with any device capable of SVG rendering (browser, application, printer, camera, etc.). With the toObject / toJSON methods, you must first load the view onto canvas. By the way, about loading on canvas. We can serialize the contents of the canvas into text, and how can we load it back?

Deserialization, SVG parser


As in serialization, there are 2 ways to load a canvas from a string: from a JSON and SVG view. For JSON, there are methods fabric.Canvas#loadFromJSON and fabric.Canvas#loadFromDatalessJSON . For SVG, the fabric.loadSVGFromURL and fabric.loadSVGFromString .
Notice that the first two methods are invoked on canvas, and the other two directly on the fabric.

There is nothing special to say about these methods. They work exactly as expected. Take, for example, the previous JSON output and put it on an empty canvas:

 var canvas = new fabric.Canvas(); canvas.loadFromJSON('{"objects":[{"type":"rect","left":50,"top":50,"width":20,"height":20,"fill":"green","overlayFill":null,"stroke":null,"strokeWidth":1,"strokeDashArray":null,"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,"selectable":true,"hasControls":true,"hasBorders":true,"hasRotatingPoint":false,"transparentCorners":true,"perPixelTargetFind":false,"rx":0,"ry":0},{"type":"circle","left":100,"top":100,"width":100,"height":100,"fill":"red","overlayFill":null,"stroke":null,"strokeWidth":1,"strokeDashArray":null,"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,"selectable":true,"hasControls":true,"hasBorders":true,"hasRotatingPoint":false,"transparentCorners":true,"perPixelTargetFind":false,"radius":50}],"background":"rgba(0, 0, 0, 0)"}'); 

... Both objects appear on the canvas:

image

Well, loading the canvas from a string is pretty simple, but what about the seemingly incomprehensible loadFromDatalessJSON method? What is its fundamental difference from the loadFromJSON that we just used? To understand why we need this method, we need to look at a more or less complex path object, for example, this one:

image

... and JSON.stringify(canvas) will output the following:

 '{"objects":[{"type":"path","left":184,"top":177,"width":175,"height":151,"fill":"#231F20","overlayFill":null,"stroke":null,"strokeWidth":1,"strokeDashArray":null,"scaleX":1,"scaleY":1,"angle":-19,"flipX":false,"flipY":false,"opacity":1,"selectable":true,"hasControls":true,"hasBorders":true,"hasRotatingPoint":false,"transparentCorners":true,"perPixelTargetFind":false,"path":[["M",39.502,61.823],["c",-1.235,-0.902,-3.038,-3.605,-3.038,-3.605],["s",0.702,0.4,3.907,1.203],["c",3.205,0.8,7.444,-0.668,10.114,-1.97],["c",2.671,-1.302,7.11,-1.436,9.448,-1.336],["c",2.336,0.101,4.707,0.602,4.373,2.036],["c",-0.334,1.437,-5.742,3.94,-5.742,3.94],["s",0.4,0.334,1.236,0.334],["c",0.833,0,6.075,-1.403,6.542,-4.173],["s",-1.802,-8.377,-3.272,-9.013],["c",-1.468,-0.633,-4.172,0,-4.172,0],["c",4.039,1.438,4.941,6.176,4.941,6.176],["c",-2.604,-1.504,-9.279,-1.234,-12.619,0.501],["c",-3.337,1.736,-8.379,2.67,-10.083,2.503],["c",-1.701,-0.167,-3.571,-1.036,-3.571,-1.036],["c",1.837,0.034,3.239,-2.669,3.239,-2.669],["s",-2.068,2.269,-5.542,0.434],["c",-3.47,-1.837,-1.704,-8.18,-1.704,-8.18],["s",-2.937,5.909,-1,9.816],["C",34.496,60.688,39.502,61.823,39.502,61.823],["z"],["M",77.002,40.772],["c",0,0,-1.78,-5.03,-2.804,-8.546],["l",-1.557,8.411],["l",1.646,1.602],["c",0,0,0,-0.622,-0.668,-1.691],["C",72.952,39.48,76.513,40.371,77.002,40.772],["z"],["M",102.989,86.943],["M",102.396,86.424],["c",0.25,0.22,0.447,0.391,0.594,0.519],["C",102.796,86.774,102.571,86.578,102.396,86.424],["z"],["M",169.407,119.374],["c",-0.09,-5.429,-3.917,-3.914,-3.917,-2.402],["c",0,0,-11.396,1.603,-13.086,-6.677],["c",0,0,3.56,-5.43,1.69,-12.461],["c",-0.575,-2.163,-1.691,-5.337,-3.637,-8.605],["c",11.104,2.121,21.701,-5.08,19.038,-15.519],["c",-3.34,-13.087,-19.63,-9.481,-24.437,-9.349],["c",-4.809,0.135,-13.486,-2.002,-8.011,-11.618],["c",5.473,-9.613,18.024,-5.874,18.024,-5.874],["c",-2.136,0.668,-4.674,4.807,-4.674,4.807],["c",9.748,-6.811,22.301,4.541,22.301,4.541],["c",-3.097,-13.678,-23.153,-14.636,-30.041,-12.635],["c",-4.286,-0.377,-5.241,-3.391,-3.073,-6.637],["c",2.314,-3.473,10.503,-13.976,10.503,-13.976],["s",-2.048,2.046,-6.231,4.005],["c",-4.184,1.96,-6.321,-2.227,-4.362,-6.854],["c",1.96,-4.627,8.191,-16.559,8.191,-16.559],["c",-1.96,3.207,-24.571,31.247,-21.723,26.707],["c",2.85,-4.541,5.253,-11.93,5.253,-11.93],["c",-2.849,6.943,-22.434,25.283,-30.713,34.274],["s",-5.786,19.583,-4.005,21.987],["c",0.43,0.58,0.601,0.972,0.62,1.232],["c",-4.868,-3.052,-3.884,-13.936,-0.264,-19.66],["c",3.829,-6.053,18.427,-20.207,18.427,-20.207],["v",-1.336],["c",0,0,0.444,-1.513,-0.089,-0.444],["c",-0.535,1.068,-3.65,1.245,-3.384,-0.889],["c",0.268,-2.137,-0.356,-8.549,-0.356,-8.549],["s",-1.157,5.789,-2.758,5.61],["c",-1.603,-0.179,-2.493,-2.672,-2.405,-5.432],["c",0.089,-2.758,-1.157,-9.702,-1.157,-9.702],["c",-0.8,11.75,-8.277,8.011,-8.277,3.74],["c",0,-4.274,-4.541,-12.82,-4.541,-12.82],["s",2.403,14.421,-1.336,14.421],["c",-3.737,0,-6.944,-5.074,-9.879,-9.882],["C",78.161,5.874,68.279,0,68.279,0],["c",13.428,16.088,17.656,32.111,18.397,44.512],["c",-1.793,0.422,-2.908,2.224,-2.908,2.224],["c",0.356,-2.847,-0.624,-7.745,-1.245,-9.882],["c",-0.624,-2.137,-1.159,-9.168,-1.159,-9.168],["c",0,2.67,-0.979,5.253,-2.048,9.079],["c",-1.068,3.828,-0.801,6.054,-0.801,6.054],["c",-1.068,-2.227,-4.271,-2.137,-4.271,-2.137],["c",1.336,1.783,0.177,2.493,0.177,2.493],["s",0,0,-1.424,-1.601],["c",-1.424,-1.603,-3.473,-0.981,-3.384,0.265],["c",0.089,1.247,0,1.959,-2.849,1.959],["c",-2.846,0,-5.874,-3.47,-9.078,-3.116],["c",-3.206,0.356,-5.521,2.137,-5.698,6.678],["c",-0.179,4.541,1.869,5.251,1.869,5.251],["c",-0.801,-0.443,-0.891,-1.067,-0.891,-3.473]'... 

... and this is only the 5th (!) Part of the entire text.

What's going on here? The Path object consists of hundreds of symbols of Bezier curves that show how this object should be displayed. All these parts ["c",0,2.67,-0.979,5.253,-2.048,9.079] in JSON format are the coordinates of one of the curves. And when there are hundreds or thousands of these curves, it is not difficult to guess how huge the string will be.

What to do?

This is where the fabric.Canvas#toDatalessJSON method comes to the rescue. Let's try:

 canvas.item(0).sourcePath = '/assets/dragon.svg'; console.log(JSON.stringify(canvas.toDatalessJSON())); 

... will output to the console:

 {"objects":[{"type":"path","left":143,"top":143,"width":175,"height":151,"fill":"#231F20","overlayFill":null,"stroke":null,"strokeWidth":1,"strokeDashArray":null,"scaleX":1,"scaleY":1,"angle":-19,"flipX":false,"flipY":false,"opacity":1,"selectable":true,"hasControls":true,"hasBorders":true,"hasRotatingPoint":false,"transparentCorners":true,"perPixelTargetFind":false,"path":"/assets/dragon.svg"}],"background":"rgba(0, 0, 0, 0)"} 

Well, much less! What have we done? Note that before calling the toDatalessJSON method toDatalessJSON we set the value of "/assets/dragon.svg" in the "sourcePath" property of the path object (dragon shape). Then we called the toDatalessJSON method, and the whole huge path string was turned into the simple string "/assets/dragon.svg".

Working with a large number of complex forms, the toDatalessJSON method allows us to significantly reduce the text representation of the canvas, and replace the large path data with a simple reference to SVG.

Returning to the loadFromDatalessJSON method, you probably guessed that it allows you to load a canvas from a dataless view (dataless). loadFromDatalessJSON can load "path" strings (such as "/assets/dragon.svg") and use them as data for path objects.

Look at the methods for loading SVG. We can use a string or URL:

 fabric.loadSVGFromString('...', function(objects, options) { var obj = fabric.util.groupSVGElements(objects, options); canvas.add(obj).renderAll(); }); 

The first argument is the SVG string, the second is the callback function. The function is called when the SVG has loaded. It takes two arguments: objects and options . objects - an array of objects obtained from SVG - paths, a group of path-objects (for complex objects), images, text, etc. To group all of these objects into one collection and make them look like they were in an SVG document, we use fabric.util.groupSVGElements for both objects and options . In the end, we get a fabric.Path or fabric.PathGroup object that we can add to the canvas.

fabric.loadSVGFromURL works in the same way, except that you use the string that contains the URL, not the SVG content. Please note that Fabric will try to get this URL via XMLHttpRequest, so the SVG link must comply with the SOP rules.

Subclassing


Since Fabric is built on the principles of OOP, then you can create “subclasses” and extend the functionality of objects easily and naturally. As you know from the first part of the series, in Fabric there is a strict hierarchy of objects. All 2D objects (path, images, text, etc.) inherit from fabric.Object , and some “classes” - like fabric.PathGroup - even have 3-level inheritance.

How about doing a “subclass” on an existing “class” in Fabric? Or maybe create a new "class"?

In order to do this, we need the method fabric.util.createClass , which is a simple abstraction of ordinary prototype inheritance in javascript. First, create a simple "class" Point:

 var Point = fabric.util.createClass({ initialize: function(x, y) { this.x = x || 0; this.y = y || 0; }, toString: function() { return this.x + '/' + this.y; } }); 

createClass takes an object and uses its properties as properties of a new “class” object. '' initialize '' is used as a constructor. Therefore, when we initialize Point, we create a new object with the properties '' x '', '' y '' and the method '' toString '':

 var point = new Point(10, 20); point.x; // 10 point.y; // 20 point.toString(); // "10/20" 

If we want to create a descendant of a “class” Point, say, a colored point, we use createClass :

 var ColoredPoint = fabric.util.createClass(Point, { initialize: function(x, y, color) { this.callSuper('initialize', x, y); this.color = color || '#000'; }, toString: function() { return this.callSuper('toString') + ' (color: ' + this.color + ')'; } }); 

Note that the object to inherit is now used as the second argument, and the first is the "class" Point, which becomes the parent for the object. To avoid duplication, we use the callSuper method, which calls the method on the parent "class". This means that if we change the Point, the changes will also affect ColoredPoint . Let's look at an example:

 var redPoint = new ColoredPoint(15, 33, '#f55'); redPoint.x; // 15 redPoint.y; // 33 redPoint.color; // "#f55" redPoint.toString(); "15/35 (color: #f55)" 

Now we know how to create our own “classes” and “subclasses”, but we can also use existing ones in Fabric. For example, create a "class" LabeledRect , which will be just a rectangle labeled. When an instance of the “class” is displayed on canvas, the caption will be displayed inside the rectangle. Something similar (circle and text) we have already considered in the chapter 'Groups'. By the way, while working with Fabric you can see that you can create abstractions here using groups as well as “classes”.

 var LabeledRect = fabric.util.createClass(fabric.Rect, { type: 'labeledRect', initialize: function(options) { options || (options = { }); this.callSuper('initialize', options); this.set('label', options.label || ''); }, toObject: function() { return fabric.util.object.extend(this.callSuper('toObject'), { label: this.get('label') }); }, _render: function(ctx) { this.callSuper('_render', ctx); ctx.font = '20px Helvetica'; ctx.fillStyle = '#333'; ctx.fillText(this.label, -this.width/2, -this.height/2 + 20); } }); 

The code may seem complicated, but everything is pretty simple.

First, we define the parent "class" as fabric.Rect to add its display capabilities. Then we define the '' type '' property and give it the value of the 'labeledRect' '. This is done to be consistent with the Fabric architecture, since all the objects there have a '' type '' property (rectangle, circle, path, text, etc.).

The constructor ( initialize ) is already familiar to us, in it we call callSuper , which calls initialize on the fabric.Rect . In addition, we give the object an inscription (label) by taking the value from options .

As a result, we have 2 methods left - toObject and _render . toObject , as we remember from the serialization chapter, is responsible for representing the object.

Since LabeledRect has the same properties as a regular rectangle (rect), we expanded the parent method toObject by simply adding a label to it.

As for the _render method, it is responsible for the immediate drawing of the object. It consists of a rectangle display ( callSuper ) and additional text display logic.

Now, if we want to show such an object:

 var labeledRect = new LabeledRect({ width: 100, height: 50, left: 100, top: 100, label: 'test', fill: '#faa' }); canvas.add(labeledRect); 

... we get the following:

image

Changing the property (label or any other) will lead to the expected result:

 labeledRect.set({ label: 'trololo', fill: '#aaf', rx: 10, ry: 10 }); 


image

You can modify the behavior of the "class" as you like. For example, add default values ​​so as not to specify them once in the constructor. Or make custom properties available on the object. If you make the additional properties customizable, you can put them in toObject , and initialize :

 ... initialize: function(options) { options || (options = { }); this.callSuper('initialize', options); //       /   100/50 this.set({ width: 100, height: 50 }); this.set('label', options.label || ''); } ... _render: function(ctx) { //        ctx.font = this.labelFont; ctx.fillStyle = this.labelFill; ctx.fillText(this.label, -this.width/2, -this.height/2 + 20); } ... 

This ends the third part of our series. Now you have an idea of ​​groups, “classes”, “subclasses” and (de) serialization. I hope that the material presented in the article will help you to solve more complex problems with Fabric. More information will be presented in the fourth part of the series .

Successes and successful development!

The translation was carried out with the consent and direct participation of the authors. Link to the source .

Source: https://habr.com/ru/post/254763/


All Articles