Creación de un efecto panorámico para SVG | Programar Plus

A principios de este mes, en Animation at Work Slack, tuvimos una discusión sobre cómo encontrar una manera de permitir que los usuarios se desplacen dentro de un SVG.

Hice esta demostración a continuación para mostrar cómo abordaría esta pregunta:

Vea la Demostración de Pen – SVG Panning de Louis Hoebregts (@Mamboleoo) en CodePen.

Estos son los cuatro pasos para que la demostración anterior funcione:

  1. Obtener eventos táctiles y de mouse del usuario
  2. Calcule las compensaciones del mouse desde su origen
  3. Guardar lo nuevo viewBox coordenadas
  4. Manejar ventana gráfica dinámica

Revisemos esos pasos uno por uno más a fondo.

1. Eventos táctiles y del mouse

Para obtener la posición del mouse o táctil, primero debemos agregar detectores de eventos en nuestro SVG. Podemos usar los eventos de puntero para manejar todo tipo de punteros (mouse / touch / stylus /…) pero esos eventos aún no son compatibles con todos los navegadores. Tendremos que agregar un respaldo para asegurarnos de que todos los usuarios puedan arrastrar el SVG.

// 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
}

Debido a que podríamos tener eventos táctiles y eventos de puntero, necesitamos crear una función diminuta para retornar a las coordenadas desde el primer dedo o desde un puntero.

// 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;
}

Una vez que la página está lista y esperando cualquier interacción del usuario, podemos comenzar a manejar los eventos mousedown / touchstart para guardar las coordenadas originales del puntero y crear una variable que nos permita saber si el puntero está abajo o no.

// 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. Calcular las compensaciones del mouse

Ahora que tenemos las coordenadas de la posición original donde el usuario comenzó a arrastrar dentro del SVG, podemos calcular la distancia entre la posición actual del puntero y su origen. Hacemos esto tanto para el eje X como para el eje Y y aplicamos los valores calculados en el viewBox.

// 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;
}

Si no se siente cómodo con el concepto de viewBox, Te sugiero que primero leas este gran artículo de Sara Soueidan.

3. Guardar viewBox actualizado

Ahora que el viewBox se ha actualizado, necesitamos guardar sus nuevos valores cuando el usuario deja de arrastrar el SVG.

Este paso es importante porque, de lo contrario, siempre calcularíamos los desplazamientos del puntero del original. viewBox valores y el usuario arrastrará el SVG desde el punto de partida cada vez.

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. Manejar la ventana gráfica dinámica

Si configuramos un ancho personalizado en nuestro SVG, es posible que notes mientras arrastras en la demostración de abajo que el pájaro se mueve más rápido o más lento que tu puntero.

Vea la ventana gráfica Pen Dynamic – SVG Panning de Louis Hoebregts (@Mamboleoo) en CodePen.

En la demostración original, el ancho del SVG coincide exactamente con su viewBox ancho. El tamaño real de su SVG también puede llamarse viewport. En una situación perfecta, cuando el usuario mueve su puntero por 1px, queremos el viewBox traducir por 1px.

Pero, la mayoría de las veces, el SVG tiene un tamaño receptivo y el viewBox lo más probable es que no coincida con el SVG viewport. Si el ancho del SVG es dos veces mayor que el viewBox, cuando el usuario mueve su puntero por 1px, la imagen dentro del SVG se traducirá por 2px.

Para solucionar esto, necesitamos calcular el proporción Entre los viewBox y el viewport y aplique esta relación mientras calcula el nuevo viewBox. Esta relación también debe actualizarse siempre que cambie el tamaño de SVG.

// 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;
});

Una vez que sepamos el proporción, necesitamos multiplicar las compensaciones del mouse por la relación para aumentar o reducir proporcionalmente las compensaciones.

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

Así es como funciona esto con un viewport que la viewBox ancho:

Vea la ventana gráfica Pen Smaller – SVG Panning de Louis Hoebregts (@Mamboleoo) en CodePen.

Y otra demo con un viewport más grande que el viewBox ancho:

Vea la ventana gráfica Pen Bigger – SVG Panning de Louis Hoebregts (@Mamboleoo) en CodePen.

[Bonus] Optimizando el código

Para hacer nuestro código un poco más corto, hay dos conceptos muy útiles en SVG que podríamos usar.

Puntos SVG

El primer concepto es utilizar puntos SVG en lugar de objetos básicos de Javascript para guardar las posiciones del puntero. Después de crear una nueva variable de punto SVG, podemos aplicarle alguna transformación de matriz para convertir la posición relativa a la pantalla en una posición relativa a las unidades de usuario SVG actuales.

Verifique el código a continuación para ver cómo funcionan las getPointFromEvent() y onPointerDown() han cambiado.

// 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);
}

¡Al usar SVG Points, ni siquiera tiene que manejar las transformaciones aplicadas en su SVG! Compare los siguientes dos ejemplos donde el primero se rompe cuando se aplica una rotación en el SVG y el segundo ejemplo usa Puntos SVG.

Vea la transformación Pen Demo + – SVG Panning de Louis Hoebregts (@Mamboleoo) en CodePen.

Vea el Pen Demo Bonus + transform – SVG Panning de Louis Hoebregts (@Mamboleoo) en CodePen.

Recto animado SVG

El segundo concepto desconocido en SVG que podemos usar para acortar nuestro código es el uso de Animated Rect.

Porque el viewBox se considera en realidad como un rectángulo SVG (x, y, ancho, alto), podemos crear una variable a partir de su valor base que actualizará automáticamente el viewBox si actualizamos esta variable.

Vea lo fácil que es ahora actualizar el viewBox de nuestro SVG!

// 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);
}

Y aquí está la demostración final. ¿Ves cuánto más corto es el código ahora? 😀

Vea la bonificación de demostración de lápiz: panorámica de SVG de Louis Hoebregts (@Mamboleoo) en CodePen.

Conclusión

Definitivamente, esta solución no es la única manera de manejar ese comportamiento. Si ya está utilizando una biblioteca para manejar sus SVG, es posible que ya tenga una función incorporada para manejarlo.

Espero que este artículo te ayude a comprender un poco más lo poderoso que puede ser SVG. Siéntase libre de contribuir al código comentando sus ideas o alternativas a esta solución.

Creditos

  • Pájaro diseñado por Freepik
  • Muchas gracias a Blake por su valiosa ayuda y a toda la buena gente de AAW Slack por sus comentarios.

(Visited 4 times, 1 visits today)