이 기사에서는 SVG 및 Vanilla JS 프레임워크를 사용하여 "별 모양에서 하트 모양까지" 애니메이션 효과를 만드는 코드를 주로 공유합니다.
모두 5개의 입방체 베지어 곡선으로 구성되어 있습니다. 아래 대화형 데모에서는 각 곡선과 곡선이 연결되는 지점을 보여줍니다. 곡선이나 연결점을 클릭하면 두 그래프의 곡선이 어떻게 일치하는지 확인할 수 있습니다.
모든 곡선이 3차 베지어 곡선으로 생성된 것을 볼 수 있습니다. 일부 곡선에는 두 개의 제어점이 겹치는 경우도 있습니다.
별과 하트를 구성하는 모양은 미니멀하고 비현실적입니다. 하지만 그들은 할 수 있습니다.
표현 애니메이션 예시에서 볼 수 있듯이 저는 이런 형태를 생성하기 위해 주로 Pug(번역: Jade, 템플릿 엔진)를 사용합니다. 하지만 여기서는 생성된 경로 데이터로 인해 전환 효과도 JavaScript로 처리됩니다. 여기에는 좌표를 계산하고 해당 좌표를 d
속성에 배치하는 작업이 포함됩니다. 따라서 JavaScript를 사용하여 이 모든 작업을 수행하는 것이 최선의 선택입니다. d
。所以使用JavaScript来做所有的这些是最好的选择。
这意味着我们不必写很多标签:
<svg> <path id='shape'/></svg>
JavaScript中,我们首先获得元素 svg
和元素 path
。path
是那个星形变心形再变回星形的形状。然后,我们给元素 svg
设置viewBox
属性,使得 SVG 沿两个轴的尺寸相等,并且坐标轴的原点(0,0)
在 SVG 正中间。这意味着,当viewBox
的尺寸值为D
时,它的左上角坐标为(-.5*D,-.5*D)
。最后,这个也很重要,就是创建一个对象来存储过渡的初始和最终状态,以及一个将我们想要的值设置给 SVG 图形属性的方法。
const _SVG = document.querySelector('svg'), _SHAPE = document.getElementById('shape'), D = 1000, O = { ini: {}, fin: {}, afn: {} }; (function init() { _SVG.setAttribute('viewBox', [-.5*D, -.5*D, D, D].join(' ')); })();
现在我们把这事解决了,可以开始更有趣的部分了!
我们用终点和控制点的初始坐标来绘制星形,用它们的最终坐标来绘制心形。 每个坐标的过渡范围是它的初始值与最终值之间的差值。在这个例子中,当星形向心形转换时,我们会转动(rotate
)它,因为我们想让星形的角朝上。我们还会改变填充(fill
),从金色的星形变成深红色的心形。
那么,我们怎么能获得这两个图形的终点和控制点的坐标呢?
在星形的例子中,我们先从一个正五角星形开始。我们的曲线(译:构成星形每个角的曲线)终点落在正五角星形边的交叉点上,我们把正五角星形的顶点作为控制点。
五个三次贝塞尔曲线的终点和控制点用黄点标识在了正五角星形的顶点和边的交叉点上(Live)。
直接给定正五角星形外接圆的半径(或者直径)就可以获得五角星形的顶点。也就是我们给 SVG 的viewBox
设定的尺寸(简单起见,在这种情况下我们不考虑高填密)。但是如何获得他们的交叉点呢?
首先,我们先看下边的说明图。注意图中正五角星形中间高亮标注的小五边形。小五边形的顶点与正五角星形边的交叉点是重合的。这个小五边形显然是个正五边形(译:五个边的长度相等)。这个小正五边形的内切圆和内径跟正五角星形的是同一个。
正五角星形和内部的正五边形的内切圆是同一个 (Live)。
因此,如果我们计算出正五角星形的内径,那么也就获得了正五边形的内径。这个内径和圆心角 一起对应正五边形的边。根据这个我们就可以获得正五边形的外接圆半径 。这样就可以倒推出正五边形顶点的坐标。这些点正是正五角星形边的交叉点,也就是星形五个三次贝塞尔曲线的终点。
我们的正五角星形可以用拓扑符号 {5/2}
来表示。也就是说,正五角星形有5
个顶点。这5个顶点均匀分布在它的外接圆上,间隔是 360°/5 = 72°
。我们从第一个点开始,跳过紧挨着的下一个点,连接到紧挨着的第二个点(这就是符号{5/2}
中2
的含义;1
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; }
svg
요소와 path
요소를 가져옵니다. 경로
는 별에서 하트로, 다시 별으로 바뀌는 모양입니다. 그런 다음 viewBox
속성을 svg
요소에 설정하여 두 축을 따른 SVG의 크기가 동일하고 좌표 축의 원점이 가 되도록 합니다. (0,0)</code >SVG 중앙에 있습니다. 이는 <code>viewBox
의 크기 값이 D
일 때 왼쪽 위 모서리 좌표가 (-.5*D,-.5*D)
라는 의미입니다. 코드>. 마지막으로 전환의 초기 및 최종 상태를 저장하는 개체와 SVG 그래픽 속성에 원하는 값을 설정하는 메서드를 만듭니다. 🎜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)); }
rotate
)합니다. 또한 채우기(fill
)를 금색 별에서 진홍색 하트로 변경하겠습니다. 🎜🎜그렇다면 이 두 도형의 끝점과 제어점의 좌표는 어떻게 얻을 수 있을까요? 🎜viewBox
에 대해 설정한 크기입니다(단순화를 위해 이 경우 높은 패딩을 고려하지 않습니다). 그러나 교차점을 얻는 방법은 무엇입니까? 🎜🎜먼저 아래 그림을 살펴보겠습니다. 그림의 정오각형 중앙에 강조 표시된 작은 오각형을 주목하세요. 작은 오각형의 꼭지점은 정오각형 측면의 교차점과 일치합니다. 이 작은 오각형은 분명히 정오각형입니다(번역: 다섯 변의 길이가 동일함). 이 작은 정오각형의 내접원과 내경은 정오각형과 동일하다. 🎜🎜정오각형의 내접원과 내부 정오각형이 같습니다(라이브). 🎜🎜그러므로 정오각형의 내경을 계산하면 정오각형의 내경도 구할 수 있습니다. 이 내부 지름과 중심각은 함께 정오각형의 측면에 해당합니다. 이를 바탕으로 정오각형의 외접원의 반지름을 구할 수 있습니다. 이런 방법으로 정오각형의 꼭지점 좌표를 추론할 수 있습니다. 이 점들은 정확히 별의 5개 입방 베지어 곡선의 끝점인 정오각형 변의 교차점입니다. 🎜🎜우리의 정오각형은 위상기호 {5/2}
로 표현될 수 있습니다. 즉, 정오각형에는 5
개의 꼭지점이 있습니다. 이 5개의 꼭짓점은 외접원에 균등하게 분포되어 있으며 간격은 360°/5 = 72°
입니다. 첫 번째 지점에서 시작하여 다음 지점을 건너뛰고 두 번째 지점에 연결합니다({5/2}
기호의 2<입니다. /code>의 의미; < code>1
은 오각형을 형성하기 위해 어떤 점도 건너뛰지 않고 첫 번째 점에 연결하는 것을 의미합니다. 이렇게 계속 연결하면 규칙적인 오각별 모양을 그릴 수 있습니다. 🎜🎜아래 데모에서 오각형 또는 오각형 버튼을 클릭하여 어떻게 그려지는지 확인하세요. 🎜이런 식으로 정오각형의 측면에 해당하는 중심각은 정오각형의 측면에 해당하는 중심각의 두 배라는 것을 얻습니다. 그러면 정오각형은 1 * (360°/5) = 1 * 72° = 72°
(또는 1 * (2 * π / 5)
라디안)입니다. 정오각형은 2 * (360° / 5) = 2 * 72° = 144°
(2 * (2 * π / 5)
라디안)입니다. 일반적으로 {p,q}
와 같은 위상기호로 표현되는 정다각형은 그 변 중 하나에 해당하는 중심각이 q * (360° / p)
( < 코드>q * (2 * π / p) 라디안). 1 * (360°/5) = 1 * 72° = 72°
(或者1 * (2 * π / 5)
弧度),那正五角星形就是2 * (360° / 5) = 2 * 72° = 144°
(2 * (2 * π / 5)
弧度)。通常,一个用拓扑符号表示为{p,q}
的正多边形,它的一个边所对应的圆心角就是 q * (360° / p)
(q * (2 * π / p)
弧度)。
正多边形的一条边所对应的圆心角:正五角星形(左,144°
)vs 正五边形(右,``72°`)(Live)。
已知正五角星形外接圆半径,也就是的viewBox
尺寸。那么,已知直角三角形斜边的长(即正五角星形外接圆的半径)和锐角的度数(正五角星形一条边所对应的角度的一半),这意味着我们可以算出正五角星形的内径(这个内径与正五角星形内部的小正五边形的内径相等)。
通过直角,可以计算出正五角星形的内径长。这个直角的斜边等于正五角星形外接圆半径,其中一个锐角的角度等于正五角星形一条边所对应的角度的一半 (Live)。
圆心角一半的余弦等于五角星形的内径比外接圆半径。就可以得出,五角星形的内径等于外接圆半径乘以这个余弦值。
现在我们得到了正五角星形内部小正五边形的内接圆半径,我们就可以计算出这个正五边形的外接圆半径了。还是通过一个小直角来计算。这个直角的斜边等于正五边形外接圆半径。一个锐角等于正五边形一条边所对应的圆心角的一半。这个锐角的一条边是这个圆心角的中直线,这个中直线是正五边形的外接圆半径。
下边的说明图中高亮标注了一个直角三角形,它是由正五边形的一条外接圆半径、内接圆半径、一个圆心角的一半构成的。如果我们已知内接圆半径和正五边形一条边所对应的圆心角,这个圆心角的一半也就是两条外接圆半径的夹角的话。用这个直角三角形我们可以计算出外接圆半径的长。
通过一个直角三角形计算正五边形外接圆的半径 (Live)。
前文提到过,正五边形圆心角的度数与正五角星形的圆心角度数是不相等的。前者是后者的一半 (360° / 5 = 72°
)。
好,现在我们有了这个半径,就可以得到所有想要的点的坐标了。这些点均匀分布在两个圆上。有5
个点在外层的圆上(正五角星形的外接圆),还有5
个在内层的圆上(小正五边形的外接圆)。共计10
个点,他们所在的半径射线的夹角是 360° / 10 = 36°
。
终点均匀分布在小正五边形的外接圆上,控制点均匀分布在正五角星形的外接圆上 (Live)。
已知两个圆的半径。外层圆的半径等于正五角星形外接圆半径,也就是我们定的有点儿随意的viewBox
尺寸的一部分(.5
或 .25
或 .32
或者我们认为效果更好地尺寸)。内层圆的半径等于正五角星形内部构成的小正五边形的外接圆半径。计算这个半径的方法是:首先,通过正五角星形的外接圆半径和它的一条边所对应的圆心角计算出正五角星形的内接圆半径。这个内接圆半径与小正五边形的内接圆半径相等;然后,再通过小正五边形一条边所对应的圆心角和它的内接圆半径来计算。
所以,基于这点,我们就能够生成绘制星形的路径的数据了。绘制它所需要的数据,我们都已经有了。
那么让我们来绘制吧!并且把上边的思考过程写成代码。
首先,先创建一个getStarPoints(f)
的函数。参数 (f)
将决定根据 viewBox
144°
) vs 정오각형(오른쪽, ``72°`) (Live) . 정다섯개 별의 외접원 반경이 알려져 있는데, 이는 viewBox
크기입니다. 그러면 우리는 직각삼각형의 빗변의 길이(즉, 정오각형의 외접원의 반지름)와 예각의 정도(정오각형의 한 변에 해당하는 각도의 절반)를 알게 되는데, 이는 정오각형을 계산할 수 있습니다. 의 내경(이 내경은 정오각형 내부의 작은 정오각형의 내경과 같습니다). 직각을 이용하면 정오각형의 내경을 계산할 수 있습니다. 이 직각의 빗변은 정오각형의 외접원의 반경과 같고 예각 중 하나의 각도는 정오각형의 한 변에 해당하는 각도의 절반과 같습니다(라이브). 🎜🎜중심각의 절반에 해당하는 코사인은 오각형의 내경과 외접원의 반지름의 비율과 같습니다. 다섯개 별의 내경은 외접원의 반지름에 이 코사인 값을 곱한 것과 같다고 결론 내릴 수 있습니다. 🎜🎜이제 정오각형 안에 작은 정오각형의 내접원 반경이 있으므로 이 정오각형의 외접원 반경을 계산할 수 있습니다. 여전히 작은 직각을 통해 계산됩니다. 이 직각의 빗변은 정오각형의 외접원의 반지름과 같습니다. 예각은 정오각형의 한 변에 해당하는 중심각의 절반과 같습니다. 이 예각의 한 변은 중심각의 중심 직선이고, 이 중심 직선은 정오각형의 외접원의 반지름이다. 🎜🎜아래 그림은 외접원의 반지름, 내접원의 반지름, 정오각형의 중심각의 절반으로 구성된 직각 삼각형을 강조합니다. 내접원의 반지름과 정오각형의 한 변에 해당하는 중심각을 안다면, 중심각의 절반은 두 외접원의 반지름 사이의 각도입니다. 이 직각삼각형을 이용하여 외접원의 반지름의 길이를 계산할 수 있습니다. 🎜🎜직각삼각형을 통해 정오각형의 외접원의 반지름을 계산합니다(라이브). 🎜🎜앞서 언급했듯이 정오각형의 중심각은 정오각형의 중심각과 같지 않습니다. 전자는 후자의 절반입니다(360° / 5 = 72°
). 🎜🎜좋아, 이제 이 반경이 있으므로 원하는 모든 지점의 좌표를 얻을 수 있습니다. 점은 두 원에 고르게 분포되어 있습니다. 외부 원(정오각형의 외접원)에는 5
점이 있고 내부 원(정오각형의 외접원)에는 5
점이 있습니다. 원). 총 10
포인트가 있으며, 해당 포인트가 위치한 반경 광선 사이의 각도는 360° / 10 = 36°
입니다. 🎜🎜끝점은 작은 정오각형의 외접원에 고르게 분포되어 있고, 제어점은 정오각형의 외접원에 고르게 분포되어 있습니다(라이브). 🎜🎜두 원의 반지름이 알려져 있습니다. 외부 원의 반경은 우리가 설정한 다소 임의적인 viewBox
크기(.5
또는 .25
또는 .32
또는 우리가 더 잘 작동한다고 생각하는 크기). 안쪽 원의 반지름은 정오각형 내부에 형성된 작은 정오각형의 외접원의 반지름과 같습니다. 이 반지름을 계산하는 방법은 다음과 같습니다. 먼저 정오각형의 외접원의 반지름과 그 변 중 하나에 해당하는 중심각을 통해 정오각형의 내접원의 반지름을 계산합니다. 이 내접원의 반지름은 작은 정오각형의 내접원의 반지름과 같습니다. 그러면 작은 정오각형의 한 변에 해당하는 중심각과 내접원의 반지름으로 계산됩니다. 🎜🎜이를 바탕으로 별의 경로를 그리는 데 필요한 데이터를 생성할 수 있습니다. 우리는 그것을 그리는 데 필요한 데이터를 이미 가지고 있습니다. 🎜🎜그럼 그려보자! 그리고 위의 사고과정을 코드로 작성해 보세요. 🎜🎜먼저 getStarPoints(f)
함수를 만듭니다. (f)
매개변수는 viewBox
의 크기에 따라 얻은 정오각형의 외접원 반경을 결정합니다. 이 함수는 좌표 배열을 반환하며, 이 배열에 배열 항목을 추가합니다. 🎜🎜이 함수에서는 먼저 상수를 계산합니다. 정오각형 별의 외접원 반경(외부 원의 반경), 정오각형 별의 한 변에 해당하는 중심각, 내부에 형성된 정오각형 정오각형별은 원의 한 변에 해당하는 중심각, 정오각형 내부에 형성된 정오각형의 한 변에 해당하는 중심각, 정오각형과 내부에 형성된 정오각형이 공유하는 내접원의 반경이다. 정오각형(regular pentagram) 5점 변형의 꼭지점은 정다섯개 별의 변의 교차점), 내부 작은 양수 5점 변형의 외접원의 반경, 총 점 수 좌표와 모든 점이 위치한 방사형 선 사이의 각도를 계산해야 합니다. 🎜🎜그런 다음 루프를 사용하여 원하는 점의 좌표를 계산하고 이를 좌표 배열에 삽입합니다. 🎜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 ? ' ' : 'C') + c }, `M${pts[pts.length - 1]}`) } }; for(let p in O) _SHAPE.setAttribute(p, O[p].afn(O[p].ini)) })();
绘制的结果可以在下边的演示中看到:
这是一个很有前途的星形。但我们想让生成的五角星形第一个尖朝下并且由它生成的星形的第一个尖朝上。目前,他们的指向都偏右了。这是因为我们是从 0°
开始的(对应时钟的三点位置)。所以为了能从时钟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('rotate', ang) }; // same as before})();
我们用类似的方式给我们的星形填充(fill
)金色。我们给初始值设置一个 RGB
字符串,用同一个函数来给属性(fill
)设置值:
(function init() { // same as before O.fill = { ini: [255, 215, 0], afn: (rgb) => fnStr('rgb', rgb) }; // same as before})();
现在我们用 SVG 绘制好了一个漂亮的金色星形,它是由五个三次贝塞尔曲线构成的:
我们已经绘制好星形了,现在来看下如何绘制心形吧!
我们先从两个半径相等并横向相交的圆开始,这两个圆都是 viewBox
尺寸的一部分(暂时定位.25
)。这两个圆相交的方式为:它们中心点相连的线落在 x
轴上,它们相交点相连的线落在 y
轴上。这两条线要相等。
我们先从两个半径相等的相交的圆开始。这两个圆的圆心落在水平轴上,他们相交的点落在垂直轴上 (Live)。
接着,我们画两条直径,这两条直径穿过靠上的那个交点。在直径与圆的另一个交点处画一条正切线。这两条正切线在 y
轴相交。
画两条直径,穿过两个圆相交的点中靠上的那个,并在直径与圆的另一个交点处画正切线,两条正切线在垂直轴相交 (Live)。
两个圆上边的交点和两个直径与圆的另两个交点构成了我们需要的5
个点中的3
个。另外两个终点则是把外侧的半圆切割成两个相等弧线的中点,这使我们得到4
个四分之一圆弧。
高亮显示了构成心形的三次贝塞尔曲线的终点以及靠下的那条曲线的控制点(Live)。
靠下的曲线控制点很明显已经得到了,就是两条切线的交点。但是另外四条曲线的控制点呢?我们怎么能把圆弧变成三次贝塞尔曲线呢?
1/4 호에 대한 3차 베지어 곡선을 얻을 수는 없지만 이 기사에 설명된 대로 근사치를 얻을 수 있습니다.
이 기사에서는 R
값과 반지름의 탄젠트(N
및 Q
)를 사용하여 반경을 사용할 수 있음을 설명합니다. 4개의 1/4 호를 그립니다. 두 반경의 접선은 P
지점에서 교차합니다. 사변형 ONPQ
의 네 각도는 90°
(또는 π / 2
와 같습니다. 그 중 세 개는 공리(O
는 90°
이고, 두 접선과 반경 사이의 각도도 90°
입니다), 마지막 각도가 계산됩니다( 내부 각도는 360°
이고 나머지 세 각도는 90°
이며 마지막 각도는 90°
입니다. 이는 ONPQ입니다.
. 동시에 ONPQ
에는 두 개의 인접한 변이 동일합니다(OQ
및 ON
의 길이는 다음과 같습니다). 반경 R
)이므로 변의 길이가 R
인 정사각형이므로 NP
와 QP
의 길이는 다음과 같습니다. R
.code>와 동일합니다. R
的半径,和半径的切线(N
和 Q
)来绘制四分之一圆弧。两条半径的切线相交于点 P
。四边形 ONPQ
的四个角都等于90°
(或π / 2
,其中三个是公理得出的(O
是90°
,两条切线与半径的夹角也是90°
),最后一个是计算得出的(内角的合是 360°
,其它三个角都是90°
, 最后一个角也就是90°
了)。这样 ONPQ
就是一个矩形。同时 ONPQ
有两个相邻的边是相等的(OQ
和 ON
的长度都等于半径R
),这样它就是一个边长为R
的正方形。所以 NP
和 QP
长也等于R
。
用三次贝塞尔曲线绘制近似四分之一圆弧的弧线 (Live)。
我们用三次贝塞尔曲线绘制的近似四分之一圆弧的弧线的控制点就在切线 NP
和 QP
上,也就是从终点算起C * R
的长度,C
在之前提到文章中算出的值是.551915
。
知道了上边这些,我们可以开始计算三次贝塞尔曲线终点和控制点的坐标了,有了这些坐标,就可以构建我们的心形了。
由于我们选择这种方式构建心形, TO0SO1
是一个四边相等(四个边都是由两个圆的半径构成)的 正方形 ,并且它的对角线也是相等的(这点前文有说过,两个圆心的连线与两个交点的连线相等)。这里,O
是两个对角线的交点,并且 OT
等于对角线 ST
的一半。T
和 S
在 y
轴上,所以他们的 x
坐标为 0
。他们的 y
坐标对应 OT
的绝对值,也就是对角线的一半(OS
同理)。
正方形 TO0SO1
(Live)。
我们可以把任意边长为l
的正方形切割成两个等腰三角形。这个等腰三角形的直角边与正方形的边重合,斜边与正方形对角线重合。
任意正方形可以被切割成两个等腰三角形(Live)。
利用勾股定理:d² = l² + l²
,我们可以计算出其中一个直角的斜边(也就是正方形的对角线)。这样根据边长就可以得出正方形对角线的长 d = √(2 * l) = l * √2
(相反,根据对角线的长就可以得出边的长 l = d / √2
)。还能计算出对角线的一半d / 2 = (l * √2) / 2 = l / √2
。
把这个应用到我们的边长为 R
的 TO0SO1
正方形上,我们得到 T
点(它的绝对值等于正方形对角线的一半)的 y
坐标是 -R / √2
,同时 S
点的 y
坐标是R / √2
。
TO0SO1
正方形四个顶点的坐标 (Live)。
类似的,O1
点在 x
轴上,所以他们的 y
轴坐标为0
,他们的 x
轴坐标是对角线 OO1
的一半:±R/√2
。
TO0SO1
是个正方形,那么它的四个角都是90°
(π / 2
圆弧)。
四边形 TA1B1S
(Live)。
如上图所示,直线 TB1
是对角线,也就是说圆弧 TB1
是圆形的一半,或者叫做180°
弧线。我们用 A1
点将这个弧分割成了相等的两半儿,得到两个相等的 90°
弧线:TA1
和 A1B1
。他们对应两个相等的 90°
角:∠TO1A1
和 ∠A1O1B1
。
根据公理 ∠TO1S
和 ∠TO1A1
都是90°
的角,这证明直线 SA1
也是直径。这告诉我们在四边形 TA1B1S
中,对角线 TB1
和 SA1
是垂直且相等的,并且相交于各自的中心点(TO1
、O1B1
、SO1
和 O1A1
都等于圆形的的半径R
)。这说明四边形 TA1B1S
是正方形,且它的对角线等于2 * R
NP
및 QP
, 즉 C * R
의 길이에 있습니다. 끝점, C
앞서 언급한 글에서 계산된 값은 .551915
입니다. 🎜🎜위 내용을 알면 3차 베지어 곡선 끝점의 좌표 계산을 시작할 수 있고 이 좌표를 사용하여 하트 모양을 구성할 수 있습니다. 🎜🎜이런 방식으로 하트 모양을 구성하기로 했기 때문에 TO0SO1
은 4개의 변이 모두 동일한 정사각형입니다. 두 원의 반지름)은 정사각형을 구성하고 대각선도 동일합니다(앞서 언급한 것처럼 두 중심점을 연결하는 선은 여기서 O</code). >는 대각선의 교차점이며 <code>OT
는 T
및 S</code에서 대각선 <code>ST
의 절반과 같습니다. >.code>y 축이므로 x
좌표는 0
입니다. y
좌표는 OT</에 해당합니다. code>의 절대값은 대각선의 절반입니다(<code>OS
에도 동일하게 적용됨). 🎜🎜스퀘어 TO0SO1
(라이브). 🎜🎜변 길이가 l
인 정사각형은 두 개의 이등변삼각형으로자를 수 있습니다. 이 이등변삼각형의 직각변은 정사각형의 변과 일치하고, 빗변은 정사각형의 대각선과 일치합니다. 🎜🎜모든 정사각형은 두 개의 이등변삼각형으로 절단될 수 있습니다(라이브). 🎜🎜피타고라스 정리: d² = l² + l²
를 사용하면 직각 중 하나의 빗변(즉, 정사각형의 대각선)을 계산할 수 있습니다. 이렇게 하면 정사각형의 대각선의 길이는 d = √(2 * l) = l * √2
변의 길이를 기준으로 구할 수 있습니다. (반대로, 변은 대각선의 길이를 기준으로 찾을 수 있습니다 l = d / √2
). 대각선 d / 2 = (l * √2) / 2 = l / √2
의 절반을 계산할 수도 있습니다. 🎜🎜이를 변 길이가 R
인 TO0SO1
정사각형에 적용하면 T
점을 얻습니다(절대값은 정사각형 쌍과 같습니다. S
지점의 y
좌표는 -R / √2
이고 S< 지점의 <code>y
좌표는 -R / √2
입니다. /code>는 R/√2
입니다. 🎜🎜TO0SO1
사각형의 네 꼭짓점 좌표(라이브). 🎜🎜마찬가지로 O1
지점은 x
축에 있으므로 해당 y
축 좌표는 0
이고 x
축 좌표는 대각선 OO1
: ±R/√2
의 절반입니다. 🎜🎜TO0SO1
은 정사각형이고 네 모서리가 90°
(π / 2
호)입니다. 🎜🎜쿼드 TA1B1S
(라이브). 🎜🎜위 그림과 같이 직선 TB1
은 대각선입니다. 이는 호 TB1
가 원의 절반, 즉 180°라는 의미입니다.
호. A1
점을 사용하여 이 호를 두 개의 동일한 반으로 나누면 두 개의 동일한 90°
호(TA1
및 < code>A1B1)가 생성됩니다. 코드>. 두 개의 동일한 90°
각도, ∠TO1A1
및 ∠A1O1B1
에 해당합니다. 🎜🎜공리 ∠TO1S
및 ∠TO1A1
에 따르면 각도는 90°
이며, 이는 직선 SA1</ code> 도 직경입니다. 이는 사변형 <code>TA1B1S
에서 대각선 TB1
과 SA1
이 수직이고 동일하며 각각의 중심점에서 교차한다는 것을 알려줍니다(< code >TO1, O1B1
, SO1
및 O1A1
은 모두 원 R
의 반경과 같습니다. ) . 이는 사변형 TA1B1S
가 정사각형이고 대각선이 2 * R
과 같음을 보여줍니다. 🎜이 시점에서 우리는 사변형 TA1B1S
의 변이 2 * R / √2 = R * √2
와 같다는 것을 알 수 있습니다. 정사각형의 모든 각도가 90°
이고 변 TS
가 수직 축과 겹치므로 변 TA1
및 SB1
> 수평이고 x
축과 평행합니다. 길이에 따라 두 점 A1
및 B1
의 x
축 좌표를 계산할 수 있습니다: ±R * √2< /코드>. <code>TA1B1S
的边等于2 * R / √2 = R * √2
。由于正方形所有的角都是90°
,并且边 TS
与垂直轴重叠,所以边 TA1
和 SB1
是水平的,且平行于 x
轴。根据他们的长度可以算出 A1
和 B1
两点的 x
轴坐标:±R * √2
。
因为 TA1
和 SB1
是水平的, 所以 A1
和 B1
两点的 y
轴坐标分别等于 T (-R / √2)
和 S (R / √2)
点。
正方形 TA1B1S
四个顶点坐标(Live)。
我们从这里得到的另一个结论是,因为 TA1B1S
是正方形,所以 A1B1
平行于 TS
,因为 TS
在 y
(垂直)轴上,所以 A1B1
也是垂直的。此外,因为 x
轴平行于 TA1
和 SB1
,并且将 TS
平分切为两断,所以 x
轴也将 A1B1
平分切为了两断。
现在让我来看看控制点。
我们先从最下边弧线的重叠的控制点开始。
四边形 TB0CB1
(Live)。
四边形 TB0CB1
的所有角都等于 90°
(因为 TO0SO1
是正方形所以 ∠T
是直角;因为 B1C
是圆的切线,它与半径 O1B1
垂直,并相交于 B1
点, 所以 ∠B1
是直角;因为其他三个都是直角,所以 ∠C
也是直角),所以它是个矩形。同样它有两个相邻的边相等:TB0
和 TB1
。这两条线都是圆形的直径,且都等于 2 * R
。最后得出结论四边形 TB0CB1
是一个边长为2 * R
的正方形。
然后我们可以得到它的对角线 TC
: 2 * R * √2
。因为 C
在 y
轴上,它的 x
轴坐标为 0
。它的 y
轴坐标是 OC
的长度。OC
的长度等于 TC
减去 OT
:2 * R * √2 - R / √2 = 4 * R / √2 - R / √2 = 3 * R / √2
。
正方形 TB0CB1
四个顶点的坐标 (Live)。
现在我们得到了最下边弧线两个重叠的控制点的坐标为(0,3 * R / √2)
。
为了获得其他曲线控制点的坐标,我们在他们的终点上画切线,并且获得这些切线的交叉点 D1
和 E1
。
四边形 TO1A1D1
和 A1O1B1E1
(Live)。
在四边形 TO1A1D1
中,已知所有角都是直角(90°
),其中三个是公理得出的(∠D1TO1
和 ∠D1A1O1
是由半径和切线获得的;∠TO1A1
是对应四分之一弧 TA1
的角),那么第四个角通过计算就得出也是直角。这证明 TO1A1D1
是矩形。又因为它有两个相邻的边相等(O1T
和 O1A1
等于半径 R
),所以 TO1A1D1
是正方形。
这说明对角线 TA1
和 O1D1
等于 R * √2
。已知 TA1
是水平的,又正方形两个对角线是垂直的,就证明 O1D1
是垂直的。那么点 O1
和 D1
的 x
轴坐标相等,O1
的 x
轴坐标是±R / √2
。因为我们知道 O1D1
的长,所以我们可以算出 y
轴坐标:如前文提到的那样用对角线的长( R * √2
)做减法。
四边形 A1O1B1E1
的情况类似。已知所有角都是直角(90°
),其中三个是公理得出的(∠E1A1O1
和 ∠E1B1O1
是由半径和切线获得的;∠A1O1B1
是对应四分之一弧 A1B1
的角),那么第四个角通过计算就得出也是直角。这证明 A1O1B1E1
是矩形。又因为它有两个相邻的边相等(O1A1
和 O1B1
等于半径R
),所以 A1O1B1E1
TA1
과 SB1
은 수평이므로 두 점 A1
과 B1</code의 <code>y<는 > /code> 축 좌표는 각각 <code>T (-R / √2)
및 S (R / √2)
지점과 같습니다. 🎜🎜사각형 TA1B1S
4개의 꼭지점 좌표(라이브). 🎜🎜여기서 얻는 또 다른 결론은 TA1B1S
가 정사각형이므로 A1B1
는 TS
와 평행하다는 것입니다. 왜냐하면 TS</code >는 <code>y
(세로) 축에 있으므로 A1B1
도 세로입니다. 게다가 x
축이 TA1
, SB1
와 평행하고, TS
가 두 부분으로 잘려져 있으므로, < code>x 축은 A1B1
도 두 부분으로 자릅니다. 🎜🎜이제 제어점을 살펴보겠습니다. 🎜🎜가장 낮은 호의 겹치는 제어점부터 시작합니다. 🎜🎜사변형 TB0CB1
(라이브). 🎜🎜사각형 TB0CB1
의 모든 각도는 90°
와 같습니다(TO0SO1
는 정사각형이므로 ∠T
는 직각; B1C
는 원의 접선이므로 반경 O1B1
에 수직이고 B1
지점에서 교차하므로 < code>∠B1는 직각입니다. 다른 세 개는 직각이므로 ∠C
도 직각이므로 직사각형입니다. 마찬가지로 TB0
와 TB1
이라는 두 개의 인접한 변이 동일합니다. 두 선 모두 원의 지름이고 둘 다 2 * R
과 같습니다. 마지막으로, 사각형 TB0CB1
은 한 변의 길이가 2 * R
인 정사각형이라는 결론이 나왔습니다. 🎜🎜그러면 대각선 TC
: 2 * R * √2
를 얻을 수 있습니다. C
는 y
축에 있으므로 x
축 좌표는 0
입니다. y
축 좌표는 OC
의 길이입니다. OC
의 길이는 TC
에서 OT
를 뺀 것과 같습니다. 2 * R * √2 - R / √2 = 4 * R / √ 2 - R / √2 = 3 * R / √2
. 🎜🎜사각형 TB0CB1
(Live)의 네 꼭짓점 좌표입니다. 🎜🎜이제 우리는 가장 낮은 호의 겹치는 두 제어점의 좌표를 (0,3 * R / √2)
로 얻었습니다. 🎜🎜다른 곡선 제어점의 좌표를 얻기 위해 끝점에 접선을 그리고 이러한 접선 D1
및 E1
의 교차점을 얻습니다. 🎜🎜사변형 TO1A1D1
및 A1O1B1E1
(라이브). 🎜🎜사각형 TO1A1D1
에서는 모든 각도가 직각(90°
)인 것으로 알려져 있으며, 그 중 세 개는 공리(∠D1TO1<)에서 파생됩니다. /code > 및 <code>∠D1A1O1
는 반경과 접선에서 얻어집니다. ∠TO1A1
는 1/4 호 TA1
에 해당하는 각도입니다. 네 번째 각도는 직각으로 계산됩니다. 이는 TO1A1D1
이 직사각형임을 증명합니다. 그리고 두 개의 인접한 변이 동일하므로(O1T
및 O1A1
는 반경 R
과 동일) TO1A1D1</ code> 정사각형이에요. 🎜🎜이는 대각선 <code>TA1
과 O1D1
이 R * √2
와 같음을 보여줍니다. TA1
이 수평이고, 정사각형의 두 대각선이 수직인 것으로 알려져 있어 O1D1
이 수직임을 증명합니다. 그러면 O1
지점과 D1
지점의 x
축 좌표가 동일하고 O1의 <code>x
축이 좌표는 ±R / √2
입니다. O1D1
의 길이를 알고 있으므로 앞서 언급한 대각선 길이를 사용하여 y
축 좌표를 계산할 수 있습니다( R * √2< /code >) 뺄셈을 하세요. 🎜🎜사각형 <code>A1O1B1E1
의 상황도 비슷합니다. 모든 각도는 직각(90°
)인 것으로 알려져 있으며, 그 중 세 개는 공리(∠E1A1O1
및 ∠E1B1O1
)에서 파생됩니다. 반경과 접선으로 주어지며, ∠A1O1B1
는 1/4 호 A1B1
에 해당하는 각도입니다. 그러면 네 번째 각도는 직각으로 계산됩니다. 이는 A1O1B1E1
이 직사각형임을 증명합니다. 그리고 두 개의 인접한 변이 동일하므로(O1A1
및 O1B1
는 반경 R
과 동일) A1O1B1E1</ code> 정사각형이에요. 🎜<p>至此,我们得到对角线 <code>A1B1
和 O1E1
的长为R * √2
。我们知道 A1B1
是垂直的,并且被水平轴切割成相等的两半儿,也就是 O1E1
在水平轴上,点 E1
的 y
轴坐标为0
。因为点 O1
的 x
轴坐标为±R / √2
,并且 O1E1
等于R * √2
,我们就可以计算出点 E1
的 x
轴坐标为:±3 * R / √2
。四边形 TO1A1D1
和 A1O1B1E1
的顶点坐标(Live)。
但是这些切线的交叉点并不是控制点,所以我们需要用近似圆弧形的方法来计算。我们想要的控制点在 TD1
、A1D1
、A1E1
和 B1E1
上,距离弧线终点(T
、A1
、B1
)大约55%
(这个值来源于前文提到的那篇文章中算出的常量C
的值)的位置。也就是说从终点到控制点的距离是C * R
。
在这种情况下,我们的控制点坐标为:终点(T
、A1
和 B1
)坐标的1 - C
,加上,切线交点(D1
和 E1
)坐标的 C
。
让我们把这些写入JavaScript代码吧!
跟星形的例子一样,我们先从函数getStarPoints(f)
开始。根据这个函数的参数 (f)
,我们可以从viewBox
的尺寸中获得辅助圆的半径。这个函数同样会返回一个坐标构成的数组,以便我们后边插入数组项。
在函数中,我们先声明常量。
辅助圆的半径。
边与这个辅助圆半径相等的小正方形对角线的一半。对角线的一半也是这些正方形外接圆半径。
三次贝塞尔曲线终点的坐标值(点T
、A1
、B1
),沿水平轴的绝对值。
然后我们把注意力放在切线交点的坐标上( 点 C
、D1
、E1
)。这些点或者与控制点(C
)重合,或者可以帮助我们获得控制点(例如点 D1
和 E1
)。
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('viewBox', [-.5*D, -.5*D, D, D].join(' ')); O.d = { ini: getStarPoints(), fin: getHeartPoints(), afn: function(pts) { return pts.reduce((a, c, i) => { return a + (i%3 ? ' ' : 'C') + c }, `M${pts[pts.length - 1]}`) } }; O.transform = { ini: -180, fin: 0, afn: (ang) => fnStr('rotate', ang) }; O.fill = { ini: [255, 215, 0], fin: [220, 20, 60], afn: (rgb) => fnStr('rgb', 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('click', e => { dir *= -1; for(let p in O) _SHAPE.setAttribute(p, O[p].afn(O[p][dir > 0 ? 'fin' : 'ini'])); }, false); })();
现在我们可以通过点击图形在两种图形中转换了:
我们最终想要的并不是两个图形间唐突的切换,而是柔和的渐变效果。所以我们用以前的文章说明的插值技术来实现。
首先我们要决定转变动画的总帧数(NF
),然后选择一种我们想要的时间函数:从星形到心形的的路径(path
)转变我们选择ease-in-out
函数,旋转角度的转变我们选择 bounce-ini-fin
函数,填充(fill
)颜色转变我们选择ease-out
函数。我们先只做这些,如果之后我们改变注意了想探索其它的选项,也可以添加。
/* same as before */const NF = 50, TFN = { 'ease-out': function(k) { return 1 - Math.pow(1 - k, 1.675) }, 'ease-in-out': function(k) { return .5*(Math.sin((k - .5)*Math.PI) + 1) }, 'bounce-ini-fin': 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: 'ease-in-out' }; O.transform = { // same as before tfn: 'bounce-ini-fin' }; O.fill = { // same as before tfn: 'ease-out' }; // same as before})();
我们继续添加请求变量 ID
(rID
)、当前帧变量 (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('click', e => { if(rID) stopAni(); dir *= -1; m = .5*(1 - dir); update(); }, false);
在 update()
函数中,我们需要设置当过渡到中间值(取决于进程变量k)时的属性。如同前边的文章中所述,最好是在开始时计算出最终值和初始值之间的差值范围,甚至是在设置监听之前就设置好,所以我们的下一步是:创建一个计算数字间差值范围的函数。无论在这种情况下,还是在数组中,无论数组的嵌套有多深,都可以这个函数来设置我们想要转变的属性的范围值。
function range(ini, fin) { return typeof ini == 'number' ? 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
)、当前属性(ini
和 rng
)的范围(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 == 'number' ? 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 == 'number' ? 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('rotate', ang), tfn: 'bounce-ini-fin', cnt: 1 }; // same as before})();
现在我们得到了我们想要的最终结果:一个从金色星形变成深红色心形的形状,每次从一个状态到另一个状态顺时针旋转半圈。
相关推荐:
위 내용은 '별 모양 하트 모양' 코드 공유를 생성하는 SVG 및 Vanilla JS 프레임워크의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!