HTML5-SVG-Füllanimation mit CSS3 und Vanilla-JavaScript
Veröffentlicht: 2022-03-10SVG steht für Scalable Vector Graphics und ist eine Standard-XML-basierte Auszeichnungssprache für Vektorgrafiken. Sie können Pfade, Kurven und Formen zeichnen, indem Sie eine Reihe von Punkten in der 2D-Ebene bestimmen. Darüber hinaus können Sie diesen Pfaden Twitch-Eigenschaften hinzufügen (z. B. Strich, Farbe, Dicke, Füllung und mehr), um Animationen zu erstellen.
Seit April 2017 ermöglicht das CSS Level 3 Fill and Stroke Module das Festlegen von SVG-Farben und Füllmustern aus einem externen Stylesheet, anstatt Attribute für jedes Element festzulegen. In diesem Lernprogramm verwenden wir eine einfache einfache Hex-Farbe, aber sowohl Füll- als auch Stricheigenschaften akzeptieren auch Muster, Farbverläufe und Bilder als Werte.
Hinweis : Beim Besuch der Awwwards-Website kann die animierte Notizanzeige nur angezeigt werden, wenn die Browserbreite auf 1024 Pixel oder mehr eingestellt ist.
- Demo: Hinweisanzeigeprojekt
- Repo: Hinweisanzeige-Repo
Dateistruktur
Beginnen wir mit dem Erstellen der Dateien im Terminal:
mkdir note-display cd note-display touch index.html styles.css scripts.js
HTML
Hier ist die anfängliche Vorlage, die sowohl css
als auch js
Dateien verknüpft:
<html lang="en"> <head> <meta charset="UTF-8"> <title>Note Display</title> <link rel="stylesheet" href="./styles.css"> </head> <body> <script src="./scripts.js"></script> </body> </html>
Jedes Notenelement besteht aus einem Listenelement: li
, das den circle
, den note
und seine label
enthält.
.circle_svg
ist ein SVG-Element, das zwei <circle>-Elemente umschließt. Der erste ist der zu füllende Pfad, während der zweite die zu animierende Füllung ist.
Die note
ist in Integer- und Dezimalzahlen unterteilt, sodass unterschiedliche Schriftgrößen darauf angewendet werden können. Das label
ist ein einfaches <span>
. Alles zusammen sieht also so aus:
<li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Transparent</span> </li>
Die cx
und cy
-Attribute definieren den x- und y-Achsenmittelpunkt des Kreises. Das r
Attribut definiert seinen Radius.
Sie haben wahrscheinlich das Unterstrich/Bindestrich-Muster in Klassennamen bemerkt. Das ist BEM, was für block
, element
und modifier
steht. Es ist eine Methode, die Ihre Elementbenennung strukturierter, organisierter und semantischer macht.
Empfohlene Lektüre : Eine Erklärung von BEM und warum Sie es brauchen
Um die Vorlagenstrukturen fertigzustellen, packen wir die vier Listenelemente in ein ungeordnetes Listenelement:
<ul class="display-container"> <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Transparent</span> </li> <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Reasonable</span> </li> <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Usable</span> </li> <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Exemplary</span> </li> </ul>
Sie fragen sich sicher, was die Bezeichnungen Transparent
, Reasonable
, Usable
und Exemplary
bedeuten. Je mehr Sie sich mit der Programmierung vertraut machen, desto mehr werden Sie feststellen, dass es beim Schreiben von Code nicht nur darum geht, die Anwendung funktionsfähig zu machen, sondern auch sicherzustellen, dass sie langfristig wartbar und skalierbar ist. Das wird nur erreicht, wenn Ihr Code leicht zu ändern ist.
„Das Akronym TRUE
sollte bei der Entscheidung helfen, ob der von Ihnen geschriebene Code in der Lage sein wird, Änderungen in der Zukunft zu berücksichtigen oder nicht.“
Fragen Sie sich also beim nächsten Mal:
-
Transparent
: Sind die Konsequenzen von Codeänderungen klar? -
Reasonable
: Lohnt sich der Kostenvorteil? -
Usable
: Kann ich es in unerwarteten Szenarien wiederverwenden? -
Exemplary
: Stellt es eine hohe Qualität als Beispiel für zukünftigen Code dar?
Hinweis : „Praktisches objektorientiertes Design in Ruby“ von Sandi Metz erklärt TRUE
zusammen mit anderen Prinzipien und wie man diese durch Designmuster erreicht. Wenn Sie sich noch nicht die Zeit genommen haben, Designmuster zu studieren, sollten Sie dieses Buch zu Ihrer Gute-Nacht-Lektüre hinzufügen.
CSS
Lassen Sie uns die Schriftarten importieren und alle Elemente zurücksetzen:
@import url('https://fonts.googleapis.com/css?family=Nixie+One|Raleway:200'); * { padding: 0; margin: 0; box-sizing: border-box; }
Die Eigenschaft box-sizing: border-box
bezieht Polsterungs- und Rahmenwerte in die Gesamtbreite und -höhe eines Elements ein, sodass seine Abmessungen einfacher berechnet werden können.
Hinweis : Für eine visuelle Erklärung zur Größenanpassung von box-sizing
lesen Sie bitte „Make Your Life Easier With CSS Box Sizing“.
body { height: 100vh; color: #fff; display: flex; background: #3E423A; font-family: 'Nixie One', cursive; } .display-container { margin: auto; display: flex; }
Durch die Kombination der Regeln display: flex
im body
und margin-auto
im .display-container
ist es möglich, das untergeordnete Element sowohl vertikal als auch horizontal zu zentrieren. Das .display-container
Element wird auch ein flex-container
sein; Auf diese Weise werden seine Kinder in derselben Reihe entlang der Hauptachse platziert.
Das .note-display
ist ebenfalls ein flex-container
. Da es viele Kinder zum Zentrieren gibt, machen wir das über die Eigenschaften justify-content
und align-items
. Alle flex-items
werden entlang der cross
und main
zentriert. Wenn Sie sich nicht sicher sind, welche das sind, sehen Sie sich den Ausrichtungsabschnitt unter „CSS Flexbox Fundamentals Visual Guide“ an.
.note-display { display: flex; flex-direction: column; align-items: center; margin: 0 25px; }
Wenden wir einen Strich auf die Kreise an, indem wir die Regeln stroke-width
-Width , stroke-opacity
und stroke-linecap
, die zusammen die Live-Enden des Strichs gestalten. Als nächstes fügen wir jedem Kreis eine Farbe hinzu:
.circle__progress { fill: none; stroke-width: 3; stroke-opacity: 0.3; stroke-linecap: round; } .note-display:nth-child(1) .circle__progress { stroke: #AAFF00; } .note-display:nth-child(2) .circle__progress { stroke: #FF00AA; } .note-display:nth-child(3) .circle__progress { stroke: #AA00FF; } .note-display:nth-child(4) .circle__progress { stroke: #00AAFF; }
Um das percent
absolut zu positionieren, muss man absolut wissen, worauf es ankommt. Das .circle
Element sollte die Referenz sein, also fügen wir position: relative
dazu hinzu.
Hinweis : Für eine tiefere, visuelle Erklärung zur absoluten Positionierung lesen Sie bitte „So verstehen Sie die absolute CSS-Position ein für alle Mal“.
Eine andere Möglichkeit, Elemente zu zentrieren, besteht darin, top: 50%
, left: 50%
und transform: translate(-50%, -50%);
die die Mitte des Elements in der Mitte des übergeordneten Elements positionieren.
.circle { position: relative; } .percent { width: 100%; top: 50%; left: 50%; position: absolute; font-weight: bold; text-align: center; line-height: 28px; transform: translate(-50%, -50%); } .percent__int { font-size: 28px; } .percent__dec { font-size: 12px; } .label { font-family: 'Raleway', serif; font-size: 14px; text-transform: uppercase; margin-top: 15px; }
Inzwischen sollte die Vorlage so aussehen:
Übergang füllen
Die Kreisanimation kann mit Hilfe von zwei Kreis-SVG-Eigenschaften erstellt werden: stroke-dasharray
und stroke-dashoffset
.
„ stroke-dasharray
definiert das Strich-Lücke-Muster in einem Strich.“
Es kann bis zu vier Werte annehmen:
- Wenn es auf eine reine Ganzzahl (
stroke-dasharray: 10
) eingestellt ist, haben Bindestriche und Lücken dieselbe Größe; - Bei zwei Werten (
stroke-dasharray: 10 5
) wird der erste auf Bindestriche angewendet, der zweite auf Lücken; - Die dritte und vierte Form (
stroke-dasharray: 10 5 2
undstroke-dasharray: 10 5 2 3
) erzeugen Striche und Lücken in verschiedenen Größen.
Das Bild auf der linken Seite zeigt, wie die Eigenschaft stroke-dasharray
von 0 auf 238 Pixel gesetzt wird, was der Länge des Kreisumfangs entspricht.
Das zweite Bild stellt die Eigenschaft stroke-dashoffset
, die den Anfang des Bindestrich-Arrays versetzt. Sie wird auch von 0 auf die Kreisumfangslänge eingestellt.
Um den Fülleffekt zu erzeugen, setzen wir den stroke-dasharray
auf die Umfangslänge, sodass seine gesamte Länge mit einem großen Strich und ohne Lücke gefüllt wird. Wir werden es auch um denselben Wert versetzen, damit es „versteckt“ wird. Dann wird das stroke-dashoffset
auf den entsprechenden Notenwert aktualisiert, wodurch der Stroke entsprechend der Übergangsdauer gefüllt wird.
Die Aktualisierung der Eigenschaften erfolgt in den Skripten über CSS-Variablen. Lassen Sie uns die Variablen deklarieren und die Eigenschaften festlegen:
.circle__progress--fill { --initialStroke: 0; --transitionDuration: 0; stroke-opacity: 1; stroke-dasharray: var(--initialStroke); stroke-dashoffset: var(--initialStroke); transition: stroke-dashoffset var(--transitionDuration) ease; }
Um den Initialwert zu setzen und die Variablen zu aktualisieren, selektieren wir zunächst alle .note-display
Elemente mit document.querySelectorAll
. Die transitionDuration
wird auf 900
Millisekunden festgelegt.
Dann durchlaufen wir das Displays-Array, wählen dessen .circle__progress.circle__progress--fill
und extrahieren das im HTML festgelegte r
-Attribut, um die Umfangslänge zu berechnen. Damit können wir die Anfangswerte --dasharray
und --dashoffset
.
Die Animation tritt auf, wenn die Variable --dashoffset
durch ein setTimeout von 100 ms aktualisiert wird:
const displays = document.querySelectorAll('.note-display'); const transitionDuration = 900; displays.forEach(display => { let progress = display.querySelector('.circle__progress--fill'); let radius = progress.r.baseVal.value; let circumference = 2 * Math.PI * radius; progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`); progress.style.setProperty('--initialStroke', circumference); setTimeout(() => progress.style.strokeDashoffset = 50, 100); });
Um den Übergang von oben beginnend zu bekommen, muss das .circle__svg
Element gedreht werden:
.circle__svg { transform: rotate(-90deg); }
Lassen Sie uns nun den dashoffset
-Wert berechnen – relativ zur Note. Der Notenwert wird über das Attribut data-* in jedes li
-Element eingefügt. Das *
kann gegen einen beliebigen Namen ausgetauscht werden, der Ihren Anforderungen entspricht, und es kann dann in JavaScript über das Dataset des Elements abgerufen werden: element.dataset.*
.
Hinweis : Weitere Informationen zum data-*-Attribut finden Sie in MDN Web Docs.
Unser Attribut heißt „ data-note
“:
<ul class="display-container"> + <li class="note-display" data-note="7.50"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Transparent</span> </li> + <li class="note-display" data-note="9.27"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Reasonable</span> </li> + <li class="note-display" data-note="6.93"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Usable</span> </li> + <li class="note-display" data-note="8.72"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Exemplary</span> </li> </ul>
Die parseFloat
Methode konvertiert die von display.dataset.note
zurückgegebene Zeichenfolge in eine Fließkommazahl. Der offset
stellt den Prozentsatz dar, der fehlt, um die maximale Punktzahl zu erreichen. Für eine 7.50
Note hätten wir also (10 - 7.50) / 10 = 0.25
, was bedeutet, dass die circumference
um 25%
ihres Wertes versetzt werden sollte:
let note = parseFloat(display.dataset.note); let offset = circumference * (10 - note) / 10;
Aktualisieren der scripts.js
:
const displays = document.querySelectorAll('.note-display'); const transitionDuration = 900; displays.forEach(display => { let progress = display.querySelector('.circle__progress--fill'); let radius = progress.r.baseVal.value; let circumference = 2 * Math.PI * radius; + let note = parseFloat(display.dataset.note); + let offset = circumference * (10 - note) / 10; progress.style.setProperty('--initialStroke', circumference); progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`); + setTimeout(() => progress.style.strokeDashoffset = offset, 100); });
Bevor wir fortfahren, extrahieren wir den Stoke-Übergang in seine eigene Methode:
const displays = document.querySelectorAll('.note-display'); const transitionDuration = 900; displays.forEach(display => { - let progress = display.querySelector('.circle__progress--fill'); - let radius = progress.r.baseVal.value; - let circumference = 2 * Math.PI * radius; let note = parseFloat(display.dataset.note); - let offset = circumference * (10 - note) / 10; - progress.style.setProperty('--initialStroke', circumference); - progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`); - setTimeout(() => progress.style.strokeDashoffset = offset, 100); + strokeTransition(display, note); }); + function strokeTransition(display, note) { + let progress = display.querySelector('.circle__progress--fill'); + let radius = progress.r.baseVal.value; + let circumference = 2 * Math.PI * radius; + let offset = circumference * (10 - note) / 10; + progress.style.setProperty('--initialStroke', circumference); + progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`); + setTimeout(() => progress.style.strokeDashoffset = offset, 100); + }
Hinweis Wertsteigerung
Es muss noch der Notenübergang von 0.00
zum Notenwert aufgebaut werden. Das erste, was Sie tun müssen, ist, die Ganzzahl- und Dezimalwerte zu trennen. Wir werden die String-Methode split()
verwenden (sie nimmt ein Argument, das bestimmt, wo der String unterbrochen wird, und gibt ein Array zurück, das beide unterbrochenen Strings enthält). Diese werden in Zahlen umgewandelt und zusammen mit dem display
und einem Flag, das angibt, ob es sich um eine Ganzzahl oder eine Dezimalzahl handelt, als Argumente an die Funktion increaseNumber()
übergeben.
const displays = document.querySelectorAll('.note-display'); const transitionDuration = 900; displays.forEach(display => { let note = parseFloat(display.dataset.note); + let [int, dec] = display.dataset.note.split('.'); + [int, dec] = [Number(int), Number(dec)]; strokeTransition(display, note); + increaseNumber(display, int, 'int'); + increaseNumber(display, dec, 'dec'); });
In der Funktion raiseNumber increaseNumber()
wählen wir entweder das Element .percent__int
oder .percent__dec
, je nach className
, und auch, ob die Ausgabe einen Dezimalpunkt enthalten soll oder nicht. Wir haben unsere transitionDuration
auf 900ms
. Um nun beispielsweise eine Zahl von 0 bis 7 zu animieren, muss die Dauer durch die Note 900 / 7 = 128.57ms
werden. Das Ergebnis stellt dar, wie lange jede Steigerungsiteration dauern wird. Das bedeutet, dass unser setInterval
alle 128.57ms
ms ausgelöst wird.
Wenn diese Variablen gesetzt sind, definieren setInterval
. Die counter
wird als Text an das Element angehängt und bei jeder Iteration erhöht:
function increaseNumber(display, number, className) { let element = display.querySelector(`.percent__${className}`), decPoint = className === 'int' ? '.' : '', interval = transitionDuration / number, counter = 0; let increaseInterval = setInterval(() => { element.textContent = counter + decPoint; counter++; }, interval); }
Cool! Es erhöht zwar die Werte, aber es tut es irgendwie für immer. Wir müssen setInterval
wenn die Noten den gewünschten Wert erreichen. Dies geschieht mit der Funktion clearInterval
:
function increaseNumber(display, number, className) { let element = display.querySelector(`.percent__${className}`), decPoint = className === 'int' ? '.' : '', interval = transitionDuration / number, counter = 0; let increaseInterval = setInterval(() => { + if (counter === number) { window.clearInterval(increaseInterval); } element.textContent = counter + decPoint; counter++; }, interval); }
Jetzt wird die Zahl bis zum Notenwert aktualisiert und mit der Funktion clearInterval()
gelöscht.
Das ist so ziemlich alles für dieses Tutorial. Ich hoffe, dass es Ihnen gefallen hat!
Wenn Sie Lust haben, etwas Interaktiveres zu bauen, schauen Sie sich mein Memory-Spiel-Tutorial an, das mit Vanilla JavaScript erstellt wurde. Es behandelt grundlegende HTML5-, CSS3- und JavaScript-Konzepte wie Positionierung, Perspektive, Übergänge, Flexbox, Ereignisbehandlung, Timeouts und Ternaries.
Viel Spaß beim Codieren!