JavaScript 2D Graphics

SVG Libraries

Two-dimensional graphics on the Web is traditionally represented either as a set of interfaces intended for the <canvas> element or as a group of APIs for parsing SVG markup. The Scalable Vector Graphics format combines the strictness of XML with flexibility of scripting techniques, so it is no wonder that the SVG DOM has formed the core of the most efficient 2D frameworks implemented in JavaScript.

An SVG library falls under one of the following categories. The first category embraces versatile libraries exposing graphics API as part of a wider range of interfaces. A prominent example of the multipurpose library is Google Closure. Closure deals with practically any aspect of the modern application. UI widgets, cryptography, browser-controlled data storage, interfaces for the network communication - all these features make the Closure library a universal tool for creating elaborate Web sites. Graphics-related functionality is implemented in classes from the goog.graphics namespace. Both SVG-based and canvas-oriented APIs are supported. Closure also contains "fallback" VML interfaces for old versions of Internet Explorer.

The second category of SVG libraries is JavaScript software providing SVG-only functionality. One of the most productive libraries belonging to this category is the popular Raphaël.js. In the IE environment lacking SVG implementation Raphaël falls down to VML. In October 2013 Adobe Web Platform Team announced the release of Snap.svg. Having dubbed Snap "jQuery or Zepto for SVG", Adobe blog clarifies the reasons for creating the library. One of the primary factors having established Raphaël as de facto standard is its efficiency in browsers all the way back to IE 6. However, supporting so many browsers means only being able to implement a common subset of SVG features. Snap was rewritten entirely from scratch by the author of Raphaël and has been designed specifically for modern browsers.

Another example of "pure SVG" libraries is svg.js - a fast and elegant solution easily extendable by plugins. The default build of svg.js supports main SVG traits - paths, basic shapes, text rendering, imaging, clipping and masking, animations and interactivity. As the library has a modular structure, enhanced SVG functionality is provided by various extensions: there are available plugins for drag-and-drop operations, filtering, complex polygon-based shapes, etc. Developers can create custom builds of svg.js containing as many modules as will prove necessary for their Web applications.

The last group of JavaScript libraries for SVG combines SVG with the <canvas> graphics. SVG is generally used as a supplementary instrument enriching the canvas graphical context: e. g. Fabric.js employs SVG as one of the serialization formats for canvas objects.

Let's analyze main features of the aforementioned libraries in working examples: such illustrative approach will help us to single out the most important interfaces for handling SVG.

Google Closure Graphics

The "entry point" of SVG graphics in the Closure library is the goog.graphics.SvgGraphics class:

var svg=new goog.graphics.SvgGraphics(100, 100);

The class has numerous methods both for drawing graphical objects and for manipulating the DOM tree of an SVG document. In browsers lacking SVG support the Closure library turns to the canvas or VML:

CanvasGraphics class
var canvas=new goog.graphics.CanvasGraphics(100, 100);

VmlGraphics class
var vml=new goog.graphics.VmlGraphics(100, 100);

The code below creates an SVG logo of Google Web Toolkit by loading two external image resources. The generated SVG contents are embedded within the parent <div> element:

SVG container
<div id="drawing-surface"></div>

creating inline SVG
goog.require('goog.graphics');

var surface=document.getElementById("drawing-surface");

var svg=new goog.graphics.SvgGraphics(100, 100); // width and height in pixels
svg.createDom(); // creates the DOM representation of the graphics area
svg.drawImage(1, 3, 58, 21, "logo.png");
svg.drawImage(24, 40, 61, 50, "google-web-toolkit.png");
svg.render(surface);

Snap.svg

