Erstellen eines Schwenkeffekts für SVG

Avatar of Louis Hoebregts
Louis Hoebregts am

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

Anfang dieses Monats hatten wir im Animation at Work Slack eine Diskussion darüber, wie wir Benutzern das Schwenken innerhalb eines SVG ermöglichen können.

Ich habe die untenstehende Demo erstellt, um zu zeigen, wie ich diese Frage angehen würde

Siehe den Pen Demo – SVG Panning von Louis Hoebregts (@Mamboleoo) auf CodePen.

Hier sind die vier Schritte, um die obige Demo zum Laufen zu bringen

  1. Erfassen von Maus- und Touch-Events des Benutzers
  2. Berechnen der Maus-Offsets von ihrem Ursprung
  3. Speichern der neuen viewBox-Koordinaten
  4. Behandeln des dynamischen Viewports

Sehen wir uns diese Schritte einzeln genauer an.

1. Maus- & Touch-Events

Um die Maus- oder Touch-Position zu erhalten, müssen wir zuerst Event-Listener auf unserem SVG hinzufügen. Wir können die Pointer Events verwenden, um alle Arten von Zeigern (Maus/Touch/Stift/...) zu verarbeiten, aber diese Events werden noch nicht von allen Browsern unterstützt. Wir müssen einen Fallback hinzufügen, um sicherzustellen, dass alle Benutzer das SVG ziehen können.

// We select the SVG into the page
var svg = document.querySelector('svg');

// If browser supports pointer events
if (window.PointerEvent) {
  svg.addEventListener('pointerdown', onPointerDown); // Pointer is pressed
  svg.addEventListener('pointerup', onPointerUp); // Releasing the pointer
  svg.addEventListener('pointerleave', onPointerUp); // Pointer gets out of the SVG area
  svg.addEventListener('pointermove', onPointerMove); // Pointer is moving
} else {
  // Add all mouse events listeners fallback
  svg.addEventListener('mousedown', onPointerDown); // Pressing the mouse
  svg.addEventListener('mouseup', onPointerUp); // Releasing the mouse
  svg.addEventListener('mouseleave', onPointerUp); // Mouse gets out of the SVG area
  svg.addEventListener('mousemove', onPointerMove); // Mouse is moving

  // Add all touch events listeners fallback
  svg.addEventListener('touchstart', onPointerDown); // Finger is touching the screen
  svg.addEventListener('touchend', onPointerUp); // Finger is no longer touching the screen
  svg.addEventListener('touchmove', onPointerMove); // Finger is moving
}

Da wir Touch-Events und Pointer-Events haben könnten, müssen wir eine kleine Funktion erstellen, die die Koordinaten entweder vom ersten Finger oder von einem Zeiger zurückgibt.

// This function returns an object with X & Y values from the pointer event
function getPointFromEvent (event) {
  var point = {x:0, y:0};
  // If event is triggered by a touch event, we get the position of the first finger
  if (event.targetTouches) {
    point.x = event.targetTouches[0].clientX;
    point.y = event.targetTouches[0].clientY;
  } else {
    point.x = event.clientX;
    point.y = event.clientY;
  }
  
  return point;
}

Sobald die Seite bereit ist und auf Benutzerinteraktionen wartet, können wir die Ereignisse `mousedown`/`touchstart` verarbeiten, um die ursprünglichen Koordinaten des Zeigers zu speichern und eine Variable zu erstellen, die uns wissen lässt, ob der Zeiger gedrückt ist oder nicht.

// This variable will be used later for move events to check if pointer is down or not
var isPointerDown = false;

// This variable will contain the original coordinates when the user start pressing the mouse or touching the screen
var pointerOrigin = {
  x: 0,
  y: 0
};

// Function called by the event listeners when user start pressing/touching
function onPointerDown(event) {
  isPointerDown = true; // We set the pointer as down
  
  // We get the pointer position on click/touchdown so we can get the value once the user starts to drag
  var pointerPosition = getPointFromEvent(event);
  pointerOrigin.x = pointerPosition.x;
  pointerOrigin.y = pointerPosition.y;
}

2. Maus-Offsets berechnen

Nachdem wir die Koordinaten der ursprünglichen Position haben, an der der Benutzer begonnen hat, innerhalb des SVG zu ziehen, können wir den Abstand zwischen der aktuellen Zeigerposition und ihrem Ursprung berechnen. Dies tun wir für die X- und Y-Achse und wenden die berechneten Werte auf die viewBox an.

