SVG/Tutorials/Text/Schrift von Zauberhand

Aus SELFHTML-Wiki
< SVG‎ | Tutorials‎ | Text(Weitergeleitet von Schrift von Zauberhand)
Wechseln zu: Navigation, Suche

Text in SVG hat den Vorteil, dass er einerseits wirklich aus Text besteht, aber andererseits doch grafisch verändert und animiert werden kann.

Ein besonders attraktives Beispiel ist die Schrift von Zauberhand, die wie aus dem Nichts auf dem Bildschirm gezeichnet wird. In diesem Tutorial wollen wir Ihnen die Voraussetzungen vorstellen und welche praktischen Probleme dabei aufgetreten sind.

Funktionsweise

Wie im Einstieg in SVG-Tutorial bereits gezeigt, kann man die Randlinie eines SVG-Objekts mit Strichmustern (stroke-dasharray) gestalten und die Randlinie mit stroke-dashoffset aus dem sichtbaren Bereich verschieben:

Haus vom Nikolaus ansehen …
path {
  stroke: #c32e04;
  fill: none;
  stroke-width: 3;
  stroke-dasharray: 900 900;
  animation: strokeAni 5s infinite linear;
}
 
@keyframes strokeAni {
	0% {
		stroke-dashoffset: 900;
	}
	100% {
		stroke-dashoffset: 0;
	}
}
<path d="M200,155 v-100 l-50,-51 l-50,52 h100 l-100,100 v-100 l100,100 h-100" />

Schrift in Pfade umwandeln

Leider gibt es keinen Königsweg Schrift zu vektorisieren. Am besten ist es, wenn Sie einen single stroke font mit nur einem Strich oder eine ähnliche Schriftart verwenden.

In Programmen wie Inkscape können Sie Text mit

Objekt > Objekt in Pfad umwandeln

vektorisieren. Allerdings werden Buchstaben hier nicht zu geschwungenen Linien, sondern als Flächen dargestellt, deren Rand dann doch wieder nur die Kontur der Buchstaben darstellt.[1]

In Illustrator gehen Sie folgendermaßen vor:

  1. Rasterisieren Sie zunächst den Text
    Objekt > In Pixelbild umwandeln
  2. Dann zeichnen Sie das Bild nach
    Objekt > Bildnachzeichnung > Erstellen und Umwandeln
  3. Stellen Sie "Pfade", "Ecken" und "Rauschen" auf 100 und wählen Sie unter "Erstellen" nur "Pfade". Spielen Sie mit dem Wert für die Kontur herum. Ich finde, dass 100 am besten passt.

Leider ist dies nicht sehr genau. Buchstaben wie A und H bestehen aus mehreren Pfaden und es gibt seltsame Ungenauigkeiten, wo sich die Pfade kreuzen. Trotz dieser Nachteile ist es eine Option für extreme Fälle, in denen Sie kein Skript installieren können und an eine bestimmte Schriftart gebunden sind. Es ist einfacher, als mit dem Stift zu beginnen.[2]

Tools

Es gibt einige Online-Tools, mit denen Sie dies ohne Programme erledigen können.

Google Font to Svg Path von Dan Marshall ist ein bequemes Online-Formular, in dem Schriften und Parameter ausgewählt, Text ein- und der entsprechende Pfad ausgegeben wird.

Google-font-to-path.png

Schrift animieren

In diesem Beispiel wollen wir eine Handschrift so animieren, dass sie wie von Zauberhand gezeichnet wird. Dafür haben wir keinen vorhandenen Font umgewandelt, sondern eine Schriftprobe eingescannt und nachgezeichnet. So bleibt der Pfad trotz vieler Kurven und Bögen relativ übersichtlich.

erster Versuch ansehen …
  <path d="M20,30 c-5,30 5,60 0,90 m30,-33 a16,16 1 1,0 0,33 c30,0 35,-50 35,-77  s-25,-10 -25,10 0,15 
           10,33 20,15 15,35 
           m60,-90 c-5,30 5,60 0,90 m17,-50 c0,-5 5,-5 10,0 s0,10 0,10 s-5,5 -12,-7 
	   m0,20 v20 c0,8 20,5 30,-10 s -5,-10 -10,0 0,20 25,10 c10,-10 15,-20 10,-50 s-10,-10 -12,0    
           0,20 13,40 c10,-10 20,-20 20,3 s -10,10 -20,3
	   m28,-0 c10,0 20,-20 10,-20 c0,0 -5,-0 -10,5 s0,20 10,22 s5,0 10,-3 
           ..." />

Die von Hand vektorisierte Schrift wird nun analog zu den bisherigen Beispielen mit einer gestrichelten Kontur versehen, die durch stroke-dashoffset außerhalb des sichtbaren Fläche positioniert ist und erst durch die Animation hereingeschoben wird.

Fazit: Der gesamte Satz ist in einem Pfad zusammengefasst. Trotzdem beginnt das in stroke-dasharray festgelegte Strichmuster bei jedem Neuansetzen des Stifts, z. B. beim m-Befehl (moveTo) wieder von Neuem.

In einem nächsten Schritt wird die Handschrift in verschiedene Pfade geteilt. Um die Startpunkte der einzelnen Pfade aus relativen moveTo-Befehlen zu ermitteln, können Sie das untere Beispiel (Berechnen der Pfadlänge) verwenden.


Die Animation wird nun für jeden Pfad verzögert aufgerufen:

zweiter Versuch mit mehreren Pfaden ansehen …
.handwritten {
  stroke: #3983ab;
  fill: none;
  stroke-width: 3;
  stroke-dasharray: 600 600;
  stroke-dashoffset: 600;
  animation: strokeAni 5s linear forwards;
  animation-delay: calc(var(--delay) * 1s);
}

#punkt {stroke:red}
 
@keyframes strokeAni {
	0% {
		stroke-dashoffset: 600;
	}
	100% {
		stroke-dashoffset: 0;
	}
}

Jeder Pfad erhält nun eine Klasse handwritten und ein style-Attribut mit einer custom property --delay. So kann die Animation zeitverzögert aufgerufen werden.

Fazit: Schon besser, aber die Länge der einzelnen Schriftzüge stimmt nicht mit der Dauer des Schreibvorgangs überein.

Berechnen der Pfadlänge

Um einen gleichmäßigen, nacheinander ablaufenden Schreibvorgang zu realisieren, müssen wir für jeden Pfad die Pfadlänge und (bei gleicher Schreibgeschwindigkeit) daraus den Start des Schreibvorgangs (animation-delay) ermitteln.

Die SVGGeometryElement.getTotalLength()-Methode gibt den vom Browser berechneten Wert für die Gesamtlänge des Pfades in dimensionslosen Größen zurück.[3]

Beispiel
let path = document.querySelector('.handwritten');
var length = path.getTotalLength();

Diese Methode war ursprünglich Teil der Schnittstelle SVGPathElement und traf nur auf Pfade zu. Mit SVG 2 wurde die Schnittstelle SVGGeometryElement eingeführt und die Methode dorthin verschoben. So können Sie auch die Randlängen anderer Grundformen feststellen.

Beachten Sie: Die vom Browser berechnete Pfadlänge berücksichtigt nicht das Attribut pathLength.


Ermitteln der Pfadlänge mit JavaScript ansehen …
const svg = document.querySelector('svg'),
            result = document.getElementById('result'),
            startMarker = document.querySelector('.marker.start'),
            endeMarker = document.querySelector('.marker.end');
		
svg.addEventListener('mouseover', zeigePfadInformationen);
svg.addEventListener('click', zeigePfadInformationen);

function zeigePfadInformationen(event) {
   const geometrieElement = event && event.target;
   if (!geometrieElement) return;
   const aidi = geometrieElement.id;
   if (geometrieElement && 
       !geometrieElement.classList.contains("marker") &&
       geometrieElement.getTotalLength) {

      const länge = geometrieElement.getTotalLength();
      result.textContent = 'Die Pfadlänge für die id "' + aidi + '" beträgt: ' +
                           länge + ' dimensionslose Einheiten.';
      markiereStartUndEnde(elem);
   } else {
      result.textContent = 'Bewege die Maus auf ein Geometrie-Element oder tippe darauf';
      verbergeStartUndEnde();
   }
}
	
function markiereStartUndEnde(geometrieElement) {
   const länge = geometrieElement.getTotalLength();
   verschiebeMarker(startMarker, geometrieElement.getPointAtLength(0));
   verschiebeMarker(endeMarker, geometrieElement.getPointAtLength(länge));
}

function verbergeStartUndEnde() {
   const defaultPoint = { x:undefined, y:undefined };	  
   verschiebeMarker(startMarker, defaultPoint);
   verschiebeMarker(endeMarker, defaultPoint);
}

function verschiebeMarker(circle, point) {
   circle.style.cx = point.x;
   circle.style.cy = point.y;
}

Das Beispiel registriert einen mouseover- und einen click-Handler auf dem SVG-Element. Es handelt sich beide Male um die gleiche Funktion; der click-Handler dient hauptsächlich dafür, auch touch-Geräte zu unterstützen, die kein mouseover kennen. Eine Bedienung mit Tastatur ist nicht vorgesehen.

Außerdem speichert das Script noch Referenzen auf das Ausgabeelement für die gefundene Pfadlänge und zwei <circle> Elemente, mit denen Anfang und Ende eines Pfades dargestellt werden sollen.

Die Handler-Funktion showPathInformation ermittelt zunächst das Element, für das ein mouseover oder click ausgeführt wurde. Dabei handelt es sich entweder um das <svg> Element selbst, oder die darin befindlichen Formen. Das SVG-Element ist kein Geometrie-Element, und die Start- und Ende-Marker sollen nicht analysiert werden, deswegen wird der Kern des Handlers für Elemente mit Klasse "marker" und für Elemente, die getTotalLength nicht implementieren, nicht weiter ausgeführt. Statt dessen wird nur ein Hinweis ins result-Element geschrieben.

Für die anderen Elemente wird mit getTotalLength die Länge des Pfades bestimmt und ausgegeben, sowie mit getPointAtLength der Anfang und das Ende des Pfades bestimmt, der den Umfang des gefundenen Elements bildet. Mit diesen beiden Punkten wird die setStartEnd Funktion aufgerufen, die die beiden Marker dann auf Start und Ende des Pfades verschiebt - oder die Marker versteckt, wenn kein Punkt, sondern null übergeben wurde.

Beachten Sie die strikte Aufteilung in kleine, separate Funktionen, wodurch Wiederverwendung von Code möglich wird. Hinzu kommt, dass die Funktionsnamen klarstellen, was die Funktion tut, so dass der Code selbstdokumentierend wird. Vielleicht stört es Sie, dass die Pfadlänge zweimal ermittelt wird. Die Alternative bestände aber darin, die Länge entweder in markiereStartUndEnde hineinzugeben, oder sie als Ergebnis zurückzugeben. Ersteres würde die Benutzung der Funktion erschweren, und letzteres würde der Funktion eine Aufgabe geben, die sie nicht hat. Die gezeigte Variante ist deshalb sauberer. Wenn man unter sehr engen Timingbedingungen programmiert, mag man entscheiden, die Sauberkeit zu Gunsten der Schnelligkeit aufzugeben. Aber nur dann!

Alternative: Vivus

Mit Vivus-Instant können Sie bestehende SVGs bequem per Drag und Drop einfügen und dann einstellen, wie diese animiert werden sollen.

Für uns wichtig ist der Animation Type, der vom Standardwert Delayed Start auf One by One geändert werden soll.

Was genau passiert, beschreibt dieser Artikel im SELF-Blog:

Das fertige Beispiel: Schrift von Zauberhand! ansehen …
...
<path class="handwritten KxLDUdPD_16" id="exclamationmark" d="m650,125 c1,1 0,0 20,-80 s-30,-20 -40,5 c-8,35 -5,40 -5,50 s 15,40 25,25"></path>
<path class="handwritten KxLDUdPD_17" id="dot" d="m630,140 c0,-10 10,-10 20,0 s0,20 0,20 s-10,10 -25,-15"></path>
<style data-made-with="vivus-instant">.KxLDUdPD_0{stroke-dasharray:91 93;stroke-dashoffset:92;animation:KxLDUdPD_draw 105ms linear 0ms forwards;}.KxLDUdPD_1{stroke-dasharray:261 263;stroke-dashoffset:262;animation:KxLDUdPD_draw 300ms linear 105ms forwards;}.KxLDUdPD_2{stroke-dasharray:91 93;stroke-dashoffset:92;animation:KxLDUdPD_draw 105ms linear 405ms forwards;}.KxLDUdPD_3{stroke-dasharray:323 325;stroke-dashoffset:324;animation:KxLDUdPD_draw 371ms linear 511ms forwards;}.KxLDUdPD_4{stroke-dasharray:81 83;stroke-dashoffset:82;animation:KxLDUdPD_draw 93ms linear 882ms forwards;}.KxLDUdPD_5{stroke-dasharray:41 43;stroke-dashoffset:42;animation:KxLDUdPD_draw 48ms linear 976ms forwards;}.KxLDUdPD_6{stroke-dasharray:153 155;stroke-dashoffset:154;animation:KxLDUdPD_draw 176ms linear 1024ms forwards;}.KxLDUdPD_7{stroke-dasharray:496 498;stroke-dashoffset:497;animation:KxLDUdPD_draw 569ms linear 1200ms forwards;}.KxLDUdPD_8{stroke-dasharray:21 23;stroke-dashoffset:22;animation:KxLDUdPD_draw 25ms linear 1770ms forwards;}.KxLDUdPD_9{stroke-dasharray:91 93;stroke-dashoffset:92;animation:KxLDUdPD_draw 105ms linear 1795ms forwards;}.KxLDUdPD_10{stroke-dasharray:26 28;stroke-dashoffset:27;animation:KxLDUdPD_draw 30ms linear 1901ms forwards;}.KxLDUdPD_11{stroke-dasharray:91 93;stroke-dashoffset:92;animation:KxLDUdPD_draw 105ms linear 1932ms forwards;}.KxLDUdPD_12{stroke-dasharray:46 48;stroke-dashoffset:47;animation:KxLDUdPD_draw 53ms linear 2037ms forwards;}.KxLDUdPD_13{stroke-dasharray:86 88;stroke-dashoffset:87;animation:KxLDUdPD_draw 99ms linear 2091ms forwards;}.KxLDUdPD_14{stroke-dasharray:219 221;stroke-dashoffset:220;animation:KxLDUdPD_draw 252ms linear 2190ms forwards;}.KxLDUdPD_15{stroke-dasharray:117 119;stroke-dashoffset:118;animation:KxLDUdPD_draw 135ms linear 2443ms forwards;}.KxLDUdPD_16{stroke-dasharray:283 285;stroke-dashoffset:284;animation:KxLDUdPD_draw 325ms linear 2578ms forwards;}.KxLDUdPD_17{stroke-dasharray:83 85;stroke-dashoffset:84;animation:KxLDUdPD_draw 96ms linear 2903ms forwards;}@keyframes KxLDUdPD_draw{100%{stroke-dashoffset:0;}}@keyframes KxLDUdPD_fade{0%{stroke-opacity:1;}94.44444444444444%{stroke-opacity:1;}100%{stroke-opacity:0;}}</style>
Beachten Sie: Das Frickl formatiert diesen CSS-Block; Bei einem Klick auf Vorschau in einem neuen Tab sehen Sie das CSS ohne Leerzeichen und Zeilenumbrüche.

Den path-Elementen wurde eine Klasse hinzugefügt.

Es wurde ein style-Element mit Regelsätzen, die eine CSS-Animation aufrufen, hinzugefügt. Wenn man das ohne Leerzeichen und Zeilenumbrüche notierte CSS auflöst, wird es klarer:

Das fertige Beispiel: Schrift von Zauberhand! ansehen …
...
.KxLDUdPD_16{
  stroke-dasharray:283 285;
  stroke-dashoffset:284;
  animation:KxLDUdPD_draw 325ms linear 2578ms forwards;
}
.KxLDUdPD_17{
  stroke-dasharray:83 85;
  stroke-dashoffset:84;
  animation:KxLDUdPD_draw 96ms linear 2903ms forwards;
}
@keyframes KxLDUdPD_draw {
  100%{stroke-dashoffset:0;}
}
@keyframes KxLDUdPD_fade {
                  0% {stroke-opacity:1;}
  94.44444444444444% {stroke-opacity:1;}
  100%{stroke-opacity:0;}
}

Für jedes Pfadelement wird die Pfadlänge ermittelt und als Wert für stroke-dasharray und stroke-dashoffset zum Verbergen verwendet.

Alle 17 Pfad-Elemente rufen die Animation KxLDUdPD_draw auf, bei der sich nur animation-duration und animation-delay unterscheiden. Die keyframes für KxLDUdPD_fade werden nicht aufgerufen, sind also folglich Ballast.

Fazit: Eine flüssige Animation, die „flüssiges“ Schreiben einer Kette von Pfaden ermöglicht. Unschön ist das doppelte style-Element, das eine Vielzahl von ähnlichen CSS-Animationen enthält.

Man könnte die Pfadlänge als custom property für die Werte für stroke-dashoffset, stroke-dasharray und animation-duration verwenden. Eine zweite custom property würde die Verzögerung beinhalten.

mögliche Optimierung mit custom properties!
.handwritten {
  stroke: #3983ab;
  fill: none;
  stroke-width: 3;
  stroke-dasharray: var(--length)  var(--length);
  stroke-dashoffset: var(--length);
  animation: strokeAni 5s linear forwards;
  animation-duration: calc(var(--length) * 12ms);
  animation-delay: calc(var(--delay) * 1s);
}

#punkt {stroke:red}
 
@keyframes strokeAni {
	0% {
		stroke-dashoffset: var(--length);
	}
	100% {
		stroke-dashoffset: 0;
	}
}
<path class="handwritten" id="exclamationmark" 
      style="--length: 284; --delay: 16;" 
          d="m650,125 c1,1 0,0 20,-80 s-30,-20 -40,5 c-8,35 -5,40 -5,50 s 15,40 25,25" />
<path class="handwritten" id="dot"
      style="--length: 84; --delay: 17;" 
          d="m630,140 c0,-10 10,-10 20,0 s0,20 0,20 s-10,10 -25,-15" />

So attraktiv eine solche Schrift jedoch ist: Als Dauerschleife ist sie nervig, als Überschrift einmal nett (Eine Wiederholung muss durch das Neuladen der Seite erfolgen) und der Effekt verpufft, wenn das SVG zum Zeitpunkt der Animation außerhalb des Viepworts ist.

Weblinks

  1. stackoverflow: inkscape-convert-text-to-object-dynamic-offset-shiftctrlc-failed
  2. graphic design: How-to-convert-text-to-single-stroke-in-illustrator
  3. W3C: The getTotalLength method

Line-Animation