Graphics in SVG

Notes

Graphics in Javascript

Javascript offers three main systems for doing more elaborate graphics.

We will focus on SVG for now, which is based on the SVG XML specification.

SVG Graphics

Here is an example of how an SVG graphic might look in the code:

<svg xmlns="http://www.w3.org/2000/svg" version="1.1"
  viewBox="0 0 100 100" preserveAspectRatio="xMidYMid slice"
  style="width:100%; height:100%; position:absolute; top:0; left:0; z-index:-1;">
  <linearGradient id="gradient">
    <stop class="begin" offset="0%"/>
    <stop class="end" offset="100%"/>
  </linearGradient>
  <rect x="0" y="0" width="100" height="100" style="fill:url(#gradient)" />
  <circle cx="50" cy="50" r="30" style="fill:url(#gradient)" />
</svg>

So in many ways it is just HTML elements, except that they are a separate set of such elements than the normal HTML elements. But the syntax is very similar, and they can be targetted and manipulated like HTML elements.

Here are the key concepts to be familiar with:

Elements

Here are the main SVG elements we can create:

svg
This is the container element. Physical dimensions are set here.
circle
For drawing a circle with a given center and radius.
ellipse
For drawing ellipses.
g
Used for grouping elements together (and setting custom transforms to them, for example).
line
For drawing a line connecting two points.
path
Used for arbitrary curves.
polyline
For a series of connected lines. There is also a polygon element that closes the endpoints.
rect
For drawing rectangles.
text
For adding text to graphs.

Of course creating this code on your own can be fairly painful. There are libraries to help you along, and SVG.js is the one we will use. Setting it up could be as simple as including the following script on your page:

<script src="https://cdnjs.cloudflare.com/ajax/libs/svg.js/2.7.1/svg.min.js"></script>

or dynamically on a page using the following:

var s = document.createElement('script');
s.setAttribute('src', 'https://cdnjs.cloudflare.com/ajax/libs/svg.js/2.7.1/svg.min.js');
document.body.appendChild(s);

This gives us an SVG global object to use. We can now use that to create elements. Here’s a typical interaction:

var aDiv = document.createElement('div');
aDiv.setAttribute('id', 'myDrawing');
document.body.prepend(aDiv);
var draw = SVG('myDrawing').size(100, 300);

This creates a new div element, and creates a new SVG empty element within it. Let’s add a rectangle in it:

var rect = draw.rect(60, 100).attr({ fill: '#f06' });

Let’s move it, then change its color:

rect.animate().move(0, 50);
rect.animate().fill('#f55');

And let’s read its coordinate values. And then change the x:

rect.x(); rect.y();
rect.x(50);

We can also round its corners:

rect.radius(10);
rect.animate()
  .radius(40);

You can find more things you can do with each element here.

Let’s work with a polyline, and then do some animation on it:

var polyline = draw.polyline('0,0 100,50 50,100')
  .fill('none').
  stroke({ width: 1 });
polyline.animate(2000)
  .plot([[0,0], [100,50], [50,100], [150,50], [200,50], [250,100], [300,50], [350,50]]);

SVG paths can do a lot of cool stuff, though they need some more work.

Paths

Paths are a challenging topic, so they deserve some more discussion. A path element contains a d attribute, which is a series of instructions to an imaginary “marker”.

Let us look at some examples:

M 15, 20      <--------   Move to the coordinates (15, 20)
m 20, 30      <--------   Move 20 pixels to the right and 30 pixels down

L 30, 40      <--------   Draw a straight line to location (30, 40)
l -30, 40     <--------   Draw a straight line going 30 pixels to the left and 40 pixels down
H 30          <--------   Draw a straight horizontal line to the point with x coordinate 30
v 10          <--------   Draw a straight vertical line going 10 pixels down
Z             <--------   Close the path, joining the current point to the start

