Создать адаптивную траекторию движения CSS? Конечно, мы это можем!

Создать адаптивную траекторию движения CSS? Конечно, мы это можем!

От автора: недавно у нас имела место дискуссия на тему Анимация в рабочем пространстве: как вы могли бы сделать траекторию движения CSS адаптивной? Какие техники будут работать? Это заставило меня задуматься.

Траектория движения CSS позволяет нам анимировать элементы вдоль пользовательских траекторий. Эти траектории следуют той же структуре, что и контуры SVG. Мы определяем контур для элемента, используя offset-path.

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

Эти значения сначала кажутся относительными, и они были бы таковыми, если бы мы использовали SVG. Но, при использовании в offset-path, они ведут себя как единицы Рх. Именно в этом проблема. Пиксельные единицы не очень адаптивны. Этот контур не изгибается, когда элемент, в котором он находится, становится меньше или больше. Давайте разберемся с этим.

Чтобы установить пространство, свойство offset-distance указывает, где должен быть элемент на этом контуре:

Мы можем не только определить расстояние, которое элемент проходит вдоль пути, но мы также можем определить вращение элемента с помощью offset-rotate. Значением по умолчанию является auto, в результате чего наш элемент следует траектории.

Чтобы анимировать элемент вдоль траектории, мы анимируем offset-distance:

Хорошо, это определяет скорость движения элементов вдоль траектории. Теперь мы должны ответить на вопрос…

Можем ли мы сделать траектории адаптивными?

Камнем преткновения в траекториях движения CSS является жестко заданная природа. Они не являются гибкими. Мы уткнулись в необходимость жесткого кодирования траектории для конкретных размеров области просмотра. Траектория, вдоль которой анимируется элемент, в 600px будет всегда 600px, независимо от того, имеет ли область просмотра ширину 300px или 3440px.

Это отличается от того, с чем мы знакомы при использовании контуров SVG. Они масштабируются вместе с размером окна просмотра SVG. Попробуйте изменить размер демонстрации, приведенной ниже, и вы увидите, что:

SVG будет масштабироваться вместе с размером области просмотра, как и содержащийся путь.

Траектория offset-path не масштабируется, и элемент смещается с траектории.

Это может быть хорошо для более простых траекторий. Но когда траектории становятся более сложными, их будет тяжело поддерживать. Особенно, если мы хотим использовать траектории, которые создали в приложениях векторного рисования.

Например, рассмотрим траекторию, с которой мы работали ранее:

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

Чтобы масштабировать ее до другого размера контейнера, нам нужно самим разработать траекторию, а затем применить ее в разных контрольных точках. Но даже для этой «простой» траектории, достаточно ли умножения всех значений траектории? Это даст нам правильное масштабирование?

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

Более сложную траекторию, например, нарисованную в векторном приложении, будет сложнее поддерживать. Разработчику потребуется открыть приложение, изменить масштаб траектории, экспортировать ее и интегрировать с CSS. Это нужно сделать для всех вариантов размера контейнера. Это не самое плохое решение, но оно требует уровня обслуживания, который нам не подходит.

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

Такое чувство, что здесь имеет смысл применить решение JavaScript. GreenSock — моя первая мысль, потому что его плагин MotionPath может масштабировать пути SVG. Но что, если мы хотим анимировать вне SVG? Можем ли мы написать функцию, которая масштабирует пути? Ну, мы могли бы, но это будет не просто.

Пробуем разные подходы

Какой инструмент позволяет нам определить траекторию каким-либо образом без особых усилий? Библиотека диаграмм! Что-то вроде D3.js позволяет нам передавать набор координат и получить сгенерированную строку траектории. Мы можем адаптировать эту строку к нашим потребностям с помощью различных кривых, размеров и т. д.

Немного повозившись, мы можем создать функцию, которая масштабирует путь на основе определенной системы координат:

Это определенно работает, но это также далеко не идеально, потому что вряд ли мы будем объявлять контуры SVG, используя наборы координат. То, что мы хотим сделать — это выбрать контур прямо из приложения для векторного рисования, оптимизировать его и поместить на страницу. Таким образом, мы можем вызвать некоторую функцию JavaScript, и она должна выполнить тяжелую работу.

Это именно то, что мы собираемся сделать. Во-первых, нам нужно создать контур. Этот был быстро выполнена в Inkscape. Другие векторные инструменты рисования также можно использовать.

Далее давайте оптимизируем SVG. После сохранения файла SVG, мы запустим его с помощью блестящего инструмента Джейка Арчибальда SVGOMG. Это даст нам что-то вроде этого:

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

Части, которые нас интересуют, path и viewBox.

Расширение решения JavaScript

Теперь мы можем создать функцию JavaScript для обработки всего остального. Ранее мы создали функцию, которая принимает набор точек данных и преобразует их в масштабируемый SVG-контур. Но теперь мы хотим пойти еще дальше, взять строку контура и обработать набор данных. Таким образом, нашим пользователям не придется беспокоиться о преобразовании контуров в наборы данных.

У нашей функции есть одно предостережение: кроме строки контура, нам также нужны некоторые границы, по которым можно масштабировать контур. Эти границы, вероятно, будут третьим и четвертым значениями атрибута viewBox в нашем оптимизированном 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,
});

Мы не будем разбирать эту функцию построчно. Вы можете сделать это в демоверсии! Но мы остановимся на важных шагах.

Во-первых, мы конвертируем строку контура в набор данных

Основное из того, что делает это возможным, это способность читать сегменты контура. Это вполне возможно благодаря API SVGGeometryElement. Мы начнем с создания элемента SVG с контуром и присвоения строки контура его атрибуту d.

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

Затем мы можем использовать API SVGGeometryElement для этого элемента контура. Все, что нам нужно сделать, это выполнить итерацию по всей длине контура и вернуть точку для каждого отрезка.

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

Далее мы генерируем коэффициенты масштабирования

Помните, как мы сказали, что нам понадобятся некоторые границы, которые могут быть определены через viewBox? И вот почему. Нам нужен какой-то способ рассчитать отношение траектории движения к контейнеру. Это соотношение будет равно отношению траектории к viewBox SVG . После этого мы будем использовать их с помощью масштабирования D3.js.

У нас есть две функции: одна для захвата наибольшего значения x и y, а другая для вычисления соотношению с 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]

Теперь нам нужно сгенерировать траекторию

Последний фрагмент головоломки заключается в создании траектории для нашего элемента. Здесь в игру вступает D3.js. Не беспокойтесь, если вы не работали с ней раньше, потому что мы используем только несколько функций из нее. В частности, мы будем использовать D3 для генерации строки траектории с помощью набора данных, который мы создали ранее.

Чтобы создать строку с набором данных, мы делаем это:

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

Проблема в том, что эти точки не масштабируются для нашего контейнера. Крутая вещь с D3 заключается в том, что она дает возможность создавать масштабирование. Она работает, как функции интерполяции. Видите, к чему все идет? Мы можем написать один набор координат, и затем D3 пересчитает путь. Мы можем сделать это на основе размера контейнера, используя сгенерированные нами соотношения.

Например, вот шкала для координат x:

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

Домен имеет размер от 0 до нашего наибольшего значения x. Диапазон в большинстве случаев будет варьироваться от 0 до ширины контейнера, умноженной на соотношение ширины.

Бывают ситуации, когда наш диапазон может отличаться, и нам нужно его масштабировать. Например, когда соотношение сторон контейнера не соответствует траектории. Рассмотрим контур в SVG с viewBoxof 0 0 100 200. Это соотношение сторон 1: 2. Но если мы затем нарисуем это в контейнере, который имеет высоту и ширину 20vmin, соотношение сторон контейнера будет 1:1. Нам нужно заполнить диапазон ширины, чтобы траектория оставалась по центру, и сохранялось соотношение сторон.

В этих случаях мы можем рассчитать смещение, чтобы траектория по-прежнему центрировалась в контейнере.

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

Когда у нас есть две шкалы, мы можем отобразить точки данных, используя шкалы, и сгенерировать новую линию.

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

Мы можем применить эту траекторию к элементу, передав ее через свойство CSS.

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

После этого нам нужно решить, когда мы хотим создавать и применять новую масштабированную траекторию. Вот одно из возможных решений:

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

Эта демонстрация (лучше всего смотреть в полноэкранном режиме) показывает три версии элемента с использованием траектории движения. Траектории здесь обозначены, чтобы вам было проще видеть масштабирование. Первая версия — немасштабированный SVG. Второй — контейнер масштабирования, иллюстрирующий, что траектория не масштабируется. Третий использует наше решение JavaScript для масштабирования траектории.

Мы сделали это!

Это был действительно крутой вызов, и я определенно многому научился! Вот пара демонстраций, использующих это решение.

Это должно стать подтверждением концепции, и выглядит это многообещающе! Вы можете добавлять в эту демонстрацию собственные оптимизированные файлы SVG, чтобы попробовать их! — должны работать большинство форматов изображения.

Я создал на GitHub и npm пакет под названием Meanderer. Вы также можете взять его с unpkg CDN, чтобы поэкспериментировать на CodePen. Я с нетерпением жду возможности увидеть, к чему это может привести, и надеюсь, что в будущем мы увидим какой-то нативный способ обработки этого.

Автор: Jhey Tompkins

Источник: https://css-tricks.com

Редакция: Команда webformyself.