// We save the original values from the viewBox
var viewBox = {
  x: 0,
  y: 0,
  width: 500,
  height: 500
};

// The distances calculated from the pointer will be stored here
var newViewBox = {
  x: 0,
  y: 0
};

// Function called by the event listeners when user start moving/dragging
function onPointerMove (event) {
  // Only run this function if the pointer is down
  if (!isPointerDown) {
    return;
  }
  // This prevent user to do a selection on the page
  event.preventDefault();

  // Get the pointer position
  var pointerPosition = getPointFromEvent(event);

  // We calculate the distance between the pointer origin and the current position
  // The viewBox x & y values must be calculated from the original values and the distances
  newViewBox.x = viewBox.x - (pointerPosition.x - pointerOrigin.x);
  newViewBox.y = viewBox.y - (pointerPosition.y - pointerOrigin.y);

  // We create a string with the new viewBox values
  // The X & Y values are equal to the current viewBox minus the calculated distances
  var viewBoxString = `${newViewBox.x} ${newViewBox.y} ${viewBox.width} ${viewBox.height}`;
  // We apply the new viewBox values onto the SVG
  svg.setAttribute('viewBox', viewBoxString);
  
  document.querySelector('.viewbox').innerHTML = viewBoxString;
}

Wenn Sie sich mit dem Konzept von viewBox nicht wohlfühlen, empfehle ich Ihnen, zuerst diesen grossartigen Artikel von Sara Soueidan zu lesen.

3. Aktualisierte viewBox speichern

Nachdem die viewBox aktualisiert wurde, müssen wir ihre neuen Werte speichern, wenn der Benutzer aufhört, das SVG zu ziehen.

Dieser Schritt ist wichtig, da wir sonst immer die Zeiger-Offsets von den ursprünglichen viewBox-Werten berechnen würden und der Benutzer das SVG jedes Mal vom Startpunkt aus ziehen würde.

function onPointerUp() {
  // The pointer is no longer considered as down
  isPointerDown = false;

  // We save the viewBox coordinates based on the last pointer offsets
  viewBox.x = newViewBox.x;
  viewBox.y = newViewBox.y;
}

4. Dynamischen Viewport handhaben

Wenn wir eine benutzerdefinierte Breite für unser SVG festlegen, bemerken Sie möglicherweise beim Ziehen in der untenstehenden Demo, dass sich der Vogel schneller oder langsamer bewegt als Ihr Zeiger.

Siehe den Pen Dynamischer Viewport – SVG Panning von Louis Hoebregts (@Mamboleoo) auf CodePen.

In der ursprünglichen Demo entspricht die Breite des SVG genau seiner viewBox-Breite. Die *tatsächliche Grösse* Ihres SVG kann auch als viewport bezeichnet werden. In einer perfekten Situation möchten wir, dass die viewBox um 1px verschoben wird, wenn sich der Zeiger des Benutzers um 1px bewegt.

Meistens hat das SVG jedoch eine responsive Grösse und die viewBox entspricht wahrscheinlich nicht dem SVG-viewport. Wenn die Breite des SVG doppelt so gross ist wie die viewBox, verschiebt sich das Bild im SVG um 2px, wenn sich der Zeiger des Benutzers um 1px bewegt.

Um dies zu beheben, müssen wir das **Verhältnis** zwischen der viewBox und dem viewport berechnen und dieses Verhältnis bei der Berechnung der neuen viewBox anwenden. Dieses Verhältnis muss auch aktualisiert werden, wenn sich die Grösse des SVG ändert.

// Calculate the ratio based on the viewBox width and the SVG width
var ratio = viewBox.width / svg.getBoundingClientRect().width;
window.addEventListener('resize', function() {
  ratio = viewBox.width / svg.getBoundingClientRect().width;
});

Sobald wir das **Verhältnis** kennen, müssen wir die Maus-Offsets mit dem Verhältnis multiplizieren, um die Offsets proportional zu erhöhen oder zu verringern.

function onMouseMove (e) {
  [...]
  newViewBox.x = viewBox.x - ((pointerPosition.x - pointerOrigin.x) * ratio);
  newViewBox.y = viewBox.y - ((pointerPosition.y - pointerOrigin.y) * ratio);
  [...]
}

Hier sehen Sie, wie dies mit einem kleineren viewport als der Breite der viewBox funktioniert

