Créer un chemin de mouvement CSS réactif? Bien sûr que nous pouvons!

Online Coding Courses for Kids

Il y a eu récemment une discussion sur la Animation au travail mou: comment pourriez-vous rendre un chemin de mouvement CSS réactif? Quelles techniques fonctionneraient? Cela m’a fait réfléchir.

Un chemin de mouvement CSS nous permet d’animer des éléments le long de chemins personnalisés définis par l’utilisateur. Ces chemins suivent la même structure que les chemins SVG. Nous définissons un chemin pour un élément en utilisant offset-path.

.block {
  offset-path: path('M20,20 C20,100 200,0 200,100');
}

Ces valeurs semblent relatives au premier abord et elles le seraient si nous utilisions SVG. Mais, lorsqu’il est utilisé dans un offset-path, ils se comportent comme des unités px. C’est exactement le problème. Les unités de pixels ne sont pas vraiment réactives. Ce chemin ne fléchit pas lorsque l’élément dans lequel il se trouve devient plus petit ou plus grand. Voyons cela.

Pour préparer le terrain, le offset-distance La propriété dicte où un élément doit se trouver sur ce chemin:

Non seulement nous pouvons définir la distance d’un élément le long d’un chemin, mais nous pouvons également définir la rotation d’un élément avec une rotation décalée. La valeur par défaut est auto, ce qui fait que notre élément suit le chemin. Consultez l’article d’almanach de la propriété pour plus de valeurs.

Pour animer un élément le long du chemin, nous animons le offset-distance:

OK, cela rattrape la vitesse sur les éléments en mouvement le long d’un chemin. Maintenant, nous devons répondre…

Pouvons-nous faire des chemins réactifs?

Le point de blocage avec les chemins de mouvement CSS est la nature codée en dur. Ce n’est pas flexible. Nous sommes coincés dans des chemins de codage en dur pour des dimensions et des tailles de fenêtre particulières. Un chemin qui anime un élément 600 pixels, animera cet élément 600 pixels, que la fenêtre ait une largeur de 300 pixels ou 3440 pixels.

Cela diffère de ce que nous connaissons lors de l’utilisation de chemins SVG. Ils évolueront avec la taille de la fenêtre de visualisation SVG.

Essayez de redimensionner cette prochaine démo ci-dessous et vous verrez:

  • Le SVG sera mis à l’échelle avec la taille de la fenêtre, tout comme le chemin d’accès contenu.
  • Le chemin de décalage ne fait pas l’échelle et l’élément se dérobe.

Cela pourrait être correct pour des chemins plus simples. Mais une fois que nos chemins deviendront plus compliqués, il sera difficile à maintenir. Surtout si nous souhaitons utiliser des chemins que nous avons créés dans des applications de dessin vectoriel.

Par exemple, considérons le chemin avec lequel nous avons travaillé précédemment:

.element {
  --path: 'M20,20 C20,100 200,0 200,100';
  offset-path: path(var(--path));
}

Pour faire évoluer cela jusqu’à une taille de conteneur différente, nous devons déterminer le chemin nous-mêmes, puis appliquer ce chemin à différents points d’arrêt. Mais même avec ce chemin «simple», s’agit-il de multiplier toutes les valeurs de chemin? Cela nous donnera-t-il la bonne mise à l’échelle?

@media(min-width: 768px) {
  .element {
    --path: 'M40,40 C40,200 400,0 400,200'; // ????
  }
}

Un chemin plus complexe, tel que celui tracé dans une application vectorielle, sera plus difficile à maintenir. Il faudra que le développeur ouvre l’application, redimensionne le chemin, l’exporte et l’intègre au CSS. Cela devra se produire pour toutes les variations de taille de conteneur. Ce n’est pas la pire solution, mais elle nécessite un niveau de maintenance dans lequel nous ne voulons peut-être pas nous lancer.

.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';
  }
}

Il semble qu’une solution JavaScript ait du sens ici. GreenSock est ma première pensée parce que son MotionPath le plugin peut mettre à l’échelle les chemins SVG. Mais que se passe-t-il si nous voulons animer en dehors d’un SVG? Pourrions-nous écrire une fonction qui met à l’échelle les chemins pour nous? Nous pourrions mais ce ne sera pas simple.

Essayer différentes approches

Quel outil nous permet de définir un chemin d’une manière ou d’une autre sans la surcharge mentale? Une bibliothèque de cartographie! Quelque chose comme D3.js nous permet de passer un ensemble de coordonnées et de recevoir une chaîne de chemin générée. Nous pouvons adapter cette chaîne à nos besoins avec différentes courbes, tailles, etc.

Avec un peu de bricolage, nous pouvons créer une fonction qui met à l’échelle un chemin en fonction d’un système de coordonnées défini:

Cela fonctionne certainement, mais c’est aussi loin d’être idéal car il est peu probable que nous allons déclarer des chemins SVG en utilisant des ensembles de coordonnées. Ce que nous voulons faire, c’est prendre un chemin directement à partir d’une application de dessin vectoriel, l’optimiser et le déposer sur une page. De cette façon, nous pouvons invoquer une fonction JavaScript et laisser faire le gros du travail.

C’est exactement ce que nous allons faire.

Tout d’abord, nous devons créer un chemin. Celui-ci a été jeté rapidement ensemble dans Inkscape. D’autres outils de dessin vectoriel sont disponibles.

Un chemin créé dans Inkscape sur une toile 300 × 300

Ensuite, optimisons le SVG. Après avoir enregistré le fichier SVG, nous l’exécuterons via le génial de Jake Archibald SVGOMG outil. Cela nous donne quelque chose dans ce sens:

Les pièces qui nous intéressent sont path et viewBox.

Extension de la solution JavaScript

Nous pouvons maintenant créer une fonction JavaScript pour gérer le reste. Plus tôt, nous avons créé une fonction qui prend un ensemble de points de données et les convertit en un chemin SVG évolutif. Mais maintenant, nous voulons aller plus loin et prendre la chaîne de chemin d’accès et travailler sur l’ensemble de données. De cette façon, nos utilisateurs n’ont jamais à se soucier d’essayer de convertir leurs chemins d’accès en ensembles de données.

Il y a une mise en garde à notre fonction: Outre la chaîne de chemin, nous avons également besoin de quelques limites par lesquelles l’échelle du chemin contre. Ces limites sont susceptibles d’être les troisième et quatrième valeurs de l’attribut viewBox dans notre SVG optimisé.

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

Nous ne passerons pas en revue cette fonction ligne par ligne. Vous pouvez le vérifier dans la démo! Mais nous soulignerons les étapes importantes qui rendent cela possible.

Tout d’abord, nous convertissons une chaîne de chemin d’accès en un ensemble de données

La plus grande partie de rendre cela possible est de pouvoir lire les segments de chemin. C’est tout à fait possible grâce à la API SVGGeometryElement. Nous commençons par créer un élément SVG avec un chemin d’accès et en affectant la chaîne de chemin d’accès à son d attribut.

// 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 = `
  
    
  `;
const pathElement = svgContainer.querySelector('path');

Ensuite, nous pouvons utiliser le API SVGGeometryElement sur cet élément de chemin. Tout ce que nous devons faire est d’itérer sur la longueur totale du chemin et de renvoyer le point à chaque longueur du chemin.

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 = `
                              
                            `;
  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;
}

Ensuite, nous générons des rapports d'échelle

Rappelez-vous comment nous avons dit que nous aurions besoin de limites probablement définies par le viewBox? C'est pourquoi. Nous avons besoin d'un moyen de calculer un rapport entre la trajectoire de mouvement et son conteneur. Ce rapport sera égal à celui du chemin contre le SVG viewBox. Nous les utiliserons ensuite avec Échelles D3.js.

Nous avons deux fonctions: une pour saisir le plus grand x et y valeurs, et une autre pour calculer les ratios par rapport à la viewBox.

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]

Maintenant, nous devons générer le chemin

La dernière pièce du puzzle est de générer réellement le chemin de notre élément. C'est là que D3.js entre en jeu. Ne vous inquiétez pas si vous ne l'avez pas utilisé auparavant, car nous n'en utilisons que quelques fonctions. Plus précisément, nous allons utiliser D3 pour générer une chaîne de chemin avec l'ensemble de données que nous avons généré précédemment.

Pour créer une ligne avec notre ensemble de données, nous procédons comme suit:

d3.line()(data); // M10.362000465393066,18.996000289916992L10.107386589050293, etc.

Le problème est que ces points ne sont pas adaptés à notre conteneur. Ce qui est cool avec D3, c'est qu'il offre la possibilité de créer des échelles. Ceux-ci agissent comme des fonctions d'interpolation. Vous voyez où cela va? Nous pouvons écrire un ensemble de coordonnées, puis demander à D3 de recalculer le chemin. Nous pouvons le faire en fonction de la taille de notre conteneur en utilisant les ratios que nous avons générés.

Par exemple, voici l'échelle de notre x coordonnées:

const xScale = d3
  .scaleLinear()
  .domain([
    0,
    maxWidth,
  ])
  .range([0, width * widthRatio]);

Le domaine va de 0 à notre plus haut x valeur. Dans la plupart des cas, la plage passera de 0 à la largeur du conteneur multipliée par notre rapport de largeur.

Il y a des moments où notre gamme peut différer et nous devons la faire évoluer. C'est à ce moment que le rapport hauteur / largeur de notre conteneur ne correspond pas à celui de notre chemin. Par exemple, considérons un chemin dans un SVG avec un viewBox de 0 0 100 200. C'est un rapport d'aspect de 1: 2. Mais si nous dessinons ensuite cela dans un conteneur qui a une hauteur et une largeur de 20 vmin, le rapport d'aspect du conteneur est de 1: 1. Nous devons remplir la plage de largeur pour garder le chemin centré et maintenir le rapport hauteur / largeur.

Ce que nous pouvons faire dans ces cas, c'est calculer un décalage afin que notre chemin soit toujours centré dans notre conteneur.

const widthRatio = (height - width) / height
const widthOffset = (ratio * containerWidth) / 2
const xScale = d3
  .scaleLinear()
  .domain([0, maxWidth])
  .range([widthOffset, containerWidth * widthRatio - widthOffset])

Une fois que nous avons deux échelles, nous pouvons cartographier nos points de données à l'aide des échelles et générer une nouvelle ligne.

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

Nous pouvons appliquer ce chemin à notre élément en le passant en ligne via une propriété CSS 👍

ELEMENT.style.setProperty('--path', `"${newPath}"`);

Ensuite, il nous appartient de décider quand nous voulons générer et appliquer un nouveau chemin à l'échelle. Voici une solution possible:

const setPath = () => {
  const scaledPath = responsivePath.generatePath(
    CONTAINER.offsetWidth,
    CONTAINER.offsetHeight
  )
  ELEMENT.style.setProperty('--path', `"${scaledPath}"`)
}
const SizeObserver = new ResizeObserver(setPath)
SizeObserver.observe(CONTAINER)

Cette démo (mieux visualisée en plein écran) montre trois versions de l'élément utilisant une trajectoire de mouvement. Les chemins sont présents pour voir plus facilement l'échelle. La première version est le SVG non mis à l'échelle. Le second est un conteneur de mise à l'échelle illustrant la façon dont le chemin n'est pas mis à l'échelle. Le troisième utilise notre solution JavaScript pour mettre le chemin à l'échelle.

Ouf, nous l'avons fait!

Ce fut un défi vraiment cool et j'en ai certainement appris beaucoup! Voici quelques démos utilisant la solution.

Cela devrait fonctionner comme une preuve de concept et semble prometteur! N'hésitez pas à déposer vos propres fichiers SVG optimisés dans cette démo pour les essayer! - il devrait capturer la plupart des proportions.

J'ai créé un package nommé "Méandre" sur GitHub et npm. Vous pouvez également le tirer vers le bas avec décompresser CDN pour jouer avec CodePen, si vous voulez l'essayer.

J'ai hâte de voir où cela pourrait aller et j'espère que nous pourrons voir une manière native de gérer cela à l'avenir. 🙏

Close Menu