Anime JS animate incrementally to a specific point along a path

Viewed 1207

https://codepen.io/jesserosenfield/pen/LYNGRXV

var path = anime.path('#prog-svg path'),
    pathEl = document.querySelectorAll('#prog-svg path')[0],
    mylength = pathEl.getTotalLength(),
    mypt1 = pathEl.getPointAtLength(mylength * .10),
    mypt2 = pathEl.getPointAtLength(mylength * .25);

var motionPath = anime({
    targets: '.prog-circ',
    translateX: path('x'),
    translateY: path('y'),
    rotate: path('angle'),
    easing: 'easeInOutCirc',
    duration: 5000,
    direction: 'alternate',
    autoplay: false,
    elasticity: 200,
    loop: false,
    update: function(anim){
      console.log(path('x'));
    }
  });

motionPath.seek(1210);

motionPath.play();

This code does what I want it to do in the broad scheme of things, but I have a more specific use case.

I'm using this SVG as a progress bar on a form:

enter image description here

When the user completes step #1 of the form, I want the circle to animate from point A to point B. When the user completes step #2 of the form, I want the circle to animate from point B to point C... and so on.

While motionpath.seek() gets me to the correct point along the path, it sets the circle there with no animation– is there an equivalent function to seek() that will get ANIMATE the circle rather than just set it?

Furthermore I attempted to use getTotalLength() and getPointAtLength() to try and animate like so:

var motionPath = anime({
    targets: '.prog-circ',
    translateX: [mypt1.x, mypt2.x],
    translateY: [mypt1.y, mypt2.y],

but that did not animate the circle along the path.

Any help much appreciated. Thanks!

1 Answers

With one long path I think it's hard to support moving between points since you need to track current progress and convert it to actual length depending on easing function.

I'd split your <path/> into 3 pieces, generate timeline for animation between those 3 pieces and then easily manipulate moving circle back and forth.

Here's an example of how it can be done:

const svg = document.getElementById('prog-svg');
const pathEl = document.querySelector('#prog-svg path');
const totalLength = pathEl.getTotalLength();

const points = [['A', 10], ['B', 25], ['C', 75], ['D', 90]];

function splitPath() {
  const interval = 3;
  const toLen = percentage => percentage * totalLength / 100;
  const paths = [];
  for (let i = 0; i < points.length; i++) {
    const from = toLen(points[i][1]);

    for (let j = i + 1; j < points.length; j++) {
      const to = toLen(points[j][1]);
      const segments = [];
      for (let k = from; k <= to; k += interval) {
        const { x, y } = pathEl.getPointAtLength(k);
        segments.push([x, y]);
      }
      paths.push({
        segments, path: `${i}-${j}`
      });
    }
  }

  paths.forEach(subPath => {
    const subPathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    subPathEl.setAttribute('class', `st0 st0--hidden`);
    subPathEl.setAttribute('d', `M ${subPath.segments.map(([x, y]) => `${x},${y}`).join(' ')}`);
    svg.appendChild(subPathEl);
    subPath.el = subPathEl;
  });

  return paths;
}

const subPaths = splitPath();

function addPoint(name, progress) {
  const point = pathEl.getPointAtLength(totalLength * progress / 100);

  const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
  text.setAttribute('fill', '#fff');
  text.setAttribute('font-size', '1.6em');
  text.textContent = name;

  const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
  circle.setAttribute('r', '30');
  circle.setAttribute('fill', '#000');

  const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
  g.setAttribute('transform', `translate(${point.x},${point.y})`);
  g.appendChild(circle);

  g.appendChild(text);

  svg.appendChild(g);

  // center text
  const textBB = text.getBBox();
  const centerX = textBB.width / 2;
  const centerY = textBB.height / 4;
  text.setAttribute('transform', `translate(${-centerX},${centerY})`);

  return circle;
}

points.forEach(([name, progress]) => addPoint(name, progress));

const progressCircle = document.querySelector('.prog-circ');
progressCircle.style.display = 'block';

const animations = subPaths.map(subPath => {
  const animePath = anime.path(subPath.el);
  return anime({
    targets: progressCircle,
    easing: 'easeInOutCirc',
    autoplay: false,
    duration: 1000,
    translateX: animePath('x'),
    translateY: animePath('y'),
    rotate: animePath('angle'),
  });
});
// move circle to the first point
animations[0].reset();

let currentStep = 0;

function moveTo(step) {
  if (step < 0 || step > animations.length) return;
  const delta = step - currentStep;

  const path = delta > 0 ? `${currentStep}-${step}` : `${step}-${currentStep}`;
  const animationIndex = subPaths.findIndex(subPath => subPath.path === path);
  const animationToPlay = animations[animationIndex];

  if (delta < 0 &&  !animationToPlay.reversed) {
    animationToPlay.reverse();
  }
  if (delta > 0 &&  animationToPlay.reversed) {
    animationToPlay.reverse();
  }
  animationToPlay.reset();
  animationToPlay.play();

  currentStep = step;
  pagination.selectedIndex = step;
}

const btnPrev = document.getElementById('btn-prev');
const btnNext = document.getElementById('btn-next');
const pagination = document.getElementById('pagination');
btnPrev.addEventListener('click', () => moveTo(currentStep - 1));
btnNext.addEventListener('click', () => moveTo(currentStep + 1));
pagination.addEventListener('change', (e) => moveTo(+e.target.value));
body {
  margin: 0;
}

.st0 {
  fill: none;
  stroke: #000000;
  stroke-width: 5;
  stroke-linecap: round;
  stroke-miterlimit: 160;
  stroke-dasharray: 28;
}

.st0--hidden {
  stroke: none;
}
.prog-circ {
  display: none;
  position: absolute;
  border-radius: 100%;
  height: 30px;
  width: 30px;
  top: -15px;
  left: -15px;
  background: #ccc;
  opacity: .7;
}

.form-actions {
  margin-top: 2em;
  display: flex;
  justify-content: center;
}

#pagination,
.form-actions button + button {
  margin-left: 2em;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.0/anime.min.js"></script>
<svg id="prog-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1919.1 155.4">
  <g>
    <path class="st0" d="M4,84.1c0,0,58.8-57.1,235.1,17.9s348.1,18.9,470.2-44.6C800.6,9.7,869.6-2,953.5,6.6c0,0,19,4.1,38.6,14.4
      c20.7,10.9,40.7,40.6,40.7,65.6c0,40.2-29.5,64.8-69.7,64.8s-70.1-29.2-70.1-69.4c0-32.3,31.2-59.6,61.8-61.8
      c67.2-4.7,103.5-46.8,375.6,70.1c164.9,70.8,220.1-1.1,371.1-11.7c120.5-8.4,213.7,28.6,213.7,28.6"/>
  </g>
</svg>

<div class="prog-circ"></div>
<div class="form-actions">
  <button id="btn-prev">Prev</button>
  <button id="btn-next">Next</button>
  <select id="pagination">
    <option value="0">A</option>
    <option value="1">B</option>
    <option value="2">C</option>
    <option value="3">D</option>
  </select>
</div>

Related