Home  >  Article  >  Web Front-end  >  SVG and Vanilla JS framework to create a "star-shaped heart-shaped" code sharing

SVG and Vanilla JS framework to create a "star-shaped heart-shaped" code sharing

小云云
小云云Original
2018-05-21 11:47:301899browse

In this article, we mainly share with you the code to create a "star-shaped to heart-shaped" animation effect using SVG and Vanilla JS framework. We hope it can help everyone.

Ideas

They are all composed of five cubic Bezier curves. The interactive demo below shows each curve and the points it connects to. Click on any curve or connection point to see how the curves of the two graphs correspond.

It can be seen that all curves are created by cubic Bezier curves. Even though some of the curves have two control points that overlap.

The shapes that make up the star and heart are minimalist and unrealistic. But they can do it.

Initial code

As you can see from the expression animation example, I usually choose to use Pug (translation: Jade, a template engine) to generate such shapes. But here, the transition effect will also be handled by JavaScript due to the generated path data. Including calculating coordinates and placing those coordinates into properties d. So using JavaScript to do all this is the best option.

This means we don’t have to write a lot of tags:

<svg>    <path id=&#39;shape&#39;/></svg>

In JavaScript, we first get the element svg and the element path. path It’s the shape where a star changes to a heart and then back to a star. Then, we set the viewBox attribute to the element svg so that the dimensions of the SVG along the two axes are equal, and the origin of the coordinate axis (0,0) is in the SVG The middle. This means that when the size value of viewBox is D, its upper left corner coordinate is (-.5*D,-.5*D). Last but not least, create an object to store the initial and final states of the transition, and a method to set the values ​​we want to the SVG graphic properties.