The Snap.svg API is based on the following components:

  • Snap - the pivotal point of the library; this global object enables fragment selection and works with path geometry; in addition, Snap exposes utility methods for angles computation, color conversion, filtering and matrix creation; the loading and parsing of SVG files is also the responsibility of the Snap object;
  • Fragment - a result of parsing an SVG string or loading an external SVG file;
  • Element - the main "building block" of the library; Element deals with SVG structure navigation and events handling;
  • Paper - the "drawing surface" of the library; the Paper object provides methods for creating basic geometric shapes, rendering text snippets and applying graphical filters to SVG elements;
  • Matrix - an auxiliary object for working with transformation matrices;
  • Set - an augmented JavaScript collection;
  • mina - an auxiliary object exposing static methods for animation timing functions.

This is an example of fetching an external SVG file from the Web and rendering it within the parent <div> element:

SVG container
<div id="drawing-surface"></div>

loading SVG
Snap.load("svg-file.svg", function(fragment) {
 var svg=fragment.select("svg");
 new Snap("#drawing-surface").append(svg);
});

The use of Snap.svg objects does not exclude traditional DOM routines. The code below employs both XML DOM and Snap interfaces. Calling the parse() method of the global Snap object returns an instance of Fragment. The Fragment can be used as a starting point for traversing the DOM tree: the object's node property exposes the parsed SVG as a DOM DocumentFragment with all its conventional API members. The root <svg> element is first represented as a DOM Element, its attributes are specified by calling the setAttribute() method. Then the same root is used as a base for drawing operations: the select() method invoked on the Snap.svg Fragment returns an Element instance. The Paper object derived from the paper property of the Element draws two images (the image() method with source, x, y, width and height parameters) and groups them (the g() method). The last step applies the Snap.svg shadow filter to the group: the filter parameters include horizontal and vertical offsets of the shadow, the blur radius, the silver color and opacity of the shadow.

SVG container
<div id="drawing-surface"></div>

using XML DOM and Snap.svg APIs
var surface=document.getElementById("drawing-surface");

var docType="<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>";
var emptyRoot="<svg></svg>";
var fragment=Snap.parse(docType+emptyRoot); // parses SVG string and converts it to Snap.svg Fragment object

var docFragment=fragment.node; // DOM DocumentFragment
var root=docFragment.childNodes.item(0); // DOM Element
root.setAttribute("xmlns", "http://www.w3.org/2000/svg");
root.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
root.setAttribute("version", "1.1");
root.setAttribute("width", "100");
root.setAttribute("height", "100");

var svg=fragment.select("svg"); // Snap.svg Element
var logo=svg.paper.image("logo.png", 20, 20, 50, 50); // Snap.svg Paper method
var palette=svg.paper.image("palette.png", 50, 50, 36, 36);
var group=svg.paper.g(logo, palette);
group.attr({"filter": svg.paper.filter(Snap.filter.shadow(2, 2, 2, "silver", 0.4))});

console.log(svg.toString()); // SVG snippet as string
svg.appendTo(surface); // convenience method for appending the current Element to the specified parent

Svg.js

Bringing the svg.js API into action starts with calling the SVG() method:

SVG container
<div id="drawing-surface"></div>

creating inline SVG
var surface = SVG("drawing-surface");
surface.size(100, 100);

Invoking the SVG() method has built an inline SVG document as a child node of the <div> element. The next steps will include the creation of a radial gradient and a rectangle filled with the gradient:

var gradient = surface.gradient("radial", function(stop) {
 stop.at({offset: 0, color: "white", opacity: 1}),
 stop.at({offset: 1, color: "azure", opacity: 0.9})
});

var rect = surface.rect(100, 100);
rect.fill(gradient);

The last step is the load of an external image:

var img=surface.image("toolkit.png");
img.attr({"x": 8, "y": 13});

Canvas and SVG

As a rule, canvas-oriented libraries bring SVG into play as an ancillary format: for example, Fabric.js supports canvas serialization to SVG:

var canvas=document.querySelector("#canvas-surface"); // reference canvas element
var fCanvas=new fabric.StaticCanvas(canvas); // Fabric.js Canvas without interactivity

