Friday, 5 June 2020

An introduction to SVG - part 2

Part 1 explored creating SVG graphics using XML embedded in a web page and then followed that up with some JavaScript that generated the SVG to duplicate the original output. This part will follow on from those simple <text> and <line> tags with some more SVG goodies starting with some shapes.

Assuming that you are following on from part 1 we can keep the current HTML, CSS and svg.js files and try out some SVG shapes with changes to the main.js file.

var div = document.getElementById("dvScale"); function initialise(){     let svgAttr = {"viewBox": "0 0 510 30"};     let svg = createSvgElem("svg", svgAttr, null, div);     svgAttr = {x: 2, y: 2, width: 30, height: 20};     createSvgElem("rect", svgAttr, null, svg); } initialise();

Which gets us a solid black rectangle. Looks like we need some style.

var div = document.getElementById("dvScale"); function initialise(){     let svgAttr = {"viewBox": "0 0 510 30"};     let svg = createSvgElem("svg", svgAttr, null, div);     svgAttr = {x: 2, y: 2, width: 30, height: 20};     let svgStyle = {stroke: "lime", fill: "white"};     createSvgElem("rect", svgAttr, svgStyle, svg); } initialise();

We could also set the stroke-width and you are likely to often want a transparent fill. Try using:

let svgStyle = {stroke: "lime", fill: "transparent", "stroke-width": 3};

Rounded corners? Specify the corner in the style object.

let svgStyle = {stroke: "black", fill: "transparent", "stroke-width": 3, rx: 5, ry: 5};

For a circle we are going to have to specify the centre and the radius while an ellipse effectively specifies two radii (one for the vertical y dimension and the other for the horizontal).

var div = document.getElementById("dvScale"); function initialise(){     let svgAttr = {"viewBox": "0 0 510 30"};     let svg = createSvgElem("svg", svgAttr, null, div);     svgAttr = {cx: 20, cy: 15, r: 12};     let svgStyle = {stroke: "hotpink", fill: "transparent", "stroke-width": 1};     createSvgElem("circle", svgAttr, svgStyle, svg);     svgAttr = {cx: 60, cy: 15, rx: 12, ry: 6};     createSvgElem("ellipse", svgAttr, svgStyle, svg); } initialise();

A polyline has multiple x/y coordinates expressed as a single attribute. You will need to decide how you will ensure that the specification is easy to maintain, particularly if a given line is long. I chose to have the x and y values separated by a space and each point separated by a comma (OK and a space).

var div = document.getElementById("dvScale"); function initialise(){     let svgAttr = {"viewBox": "0 0 510 30"};     let svg = createSvgElem("svg", svgAttr, null, div);     svgAttr = {points: "0 0, 10 10, 0 20, 30 20, 40 10"};     let svgStyle = {stroke: "teal", "stroke-width": 1};     createSvgElem("polyline", svgAttr, svgStyle, svg); } initialise();

But that code draws:

The polyline is (perhaps unexpectedly) filled, so we probably need to be more specific. Change the style object as follows and give it another run.

let svgStyle = {stroke: "teal", "stroke-width": 1, fill: "transparent"};

The SVG polygon looks just like a <polyline> but there is an implicit line drawn from the last specified point back to the first, to enclose the shape.

var div = document.getElementById("dvScale"); function initialise(){     let svgAttr = {"viewBox": "0 0 510 30"};     let svg = createSvgElem("svg", svgAttr, null, div);     svgAttr = {points: "15 0, 0 10, 5 30, 25 30, 30 10"};     let svgStyle = {stroke: "red", "stroke-width": 1, fill: "transparent"};     createSvgElem("polygon", svgAttr, svgStyle, svg); } initialise();

The <polyline> and <polygon> tags bring us to the even more flexible but potentially complex <path>.

The specification for a <path> tag "d" attribute is akin to a language as it can contain a sequence of commands. The command sequence is used to define a set of lines and curves. For an explanation of the individual command options I recommend reading the Mozilla page here.

In brief. The command designated by the uppercase letter "M" is a "move to" command with a specified x/y coordinate. A lower case "m" specifies a relative movement from the current location. L and l define drawn lines to an absolute or relative location. H and h draws a horizontal line with just an x location specified. Similarly V and v draw vertical lines with just a y value. Z or z closes a path by drawing a line back to the start point. C and c commands can be used to draw Cubic Bezier curves. Similarly S, Q and T will draw alternate Bezier curve types. A and a can be used to draw arcs. All of the path components form a contiguous whole.

