Es gab kürzlich eine Diskussion im Animation at Work Slack: *Wie kann man einen CSS-Bewegungspfad responsiv gestalten?* Welche Techniken wären dafür geeignet? Das hat mich zum Nachdenken gebracht.
Ein CSS-Bewegungspfad ermöglicht es uns, Elemente entlang benutzerdefinierter Pfade zu animieren. Diese Pfade folgen derselben Struktur wie SVG-Pfade. Wir definieren einen Pfad für ein Element mit offset-path.
.block {
offset-path: path('M20,20 C20,100 200,0 200,100');
}
Diese Werte erscheinen zunächst relativ und wären es auch, wenn wir SVG verwenden würden. Aber bei der Verwendung von offset-path verhalten sie sich wie px-Einheiten. Das ist genau das Problem. Pixel-Einheiten sind nicht wirklich responsiv. Dieser Pfad passt sich nicht an, wenn das Element, in dem er sich befindet, kleiner oder größer wird. Lassen Sie uns das herausfinden.
Um die Bühne zu bereiten: Die Eigenschaft offset-distance bestimmt, wo sich ein Element auf diesem Pfad befinden soll.
Wir können nicht nur den Abstand eines Elements entlang eines Pfades definieren, sondern auch die Drehung eines Elements mit offset-rotate. Der Standardwert ist auto, was dazu führt, dass unser Element dem Pfad folgt. Schauen Sie sich den Almanach-Artikel zu dieser Eigenschaft für weitere Werte an.
Um ein Element entlang des Pfades zu animieren, animieren wir die offset-distance.
Okay, damit sind wir auf dem neuesten Stand der Bewegung von Elementen entlang eines Pfades. Jetzt müssen wir die Frage beantworten...
Können wir responsive Pfade erstellen?
Der Knackpunkt bei CSS-Bewegungspfaden ist die hartcodierte Natur. Sie ist nicht flexibel. Wir sind gezwungen, Pfade für bestimmte Dimensionen und Ansichtsfenstergrößen hart zu kodieren. Ein Pfad, der ein Element 600 Pixel animiert, wird dieses Element unabhängig davon, ob das Ansichtsfenster 300 Pixel oder 3440 Pixel breit ist, 600 Pixel animieren.
Das unterscheidet sich von dem, was wir von SVG-Pfaden kennen. Sie skalieren mit der Größe des SVG-Viewports.
Probieren Sie, die folgende Demo zu verkleinern, und Sie werden sehen.
- Das SVG skaliert mit der Ansichtsfenstergröße, ebenso der enthaltene Pfad.
- Der
offset-pathskaliert nicht und das Element gerät vom Kurs ab.
Das mag für einfachere Pfade in Ordnung sein. Aber sobald unsere Pfade komplizierter werden, wird es schwierig, sie zu warten. Insbesondere wenn wir Pfade verwenden möchten, die wir in Vektorgrafikanwendungen erstellt haben.
Betrachten Sie zum Beispiel den Pfad, mit dem wir zuvor gearbeitet haben.
.element {
--path: 'M20,20 C20,100 200,0 200,100';
offset-path: path(var(--path));
}
Um ihn auf eine andere Containergröße zu skalieren, müssten wir den Pfad selbst neu berechnen und ihn dann bei verschiedenen Breakpoints anwenden. Aber selbst bei diesem „einfachen“ Pfad stellt sich die Frage, ob wir alle Pfadwerte multiplizieren müssen? Bringt uns das die richtige Skalierung?
@media(min-width: 768px) {
.element {
--path: 'M40,40 C40,200 400,0 400,200'; // ????
}
}
Ein komplexerer Pfad, wie z. B. einer, der in einer Vektoranwendung gezeichnet wurde, ist schwieriger zu warten. Der Entwickler müsste die Anwendung öffnen, den Pfad skalieren, ihn exportieren und in CSS integrieren. Dies müsste für alle Containergrößenvarianten geschehen. Es ist keine schlechte Lösung, aber sie erfordert ein Maß an Wartung, in das wir uns vielleicht nicht begeben wollen.
.element {
--path: 'M40,228.75L55.729166666666664,197.29166666666666C71.45833333333333,165.83333333333334,102.91666666666667,102.91666666666667,134.375,102.91666666666667C165.83333333333334,102.91666666666667,197.29166666666666,165.83333333333334,228.75,228.75C260.2083333333333,291.6666666666667,291.6666666666667,354.5833333333333,323.125,354.5833333333333C354.5833333333333,354.5833333333333,386.0416666666667,291.6666666666667,401.7708333333333,260.2083333333333L417.5,228.75';
offset-path: path(var(--path));
}
@media(min-width: 768px) {
.element {
--path: 'M40,223.875L55.322916666666664,193.22916666666666C70.64583333333333,162.58333333333334,101.29166666666667,101.29166666666667,131.9375,101.29166666666667C162.58333333333334,101.29166666666667,193.22916666666666,162.58333333333334,223.875,223.875C254.52083333333334,285.1666666666667,285.1666666666667,346.4583333333333,315.8125,346.4583333333333C346.4583333333333,346.4583333333333,377.1041666666667,285.1666666666667,392.4270833333333,254.52083333333334L407.75,223.875';
}
}
@media(min-width: 992px) {
.element {
--path: 'M40,221.625L55.135416666666664,191.35416666666666C70.27083333333333,161.08333333333334,100.54166666666667,100.54166666666667,130.8125,100.54166666666667C161.08333333333334,100.54166666666667,191.35416666666666,161.08333333333334,221.625,221.625C251.89583333333334,282.1666666666667,282.1666666666667,342.7083333333333,312.4375,342.7083333333333C342.7083333333333,342.7083333333333,372.9791666666667,282.1666666666667,388.1145833333333,251.89583333333334L403.25,221.625';
}
}
Es scheint, dass hier eine JavaScript-Lösung sinnvoll ist. GreenSock ist mein erster Gedanke, denn sein MotionPath-Plugin kann SVG-Pfade skalieren. Aber was, wenn wir außerhalb eines SVG animieren wollen? Könnten wir eine Funktion schreiben, die die Pfade für uns skaliert? Das könnten wir, aber es ist nicht einfach.
Versuch verschiedener Ansätze
Welches Werkzeug ermöglicht uns, einen Pfad auf eine Weise zu definieren, ohne den mentalen Aufwand? Eine Charting-Bibliothek! Etwas wie D3.js erlaubt uns, eine Menge von Koordinaten zu übergeben und eine generierte Pfadzeichenfolge zu erhalten. Wir können diese Zeichenfolge mit verschiedenen Kurven, Größen usw. an unsere Bedürfnisse anpassen.
Mit ein wenig Tüftelei können wir eine Funktion erstellen, die einen Pfad basierend auf einem definierten Koordinatensystem skaliert.
Das funktioniert definitiv, ist aber auch nicht ideal, da wir unwahrscheinlich SVG-Pfade anhand von Koordinatensätzen deklarieren werden. Was wir tun wollen, ist, einen Pfad direkt aus einer Vektorgrafikanwendung zu nehmen, ihn zu optimieren und auf eine Seite zu legen. So können wir eine JavaScript-Funktion aufrufen und diese die Hauptarbeit machen lassen.
Das ist also genau das, was wir tun werden.
Zuerst müssen wir einen Pfad erstellen. Dieser wurde in Inkscape schnell zusammengestellt. Andere Vektorgrafikwerkzeuge sind verfügbar.

Als Nächstes optimieren wir das SVG. Nachdem wir die SVG-Datei gespeichert haben, führen wir sie durch Jake Archibalds brillantes SVGOMG-Tool. Das liefert uns etwas in dieser Art.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 79.375 79.375" height="300" width="300"><path d="M10.362 18.996s-6.046 21.453 1.47 25.329c10.158 5.238 18.033-21.308 29.039-18.23 13.125 3.672 18.325 36.55 18.325 36.55l12.031-47.544" fill="none" stroke="#000" stroke-width=".265"/></svg>
Die Teile, an denen wir interessiert sind, sind path und viewBox.
Erweiterung der JavaScript-Lösung
Nun können wir eine JavaScript-Funktion erstellen, um den Rest zu erledigen. Zuvor haben wir eine Funktion erstellt, die einen Satz von Datenpunkten entgegennimmt und sie in einen skalierbaren SVG-Pfad umwandelt. Aber jetzt wollen wir das noch einen Schritt weiter gehen und die Pfadzeichenfolge nehmen und den Datensatz ermitteln. So müssen sich unsere Benutzer nie darum kümmern, ihre Pfade in Datensätze umzuwandeln.
Es gibt eine Einschränkung für unsere Funktion: Neben der Pfadzeichenfolge benötigen wir auch einige Grenzen, gegen die der Pfad skaliert werden soll. Diese Grenzen sind wahrscheinlich die dritten und vierten Werte des viewBox-Attributs in unserem optimierten SVG.
const path =
"M10.362 18.996s-6.046 21.453 1.47 25.329c10.158 5.238 18.033-21.308 29.039-18.23 13.125 3.672 18.325 36.55 18.325 36.55l12.031-47.544";
const height = 79.375 // equivalent to viewbox y2
const width = 79.375 // equivalent to viewbox x2
const motionPath = new ResponsiveMotionPath({
height,
width,
path,
});
Wir werden diese Funktion nicht Zeile für Zeile durchgehen. Sie können sie sich in der Demo ansehen! Aber wir werden die wichtigen Schritte hervorheben, die dies ermöglichen.
Zuerst wandeln wir eine Pfadzeichenfolge in einen Datensatz um.
Der wichtigste Teil, der dies ermöglicht, ist die Fähigkeit, die Pfadsegmente zu lesen. Das ist dank der SVGGeometryElement API vollständig möglich. Wir beginnen damit, ein SVG-Element mit einem Pfad zu erstellen und die Pfadzeichenfolge seinem d-Attribut zuzuweisen.
// To convert the path data to points, we need an SVG path element.
const svgContainer = document.createElement('div');
// To create one though, a quick way is to use innerHTML
svgContainer.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg">
<path d="${path}" stroke-width="${strokeWidth}"/>
</svg>`;
const pathElement = svgContainer.querySelector('path');
Dann können wir die SVGGeometryElement API auf diesem Pfadelement verwenden. Alles, was wir tun müssen, ist, über die Gesamtlänge des Pfades zu iterieren und den Punkt bei jeder Länge des Pfades zurückzugeben.
convertPathToData = path => {
// To convert the path data to points, we need an SVG path element.
const svgContainer = document.createElement('div');
// To create one though, a quick way is to use innerHTML
svgContainer.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg">
<path d="${path}"/>
</svg>`;
const pathElement = svgContainer.querySelector('path');
// Now to gather up the path points.
const DATA = [];
// Iterate over the total length of the path pushing the x and y into
// a data set for d3 to handle 👍
for (let p = 0; p < pathElement.getTotalLength(); p++) {
const { x, y } = pathElement.getPointAtLength(p);
DATA.push([x, y]);
}
return DATA;
}
Als Nächstes generieren wir Skalierungsverhältnisse.
Erinnern Sie sich, wie wir sagten, dass wir Grenzen benötigen, die wahrscheinlich durch den viewBox definiert sind? Deshalb. Wir brauchen eine Möglichkeit, ein Verhältnis des Bewegungspfades zu seinem Container zu berechnen. Dieses Verhältnis ist gleich dem des Pfades zum SVG-viewBox. Wir werden diese dann mit D3.js-Skalen verwenden.
Wir haben zwei Funktionen: eine, um die größten x- und y-Werte abzurufen, und eine andere, um die Verhältnisse im Verhältnis zum viewBox zu berechnen.
getMaximums = data => {
const X_POINTS = data.map(point => point[0])
const Y_POINTS = data.map(point => point[1])
return [
Math.max(...X_POINTS), // x2
Math.max(...Y_POINTS), // y2
]
}
getRatios = (maxs, width, height) => [maxs[0] / width, maxs[1] / height]
Jetzt müssen wir den Pfad generieren.
Das letzte Puzzleteil ist die eigentliche Erstellung des Pfades für unser Element. Hier kommt D3.js ins Spiel. Machen Sie sich keine Sorgen, wenn Sie es noch nie benutzt haben, denn wir verwenden nur ein paar Funktionen daraus. Konkret werden wir D3 verwenden, um eine Pfadzeichenfolge mit dem zuvor erstellten Datensatz zu generieren.
Um eine Linie mit unserem Datensatz zu erstellen, machen wir Folgendes.
d3.line()(data); // M10.362000465393066,18.996000289916992L10.107386589050293, etc.
Das Problem ist, dass diese Punkte nicht an unseren Container skaliert sind. Das Coole an D3 ist, dass es die Möglichkeit bietet, Skalen zu erstellen. Diese fungieren als Interpolationsfunktionen. Sehen Sie, worauf das hinausläuft? Wir können einen Satz von Koordinaten schreiben und D3 den Pfad neu berechnen lassen. Das können wir basierend auf unserer Containergröße tun, indem wir die von uns generierten Verhältnisse verwenden.
Zum Beispiel ist hier die Skala für unsere x-Koordinaten.
const xScale = d3
.scaleLinear()
.domain([
0,
maxWidth,
])
.range([0, width * widthRatio]);
Die Domain reicht von 0 bis zu unserem höchsten x-Wert. Der Bereich geht in den meisten Fällen von 0 bis zur Containerbreite, multipliziert mit unserem Breitenverhältnis.
Es gibt Fälle, in denen unser Bereich abweicht und wir ihn skalieren müssen. Dies geschieht, wenn das Seitenverhältnis unseres Containers nicht mit dem unseres Pfades übereinstimmt. Betrachten Sie zum Beispiel einen Pfad in einem SVG mit einem viewBox von 0 0 100 200. Das ist ein Seitenverhältnis von 1:2. Wenn wir dies dann in einem Container zeichnen, dessen Höhe und Breite 20vmin betragen, beträgt das Seitenverhältnis des Containers 1:1. Wir müssen den Breitenbereich auffüllen, um den Pfad zentriert zu halten und das Seitenverhältnis beizubehalten.
Was wir in diesen Fällen tun können, ist, einen Offset zu berechnen, damit unser Pfad weiterhin in unserem Container zentriert ist.
const widthRatio = (height - width) / height
const widthOffset = (ratio * containerWidth) / 2
const xScale = d3
.scaleLinear()
.domain([0, maxWidth])
.range([widthOffset, containerWidth * widthRatio - widthOffset])
Sobald wir zwei Skalen haben, können wir unsere Datenpunkte mithilfe der Skalen abbilden und eine neue Linie generieren.
const SCALED_POINTS = data.map(POINT => [
xScale(POINT[0]),
yScale(POINT[1]),
]);
d3.line()(SCALED_POINTS); // Scaled path string that is scaled to our container
Wir können diesen Pfad auf unser Element anwenden, indem wir ihn per Inline-CSS-Eigenschaft übergeben 👍
ELEMENT.style.setProperty('--path', `"${newPath}"`);
Dann liegt es in unserer Verantwortung zu entscheiden, wann wir einen neuen skalierten Pfad generieren und anwenden wollen. Hier ist eine mögliche Lösung.
const setPath = () => {
const scaledPath = responsivePath.generatePath(
CONTAINER.offsetWidth,
CONTAINER.offsetHeight
)
ELEMENT.style.setProperty('--path', `"${scaledPath}"`)
}
const SizeObserver = new ResizeObserver(setPath)
SizeObserver.observe(CONTAINER)
Diese Demo (am besten im Vollbildmodus betrachtet) zeigt drei Versionen des Elements mit einem Bewegungspfad. Die Pfade sind vorhanden, um die Skalierung leichter zu erkennen. Die erste Version ist das unskalierte SVG. Die zweite ist ein skalierbarer Container, der veranschaulicht, wie der Pfad nicht skaliert. Die dritte verwendet unsere JavaScript-Lösung, um den Pfad zu skalieren.
Puh, wir haben es geschafft!
Das war eine wirklich coole Herausforderung und ich habe definitiv viel daraus gelernt! Hier sind ein paar Demos, die die Lösung verwenden.
Es sollte als Machbarkeitsnachweis funktionieren und sieht vielversprechend aus! Laden Sie gerne Ihre eigenen optimierten SVG-Dateien in diese Demo, um sie auszuprobieren! – sie sollte die meisten Seitenverhältnisse erfassen.
Ich habe ein Paket namens „Meanderer“ auf GitHub und npm erstellt. Sie können es auch über den unpkg CDN herunterladen, um damit in CodePen zu experimentieren, wenn Sie es ausprobieren möchten.
Ich bin gespannt, wohin das führen könnte und hoffe, dass wir in Zukunft eine native Möglichkeit dafür sehen werden. 🙏
Toller Artikel!
Auch ich war fasziniert von Pfadanimationen und hatte
ein winziges Skript (vor vielen Jahren) erstellt, das die Bewegung auf einem Pfad automatisiert.
https://github.com/yairEO/pathAnimator
Danke vsync!
Ooo, hab es mir angesehen, das ist cool. Es ist ein faszinierendes Thema. Sehr interessiert daran, wohin es in CSS gehen wird.
Wenn es zukünftig eine Art relative Positionsunterstützung geben wird, vielleicht oder etwas anderes.
Ich frage mich, ob wir jemals
%-Einheiten in derpath()-Funktion bekommen werden, so wie wir sie für diepolygon()-Funktion erhalten, die wir mit Dingen wieclip-pathverwenden können? Das machtclip-pathwirklich flexibel, und diese gleiche Fähigkeit hier würde dies *viel* einfacher machen.Absolut! Es wäre eine wirklich coole Funktion, wenn wir sie hätten. Das einzige Problem bei
clip-path: polygon()ist die Kurvenunterstützung. Jede Form von relativer Syntax würde auch eine Möglichkeit erfordern, Kurvengriffe zu definieren. Ich denke, dort wird es vielleicht ein wenig knifflig. Kurvensyntax von Hand zu erstellen ist nicht einfach, ha. Und wenn wir keine Kurven benötigen, kann vieles mit verkettetentranslategehandhabt werden.Wie würden Sie sich vorstellen, Kurven von Hand zu schreiben? Oder vielleicht gäbe es eine Möglichkeit, eine Art "Out-of-the-Box"-Kurve zu definieren, wie mit
animation-timing-function.Zum Beispiel hier mein Pfad
--path: path(0 0, 50% 0, 100% 50%). Jetzt möchte ich, dass er eine einfache Kurve verwendet,offset-path-curve: normal || none || andere Optionen.Ich habe zuletzt hier kommentiert, warum ich glaube, dass dies den Parser brechen könnte.
Das Neutasten des Pfades als Polygonzug ist eine große Lösung für ein kleines Problem und funktioniert bestenfalls suboptimal. Der resultierende Bewegungspfad weist Informationsverluste auf, da er nur den ursprünglichen Pfad annähert, und gleichzeitig könnte er die Größe der Pfaddaten dramatisch erhöhen: In Ihrem Beispiel geht die Pfadzeichenfolge von 137 Zeichen für 9 Knoten auf 5718 Zeichen für 155 Knoten.
Ein wesentlich einfacherer (in Bezug auf die benötigten Ressourcen) und besserer Weg (in Bezug auf verlustfreie Transformation) wäre die Verwendung einer Bibliothek, die das Umschreiben der Pfaddaten für beliebige Transformationen übernimmt, sodass jeder Pfadbefehl in seiner Struktur erhalten bleibt, während nur die Kontrollpunktzahlen angepasst werden.
Ich habe selbst eine solche Bibliothek als Teil eines unveröffentlichten Projekts geschrieben, und es gibt auch andere. Eine schnelle Suche ergab beispielsweise svgpath, das in der Lage wäre, alle Ihre Anforderungen zu erfüllen.
Was die Ermittlung der Transformation zur Anpassung Ihres Pfades an den Container betrifft, so wirkt Ihre Lösung etwas improvisiert. Der korrekte Algorithmus ist in der Spezifikation beschrieben. Hier ist eine Implementierung, bei der Sie einfach alle Attributzeichenfolgen Ihres Quell-SVG übergeben und die korrekten Skalierungs- und Verschiebungs-Transformationen erhalten.
Das mag sein, aber es ist eine Lösung.
Es ist ein Proof of Concept. Es wurde nie behauptet, perfekt oder optimal zu sein. Es erforscht, was möglich ist. Es wird nicht als etwas beworben, das man auf seinen Produktionswebsites verwenden sollte.
Das Skalieren von SVG ist nicht das Problem. Das kann bereits im Browser erfolgen. Das Skalieren der Pfadzeichenfolge ist das Problem. Wir verwenden kein SVG. Aber
offset-pathverwendet die SVG-Pfad-Syntax. Skalierung ist eine Lösung, aber was ist mit Szenarien, in denen wir die sich bewegenden Elemente nicht skalieren wollen.So oder so, ich freue mich darauf, Ihre Lösung zu sehen.
Hier ist sie: https://github.com/ccprog/pathfit (Ich werde dies weiter verbessern und irgendwann zu npm veröffentlichen.)
Es tut mir leid, dass meine Kommentare zur SVG-Skalierung unklar waren. Wie Sie sagten, haben SVGs einen Größenalgorithmus, um sie responsiv zu machen. Um diese Responsivität für die Verwendung mit CSS
path()nachzuahmen, ist es sinnvoll, genau diesen Algorithmus neu zu erstellen, um von derviewBox(oder der intrinsischen Größe) eines Quell-SVG, aus dem die Pfaddaten stammen, zur Breite und Höhe des Container-Elements zu gelangen, auf das Sie den Bewegungspfad anwenden.Als Pluspunkt erhalten Sie, wenn Sie sich an den Algorithmus der Spezifikation halten, einen Mechanismus zur Beschreibung, wo und wie Sie Ihren Bewegungspfad platzieren, wenn das Seitenverhältnis des ursprünglichen SVG-Quellcodes und des Containers nicht übereinstimmen, indem Sie die Syntax des
preserveAspectRatio-Attributs des<svg>-Elements verwenden.Das ist großartig! Danke für das Teilen.
Ich bin ganz dafür, eine bessere Lösung zu finden. Wie ich bereits sagte, war dies lediglich ein Proof of Concept, um zu erforschen, was möglich ist, und Gespräche wie diese anzustoßen. Meine Absicht ist es, eine gute Lösung zu finden, die Leute nutzen können, während sie diese Pfadgenerierung woanders auslagern.
Zum Zeitpunkt meiner Recherche konnte ich kein browserfertiges Paket finden, das etwas in der Nähe dessen tun würde, was ich suchte.
svgpathkann das, was wir wollen. Aber ich hatte kein Glück beim Bundling. Idealerweise wäre ein Paket, das dies tut und Tree-Shaking unterstützt, fantastisch. So wäre es ein einfacher Fall vonimport { scale } from 'svg-path-transformer-package'. Das ist der einzige benötigte Teil. Die Mikrolibrary kann die Berechnung dieser Skalen oder ob die Transformation überhaupt erforderlich ist, übernehmen. In Fällen, in denen es uns egal ist, ob das sich bewegende Element skaliert wird, können wir einfach den gesamten Container entsprechend skalieren.Heiliger Bimbam, Jhey – das war genau der F&E-Prozess, den ich für das Erreichen dessen, was du gemacht hast, gesucht habe, und du hast mir viele Arbeitsstunden erspart. Vielen Dank für den detaillierten Bericht – ich habe viel gelernt und schätze es, dass Sie Ihre Reise geteilt haben, wie Sie zu folgendem, responsivem Pfad-Tracking gekommen sind. Prost, Mann!