Tight Fitting SVG Shapes, the Present and Future

Avatar of Ana Tudor
Ana Tudor on (Updated on )

I have recently found myself wanting to tightly pack an SVG shape into an HTML container. By tight packing, I mean I want the outermost points of my shape to be right on the edges on the container, without anything going out of the container or getting cut off.

The following demo shows an example of this:

See the Pen 4 point star – tight fitted into container by Ana Tudor (@thebabydino) on CodePen.

The outermost vertices of this four-point star are situated on the edges of its container. We have made the SVG fully cover its parent, and we have picked the coordinate system and the coordinates of the star’s points such that the (0,0) point of the SVG canvas is right in the middle and the star’s outermost vertices are on the axes of this coordinate system at the edges of the container. The demo below illustrates how the code works in this case:

See the Pen 4 point star – visual code explanation by Ana Tudor (@thebabydino) on CodePen.

Now let’s say we don’t want it to have a fill, but a stroke of variable width. Drag the slider to adjust the star’s stroke-width:

See the Pen 4 point star – variable stroke by Ana Tudor (@thebabydino) on CodePen.

As the demo above shows, increasing the stroke-width creates a problem: the stroke extends both inside and outside the initial boundaries of the four-point star, making it increase in size and not fit inside its container anymore. It’s obvious that we either need to adjust the position of the vertices or change the viewBox attribute on our <svg> element.

Solution: adjusting the viewBox

The following demo illustrates how adjusting the viewBox would work:

See the Pen 4 point star – adjusting the viewBox by Ana Tudor (@thebabydino) on CodePen.

Dragging the slider thumb to the left decreases the viewBox width and height, keeping them equal at all times, and changes the coordinates of the top left corner of the SVG canvas such that its (0, 0) point is always right in the middle. This is like a “zooming in” effect.

Dragging the slider thumb to the right increases the viewBox width and height, keeping them equal at all times, and changes the coordinates of the top left corner of the SVG canvas such that its (0, 0) point is always right in the middle. This is like a “zooming out” effect.

In our case, in order to fit our four-point star after increasing its stroke-width, we need to zoom out, so we need to increase the viewBox dimensions. But, by how much do we need to increase them in order to have the star just touching the edges? If we don’t zoom out enough, then our star still gets cropped. If we zoom out too much, then it won’t touch the edges anymore. So, what are the viewBox dimensions for the right amount of zoom and how do we get them?

In order to figure this out, we need to pick an exact value we want to increase the stroke-width to—let’s say 20. We’ll now take a closer look at what happens around the outer vertices after increasing the stroke-width to 20:

In the figure above, we have our star represented twice, both before and after increasing the stroke-width. We can see that, after increasing the stroke-width, our star extends outside the initial boundaries by a distance d in all directions. This means that we should increase the viewbox dimensions by twice this distance d.

All right, but how can we get d? Well, in order to do this, we’ll focus on just one of the outer vertices, let’s say the point at (250,0).

We’ll call this point P. We’ll also consider another point, Q, which is the rightmost point of the star after we have increased its stroke-width. It’s situated on the x axis, just like P, only at a distance d to the right of P. If we draw a perpendicular through P onto one of the star’s edges, we get the right triangle PQR, where PQ is equal to d and PR is equal to half the known stroke-width we have applied.

In this right triangle, PQ is the hypotenuse, and the sine of the angle α is the ratio between PR and PQ. PR is equal to half of our known stroke-width (20/2 = 10), while PQ is d. So, from here we get that d equals the ratio between half the stroke-width (10) and the sine of the angle α.

But, it looks like we just replaced one unknown with another, because we don’t know α. However, we can compute it.

Our star is symmetrical with respect to the axes of coordinates, meaning that any of the two axes split our star into two perfectly mirrored halves. In addition to this, when a transversal line cuts two or more parallel lines, the corresponding angles (the angles occupying the same position at each intersection) are equal, as the demo below shows:

See the Pen corresponding angles by Ana Tudor (@thebabydino) on CodePen.

All these mean that, in the figure below, all marked angles are equal. Those above the x axis are each equal to the ones right below them because the star is symmetrical. All those on one side of the axis are again equal, as the dotted lines on the same side of the axis are parallel (and the transversal line is the x axis).

Since all these angles are equal, we can compute α from another right triangle—the one formed by the point P, the next vertex of our star (that we’ll call S), and its projection onto the x axis (that we’ll call T). The projection of a point onto a line is obtained at the intersection between the line and a perpendicular on it drawn through the point.

We know the coordinates of these three points. From the code we have used to create the star, point P is situated at (250,0), and point S is situated at (20,20).

Point T, the projection of point S onto the x axis, has the same x coordinate as S, while its y coordinate is 0. This means that T is situated at (20,0).

Knowing these coordinates, we can compute the lengths of the ST and PT segments. The length of the vertical ST segment is 20, while the length of the horizontal PT segment is 250 - 20 = 230.

The PST triangle is a right triangle, so the tangent of α is the ratio between the ST and the PT segments. Both these segments are known, which means we can compute α as the arctangent of their ratio: atan(20/230).

Now that we have computed α, we can replace it in the relation that gives us the distance d we were looking for: 10/sin(atan(20/230)) ≈ 115 (by the way, I simply googled the result of that). This means that the coordinates of the top left point will be (-250 - 115,-250 - 115) = (-365, -365) and that the viewbox width and height will be 500 + 2*115 = 500 + 230 = 730. In this case, after changing the viewBox (and leaving everything else unchanged), our code becomes:

<svg viewBox='-365 -365 730 730'>
  <polygon points='250,0 20,20 0,250 -20,20 -250,0 -20,-20 0,-250 20,-20'/>
</svg>

You can see the result in the pen below:

See the Pen 4 point star – viewBox correction applied by Ana Tudor (@thebabydino) on CodePen.

Problems

You’ve probably realized the demo above doesn’t look right. The star still looks cropped, even if it now fits inside the visible part of the SVG canvas. Why is this happening, and how can we fix it?

stroke-linejoin

Well, whenever we have a shape made up of various segments that meet at corners, we have a few options about how that’s going to look. These options are controlled by a property called stroke-linejoin. This property can take one of the following four values:

  • miter: (the default) — the segments meet at a sharp angle
  • round: the corner is rounded
  • bevel: looks something like miter, except the vertex is cut off
  • inherit: whatever value was used for its parent group

This pen provides some actual examples:

See the Pen stroke-linejoin values by Ana Tudor (@thebabydino) on CodePen.

Now if we look at the pen above carefully, we’ll notice something interesting about the last join on the first column. It has stroke-linejoin: miter, but the visual result looks different from those of the other joins on the first column (the miter column). In fact, it looks just the result we get for the last join on the last column (the bevel column). And, it looks just like the problem we have with the star at this point. Our line join should be a miter because we haven’t set stroke-linejoin to anything else. But, what we see is a bevel.

stroke-miterlimit

This is because of a property called stroke-miterlimit. As the name may suggest, this property imposes a limit on how much the miter can extend. When this limit is exceeded, the join is simply converted from a miter to a bevel.

Note: SVG2 specifies that line-join will get a couple more possible values, one of them being miter-clip, which will make the join be clipped at the limit value and not to the bevel.

The demo below shows that the smaller the angle between two segments gets, the more the miter would normally extend if the set stroke-miterlimit was high enough so that it never got converted to a bevel. It also shows how the angle at which the miter is converted to a bevel decreases as the value of stroke-miterlimit increases:

See the Pen stroke-miterlimit by Ana Tudor (@thebabydino) on CodePen.

All right, but what’s the connection between the stroke-miterlimit and the actual miter length? In the previous demo, the values of the stroke-miterlimit range from 2 to 15, while the actual miter lengths are over 50 and can go well into the hundreds. The miter length also depends on the stroke-width, so it’s pretty obvious that the imposed limit is not on the actual miter length itself. Instead, this limit is on the ratio between the miter length and the stroke width. And, as the following image shows, this ratio is equal to 1 over the sine of half the angle between the lines that are joined.

The sine of an acute angle (< 90°) increases as the angle increases, so 1 over the sine (the ratio on which we impose the limit set by stroke-miterlimit) is going to increase as the angle decreases.

See the Pen
How sin(θ) and 1/sin(θ) change with θ
by Ana Tudor (@thebabydino) on CodePen.

This kind of explains why the join transforms from miter to bevel. If the angle gets really close to 0, then its sine also gets really close to 0, making the inverse of this value a very large number and having the miter extend a lot. And, while I don’t have any actual metrics on that, I’m thinking it can’t possibly be good for performance.

Fixing the star demo

Now coming back to our star, its angles are not that close to zero and all we need to do in order to prevent the joins from being transformed into bevels is set stroke-miterlimit to 1 over the sine of the α angle we had previously computed. α was atan(20/230), so the value we want is 1/sin(atan(20/230)) ≈ 11.5 and we’ll set stroke-miterlimit: 12 just to be sure. The following pen shows how simply adding this one line to the CSS fixes everything.

See the Pen 4 point star – viewBox & stroke-mitterlimit corrections applied by Ana Tudor (@thebabydino) on CodePen.

Note: If we don’t feel like doing all these computations, then we can simply set stroke-miterlimit to a ridiculously high value. Just how high? Well, 60 is enough for a angle between the lines that are being joined, and it’s pretty unlikely we’ll often need more than that.

Problems

All right, but this method of refitting the star inside its container by changing the viewBox and therefore zooming out has also made the stroke look thinner than we initially intended it to be. We could try to fix this with vector-effect: non-scaling-stroke, but this only creates more problems as the stroke width won’t scale anymore as the SVG’s container, and consequently the SVG itself, change size.

So if we want to solve this, we have to leave the viewBox unchanged and just tweak the coordinates of our star’s vertices.

Solution: tweaking the coordinates of the vertices

We know by how much the star expands outwards if we set stroke-width: 20. We have computed this distance d to be about 115. This means we have to move the outermost vertices inside by 115, from 250 along the axes to 250 - 115 = 135. We can’t change just the coordinates of our outermost vertices, as that would distort our star and, the same time, changing the angles would change how much the miter extends, and the star wouldn’t be tightly packed anymore anyway.

See the Pen 4 point star – adjusting the outer vertices by Ana Tudor (@thebabydino) on CodePen.

Instead, what we need to do is first compute a scaling factor which will be the ratio between the new non-zero coordinate of an outermost vertex and the old one: 135/250 = .54. Then we multiply the old coordinates of the other vertices by this factor in order to get their new coordinates (20*.54 = 10.8).

See the Pen 4 point star – adjusting all vertices by Ana Tudor (@thebabydino) on CodePen.

And… this is it! You can see the final demo here:

See the Pen 4 point star – vertex corrections applied by Ana Tudor (@thebabydino) on CodePen.

Future Solution: stroke-alignment

Of course, the future should bring a lot of good things, including avoiding all the pain that these computations might cause for some. SVG2 will allow us to specify a stroke-alignment — that’s right, controlling whether increasing the stroke-width will make the stroke expand inwards, outwards or on both sides of the element’s outline is going to be posible with just one line of code.

In our case, that would be stroke-alignment: inner. And poof! No other tricks required, no zooming by tweaking the viewBox, no repositioning of the points of our shape… none of that anymore.

But, in the meanwhile, let’s see what I’ve been using this for.

Use case: 3D!

More than two years ago, I started playing with CSS 3D transforms in order to build various 3D shapes. The non-rectangular flat faces (with three, five, six, or ten edges) were created by nesting 2D transformed HTML elements and cutting out unwanted parts with overflow: hidden. Over the past few months, I’ve also started playing with SVG and began thinking about redoing those 3D shapes, this time using an SVG <polygon> for the non-rectangular faces.

Note: If you need a basic geometry refresher, this Pen explains what a polygon is and gives some examples.

This is where I ran into the problem presented above, because how much I move the 2D faces in 3D depends on their 2D dimensions. So, the simplest solution to that in my mind was to have the circumcircle (the circle on which all the vertices are situated) of my regular polygons dead in the middle of the SVG, and at least one of its vertices right on the edge. Since the SVG element would have the same dimensions as its HTML container, this would mean that half the dimension of the HTML container would be equal to the circumradius of the SVG polygon inside.

Note that not every polygon has a circumcircle (but all regular polygons and all triangles do).

So let’s see how I dealt with it all by creating the simplest 3D shapes—prisms! Prisms are polyhedra (3D solids with flat faces and straight edges) with two n-polygonal base faces, connected by n quadrilateral faces. For simplicity, we’ll consider all the lateral faces rectangular and the base polygons regular (all angles are equal and all edge lengths are equal). The demo below allows creating and also flattening a number of prisms.

See the Pen build a prism by Ana Tudor (@thebabydino) on CodePen.

The rectangular faces are easy to get. HTML elements are rectangles by default, so we’ll just use one element for each rectangular face. But what about the bases?