Siehe den Pen Kleinerer Viewport – SVG Panning von Louis Hoebregts (@Mamboleoo) auf CodePen.

Und eine weitere Demo mit einem viewport, der grösser als die Breite der viewBox ist

Siehe den Pen Grösserer Viewport – SVG Panning von Louis Hoebregts (@Mamboleoo) auf CodePen.

[Bonus] Code optimieren

Um unseren Code etwas kürzer zu machen, gibt es zwei sehr nützliche Konzepte in SVG, die wir verwenden könnten.

SVG-Punkte

Das erste Konzept ist die Verwendung von SVG Points anstelle von einfachen JavaScript-Objekten zur Speicherung der Zeigerpositionen. Nach dem Erstellen einer neuen SVG Point-Variable können wir eine Matrix-Transformation darauf anwenden, um die Position relativ zum Bildschirm in eine Position relativ zu den aktuellen SVG-Benutzereinheiten umzuwandeln.

Überprüfen Sie den untenstehenden Code, um zu sehen, wie sich die Funktionen getPointFromEvent() und onPointerDown() geändert haben.

// Create an SVG point that contains x & y values
var point = svg.createSVGPoint();

function getPointFromEvent (event) {
  if (event.targetTouches) {
    point.x = event.targetTouches[0].clientX;
    point.y = event.targetTouches[0].clientY;
  } else {
    point.x = event.clientX;
    point.y = event.clientY;
  }
  
  // We get the current transformation matrix of the SVG and we inverse it
  var invertedSVGMatrix = svg.getScreenCTM().inverse();
  
  return point.matrixTransform(invertedSVGMatrix);
}

var pointerOrigin;
function onPointerDown(event) {
  isPointerDown = true; // We set the pointer as down
  
  // We get the pointer position on click/touchdown so we can get the value once the user starts to drag
  pointerOrigin = getPointFromEvent(event);
}

Durch die Verwendung von SVG-Punkten müssen Sie nicht einmal Transformationen verarbeiten, die auf Ihrem SVG angewendet werden! Vergleichen Sie die folgenden beiden Beispiele, bei denen das erste bei einer Drehung des SVG fehlschlägt und das zweite Beispiel SVG-Punkte verwendet.

Siehe den Pen Demo + Transformation – SVG Panning von Louis Hoebregts (@Mamboleoo) auf CodePen.

Siehe den Pen Demo Bonus + Transform – SVG Panning von Louis Hoebregts (@Mamboleoo) auf CodePen.

SVG Animiertes Rechteck

Das zweite unbekannte Konzept in SVG, das wir zur Verkürzung unseres Codes verwenden können, ist die Verwendung von Animated Rect.

Da die viewBox tatsächlich als SVG-Rechteck (x, y, Breite, Höhe) betrachtet wird, können wir eine Variable aus ihrem Basiswert erstellen, die die viewBox automatisch aktualisiert, wenn wir diese Variable aktualisieren.

Sehen Sie, wie viel einfacher es jetzt ist, die viewBox unseres SVG zu aktualisieren!

// We save the original values from the viewBox
var viewBox = svg.viewBox.baseVal;

function onPointerMove (event) {
  if (!isPointerDown) {
    return;
  }
  event.preventDefault();

  // Get the pointer position as an SVG Point
  var pointerPosition = getPointFromEvent(event);

  // Update the viewBox variable with the distance from origin and current position
  // We don't need to take care of a ratio because this is handled in the getPointFromEvent function
  viewBox.x -= (pointerPosition.x - pointerOrigin.x);
  viewBox.y -= (pointerPosition.y - pointerOrigin.y);
}

Und hier ist die finale Demo. Sehen Sie, wie viel kürzer der Code jetzt ist? 😀

Siehe den Pen Demo Bonus – SVG Panning von Louis Hoebregts (@Mamboleoo) auf CodePen.

Fazit

Diese Lösung ist definitiv nicht der einzige Weg, um ein solches Verhalten zu realisieren. Wenn Sie bereits eine Bibliothek zur Verarbeitung Ihrer SVGs verwenden, verfügt diese möglicherweise bereits über eine integrierte Funktion dafür.

Ich hoffe, dieser Artikel hilft Ihnen, die Leistungsfähigkeit von SVG besser zu verstehen! Fühlen Sie sich frei, zum Code beizutragen, indem Sie Ihre Ideen oder Alternativen zu dieser Lösung kommentieren.

Danksagungen