Anime JS 沿路径递增到特定点的动画

Anime JS animate incrementally to a specific point along a path

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();

这段代码在广泛的方案中完成了我想要它做的事情,但我有一个更具体的用例。

我将此 SVG 用作表单上的进度条:

当用户完成表格的第 1 步时,我希望圆从 A 点到 B 点进行动画处理。当用户完成表格的第 2 步时,我希望圆从 B 点到 B 点进行动画处理C点……等等。

虽然 motionpath.seek() 让我沿着路径到达正确的点,但它在没有动画的情况下设置了圆圈 - 是否有与 seek() 等效的函数,它将使圆圈动画化,而不仅仅是设置了吗?

此外,我尝试使用 getTotalLength()getPointAtLength() 来尝试制作动画:

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

但这并没有为沿路径的圆圈设置动画。

非常感谢任何帮助。谢谢!

对于一条长路径,我认为很难支持点之间的移动,因为您需要跟踪当前进度并根据缓动函数将其转换为实际长度。

我会把你的 <path/> 分成 3 部分,为这 3 部分之间的动画生成时间轴,然后轻松地来回操纵移动的圆圈。

这是一个如何完成的示例:

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>