HTML5 SVG Fill Animation с помощью CSS3 и ванильного JavaScript

Опубликовано: 2022-03-10
Краткое резюме ↬ В этой статье вы узнаете, как создать анимированное отображение заметок на веб-сайте Awwwards. В нем обсуждается элемент круга HTML5 SVG, его свойства обводки и способы их анимации с помощью переменных CSS и ванильного JavaScript.

SVG расшифровывается как Scalable Vector G raphics и представляет собой стандартный язык разметки на основе XML для векторной графики. Он позволяет рисовать пути, кривые и фигуры, определяя набор точек на 2D-плоскости. Кроме того, вы можете добавить свойства подергивания на эти пути (такие как обводка, цвет, толщина, заливка и т. д.) для создания анимации.

С апреля 2017 года модуль CSS Level 3 Fill and Stroke позволяет задавать цвета SVG и шаблоны заливки из внешней таблицы стилей вместо установки атрибутов для каждого элемента. В этом уроке мы будем использовать простой простой шестнадцатеричный цвет, но свойства заливки и обводки также принимают узоры, градиенты и изображения в качестве значений.

Примечание . При посещении веб-сайта Awwwards анимированные заметки можно просмотреть только в том случае, если ширина браузера установлена ​​на 1024 пикселя или больше.

Примечание Отображение демонстрационной версии проекта
Демонстрация конечного результата (Большой превью)
  • Демонстрация: проект отображения заметок
  • Репозиторий: Репозиторий отображения заметок
Еще после прыжка! Продолжить чтение ниже ↓

Структура файла

Начнем с создания файлов в терминале:

 mkdir note-display cd note-display touch index.html styles.css scripts.js

HTML

Вот исходный шаблон, который связывает файлы css и js :

 <html lang="en"> <head> <meta charset="UTF-8"> <title>Note Display</title> <link rel="stylesheet" href="./styles.css"> </head> <body> <script src="./scripts.js"></script> </body> </html>

Каждый элемент примечания состоит из элемента списка: li , который содержит circle , значение note и его label .

Элемент элемента списка и прямые дочерние элементы
Элемент элемента списка и его прямые дочерние элементы: .circle , .percent и .label . (Большой превью)

.circle_svg — это элемент SVG, который заключает в себе два элемента <circle>. Первый — это путь, который нужно заполнить, а второй — заливка, которая будет анимирована.

SVG-элементы
SVG-элементы. Обертка SVG и круговые теги. (Большой превью)

note разделена на целые числа и десятичные дроби, поэтому к ним можно применять разные размеры шрифта. label представляет собой простой <span> . Итак, все это вместе выглядит так:

 <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Transparent</span> </li>

Атрибуты cx и cy определяют центральную точку оси x и оси y окружности. Атрибут r определяет его радиус.

Вы, наверное, заметили шаблон подчеркивания/дефиса в именах классов. Это БЭМ, что означает block , element и modifier . Это методология, которая делает ваши имена элементов более структурированными, организованными и семантическими.

Рекомендуемое чтение : Объяснение БЭМ и зачем оно вам нужно

Чтобы закончить структуру шаблона, давайте обернем четыре элемента списка в элемент неупорядоченного списка:

Оболочка ненумерованного списка
Оболочка неупорядоченного списка содержит четыре дочерних элемента li (большой предварительный просмотр)
 <ul class="display-container"> <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Transparent</span> </li> <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Reasonable</span> </li> <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Usable</span> </li> <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Exemplary</span> </li> </ul>

Вы, должно быть, спрашиваете себя, что означают ярлыки « Transparent », « Reasonable », « Usable для использования» и « Exemplary ». Чем больше вы будете знакомиться с программированием, тем больше поймете, что написание кода — это не только создание функционального приложения, но и гарантия того, что его можно будет поддерживать и масштабировать в долгосрочной перспективе. Это достигается только в том случае, если ваш код легко изменить.

«Акроним TRUE должен помочь решить, сможет ли код, который вы пишете, приспособиться к изменениям в будущем или нет».