const _SVG = document.querySelector(&#39;svg&#39;), 
    _SHAPE = document.getElementById(&#39;shape&#39;), 
    D = 1000, 
    O = { ini: {}, fin: {}, afn: {} };

(function init() {
    _SVG.setAttribute(&#39;viewBox&#39;, [-.5*D, -.5*D, D, D].join(&#39; &#39;));
})();

Now that we have that out of the way, we can start the more fun part!

Geometric drawing of graphics

We use the initial coordinates of the end point and control point to draw the star, and use their final coordinates to draw the heart. The transition range of each coordinate is the difference between its initial and final values. In this example, we rotate (rotate) the star when it transforms into a heart, because we want the corners of the star to point up. We'll also change the fill (fill) from a gold star to a crimson heart.

So, how can we obtain the coordinates of the end points and control points of these two graphics?

Star

In the star example, we start with a regular five-pointed star. The end point of our curve (translation: the curve that forms each corner of the star) falls at the intersection of the sides of the regular pentagram, and we use the vertex of the regular pentagram as the control point.

The end points and control points of the five cubic Bezier curves are marked with yellow dots at the intersections of the vertices and sides of the regular pentagram (Live) .

The vertex of the pentagram can be obtained by directly giving the radius (or diameter) of the circumscribed circle of the regular pentagram. That is the size we set for the SVG's viewBox (for simplicity, we do not consider high padding in this case). But how to get their intersection?

First, let’s look at the illustration below. Note the highlighted small pentagon in the middle of the regular pentagram in the picture. The vertices of the small pentagon coincide with the intersection points of the sides of the regular pentagon. This small pentagon is obviously a regular pentagon (translation: five sides of equal length). The inscribed circle and inner diameter of this small regular pentagon are the same as those of the regular pentagram.

The inscribed circles of the regular pentagon and the internal regular pentagon are the same (Live).

Therefore, if we calculate the inner diameter of the regular pentagon, we also obtain the inner diameter of the regular pentagon. This inner diameter and the central angle together correspond to the sides of the regular pentagon. Based on this, we can obtain the radius of the circumcircle of the regular pentagon. In this way, the coordinates of the vertices of the regular pentagon can be deduced. These points are exactly the intersection points of the sides of the regular pentagram, which are the end points of the five cubic Bezier curves of the star.

Our regular five-pointed star can be represented by the topological symbol {5/2}. In other words, the regular pentagram has 5 vertices. These five vertices are evenly distributed on its circumscribed circle, and the interval is 360°/5 = 72°. We start from the first point, skip the next point, and connect to the second point (this is 2## in the symbol {5/2} The meaning of #; 1 means connecting to the first point without skipping any points to form a pentagon). If you keep connecting like this, you can draw a regular five-pointed star shape.

In the demo below, click on the pentagon or pentagram button to see how they are drawn.

In this way, we get that the central angle corresponding to the side of the regular pentagon is twice the central angle corresponding to the side of the regular pentagon. Then the regular pentagon is 1 * (360°/5) = 1 * 72° = 72° (or 1 * (2 * π / 5)radians), then the regular pentagon is The pentagram is 2 * (360° / 5) = 2 * 72° = 144° (2 * (2 * π / 5)radians). Usually, a regular polygon represented by topological symbols as {p,q}, the central angle corresponding to one of its sides is q * (360° / p) (q * (2 * π / p) radians).

The central angle corresponding to one side of a regular polygon: regular pentagram (left, 144°) vs regular pentagon (right, ``72°`) (Live).

The radius of the circumcircle of the regular pentagram is known, which is the viewBox size. Then, we know the length of the hypotenuse of the right triangle (that is, the radius of the circumcircle of the regular pentagram) and the degree of the acute angle (half the angle corresponding to one side of the regular pentagram), which means that we can calculate the regular pentagram The inner diameter of (this inner diameter is equal to the inner diameter of the small regular pentagon inside the regular pentagram).

Through right angles, the inner diameter of the regular pentagram can be calculated. The hypotenuse of this right angle is equal to the radius of the circumcircle of the regular pentagram, and the angle of one of the acute angles is equal to half the angle corresponding to one side of the regular pentagram (Live).

The cosine of half the central angle is equal to the ratio of the inner diameter of the pentagram to the radius of the circumscribed circle. It can be concluded that the inner diameter of the five-pointed star is equal to the radius of the circumscribed circle multiplied by this cosine value.

Now that we have the radius of the inscribed circle of the small regular pentagon inside the regular pentagram, we can calculate the radius of the circumcircle of this regular pentagon. Still calculated through a small right angle. The hypotenuse of this right angle is equal to the radius of the circumcircle of the regular pentagon. An acute angle is equal to half the central angle corresponding to one side of a regular pentagon. One side of this acute angle is the central straight line of the central angle, and this central straight line is the radius of the circumcircle of the regular pentagon.

The illustration below highlights a right triangle, which is composed of the radius of a circumscribed circle, the radius of an inscribed circle, and half of a central angle of a regular pentagon. If we know the radius of the inscribed circle and the central angle corresponding to one side of the regular pentagon, half of the central angle is the angle between the radii of the two circumscribed circles. Using this right triangle we can calculate the length of the radius of the circumscribed circle.

Calculate the radius of the circumcircle of a regular pentagon through a right triangle (Live).

As mentioned earlier, the central angle of a regular pentagon is not equal to the central angle of a regular pentagram. The former is half of the latter (360° / 5 = 72°).

Okay, now that we have this radius, we can get the coordinates of all the desired points. The points are evenly distributed on both circles. There are 5 points on the outer circle (the circumscribed circle of a regular pentagon), and 5 points on the inner circle (the circumscribed circle of a small regular pentagon). ). There are a total of 10 points, and the angle between the radius rays where they are located is 360° / 10 = 36°.

The end points are evenly distributed on the circumscribed circle of the small regular pentagon, and the control points are evenly distributed on the circumscribed circle of the regular pentagram (Live).

The radii of the two circles are known. The radius of the outer circle is equal to the radius of the circumcircle of the regular pentagram, which is part of the viewBox size we set somewhat randomly (.5 or .25 or .32 or whichever size we think works better). The radius of the inner circle is equal to the radius of the circumcircle of the small regular pentagon formed inside the regular pentagram. The method to calculate this radius is: first, calculate the radius of the inscribed circle of the regular pentagram through the radius of the circumcircle of the regular pentagram and the central angle corresponding to one of its sides. The radius of this inscribed circle is equal to the radius of the inscribed circle of the small regular pentagon; then, it is calculated by the central angle corresponding to one side of the small regular pentagon and the radius of its inscribed circle.

So, based on this, we can generate data for drawing the path of the star. We already have the data we need to plot it.

So let’s draw it! And write the above thinking process into code.

First, create a function getStarPoints(f). Parameter (f) will determine the radius of the circumscribed circle of the regular pentagram obtained based on the size of viewBox. This function returns an array of coordinates, and we will then add array items to this array.

In this function, we first calculate the constants: the radius of the circumcircle of the regular pentagram (the radius of the outer circle), the central angle corresponding to one side of the regular pentagram, and the internal angle of the regular pentagram. The central angle corresponding to one side of the regular pentagon, the central angle corresponding to one side of the regular pentagon formed inside the regular pentagon, and the inscribed circle shared by the regular pentagon and the regular pentagon formed inside it. The radius of the regular five deformation (the vertex of the regular five-pointed star is the intersection of the sides of the regular five-pointed star), the radius of the circumscribed circle of the internal small regular five-pointed deformation, the total number of points whose coordinates need to be calculated, and the angle between the radial lines where all points are located.

Then, use a loop to calculate the coordinates of the points we want and insert them into the coordinate array.

const P = 5; // 三次曲线、多边形顶点数function getStarPoints(f = .5) {    const RCO = f*D,              // outer (pentagram) circumradius 
        BAS = 2*(2*Math.PI/P),    // base angle for star poly
        BAC = 2*Math.PI/P,        // base angle for convex poly 
        RI = RCO*Math.cos(.5*BAS),// pentagram/ inner pentagon inradius
        RCI = RI/Math.cos(.5*BAC),// inner pentagon circumradius 
        ND = 2*P,                 // total number of distinct points we need to get 
        BAD = 2*Math.PI/ND,       // base angle for point distribution        
        PTS = [];                 // array we fill with point coordinates    for(let i = 0; i < ND; i++) {

    }    return PTS;
}

计算坐标需要的条件:用点所在圆的半径,以及一条半径与水平轴线构成的夹角。如下面的交互式演示所示(拖动点来查看它的笛卡尔坐标如何变化):

在我们的例子里,当前的半径有两个。一个是外圆的半径(正五角星形的外接圆半径RCO),可以帮助算出索引值为偶数的点的的坐标(0, 2, ...)。还有一个是内接圆的半径(内部小正五边形的外接圆半径RCI),可以帮助算出索引值为奇数的点的的坐标(1, 3, ...)。当前点与圆心点的连线所构成的径向线的夹角等于点的索引值(i)乘以所有点所在的径向线的夹角(BAD,在我们的例子里恰巧是36°π / 10)。

因此,循环体里的代码如下:

for(let i = 0; i < ND; i++) {
    let cr = i%2 ? RCI : RCO, 
        ca = i*BAD, 
        x = Math.round(cr*Math.cos(ca)), 
        y = Math.round(cr*Math.sin(ca));
}

由于我们给viewBox 设定的尺寸足够大,所以我们可以放心的给坐标值做四舍五入计算,舍弃小数部分,这样我们的代码看起来会更干净。

我们会把外层圆(索引值是偶数的情况)计算出的坐标值推入坐标数组中两次。因为实际上星形在这个点上有两个重叠的控制点。如果要绘制成心形,就要把这两个重叠的控制点放在别的的位置上。

for(let i = 0; i < ND; i++) {    // same as before

    PTS.push([x, y]);    if(!(i%2)) PTS.push([x, y]);
}

接下来,我们给对象O添加数据。添加一个属性(d)来储存有关路径的数据。设置一个初始值来储存数组,这个数组是由上文提到的函数计算出的点的坐标组成的。我们还创建了一个函数用来生成实际的属性值(这个例子中,曲线的两个终点坐标的差值范围是路径的数据串,浏览器根据这个数据串绘制图形)。最后,我们获得了所有已经保存了数据的属性,并将这些属性的值作为前面提到的函数的返回值:

(function init() {    // same as before

    O.d = {
        ini: getStarPoints(), 
        afn: function(pts) {            return pts.reduce((a, c, i) => {                return a + (i%3 ? &#39; &#39; : &#39;C&#39;) + c
            }, `M${pts[pts.length - 1]}`)
        }
    };    for(let p in O) _SHAPE.setAttribute(p, O[p].afn(O[p].ini))
})();

绘制的结果可以在下边的演示中看到:

这是一个很有前途的星形。但我们想让生成的五角星形第一个尖朝下并且由它生成的星形的第一个尖朝上。目前,他们的指向都偏右了。这是因为我们是从 开始的(对应时钟的三点位置)。所以为了能从时钟6点的位置开始,我们给getStarPoints() 函数中的每个角加 90°π / 2 弧度)。

ca = i*BAD + .5*Math.PI

这样生成的五角星形和由它生成的星形的第一个角就都朝下了。为了旋转星形,我们需要给它的 transform 属性设置成旋转半个圆的角度。为了到达这个效果,我们首先设置初始的旋转角度为-180 。然后,我们把生成实际属性值的函数设置成这样一个函数。这个函数接收两个参数,一个是函数名字,另一个为参数,函数返回由这两个参数组成的字符串:

function fnStr(fname, farg) { 
    return `${fname}(${farg})` 
};

(function init() {    // same as before

    O.transform = { 
        ini: -180,  
        afn: (ang) => fnStr(&#39;rotate&#39;, ang) 
    };    // same as before})();

我们用类似的方式给我们的星形填充(fill)金色。我们给初始值设置一个 RGB 字符串,用同一个函数来给属性(fill)设置值:

(function init() {    // same as before 

    O.fill = { 
        ini: [255, 215, 0],  
        afn: (rgb) => fnStr(&#39;rgb&#39;, rgb) 
    };    // same as before})();

现在我们用 SVG 绘制好了一个漂亮的金色星形,它是由五个三次贝塞尔曲线构成的:

心形

我们已经绘制好星形了,现在来看下如何绘制心形吧!

我们先从两个半径相等并横向相交的圆开始,这两个圆都是 viewBox 尺寸的一部分(暂时定位.25)。这两个圆相交的方式为:它们中心点相连的线落在 x 轴上,它们相交点相连的线落在 y 轴上。这两条线要相等。

我们先从两个半径相等的相交的圆开始。这两个圆的圆心落在水平轴上,他们相交的点落在垂直轴上 (Live)。

接着,我们画两条直径,这两条直径穿过靠上的那个交点。在直径与圆的另一个交点处画一条正切线。这两条正切线在 y 轴相交。

画两条直径,穿过两个圆相交的点中靠上的那个,并在直径与圆的另一个交点处画正切线,两条正切线在垂直轴相交 (Live)。

两个圆上边的交点和两个直径与圆的另两个交点构成了我们需要的5个点中的3个。另外两个终点则是把外侧的半圆切割成两个相等弧线的中点,这使我们得到4个四分之一圆弧。

高亮显示了构成心形的三次贝塞尔曲线的终点以及靠下的那条曲线的控制点(Live)。

靠下的曲线控制点很明显已经得到了,就是两条切线的交点。但是另外四条曲线的控制点呢?我们怎么能把圆弧变成三次贝塞尔曲线呢?

We cannot get the cubic Bezier curve of a quarter arc, but we can get an approximation, as explained in this article.

This article tells us that we can use a radius with a value of R and the tangent of the radius (N and Q) to draw Quarter arc. The tangents of the two radii intersect at point P. The four angles of the quadrilateral ONPQ are equal to 90° (or π / 2, three of which are derived from the axioms (O is 90°, the angle between the two tangent lines and the radius is also 90°), the last one is calculated (the sum of the interior angles is 360°, The other three corners are 90°, and the last corner is 90°). In this way, ONPQ is a rectangle at the same time. # There are two adjacent sides that are equal (the lengths of OQ and ON are both equal to the radius R), so it is a side with length ## The squares of #R are also equal to the lengths of NP and QP using cubic Bezier curves. Draw an arc that approximates a quarter arc (Live) The control point of the arc that approximates a quarter arc that we draw using a cubic Bezier curve is on the tangent line#. On ##NP and QP

, that is, the length of

C * R from the end point, the value of C

calculated in the previously mentioned article is

.551915. Knowing the above, we can start to calculate the coordinates of the cubic Bezier curve end point and control points. With these coordinates, we can construct our heart shape. . Since we choose this way to construct the heart shape, TO0SO1 is a square with four equal sides (all four sides are composed of the radii of two circles), and its opposite corners The lines are also equal (as mentioned before, the line connecting the two center points is equal to the line connecting the two intersection points). Here, O is the intersection of the two diagonals, and OT

is equal to half of the diagonal

ST

and

T are on the y axis, so their The x coordinates are 0. Their y coordinates correspond to the absolute value of OT, which is half of the diagonal (OS Same reason). SquareTO0SO1 (Live). We can make a square with any side length l. Cut into two isosceles triangles. The right-angled side of this isosceles triangle coincides with the side of the square, and the hypotenuse coincides with the diagonal of the square. Any square can be cut into two isosceles triangles. (Live).

Using the Pythagorean Theorem: d² = l² + l², we can calculate the hypotenuse of one of the right angles (that is, the diagonal of the square). In this way, the length of the diagonal of the square can be obtained from the length of the side d = √(2 * l) = l * √2

(On the contrary, the length of the side can be obtained from the length of the diagonal

l = d / √2). You can also calculate half of the diagonal d / 2 = (l * √2) / 2 = l / √2

.

Applying this to our TO0SO1

square with side length

R, we get the T point (whose absolute value is equal to the square The y coordinate of half of the diagonal) is -R / √2, and the y

coordinate of the

S point is R/√2. TO0SO1 The coordinates of the four vertices of the square (Live). Similarly, the O1 points are on the x axis, so their y

axis coordinates are

0, and their x The axis coordinate is half of the diagonal OO1

:

±R/√2. TO0SO1 is a square, then its four corners are 90° (π / 2 arc). Quadrilateral TA1B1S (Live)

.

As shown in the picture above, the straight line TB1 is a diagonal line, which means that the arc TB1 is half of the circle, or 180°

arc. We divide this arc into two equal halves using the

A1 point, resulting in two equal 90° arcs: TA1

and

A1B1 . They correspond to two equal 90° angles: ∠TO1A1 and ∠A1O1B1. According to the axiom ∠TO1S and ∠TO1A1 are both angles of 90°, which proves that the straight line SA1 is also a diameter . This tells us that in quadrilateral TA1B1S, diagonals TB1

and

SA1 are perpendicular and equal, and intersect at their respective center points (TO1 , O1B1, SO1 and O1A1 are all equal to the radius of the circle R). This means that quadrilateral TA1B1S is a square and its diagonal is equal to 2 * R.

Here we can get the side of the quadrilateral TA1B1S equal to 2 * R / √2 = R * √2. Since all the angles of the square are 90° and side TS overlaps the vertical axis, sides TA1 and SB1 are horizontal , and parallel to the x axis. According to their lengths, the x axis coordinates of the two points A1 and B1 can be calculated: ±R * √2.

Because TA1 and SB1 are horizontal, so A1 and B1 are two points of y The axis coordinates are equal to the T (-R / √2) and S (R / √2) points respectively.

Square TA1B1S Four vertex coordinates (Live).

Another conclusion we get from this is that since TA1B1S is a square, A1B1 is parallel to TS because TS is on the y (vertical) axis, so A1B1 is also vertical. Furthermore, since the x axis is parallel to TA1 and SB1, and TS is cut in half, x The shaft also cuts A1B1 in half.

Now let me look at the control points.

We start with the overlapping control points of the lowermost arc.

Quadrilateral TB0CB1 (Live).

All angles of quadrilateralTB0CB1 are equal to 90° (because TO0SO1 is a square so ∠T is a right angle; Because B1C is the tangent of the circle, it is perpendicular to the radius O1B1, and intersects at the B1 point, so ∠B1 is a right angle; because The other three are all right angles, so ∠C is also a right angle), so it is a rectangle. Likewise it has two adjacent sides that are equal: TB0 and TB1. Both lines are diameters of circles, and both are equal to 2 * R. Finally, it is concluded that quadrilateral TB0CB1 is a square with side length 2 * R.

Then we can get its diagonal TC: 2 * R * √2. Because C is on the y axis, its x axis coordinate is 0. Its y axis coordinate is the length of OC. The length of OC is equal to TC minus OT: 2 * R * √2 - R / √2 = 4 * R / √2 - R / √2 = 3 * R / √2.

Square TB0CB1 Coordinates of the four vertices (Live).

Now we have the coordinates of the two overlapping control points of the lowermost arc as (0,3 * R / √2).

In order to obtain the coordinates of other curve control points, we draw tangent lines at their end points and obtain the intersection points of these tangent lines D1 and E1.

Quads TO1A1D1 and A1O1B1E1 (Live).

In the quadrilateralTO1A1D1, it is known that all angles are right angles (90°), three of which are derived from the axioms (∠D1TO1 and ∠D1A1O1 are obtained from the radius and tangent; ∠TO1A1 is the angle corresponding to the quarter arc TA1), then the fourth angle Through calculation, it is concluded that it is also a right angle. This proves that TO1A1D1 is a rectangle. And because it has two adjacent sides that are equal (O1T and O1A1 are equal to the radius R), TO1A1D1 is a square.

This shows that the diagonals TA1 and O1D1 are equal to R * √2. It is known that TA1 is horizontal, and the two diagonals of the square are vertical, it proves that O1D1 is vertical. Then the x axis coordinates of points O1 and D1 are equal, and the x axis coordinates of O1 are ±R /√2. Since we know the length of O1D1, we can calculate the y axis coordinates: as mentioned earlier using the length of the diagonal ( R * √2 ) do subtraction.

The situation with quadrilateral A1O1B1E1 is similar. It is known that all angles are right angles (90°), three of which are derived from axioms (∠E1A1O1 and ∠E1B1O1 are obtained from radii and tangents ; ∠A1O1B1 is the angle corresponding to a quarter arc A1B1), then the fourth angle is calculated to be a right angle. This proves that A1O1B1E1 is a rectangle. And because it has two adjacent sides that are equal (O1A1 and O1B1 are equal to the radius R), A1O1B1E1 is a square.

至此,我们得到对角线 A1B1O1E1 的长为R * √2。我们知道 A1B1 是垂直的,并且被水平轴切割成相等的两半儿,也就是 O1E1 在水平轴上,点 E1y 轴坐标为0。因为点 O1x 轴坐标为±R / √2,并且 O1E1 等于R * √2,我们就可以计算出点 E1x 轴坐标为:±3 * R / √2

四边形 TO1A1D1A1O1B1E1 的顶点坐标(Live)

但是这些切线的交叉点并不是控制点,所以我们需要用近似圆弧形的方法来计算。我们想要的控制点在 TD1A1D1A1E1B1E1 上,距离弧线终点(TA1B1)大约55%(这个值来源于前文提到的那篇文章中算出的常量C的值)的位置。也就是说从终点到控制点的距离是C * R

在这种情况下,我们的控制点坐标为:终点(TA1B1)坐标的1 - C,加上,切线交点(D1E1)坐标的 C

让我们把这些写入JavaScript代码吧!

跟星形的例子一样,我们先从函数getStarPoints(f) 开始。根据这个函数的参数 (f) ,我们可以从viewBox 的尺寸中获得辅助圆的半径。这个函数同样会返回一个坐标构成的数组,以便我们后边插入数组项。

在函数中,我们先声明常量。

  • 辅助圆的半径。

  • 边与这个辅助圆半径相等的小正方形对角线的一半。对角线的一半也是这些正方形外接圆半径。

  • 三次贝塞尔曲线终点的坐标值(点TA1B1),沿水平轴的绝对值。

然后我们把注意力放在切线交点的坐标上( 点 CD1E1 )。这些点或者与控制点(C)重合,或者可以帮助我们获得控制点(例如点 D1E1)。

function getHeartPoints(f = .25) {    const R = f*D,                      // helper circle radius 
        RC = Math.round(R/Math.SQRT2),  // circumradius of square of edge R 
        XT = 0, YT = -RC,               // coords of point T 
        XA = 2*RC, YA = -RC,            // coords of A points (x in abs value) 
        XB = 2*RC, YB = RC,             // coords of B points (x in abs value)
        XC = 0, YC = 3*RC,              // coords of point C 
        XD = RC, YD = -2*RC,            // coords of D points (x in abs value) 
        XE = 3*RC, YE = 0;              // coords of E points (x in abs value)}

点击下边交互演示上的点,可以展示这些点的坐标:

现在我们可以通过终点和切线交点来获得控制点:

function getHeartPoints(f = .25) {    // same as before    // const for cubic curve approx of quarter circle    const C = .551915, 
        CC = 1 - C, 
        // coords of ctrl points on TD segs
        XTD = Math.round(CC*XT + C*XD), YTD = Math.round(CC*YT + C*YD), 

        // coords of ctrl points on AD segs
        XAD = Math.round(CC*XA + C*XD), YAD = Math.round(CC*YA + C*YD), 

        // coords of ctrl points on AE segs 
        XAE = Math.round(CC*XA + C*XE), YAE = Math.round(CC*YA + C*YE), 

        // coords of ctrl points on BE segs
        XBE = Math.round(CC*XB + C*XE), YBE = Math.round(CC*YB + C*YE);    // same as before}

下一步,我们要把相关的坐标合成一个数组,并将这个数组返回。在星形的例子中,我们是从最下边的弧形开始的,然后按照顺时针方向绘制,所以在这里我们用同样的方法。每个曲线,我们为控制点放入两组坐标,为终点放入一组坐标。

请注意,第一个曲线(最下边的那个),他的两个控制点重叠了,所以我们把相同的坐标组合推入两次。代码看起来也许并不像绘制星形时那样整洁好看,但可以满足我们的需求:

return [
    [XC, YC], [XC, YC], [-XB, YB], 
    [-XBE, YBE], [-XAE, YAE], [-XA, YA], 
    [-XAD, YAD], [-XTD, YTD], [XT, YT], 
    [XTD, YTD], [XAD, YAD], [XA, YA], 
    [XAE, YAE], [XBE, YBE], [XB, YB]
];

现在我们可以把星形的最终状态设置成函数getHeartPoints(),没有旋转,没有填充( fill)深红色。然后把当前状态设置成最终状态,以便能看到心形:

function fnStr(fname, farg) { 
    return `${fname}(${farg})` 
};

(function init() {  
    _SVG.setAttribute(&#39;viewBox&#39;, [-.5*D, -.5*D, D, D].join(&#39; &#39;));

    O.d = {
        ini: getStarPoints(), 
        fin: getHeartPoints(), 
        afn: function(pts) {        return pts.reduce((a, c, i) => {            return a + (i%3 ? &#39; &#39; : &#39;C&#39;) + c
        }, `M${pts[pts.length - 1]}`)
        }
    };

    O.transform = {
        ini: -180, 
        fin: 0, 
        afn: (ang) => fnStr(&#39;rotate&#39;, ang)
    };

    O.fill = {
        ini: [255, 215, 0], 
        fin: [220, 20, 60], 
        afn: (rgb) => fnStr(&#39;rgb&#39;, rgb)
    };    for(let p in O) _SHAPE.setAttribute(p, O[p].afn(O[p].fin))
})();

这个心形看上去很不错:

确保两个图形是对齐的

如果我们不给图形填充( fill)颜色、不旋转(transform)图形,只是看他们的骨架(stroke)叠在一起。就会发现它们并没有对齐:

解决这个问题最简单的方法就是利用辅助圆的半径把心形向上移动一些:

return [ /* same coords */ ].map(([x, y]) => [x, y - .09*R])

现在我们已经对齐了,忽略我们是如何调整这两个例子的f参数的。这个参数在星形中决定了五角星形外接圆半径与viewBox尺寸的对应关系(默认值是 .5),在心形中决定了辅助圆的半径与viewBox尺寸的对应关系(默认值是 .25)。

在两个图形中切换

当点击的时候,我们希望能从一种图形转换成另一种。为了做到这个,我们设置一个dir变量,当我们从星形变成心形时,它的值是1。当我们从心形转换成星形时,它的值是-1。初始值是-1,已达到刚刚从心形转换成星形的效果。

然后我们在元素_SHAPE上添加一个click事件监听,监听的函数内容为:改变变量dir的值、改变图形的属性。这样就可以获得从一个金色星形转换成深红色心形,再变回星形的效果:

let dir = -1;

(function init() {  
    // same as before

    _SHAPE.addEventListener(&#39;click&#39;, e => {
        dir *= -1;        for(let p in O)
        _SHAPE.setAttribute(p, O[p].afn(O[p][dir > 0 ? &#39;fin&#39; : &#39;ini&#39;]));
    }, false);
})();

现在我们可以通过点击图形在两种图形中转换了:

在两个图形中转变

我们最终想要的并不是两个图形间唐突的切换,而是柔和的渐变效果。所以我们用以前的文章说明的插值技术来实现。

首先我们要决定转变动画的总帧数(NF),然后选择一种我们想要的时间函数:从星形到心形的的路径(path)转变我们选择ease-in-out函数,旋转角度的转变我们选择 bounce-ini-fin 函数,填充(fill)颜色转变我们选择ease-out 函数。我们先只做这些,如果之后我们改变注意了想探索其它的选项,也可以添加。

/* same as before */const NF = 50, 
TFN = {    &#39;ease-out&#39;: function(k) {        return 1 - Math.pow(1 - k, 1.675)
    }, 
    &#39;ease-in-out&#39;: function(k) {        return .5*(Math.sin((k - .5)*Math.PI) + 1)
    },    &#39;bounce-ini-fin&#39;: function(k, s = -.65*Math.PI, e = -s) {        return (Math.sin(k*(e - s) + s) - Math.sin(s))/(Math.sin(e) - Math.sin(s))
    }
};

然后我们为每种属性指定转换时使用的时间函数。

(function init() {  
    // same as before

    O.d = {        // same as before 
        tfn: &#39;ease-in-out&#39;
    };

    O.transform = {        // same as before
        tfn: &#39;bounce-ini-fin&#39;
    };

    O.fill = {        // same as before 
        tfn: &#39;ease-out&#39;
    };    // same as before})();

我们继续添加请求变量 IDrID)、当前帧变量 (cf) 、点击时第一个被调用并在每次显示刷新的时候都会被调用的函数update() 、当过渡结束时被调用的函数stopAni(),这个函数用来退出循环动画。在 update()函数里我们更新当前帧 cf,计算进程变量 k,判断过渡是否结束,是退出循环动画还是继续动画。

我们还会添加一个乘数变量 m ,用于防止我们从最终状态(心形)返归到最初状态(星形)时倒转时间函数。

let rID = null, cf = 0, m;function stopAni() {
    cancelAnimationFrame(rID);
    rID = null;  
};function update() {
    cf += dir;    let k = cf/NF;    if(!(cf%NF)) {
        stopAni();        return
    }

    rID = requestAnimationFrame(update)
};

然后我们需要改变点击时所做的事情:

addEventListener(&#39;click&#39;, e => {    if(rID) stopAni();
    dir *= -1;
    m = .5*(1 - dir);
    update();
}, false);

update()函数中,我们需要设置当过渡到中间值(取决于进程变量k)时的属性。如同前边的文章中所述,最好是在开始时计算出最终值和初始值之间的差值范围,甚至是在设置监听之前就设置好,所以我们的下一步是:创建一个计算数字间差值范围的函数。无论在这种情况下,还是在数组中,无论数组的嵌套有多深,都可以这个函数来设置我们想要转变的属性的范围值。

function range(ini, fin) {    return typeof ini == &#39;number&#39; ? 
        fin - ini : 
        ini.map((c, i) => range(ini[i], fin[i]))
};

(function init() {  
    // same as before    for(let p in O) {
        O[p].rng = range(O[p].ini, O[p].fin);
        _SHAPE.setAttribute(p, O[p].afn(O[p].ini));
    }    // same as before})();

现在只剩下 update() 函数中有关插值的部分了。使用一个循环,我们会遍历所有我们想要从一个状态顺滑转换到另一个状态的属性。在这个循环中,我们先得到插值函数的运算结果,然后将这些属性设置成这个值。插值函数的运算结果取决于初始值(s)、当前属性(inirng)的范围(s)、我们使用的定时函数(tfn) 和进度(k):

function update() {  
    // same as before    for(let p in O) {        let c = O[p];

        _SHAPE.setAttribute(p, c.afn(int(c.ini, c.rng, TFN[c.tfn], k)));
    }    // same as before};

最后一步是编写这个插值函数。这跟获得范围值的那个函数非常相似:

function int(ini, rng, tfn, k) {    return typeof ini == &#39;number&#39; ? 
        Math.round(ini + (m + dir*tfn(m + dir*k))*rng) : 
        ini.map((c, i) => int(ini[i], rng[i], tfn, k))
};

最后获得了一个形状,当点击它时可以从星形过渡转换成心形,第二次点击的时候会变回星形!

这几乎就是我们想要的了:但还有一个小问题。对于像角度值这样的循环值,我们并不想在第二次点击的时候将他调转。相反,我们希望他继续顺着同一个方向旋转。通过两次点击后,正好能旋转一周,回到起点。

我们通过给代码添加一个可选的属性,稍稍调整更新函数和插值函数:

function int(ini, rng, tfn, k, cnt) {    return typeof ini == &#39;number&#39; ? 
        Math.round(ini + cnt*(m + dir*tfn(m + dir*k))*rng) : 
        ini.map((c, i) => int(ini[i], rng[i], tfn, k, cnt))
};function update() {  
    // same as before    for(let p in O) {        let c = O[p];

        _SHAPE.setAttribute(p, c.afn(int(c.ini, c.rng, TFN[c.tfn], k, c.cnt ? dir : 1)));
    }    // same as before };

(function init() {  
    // same as before

    O.transform = {
        ini: -180, 
        fin: 0, 
        afn: (ang) => fnStr(&#39;rotate&#39;, ang),
        tfn: &#39;bounce-ini-fin&#39;,
        cnt: 1
    };    // same as before})();

现在我们得到了我们想要的最终结果:一个从金色星形变成深红色心形的形状,每次从一个状态到另一个状态顺时针旋转半圈。

相关推荐:

jquery心形点赞关注效果的简单实现

The above is the detailed content of SVG and Vanilla JS framework to create a "star-shaped heart-shaped" code sharing. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn