An adaptive css3 turntable with square content box

In this article we will build a turntable with squares in each sector unit using SCSS. There are many fantastic examples such as rotating-menu and Pie-Menu. We will focus on creating such turntables step by step.

1. A turntable with arbitrary number of sectors

i. Building a circle

Consider the turntable’s outer container as a circle. You can create the circle with div/css3. Either fixed or float sizing is OK for it. In this post I use float sizing method to make it more adaptive. Below is an example scss snippet of basic rules that styling such a container.

<div class="sectors-container"></div>

  $container-scale: 60%;
.sectors-container {
  width: $container-scale;
  padding-top: $container-scale;
  position: relative;
  overflow: hidden;
  border-radius: 50%;
  }

The variable $container-scale can be adjusted to any size in any units if you need. You may also apply vw/vh rules to both width and height.

ii. Styling a single sector area

The turntable has arbitrary number of child sectors that uniform the circle. To get a single sector with one arbitrary arc angel less than 180deg we use css3 transform skew property, which is a general approach shown in others’ great posts above.

Some may apply shew on one axis only. We apply skew on both x and y axis with half the skew angel so that the result will be a diamond rather than a parallelogram. We’ll also get more convenience of this strategy which will be discussed later in this post.

Suppose we need to divide one circle into \(n\) sectors with the same arc angle. Let \(\begin{align}\theta\end{align}\) be half the arc angle. Let \(\begin{align}\alpha\end{align}\) be the that skew angle, we have: \begin{align}\theta + \alpha = \frac\pi{4}\end{align}

iii. Positioning of sectors in circle

In order to position a sector easily in one circle, we choose one of the sector’s corner as transform origin. In this post, for example, we use the right-bottom corner and make it coincide with the circle center.

.sectors-container >div {
    right: 50%;
    bottom: 50%;
    width: 50%;
    height: 50%;
    position: absolute;
    transform-origin: 100% 100%;
    }

iv. Fill sectors into circle

Now we duplicate other sectors and position them around the circle. Below is the basic code for 7 sectors in total.

$list: 7;
$skew-angle: 45deg-180deg/$list;
@for $idx from 1 through $list {
    .sectors-container >div:nth-child(#{$idx}) {
      transform: rotate(($idx)*360deg/$list) skew($skew-angle, $skew-angle);
    }
  }

And after applying rotate on each sector with increment angel, we get an adaptive turntable container.

2. Square content area in each sector, sizing and positioning

i. Square content area in each sector

If float content sizing & positioning is important, we need some precise controls. Imagine you need to put content with different size into a turntable, and the content area should be as large as possible. Thus finding out the largest inscribed square parallel with the symmetry axis of the sector seems to be a good start.

<div class="sectors-container">
    <div data-role="sector">
        <span data-role="content">content</span>
    </div>
    ...
</div>

In the dom structure, the only child of each sector is the content dom. We can draw this geometric approach intuitivly on a white paper as shown in figure 1. Below is the derivation of fetching necessary parameters for style rules.

C

D

O

A

B

\begin{align}\theta\end{align}

\begin{align}\alpha\end{align}

Figure 1

 

Set \(C\) as the center of the circle, \(O\) as the center of the square, ad \(D\) as the intersection point of segment \(CD\) and the square edge \(AB\). Set the edge length of the content square as \(s\), the length of \(CD\) as \(d\), and the radius of the container circle as \(r\).According to pythagorean theorem we have the relation between \(s\), \(d\) and already known variables \(r\), \(\begin{align}\theta\end{align}\): \begin{align} tan(\theta) = \frac{\frac{s}{2}}{d} \\ (s+d)^2 + (\frac{s}{2})^2 = r^2 \end{align} Solving this system we have the value of \(s\) and \(d\): \begin{align} d & = \frac{r}{\sqrt{(2 tan(\theta) + 1)^2 + tan^2(\theta)}} \\\\ & = \frac{r}{\sqrt{5 tan^2(\theta) + 4 tan(\theta) + 1}} \\ \\ s & = 2 tan(\theta) d \\\\ & = \frac{2 tan(\theta) r}{\sqrt{5 tan^2(\theta) + 4 tan(\theta) + 1}} \\ \end{align}

ii. Retrieve the style property values

The above calculated value cannot be applied as css property values directly because the skew transform is not scale-invariant. In order to positioning/sizing the inner content square in each sector, we need to make reverse derivation to retrieve css rules for squares before transform.

C

O0

E

F

Figure 2

 

The above figure 2 shows the original status of a single sector and it’s content child before transform. The major process form figure 2 to figure 1 can be described as follows:

 step1. skew <sector> which is the parent of content (transform origin is right bottom);
 step2. skew <content> back to correct the parent's skew effect (transform origin is center);
 step3. rotate <content> to make the content square parallel with 
        the symmetry axis of the sector;

Set \((x,y)\) as any vector on the sector plane. The skew process in step 1 will transform it to another vector \((x',y')\). The relation between them is: \begin{equation*} \begin{bmatrix} x' \\ y' \end{bmatrix} = \begin{bmatrix} 1 & tan(\alpha) \\ tan(\alpha) & 1 \end{bmatrix} \begin{bmatrix} x \\ y \end{bmatrix} \end{equation*} For any segment on the original sector, we can calculate the scale variation ratio of the transform: \begin{align}(1+tan(\alpha))\end{align}

 

