In diesem Artikel teilen wir Ihnen hauptsächlich den Code zum Erstellen eines „sternförmigen bis herzförmigen“ Animationseffekts mit SVG und dem Vanilla JS-Framework mit. Wir hoffen, dass er allen helfen kann.
Sie bestehen alle aus fünf kubischen Bezier-Kurven. Die interaktive Demo unten zeigt jede Kurve und die Punkte, mit denen sie verbunden ist. Klicken Sie auf eine beliebige Kurve oder einen Verbindungspunkt, um zu sehen, wie die Kurven der beiden Diagramme korrespondieren.
Es ist ersichtlich, dass alle Kurven durch kubische Bezier-Kurven erstellt werden. Auch wenn einige der Kurven zwei Kontrollpunkte haben, die sich überlappen.
Die Formen, aus denen der Stern und das Herz bestehen, sind minimalistisch und unrealistisch. Aber sie können es schaffen.
Wie Sie dem Beispiel für eine Ausdrucksanimation entnehmen können, verwende ich normalerweise Pug (Übersetzung: Jade, eine Vorlagen-Engine), um solche Formen zu generieren. Aber auch hier wird der Übergangseffekt aufgrund der generierten Pfaddaten von JavaScript übernommen. Einschließlich der Berechnung von Koordinaten und der Eingabe dieser Koordinaten in Attribute d
. Daher ist die Verwendung von JavaScript für all dies die beste Option.
Das bedeutet, dass wir nicht viele Tags schreiben müssen:
<svg> <path id='shape'/></svg>
In JavaScript erhalten wir zuerst das Element svg
und das Element path
. path
Es ist die Sternform, die sich in eine Herzform und dann wieder in eine Sternform verwandelt. Dann legen wir das Attribut svg
auf das Element viewBox
fest, sodass die Abmessungen des SVG entlang der beiden Achsen gleich sind und der Ursprung der Koordinatenachse (0,0)
in der Mitte des SVG liegt. Das heißt, wenn der Bemaßungswert von viewBox
D
ist, ist seine obere linke Eckkoordinate (-.5*D,-.5*D)
. Zu guter Letzt erstellen Sie ein Objekt zum Speichern des Anfangs- und Endzustands des Übergangs sowie eine Methode zum Festlegen der gewünschten Werte für die SVG-Grafikeigenschaften.
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(' ')); })();
Da wir das nun geklärt haben, können wir mit dem unterhaltsameren Teil beginnen!
Wir verwenden die Anfangskoordinaten des Endpunkts und der Kontrollpunkte, um den Stern zu zeichnen, und ihre Endkoordinaten, um das Herz zu zeichnen. Der Übergangsbereich jeder Koordinate ist die Differenz zwischen ihrem Anfangswert und ihrem Endwert. In diesem Beispiel drehen wir (rotate
) den Stern, während er sich in ein Herz verwandelt, weil wir möchten, dass die Ecken des Sterns nach oben zeigen. Wir werden auch die Füllung (fill
) von einem goldenen Stern in ein purpurrotes Herz ändern.
Wie können wir also die Koordinaten der Endpunkte und Kontrollpunkte dieser beiden Figuren erhalten?
Im Sternbeispiel beginnen wir mit einem regelmäßigen Pentagramm. Der Endpunkt unserer Kurve (Übersetzung: die Kurve, die jede Ecke des Sterns bildet) fällt auf den Schnittpunkt der Seiten des regelmäßigen Pentagramms, und wir verwenden den Scheitelpunkt des regelmäßigen Pentagramms als Kontrollpunkt.
Die Endpunkte und Kontrollpunkte der fünf kubischen Bezier-Kurven sind mit gelben Punkten an den Schnittpunkten der Scheitelpunkte und Seiten des regelmäßigen Pentagramms (Live) markiert .
Der Scheitelpunkt des Pentagramms kann durch direkte Angabe des Radius (oder Durchmessers) des umschriebenen Kreises des regelmäßigen Pentagramms ermittelt werden. Das ist die Größe, die wir für die SVGs viewBox
festgelegt haben (der Einfachheit halber berücksichtigen wir in diesem Fall keine hohe Polsterung). Aber wie bekommt man ihren Schnittpunkt?
Schauen wir uns zunächst die Abbildung unten an. Beachten Sie das hervorgehobene kleine Fünfeck in der Mitte des regelmäßigen Pentagramms im Bild. Die Eckpunkte des kleinen Fünfecks fallen mit den Schnittpunkten der Seiten des regelmäßigen Fünfecks zusammen. Dieses kleine Fünfeck ist offensichtlich ein regelmäßiges Fünfeck (Übersetzung: fünf gleich lange Seiten). Der eingeschriebene Kreis und der Innendurchmesser dieses kleinen regelmäßigen Fünfecks entsprechen denen des regelmäßigen Pentagramms.
Die eingeschriebenen Kreise des regelmäßigen Fünfecks und des inneren regelmäßigen Fünfecks sind gleich (Live).
Wenn wir also den Innendurchmesser des regelmäßigen Fünfecks berechnen, erhalten wir auch den Innendurchmesser des regelmäßigen Fünfecks. Dieser Innendurchmesser und der Mittelpunktswinkel entsprechen zusammen den Seiten des regelmäßigen Fünfecks. Auf dieser Grundlage können wir den Radius des Umkreises des regelmäßigen Fünfecks ermitteln. Auf diese Weise können die Koordinaten der Eckpunkte des regelmäßigen Fünfecks abgeleitet werden. Diese Punkte sind genau die Schnittpunkte der Seiten des regelmäßigen Pentagramms, die die Endpunkte der fünf kubischen Bézier-Kurven des Sterns sind.
Unser reguläres Pentagramm kann durch das topologische Symbol {5/2}
dargestellt werden. Mit anderen Worten: Das reguläre Pentagramm hat 5
Eckpunkte. Diese 5 Eckpunkte sind gleichmäßig auf dem umschriebenen Kreis verteilt und die Abstände sind 360°/5 = 72°
. Wir beginnen am ersten Punkt, überspringen den nächsten Punkt und verbinden uns mit dem zweiten Punkt (das ist die Bedeutung von {5/2}
im Symbol 2
; 1
bedeutet Verbindung). Gehen Sie zum ersten Punkt, ohne einen Punkt zu überspringen, um ihn zu bilden ein Fünfeck). Wenn Sie so weitermachen, können Sie eine regelmäßige fünfzackige Sternform zeichnen.
Klicken Sie in der Demo unten auf die Schaltfläche Fünfeck oder Pentagramm, um zu sehen, wie sie gezeichnet werden.
Auf diese Weise erhalten wir, dass der Zentralwinkel, der der Seite des regelmäßigen Fünfecks entspricht, doppelt so groß ist wie der Zentralwinkel, der der Seite des regelmäßigen Fünfecks entspricht. Dann ist das regelmäßige Fünfeck 1 * (360°/5) = 1 * 72° = 72°
(oder 1 * (2 * π / 5)
Bogenmaß) und das regelmäßige Fünfeck ist 2 * (360° / 5) = 2 * 72° = 144°
(2 * (2 * π / 5)
Bogenmaß). Normalerweise wird ein regelmäßiges Polygon durch das topologische Symbol {p,q}
dargestellt, wobei der einer seiner Seiten entsprechende Mittelpunktswinkel q * (360° / p)
(q * (2 * π / p)
Bogenmaß) beträgt.
Der Mittelpunktswinkel, der einer Seite eines regelmäßigen Polygons entspricht: reguläres Pentagramm (links, 144°
) vs. reguläres Fünfeck (rechts, „72°“) (Live).
Der Radius des Umkreises des regelmäßigen Pentagramms ist bekannt, was der viewBox
-Größe entspricht. Dann kennen wir die Länge der Hypotenuse des rechtwinkligen Dreiecks (d. h. den Radius des Umkreises des regelmäßigen Pentagramms) und den Grad des spitzen Winkels (der halbe Winkel, der einer Seite des regelmäßigen Pentagramms entspricht), was bedeutet dass wir den Innendurchmesser des regulären Pentagramms berechnen können (dieser Innendurchmesser ist gleich dem Innendurchmesser des kleinen regulären Fünfecks innerhalb des regulären Pentagramms).
Durch rechte Winkel kann der Innendurchmesser des regelmäßigen Pentagramms berechnet werden. Die Hypotenuse dieses rechten Winkels ist gleich dem Radius des Umkreises des regelmäßigen Pentagramms, und der Winkel eines der spitzen Winkel ist gleich der Hälfte des Winkels, der einer Seite des regelmäßigen Pentagramms (Live) entspricht.
Der Kosinus des halben Zentralwinkels ist gleich dem Verhältnis des Innendurchmessers des Pentagramms zum Radius des umschriebenen Kreises. Daraus lässt sich schließen, dass der Innendurchmesser des Pentagramms gleich dem Radius des umschriebenen Kreises multipliziert mit diesem Kosinuswert ist.
Da wir nun den Radius des eingeschriebenen Kreises des kleinen regelmäßigen Fünfecks innerhalb des regelmäßigen Pentagramms haben, können wir den Radius des Umkreises dieses regelmäßigen Fünfecks berechnen. Immer noch durch einen kleinen rechten Winkel berechnet. Die Hypotenuse dieses rechten Winkels ist gleich dem Radius des Umkreises des regelmäßigen Fünfecks. Ein spitzer Winkel ist gleich der Hälfte des Mittelpunktswinkels, der einer Seite eines regelmäßigen Fünfecks entspricht. Eine Seite dieses spitzen Winkels ist die Mittelgerade des Mittelwinkels, und diese Mittelgerade ist der Radius des Umkreises des regelmäßigen Fünfecks.
Die Abbildung unten zeigt ein rechtwinkliges Dreieck, das sich aus dem halben Radius eines Umkreises, dem Radius eines eingeschriebenen Kreises und einem Mittelpunktswinkel eines regelmäßigen Fünfecks zusammensetzt. Wenn wir den Radius des eingeschriebenen Kreises und den Mittelpunktswinkel kennen, der einer Seite des regelmäßigen Fünfecks entspricht, ist die Hälfte des Mittelpunktswinkels der Winkel zwischen den Radien der beiden umschriebenen Kreise. Mit diesem rechtwinkligen Dreieck können wir die Länge des Radius des umschriebenen Kreises berechnen.
Berechnen Sie den Radius des Umkreises eines regelmäßigen Fünfecks durch ein rechtwinkliges Dreieck (Live).
Wie bereits erwähnt, ist der Mittelpunktswinkel eines regelmäßigen Fünfecks nicht gleich dem Mittelpunktswinkel eines regelmäßigen Pentagramms. Ersteres ist die Hälfte (360° / 5 = 72°
) von Letzterem.
Okay, jetzt, da wir diesen Radius haben, können wir die Koordinaten aller gewünschten Punkte erhalten. Die Punkte sind gleichmäßig auf beide Kreise verteilt. Es gibt 5
Punkte auf dem äußeren Kreis (dem Umkreis des regelmäßigen Fünfecks) und 5
Punkte auf dem inneren Kreis (dem Umkreis des kleinen regelmäßigen Fünfecks). Es gibt insgesamt 10
Punkte, und der Winkel zwischen den Radiusstrahlen, an denen sie sich befinden, beträgt 360° / 10 = 36°
.
Die Endpunkte sind gleichmäßig auf dem umschriebenen Kreis des kleinen regelmäßigen Fünfecks verteilt, und die Kontrollpunkte sind gleichmäßig auf dem umschriebenen Kreis des regelmäßigen Pentagramms (Live) verteilt.
Die Radien der beiden Kreise sind bekannt. Der Radius des äußeren Kreises ist gleich dem Radius des umschriebenen Kreises des regelmäßigen Pentagramms, der Teil der viewBox
-Größe ist, die wir etwas willkürlich festlegen (.5
oder .25
oder .32
oder eine Größe, die wir denken wird besser funktionieren). Der Radius des inneren Kreises ist gleich dem Radius des Umkreises des kleinen regelmäßigen Fünfecks, das innerhalb des regelmäßigen Pentagramms gebildet wird. Die Methode zur Berechnung dieses Radius ist: Berechnen Sie zunächst den Radius des eingeschriebenen Kreises des regelmäßigen Pentagramms durch den Radius des Umkreises des regelmäßigen Pentagramms und den Mittelpunktswinkel, der einer seiner Seiten entspricht. Der Radius dieses eingeschriebenen Kreises ist gleich dem Radius des eingeschriebenen Kreises des kleinen regelmäßigen Fünfecks und wird dann aus dem Mittelpunktswinkel, der einer Seite des kleinen regelmäßigen Fünfecks entspricht, und dem Radius seines eingeschriebenen Kreises berechnet.
Auf dieser Grundlage können wir Daten generieren, um den Weg des Sterns zu zeichnen. Wir haben bereits die Daten, die wir zum Plotten benötigen.
Also lasst uns zeichnen! Und schreiben Sie den obigen Denkprozess in Code.
Erstellen Sie zunächst eine getStarPoints(f)
-Funktion. Der Parameter (f)
bestimmt den Radius des Umkreises des regelmäßigen Pentagramms, das basierend auf der Größe von viewBox
erhalten wird. Diese Funktion gibt ein Array von Koordinaten zurück, und wir fügen diesem Array dann Array-Elemente hinzu.
In dieser Funktion berechnen wir zunächst die Konstanten: den Radius des Umkreises des regelmäßigen Pentagramms (den Radius des äußeren Kreises), den Mittelpunktswinkel, der einer Seite des regelmäßigen Pentagramms entspricht, und den Innenwinkel Der zentrale Winkel, der einer Seite des regelmäßigen Fünfecks entspricht, der zentrale Winkel, der einer Seite des regelmäßigen Fünfecks entspricht, die innerhalb des regelmäßigen Fünfecks gebildet wird, und der eingeschriebene Kreis, der vom regelmäßigen Fünfeck und dem darin gebildeten regelmäßigen Fünfeck geteilt wird . Der Radius der regelmäßigen fünfzackigen Verformung (der Scheitelpunkt des regelmäßigen fünfzackigen Sterns ist der Schnittpunkt der Seiten des regelmäßigen fünfzackigen Sterns), der Radius des umschriebenen Kreises der inneren kleinen regelmäßigen fünfzackigen Verformung Gesamtzahl der Punkte, deren Koordinaten berechnet werden müssen, und der Winkel zwischen den Radiallinien, an denen sich alle Punkte befinden.
Anschließend berechnen Sie mithilfe einer Schleife die Koordinaten der gewünschten Punkte und fügen sie in das Koordinatenarray ein.
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)。
靠下的曲线控制点很明显已经得到了,就是两条切线的交点。但是另外四条曲线的控制点呢?我们怎么能把圆弧变成三次贝塞尔曲线呢?
Wir können die kubische Bezier-Kurve eines Viertelbogens nicht erhalten, aber wir können eine Näherung erhalten, wie in diesem Artikel erläutert.
In diesem Artikel erfahren wir, dass wir einen Radius mit dem Wert R
und die Tangenten des Radius (N
und Q
) verwenden können, um einen Viertelbogen zu zeichnen. Die Tangenten der beiden Radien schneiden sich im Punkt P
. Die vier Winkel des Vierecks ONPQ
sind gleich 90°
(oder π / 2
, von denen drei aus den Axiomen abgeleitet sind (O
ist 90°
, und der Winkel zwischen den beiden Tangenten und dem Radius ist ebenfalls 90°
), Der letzte wird berechnet (die Summe der Innenwinkel ist 360°
, die anderen drei Winkel sind 90°
und der letzte Winkel ist 90°
). Auf diese Weise ist ONPQ
a). Rechteck gleichzeitig. Zwei benachbarte Seiten sind gleich (die Längen von ONPQ
und OQ
sind beide gleich dem Radius ON
), es handelt sich also um ein Quadrat mit der Seitenlänge R
, also den Längen von R
und NP
sind gleich QP
Verwenden Sie die kubische Bezier-Kurve, um einen Bogen zu zeichnen, der einem Viertelbogen entspricht (Live). Die durch die Kurve gezeichneten ungefähren Viertelbögen liegen an den Tangenten R
und
Wenn wir das oben Gesagte kennen, können wir mit der Berechnung der Koordinaten des Endpunkts und des Kontrollpunkts der kubischen Bezier-Kurve beginnen. Mit diesen Koordinaten können wir unsere Herzform konstruieren. 🎜>Da wir uns für die Konstruktion entschieden haben Auf diese Weise ist die Herzform
ein Quadrat mit vier gleichen Seiten (alle vier Seiten bestehen aus den Radien zweier Kreise) und seine Diagonalen sind ebenfalls gleich (dies). Wie bereits erwähnt, ist die Linie, die die beiden Mittelpunkte verbindet, gleich Dabei ist der Schnittpunkt der beiden Diagonalen, und NP
entspricht der Hälfte der Diagonalen QP
und C * R
liegen auf dem C
Achse, also sind ihre .551915
-Koordinaten
-Koordinaten entsprechen dem absoluten Wert von
, der der Hälfte der Diagonale entspricht (. >Ähnlich). TO0SO1
QuadratO
(Live)OT
ST
T
Wir können jedes Quadrat mit Seitenlänge S
in zwei gleichschenklige Dreiecke schneiden. Die rechte Seite dieses gleichschenkligen Dreiecks fällt mit der Seite des Quadrats zusammen , und die Hypotenuse fällt mit der Diagonale des Quadrats zusammen y
x
0
Jedes Quadrat kann in zwei gleichschenklige Dreiecke geschnitten werden (Live) y
OT
Mit dem Satz des Pythagoras: OS
kann die Hypotenuse eines der rechten Winkel (also die Diagonale des Quadrats) anhand der Seitenlänge ermitteln aus der Länge der Diagonale
berechnen und diese auf unsere Seitenlänge anwenden Die TO0SO1
-Koordinate des -Punktes (sein absoluter Wert entspricht der Hälfte der Diagonale des Quadrats) ist
-Koordinate des l
-Punktes
Koordinaten (Live) der vier Eckpunkte des Quadrats
In ähnlicher Weise liegt der Punkt auf der d² = l² + l²
Achse. Ihre d = √(2 * l) = l * √2
Achsenkoordinate ist also l = d / √2
. und ihre d / 2 = (l * √2) / 2 = l / √2
Achsenkoordinate ist die Hälfte der Diagonale
. R
TO0SO1
T
ist ein Quadrat, dann sind seine vier Ecken y
(-R / √2
Bögen). S
y
R / √2
Quadrilateral
. TO0SO1
Wie im Bild oben gezeigt, ist die gerade Linie eine diagonale Linie, was bedeutet, dass der Bogen
Bogen bezeichnet. Wir teilen diesen Bogen mithilfe des O1
-Punkts in zwei gleiche Hälften, sodass zwei gleiche x
-Bögen entstehen: y
und 0
. Sie entsprechen zwei gleichen x
Winkeln: OO1
und ±R/√2
.
Nach den Axiomen sind TO0SO1
und 90°
beide Winkel von π / 2
, was beweist, dass die Gerade
die Diagonalen und TA1B1S
senkrecht und gleich sind und sich in ihren jeweiligen Mittelpunkten schneiden (,
und TB1
sind alle). Entspricht dem Radius des Kreises TB1
). Das bedeutet, dass das Viereck 180°
ein Quadrat ist und seine Diagonale gleich A1
ist. 90°
An diesem Punkt können wir feststellen, dass die Seiten des Vierecks TA1B1S
gleich 2 * R / √2 = R * √2
sind. Da alle Ecken eines Quadrats 90°
sind und die Seite TS
die vertikale Achse überlappt, sind die Seiten TA1
und SB1
horizontal und parallel zur x
-Achse. Anhand ihrer Längen können die A1
Achsenkoordinaten der beiden Punkte B1
und x
berechnet werden: ±R * √2
.
Da TA1
und SB1
horizontal sind, sind die A1
Achsenkoordinaten der beiden Punkte B1
und y
jeweils gleich den Punkten T (-R / √2)
und S (R / √2)
.
Quadrat TA1B1S
Vier Scheitelpunktkoordinaten (Live) .
Eine weitere Schlussfolgerung, die wir hier ziehen, ist, dass, weil TA1B1S
ein Quadrat ist, also A1B1
parallel zu TS
ist und weil TS
auf der y
(vertikalen) Achse liegt, also A1B1
Auch vertikal. Da außerdem die x
-Achse parallel zu TA1
und SB1
ist und TS
in zwei Hälften schneidet, schneidet die x
-Achse auch A1B1
in zwei Hälften.
Jetzt schaue ich mir die Kontrollpunkte an.
Wir beginnen mit den überlappenden Kontrollpunkten des untersten Bogens.
Quadrilateral TB0CB1
(Live).
Alle Winkel des Vierecks TB0CB1
sind gleich 90°
(weil TO0SO1
ein Quadrat ist, ist ∠T
ein rechter Winkel; weil B1C
eine Tangente an den Kreis ist, steht er senkrecht zum Radius O1B1
und schneidet sich im Punkt B1
, also ist ∠B1
ein rechter Winkel; da die anderen drei alle rechte Winkel sind, ist ∠C
ebenfalls ein rechter Winkel), also ist es ein Rechteck. Ebenso hat es zwei benachbarte Seiten, die gleich sind: TB0
und TB1
. Beide Linien sind Durchmesser von Kreisen und beide sind gleich 2 * R
. Abschließend kommt man zu dem Schluss, dass das Viereck TB0CB1
ein Quadrat mit der Seitenlänge 2 * R
ist.
Dann können wir seine Diagonale erhalten TC
: 2 * R * √2
. Da sich C
auf der y
-Achse befindet, ist seine x
-Achsenkoordinate 0
. Seine y
-Achsenkoordinate ist die Länge von OC
. Die Länge von OC
entspricht TC
minus OT
: 2 * R * √2 - R / √2 = 4 * R / √2 - R / √2 = 3 * R / √2
.
Quadrat TB0CB1
Koordinaten der vier Eckpunkte (Live).
Jetzt haben wir die Koordinaten der beiden überlappenden Kontrollpunkte des untersten Bogens als (0,3 * R / √2)
.
Um die Koordinaten anderer Kurvenkontrollpunkte zu erhalten, zeichnen wir Tangenten an ihren Endpunkten und erhalten die Schnittpunkte D1
und E1
dieser Tangenten.
Quadrilateral TO1A1D1
und A1O1B1E1
(Live).
Im Viereck TO1A1D1
sind alle Winkel bekanntermaßen rechte Winkel (90°
), von denen drei aus Axiomen abgeleitet sind (∠D1TO1
und ∠D1A1O1
werden aus Radien und Tangenten erhalten; ∠TO1A1
ist der Winkel, der einem Viertelbogen entspricht TA1
), dann ist der vierte Winkel durch Berechnung auch ein rechter Winkel. Dies beweist, dass TO1A1D1
ein Rechteck ist. Und weil es zwei benachbarte Seiten hat, die gleich sind (O1T
und O1A1
sind gleich dem Radius R
), ist TO1A1D1
ein Quadrat.
Das bedeutet, dass die Diagonalen TA1
und O1D1
gleich R * √2
sind. Es ist bekannt, dass TA1
horizontal ist und die beiden Diagonalen des Quadrats vertikal sind. Dies beweist, dass O1D1
vertikal ist. Dann sind die O1
-Achsenkoordinaten der Punkte D1
und x
gleich und die O1
-Achsenkoordinate von x
ist ±R / √2
. Da wir die Länge von O1D1
kennen, können wir die Koordinaten der y
-Achse berechnen, indem wir wie bereits erwähnt die Länge der Diagonale (R * √2
) subtrahieren. Ähnlich verhält es sich mit dem
Viereck A1O1B1E1
. Es ist bekannt, dass alle Winkel rechte Winkel (90°
) sind, von denen drei aus Axiomen abgeleitet sind (∠E1A1O1
und ∠E1B1O1
werden aus Radien und Tangenten erhalten; ∠A1O1B1
ist der entsprechende Viertelbogenwinkel A1B1
) , dann wird der vierte Winkel als rechter Winkel berechnet. Dies beweist, dass A1O1B1E1
ein Rechteck ist. Und weil es zwei benachbarte Seiten hat, die gleich sind (O1A1
und O1B1
sind gleich dem Radius R
), ist A1O1B1E1
ein Quadrat.
至此,我们得到对角线 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})();
现在我们得到了我们想要的最终结果:一个从金色星形变成深红色心形的形状,每次从一个状态到另一个状态顺时针旋转半圈。
相关推荐:
Das obige ist der detaillierte Inhalt vonSVG- und Vanilla JS-Framework zum Erstellen eines „sternförmigen herzförmigen' Code-Sharings. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!