Well, for each base, we’ll take a square (equal width and height, preferably in vmin units, so that they scale with the viewport) HTML element with an SVG inside. The <svg> element covers its container completely (width and height are both set to be 100%), and the polygon inside touches the edges, but nothing gets cut out. Since the two bases are identical, we just define the polygon once and then reference it for each base.

<svg height='0' width='0'>
  <defs>
    <polygon id='basepoly'/>
  </defs>
</svg>

<div class='prism'>
  <div class='prism__face--base'>
    <svg>
      <use xlink:href='#basepoly'/>
    </svg>
  </div>
  <div class='prism__face--base'>
    <svg>
      <use xlink:href='#basepoly'/>
    </svg>
  </div>

  <div class='prism__face--lateral'></div>
  <div class='prism__face--lateral'></div>
  <div class='prism__face--lateral'></div>

  
</div>

However, in this case we’d end up setting the same viewBox on both SVG elements inside the base faces so a small improvement over this version would be to use <symbol>, so that we only have to set the viewBox in one place (on the <symbol> element).

<svg height='0' width='0'>
  <symbol id='basepoly'>
    <polygon/>
  </symbol>
</svg>

<div class='prism'>
  <div class='prism__face--base'>
    <svg>
      <use xlink:href='#basepoly'/>
    </svg>
  </div>
  <div class='prism__face--base'>
    <svg>
      <use xlink:href='#basepoly'/>
    </svg>
  </div>

  <div class='prism__face--lateral'></div>
  <div class='prism__face--lateral'></div>
  <div class='prism__face--lateral'></div>

  <!-- more lateral faces if needed -->
</div>

The basic CSS would position all the geometric elements absolutely in the middle of their container. We’ll make the parent of our 3D shape, in our case the <body> element be the scene by setting a perspective on it. Since the prism is absolutely positioned, we’ll also have to set a height on the <body> and, because we have no other content, we’ll make the body cover the entire viewport. We’ll also give the face elements explicit width and height and make sure they’re dead in the middle by using negative margins. And given that we might want to animate the prism itself in 3D, we’ll set transform-style: preserve-3d on it.

body {
  height: 100vh;
  perspective: 32em;
}

[class*=prism] {
  position: absolute;
  top: 50%; left: 50%;
}

.prism { transform-style: preserve-3d; }

.prism__face--base {
  margin: -13vmin;
  width: 26vmin; height: 26vmin;
}

.prism__face--base svg {
  width: 100%;
  height: 100%;
}

.prism__face--lateral {
  margin: -16vmin -13vmin;
  width: 26vmin; height: 32vmin;
}

Now let’s see how we can get our tight-fitted regular polygon. We start from the fact that all the vertices of a regular polygon are situated on a circle called the circumcircle. But how exactly do we position the vertices on this circle?

Before anything else, we need to know what a circle looks like. A full circle has 360° and we start from the + of the x axis (3 o’clock).

See the Pen full circle – responsive SVG explanation by Ana Tudor (@thebabydino) on CodePen.

If our polygon has n equal length edges, then the arc corresponding to one edge is going to have the number of degrees of a full circle (360°) over n. In the case of an equilateral triangle, n is 3, so the angle is 120°. For a square, n is 4, so the angle is 90°. For a regular pentagon, n is 5, so the angle is 72°.

Now if we go around the circle, starting from , in n steps, then at every step we have a polygon vertex, as illustrated in the demo below.

See the Pen construct regular polygon by Ana Tudor (@thebabydino) on CodePen.

For a triangle, n is 3, so each step is 360°/3 = 120°. This puts the three vertices of our equilateral triangle at 0°,120°, and 240°. For a square, n is 4, making each step 360°/4 = 90°, and puting the four vertices at , 90°, 180°, and 270°. For a regular pentagon, n is 5, the angular step is 360°/5 = 72° and the vertices are situated at , 72°, 144°, 216°, and 288°.

Now we need the coordinates of these vertices in order to draw our polygon. The position of a point in a plane is given by the length of the segment connecting the point to the origin (in our case, the radius r of our circle) and the angle between that segment and the x axis (in our case, i times the angular step, where i is the index of the vertex).

xi = r*cos(i*360°/n);
yi = r*sin(i*360°/n);

If we know the number of vertices/edges, then it’s easy to compute the angles, but how much is the radius? Well, not taking into account the stroke-width of our <polygon> element, this would be half the dimension of our square SVG viewBox (which, let’s say, we’ll take to be 800). Adjusting for the stroke-width is going to mean subtracting half the miter length from this value, just like we did for the four-point star earlier.