. . . calling Fabric.js Canvas methods to create graphical objects . . .

console.log(fCanvas.toSVG()); // SVG as string

The resultant markup can be used to build an inline SVG document or save it as a file:

var surface=document.getElementById("drawing-surface");

creating inline SVG document
var parser=new DOMParser();
var svg=parser.parseFromString(fCanvas.toSVG(), "image/svg+xml");
var surface=document.getElementById("drawing-surface");
surface.appendChild(svg.rootElement); // creating inline SVG within an HTML element

creating a link to save SVG locally
var blob=new Blob([fCanvas.toSVG()], {type: "image/svg+xml"});
var link=document.createElement("a");
link.href=URL.createObjectURL(blob);
link.download="canvas.svg";
link.textContent="save SVG file";
document.body.appendChild(link);

Fabric.js supports SVG deserialization, too: the library's SVG parser loads SVG from a string or a file and translates the parsed markup into a series of canvas graphical objects. Similar functionality is provided by such libraries as cakejs, canvg or jscapturecanvas. Canvg is extremely useful in environments lacking SVG implementation but supporting the <canvas> 2D context. For example, to display an SVG file in the native browser on the Android 2.2 platform, the following code can be employed:

<canvas id="canvas-surface" width="127" height="100"></canvas>

SVG parsing and rendering
canvg("canvas-surface", "Duke.svg"); // canvas id attribute and SVG URL

SVG deserialization is not a trivial task, so complex SVG files parsed by third-party libraries are sometimes rendered incorrectly. If the browser supports both canvas and SVG natively, the preferred way to display SVG graphics on the canvas plane is to call the drawImage() method of the canvas 2D context:

input element for selecting an SVG file
<input type="file" name="svg-file-picker" accept="image/svg+xml" onchange="displaySVG(this.files[0])">

element to preview the selected file
<div id="image-viewer"></div>
parent element for dynamically created canvas
<div id="canvas-holder"></div>

rendering SVG on the canvas
var viewer=document.querySelector("#image-viewer");
var surface=document.querySelector("#canvas-holder");

function displaySVG(file) {
 var fileReader=new FileReader();
 fileReader.onload=function(event) {
  var url=event.target.result;
  var img=new Image();
  img.src=url;
  img.onload=function() {
   viewer.appendChild(img);
   var canvas=document.createElement("canvas");
   var context=canvas.getContext("2d");
   canvas.width=img.width;
   canvas.height=img.height;
   context.drawImage(img, 0, 0);
   surface.appendChild(canvas);
  };
 };
 fileReader.readAsDataURL(file);
}

Using the canvas 2D context as an intermediary between SVG and other image formats enables the client-side "rasterization" of SVG files: they can be converted to PNG, JPEG, or WebP images without turning to a server code. Continuing the example above, this code snippet saves the SVG-based canvas data as PNG:

var svg2png=document.querySelector("button#svg-to-png-converter");
svg2png.addEventListener(
 "click",
 function() {
  var fileControl=document.querySelector("input");
  var canvas=document.querySelector("canvas");
  var url=canvas.toDataURL(); // default MIME type is image/png
  var link=document.createElement("a");
  link.href=url;
  link.download=fileControl.files[0].name.replace(".svg", ".png");
  link.textContent="Save As PNG";
  document.body.appendChild(link);
 },
 false
);

To save the same data as a JPEG or WebP image, the respective MIME type should be passed to the toDataURL() method:

var url=canvas.toDataURL("image/jpeg");

SVG standards are gradually evolving. SVG 2 has modified a set of substantial interfaces in the SVG DOM. The latest trends in the world of vector graphics will be eventually implemented in browsers. Scripting libraries will undergo changes, too, but their main destination will remain the same: to level the browser-related discrepancies and to present the developer with an abstraction layer hiding the low-level features of SVG. After all, innovative JavaScript libraries not only improve the way the Web is functioning: they make the Web brighter!