Maison >interface Web >js tutoriel >Framework SVG et Vanilla JS pour créer un partage de code 'en forme d'étoile'

Framework SVG et Vanilla JS pour créer un partage de code 'en forme d'étoile'

小云云
小云云original
2018-05-21 11:47:301977parcourir

Dans cet article, nous partageons principalement avec vous le code pour créer un effet d'animation "en forme d'étoile à en forme de cœur" à l'aide du framework SVG et Vanilla JS. Nous espérons que cela pourra aider tout le monde.

Idée

Elles sont toutes composées de cinq courbes de Bézier cubiques. La démo interactive ci-dessous montre chaque courbe et les points auxquels elle se connecte. Cliquez sur n’importe quelle courbe ou point de connexion pour voir comment les courbes des deux graphiques correspondent.

On peut voir que toutes les courbes sont créées par des courbes de Bézier cubiques. Même si certaines courbes ont deux points de contrôle qui se chevauchent.

Les formes qui composent l'étoile et le cœur sont minimalistes et irréalistes. Mais ils peuvent le faire.

Code initial

Comme vous pouvez le voir dans l'exemple d'animation d'expression, je choisis généralement d'utiliser Pug (traduction : Jade, un moteur de modèles) pour générer de telles formes. Mais ici, l'effet de transition sera également géré par JavaScript en raison des données de chemin générées. Y compris le calcul des coordonnées et la mise de ces coordonnées dans les attributs d. Utiliser JavaScript pour faire tout cela est donc la meilleure option.

Cela signifie que nous n'avons pas besoin d'écrire beaucoup de balises :

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

En JavaScript, nous récupérons d'abord l'élément svg et l'élément path . path C’est la forme d’étoile qui se transforme en forme de cœur puis redevient une forme d’étoile. Ensuite, nous définissons l'attribut svg sur l'élément viewBox pour que les dimensions du SVG le long des deux axes soient égales et que l'origine de l'axe de coordonnées (0,0) soit au milieu du SVG. Cela signifie que lorsque la valeur de dimension de viewBox est D, sa coordonnée du coin supérieur gauche est (-.5*D,-.5*D). Enfin et surtout, créez un objet pour stocker les états initial et final de la transition, ainsi qu'une méthode pour définir les valeurs souhaitées pour les propriétés graphiques SVG.

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;));
})();

Maintenant que cela est réglé, nous pouvons commencer la partie la plus amusante !

Dessin géométrique de graphiques

Nous utilisons les coordonnées initiales du point final et du point de contrôle pour dessiner l'étoile, et utilisons leurs coordonnées finales pour dessiner le cœur. La plage de transition de chaque coordonnée est la différence entre sa valeur initiale et sa valeur finale. Dans cet exemple, nous tournons (rotate) l'étoile lorsqu'elle se transforme en cœur, car nous voulons que les coins de l'étoile pointent vers le haut. Nous allons également changer le remplissage (fill) d'une étoile dorée à un cœur cramoisi.

Alors, comment obtenir les coordonnées des points finaux et des points de contrôle de ces deux figures ?

Étoile

Dans l'exemple de l'étoile, on commence par un pentagramme régulier. Le point final de notre courbe (traduction : la courbe qui forme chaque coin de l’étoile) se situe à l’intersection des côtés du pentagramme régulier, et nous utilisons le sommet du pentagramme régulier comme point de contrôle.

Les points finaux et les points de contrôle des cinq courbes cubiques de Bézier sont marqués de points jaunes aux intersections des sommets et des côtés du pentagramme régulier (Live) .

Le sommet du pentagramme peut être obtenu en donnant directement le rayon (ou diamètre) du cercle circonscrit du pentagramme régulier. C'est la taille que nous avons définie pour les SVG viewBox (pour des raisons de simplicité, nous ne prenons pas en compte un remplissage élevé dans ce cas). Mais comment obtenir leur intersection ?