Similarly, for any vector on the inner square plane, the skew back process in step 2 will transform it to another vector \((x'',y'')\). The relation between them is: \begin{equation*} \begin{bmatrix} x'' \\ y'' \end{bmatrix} = \begin{bmatrix} 1 & tan(-\alpha) \\ tan(-\alpha) & 1 \end{bmatrix} \begin{bmatrix} x' \\ y' \end{bmatrix} \end{equation*} The scale variation ratio of the transform is: \begin{align}(1+tan(-\alpha))\end{align}

NOTE: Although these two transforms is of different coordinate system, we can still calculate the right scale variation ratio without an uniform coordinate representation.

 

Now we will retrieve the css rules of the inner square.
Sizing of the square is decided by width/height property in our case. The original width/height has changed twice during step 1 and step 2. Set the original width/height as \(\mathbf{s_0}\). According to upper derivation we have: \begin{align} \mathbf{s_0} = \frac{s}{(1+tan(-\alpha)) (1+tan(\alpha))} \end{align}
Positioning of the square is decided by top/right/bottom/left property. In our case, right+bottom is enough, aligned to it's parent. As shown in figure 2, the original edge distance is the length of \(EF\). \begin{align} |\vec{\mathbf{EF}}| = |\vec{\mathbf{O_0E}}| - |\vec{\mathbf{O_0F}}| \\ |\vec{\mathbf{O_0C}}| = \sqrt{2}|\vec{\mathbf{O_0E}}| \end{align} Set the edge distance \(\vec{\mathbf{EF}}\) as \(e\). The length of \(\vec{\mathbf{O_0F}}\) is half the square edge length, i.e. \(\begin{align}\frac{s_0}{2}\end{align}\). Thus the above two equations can be simplified as: \begin{align} e = \frac{|\vec{\mathbf{O_0C}}|}{\sqrt{2}} - \frac{s_0}{2} \end{align} In step 1, the center of the square is transformed to O in figure 1, and the length of \(\vec{\mathbf{O_0C}}\) has changed \(\begin{align}(1+tan(\alpha))\end{align}\) times. Step 2 doesn't affect the position of O because the transform origin is the square center. Thus we have: \begin{align} (1+tan(\alpha)) |\vec{\mathbf{O_0C}}| = |\vec{\mathbf{OC}}| \end{align} where \(\vec{\mathbf{OC}}\) in figure 1 is transformed vector of \(\vec{\mathbf{O_0C}}\) in figure 2.
The module of \(\vec{\mathbf{OC}}\) can be calculated as: \begin{align} |\vec{\mathbf{OC}}| = d + \frac{s}{2} \end{align} After all we have the edge distance value: \begin{align} e & = \frac{|\vec{\mathbf{O_0C}}|}{\sqrt{2}} - \frac{s_0}{2} \\\\ & = \frac{|\vec{\mathbf{OC}}|}{\sqrt{2} (1+tan(\alpha))} - \frac{s_0}{2} \\\\ & = \frac{d + \frac{s}{2}}{\sqrt{2} (1+tan(\alpha))} - \frac{s_0}{2} \\\\ \end{align}

iii. Result

Below is some examples of turntables with different amount of sectors.

1
2
3
1
2
3
4
5
6
1
2
3
4
5
6
7

I will paste the core SCSS code as the end.

$container-scale: 60%;
$list: 7;
$rotate-offset: 45deg-180deg/$list;
$skew-angle: 45deg-180deg/$list;

.sectors-container {
  list-style: none;
  width: $container-scale;
  margin: 30px auto;
  padding-top: $container-scale;
  position: relative;
  overflow: hidden;
  border-radius: 50%;
  box-shadow: 0 0 10rem rgba(255,255,255,0.3);
  border: 1px solid #000;
  background: #eee;

  /**/
  $radius: 100%;
  $theta : pi() / $list;
  $alpha : pi() / 4 - $theta;

  $center-distance: $radius / sqrt(5 * pow(tan($theta), 2) + 4 * tan($theta) + 1);
  $transformed-edge-length: 2 * tan($theta) * $center-distance;

  $skew-scale-ratio-1: (1 + tan($alpha));
  $skew-scale-ratio-2: (1 + tan(-$alpha));
  $skew-scale-ratio: $skew-scale-ratio-1 * $skew-scale-ratio-2;

  $initial-edge-length: $transformed-edge-length / $skew-scale-ratio;

  $edge-distance: -1/2 * $initial-edge-length + ($center-distance + $transformed-edge-length / 2)/ $skew-scale-ratio-1 / sqrt(2);

  >div {
    right: 50%;
    bottom: 50%;
    width: 50%;
    height: 50%;
    position: absolute;
    transform-origin: 100% 100%;
    span{
      width: $initial-edge-length ;
      height: $initial-edge-length;
      display: flex;
      align-items:center;
      text-align: center;
      justify-content:center;
      vertical-align: bottom;
      position: absolute;
      color: #1875E7;
      font-weight: bolder;
      bottom: $edge-distance;
      right: $edge-distance;
      transform: skew(-$skew-angle, -$skew-angle) rotate(-45deg);
      z-index: 10;
      font-size: 5vw;
      //border: 1px solid #000;
    }
  }
  @for $idx from 1 through $list {
    >div:nth-child(#{$idx}) {
      transform: rotate(($idx)*360deg/$list + $rotate-offset) skew($skew-angle, $skew-angle);
    }
  }
}

The scss math dependency can be fetched from sass-math and sass-sqrt-function

Written on July 28, 2017