In order to do this, we need to compute the angle of our regular polygon, because the miter length depends on the stroke-width and on the polygon angle. We know that the angles in a triangle always add up to 180° (you can drag the vertices to play with the triangle in the demo).

See the Pen triangle angles add up to 180° by Ana Tudor (@thebabydino) on CodePen.

Knowing this, and knowing that we have three angles in a triangle, we get that the angle of a regular (or equilateral) triangle is 60°. But what about other regular polygons? Well, if we connect the first vertex of our polygon to all the other vertices, we can see that we’ve split our polygon into n - 2 triangles.

See the Pen every convex n-polygon can be split into n-2 triangles by Ana Tudor (@thebabydino) on CodePen.

This means that the angles in a polygon with n vertices/edges add up to (n - 2)*180°. Given that all n angles of this regular polygon are equal, we get that each angle is (n - 2)*180°/n. If we actually do the computations, we find that we have 60° for an equilateral triangle, 90° for a square, 108° for a regular pentagon, 120° for a regular hexagon, and so on…

We now have all the data we need for setting the points attribute of our base polygon.

var r = 400 /* circumradius, no correction, half of 800 */, 
    sw = 20 /* stroke-width */, 
    n = 3 /* number of edges */, 

    sym = document.getElementById('basepoly'), 
    poly = sym.querySelector('polygon'), 
    vb = [-r, -r, 2*r, 2*r], 
    points_attr_text = '', 

    base_angle = 2*Math.PI/n, 
    poly_angle = (n - 2)*Math.PI/n, 
    correction = .5*sw/Math.sin(.5*poly_angle), 
    r_final = r - correction;

/* set the viewBox */
/* "viewBox", not "viewbox" (won't work) */
sym.setAttribute('viewBox', vb.join(' '));

for(var i = 0; i < n; i++) {
    curr_angle = i*base_angle;
    x = r_final*Math.cos(curr_angle);
    y = r_final*Math.sin(curr_angle);

    points_attr_text += x + ',' + y + ' ';
}

poly.setAttribute('points', points_attr_text);
poly.setAttribute('stroke-width', sw);

You can see this code working in the following pen. You can tweak the values for the number of edges/ vertices, circumradius or stroke-width in order to get different results. We could pack these polygons even tighter through various methods, but then we’d lose the advantage of having a simple and consistent method of doing it for any number of edges/vertices.

See the Pen n-polygon touching the edges of the SVG container by Ana Tudor (@thebabydino) on CodePen.

Alternatively, we could get the exact same result using Jade.

- var n = 3;
- var sw = 20;
- var r = 400;

- var base_angle = 2*Math.PI/n;
- var poly_angle = (n - 2)*Math.PI/n;
- var correction = .5*sw/Math.sin(.5*poly_angle);
- var r_final = r - correction;

mixin polygon(n)
    - var points = '';
    - for(var i = 0; i < n; i++) {
        - var curr_angle = i*base_angle;
        - var x = r_final*Math.cos(curr_angle)
        - var y = r_final*Math.sin(curr_angle);
        - points += ~~x + ',' + ~~y + ' ';
    - }
    polygon(points=points stroke-width='#{sw}')

svg(width='0' height='0')
    symbol(id='basepoly' 
                 viewbox='#{-r} #{-r} #{2*r} #{2*r}')
        +polygon(n)

svg(class='vis')
    use(xlink:href='#basepoly')

The next step we take is to combine this with the initial code we had and also auto-generate the lateral faces while we create the polygon. You can see the result in the pen below (or, if you prefer to do this the Jade way, be sure to check out this version).

See the Pen generate prism faces (JS) by Ana Tudor (@thebabydino) on CodePen.

All right, but at this point, all our faces are simply stacked one on top of the other in the middle of the screen, so we need to position them in 3D such that they form a prism.

Let’s start with the base faces. Currently, they’re in the vertical plane of the screen and we want them to be in a horizontal plane perpendicular onto the screen; one facing up, and the other one facing down. To do this, we’ll apply a rotateX(90deg) transform for the one we want to face up, and a rotateX(-90deg) transform for the one we want to face down. Applying a rotation on an element also rotates its system of coordinates, so now the z axis, the one that was initially coming out of the screen, towards us, is pointing up and down respectively for the two rotated faces. These faces are now horizontal, but they’re cutting the vertical lateral faces in the middle. So, what we need to do is to translate them vertically (one up and the other one down) by half the height of the lateral faces (which we have taken to be 32vmin in this case, so half of that is 16vmin). Translating vertically after the rotation means translating along the z axis (which was made vertical by the rotation), so we’ll have to apply a translateY transform. The code for this will be:

.prism__face--base:nth-child(1) {
  transform: rotateX(90deg) translateZ(16vmin); /* up */
}
.prism__face--base:nth-child(2) {
  transform: rotateX(-90deg) translateZ(16vmin); /* down */
}

After adding these two rule sets, the two base faces are now in place.

See the Pen position prism bases (JS) by Ana Tudor (@thebabydino) on CodePen.

Positioning the lateral faces means first rotating them around their y axis (with a rotateY transform) such that they each face an edge of the bases, then translating them outwards (with a translateZ()) by the inradius. But what the heck is the inradius? Well, it’s the radius of a circle (called the incircle) that touches each edge of a polygon at precisely one point (each edge is tangent to the incircle). Just like in the case of the circumcircle, not all polygons have an incircle, but all regular ones, like those we are dealing with in the case of our prism bases, do.

Moreover, in the case of regular polygons, the circumcircle and the incircle are centred at the same point, while the circumradius and the inradius split the polygon’s angles and respective edges into two equal halves. Representing it all graphically gives us something like in this Pen:

See the Pen in/circumcircle of a regular polygon – WORK IN PROGRESS (SVG version) by Ana Tudor (@thebabydino) on CodePen.

Here, the circumradii are the segments connecting the central point to the polygon vertices, and the inradii are the segments connecting the central point to the midpoints of the polygon’s edges. Given that the polygon’s edges are tangent to the incircle, the inradii drawn in the demo are perpendicular onto the polygon’s edges.

If we take a triangle formed by an inradius, a circumradius, and half a polygon edge, this triangle is a right triangle and allows us to compute the inradius relative to the circumradius and the polygon’s angle.

See the Pen relation betwen circumradius, inradius, edge length, polygon angle by Ana Tudor (@thebabydino) on CodePen.

In the demo above, this is the OVM triangle. OV is the circumradius, OM is the inradius and VM is half the regular’s polygon edge. The VMO angle is a right angle (which makes the opposing edge, OV, the hypotenuse) and the OVM angle is half the polygon’s angle. The sine of the OVM angle is the ratio between OM and OV and we’ll use this relation to compute OM, as we already know OV and the OVM angle.

The circumradius is half the dimension of the base face that we have set in CSS and we already have the angle of the polygon, so this is all we need for the value of the inradius:

base_face = prism.querySelector('.prism__face--base');
cradius = .5*getComputedStyle(base_face).width.split('px')[0];
iradius = cradius*Math.sin(.5*poly_angle);

And now all we have left to do is to set the proper transforms on each lateral face:

curr_y_rot = i*base_angle + .5*poly_angle;
curr_lat_face.style.transform = 
    'rotateY(' + curr_y_rot + 
    'translateZ(' + iradius + 'px)';

All right, this is getting really close:

See the Pen position prism faces stage #1 (JS) by Ana Tudor (@thebabydino) on CodePen.

The only problem we still have is that the widths of the lateral faces are still not right. We have taken them to be equal to the dimension of the base face, but the base face is twice the circumradius of the base polygon and that’s not equal to the edge of the base polygon. But we can compute the edge from the same triangle as the inradius:

edge_len = 2*cradius*Math.cos(.5*poly_angle);

… then set the proper width and margin-left for the lateral faces:

curr_lat_face.style.width = edge_len + 'px';
curr_lat_face.style.marginLeft = -.5*edge_len + 'px';

And now we have a nice prism, which you can check out in the Pen below (or in its Jade vesion)

See the Pen position prism faces full (JS) by Ana Tudor (@thebabydino) on CodePen.

Best of all, changing the number of edges is as simple as just changing the n variable in the JS—try that for yourself! There are still a couple of little problems to be ironed out. We don’t really need to set the same width and margin-left inline on every lateral face— we could simply put these into a style element. Also, on resize, we need to update these values, as well as the transforms on the lateral faces, because they depend on the pixel dimensions of the bases, which we have set in viewport units so that the prisms scale on resize. This final pen fixes all such little problems:

See the Pen position prism faces final (JS) by Ana Tudor (@thebabydino) on CodePen.