Продолжаем смотреть технические приемы создания различных анимаций в интерфейсах. Мы уже познакомились с частицами, масками и изменением форм различных объектов — настал черед рисовать пунктирные линии.

Перед тем, как перейти к статье, сделаю небольшое отступление. Эта серия статей предназначена для разработчиков (в первую очередь начинающих), которые хотят делать красивые вещи, но совершенно запутались в сложных инструментах. Каждый раз мы затрагиваем какой-то один прием использования того или иного инструмента и смотрим, к созданию какиих эффектов его можно применить. Комментарии о том, что “во времена флеша было лучше” или что “нужно анимации рисовать в AfterEffects” безусловно имеют право на существование, но будут вырваны из контекста и совершенно не помогут начинающим в решении их задачи.

Вернемся к теме. У SVG элементов есть набор свойств, содержащих в себе слово stroke – штрих. Эти свойства позволяют нам делать любую линию пунктирной. В первую очередь нас будут интересовать следующие свойства:

  • stroke
  • stroke-width
  • stroke-linecap
  • stroke-dasharray
  • stroke-dashoffset

Свойство stroke позволяет задать цвет линий, stroke-width – их толщину, stroke-linecap дает возможность задать стиль для закругления концов линии. Обычно мы используем или значение по умолчанию (нет никаких скруглений), либо значение round (скругление по форме круга). В примерах можно будет увидеть оба варианта.

Эти три свойства не представляют особой сложности для понимания, а вот оставшиеся лучше разобрать отдельно.

Stroke-dasharray




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

Представьте себе штриховой пунктир. Начинаем смотреть на него с начала. В нем есть штрихи и пробелы. Они чередуются. Штрих, пробел, штрих, пробел и.т.д. А теперь представьте себе последовательность длин. Скажем 8мм, 2мм, 8мм, 2мм и.т.д. Как в ГОСТе написано. Получается, что у нас есть цикл из 2 значений. 8мм всегда попадает на штрих, 2мм – на пробел.

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

А мождо взять длинный цикл, скажем 1мм, 5мм, 20мм (похожие значения есть на иллюстрации). Тогда совпадения будут идти более длинной последовательностью: 1мм – штрих, 5 – пробел, 20 – штрих, 1 – пробел, 5 – штрих, 20 – пробел, и снова 1 – штрих. Все, цикл замкнулся.

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

Stroke-dashoffset




Свойство stroke-dashoffset нужно для того, чтобы сдвинуть пунктир относительно линии, по которой он идет. В примере он идет по отрезкам прямых, так что результат, который дает это свойство, вполне очевиден. Главное помнить о знаке смещения – интуитивно может казаться, что плюс и минус перепутаны.

Еще одна песочница для желающих попробовать.

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

Прелоадер из одного штриха




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

<svg viewBox='0 0 100 100'>
    <rect x='10' y='10' height='80' width='80'
        fill='none' stroke='#fff' stroke-width='5' stroke-linecap='round' />
</svg>

Длина одной стороны квадрата – 80. Периметр – 320. Это длина линии, на которой все будет происходить. Если бы это был сложный нарисованный от руки path, то мы бы заранее узнали, какая там у него получилась длина. Фрагмент, который мы будем крутить по кругу, тоже будет иметь длину 80. Это никак не привязано к длине стороны квадрата, просто совпадение. Остается 320 – 80 = 240. Возвращаясь в контекст пунктиров, можно сказать, что 80 – штрих, 240 – пробел:

<svg viewBox='0 0 100 100'>
    <rect x='10' y='10' height='80' width='80'
        fill='none' stroke='#fff' stroke-width='5' stroke-linecap='round'
        stroke-dasharray='80 240' stroke-dashoffset='0' />
</svg>

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

anime({
    targets: '. . .',
    strokeDashoffset: -320, // меняем значение
    easing: 'linear',
    duration: 1000,
    loop: true
});


Продолжаем...


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



Для подобных переходов обычно берут две кривых – одну для прелоадера, другую для конечной анимации. Мы добавим path:

<svg viewBox='0 0 100 100'>
    <rect . . .  stroke-dasharray='80 240' stroke-dashoffset='0' />
    <path  . . . d='m50 90 L 10 90 L 10 50 L 50 90 L 90 10' stroke-dasharray='142 200' stroke-dashoffset='62' />
</svg>

Обратите внимание, это не чисто галочка, это часть квадрата + галочка. Переход от одной кривой к другой хорошо скрывать с помощью частичного их перекрытия. Фрагмент движется по квадрату, доходит до определенной точки, мы скрываем квадрат и показываем вторую кривую с точно таким же фрагментом на том же самом месте, где он был до перехода. Дальше уже новый фрагмент движется по новой кривой, но нам кажется, что изменилась траектория движения изначального фрагмента.

let finish = anime({
    targets: 'ok',
    strokeDashoffset: -84,
    autoplay: false
});

let animation = anime({
    targets: 'loader',
    strokeDashoffset: -320,
    loop: true,
    run: (anim) => {
        if (loadingCompleted && anim.progress > 60 && anim.progress < 61) {
            anim.pause();
            document.getElementById('loader').style.display = 'none';
            document.getElementById('ok').style.display = 'block';
            finish.play();
        }
    }
});

Анимирование stroke-dasharray работает не очень хорошо в определенных браузерах (не буду показывать пальцем на IE), так что приходится обойтись анимированием stroke-dashoffset. Изначальное значение stroke-dashoffset для новой кривой рассчитывается исходя из длины нового фрагмента. Его длина (142) минус длина начального (80) = смещение (62).


Труъ подчеркивания


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

Первый пример – это обычное горизонтальное меню. Оно есть на каждом втором сайте. Мы немного разнообразим эффект подчеркивания активного элемента этого меню.



Сразу обратим внимание на разметку:

<div class='main-navigation'>
    <a class='item'>Link1</a>
    <a class='item'>Link2</a>
    <a class='item'>Link3</a>
    <a class='item'>Link4</a>
    <a class='item'>Link5</a>
    <svg class='underline' viewBox='0 0 100 1'>
        <line x1='0' x2='100' y1='0' y2='0' stroke-dasharray='20 80'></line>
    </svg>
</div>

Добавление SVG не сильно ее ломает. Так что этот прием в целом можно применять и при доработке уже существующей верстки. Значения 20, 80 получились, как нетрудно догадаться, из 100. Это длина линии. У нас есть пять элементов, а нужно подчеркивать только один. 100 / 5 = 20, а 80 остается на большой-большой пробел. Дальше дело техники (index – номер пункта меню, к которому нужно переместить подчеркивание):

anime({
    targets: '. . .',
    strokeDashoffset: [-(100 / 5) * index],
    stroke: `hsl(${ (index * 360 / 5) % 360 }, 100%, 50%)`
});

Остается только делать это при наведении мыши или фокусе с клавиатуры на элементах. Думаю не стоит рассказывать, как это делается. В примере анимация автоматизирована для удобства просмотра.


Мега-подчеркивание




Если предыдущий пример многим уже порядком поднадоел, то этот – еще нет. Общая идея та же, что и в предыдущем примере, просто элементы находятся один под другим, а линия немного кривая.

<form>
    <input type='text' placeholder='email' />
    <input type='password' placeholder='password' />
    <svg viewBox='0 0 100 100'>
        <path . . . d='. . .' stroke-dasharray='. . .' stroke-dashoffset='. . .'></path>
    </svg>
</form>

Как видите, разметка не очень сложная. После небольшой стилизации всего этого мы можем нарисовать path, зная, какие соотноцения размеров есть в нашей форме. Таким же образом находим длину фрагмента, который будет двигаться. А дальше вешаем на focus/blur обработчик с нашим перемещением:

email.addEventListener('focus', () => {
    anime({
        targets: '. . .',
        strokeDashoffset: 0
    })
});


Скролллллл...


