Kaleidoscope | 萬花筒

阅读 775
收藏 18
2016-12-30
原文链接:github.com

[原創] Kaleidoscope Back

kaleidoscope

Figure 1 A simple preview of kaleidoscopes

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

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.

How to create?

At the start, I have utilized the feature of div, which is an HTML elements with a shape of rectangles. With setting a 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 the Figure 1.

Then, to simulate such rotating animation, I would like to use transform and transform-origin property to implement it, rather than use Canvas, as Canvas is something complicated, which has been always 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)';

kaleidoscope

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:

kaleidoscope

After calculating, we can easily get that:

kaleidoscope

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

kaleidoscope

Figure 3 Multinested 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 mutinested squares */
const num = 6;

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

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

/** append mutinested 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 animiate */
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);

How to create regular polygons?

From the preceding section, we can see that we have just implemented for squares, but I wonder how to use it 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 a same value, but what if generating other regular polygons? SVG is my choice.

kaleidoscope

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 positions of each vertex.

kaleidoscope

Figure 5 An equilateral triangles within a specific circle

In addition, can such polygons be generated within a circle with a specific radius (Figure 5)? Apparently, the answer is yes.

kaleidoscope

Figure 6 The specific cordinates

Before calculating, to make it more clear, I have drawn the 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:

kaleidoscope

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';

    /** return the group so that we can create multinested 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));
}

How to make it reusable?

Now that 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.

kaleidoscope

Figure 7 The case of equilaterial 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:

kaleidoscope

Then solve it:

kaleidoscope

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

kaleidoscope

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 mutinested 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.

Demo

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


本文对你有帮助?欢迎扫码加入前端学习小组微信群:

评论