Если для анимации объекта в canvas (и не только), нужно перемещать его по некоторому желаемому пути, возможно даже по нескольким, которые могут выбираться случайным образом или последовательно, то это можно сделать с помощью svg путей. Давайте, для начала, запустим по траектории простой но зеленый квадрат.

image


Для этого сделаем или позаимствуем svg, с одним или несколькими путями.

Создадим элемент с помощью функции document.createElementNS. MDN сообщает нам, что метод имеет базовую поддержку во всех современных браузерах. Затем добавим созданному элементу путь.

let path = document.createElementNS("http://www.w3.org/2000/svg", "path");

path.setAttribute('d', 'M148.185,118.975c0,0-92.592,39.507-80.247,79.013,s79.012,143.21,129.629,124.691s64.198-113.856,120.988-100.755s118.518,30.384,116.049,109.397s-82.715,118.519-97.53,201.235,s-92.593,139.505,0,159.259');

Здесь, в атрибуты, внесен первый попавшийся на глаза путь из какого-то svg файла, методом копируй-вставляй. Конечно это не единственный и более того, не самый удобный способ, но достаточно наглядный для использование в первом примере.

Теперь в цикле, будем получать координаты точек пути и назначать их нашему объекту. Для этого нам хватит двух методов SVGGeometryElement:

path.getTotalLength() 

возвращает вычисленное значение общей длины пути и

 path.getPointAtLength(index)

Получает аргументом float число, а возвращает объект SVGPoint у которого есть, интересующие нас, координаты x и y. При значениях аргумента, меньше нуля или больше длины пути, в качестве результата будет возвращаться первая или последняя точки соответственно.

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

> Полный код примера на codepen

Но, можно использовать более интересный вариант двигать объект по координатам нескольких путей, например такой:

image


Опять же, возьмем svg файл с несколькими путями. Тот который был использован в примере, сделан в редакторе Inscape. Теперь надо получить эти пути, это возможно через разбор объекта или, если svg был получен в виде текстового файла, то следующей функцией, с помощью регулярных выражений, можно получить их как строки.

extractPathsfromSvg: function(svg){
        let results = svg.match(/<path\b([\s\S]*?)><\/path>/g);
        let paths = [];
        let len = results.length;
        for(let i = 0; i < len; i++){
            let str = results[i];
            let data = str.match(/[^\w]d="([\s\S]*?)"/);
            paths.push(data[1]);
        }
        return paths;
    }

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

Что бы добавить больше контроля при движение объекта по координатам пути можно использовать твины. Для тестовых примеров я взял первую попавшуюся на глаза библиотеку GreenSock, но это могла оказаться и любая другая.

В первом случае при движении квадрата по единственному пути, создадим промежуточный объект помощник, и передадим его при создании твина.

var helper = {progress: 0}
helper.update = function(value){
  point = path.getPointAtLength(totalLength * helper.progress);
  x = point.x;
  y = point.y;
  ctx.clearRect(0, 0, canvas.width, canvas.height); 
  ctx.drawImage(img, x, y );
}
var tw = new TweenLite.to(helper, 5, {progress: 0, });
tw.eventCallback("onUpdate", helper.update);

Увидеть движение квадрата по пути с использованием твина, в первом примере на codepen, можно поставив галочку use tween.

При движении по нескольким путям, поступим следующим образом. Как и ранее создадим объект helper, со свойством progress. Посчитаем общую длину всех путей, и назначим ее handler.progress. Создадим переменную traversed в которой будут суммироваться уже пройденные пути.

Для получения точки на текущем пути, отнимаем от helper.progress, который меняется в твине, уже пройденный путь — traversed. Используем координаты точки как обычно.

var traversed = 0;
helper.progress = totalLenghtAllPath;
helper.update = function() {
      var localPoint = helper.progress - traversed;
       if(localPoint > curPath.getTotalLength()){
            traversed += curPath.getTotalLength();
            curPath = paths[next()];
            if(curPath){
                return false;
            }
            localPoint = helper.progress - traversed;
        }
     /* код которому нужны координаты точки пути */
}
var tw = TweenLite.to(
        helper, 
         25, 
        {progress: totalLenghtAllPath, ease: Power2.easeOut }
);      
tw.eventCallback("onUpdate", helper.update); 

