От создателя: не так давно у нас имела пространство дискуссия на тему Анимация в рабочем пространстве: как вы могли бы создать траекторию движения CSS адаптивной? Какие техники будут работать? Это принудило меня задуматься.
Линия движения движения CSS дозволяет нам анимировать элементы вдоль пользовательских траекторий. Эти линии движения следуют той же структуре, что и контуры SVG. Мы определяем контур для элемента, используя offset-path.
CSS
.block {
offset-path: path(‘M20,20 C20,100 200,0 200,100’);
}
123 | .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 не масштабируется, и элемент сдвигается с линии движения.
Это быть может отлично для наиболее обычных траекторий. Но когда линии движения стают наиболее сложными, их будет тяжело поддерживать. В особенности, если мы желаем употреблять линии движения, которые сделали в приложениях векторного рисования.
К примеру, разглядим траекторию, с которой мы работали ранее:
CSS
.element {
—path: ‘M20,20 C20,100 200,0 200,100’;
offset-path: path(var(—path));
}
1234 | .element { —path: ‘M20,20 C20,100 200,0 200,100’; offset-path: path(var(—path));} |
Чтоб масштабировать ее до другого размера контейнера, нам необходимо самим создать траекторию, а потом применить ее в различных контрольных точках. Но даже для данной для нас «обычный» линии движения, довольно ли умножения всех значений линии движения? Это даст нам правильное масштабирование?
CSS
@media(min-width: 768px) {
.element {
—path: ‘M40,40 C40,200 400,0 400,200’; // ????
}
}
12345 | @media(min-width: 768px) { .element { —path: ‘M40,40 C40,200 400,0 400,200’; // ???? }} |
Наиболее сложную траекторию, к примеру, нарисованную в векторном приложении, будет труднее поддерживать. Разрабу будет нужно открыть приложение, поменять масштаб линии движения, экспортировать ее и интегрировать с CSS. Это необходимо создать для всех вариантов размера контейнера. Это не самое нехорошее решение, но оно просит уровня обслуживания, который нам не подступает.
CSS следующего поколения: @container
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’;
}
}
12345678910111213141516 | .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 — моя 1-ая идея, поэтому что его плагин MotionPath может масштабировать пути SVG. Но что, если мы желаем анимировать вне SVG? Можем ли мы написать функцию, которая масштабирует пути? Ну, мы могли бы, но это будет не попросту.
Пробуем различные подходы
Какой инструмент дозволяет нам найти траекторию любым образом без особенных усилий? Библиотека диаграмм! Что-то вроде D3.js дозволяет нам передавать набор координат и получить сгенерированную строчку линии движения. Мы можем адаптировать эту строчку к нашим потребностям при помощи разных кривых, размеров и т. д.
Мало повозившись, мы можем сделать функцию, которая масштабирует путь на базе определенной системы координат:
Это точно работает, но это также далековато не совершенно, поэтому что навряд ли мы будем объявлять контуры SVG, используя наборы координат. То, что мы желаем создать — это избрать контур прямо из приложения для векторного рисования, улучшить его и поместить на страничку. Таковым образом, мы можем вызвать некую функцию JavaScript, и она обязана выполнить томную работу.
Это конкретно то, что мы собираемся создать. Во-1-х, нам необходимо сделать контур. Этот был стремительно выполнена в Inkscape. Остальные векторные инструменты рисования также можно употреблять.
Дальше давайте оптимизируем SVG. Опосля сохранения файла SVG, мы запустим его при помощи блестящего инструмента Джейка Арчибальда SVGOMG. Это даст нам что-то вроде этого:
1 |
Части, которые нас заинтересовывают, path и viewBox.
Расширение решения JavaScript
Сейчас мы можем сделать функцию JavaScript для обработки всего остального. Ранее мы сделали функцию, которая воспринимает набор точек данных и конвертирует их в масштабируемый SVG-контур. Но сейчас мы желаем пойти еще далее, взять строчку контура и обработать набор данных. Таковым образом, нашим юзерам не придется волноваться о преобразовании контуров в наборы данных.
У нашей функции есть одно предостережение: не считая строчки контура, нам также необходимы некие границы, по которым можно масштабировать контур. Эти границы, возможно, будут третьим и четвертым значениями атрибута viewBox в нашем оптимизированном SVG.
JavaScript
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,
});
12345678910 | 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 y2const width = 79.375 // equivalent to viewbox x2 const motionPath = new ResponsiveMotionPath({ height, width, path,}); |
Мы не будем разбирать эту функцию построчно. Вы сможете создать это в демоверсии! Но мы остановимся на принципиальных шагах.
Во-1-х, мы конвертируем строчку контура в набор данных
Основное из того, что делает это вероятным, это способность читать сегменты контура. Это полностью может быть благодаря API SVGGeometryElement. Мы начнем с сотворения элемента SVG с контуром и присвоения строчки контура его атрибуту d.
Search Console повысила точность статистики по сканированию сайта
JavaScript
// 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’);
12345678 | // 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 innerHTMLsvgContainer.innerHTML = ` `;const pathElement = svgContainer.querySelector(‘path’); |
Потом мы можем употреблять API SVGGeometryElement для этого элемента контура. Все, что нам необходимо создать, это выполнить итерацию по всей длине контура и возвратить точку для всякого отрезка.
JavaScript
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;
}
123456789101112131415161718 | 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;} |
Дальше мы генерируем коэффициенты масштабирования
Помните, как мы произнесли, что нам пригодятся некие границы, которые могут быть определены через viewBox? И вот почему. Нам нужен некий метод высчитать отношение линии движения движения к контейнеру. Это соотношение будет равно отношению линии движения к viewBox SVG . Опосля этого мы будем употреблять их при помощи масштабирования D3.js.
У нас есть две функции: одна для захвата большего значения x и y, а иная для вычисления соотношению с viewBox.
JavaScript
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]
123456789 | 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 для генерации строчки линии движения при помощи набора данных, который мы сделали ранее.
Чтоб сделать строчку с набором данных, мы делаем это:
JavaScript
d3.line()(data); // M10.362000465393066,18.996000289916992L10.107386589050293, etc.
1 | d3.line()(data); // M10.362000465393066,18.996000289916992L10.107386589050293, etc. |
Неувязка в том, что эти точки не масштабируются для нашего контейнера. Крутая вещь с D3 состоит в том, что она дает возможность создавать масштабирование. Она работает, как функции интерполяции. Видите, к чему все идет? Мы можем написать один набор координат, и потом D3 перечтет путь. Мы можем создать это на базе размера контейнера, используя сгенерированные нами соотношения.
К примеру, вот шкала для координат x:
JavaScript
const xScale = d3
.scaleLinear()
.domain([
0,
maxWidth,
])
.range([0, width * widthRatio]);
1234567 | 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. Нам необходимо заполнить спектр ширины, чтоб линия движения оставалась по центру, и сохранялось соотношение сторон.
В этих вариантах мы можем высчитать смещение, чтоб линия движения как и раньше центрировалась в контейнере.
JavaScript
const widthRatio = (height — width) / height
const widthOffset = (ratio * containerWidth) / 2
const xScale = d3
.scaleLinear()
.domain([0, maxWidth])
.range([widthOffset, containerWidth * widthRatio — widthOffset])
123456 | const widthRatio = (height — width) / heightconst widthOffset = (ratio * containerWidth) / 2const xScale = d3 .scaleLinear() .domain([0, maxWidth]) .range([widthOffset, containerWidth * widthRatio — widthOffset]) |
Когда у нас есть две шкалы, мы можем показать точки данных, используя шкалы, и сгенерировать новейшую линию.
JavaScript
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
12345 | 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.
JavaScript
ELEMENT.style.setProperty(‘—path’, `»${newPath}»`);
1 | ELEMENT.style.setProperty(‘—path’, `»${newPath}»`); |
Опосля этого нам необходимо решить, когда мы желаем создавать и использовать новейшую масштабированную траекторию. Вот одно из вероятных решений:
JavaScript
const setPath = () => {
const scaledPath = responsivePath.generatePath(
CONTAINER.offsetWidth,
CONTAINER.offsetHeight
)
ELEMENT.style.setProperty(‘—path’, `»${scaledPath}»`)
}
const SizeObserver = new ResizeObserver(setPath)
SizeObserver.observe(CONTAINER)
123456789 | const setPath = () => { const scaledPath = responsivePath.generatePath( CONTAINER.offsetWidth, CONTAINER.offsetHeight ) ELEMENT.style.setProperty(‘—path’, `»${scaledPath}»`)}const SizeObserver = new ResizeObserver(setPath)SizeObserver.observe(CONTAINER) |
Эта демонстрация (идеальнее всего глядеть в полноэкранном режиме) указывает три версии элемента с внедрением линии движения движения. Линии движения тут обозначены, чтоб для вас было проще созидать масштабирование. 1-ая версия — немасштабированный SVG. 2-ой — контейнер масштабирования, иллюстрирующий, что линия движения не масштабируется. 3-ий употребляет наше решение JavaScript для масштабирования линии движения.
Мы сделали это!
Это был вправду крутой вызов, и я точно многому научился! Вот пара демонстраций, использующих это решение.
Это обязано стать доказательством концепции, и смотрится это многообещающе! Вы сможете добавлять в эту демонстрацию собственные оптимизированные файлы SVG, чтоб испытать их! — должны работать большая часть форматов изображения.
Я сделал на GitHub и npm пакет под заглавием Meanderer. Вы также сможете взять его с unpkg CDN, чтоб поэкспериментировать на CodePen. Я с нетерпением жду способности узреть, к чему это может привести, и надеюсь, что в дальнейшем мы увидим некий нативный метод обработки этого.
Создатель: Jhey Tompkins
Редакция: Команда webformyself.
Источник