C 10,20 20,20 30,10  <------ Draw a "cubic bezier curve" to the point (30, 10) using the two control points (10, 20) and (20, 20)
S 20,20 10,10   <------ Continue the previous cubic bezier step, and use as a first control point the reflection of the previous control point, and as a second control point the point (20, 20), and end at the point (10, 10).
c, s    <---- all coordinates are relative
Q 20,20 30,30   <------- A quadratic curve from the current point to the point (30, 30), using the control point (10, 10)
T, t     <----- Continue a quadratic curve with the reflected control point.

A        <----- Used for arcs. We will not discuss these.

All this is pretty complicated in the abstract, perhaps some examples would demonstrate. The following attempts to draw the letter G:

var letterG = draw.path('M 50,50 v -2 h 10 v 2 c -5,0 -5,5 -5,30 c -40,15 -40,0 -40,-32 s 0,-32 40,-30 q 2,0 2,-2 h 2 v 25 h -2 c -30,-20 -35,0 -35,10 c 0,30 25,30 30,20 c 0,-20 2,-20 -5,-20 z').fill('white').stroke('black');

Practice: Try to create some other letters.

Gradients and Patterns

You can create interesting fills with gradients and patterns. Here’s an example of a gradient from the SVG.js documentation:

var gradient = draw.gradient('linear', function(stop) {
  stop.at(0, '#333');
  stop.at(0.5, '#A44');
  stop.at(1, '#fff');
}).from(0, 0).to(1, 1);

var c = draw.circle().x(50).y(50).radius(30).fill(gradient);

We can also create patterns from any existing elements, then use them to fill other elements. For example here’s a checkered pattern:

var pattern = draw.pattern(20, 20, function(add) {
  add.rect(20,20).fill('#f06');
  add.rect(10,10);
  add.rect(10,10).move(10,10);
});

var c = draw.circle().x(50).y(50).radius(30).fill(pattern);

Transforms

There are a number of transformations. We’ll make a simple clock using them to rotate the indices.

var circle = draw.circle(80).cx(50).cy(50).fill('white').stroke('black');
var secIndex = draw.path('M 50,50 v -38').stroke('black').fill('white');
var minIndex = draw.group();
minIndex.path('M 50,50 m 0,-35 m -5,5 l 5,-5 l 5,5').stroke('black').fill('white').attr('stroke-width', 2).attr('stroke-linecap', 'round');
minIndex.path('M 50,50 v -35').stroke('black').fill('white').attr('stroke-width', 2);
var hourIndex = draw.group();
hourIndex.path('M 50,50 m 0,-15 m -5,5 l 5,-5 l 5,5 m -5,-5').stroke('black').fill('white').attr('stroke-width', 3).attr('stroke-linecap', 'round');
hourIndex.path('M 50,50 v -15').stroke('black').fill('white').attr('stroke-width', 3);

function showTime() {
  let now = new Date();
  secIndex.transform({ rotation: (6 * now.getSeconds()), cx: 50, cy: 50 });
  minIndex.transform({ rotation: (6 * now.getMinutes()), cx: 50, cy: 50 });
  hourIndex.transform({ rotation: (6 * now.getHours()), cx: 50, cy: 50 });
}
showTime();
var t = setInterval(showTime, 1000);

Practice 1: Add ticks at every hour. Start by putting one at 12 o’clock, then using use and rotation for the other 11.

Practice 2: Add the hours numbers.

Events on SVG

SVG elements are normal DOM elements, and we can therefore place handlers on them to react when they are clicked etc. In this section we will decide a “door” which when clicked will open.

Let’s start with the door basics. We want a rectangular frame containing another rectangle.

var aDiv = document.createElement('div');
aDiv.setAttribute('id', 'door');
aDiv.setAttribute('style', 'height: 200px;width:80px;');
document.body.prepend(aDiv);
var draw = SVG('door').size(100, 200);

var frame = draw.rect(80, 120).x(10).y(10).fill('white').stroke('purple').attr('stroke-width', 3);
var door = draw.rect(76, 116).x(12).y(12).fill('magenta').stroke('magenta');

Then we want the door to “slide” when the user clicks it:

door.click(function() {
   this.animate().transform({ relative: true, scaleX: 0, cx: 90, cy: 130 });
});

You can read more about the available SVG transformations over here.