You are probably not going to hand code (or edit XML for) lengthy <path> tags. However there are a number of SVG editors around that can be used to create and maintain complex shapes. Once a shape has been defined then you can scale it and apply styles.

You might like to copy and paste the following example paths and then maybe play around with the <div> dimensions, viewBox attributes and the style object.

var div = document.getElementById("dvScale"); function initialise(){     let svgAttr = {"viewBox": "0 0 510 60"};     let svg = createSvgElem("svg", svgAttr, null, div);     let svgStyle = {stroke: "red", "stroke-width": 1, fill: "transparent"};     svgAttr = {d: "m31.847256,20.147449c7.627459,-16.538212 37.512093,0 0,21.263415c-37.512093,-21.263415 \      -7.627459,-37.801627 0,-21.263415z"};     createSvgElem("path", svgAttr, svgStyle, svg);     svgAttr = {d: "m74.805598,52.920259c-1.325839,-1.078225 2.076708,-1.107086 \     1.542153,-2.302318c0.977352,-1.583842 3.518568,-2.542536 3.892802,-4.342332c-1.030665,-0.500215 \      -1.623434,-1.709964 0.342901,-1.566342c1.44199,-0.756148 1.261771,-2.303513 1.695423,-3.439055 \      c0.40623,-1.83279 0.498224,-3.687829 0.67271,-5.535677c-1.668471,-0.089279 -3.378213,-0.027091 \      -5.003929,-0.324922c0.046763,-1.228895 3.354712,-0.757366 3.50527,-1.916973c2.40519,-0.030278 \      0.133222,-2.53291 -0.84626,-3.129996c-1.253728,-1.007249 -2.088776,-2.597297 -1.135676,-3.841233 \      c1.363243,-0.630993 4.445648,-0.246902 4.306324,-1.90514c-0.92712,-0.368159 -2.104238,-0.134364 \      -3.143361,-0.199472c0.063904,-0.773238 0.127806,-1.546474 0.19171,-2.319711c1.374225,-0.073638 \      2.748453,-0.147281 4.122678,-0.220925c-0.697538,-0.505036 -1.741481,-0.876715 -2.002715,-1.591114 \      c0.888562,-1.017564 3.03006,-0.927739 4.564204,-0.918118c1.491783,-0.071682 4.211677,0.621513 \      2.178285,1.738058c-0.164809,0.274194 -1.76877,0.955962 -0.777544,0.881636c1.269146,0 2.538289,0 3.807432,0 \      c-0.003647,0.755909 -0.032817,1.519331 0.222013,2.259079c-0.968779,0.50261 -4.110267,-0.42933 \      -3.215785,1.121467c1.00718,0.648374 2.549715,0.631263 3.827187,0.916809c0.745128,0.05186 1.337877,0.187126 \      1.06936,0.793888c0.140053,1.12655 -0.267946,2.259795 -1.379505,3.114961c-0.78685,0.892343 -2.554104,2.009454 \      -2.030939,3.04235c0.989808,-0.112243 1.784926,0.34051 0.994348,0.849666c0.811317,0.418481 3.668275,0.322648 \      3.050353,1.370331c-1.44748,0.333793 -3.002438,0.194731 -4.504179,0.228793c0.399856,2.859806 0.952729,5.719883 \      1.876543,8.520375c0.539773,0.621425 2.683955,1.099034 0.92425,1.909172c-0.656721,1.508686 1.44088,2.638331 \      2.758263,3.606642c1.448451,0.755733 0.745692,2.276334 2.794824,2.518852c1.613781,1.089034 -2.215606,1.06004 \      -3.198757,1.062415c-6.508579,0.04889 -13.024179,0.11729 -19.530211,-0.028817c-0.532596,-0.046826 -1.190603,-0.036114 \      -1.57017,-0.35235l-0.000002,0.000001z"};     createSvgElem("path", svgAttr, svgStyle, svg); } initialise();

I am sure that the second drawn object (a chess piece) underlines the suggestion that you might like to do an Internet search for an SVG editor when faced with some sorts of SVG drawing challenges.

A graduated fill would be fun although it is just a little bit more fiddly than the <canvas> equivalent. This is because when building SVG graphics we are actually constructing XML. With that in mind, the following should seem straightforward. I did edit the main.css file to increase the size of the dvScale <div> to create a bit more display space though. I went for 500px wide and 300px high and that will be reflected in the dimensions of the viewBox attribute.

The code comments document the steps.
var div = document.getElementById("dvScale"); function initialise(){     let svgAttr = {"viewBox": "0 0 500 300"};     let svg = createSvgElem("svg", svgAttr, null, div);     // gradients are located in a "defs" element in the XML     let defs = createSvgElem("defs", null, null, svg);     // now we define the gradient attributes including an id     svgAttr = {x1: 0, x2: 0, y1: 0, y2: 1, id: "grad1"}; // vertical gradient     let grad = createSvgElem("linearGradient", svgAttr, null, defs);     // we now create 3 stops for the gradient     svgAttr = {offset: "0%", "stop-color": "red"};     createSvgElem("stop", svgAttr, null, grad);     svgAttr = {offset: "50%", "stop-color": "black", "stop-opacity": 0};     createSvgElem("stop", svgAttr, null, grad);     svgAttr = {offset: "100%", "stop-color": "blue"};     createSvgElem("stop", svgAttr, null, grad);     // now a rectangle to show the gradient off     svgAttr = {x: 20, y: 20, width: 100, height: 100, rx: 10, ry: 10};     createSvgElem("rect", svgAttr, {fill: "url(#grad1)"}, svg); // the style adds the gradient fill } initialise();

The result is quite pleasing:

There are also radial gradients with some additional features but the ability to create a pattern and use that as a fill is a standout feature.

var div = document.getElementById("dvScale"); function initialise(){     let svgAttr = {"viewBox": "0 0 500 300"};     let svg = createSvgElem("svg", svgAttr, null, div);     // patterns like gradients are inside a defs element     let defs = createSvgElem("defs", null, null, svg);     // now a horizontal gradient     svgAttr = {id: "grad1"};     let grad = createSvgElem("linearGradient", svgAttr, null, defs);     // with 2 stops     svgAttr = {offset: "10%", "stop-color": "white"};     createSvgElem("stop", svgAttr, null, grad);     svgAttr = {offset: "95%", "stop-color": "blue"};     createSvgElem("stop", svgAttr, null, grad);     // now we define the pattern     svgAttr = {x: 0, y: 0, width: 0.25, height: 0.25, id: "patt1"};     let patt = createSvgElem("pattern", svgAttr, null, defs);     // we can now create the components of the pattern     svgAttr = {x: 0, y: 0, width: 50, height: 50};     createSvgElem("rect", svgAttr, {fill: "cyan"}, patt); // a square     svgAttr = {cx: 25, cy: 25, r: 20}; // with a circle inside it     createSvgElem("circle", svgAttr, {fill: "url(#grad1)", "fill-opacity": 0.5}, patt);     // and a rectangle to show the repeating pattern off     svgAttr = {x: 20, y: 20, width: 200, height: 200, rx: 10, ry: 10};     createSvgElem("rect", svgAttr, {fill: "url(#patt1)"}, svg); } initialise();

The pattern repeats as it fills the rectangle. Patterns can be created using any combination of SVG drawing elements.

SVG graphics includes transformations and if you are mathematically inclined you can apply matrices but for the moment we could try a rotation. You will need to create a bit more screen space for this one but start by just changing the svgAttr object for the pattern filled rectangle to read:

svgAttr = {x: 120, y: 0, width: 200, height: 200, rx: 10, ry: 10, transform: "rotate(30)"};

Not bad for just a few lines of code.

A prowl around the Mozilla web site will reveal many other SVG features but I hope that this two part introduction has shown how to apply at least the basics from JavaScript. I think though that we should end with an animation.

Edit the final rectangle attribute object as shown below and store a reference to the created "rect" in a variable. We need that as we are going to add an "animate" tag to the "rect". Then add the two extra lines that define an animation that repeatedly adjusts the radius applied to the corners over a 10 second time-frame. Enjoy.

svgAttr = {x: 120, y: 0, width: 200, height: 200, transform: "rotate(30)"}; let rect = createSvgElem("rect", svgAttr, {fill: "url(#patt1)"}, svg); svgAttr = {attributeName: "rx", values: "0;100;0", dur: "10s", repeatCount: "indefinite"}; createSvgElem("animate", svgAttr, null, rect);

<added 29/06/20>
This texture library for SVG looks interesting and could be just the thing for charts and diagrams.
</added>

No comments:

Post a comment