[原創] Kaleidoscope Back

kaleidoscope

Figure 1 A simple preview of kaleidoscopes

In Chinese, we will name kaleidoscopes as "萬花筒", which is a toy showing colourful shapes of thousands of flowers. Today, inspired by the project, spring loaders, I have tried to create a similar component with CSS and SVG, rather than Canvas. As Figure 1 shown, that's my kaleidoscope.

1. Introduction

Without reading the article, I have just created it through several processes. How to create, how to create regular polygons, and how to make it reusable for all kinds of regular polygons.

1.1 How to create?

At the start, I have utilized the feature of div, which is an HTML element with a shape of rectangles. By setting the same value for the property width and height of these elements, I can easily generate a square like the first item from the left side in Figure 1.

Then, to simulate such rotating animation, I would like to use the transform and transform-origin property to implement it, rather than use Canvas, as Canvas is something complicated, which has always been confusing me.

.item {
    transform: scale(0.5) rotateZ(45deg);
    transform-origin: center;

    /** other browsers */
    -webkit-transform: scale(0.5) rotateZ(45deg);
    -webkit-transform-origin: center;

    -moz-transform: scale(0.5) rotateZ(45deg);
    -moz-transform-origin: center;

    -ms-transform: scale(0.5) rotateZ(45deg);
    -ms-transform-origin: center;

    -o-transform: scale(0.5) rotateZ(45deg);
    -o-transform-origin: center;
}

The code snippet above has shown us how to use such two properties to animate our elements. With setting scale(0.5) and rotateZ(45deg), the element will be scaled by half, and rotate 45 degrees clockwise.

If using JavaScript to change this value dynamically, we can code like this:

/** theta is the rotating degree */
const theta = 45; 
const scaleRatio = 0.5;
const target = document.querySelector('.item');

target.style.transformOrigin = 'center';
target.style.transform = 'scale(' + scaleRatio + ') rotateZ(' + theta + 'deg)';

/** other browsers */
target.style.WebkitTransformOrigin = 'center';
target.style.WebkitTransform = 'scale(' + scaleRatio + ') rotateZ(' + theta + 'deg)';

target.style.MozTransformOrigin = 'center';
target.style.MozTransform = 'scale(' + scaleRatio + ') rotateZ(' + theta + 'deg)';

target.style.MsTransformOrigin = 'center';
target.style.MsTransform = 'scale(' + scaleRatio + ') rotateZ(' + theta + 'deg)';

target.style.OTransformOrigin = 'center';
target.style.OTransform = 'scale(' + scaleRatio + ') rotateZ(' + theta + 'deg)';

x
x
y
y
θ
a(x+y)

Figure 2 The relationship between sita and scale

Then as the Figure 2 shown, how to calculate a proper value of scaleRatio (a) with theta (θ) as the variable, in order that vertexes of the item can stay on each side of another square? We may just have to solve the following equation:

After calculating, we can easily get that:

As we can see, the value of scale can be calculated with sita as the variable.

Figure 3 Multi-nested squares

Besides, we should also solve the case when a square is embedded with another square, while another square is embedded with another square (Figure 3). To work around this complicated problem of calculation, I have used relative motions instead. For example, I will create this three squares like this:

<div class="rect rect1">
    <div class="rect rect2">
        <div class="rect rect3"></div>
    </div>
</div>

Then rotate the second square and the third one with scaling at the same time:

.rect2 {
    transform: scale(0.8155975296668455) rotateZ(15deg);
    transform-origin: center;
}

.rect3 {
    transform: scale(0.8155975296668455) rotateZ(15deg);
    transform-origin: center;
}

Then, that's done! Easy right? The following code is implemented for this square item:

/** the difference of degree in each duration */
const stepT = 0.5;

/** the time (ms) to update the sita value */
const dur = 10;

/** the number of multi-nested squares */
const num = 6;

/** the current value of theta */
var theta = 0;

/** direction to change */
var dir = 1;

/** append multi-nested squares */
for (var i = 0; i < num; i++) {
   var div = document.createElement('div');
   div.className = 'rect rect' + (i + 1);
   document.querySelector('.rect' + i).appendChild(div);
}

/** start to animate */
setInterval(function () {
   theta += stepT * dir;

   if (Math.abs(theta) > 90) {
      dir *= -1;
      theta = (Math.abs(theta) - 90) * dir;
   }

    /** calculate the scaleRatio */
   const tan = parseFloat(Math.tan(Math.abs(theta) * Math.PI / 180).toFixed(2));
   const scaleRatio = Math.sqrt(1 + tan * tan) / (1 + tan);

   for (var i = 0; i < num; i++) {
      document.querySelector('.rect' + (i + 1)).style.transform = 'scale(' + scaleRatio + ') rotate(' + theta + 'deg)';
   }
}, dur);

1.2 How to create regular polygons?

From the preceding section, we can see that we have just implemented squares, but I wonder how to use them in other regular polygons like equilateral triangles or even pentagons. Before discussing the problem, we should create such regular polygons at first.

Certainly, we can easily generate squares by setting the width and the height with the same value, but what if generating other regular polygons? SVG is my choice.

(x0, y0)
(x1, y1)
(x2, y2)
x
y

Figure 4 A triangle

In SVG, we can easily create a polygon by using polygon tags, with setting the attribute points to set up each vertex. For instance, the triangle in the Figure 4 can be created like this:

<svg>
    <polygon stroke="#000" fill="none" points="x0,y0 x1,y1 x2,y2"></polygon>
</svg>

Utilizing this feature, what we should do to generate regular polygons is just to solve the positions of each vertex.

r

Figure 5 An equilateral triangles within a specific circle

Besides, can such polygons be generated within a circle with a specific radius (Figure 5)? The answer is yes.

r
θ
x
y
(x0, y0)

Figure 6 The specific coordinates

Before calculating, to make it more clear, I have drawn Figure 6 which has shown coordinates for us, and in which the relationship between θ and (x,y) is what we should solve later.

Then, here is the equation:

After that, just create regular polygons with θ:

<svg class="svg__container"></svg>
function createPolygon(obj, sides, startDeg) {
    /** the degree of the start position */
   startDeg = startDeg || 0;

   /** generate group to embed other group */
   var group = document.createElementNS('http://www.w3.org/2000/svg', 'g');

   /** the polygon you want to create */
   var polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');

    /** an array for storing points of this regular polygon */
   var pos = [];

   /** the radius of the given circle */
   const radius = document.querySelector('.svg__container').clientWidth / 2;

   for (var i = 0; i < sides; i++) {
      const deg = 360 / sides * i + startDeg;
      const x = radius * (1 - Math.sin(deg * Math.PI / 180));
      const y = radius * (1 - Math.cos(deg * Math.PI / 180));

      pos.push(x + ',' + y);
   }

   polygon.setAttribute('points', pos.join(' '));
   group.appendChild(polygon);
   obj.appendChild(group);

   /** set up the origin position of your transformation */
   group.style.transformOrigin = radius + 'px ' + radius + 'px';

   /** returns the group so that we can create multi nested elements recursively */
   return group;
}

/** equilateral triangles */
const sides = 3;

/** to create 6 nested triangles */
const num = 6;

var parent = document.querySelectorAll('.svg__container')[0];

for (var i = 0; i < num; i++) {
   parent = createPolygon(parent, sides, 45);
   parent.setAttribute('class', 'polygon polygon' + (i + 1));
}

1.3 How to make it reusable?

Now we can generate any side of regular polygons (the number of sides should be more than 3), it's time to solve the problem mentioned above? To make it reusable.

x
y
θ
a(x+y)

Figure 7 The case of equilateral triangles

Firstly, we should solve the case in which the number of sides is 3 (Figure 7). Based on the Law of Sine, it's easily to get the following equation:

Then solve it:

What if the number of sides is 4, 5, 6, etc.? You may find that all of them should obey the following rule, where n stands for the number of sides:

Then, just complete it with JavaScript:

const sides = 5;
var parent = document.querySelectorAll('.svg__container')[1];

const stepT = 0.8;
const dur = 10;
const num = 7;

var theta = 0;
var dir = 1;

/** append multi-nested polygons */
for (var i = 0; i < num; i++) {
   parent = createPolygon(parent, sides);
   parent.setAttribute('class', 'polygon polygon' + (i + 1));
}

/** start to animate */
setInterval(function () {
   theta += stepT * dir;

   const innerCornerDeg = Math.round(360 / sides);

   if (Math.abs(theta) > innerCornerDeg) {
      dir *= -1;
      theta = (Math.abs(theta) - innerCornerDeg) * dir;
   }

   const thetaValue = Math.abs(theta) * Math.PI / 180;
   const scaleRatio = Math.sin((180 - innerCornerDeg) * Math.PI / 180) / (Math.sin(thetaValue) + Math.sin(innerCornerDeg * Math.PI / 180 - thetaValue));

   for (var i = 1; i < num; i++) {
      document.querySelector('.polygon' + (i + 1)).style.transform = 'scale(' + scaleRatio + ') rotate(' + theta + 'deg)';
   }
}, dur);

If you want to use it in React as a component, you can check the POC here.

2. Demo

See the Pen mOzbWW by aleen42 (@aleen42) on CodePen.

Empty Comments
Sign in GitHub

As the plugin is integrated with a code management system like GitLab or GitHub, you may have to auth with your account before leaving comments around this article.

Notice: This plugin has used Cookie to store your token with an expiration.