Все мы скроллируем. Каждый день по много раз. Иногда нам встречается скролл по страницам, замедленный или горизонтальный… Я как-то раз видел все это в одном флаконе. Ужас. Но есть менее разрушительные для психики эффекты, которые хорошо выглядят на лендингах, которые мы скроллируем от начала до конца скорее всего только один раз. Я говорю о линиях, которые нас преследуют. Они могут быть привязаны к выполнению каких-то действий, презентации услуг или скроллу, как в нашем примере.

Для начала нам нужно нарисовать саму линию. У нас нет контента, так что ее форма не столь важна – она ни к чему не привязана.



Затем мы ее повернем в вертикальное положение и разместим на странице. Здесь самое главное – это следить за расположением контента и длиной самого SVG элемента, чтобы ничего не поехало в разные стороны. Такие вещи всегда уникальны, так что сложно дать какие-то универсальные рекомендации по этому поводу.



В техническом плане не будет ничего нового. Мы просто привязываем значение stroke-dashoffset к скроллу:

window.addEventListener('scroll', () => {
    let d = document.body.offsetHeight / path.getTotalLength();
    let offset = -Math.floor(window.pageYOffset / d) + 50;

    anime({
        targets: '. . .',
        strokeDashoffset: [offset],
        duration: 0
    });
});

Результат получается довольно интересным.


Просто рисовать тоже можно




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


В этом примере также происходит изменение цвета букв, но это лишь детали. Самое главное здесь — рисование кривых.

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



Мы рисуем кривую-кривую линию, делаем пунктир и двигаем его. Ничего сложного, все так же, как и в предыдущих примерах. В песочнице использована CSS анимация для реализации движения, но имейте в виду, что IE имеет проблемы с интеграцией таких анимаций и SVG, так что используйте JS и все будет хорошо.


Заключение


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

Комментарии (14)


  1. Boyd_Rice
    27.02.2018 03:19

    Интересная статья, спасибо. Вопрос, правда, к скорости такой анимации, ибо у меня лично она заметно подлагивает.


    1. Noserdan
      27.02.2018 08:57

      в примерах этих подлагивает?
      Лично у меня все в порядке, так что…


    1. nicelight_nsk
      27.02.2018 09:55

      а какой браузер\система, процессор?
      у меня на телефоне и на простом офисном компе отлично


      1. Boyd_Rice
        27.02.2018 13:23

        Edge/i5 4го поколения мобильный


    1. 0xy
      27.02.2018 12:19

      Дело может быть еще в нагрузке от CodePen. Скорее он подлагивает, а не сами примеры.


      1. Boyd_Rice
        27.02.2018 13:23

        Да, скорее всего


  1. nicelight_nsk
    27.02.2018 10:00

    Извиняюсь за совсем ламерскую просьбу, не могли бы Вы сформировать тимплейт для совсем новичков c *.html файлом, в котором можно прописывать Ваши примеры кода.
    Пробовал экспорт из codepen, не работает. Либо намекните как проавильно загуглить. Спасибо.


    1. Zlaty
      27.02.2018 12:45

      А что именно «не работает»? Загрузка архива?
      Если да — отключите блокировщик рекламы.


      1. nicelight_nsk
        27.02.2018 15:04

        архив загрузил, распаковал, запускаю html в нем нет результата. Что я делаю не так?


        1. sfi0zy Автор
          27.02.2018 15:09

          Уточните, какой из примеров после распаковки дает пустой html (или что там не работает).


          1. nicelight_nsk
            27.02.2018 17:48

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


  1. var_bin
    27.02.2018 14:29

    Добрый день sfi0zy,


    Спасибо за статью. Очень познавательно. Как раз развлекаюсь с stroke* параметрами для SVG + анимация. Собирался тоже написать статью про свой опыт и набитые шишки)


    1. nicelight_nsk
      27.02.2018 15:04

      не стесняйтесь. Хорошего мало не бывает.


      1. Noserdan
        28.02.2018 08:06

        *много