Tout d’abord, regardons l’illustration ci-dessous. Notez le petit pentagone mis en évidence au milieu du pentagramme régulier de l'image. Les sommets du petit pentagone coïncident avec les points d'intersection des côtés du pentagone régulier. Ce petit pentagone est évidemment un pentagone régulier (traduction : cinq côtés d'égale longueur). Le cercle inscrit et le diamètre intérieur de ce petit pentagone régulier sont les mêmes que ceux du pentagramme régulier.

Les cercles inscrits du pentagone régulier et du pentagone régulier interne sont les mêmes (Live).

Par conséquent, si nous calculons le diamètre intérieur du pentagone régulier, nous obtenons également le diamètre intérieur du pentagone régulier. Ce diamètre intérieur et l'angle central correspondent ensemble aux côtés du pentagone régulier. Sur cette base, nous pouvons obtenir le rayon du cercle circonscrit au pentagone régulier. De cette manière, les coordonnées des sommets du pentagone régulier peuvent être déduites. Ces points sont exactement les points d'intersection des côtés du pentagramme régulier, qui sont les points extrêmes des cinq courbes cubiques de Bézier de l'étoile.

Notre pentagramme régulier peut être représenté par le symbole topologique {5/2}. En d’autres termes, le pentagramme régulier a 5 sommets. Ces 5 sommets sont répartis uniformément sur son cercle circonscrit, et les intervalles sont 360°/5 = 72°. Nous commençons par le premier point, sautons le point suivant et nous connectons au deuxième point (c'est la signification de {5/2} dans le symbole 2 ; 1 signifie connexion Aller au premier point sans sauter aucun point pour former un pentagone). Si vous continuez à vous connecter ainsi, vous pouvez dessiner une forme régulière d’étoile à cinq branches.

Dans la démo ci-dessous, cliquez sur le bouton pentagone ou pentagramme pour voir comment ils sont dessinés.

De cette façon, on obtient que l'angle au centre correspondant au côté du pentagone régulier est le double de l'angle au centre correspondant au côté du pentagone régulier. Alors le pentagone régulier est 1 * (360°/5) = 1 * 72° = 72° (ou 1 * (2 * π / 5) radians), et le pentagone régulier est 2 * (360° / 5) = 2 * 72° = 144° (2 * (2 * π / 5) radians). Habituellement, pour un polygone régulier représenté par {p,q} en notation topologique, l'angle au centre correspondant à l'un de ses côtés est q * (360° / p) (q * (2 * π / p) radians).

L'angle au centre correspondant à un côté d'un polygone régulier : pentagramme régulier (gauche, 144°) vs pentagone régulier (droite, ``72°`) (Live).

Le rayon du cercle circonscrit du pentagramme régulier est connu, qui est la viewBox taille. On connaît alors la longueur de l'hypoténuse du triangle rectangle (c'est-à-dire le rayon du cercle circonscrit du pentagramme régulier) et le degré de l'angle aigu (la moitié de l'angle correspondant à un côté du pentagramme régulier), ce qui signifie que l'on peut calculer le pentagramme régulier Le diamètre intérieur de (ce diamètre intérieur est égal au diamètre intérieur du petit pentagone régulier à l'intérieur du pentagramme régulier).

Grâce aux angles droits, le diamètre intérieur du pentagramme régulier peut être calculé. L'hypoténuse de cet angle droit est égale au rayon du cercle circonscrit du pentagramme régulier, et l'angle de l'un des angles aigus est égal à la moitié de l'angle correspondant à un côté du pentagramme régulier (Live).

Le cosinus de la moitié de l'angle central est égal au rapport du diamètre intérieur du pentagramme au rayon du cercle circonscrit. On peut conclure que le diamètre intérieur de l'étoile à cinq branches est égal au rayon du cercle circonscrit multiplié par cette valeur de cosinus.

Maintenant que nous avons le rayon du cercle inscrit du petit pentagone régulier à l'intérieur du pentagramme régulier, nous pouvons calculer le rayon du cercle circonscrit de ce pentagone régulier. Toujours calculé par un petit angle droit. L'hypoténuse de cet angle droit est égale au rayon du cercle circonscrit au pentagone régulier. Un angle aigu est égal à la moitié de l’angle au centre correspondant à un côté d’un pentagone régulier. Un côté de cet angle aigu est la droite centrale de l'angle central, et cette droite centrale est le rayon du cercle circonscrit du pentagone régulier.

L'illustration ci-dessous met en évidence un triangle rectangle, composé de la moitié du rayon d'un cercle circonscrit, du rayon d'un cercle inscrit et d'un angle central d'un pentagone régulier. Si l'on connaît le rayon du cercle inscrit et l'angle au centre correspondant à un côté du pentagone régulier, la moitié de l'angle au centre est l'angle entre les rayons des deux cercles circonscrits. En utilisant ce triangle rectangle, nous pouvons calculer la longueur du rayon du cercle circonscrit.

Calculez le rayon du cercle circonscrit d'un pentagone régulier passant par un triangle rectangle (Live).

Comme mentionné précédemment, l'angle central d'un pentagone régulier n'est pas égal à l'angle central d'un pentagramme régulier. Le premier est la moitié (360° / 5 = 72°) du second.

D'accord, maintenant que nous avons ce rayon, nous pouvons obtenir les coordonnées de tous les points que nous voulons. Les points sont répartis uniformément sur les deux cercles. Il y a des 5 points sur le cercle extérieur (le cercle circonscrit d'un pentagone régulier), et des 5 points sur le cercle intérieur (le cercle circonscrit d'un petit pentagone régulier). Il y a 10 points au total, et l'angle entre les rayons du rayon où ils se trouvent est 360° / 10 = 36°.

Les points terminaux sont répartis uniformément sur le cercle circonscrit du petit pentagone régulier, et les points de contrôle sont répartis uniformément sur le cercle circonscrit du pentagramme régulier (Live).

Les rayons des deux cercles sont connus. Le rayon du cercle extérieur est égal au rayon du cercle circonscrit du pentagramme régulier, qui fait partie de la viewBox taille que nous fixons de manière quelque peu arbitraire (.5 ou .25 ou .32 ou une taille que nous pensons fonctionnera mieux). Le rayon du cercle intérieur est égal au rayon du cercle circonscrit du petit pentagone régulier formé à l’intérieur du pentagramme régulier. La méthode pour calculer ce rayon est la suivante : tout d'abord, calculer le rayon du cercle inscrit du pentagramme régulier en passant par le rayon du cercle circonscrit du pentagramme régulier et l'angle au centre correspondant à l'un de ses côtés. Le rayon de ce cercle inscrit est égal au rayon du cercle inscrit du petit pentagone régulier ; alors, il est calculé par l'angle au centre correspondant à un côté du petit pentagone régulier et le rayon de son cercle inscrit.

Ainsi, sur cette base, nous pouvons générer des données pour tracer la trajectoire de l'étoile. Nous disposons déjà des données dont nous avons besoin pour le tracer.

Alors dessinons ! Et écrivez le processus de réflexion ci-dessus dans le code.

Tout d'abord, créez une fonction getStarPoints(f). Le paramètre (f) déterminera le rayon du cercle circonscrit du pentagramme régulier obtenu en fonction de la taille de viewBox. Cette fonction renvoie un tableau de coordonnées, et nous ajouterons ensuite des éléments du tableau à ce tableau.

Dans cette fonction, on calcule d'abord les constantes : le rayon du cercle circonscrit du pentagramme régulier (le rayon du cercle extérieur), l'angle au centre correspondant à un côté du pentagramme régulier, et l'angle interne du pentagramme régulier. L'angle central correspondant à un côté du pentagone régulier, l'angle central correspondant à un côté du pentagone régulier formé à l'intérieur du pentagone régulier, et le cercle inscrit partagé par le pentagone régulier et le pentagone régulier formé à l'intérieur de celui-ci. . Le rayon de la déformation régulière à cinq branches (le sommet de l'étoile régulière à cinq branches est l'intersection des côtés de l'étoile régulière à cinq branches), le rayon du cercle circonscrit de la petite déformation régulière interne à cinq branches, le nombre total de points dont les coordonnées doivent être calculées et l'angle entre les lignes radiales où se trouvent tous les points.

Ensuite, utilisez une boucle pour calculer les coordonnées des points souhaités et insérez-les dans le tableau de coordonnées.

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)。

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

On ne peut pas obtenir la courbe de Bézier cubique d'un quart d'arc, mais on peut en obtenir une approximation, comme expliqué dans cet article.

Cet article nous apprend que l'on peut utiliser un rayon d'une valeur de R, et les tangentes du rayon (N et Q) pour tracer un quart d'arc. Les tangentes des deux rayons se coupent au point P. Les quatre angles du quadrilatère ONPQ sont tous égaux à 90° (ou π / 2, dont trois sont dérivés des axiomes (O est 90°, et l'angle entre les deux tangentes et le rayon est également 90°), Le dernier est calculé (la somme des angles intérieurs est 360°, les trois autres angles sont 90°, et le dernier angle est 90° De cette façon, ONPQ est). un rectangle à la fois. Deux côtés adjacents sont égaux (les longueurs de ONPQ et OQ sont toutes deux égales au rayon ON), c'est donc un carré de longueur de côté R donc les longueurs de R et NP sont également égaux à QP Utilisez la courbe de Bézier cubique pour dessiner un arc qui se rapproche d'un quart d'arc (Live). R

Nous utilisons la courbe de Bézier cubique. du quart d'arc approximatif tracé par la courbe sont sur les tangentes et , c'est-à-dire la longueur de

à partir du point final,

est calculée dans l'article mentionné précédemment La valeur est NP. QPC * R Connaissant ce qui précède, nous pouvons commencer à calculer les coordonnées du point final et du point de contrôle de la courbe de Bézier cubique. Avec ces coordonnées, nous pouvons construire notre forme de cœur 🎜>C Puisque nous avons choisi de le faire. construisez la forme du cœur de cette façon, .551915 est un carré avec quatre côtés égaux (les quatre côtés sont composés des rayons de deux cercles), et ses diagonales sont également égales (ceci, comme mentionné précédemment, la ligne reliant les deux points centraux est égal à la ligne reliant les deux points d'intersection). Ici,

est l'intersection des deux diagonales, et

est égal à la moitié de la diagonale

et

sont sur la. TO0SO1 axe, donc leurs coordonnées O sont OT Leurs ST correspondent à la valeur absolue de T, qui est la moitié de la diagonale (S. > De même). 🎜>y Carréx (Live)0 yOT On peut couper n'importe quel carré de longueur de côté OS en deux triangles isocèles. Le côté droit de ce triangle isocèle coïncide avec le côté de . le carré, et l'hypoténuse coïncide avec la diagonale du carré

Tout carré peut être coupé en deux triangles isocèles TO0SO1En utilisant le théorème de Pythagore :

, on peut calculer l'hypoténuse de l'un des angles droits (c'est-à-dire la diagonale du carré). De cette façon, la longueur de la diagonale du carré peut être trouvée en fonction de la longueur du côté<. la longueur du c peut trouv partir de diagonale>). On peut aussi calculer la moitié de la diagonale l et appliquer cela à notre longueur de côté Pour sur le carré

. obtenir la coordonnée du point (sa valeur absolue est égale à la moitié de la diagonale du carré) est

, et en même temps la coordonnée

du point d² = l² + l² est d = √(2 * l) = l * √2l = d / √2d / 2 = (l * √2) / 2 = l / √2

coordonnées (Live) des quatre sommets du carré

De même, le point R est sur l'axe TO0SO1 Donc leur coordonnée d'axe T est , et leur coordonnée d'axe y est la moitié de la diagonale -R / √2 : S. yR / √2

est un carré, alors ses quatre coins sont des

( arcs). TO0SO1

Quadrilatère

(Live)O1. xyComme le montre l'image ci-dessus, la ligne droite 0 est une ligne diagonale, ce qui signifie que l'arc x est la moitié du cercle, ou on l'appelle un OO1 arc. Nous divisons cet arc en deux moitiés égales à l'aide du point ±R/√2, ce qui donne deux arcs

égaux :

et TO0SO1 . Ils correspondent à deux 90° angles égaux : π / 2 et

.

D'après les axiomes TA1B1S et sont tous deux des angles de

, ce qui prouve que la droite

est aussi un diamètre. Cela nous indique que dans le quadrilatère TB1, les diagonales TB1 et 180° sont perpendiculaires et égales, et se coupent en leurs points centraux respectifs (A1, 90°, TA1 et A1B1 sont tous Égal au rayon du cercle 90°). Cela signifie que le quadrilatère ∠TO1A1 est un carré et que sa diagonale est égale à ∠A1O1B1.

À ce stade, nous pouvons comprendre que les côtés du quadrilatère TA1B1S sont égaux à 2 * R / √2 = R * √2. Puisque tous les coins d'un carré sont 90° et que le côté TS chevauche l'axe vertical, les côtés TA1 et SB1 sont horizontaux et parallèles à l'axe x. En fonction de leurs longueurs, les coordonnées de l'axe A1 des deux points B1 et x peuvent être calculées : ±R * √2.

Parce que TA1 et SB1 sont horizontaux, les coordonnées de l'axe A1 des deux points B1 et y sont égales aux points T (-R / √2) et S (R / √2) respectivement.

Carré TA1B1S Quatre coordonnées de sommet (Live) .

Une autre conclusion que nous tirons d'ici est que parce que TA1B1S est un carré, donc A1B1 est parallèle à TS et parce que TS est sur l'axe y (vertical), donc A1B1 Également vertical. De plus, puisque l'axe x est parallèle à TA1 et SB1 et coupe TS en deux, l'axe x coupe également A1B1 en deux.

Maintenant, laissez-moi regarder les points de contrôle.

Nous commençons par les points de contrôle qui se chevauchent de l'arc le plus bas.

Quadrilatère TB0CB1 (Live).

Tous les angles du quadrilatère TB0CB1 sont égaux à 90° (car TO0SO1 est un carré donc ∠T est un angle droit ; parce que B1C est une tangente au cercle, il est perpendiculaire au rayon O1B1, et coupe au point B1, donc ∠B1 est un angle droit puisque les trois autres sont tous des angles droits, ∠C est aussi un angle droit), c'est donc un rectangle. De même, il a deux côtés adjacents égaux : TB0 et TB1. Les deux lignes sont des diamètres de cercles et les deux sont égales à 2 * R. Finalement, on conclut que le quadrilatère TB0CB1 est un carré de côté 2 * R.

Ensuite on peut obtenir sa diagonale TC : 2 * R * √2. Parce que C est sur l'axe y, sa coordonnée d'axe x est 0. Sa coordonnée de l'axe y est la longueur de OC. La longueur de OC est égale à TC moins OT : 2 * R * √2 - R / √2 = 4 * R / √2 - R / √2 = 3 * R / √2.

Carré TB0CB1 Coordonnées des quatre sommets (Live).

Nous avons maintenant les coordonnées des deux points de contrôle qui se chevauchent sur l'arc le plus bas comme (0,3 * R / √2).

Pour obtenir les coordonnées d'autres points de contrôle de courbe, nous traçons des lignes tangentes à leurs points d'extrémité et obtenons les points d'intersection D1 et E1 de ces lignes tangentes.

Quadrilatère TO1A1D1 et A1O1B1E1 (Live).

Dans le quadrilatère TO1A1D1, tous les angles sont connus pour être des angles droits (90° ), dont trois sont dérivés d'axiomes (∠D1TO1 et ∠D1A1O1 sont obtenus à partir de rayons et de tangentes ; ∠TO1A1 est l'angle correspondant à un quart d'arc TA1), alors le quatrième angle est aussi un angle droit par calcul. Cela prouve que TO1A1D1 est un rectangle. Et comme il a deux côtés adjacents égaux (O1T et O1A1 sont égaux au rayon R), TO1A1D1 est un carré.

Cela signifie que les diagonales TA1 et O1D1 sont égales à R * √2. On sait que TA1 est horizontal, et les deux diagonales du carré sont verticales, cela prouve que O1D1 est vertical. Ensuite, les coordonnées de l'axe O1 des points D1 et x sont égales, et la coordonnée de l'axe O1 de x est ±R / √2. Puisque nous connaissons la longueur de O1D1, nous pouvons calculer les coordonnées de l'axe de y en soustrayant la longueur de la diagonale (R * √2) comme mentionné précédemment. La situation est similaire pour le

quadrilatère A1O1B1E1. On sait que tous les angles sont des angles droits (90°), dont trois sont dérivés d'axiomes (∠E1A1O1 et ∠E1B1O1 sont obtenus à partir de rayons et de tangentes ; ∠A1O1B1 est le quart d'arc correspondant A1B1 angle) , alors le quatrième angle est calculé comme étant un angle droit. Cela prouve que A1O1B1E1 est un rectangle. Et comme il a deux côtés adjacents égaux (O1A1 et O1B1 sont égaux au rayon R), A1O1B1E1 est un carré.

至此,我们得到对角线 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心形点赞关注效果的简单实现

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn