Enganliegende SVG-Formen, die Gegenwart und Zukunft

Avatar of Ana Tudor
Ana Tudor am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Ich habe kürzlich festgestellt, dass ich eine SVG-Form eng in einen HTML-Container packen wollte. Mit eng packen meine ich, dass die äußersten Punkte meiner Form direkt an den Kanten des Containers liegen sollen, ohne dass etwas aus dem Container herausragt oder abgeschnitten wird.

Das folgende Demo zeigt ein Beispiel dafür

Siehe den Stift 4-zackiger Stern – eng in den Container gepackt von Ana Tudor (@thebabydino) auf CodePen.

Die äußersten Ecken dieses vierzackigen Sterns liegen an den Kanten seines Containers. Wir haben das SVG dazu gebracht, seine Eltern vollständig abzudecken, und wir haben das Koordinatensystem und die Koordinaten der Eckpunkte des Sterns so gewählt, dass der (0,0)-Punkt der SVG-Leinwand genau in der Mitte liegt und die äußersten Ecken des Sterns auf den Achsen dieses Koordinatensystems an den Kanten des Containers liegen. Das untenstehende Demo veranschaulicht, wie der Code in diesem Fall funktioniert

Siehe den Stift 4-zackiger Stern – visuelle Code-Erklärung von Ana Tudor (@thebabydino) auf CodePen.

Nehmen wir nun an, wir möchten keine Füllung, sondern eine Kontur variabler Stärke. Ziehen Sie den Schieberegler, um die stroke-width des Sterns anzupassen

Siehe den Stift 4-zackiger Stern – variable Kontur von Ana Tudor (@thebabydino) auf CodePen.

Wie das obige Demo zeigt, verursacht die Erhöhung der stroke-width ein Problem: Die Kontur dehnt sich sowohl innerhalb als auch außerhalb der ursprünglichen Grenzen des vierzackigen Sterns aus, wodurch er größer wird und nicht mehr in seinen Container passt. Es ist offensichtlich, dass wir entweder die Position der Eckpunkte anpassen oder das viewBox-Attribut unseres <svg>-Elements ändern müssen.

Lösung: Anpassen der viewBox

Das folgende Demo veranschaulicht, wie die Anpassung der viewBox funktionieren würde

Siehe den Stift 4-zackiger Stern – Anpassen der viewBox von Ana Tudor (@thebabydino) auf CodePen.

Durch Ziehen des Schiebereglers nach links werden die Breite und Höhe der viewBox verringert, wobei sie jederzeit gleich bleiben, und die Koordinaten der oberen linken Ecke der SVG-Leinwand werden so geändert, dass ihr (0, 0)-Punkt immer genau in der Mitte liegt. Dies ist wie ein "Zoom-In"-Effekt.

Durch Ziehen des Schiebereglers nach rechts werden die Breite und Höhe der viewBox erhöht, wobei sie jederzeit gleich bleiben, und die Koordinaten der oberen linken Ecke der SVG-Leinwand werden so geändert, dass ihr (0, 0)-Punkt immer genau in der Mitte liegt. Dies ist wie ein "Zoom-Out"-Effekt.

In unserem Fall müssen wir, um unseren vierzackigen Stern nach Erhöhung seiner stroke-width anzupassen, herauszoomen, also die Dimensionen der viewBox erhöhen. Aber um wie viel müssen wir sie erhöhen, damit der Stern gerade die Kanten berührt? Wenn wir nicht genug herauszoomen, wird unser Stern immer noch abgeschnitten. Wenn wir zu weit herauszoomen, wird er die Kanten nicht mehr berühren. Was sind also die viewBox-Dimensionen für die richtige Zoomstufe und wie bekommen wir sie?

Um dies herauszufinden, müssen wir einen genauen Wert festlegen, auf den wir die stroke-width erhöhen möchten – sagen wir 20. Wir werden uns nun genauer ansehen, was mit den äußeren Eckpunkten nach Erhöhung der stroke-width auf 20 passiert

In der obigen Abbildung haben wir unseren Stern zweimal dargestellt, sowohl vor als auch nach Erhöhung der stroke-width. Wir sehen, dass sich unser Stern nach Erhöhung der stroke-width in allen Richtungen um eine Distanz *d* über die ursprünglichen Grenzen hinaus erstreckt. Das bedeutet, dass wir die viewBox-Dimensionen um das Doppelte dieser Distanz *d* erhöhen sollten.

Gut, aber wie bekommen wir *d*? Nun, um dies zu tun, konzentrieren wir uns auf einen der äußeren Eckpunkte, sagen wir den Punkt bei (250,0).

Wir nennen diesen Punkt *P*. Wir betrachten auch einen anderen Punkt, *Q*, der der rechteste Punkt des Sterns ist, nachdem wir seine stroke-width erhöht haben. Er liegt auf der x-Achse, genau wie *P*, nur in einer Entfernung von *d* rechts von *P*. Wenn wir eine Senkrechte durch *P* auf eine der Kanten des Sterns ziehen, erhalten wir das rechtwinklige Dreieck *PQR*, wobei *PQ* gleich *d* und *PR* gleich der Hälfte der bekannten stroke-width ist, die wir angewendet haben.

In diesem rechtwinkligen Dreieck ist *PQ* die Hypotenuse, und der Sinus des Winkels *α* ist das Verhältnis zwischen *PR* und *PQ*. *PR* ist gleich der Hälfte unserer bekannten stroke-width (20/2 = 10), während *PQ* gleich *d* ist. Also erhalten wir von hier aus, dass *d* gleich dem Verhältnis zwischen der Hälfte der stroke-width (10) und dem Sinus des Winkels *α* ist.

Aber es scheint, als hätten wir gerade eine Unbekannte durch eine andere ersetzt, da wir *α* nicht kennen. Wir können es jedoch berechnen.

Unser Stern ist symmetrisch bezüglich der Koordinatenachsen, was bedeutet, dass jede der beiden Achsen unseren Stern in zwei perfekt gespiegelte Hälften teilt. Zusätzlich, wenn eine Transversale zwei oder mehr parallele Linien schneidet, sind die entsprechenden Winkel (die Winkel, die an jeder Schnittstelle dieselbe Position einnehmen) gleich, wie das folgende Demo zeigt

Siehe den Stift entsprechende Winkel von Ana Tudor (@thebabydino) auf CodePen.

All dies bedeutet, dass in der folgenden Abbildung alle markierten Winkel gleich sind. Die oberhalb der x-Achse liegenden sind jeweils gleich denen direkt darunter, da der Stern symmetrisch ist. Alle auf einer Seite der Achse liegenden sind wiederum gleich, da die gepunkteten Linien auf derselben Seite der Achse parallel sind (und die Transversale die x-Achse ist).

Da all diese Winkel gleich sind, können wir *α* aus einem anderen rechtwinkligen Dreieck berechnen – dem, das aus dem Punkt *P*, dem nächsten Eckpunkt unseres Sterns (den wir *S* nennen werden) und seiner Projektion auf die x-Achse (den wir *T* nennen werden) gebildet wird. Die Projektion eines Punktes auf eine Linie wird am Schnittpunkt zwischen der Linie und einer durch den Punkt gezogenen Senkrechten erhalten.

Wir kennen die Koordinaten dieser drei Punkte. Aus dem Code, den wir zur Erstellung des Sterns verwendet haben, liegt Punkt *P* bei (250,0) und Punkt S bei (20,20).

Punkt *T*, die Projektion von Punkt *S* auf die x-Achse, hat dieselbe x-Koordinate wie *S*, während seine y-Koordinate 0 ist. Das bedeutet, dass *T* bei (20,0) liegt.

Mit diesen Koordinaten können wir die Längen der Segmente *ST* und *PT* berechnen. Die Länge des vertikalen Segments *ST* beträgt 20, während die Länge des horizontalen Segments *PT* 250 - 20 = 230 beträgt.

Das Dreieck *PST* ist ein rechtwinkliges Dreieck, also ist der Tangens von *α* das Verhältnis zwischen den Segmenten *ST* und *PT*. Beide Segmente sind bekannt, was bedeutet, dass wir *α* als Arkustangens ihres Verhältnisses berechnen können: atan(20/230).

Nachdem wir nun *α* berechnet haben, können wir es in die Beziehung einsetzen, die uns die gesuchte Distanz *d* liefert: 10/sin(atan(20/230)) ≈ 115 (übrigens, ich habe das Ergebnis einfach gegoogelt). Das bedeutet, dass die Koordinaten des oberen linken Punktes (-250 - 115,-250 - 115) = (-365, -365) sind und die Breite und Höhe der viewBox 500 + 2*115 = 500 + 230 = 730 betragen. In diesem Fall wird unser Code nach Änderung der viewBox (und unveränderter Beibehaltung aller anderen Elemente) wie folgt:

<svg viewBox='-365 -365 730 730'>
  <polygon points='250,0 20,20 0,250 -20,20 -250,0 -20,-20 0,-250 20,-20'/>
</svg>

Das Ergebnis sehen Sie im folgenden Stift

Siehe den Stift 4-zackiger Stern – viewBox-Korrektur angewendet von Ana Tudor (@thebabydino) auf CodePen.

Probleme

Sie haben wahrscheinlich bemerkt, dass das obige Demo nicht richtig aussieht. Der Stern sieht immer noch abgeschnitten aus, auch wenn er nun in den sichtbaren Teil der SVG-Leinwand passt. Warum passiert das und wie können wir es beheben?

stroke-linejoin

Nun, wann immer wir eine Form haben, die aus verschiedenen Segmenten besteht, die an Ecken aufeinandertreffen, haben wir einige Optionen, wie das aussehen wird. Diese Optionen werden durch eine Eigenschaft namens stroke-linejoin gesteuert. Diese Eigenschaft kann einen der folgenden vier Werte annehmen

  • miter: (Standard) – die Segmente treffen sich in einem scharfen Winkel
  • round: die Ecke ist abgerundet
  • bevel: sieht etwas aus wie miter, außer dass der Scheitelpunkt abgeschnitten ist
  • inherit: welcher Wert auch immer für seine Elterngruppe verwendet wurde

Dieser Stift bietet einige praktische Beispiele

Siehe den Stift stroke-linejoin-Werte von Ana Tudor (@thebabydino) auf CodePen.

Wenn wir uns nun den obigen Stift genau ansehen, werden wir etwas Interessantes an der letzten Verbindung in der ersten Spalte bemerken. Sie hat stroke-linejoin: miter, aber das visuelle Ergebnis sieht anders aus als bei den anderen Verbindungen in der ersten Spalte (der miter-Spalte). Tatsächlich sieht sie genau so aus wie das Ergebnis, das wir für die letzte Verbindung in der letzten Spalte (der bevel-Spalte) erhalten. Und sie sieht genau so aus wie das Problem, das wir zu diesem Zeitpunkt mit dem Stern haben. Unsere Linienverbindung sollte ein miter sein, da wir stroke-linejoin nicht auf etwas anderes gesetzt haben. Aber was wir sehen, ist ein bevel.

stroke-miterlimit

Das liegt an einer Eigenschaft namens stroke-miterlimit. Wie der Name schon sagt, setzt diese Eigenschaft eine Grenze dafür, wie weit der Miter reichen kann. Wenn diese Grenze überschritten wird, wird die Verbindung einfach von einem Miter in einen Abschrägung umgewandelt.

Hinweis: SVG2 gibt vor, dass line-join einige weitere mögliche Werte erhalten wird, darunter miter-clip, welches bewirkt, dass die Verbindung an der Grenzmarke und nicht abgeschrägt wird.

Das folgende Demo zeigt, dass der Winkel zwischen zwei Segmenten kleiner wird, je mehr der miter normalerweise hervorsteht, *wenn* der gesetzte stroke-miterlimit hoch genug wäre, sodass er niemals in einen bevel umgewandelt wird. Es zeigt auch, wie der Winkel, bei dem der miter in einen bevel umgewandelt wird, mit steigendem Wert von stroke-miterlimit abnimmt

Siehe den Stift stroke-miterlimit von Ana Tudor (@thebabydino) auf CodePen.

Gut, aber was ist die Verbindung zwischen dem stroke-miterlimit und der tatsächlichen Miter-Länge? Im vorherigen Demo reichen die Werte des stroke-miterlimit von 2 bis 15, während die tatsächlichen Miter-Längen über 50 liegen und leicht über Hunderte gehen können. Die Miter-Länge hängt auch von der stroke-width ab, daher ist es ziemlich offensichtlich, dass das auferlegte Limit nicht auf der tatsächlichen Miter-Länge selbst liegt. Stattdessen liegt dieses Limit auf dem Verhältnis zwischen der Miter-Länge und der Strichstärke. Und wie die folgende Abbildung zeigt, ist dieses Verhältnis gleich 1 geteilt durch den Sinus der Hälfte des Winkels zwischen den verbundenen Linien.

Der Sinus eines spitzen Winkels (< 90°) steigt mit zunehmendem Winkel, sodass 1 geteilt durch den Sinus (das Verhältnis, auf das wir das durch stroke-miterlimit gesetzte Limit auferlegen) mit abnehmendem Winkel steigt.

Siehe den Stift
Wie sich sin(θ) und 1/sin(θ) mit θ ändern
von Ana Tudor (@thebabydino) auf CodePen.

Das erklärt irgendwie, warum die Verbindung von miter zu bevel wechselt. Wenn der Winkel *wirklich* nahe an 0 heranreicht, nähert sich auch sein Sinus 0 an, was den Kehrwert dieses Wertes zu einer sehr großen Zahl macht und dazu führt, dass der Miter stark hervorsteht. Und obwohl ich keine wirklichen Metriken dazu habe, denke ich, dass es für die Leistung nicht gut sein kann.

Behebung des Stern-Demos

Zurück zu unserem Stern: Seine Winkel liegen nicht so nahe bei Null, und alles, was wir tun müssen, um zu verhindern, dass die Verbindungen in Abschrägungen umgewandelt werden, ist, stroke-miterlimit auf 1 geteilt durch den Sinus des zuvor berechneten *α*-Winkels zu setzen. *α* war atan(20/230), also ist der gewünschte Wert 1/sin(atan(20/230)) ≈ 11,5, und wir setzen stroke-miterlimit: 12, um sicherzugehen. Der folgende Stift zeigt, wie das Hinzufügen dieser einen Zeile zu CSS alles behebt.

Siehe den Stift 4-zackiger Stern – viewBox & stroke-miterlimit Korrekturen angewendet von Ana Tudor (@thebabydino) auf CodePen.

Hinweis: Wenn wir uns nicht die Mühe machen wollen, all diese Berechnungen durchzuführen, können wir einfach stroke-miterlimit auf einen lächerlich hohen Wert setzen. Wie hoch? Nun, 60 reichen für einen Winkel zwischen den Linien, die verbunden werden, und es ist ziemlich unwahrscheinlich, dass wir mehr als das oft benötigen.

Probleme

Gut, aber diese Methode, den Stern wieder in seinen Container einzupassen, indem die viewBox geändert und somit herausgezoomt wird, hat auch dazu geführt, dass die Kontur dünner aussieht, als wir sie ursprünglich beabsichtigt hatten. Wir könnten versuchen, dies mit vector-effect: non-scaling-stroke zu beheben, aber das schafft nur mehr Probleme, da die Konturstärke nicht mehr skaliert, wenn sich der Container des SVG und damit das SVG selbst in der Größe ändern.

Wenn wir dies also lösen wollen, müssen wir die viewBox unverändert lassen und nur die Koordinaten der Eckpunkte unseres Sterns anpassen.

Lösung: Anpassen der Koordinaten der Eckpunkte

Wir wissen, um wie viel der Stern nach außen expandiert, wenn wir stroke-width: 20 setzen. Wir haben diese Distanz *d* zu etwa 115 berechnet. Das bedeutet, wir müssen die äußersten Eckpunkte um 115 nach innen verschieben, von 250 entlang der Achsen zu 250 - 115 = 135. Wir können nicht nur die Koordinaten unserer äußersten Eckpunkte ändern, da dies unseren Stern verzerren würde und gleichzeitig die Winkel ändern würde, wie stark der Miter hervorsteht, und der Stern wäre sowieso nicht mehr eng gepackt.

Siehe den Stift 4-zackiger Stern – Anpassung der äußeren Eckpunkte von Ana Tudor (@thebabydino) auf CodePen.

Stattdessen müssen wir zuerst einen Skalierungsfaktor berechnen, der das Verhältnis zwischen der neuen Nicht-Null-Koordinate eines äußersten Eckpunkts und der alten ist: 135/250 = .54. Dann multiplizieren wir die alten Koordinaten der anderen Eckpunkte mit diesem Faktor, um ihre neuen Koordinaten zu erhalten (20*.54 = 10.8).

Siehe den Stift 4-zackiger Stern – Anpassung aller Eckpunkte von Ana Tudor (@thebabydino) auf CodePen.

Und… das war's! Das endgültige Demo sehen Sie hier

Siehe den Stift 4-zackiger Stern – Eckpunktkorrekturen angewendet von Ana Tudor (@thebabydino) auf CodePen.

Zukünftige Lösung: stroke-alignment

Natürlich wird die Zukunft viele gute Dinge bringen, darunter auch die Vermeidung all der Mühen, die diese Berechnungen für einige verursachen können. SVG2 wird es uns ermöglichen, eine stroke-alignment anzugeben – richtig, die Kontrolle darüber, ob die Erhöhung der stroke-width die Kontur nach innen, nach außen oder auf beiden Seiten der Umrisslinie des Elements erweitern lässt, wird mit nur einer Codezeile möglich sein.

In unserem Fall wäre das stroke-alignment: inner. Und *puff*! Keine anderen Tricks nötig, kein Zoomen durch Anpassen der viewBox, kein Neupositionieren der Punkte unserer Form… nichts davon mehr.

Aber in der Zwischenzeit sehen wir uns an, wofür ich das verwendet habe.

Anwendungsfall: 3D!

Vor über zwei Jahren begann ich mit CSS 3D-Transformationen zu spielen, um verschiedene 3D-Formen zu bauen. Die nicht-rechteckigen flachen Flächen (mit drei, fünf, sechs oder zehn Kanten) wurden durch Verschachtelung von 2D-transformierten HTML-Elementen und Ausschneiden unerwünschter Teile mit overflow: hidden erstellt. In den letzten Monaten habe ich auch mit SVG begonnen zu experimentieren und darüber nachgedacht, diese 3D-Formen neu zu erstellen, diesmal unter Verwendung eines SVG <polygon> für die nicht-rechteckigen Flächen.

Hinweis: Wenn Sie eine grundlegende Auffrischung der Geometrie benötigen, erklärt dieser Stift, was ein Polygon ist und gibt einige Beispiele.

Hier stieß ich auf das oben genannte Problem, denn wie stark ich die 2D-Flächen in 3D bewege, hängt von ihren 2D-Dimensionen ab. Die einfachste Lösung dafür war für mich, den Umkreis (den Kreis, auf dem alle Eckpunkte liegen) meiner regelmäßigen Polygone genau in der Mitte des SVG zu haben und mindestens einen seiner Eckpunkte direkt an der Kante. Da das SVG-Element die gleichen Abmessungen wie sein HTML-Container hätte, würde dies bedeuten, dass die halbe Abmessung des HTML-Containers dem Umkreisradius des darin enthaltenen SVG-Polygons entspricht.

Beachten Sie, dass nicht jedes Polygon einen Umkreis hat (aber alle regelmäßigen Polygone und alle Dreiecke schon).

Schauen wir uns also an, wie ich das alles gelöst habe, indem ich die einfachsten 3D-Formen – Prismen – erstellt habe! Prismen sind Polyeder (3D-Körper mit flachen Flächen und geraden Kanten) mit zwei n-polygonalen Grundflächen, die durch n viereckige Flächen verbunden sind. Der Einfachheit halber betrachten wir alle Seitenflächen als rechteckig und die Grundpolygone als regelmäßig (alle Winkel sind gleich und alle Kantenlängen sind gleich). Das folgende Demo ermöglicht das Erstellen und Abwickeln einer Anzahl von Prismen.

Siehe den Stift ein Prisma bauen von Ana Tudor (@thebabydino) auf CodePen.

Die rechteckigen Flächen sind leicht zu bekommen. HTML-Elemente sind standardmäßig Rechtecke, also verwenden wir für jede rechteckige Fläche ein Element. Aber was ist mit den Grundflächen?

Nun, für jede Grundfläche nehmen wir ein quadratisches (gleiche width und height, vorzugsweise in vmin-Einheiten, damit sie sich mit dem Viewport skalieren) HTML-Element mit einem SVG darin. Das <svg>-Element bedeckt seinen Container vollständig (width und height sind beide auf 100% gesetzt), und das polygon darin berührt die Kanten, aber nichts wird abgeschnitten. Da die beiden Grundflächen identisch sind, definieren wir das polygon nur einmal und verweisen dann für jede Grundfläche darauf.

<svg height='0' width='0'>
  <defs>
    <polygon id='basepoly'/>
  </defs>
</svg>

<div class='prism'>
  <div class='prism__face--base'>
    <svg>
      <use xlink:href='#basepoly'/>
    </svg>
  </div>
  <div class='prism__face--base'>
    <svg>
      <use xlink:href='#basepoly'/>
    </svg>
  </div>

  <div class='prism__face--lateral'></div>
  <div class='prism__face--lateral'></div>
  <div class='prism__face--lateral'></div>

  
</div>

In diesem Fall würden wir jedoch die gleiche viewBox auf beiden SVG-Elementen innerhalb der Grundflächen setzen, so dass eine kleine Verbesserung gegenüber dieser Version die Verwendung von <symbol> wäre, so dass wir die viewBox nur an einer Stelle setzen müssen (auf dem <symbol>-Element).

<svg height='0' width='0'>
  <symbol id='basepoly'>
    <polygon/>
  </symbol>
</svg>

<div class='prism'>
  <div class='prism__face--base'>
    <svg>
      <use xlink:href='#basepoly'/>
    </svg>
  </div>
  <div class='prism__face--base'>
    <svg>
      <use xlink:href='#basepoly'/>
    </svg>
  </div>

  <div class='prism__face--lateral'></div>
  <div class='prism__face--lateral'></div>
  <div class='prism__face--lateral'></div>

  <!-- more lateral faces if needed -->
</div>

Die grundlegende CSS würde alle geometrischen Elemente absolut in der Mitte ihres Containers positionieren. Wir machen den Elternteil unserer 3D-Form, in unserem Fall das <body>-Element, zur Szene, indem wir ihm eine perspective geben. Da das Prisma absolut positioniert ist, müssen wir auch eine height für den <body> festlegen, und da wir keine anderen Inhalte haben, lassen wir den Body den gesamten Viewport abdecken. Wir geben den Flächenelementen auch explizite Breite und Höhe und stellen sicher, dass sie genau in der Mitte liegen, indem wir negative Ränder verwenden. Und da wir das Prisma selbst in 3D animieren möchten, setzen wir transform-style: preserve-3d darauf.

body {
  height: 100vh;
  perspective: 32em;
}

[class*=prism] {
  position: absolute;
  top: 50%; left: 50%;
}

.prism { transform-style: preserve-3d; }

.prism__face--base {
  margin: -13vmin;
  width: 26vmin; height: 26vmin;
}

.prism__face--base svg {
  width: 100%;
  height: 100%;
}

.prism__face--lateral {
  margin: -16vmin -13vmin;
  width: 26vmin; height: 32vmin;
}

Nun sehen wir uns an, wie wir unser eng anliegendes regelmäßiges Polygon erhalten können. Wir gehen von der Tatsache aus, dass alle Eckpunkte eines regelmäßigen Polygons auf einem Kreis liegen, dem Umkreis. Aber wie genau positionieren wir die Eckpunkte auf diesem Kreis?

Zuerst müssen wir wissen, wie ein Kreis aussieht. Ein voller Kreis hat 360°, und wir beginnen von der +Seite der x-Achse (3 Uhr).

Siehe den Stift voller Kreis – responsive SVG-Erklärung von Ana Tudor (@thebabydino) auf CodePen.

Wenn unser Polygon n gleich lange Kanten hat, dann hat der Bogen, der einer Kante entspricht, die Anzahl der Grade eines vollen Kreises (360°) geteilt durch n. Im Fall eines gleichseitigen Dreiecks ist n 3, also beträgt der Winkel 120°. Für ein Quadrat ist n 4, also beträgt der Winkel 90°. Für ein regelmäßiges Fünfeck ist n 5, also beträgt der Winkel 72°.

Wenn wir nun den Kreis durchlaufen, beginnend bei , in n Schritten, dann haben wir bei jedem Schritt einen Eckpunkt des Polygons, wie in der folgenden Demo veranschaulicht.

Siehe den Stift regelmäßiges Polygon konstruieren von Ana Tudor (@thebabydino) auf CodePen.

Für ein Dreieck ist n 3, also ist jeder Schritt 360°/3 = 120°. Dies platziert die drei Eckpunkte unseres gleichseitigen Dreiecks bei , 120° und 240°. Für ein Quadrat ist n 4, wodurch jeder Schritt 360°/4 = 90° beträgt und die vier Eckpunkte bei , 90°, 180° und 270° liegen. Für ein regelmäßiges Fünfeck ist n 5, der Winkelschritt beträgt 360°/5 = 72° und die Eckpunkte liegen bei , 72°, 144°, 216° und 288°.

Nun benötigen wir die Koordinaten dieser Eckpunkte, um unser Polygon zu zeichnen. Die Position eines Punktes in einer Ebene wird durch die Länge des Segments bestimmt, das den Punkt mit dem Ursprung verbindet (in unserem Fall der Radius r unseres Kreises) und den Winkel zwischen diesem Segment und der x-Achse (in unserem Fall i mal der Winkelschritt, wobei i der Index des Eckpunkts ist).

xi = r*cos(i*360°/n);
yi = r*sin(i*360°/n);

Wenn wir die Anzahl der Eckpunkte/Kanten kennen, ist es einfach, die Winkel zu berechnen, aber wie groß ist der Radius? Nun, ohne die stroke-width unseres <polygon>-Elements zu berücksichtigen, wäre dies die halbe Abmessung unserer quadratischen viewBox (die wir, sagen wir mal, auf 800 setzen). Die Anpassung an die stroke-width bedeutet, dass wir die Hälfte der Miterlänge von diesem Wert subtrahieren müssen, genau wie wir es zuvor für den vierzackigen Stern getan haben.

Um dies zu tun, müssen wir den Winkel unseres regelmäßigen Polygons berechnen, da die Miterlänge von der stroke-width und dem Polygonwinkel abhängt. Wir wissen, dass die Winkel in einem Dreieck immer 180° ergeben (Sie können die Eckpunkte ziehen, um mit dem Dreieck im Demo zu spielen).

Siehe den Stift die Winkel eines Dreiecks ergeben 180° von Ana Tudor (@thebabydino) auf CodePen.

Wenn wir dies wissen und wissen, dass wir drei Winkel in einem Dreieck haben, erhalten wir, dass der Winkel eines regelmäßigen (oder gleichseitigen) Dreiecks 60° beträgt. Aber was ist mit anderen regelmäßigen Polygonen? Nun, wenn wir den ersten Eckpunkt unseres Polygons mit allen anderen Eckpunkten verbinden, sehen wir, dass wir unser Polygon in n - 2 Dreiecke geteilt haben.

Siehe den Stift jedes konvexe n-Polygon kann in n-2 Dreiecke zerlegt werden von Ana Tudor (@thebabydino) auf CodePen.

Das bedeutet, dass die Winkel in einem Polygon mit n Eckpunkten/Kanten sich zu (n - 2)*180° summieren. Da alle n Winkel dieses regelmäßigen Polygons gleich sind, erhalten wir, dass jeder Winkel (n - 2)*180°/n beträgt. Wenn wir die Berechnungen durchführen, stellen wir fest, dass wir 60° für ein gleichseitiges Dreieck, 90° für ein Quadrat, 108° für ein regelmäßiges Fünfeck, 120° für ein regelmäßiges Sechseck usw. haben.

Wir haben nun alle Daten, die wir für das Setzen des points-Attributs unseres Basispolygons benötigen.

var r = 400 /* circumradius, no correction, half of 800 */, 
    sw = 20 /* stroke-width */, 
    n = 3 /* number of edges */, 

    sym = document.getElementById('basepoly'), 
    poly = sym.querySelector('polygon'), 
    vb = [-r, -r, 2*r, 2*r], 
    points_attr_text = '', 

    base_angle = 2*Math.PI/n, 
    poly_angle = (n - 2)*Math.PI/n, 
    correction = .5*sw/Math.sin(.5*poly_angle), 
    r_final = r - correction;

/* set the viewBox */
/* "viewBox", not "viewbox" (won't work) */
sym.setAttribute('viewBox', vb.join(' '));

for(var i = 0; i < n; i++) {
    curr_angle = i*base_angle;
    x = r_final*Math.cos(curr_angle);
    y = r_final*Math.sin(curr_angle);

    points_attr_text += x + ',' + y + ' ';
}

poly.setAttribute('points', points_attr_text);
poly.setAttribute('stroke-width', sw);

Sie können diesen Code im folgenden Stift sehen. Sie können die Werte für die Anzahl der Kanten/Eckpunkte, den Umkreisradius oder die stroke-width ändern, um unterschiedliche Ergebnisse zu erzielen. Wir könnten diese Polygone durch verschiedene Methoden noch enger packen, aber dann würden wir den Vorteil einer einfachen und konsistenten Methode für jede Anzahl von Kanten/Eckpunkten verlieren.

Siehe den Stift n-Polygon, das die Kanten des SVG-Containers berührt von Ana Tudor (@thebabydino) auf CodePen.

Alternativ könnten wir exakt dasselbe Ergebnis mit Jade erzielen.

- var n = 3;
- var sw = 20;
- var r = 400;

- var base_angle = 2*Math.PI/n;
- var poly_angle = (n - 2)*Math.PI/n;
- var correction = .5*sw/Math.sin(.5*poly_angle);
- var r_final = r - correction;

mixin polygon(n)
    - var points = '';
    - for(var i = 0; i < n; i++) {
        - var curr_angle = i*base_angle;
        - var x = r_final*Math.cos(curr_angle)
        - var y = r_final*Math.sin(curr_angle);
        - points += ~~x + ',' + ~~y + ' ';
    - }
    polygon(points=points stroke-width='#{sw}')

svg(width='0' height='0')
    symbol(id='basepoly' 
                 viewbox='#{-r} #{-r} #{2*r} #{2*r}')
        +polygon(n)

svg(class='vis')
    use(xlink:href='#basepoly')

Der nächste Schritt ist die Kombination mit dem ursprünglichen Code und die automatische Generierung der Seitenflächen, während wir das Polygon erstellen. Das Ergebnis sehen Sie im folgenden Stift (oder, wenn Sie es lieber auf die Jade-Art machen möchten, schauen Sie sich diese Version an).

Siehe den Stift Prismaflächen generieren (JS) von Ana Tudor (@thebabydino) auf CodePen.

Gut, aber zu diesem Zeitpunkt sind alle unsere Flächen einfach übereinander gestapelt in der Mitte des Bildschirms, so dass wir sie in 3D positionieren müssen, damit sie ein Prisma bilden.

Beginnen wir mit den Grundflächen. Derzeit befinden sie sich in der vertikalen Ebene des Bildschirms, und wir möchten, dass sie sich in einer horizontalen Ebene befinden, die senkrecht zum Bildschirm steht; eine nach oben und die andere nach unten gerichtet. Um dies zu tun, wenden wir eine rotateX(90deg)-Transformation für diejenige an, die nach oben zeigen soll, und eine rotateX(-90deg)-Transformation für diejenige, die nach unten zeigen soll. Das Anwenden einer Drehung auf ein Element dreht auch sein Koordinatensystem, so dass die z-Achse, die ursprünglich aus dem Bildschirm zu uns herausragte, nun nach oben bzw. unten für die beiden gedrehten Flächen zeigt. Diese Flächen sind nun horizontal, aber sie schneiden die vertikalen Seitenflächen in der Mitte. Also müssen wir sie vertikal (eine nach oben und die andere nach unten) um die halbe Höhe der Seitenflächen (die wir in diesem Fall auf 32vmin gesetzt haben, also die Hälfte davon ist 16vmin) verschieben. Das vertikale Verschieben nach der Drehung bedeutet eine Verschiebung entlang der z-Achse (die durch die Drehung vertikal geworden ist), also müssen wir eine translateY-Transformation anwenden. Der Code dafür lautet:

.prism__face--base:nth-child(1) {
  transform: rotateX(90deg) translateZ(16vmin); /* up */
}
.prism__face--base:nth-child(2) {
  transform: rotateX(-90deg) translateZ(16vmin); /* down */
}

Nachdem diese beiden Regelsets hinzugefügt wurden, sind die beiden Grundflächen nun an ihrem Platz.

Siehe den Stift Prisma-Grundflächen positionieren (JS) von Ana Tudor (@thebabydino) auf CodePen.

Das Positionieren der Seitenflächen bedeutet, dass wir sie zuerst um ihre y-Achse drehen (mit einer rotateY-Transformation), so dass jede einer Kante der Grundflächen zugewandt ist, und sie dann nach außen verschieben (mit einer translateZ()) um den Inradius. Aber was zum Teufel ist der Inradius? Nun, es ist der Radius eines Kreises (genannt Inkreis), der jede Kante eines Polygons an genau einem Punkt berührt (jede Kante ist Tangente an den Inkreis). Genau wie bei dem Umkreis haben nicht alle Polygone einen Inkreis, aber alle regelmäßigen, wie die, mit denen wir es im Fall unserer Prisma-Grundflächen zu tun haben, schon.

Darüber hinaus sind bei regelmäßigen Polygonen der Umkreis und der Inkreis im selben Punkt zentriert, während der Umkreisradius und der Inkreisradius die Winkel und die entsprechenden Kanten des Polygons in zwei gleiche Hälften teilen. Wenn wir das alles grafisch darstellen, erhalten wir etwas Ähnliches wie in diesem Pen.

Siehe den Pen in/circumcircle of a regular polygon – WORK IN PROGRESS (SVG version) von Ana Tudor (@thebabydino) auf CodePen.

Hierbei sind die Umkreisradien die Segmente, die den zentralen Punkt mit den Eckpunkten des Polygons verbinden, und die Inkreisradien sind die Segmente, die den zentralen Punkt mit den Mittelpunkten der Kanten des Polygons verbinden. Da die Kanten des Polygons tangential zum Inkreis sind, sind die im Demo gezeichneten Inkreisradien senkrecht zu den Kanten des Polygons.

Wenn wir ein Dreieck nehmen, das aus einem Inkreisradius, einem Umkreisradius und der Hälfte einer Polygonkante gebildet wird, ist dieses Dreieck ein rechtwinkliges Dreieck und ermöglicht es uns, den Inkreisradius relativ zum Umkreisradius und zum Winkel des Polygons zu berechnen.

Siehe den Pen relation betwen circumradius, inradius, edge length, polygon angle von Ana Tudor (@thebabydino) auf CodePen.

Im obigen Demo ist dies das Dreieck OVM. OV ist der Umkreisradius, OM ist der Inkreisradius und VM ist die Hälfte der Kante des regelmäßigen Polygons. Der Winkel VMO ist ein rechter Winkel (wodurch die gegenüberliegende Kante, OV, die Hypotenuse ist) und der Winkel OVM ist die Hälfte des Winkels des Polygons. Der Sinus des Winkels OVM ist das Verhältnis zwischen OM und OV, und wir werden diese Beziehung verwenden, um OM zu berechnen, da wir OV und den Winkel OVM bereits kennen.

Der Umkreisradius ist die halbe Dimension der Grundfläche, die wir in CSS eingestellt haben, und wir haben bereits den Winkel des Polygons, also ist dies alles, was wir für den Wert des Inkreisradius benötigen.

base_face = prism.querySelector('.prism__face--base');
cradius = .5*getComputedStyle(base_face).width.split('px')[0];
iradius = cradius*Math.sin(.5*poly_angle);

Und nun müssen wir nur noch die richtigen Transformationen auf jeder Seitenfläche anwenden.

curr_y_rot = i*base_angle + .5*poly_angle;
curr_lat_face.style.transform = 
    'rotateY(' + curr_y_rot + 
    'translateZ(' + iradius + 'px)';

Alles klar, das kommt dem Ganzen schon sehr nahe.

Siehe den Pen position prism faces stage #1 (JS) von Ana Tudor (@thebabydino) auf CodePen.

Das einzige Problem, das wir noch haben, ist, dass die Breiten der Seitenflächen immer noch nicht stimmen. Wir haben sie gleich der Dimension der Grundfläche gesetzt, aber die Grundfläche ist doppelt so groß wie der Umkreisradius des Grundpolygons und das ist nicht gleich der Kantenlänge des Grundpolygons. Aber wir können die Kantenlänge aus demselben Dreieck wie den Inkreisradius berechnen.

edge_len = 2*cradius*Math.cos(.5*poly_angle);

...dann die richtige Breite und den linken Rand für die Seitenflächen einstellen.

curr_lat_face.style.width = edge_len + 'px';
curr_lat_face.style.marginLeft = -.5*edge_len + 'px';

Und jetzt haben wir ein schönes Prisma, das Sie im folgenden Pen (oder in seiner Jade-Version) überprüfen können.

Siehe den Pen position prism faces full (JS) von Ana Tudor (@thebabydino) auf CodePen.

Das Beste daran ist, dass das Ändern der Anzahl der Kanten so einfach ist, wie die Variable n im JS zu ändern – probieren Sie es selbst aus! Es gibt immer noch ein paar kleine Probleme, die behoben werden müssen. Wir müssen nicht wirklich die gleiche width und margin-left inline auf jeder Seitenfläche setzen – wir könnten diese einfach in ein Style-Element einfügen. Außerdem müssen wir beim Ändern der Größe diese Werte sowie die Transformationen auf den Seitenflächen aktualisieren, da diese von den Pixeldimensionen der Grundflächen abhängen, die wir in Viewport-Einheiten gesetzt haben, damit sich die Prismen bei Größenänderung skalieren. Dieser letzte Pen behebt all diese kleinen Probleme.

Siehe den Pen position prism faces final (JS) von Ana Tudor (@thebabydino) auf CodePen.