HTML5 SVG Fill Animation с помощью CSS3 и ванильного JavaScript
Опубликовано: 2022-03-10SVG расшифровывается как 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_svg
— это элемент SVG, который заключает в себе два элемента <circle>. Первый — это путь, который нужно заполнить, а второй — заливка, которая будет анимирована.
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
. Это методология, которая делает ваши имена элементов более структурированными, организованными и семантическими.
Рекомендуемое чтение : Объяснение БЭМ и зачем оно вам нужно
Чтобы закончить структуру шаблона, давайте обернем четыре элемента списка в элемент неупорядоченного списка:
<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
установлено в диапазоне от 0 до 238 пикселей, что соответствует длине окружности круга.
Второе изображение представляет свойство stroke-dashoffset
, которое смещает начало массива штрихов. Он также устанавливается от 0 до длины окружности.
Чтобы создать эффект заполнения, мы установим 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, обработка событий, тайм-ауты и троичные элементы.
Удачного кодирования!