ホームページ > 記事 > ウェブフロントエンド > 「星型ハート型」のコード共有を作成するための SVG および Vanilla JS フレームワーク
この記事では主に、SVG と Vanilla JS フレームワークを使用して「星型からハート型」のアニメーション効果を作成するコードを共有します。
それらはすべて 5 つの 3 次ベジェ曲線で構成されています。以下のインタラクティブなデモは、各曲線とそれが接続するポイントを示しています。任意の曲線または接続点をクリックすると、2 つのグラフの曲線がどのように対応しているかを確認できます。
すべての曲線が 3 次ベジェ曲線によって作成されていることがわかります。一部のカーブには 2 つの制御点が重なっていますが。
星とハートを構成する形はミニマルで非現実的です。しかし、彼らにはそれができるのです。
式アニメーションの例からわかるように、私は通常、このような形状を生成するために 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; }JavaScript では、まず要素
svg
と要素 path
を取得します。 path
は、星からハートに変化し、また星に戻る形です。次に、2 つの軸に沿った SVG の寸法が等しく、座標軸の原点が になるように、<code>viewBox
属性を要素 svg
に設定します。 (0,0)SVG の真ん中にあります。これは、viewBox
のサイズ値が D
の場合、その左上隅の座標は (-.5*D,-.5*D)
であることを意味します。コード>。最後になりましたが、遷移の初期状態と最終状態を保存するオブジェクトと、SVG グラフィック プロパティに必要な値を設定するメソッドを作成します。 🎜rreee🎜これで邪魔にならないようになったので、楽しい部分を始めましょう! 🎜🎜グラフィックの幾何学描画🎜🎜終点と制御点の初期座標を使用して星形を描画し、それらの最終座標を使用してハート形を描画します。 各座標の遷移範囲は、その初期値と最終値の差です。この例では、星の角を上に向けたいので、星がハートに変形するときに回転 (rotate
) します。また、塗りつぶし (fill
) を金色の星から深紅のハートに変更します。 🎜🎜では、これら 2 つの形状の終点と制御点の座標を取得するにはどうすればよいでしょうか? 🎜viewBox
に設定したサイズです (簡単にするために、この場合は高いパディングは考慮しません)。しかし、それらの交差点を取得するにはどうすればよいでしょうか? 🎜🎜まず、下の図を見てみましょう。写真の正五角形の中央にある強調表示された小さな五角形に注目してください。小さな五角形の頂点は、正五角形の辺の交点と一致します。この小さな五角形は明らかに正五角形(同じ長さの5つの辺)です。この小さな正五角形の内接円と内径は正五角形と同じです。 🎜🎜正五角形の内接円とその内側の正五角形は同じです(ライブ)。正五角形の内径🎜🎜 したがって、正五角形の内径を計算すると、正五角形の内径も得られます。この内径と中心角は正五角形の辺に相当します。これに基づいて、正五角形の外接円の半径を求めることができます。このようにして、正五角形の頂点の座標を推定することができます。これらの点はまさに正五角形の辺の交点であり、星の 5 つの 3 次ベジェ曲線の終点になります。 🎜🎜通常の五芒星は、位相記号 {5/2}
で表すことができます。つまり、正五芒星には 5
個の頂点があります。これら 5 つの頂点は外接円上に均等に配置されており、その間隔は 360°/5 = 72°
です。最初のポイントから開始し、次のポイントをスキップして、2 番目のポイントに接続します (これは、シンボル {5/2}
の 2 の意味; 1
は、点を飛ばさずに最初の点に接続して五角形を形成することを意味します。このようにつないでいくと、規則的な五芒星の形を描くことができます。 🎜🎜以下のデモでは、五角形または五芒星のボタンをクリックして、それらがどのように描画されるかを確認します。 🎜このようにすると、正五角形の辺に対応する中心角は、正五角形の辺に対応する中心角の2倍であることがわかります。この場合、正五角形は 1 * (360°/5) = 1 * 72° = 72°
(または 1 * (2 * π / 5)
ラジアン) となります。正五角形は 2 * (360° / 5) = 2 * 72° = 144°
(2 * (2 * π / 5)
ラジアン) です。通常、トポロジ記号で表される正多角形は {p,q}
であり、その辺の 1 つに対応する中心角は 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°
) と正五角形 (右、``72°'') (ライブ) 。 正五芒星の外接円の半径は既知であり、これは viewBox
サイズです。次に、直角三角形の斜辺の長さ (つまり、正五角形の外接円の半径) と鋭角の角度 (正五角形の 1 辺に相当する角度の半分) がわかります。正五角形の内径を計算できます(この内径は、正五角形の内側の小さな正五角形の内径に等しい)。
直角を使用すると、正五角形の内径を計算できます。この直角の斜辺は正五角形の外接円の半径に等しく、鋭角の一方の角度は正五角形の一辺に相当する角度の半分に等しい(ライブ)。 🎜🎜中心角の半分の余弦は、五芒星の内径と外接円の半径の比に等しくなります。五芒星の内径は、外接円の半径にこの余弦値を乗じた値に等しいと結論付けることができます。 🎜🎜正五角形の内側にある小さな正五角形の内接円の半径がわかったので、この正五角形の外接円の半径を計算できます。それでも小さな直角を通して計算されます。この直角の斜辺は、正五角形の外接円の半径に等しい。鋭角は、正五角形の一辺に対応する中心角の半分に等しい。この鋭角の一辺が中心角の中心直線であり、この中心直線が正五角形の外接円の半径となる。 🎜🎜下の図は、外接円の半径、内接円の半径、正五角形の中心角の半分で構成される直角三角形を強調表示しています。内接円の半径と正五角形の一辺に相当する中心角がわかれば、中心角の半分は2つの外接円の半径がなす角度になります。この直角三角形を使用して、外接円の半径の長さを計算できます。 🎜🎜直角三角形を通る正五角形の外接円の半径を計算します (ライブ)。 🎜🎜前述したように、正五角形の中心角は正五角形の中心角と等しくありません。前者は後者の半分です (360° / 5 = 72°
)。 🎜🎜 さて、この半径が得られたので、必要なすべての点の座標を取得できます。ポイントは両方の円に均等に配分されます。外側の円(正五角形の外接円)には 5
個の点があり、内側の円(正五角形の外接円)には 5
個の点があります。丸)。合計で 10
個のポイントがあり、それらが位置する半径光線間の角度は 360° / 10 = 36°
です。 🎜🎜端点は小さな正五角形の外接円上に均等に配置され、制御点は正五角形(ライブ) の外接円上に均等に配置されます。 🎜🎜 2 つの円の半径はわかっています。外側の円の半径は正五芒星の外接円の半径と等しく、これは設定した任意の viewBox
サイズ (.5
または ) の一部です。 >.25
または .32
またはより適切に機能すると思われるサイズ)。内円の半径は、正五角形の内側に形成される小さな正五角形の外接円の半径と等しい。この半径の計算方法は、まず、正五角形の外接円の半径とその一辺に相当する中心角から、正五角形の内接円の半径を計算します。この内接円の半径は、小さな正五角形の内接円の半径と等しいので、小さな正五角形の一辺に相当する中心角とその内接円の半径によって計算される。 🎜🎜それで、これに基づいて星の軌跡を描くためのデータを生成することができます。プロットに必要なデータはすでにあります。 🎜🎜それでは絵を描きましょう!そして、上記の思考プロセスをコードに書きます。 🎜🎜まず、getStarPoints(f)
の関数を作成します。パラメータ (f)
は、viewBox
のサイズに基づいて得られる正五角形の外接円の半径を決定します。この関数は座標の配列を返します。次に、この配列に配列項目を追加します。 🎜🎜この関数では、まず、正五角形の星の外接円の半径(外円の半径)、正五角形の星の一辺に相当する中心角、その内側にできる正五角形の定数を計算します。正五角形の星の一辺に相当する中心角、正五角形の星の内側に形成される正五角形の一辺に相当する中心角、正五角形の星と正五角形が共有する内接円の半径。正五角形の星(正五角形)の内側に形成される五点変形の頂点は正五角星の辺の交点)、内部の小さな正の五点変形の外接円の半径、座標を計算する必要がある点の総数と、すべての点が位置する放射状の線の間の角度。 🎜🎜次に、ループを使用して必要な点の座標を計算し、それらを座標配列に挿入します。 🎜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)。
靠下的曲线控制点很明显已经得到了,就是两条切线的交点。但是另外四条曲线的控制点呢?我们怎么能把圆弧变成三次贝塞尔曲线呢?
4 分の 1 円弧の 3 次ベジェ曲線を取得することはできませんが、この記事で説明されているように近似値を取得することはできます。
この記事では、R
の値を持つ半径とその半径の接線 (N
と Q
) を使用して、 4つの半分の円弧を描きます。 2 つの半径の接線は点 P
で交差します。四角形 ONPQ
の 4 つの角度は 90°
(または π / 2
) に等しく、そのうちの 3 つは公理 ( >O
は 90°
であり、2 つの接線と半径の間の角度も 90°
です)、最後の角度が計算されます (内角は 360°
、他の 3 つの角度は 90°
、最後の角度は 90°
です)。これは ONPQ です。
。同時に、ONPQ
には等しい 2 つの辺があります (OQ
と ON
は長さが等しい)。半径 R
)、辺の長さが R
の正方形であるため、NP
と QP
の長さは次のようになります。 R
とも同じです。 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 つの等しい辺を持つ正方形になります (4 つの辺はすべて次のように構成されます)。 2 つの円の半径は正方形を構成し、その対角線も等しい (前述したように、2 つの中心点を結ぶ線は 2 つの交点を結ぶ線に等しい)。ここでは、O
です。対角線の交点は 2 であり、OT
は T
と S
における対角線の ST
の半分に等しい。 . code>y 軸であるため、x
座標は 0
に対応します。 >。 code> の絶対値は対角線の半分です (OS
も同様です)。 🎜🎜スクエア TO0SO1
(ライブ)。 🎜🎜辺の長さが l
の正方形を 2 つの二等辺三角形に切り取ることができます。この二等辺三角形の直角の辺は正方形の辺と一致し、斜辺は正方形の対角線と一致します。 🎜🎜任意の正方形を 2 つの二等辺三角形に切断できます (ライブ)。 🎜🎜ピタゴラスの定理: d² = l² + l²
を使用すると、直角の 1 つ (つまり、正方形の対角線) の斜辺を計算できます。このように、正方形の対角線の長さは、辺の長さから求めることができます d = √(2 * l) = l * √2
(逆に、辺は対角線の長さに基づいて見つけることができます l = d / √2
)。対角線の半分 d / 2 = (l * √2) / 2 = l / √2
を計算することもできます。 🎜🎜これを辺の長さが R
の TO0SO1
正方形に適用すると、T
点が得られます (その絶対値は正方形のペアに等しいです)点 S
の y
座標は -R / √2
であり、点 Sy
座標です。 /code> は R/√2
です。 🎜🎜TO0SO1
正方形 (ライブ) の 4 つの頂点の座標。 🎜🎜同様に、O1
ポイントは x
軸上にあるため、その y
軸座標は 0
であり、 x
軸の座標は、対角線 OO1
の半分です: ±R/√2
。 🎜🎜TO0SO1
は正方形であり、その 4 つの角は 90°
(π / 2
の円弧) です。 🎜🎜クワッド TA1B1S
(ライブ)。 🎜🎜上の図に示すように、直線 TB1
は対角線です。これは、円弧 TB1
が円の半分、つまり 180° であることを意味します。
アーク。 A1
点を使用してこの円弧を 2 つの等しい半分に分割すると、2 つの等しい 90°
円弧、TA1
と A1B1コード>。これらは、2 つの等しい 90°
角度、 ∠TO1A1
と ∠A1O1B1
に対応します。 🎜🎜公理によると、∠TO1S
と ∠TO1A1
は両方とも 90°
の角度であり、直線 SA1 も直径です。これは、四角形 <code>TA1B1S
では、対角線 TB1
と SA1
が垂直で等しく、それぞれの中心点で交差していることがわかります ( >TO1、O1B1
、SO1
、および O1A1
はすべて、円 R
の半径に等しいです。 )。これは、四角形 TA1B1S
が正方形であり、その対角線が 2 * R
に等しいことを示しています。 🎜この時点で、四角形 TA1B1S
の辺が 2 * R / √2 = R * √2
に等しいことがわかります。正方形のすべての角度が 90°
であり、辺 TS
が縦軸に重なるため、辺 TA1
と SB1
>は水平であり、x
軸に平行です。それらの長さに応じて、2 つの点 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
は水平であるため、2 つの点 A1
と B1
の y /code> 軸の座標は、それぞれ <code>T (-R / √2)
点と S (R / √2)
点に等しくなります。 🎜🎜正方形 TA1B1S
4 つの頂点座標 (ライブ)。 🎜🎜ここから得られるもう 1 つの結論は、TA1B1S
が正方形であるため、A1B1
は TS
と平行であるということです。 > は y
(垂直) 軸上にあるため、A1B1
も垂直になります。また、x
軸は TA1
と SB1
に平行であり、TS
は 2 等分されます。 x 軸も A1B1
を 2 つの半分に分割します。 🎜🎜それではコントロールポイントを見てみましょう。 🎜🎜一番下の円弧の重なり合う制御点から始めます。 🎜🎜四角形 TB0CB1
(ライブ)。 🎜🎜四角形 TB0CB1
のすべての角度は 90°
に等しい (TO0SO1
は正方形であるため、∠T
は直角; B1C
は円の接線であるため、半径 O1B1
に対して垂直であり、B1
点で交差します。 code>∠B1 は直角です。他の 3 つは直角なので、∠C
も直角です)。したがって、これは長方形になります。同様に、隣接する 2 つの等しい辺、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
(ライブ) の 4 つの頂点の座標。 🎜🎜これで、一番下の円弧の重なっている 2 つの制御点の座標が (0,3 * R / √2)
として得られます。 🎜🎜他の曲線制御点の座標を取得するには、その端点に接線を引き、これらの接線の交点 D1
と E1
を取得します。 🎜🎜四角形 TO1A1D1
と A1O1B1E1
(ライブ)。 🎜🎜四角形 TO1A1D1
では、すべての角が直角 (90°
) であることが知られており、そのうちの 3 つは公理 (∠D1TO1 と <code>∠D1A1O1
は半径と接線から取得されます。∠TO1A1
は 4 分の 1 の円弧 TA1
に対応する角度です。 4 番目の角度は直角になるように計算されます。これは、TO1A1D1
が長方形であることを証明します。また、隣接する 2 つの辺が等しいため (O1T
と O1A1
は半径 R
に等しい)、TO1A1D1コード> 正方形です。 🎜🎜これは、対角線 <code>TA1
と O1D1
が R * √2
に等しいことを示しています。 TA1
が水平、正方形の 2 つの対角線が垂直であることがわかっているため、O1D1
が垂直であることがわかります。次に、点 O1
と D1
の x
軸座標は等しく、O1 の <code>x
軸は等しくなります。 座標は ±R / √2
です。 O1D1
の長さがわかっているので、前述したように対角線の長さを使用して y
軸座標を計算できます ( R * √2) 引き算をします。 🎜🎜四角形 <code>A1O1B1E1
の状況も同様です。すべての角度は直角 (90°
) であることが知られており、そのうち 3 つは公理 (∠E1A1O1
と ∠E1B1O1
) から導出されます。半径と接線で与えられる; ∠A1O1B1
は 4 分の 1 の円弧 A1B1
に対応する角度です)、4 番目の角度は直角として計算されます。これは、A1O1B1E1
が長方形であることを証明します。また、隣接する 2 つの辺が等しいため (O1A1
と O1B1
は半径 R
に等しい)、A1O1B1E1コード> 正方形です。 🎜<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 中国語 Web サイトの他の関連記事を参照してください。