In part 2 of this series on declarative graphics with React and SVG we explore SVG arcs, angles, and transformations, along with a useful little digression into alternative coordinate systems, with practical examples.

02 October 2020

Share

In this mini-series we’re looking at rendering interactive graphical UI’s using components built with React and SVG.

In part 1, Declarative Graphics, we used React to compose simple graphical components from declarative SVG primitives, and covered the basics of the viewBox and the viewport.

Throughout the series we’ll be putting together an interactive floor-plan. In this, part 2, we’re going to add security cameras to the plan and show their field of view, giving us the opportunity to explore arcs, angles, and transformations, along with a useful little digression into alternative coordinate systems.

Let’s start by creating a simple `Camera`

component that we can place inside any `Room`

at a position and orientation we specify. The basic camera will look like this:

Our camera shape is straight-forward, and can be composed as a path with 8 straight-line segments joining up 8 coordinates. SVG `path`

elements are powerful tools that create shapes from lines, curves, and arcs and can be stroked and filled just like `circle`

and `rect`

.

Our initial use of path is relatively simple and consists of simple straight line segments using the `L`

command to join up the 8 coordinates of our camera, marked below with black circles.

We’ll make it easy on ourselves by using single digits of a 4x4 square, so our path is easy to create. The path element takes its path data in a `d`

attribute `<path d={``}/>`

.

We begin by telling it to “move” to the coordinate of the first black circle at (0,0), then proceed to draw lines to each of the other absolute coordinates. The final segment is drawn automatically when we “close” the path with `Z`

.

```
<path
d={`M0,0 L4,0 L3,1 L4,1 L4,4 L0,4 L0,1 L1,1 Z`}
fill="black"
/>
```

Path commands such as `L`

can be absolute or relative, designated by upper or lower case respectively, so an alternative way to write the same camera path is to use relative coordinates, like so:

```
<path
d={`M0,0 l4,0 l-1,1 l1,0 l0,3 l-4,0 l0,-3 l1,0 Z`}
fill="black"
/>
```

Alternatively, a convenient shortcut when drawing lines which are perfectly vertical or horizontal is to use `V`

or `H`

with absolute coords, or `v`

and `h`

with relative coords, yielding yet another alternative way of drawing the same path:

```
<path
d={`M0,0 h4 l-1,1 h1 v3 h-4 v-3 h1 Z`}
fill="black"
/>
```

That’s great, we have a nice camera shape, but there’s a problem: we’ve drawn it with the top corner of the camera at the origin of our user-space. How can we place it at a specific location in our user-space? And what if we want to draw more than one Camera at different locations?

One simple option might be to use string interpolation to set the initial position `Mx,y`

to something other than `0,0`

.

```
const Camera = ({x, y}) => (
<path
d={`M${x},${y} h4 l-1,1 h1 v3 h-4 v-3 h1 Z`}
fill="black"
/>
);
```

OK, that works, but the camera is not positioned *quite* where we want it — we probably meant that x,y is the centre of the camera, so we need to offset it by -50% on each axis. We know that the camera is 4 user-space units wide and 4 units high, so the maths is easy:

```
const Camera = ({x, y}) => (
<path
d={`M${x-2},${y-2} h4 l-1,1 h1 v3 h-4 v-3 h1 Z`}
fill="black"
/>
);
```

This is reasonably straight-forward, and because we used relative coords to define the path we only had to do any math for the initial move `M`

operation. However, there’s an alternative way of positioning things without tinkering with the path, which also opens up some exciting new opportunities…

Shifting the camera to centre it on the given coordinates involved a reasonably simple change to the path, but things would rapidly get out of hand if we want to, say, rotate the camera by 45 degrees using this approach.

Instead, what if we could design our camera for the simple case of being drawn at the origin and pointing straight upwards, and then apply transformations to position and rotate it as required, avoiding any complicated maths?

In fact we can do just that using the `transform`

attribute, which takes a list of transformation operations and applies them in order. Valid transform operations are:

`translate`

— move horizontally and vertically`rotate`

`scale`

- skew (
`skewX`

and`skewY`

) `matrix`

(a mathematical expression that consolidates all of the above)

We can specify a list of these operations in a single transform string like this:

`transform="translate(20,30) rotate(45) scale(10)"`

Recall from part 1 that the initial “user-space” specified by the viewBox is the coordinate system in which our SVG primitives are drawn.

By providing a transform attribute with a transform list we are creating a *new, nested user-space* — a new coordinate system in which the attributed element is drawn.

When applied to a simple element such as a `circle`

, `rect`

, `path`

, etc., the new user-space only applies to that element. However, we can also set a transform on a group `<g>`

element, in which case all elements nested within that group are also drawn in the user-space of the transformed `g`

element.

With this in mind, we can now declare our Camera component such that it can be easily positioned and rotated:

```
const Camera = ({ x, y, angle }) => (
<g transform={`translate(${x}, ${y}) rotate(${angle})`}>
<path
transform="translate(-2, -2)"
d="M0,0 h4 l-1,1 h1 v3 h-4 v-3 h1 Z"
fill="#000"
/>
</g>
);
```

This is great — the code that describes the camera’s shape is now entirely static and distinct from the code that positions and orients the camera. We can make this more explicit by extracting a component for drawing the camera body:

```
const CameraBody = () => (
<path
transform="translate(-2, -2)"
d="M0,0 h4 l-1,1 h1 v3 h-4 v-3 h1 Z"
fill="#000"
/>
);
const Camera = ({ x, y, angle }) => (
<g transform={`translate(${x}, ${y}) rotate(${angle})`}>
<CameraBody />
</g>
);
```

The camera is quite a dark shape against the dark blue background of our floor-plan, so to increase contrast lets give it a semi-transparent circular “enclosure” that lightens the background.

Because this enclosure is nested inside our camera’s group `<g>`

element it exists within the camera’s user-space, which means we can simply draw it at the origin and it will move and rotate with the camera — result!

Note that we draw the enclosure first, before drawing the camera body, so that the camera body appears on top of the enclosure (on the z axis) and does not get lightened by it.

```
const CameraEnclosure = () => (
<circle
cx="0"
cy="0"
r="3.5"
fill="rgba(255,255,255,0.25)"
stroke="#fff"
strokeWidth={0.25}
/>
);
const Camera = ({ x, y, angle }) => (
<g transform={`translate(${x}, ${y}) rotate(${angle})`}>
<CameraEnclosure />
<CameraBody />
</g>
);
```

And now we can render multiple cameras in different positions and orientations and see them clearly on our blueprint backdrop:

```
{
Array.from({ length: 6 }).map((_, i) => (
<Camera
key={`camera_${i}`}
x={Math.round(Math.random() * 5000)}
y={Math.round(Math.random() * 3000)}
angle={Math.random() * 360}
/>
))
}
```

Before we add the cameras to our floor plan, let’s look at showing the camera’s field of view as a segment of a circle centred at the camera origin, looking like this:

We’re not really trying to model the real world here, just keeping things reasonably simple by defining the field of view as an angle describing the arc, and a distance or range that describes how far the camera can “see”. The API for the `FieldOfView`

component could then just be:

`<FieldOfView angle={40} range={500} />`

Note that we don’t need to specify a position for `FieldOfView`

because it will be nested inside the `Camera`

’s group `g`

element, and therefore drawn in the user-space of the `Camera`

.

We can get a feel for how the props relate to the rendered field of view from the diagram below. Remember that the camera is drawn pointing upwards, so the mid-point of the field of view needs to be directly up the y-axis from the origin.

A quick and dirty approximation of the field of view would be a triangle connecting the dots A, B, and C along the dashed lines in the above diagram. We already know the position of A (the origin of our `Camera`

’s user-space), so we just need to work out the positions of B and C, then this approximated path is super-easy to write.

To do that we need to put our math hats on again for some basic trigonometry, but I want to take a little diversion (*or a tangent, ahahaha*) and talk a little bit about coordinate systems.

If — unlike me — you are a bit of a math whiz, you might already be familiar with Euclidean spaces and coordinate systems, in which case feel free to skim the next section. If not, follow me down the rabbit-hole, *I promise its worth it!*

So far we’ve been working with the familiar *Cartesian* coordinate system for representing points in a Euclidean space, where `[x,y]`

represents a distance from the origin along the x and y axes. But this isn’t the only way to represent the same point in Euclidean space!

When working with radial UI’s (think clocks, compasses, dials, and just about anything that has symmetry around a centre), there’s an alternative coordinate representation that makes everything *waaay* simpler to visualise intuitively: a polar coordinate system.

Using polar coordinates, the Cartesian point `[x, y]`

can be expressed instead as a *distance* from the origin or “pole” and an *angle* offset from a reference angle, i.e. instead of `[x,y]`

we have `[distance, angle]`

.

In polar coordinates, the positions of points B and C in our field-of-view are intuitively easy to read from the diagram:

Both are at a distance `range`

from the pole. B is at an angle of `-angle/2`

, while C is at `angle/2.`

So the polar coordinates for B and C are `[range, -angle/2]`

and `[range, angle/2]`

respectively. Boom!

Converting from polar to Cartesian coordinates is straight-forward, all we need is a simple function:

```
export const REFERENCE_DIRECTION = -90;
export const polarToCartesian = (distance, angleInDegrees) => {
let angleInRadians = toRadians(REFERENCE_DIRECTION + angleInDegrees);
return {
x: distance * Math.cos(angleInRadians),
y: distance * Math.sin(angleInRadians)
};
};
```

Note: because of the way sine and cosine work, the reference direction for polar coordinates defaults to being along the positive Cartesian x-axis (or 3 o’clock, or due West if you picture a compass). I find it much more intuitive to work with a reference direction due North or 12 o’clock, hence I’m subtracting 90 degrees to establish my reference direction.

Armed with this function we can calculate the Cartesian coordinates of B and C as simply:

```
let B = polarToCartesian(range, -angle/2);
let C = polarToCartesian(range, angle/2);
```

And with that, the triangular approximation of the field of view is easy to write:

```
const FieldOfView = ({ angle, range }) => {
let B = polarToCartesian(range, -angle/2);
let C = polarToCartesian(range, angle/2);
return (
<path
d={`
M0,0
L${B.x},${B.y}
L${C.x},${C.y}
`}
fill="rgba(0,0,255,0.25)"
/>
);
};
```

Overlaying this approximation on our earlier diagram gives this:

All this talk of polar coordinate systems might feel a bit like overkill, but it really does make things easier to visualise, completely avoids a whole bunch of edge cases (for example if the field-of-view angle is greater than 180 degrees), and where it *really* shines is when you need to capture user-interaction, e.g. from Cartesian coordinates in mouse or touch events, in terms of the angle and distance — as is often the case with dials, clocks and radial meters and inputs.

If you’re unconvinced of the utility of polar coordinates, a nice exercise might be to try to rewrite the `FieldOfView`

component *without* using them (and make sure to handle the greater than 180 degrees case). If after that you’re *still* unconvinced, consider how you might capture input such as in this example circular-slider that I wrote a few years ago.

Whew! We’ve covered a lot of ground and learned a ton of helpful stuff, so we're all warmed up to take a look at the most convoluted concept in SVG paths — arcs.

Arcs are convoluted partly because they are defined — like the other path commands — as starting and ending at specific coordinates (the red dots in the image above). This makes complete sense in the context of a path, but accounts for some of the difficulty in getting to grips with the arc command, which looks like this:

`A rx ry x-axis-rotation large-arc-flag sweep-flag x y`

An arc is basically a *segment of the boundary of an ellipse*, so the arc command has to do several things:

- describe the shape and orientation of the ellipse (this is handled by the
`rx`

,`ry`

,`x-axis-rotation`

parameters) - specify the end points of the arc (start point is implicit, end point is specified by
`x, y`

) - select which of the 4 possible arcs that fit criteria 1 and 2 should actually be drawn (
`large-arc-flag`

,`sweep-flag`

)

Point 3 is where things get tricky, so let’s have a quick look at the easy parts first.

The shape of the ellipse that the arc lies upon is described by the first 3 parameters `rx`

, `ry`

, and `x-axis-rotation`

, where `rx`

is the radius on the x axis, `ry`

is the radius on the y axis, and `x-axis-rotation`

is a rotation angle against the x-axis. This is enough to describe the *shape* and *orientation* of the ellipse, but not its *position*.

The starting point for the arc is implicit, and not part of the arc command itself — it is simply the coordinate at which the previous path command left off. The end-point is supplied as the last two parameters of the arc command, `x`

and `y`

.

We’ve defined the shape of the ellipse, and the two coordinates that the ellipse’s boundary has to pass through, but that isn’t yet enough to describe the arc we want.

In fact, at this point there are 4 possible arcs that we could mean, made from two possible arc-segments of two possible ellipses! Consider the diagram below.

To finally nail down which arc we want to draw, we need to specify which path to take around which ellipse. This is the purpose of the two flag parameters `large-arc-flag`

and `sweep-flag`

which must have a value of either 1 or 0 to specify true or false respectively, and combine to decide which of the 4 possible arcs to draw.

From the diagram above its easy to guess what the `large-arc-flag`

is for — it specifies whether we mean to take the long way around (the larger arc, blue or orange in our diagram) on whichever ellipse, or the shorter arc (pink or green). Setting the `large-arc-flag`

to 1 will send us around one of those two longer arcs (blue/orange), while 0 would pick the shorter (pink/green) arcs instead.

The `sweep-flag`

is the final deciding factor that selects which arc we meant, and is basically answering the question “should I travel clockwise around the ellipse from the start point to the end point?”

Independently of `large-arc-flag`

, for each possible value of the `sweep-flag`

we again have two possible segments:

So we see that the values of `large-arc-flag`

and `sweep-flag`

can combine to determine which of the 4 possible arcs we want to draw, by specifying whether to take the long way around, and whether to move clockwise or anti-clockwise from the start point to the end point.

To achieve the arc we started this section with we want to draw the pink arc, which means taking the small arc in the clockwise direction, so the values we need for `large-arc-flag`

and `sweep-flag`

are 0 and 1 respectively to produce this:

One last thing I want to mention, which the mathematically inclined may have spotted, is how on earth would you go about making sure that for the given `rx`

, `ry`

, and `x-axis-rotation`

, the ellipse actually will touch both the start and end point? To illustrate what I mean, consider the path below:

`M20,50 L40,50 A10,10,0,1,1,70,50 L90,50`

Here we’ve specified a horizontal line, followed by circular arc (`rx`

and `ry`

are equal, both 10), that spans the gap from the start point at 40,50 to the end point at 70,50, followed by another horizontal line… but wait, the math doesn’t work out — a radius of 10 means the arc will only bridge between two points that are 20 units apart, but our start and end points are 30 units apart! Logically it seems like this might produce the disconnected path shown below:

In fact, the SVG path goes out of its way to handle this for you by scaling the arc until it will fit to your end points — which effectively means that `rx`

and `ry`

are actually just specifying *proportional* radii to establish the shape of the ellipse, and you don’t have to get into the complicated math of making sure that all of your numbers fit together perfectly.

This is what the path above actually produces:

When we last saw our `FieldOfView`

we’d used a triangle to approximate the shape we wanted. Armed with our knowledge of arcs we can finally complete the intended shape below.

Here's a quick reminder of the arc command:

`A rx ry x-axis-rotation large-arc-flag sweep-flag x y`

We already know the start and end points of the arc — B and C in the diagram, which we calculated using our `polarToCartesian`

function.

We also know the radii (`rx`

and `ry`

), which are both simply the `range`

prop (although we could just set them both to be 1, given that they only establish the proportions of the ellipse, and our arc is circular rather than ellipsoid).

`x-axis-rotation`

has no effect when the radii are equal (because it doesn’t matter how much you rotate a circle, it will always look the same!), so we can leave that at 0.

That just leaves us with those pesky flags to work out, but actually our case is relatively simple.

The `large-arc-flag`

is determined by whether or not our field of view angle is larger than 180 degrees. If it is then large-arc-flag must be 1, and otherwise it must be zero.

Finally, recall that we are drawing our camera pointing directly upwards, with the field of view arc starting at point B at `-angle/2`

, and the arc ending point C at `+angle/2`

. The field of view arc will therefore always start somewhere to our left and end to our right if we’re looking out from the camera’s perspective, and we will always want to travel around the ellipse in a clockwise direction, so the `sweep-flag`

will always be 1.

Our arc command is therefore simple to write using string interpolation:

`A${range},${range},0,${angle > 180 ? "1": "0"},1,${C.x},${C.y}`

And the `Camera`

component, complete with `FieldOfView`

, now looks like this:

That’s a lot of bang for buck — we have a neat, re-usable Camera component that we can position anywhere in our SVG and point in any direction, and all that in barely 50 lines of declarative code!

Finally we can drop some camera's into our floor-plan, which gives us this little beauty:

In this post we covered an awful lot of ground:

- Basics of the SVG
`<path>`

element for drawing simple paths using lines, both absolute and relative. - Using the
`transform`

attribute to position, orient, and scale our components separately from the definition of their shape, by applying transforms to an enclosing group`<g>`

element. - A digression into polar coordinates — a useful addition to our toolbox for thinking about and working with radial imagery and UI’s.
- And finally, we looked at the SVG path’s arc command, and broke it down so that we can understand how to tame it.

In “Part 3 — Clip-paths”, we’ll look at how we can keep those lovely camera field-of-view’s from spilling out of the rooms.