Итак, в следующий раз спросите себя:

  • Transparent : Ясны ли последствия изменений кода?
  • Reasonable : стоит ли рентабельность того?
  • Usable : смогу ли я повторно использовать его в неожиданных сценариях?
  • Exemplary : представляет ли он высокое качество в качестве примера для будущего кода?

Примечание : «Практическое объектно-ориентированное проектирование в Ruby» Санди Мец объясняет TRUE наряду с другими принципами и способами их достижения с помощью шаблонов проектирования. Если вы еще не потратили время на изучение шаблонов проектирования, подумайте о том, чтобы добавить эту книгу к чтению перед сном.

CSS

Давайте импортируем шрифты и применим сброс ко всем элементам:

 @import url('https://fonts.googleapis.com/css?family=Nixie+One|Raleway:200'); * { padding: 0; margin: 0; box-sizing: border-box; }

Свойство box-sizing: border-box включает значения отступов и границ в общую ширину и высоту элемента, что упрощает расчет его размеров.

Примечание . Для наглядного пояснения по box-sizing читайте статью «Упростите себе жизнь с помощью CSS Box Sizing».

 body { height: 100vh; color: #fff; display: flex; background: #3E423A; font-family: 'Nixie One', cursive; } .display-container { margin: auto; display: flex; }

Комбинируя правила display: flex в body и margin-auto в .display-container , можно центрировать дочерний элемент как по вертикали, так и по горизонтали. Элемент .display-container также будет flex-container ; таким образом, его дочерние элементы будут размещены в одном ряду вдоль главной оси.

Элемент списка .note-display также будет flex-container . Поскольку дочерних элементов для центрирования много, давайте сделаем это через свойства justify-content и align-items . Все flex-items будут центрированы по cross и main оси. Если вы не уверены, что это такое, ознакомьтесь с разделом выравнивания в «Визуальном руководстве по основам CSS Flexbox».

 .note-display { display: flex; flex-direction: column; align-items: center; margin: 0 25px; }

Давайте применим обводку к кругам, установив правила stroke-width , stroke-opacity и stroke-linecap , которые вместе стилизуют живые концы обводки. Далее давайте добавим цвет к каждому кругу:

 .circle__progress { fill: none; stroke-width: 3; stroke-opacity: 0.3; stroke-linecap: round; } .note-display:nth-child(1) .circle__progress { stroke: #AAFF00; } .note-display:nth-child(2) .circle__progress { stroke: #FF00AA; } .note-display:nth-child(3) .circle__progress { stroke: #AA00FF; } .note-display:nth-child(4) .circle__progress { stroke: #00AAFF; }

Для того, чтобы позиционировать percent элемент абсолютно, необходимо точно знать, к чему. Элемент .circle должен быть ссылкой, поэтому давайте добавим position: relative него.

Примечание . Более подробное визуальное объяснение абсолютного позиционирования см. в статье «Как понять абсолютную позицию CSS раз и навсегда».

Другой способ центрирования элементов — объединить top: 50% , left: 50% и transform: translate(-50%, -50%); которые располагают центр элемента в центре его родителя.

 .circle { position: relative; } .percent { width: 100%; top: 50%; left: 50%; position: absolute; font-weight: bold; text-align: center; line-height: 28px; transform: translate(-50%, -50%); } .percent__int { font-size: 28px; } .percent__dec { font-size: 12px; } .label { font-family: 'Raleway', serif; font-size: 14px; text-transform: uppercase; margin-top: 15px; }

К настоящему времени шаблон должен выглядеть так:

Готовый первоначальный шаблон
Готовые элементы и стили шаблона (большой предварительный просмотр)

Заполнить переход

Круговую анимацию можно создать с помощью двух свойств круга SVG: stroke-dasharray и stroke-dashoffset .

« stroke-dasharray определяет шаблон тире-промежутка в штрихе».

Может принимать до четырех значений:

  • Когда установлено только целое число ( stroke-dasharray: 10 ), тире и пробелы имеют одинаковый размер;
  • Для двух значений ( stroke-dasharray: 10 5 ) первое применяется к тире, второе — к пробелам;
  • Третья и четвертая формы ( stroke-dasharray: 10 5 2 и stroke-dasharray: 10 5 2 3 ) будут генерировать тире и пробелы разных размеров.
Значения свойства Stroke dasharray
значения свойства stroke-dasharray (большой предварительный просмотр)

На изображении слева показано, что свойство stroke-dasharray установлено в диапазоне от 0 до 238 пикселей, что соответствует длине окружности круга.

Второе изображение представляет свойство stroke-dashoffset , которое смещает начало массива штрихов. Он также устанавливается от 0 до длины окружности.

Stroke dasharray и свойства dashoffset
stroke-dasharray и штрих-тиреофсет свойства (большой предварительный просмотр)

Чтобы создать эффект заполнения, мы установим stroke-dasharray на длину окружности, чтобы вся его длина была заполнена большой чертой без пробелов. Мы также сместим его на то же значение, чтобы оно стало «скрытым». Затем stroke-dashoffset будет обновлено до соответствующего значения ноты, заполняя штрих в соответствии с длительностью перехода.

Обновление свойств будет выполняться в сценариях через переменные CSS. Давайте объявим переменные и установим свойства:

 .circle__progress--fill { --initialStroke: 0; --transitionDuration: 0; stroke-opacity: 1; stroke-dasharray: var(--initialStroke); stroke-dashoffset: var(--initialStroke); transition: stroke-dashoffset var(--transitionDuration) ease; }

Чтобы установить начальное значение и обновить переменные, начнем с выбора всех .note-display с помощью document.querySelectorAll . Для transitionDuration будет установлено значение 900 миллисекунд.

Затем мы перебираем массив дисплеев, выбираем его .circle__progress.circle__progress--fill и извлекаем атрибут r , установленный в HTML, для расчета длины окружности. При этом мы можем установить начальные --dasharray и --dashoffset .

Анимация произойдет, когда переменная --dashoffset обновится на 100 мс setTimeout:

 const displays = document.querySelectorAll('.note-display'); const transitionDuration = 900; displays.forEach(display => { let progress = display.querySelector('.circle__progress--fill'); let radius = progress.r.baseVal.value; let circumference = 2 * Math.PI * radius; progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`); progress.style.setProperty('--initialStroke', circumference); setTimeout(() => progress.style.strokeDashoffset = 50, 100); });

Чтобы переход начинался сверху, необходимо повернуть элемент .circle__svg :

 .circle__svg { transform: rotate(-90deg); } 
Переход свойств штриха
Переход свойств обводки (большой предварительный просмотр)

Теперь давайте посчитаем значение dashoffset — относительно ноты. Значение примечания будет вставлено в каждый элемент li через атрибут data-*. * можно переключить на любое имя, которое соответствует вашим потребностям, а затем его можно получить в JavaScript через набор данных элемента: element.dataset.* .

Примечание . Подробнее об атрибуте data-* можно прочитать в веб-документах MDN.

Наш атрибут будет называться « data-note »:

 <ul class="display-container"> + <li class="note-display" data-note="7.50"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Transparent</span> </li> + <li class="note-display" data-note="9.27"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Reasonable</span> </li> + <li class="note-display" data-note="6.93"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Usable</span> </li> + <li class="note-display" data-note="8.72"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Exemplary</span> </li> </ul>

Метод parseFloat преобразует строку, возвращаемую display.dataset.note , в число с плавающей запятой. offset представляет собой процент, которого не хватает для достижения максимального балла. Итак, для банкноты 7.50 у нас будет (10 - 7.50) / 10 = 0.25 , что означает, что длина circumference должна быть смещена на 25% от ее значения:

 let note = parseFloat(display.dataset.note); let offset = circumference * (10 - note) / 10;

Обновление scripts.js :

 const displays = document.querySelectorAll('.note-display'); const transitionDuration = 900; displays.forEach(display => { let progress = display.querySelector('.circle__progress--fill'); let radius = progress.r.baseVal.value; let circumference = 2 * Math.PI * radius; + let note = parseFloat(display.dataset.note); + let offset = circumference * (10 - note) / 10; progress.style.setProperty('--initialStroke', circumference); progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`); + setTimeout(() => progress.style.strokeDashoffset = offset, 100); }); 
Переход свойств штриха до значения ноты
Переход свойств штриха к значению ноты (большой предварительный просмотр)

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

 const displays = document.querySelectorAll('.note-display'); const transitionDuration = 900; displays.forEach(display => { - let progress = display.querySelector('.circle__progress--fill'); - let radius = progress.r.baseVal.value; - let circumference = 2 * Math.PI * radius; let note = parseFloat(display.dataset.note); - let offset = circumference * (10 - note) / 10; - progress.style.setProperty('--initialStroke', circumference); - progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`); - setTimeout(() => progress.style.strokeDashoffset = offset, 100); + strokeTransition(display, note); }); + function strokeTransition(display, note) { + let progress = display.querySelector('.circle__progress--fill'); + let radius = progress.r.baseVal.value; + let circumference = 2 * Math.PI * radius; + let offset = circumference * (10 - note) / 10; + progress.style.setProperty('--initialStroke', circumference); + progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`); + setTimeout(() => progress.style.strokeDashoffset = offset, 100); + }

Примечание Увеличение значения

Остается построить переход ноты от 0.00 к значению ноты. Первое, что нужно сделать, это разделить целые и десятичные значения. Мы будем использовать строковый метод split() (он принимает аргумент, определяющий, где будет разорвана строка, и возвращает массив, содержащий обе разорванные строки). Они будут преобразованы в числа и переданы в качестве аргументов функции increaseNumber() вместе с элементом display и флагом, указывающим, является ли это целым числом или десятичным числом.

 const displays = document.querySelectorAll('.note-display'); const transitionDuration = 900; displays.forEach(display => { let note = parseFloat(display.dataset.note); + let [int, dec] = display.dataset.note.split('.'); + [int, dec] = [Number(int), Number(dec)]; strokeTransition(display, note); + increaseNumber(display, int, 'int'); + increaseNumber(display, dec, 'dec'); });

В функцииувеличитьNumber increaseNumber() мы выбираем элемент .percent__int или .percent__dec , в зависимости от className , а также от того, должен ли вывод содержать десятичную точку или нет. Мы установили transitionDuration на 900ms . Теперь, чтобы оживить число от 0 до 7, например, продолжительность должна быть разделена на ноту 900 / 7 = 128.57ms . Результат показывает, сколько времени займет каждая итерация увеличения. Это означает, что наш setInterval будет срабатывать каждые 128.57ms .

Установив эти переменные, давайте определим setInterval . Переменная counter будет добавлена ​​к элементу в виде текста и будет увеличиваться на каждой итерации:

 function increaseNumber(display, number, className) { let element = display.querySelector(`.percent__${className}`), decPoint = className === 'int' ? '.' : '', interval = transitionDuration / number, counter = 0; let increaseInterval = setInterval(() => { element.textContent = counter + decPoint; counter++; }, interval); } 
Бесконечное увеличение счетчика
Бесконечное увеличение счетчика (большой предварительный просмотр)

Прохладный! Это увеличивает значения, но это делает это навсегда. Нам нужно очистить setInterval , когда заметки достигают желаемого значения. Это делается с помощью функции clearInterval :

 function increaseNumber(display, number, className) { let element = display.querySelector(`.percent__${className}`), decPoint = className === 'int' ? '.' : '', interval = transitionDuration / number, counter = 0; let increaseInterval = setInterval(() => { + if (counter === number) { window.clearInterval(increaseInterval); } element.textContent = counter + decPoint; counter++; }, interval); } 
Готовый проект отображения заметок
Готовый проект (Большой превью)

Теперь число обновляется до значения ноты и очищается с помощью функции clearInterval() .

Это в значительной степени все для этого урока. Я надеюсь, что вам понравилось!

Если вам хочется создать что-то более интерактивное, ознакомьтесь с моим учебным пособием по игре на память, созданным с помощью ванильного JavaScript. Он охватывает основные концепции HTML5, CSS3 и JavaScript, такие как позиционирование, перспектива, переходы, Flexbox, обработка событий, тайм-ауты и троичные элементы.

Удачного кодирования!