ehwz
ehwz

Reputation: 53

Animate SVG along path with Framer Motion

Is there a way to animate a SVG element along a path (preferably a SVG path itself) with Framer Motion? Furthermore, is it possible that the animated SVG element changes its rotation in turns? The following shows an example of what I would like to achieve (not exactly, just an example):

https://tympanus.net/codrops/2019/12/03/motion-paths-past-present-and-future/

With the examples given in the Framer Motion documentation I do not find anything like that in the declarative ways, but I wonder whether this is achievable through MotionValues, the onUpdate method or the imperative AnimationControls somehow?

Upvotes: 5

Views: 30121

Answers (2)

epietrowicz
epietrowicz

Reputation: 384

I don't believe offset-path works on Safari just yet so I was able to implement this without offset-path using Framer Motion's useMotionValue hook.

This starts by getting the total length possible of our progress path. Then, we get the starting path length from the motion listener. This is 0 to start.

Now we multiply the starting length (0) with the total path length to get the starting X and Y coordinates of our circles.

ProgressX and ProgressY are tied to the center of the circles.

In this case, it should place our circles at exactly the start point of the path.

Doing it this way allows the circles to start at any distance from the start by modifying the motion listener's initial value.

Then, just listen to the progressLength's updates and update the center of the circles whenever it changes.

import { motion, useMotionValue } from "framer-motion";
import { useRef, useEffect } from "react";

export default function App() {
  const pathRefForeground = useRef(null);

  const progressLength = useMotionValue(0);
  const progressX = useMotionValue(0);
  const progressY = useMotionValue(0);

  useEffect(() => {
    const pathElementForeground = pathRefForeground.current;
    const totalPathLength = pathElementForeground.getTotalLength();
    const initialProgress = progressLength.get();

    const initialCoords = pathElementForeground.getPointAtLength(
      initialProgress * totalPathLength
    );

    progressX.set(initialCoords.x);
    progressY.set(initialCoords.y);

    const unsubscribe = progressLength.onChange((latestPercent) => {
      const latestPathProgress = pathElementForeground.getPointAtLength(
        latestPercent * totalPathLength
      );

      progressX.set(latestPathProgress.x);
      progressY.set(latestPathProgress.y);
    });

    return unsubscribe;
  }, []);

  const transition = {
    repeat: Infinity,
    bounce: 0.75,
    type: "spring",
    duration: 2
  };

  const progress = 50;

  return (
    <div
      className="App"
      style={{
        minHeight: 500,
        display: "flex",
        flexDirection: "column",
        justifyContent: "center",
        alignItems: "center",
        background: "#d9eefd"
      }}
    >
      <motion.svg width="500" height="50" viewBox="0 0 500 30">
        <path
          stroke="white"
          strokeWidth="10"
          strokeLinecap="round"
          d="M15 15 H490"
        />
        <motion.path
          d="M15 15 H490"
          stroke="#1f88eb"
          strokeWidth="10"
          strokeLinecap="round"
          ref={pathRefForeground}
          pathLength={progressLength}
          initial={{ pathLength: 0 }}
          animate={{ pathLength: progress / 100 }}
          transition={transition}
        />
        <motion.circle cx={progressX} cy={progressY} r="15" fill="#1f88eb" />
        <motion.circle cx={progressX} cy={progressY} r="5" fill="white" />
      </motion.svg>
    </div>
  );
}

Here's a live demo: https://codesandbox.io/s/patient-sea-nbhs5u?file=/src/App.js

Upvotes: 1

double.emms
double.emms

Reputation: 553

You can animate the pathLength property by using a motion.path. Then pair that with the offsetDistance on the element that is following the path.

import React from "react"
import { motion } from "framer-motion"
import "./styles.css"

const transition = { duration: 4, yoyo: Infinity, ease: "easeInOut" }

export default function App() {
  return (
    <div className="container">
      <svg xmlns="http://www.w3.org/2000/svg" width="451" height="437">
        <motion.path
          d="M 239 17 C 142 17 48.5 103 48.5 213.5 C 48.5 324 126 408 244 408 C 362 408 412 319 412 213.5 C 412 108 334 68.5 244 68.5 C 154 68.5 102.68 135.079 99 213.5 C 95.32 291.921 157 350 231 345.5 C 305 341 357.5 290 357.5 219.5 C 357.5 149 314 121 244 121 C 174 121 151.5 167 151.5 213.5 C 151.5 260 176 286.5 224.5 286.5 C 273 286.5 296.5 253 296.5 218.5 C 296.5 184 270 177 244 177 C 218 177 197 198 197 218.5 C 197 239 206 250.5 225.5 250.5 C 245 250.5 253 242 253 218.5"
          fill="transparent"
          strokeWidth="12"
          stroke="rgba(255, 255, 255, 0.69)"
          strokeLinecap="round"
          initial={{ pathLength: 0 }}
          animate={{ pathLength: 1 }}
          transition={transition}
        />
      </svg>
      <motion.div
        className="box"
        initial={{ offsetDistance: "0%", scale: 2.5 }}
        animate={{ offsetDistance: "100%", scale: 1 }}
        transition={transition}
      />
    </div>
  )
}

Example source: Matt Perry: https://codesandbox.io/s/framer-motion-motion-along-a-path-41i3v

Upvotes: 14

Related Questions