Код упрощенный, полный код здесь:

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


  1. Sergey-Pimenov
    05.12.2018 15:03

    Очень-очень интересная техника, спасибо что написали. getPointAtLength — вообще открытие, это просто огонь! Если у вас ещё какие-нибудь новые интересные приёмы найдутся, напишите, пожалуйста. А то редко что-то новое попадается в сфере анимации, а тут ещё и Vanilla JS вариант применения.


    1. citizen55 Автор
      05.12.2018 16:55

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


    1. noodles
      06.12.2018 10:24

      getPointAtLength — вообще открытие, это просто огонь!

      так Deprecated же(


      1. Sergey-Pimenov
        06.12.2018 11:17

        Я уж было испугался, но этот метод просто переехал в другое место. Так что можно просто использовать новый вариант с фолбеком на старый вариант (который в статье)


        1. citizen55 Автор
          06.12.2018 17:37

          Да как бы ссылка была на новый интерфейс — SVGGeometryElement. Старый был SVGPathElement


    1. 3aicheg
      06.12.2018 11:30

      Недавно для мелкого хобби-проекта пытался сделать анимированную пунктирную линию поверх произвольного фона, и чтобы каждый пунктир был закруглён по концам и ещё обведён контрастным цветом. Ну, типа, юзер потыкал мышью по картинке, а потом через все точки пунктирный сплайн медленно прорисовался, такой юзкейс. Сделал через анимированную маску видимости, всё получилось, кроме самопересечений. Их пришлось делать, пряча svg, кладя вместо него канву и рисуя по ней тот же путь, получая точки через getPointAtLength.


  1. Keyten
    05.12.2018 17:52

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


    1. citizen55 Автор
      05.12.2018 18:44

      Если вы про второй, там полно вещей которые нагружают процессор, и код я бы сказал очень не оптимизированный, да и цели такой не было, все таки это пример, а не готовое решение. А насчет таймаута, вы наверное не дошли до места, где вызывается rAF.


      1. Keyten
        06.12.2018 18:43

        А, тогда ок :) Про raf я просто не так понял кусок кода, да, вы правы.

        Если хотите, покажу, куда копать, чтобы освоить ещё пару интересных вещей (это к комментарию habr.com/post/432114/#comment_19458236).


        1. citizen55 Автор
          07.12.2018 15:31

          Интригу создали, если они и правда интересные, то не скрывайте, напишите обязательно.


  1. vmm86
    05.12.2018 23:07

    В 2013 году сделали простенький сайт для детского сада. Пришла в голову идея в заголовок поместить движущийся паровозик из мультфильма, т.к. сад подведомствен «РЖД». Реализовали это встроенными средствами самого SVG, в частности, используя теги AnimateTransform и AnimateMotion. Кажется, получилось неплохо.-)


    1. Sergey-Pimenov
      05.12.2018 23:57

      SVG SMIL вообще крутейшая вещь. Одно время команда Хромиума объявила её «Deprecated» и собиралась вообще выпилить её, так SMIL и прожил в таком статусе несколько лет. Прочитал ваше сообщение, решил проверить на caniuse как там поживает статус SMIL'а. Оказалось ещё в середине 2016 хромиумцы под давлением сообщества решили отменить «депрекацию» и выпиливание. Это же просто праздник, теперь можно смело продолжать использовать на проде! Просто нативных аналогов вообще нет, там и морфинг, и (как в вашем примере) анимация вдоль пути. В общем хорошо, что вы упомянули ваш опыт


    1. citizen55 Автор
      06.12.2018 10:45

      Получилось симпатично, даже колеса крутятся, но у вас анимация «внутри» svg, в моем случае изначально надо было двигать растровое изображение на холсте. Хотя, таким образом можно перемещать и любой блок на html странице, но говорить о широкой поддержке, в этом случае, вряд ли приходится.


      1. ivan386
        06.12.2018 11:33

        Я прочитав заголовок подумал что canvas в svg запихнули и двигали по путям.


        1. citizen55 Автор
          06.12.2018 17:52

          Шикарно было бы наоборот — svg в canvas запихать.