Draw a dashed path using Framer Motion in React - reactjs

I'm trying to draw a dashed path, or at least give that illusion, using Framer Motion. Think animating a foot path on a treasure map. Animating the path length seems to be a common method, and so I've implemented it like below.
<motion.span
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
d="...a list of coordinates"
stroke="#000"
strokeWidth="5"
strokeDasharray="8"
/>
But it appears animating the path length doesn't work well with strokeDasharray. When I add the strokeDasharray value using the attribute, the path length animates but the strokeDasharray value, when inspected, reads 2000px instead of 8px. And when I add the strokeDasharray using CSS or inline styling, the path is dashed correctly, but the animation doesn't work.
From what I've read, strokeDasharray uses the path length when doing it's computations, so I'm guessing the initial "0" value is throwing things off. Might be way off. I don't know.
Is there a simple fix here? Or should I reassess how I go about the animation? Thank you!

Not a solution using Framer Motion, but found this pen by Ruskinz that does the job using some css animation. The HTML looks like this:
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="340" height="333" viewBox="0 0 340 333">
<defs>
<path id="path1" d="M66.039,133.545c0,0-21-57,18-67s49-4,65,8s30,41,53,27s66,4,58,32s-5,44,18,57s22,46,0,45s-54-40-68-16s-40,88-83,48s11-61-11-80s-79-7-70-41 C46.039,146.545,53.039,128.545,66.039,133.545z" />
<mask id="mask1"><use class="mask" xlink:href="#path1"></mask>
</defs>
<use class="paths" xlink:href="#path1" mask="url(#mask1)" />
</svg>
And the CSS looks like this:
.paths {
fill: none;
stroke: grey;
stroke-dasharray: 5;
stroke-width: 5;
stroke-linejoin: round;
}
.mask {
fill: none;
stroke: white;
stroke-width: 10;
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
animation: dash 5s linear alternate infinite;
}
/* does not work in IE, need JS to animate there */
#keyframes dash {
from {
stroke-dashoffset: 1000;
}
to {
stroke-dashoffset: 0;
}
}
See the full pen at https://codepen.io/elliz/pen/prYqwx

I had the same issue because i wanted to animate it with framer-motion and not in css. What i just did is i just put an exact copy of the path with the dashedArray line below the path which i'm going to animate.
It will act as an overlay. I just gave it the stroke color of the background and tweaked the stroke-width. I don't know how it would be with a linearGradient background. But in my case with a static background color it worked.
import { motion } from 'framer-motion';
export default function Path({ pathColor, bg }) {
return (
<svg
width="245.24878"
height="233.49042"
viewBox="0 0 64.888737 61.777671"
version="1.1"
id="svg1033">
<defs
id="defs1030" />
<g
id="layer1"
transform="translate(-20.472293,-22.027827)">
<g
id="g484"
transform="translate(11.886667,6.306109)"
>
<motion.path
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{
pathLength: { delay: 0.4, type: "tween", duration: 3, bounce: 0 }
}}
stroke={pathColor}
strokeDasharray='3.846, 1.282'
strokeDashoffset='0'
strokeWidth='0.641'
style={{ fill: 'none', fillRule: 'evenodd', strokeLinejoin: 'round' }}
d="m 70.258127,15.782623 c 0,0 -1.867161,10.194243 -5.854843,12.473363 -9.471023,5.413069 -22.204956,-6.41444 -32.583479,-3.054701 -9.553598,3.092694 -21.015474,9.948708 -22.6557013,19.855557 -1.7758628,10.726077 5.8258513,25.311914 16.2917403,28.255989 11.258271,3.166974 19.313188,-18.990719 30.80157,-16.800859 5.208004,0.992724 10.182339,12.218805 10.182339,12.218805"
id="path1154"
/>
<path
stroke={bg}
strokeDasharray='3.846, 2.282'
strokeDashoffset='0'
strokeWidth='1.641'
style={{ fill: 'none', fillRule: 'evenodd', strokeLinejoin: 'round' }}
d="m 70.258127,15.782623 c 0,0 -1.867161,10.194243 -5.854843,12.473363 -9.471023,5.413069 -22.204956,-6.41444 -32.583479,-3.054701 -9.553598,3.092694 -21.015474,9.948708 -22.6557013,19.855557 -1.7758628,10.726077 5.8258513,25.311914 16.2917403,28.255989 11.258271,3.166974 19.313188,-18.990719 30.80157,-16.800859 5.208004,0.992724 10.182339,12.218805 10.182339,12.218805"
id="path1155"
/>
</g>
</g>
</svg>
);
}

Related

freamer motion pathLength props in SVG animation not working

i got the below svg , i wanted the svg image to be drawn on the screen as soon as the page loads but just only the opacity aspect of the animation is playing , am i doing anything wrong here? , below i defined my variant ina saparate file then imported it to the component file , i set the pathLength to o at the hidden stage and 1 at the animate stage with duration of 2
export const pathVariants = {
hidden: {
opacity: 0,
pathLength: 0,
},
visible: {
opacity: 1,
pathLength: 1,
transition: {
duration: 2,
ease: "easeInOut",
},
},
};
import { motion } from "framer-motion";
import { pathVariants } from "../animations/motion-variants";
<Box className={classes.musicnote}>
<svg
version="1.1"
id="musical_notes"
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
width="180px"
height="180px"
viewBox="0 0 353 413"
enable-background="new 0 0 353 413"
>
<motion.path
fill-rule="evenodd"
clip-rule="evenodd"
d="M253.084,7.255c14.601,14.299,22.646,33.409,41.014,44.169
c14.282,8.366,35.335,8.5,44.169,22.084c5.939,9.133,7.419,27.392,0,35.756c-5.068-35.817-35.035-32.236-57.841-49.428
c-5.958-6.66-11.919-13.321-17.878-19.981c4.557,41.711,9.115,83.435,13.671,125.146c2.243,12.553,8.561,32.938,4.207,46.273
c-4.949,15.16-21.632,26.625-37.859,30.498c-30.948,7.385-66.418-20.607-55.737-51.531c11.528-33.374,57.613-34.349,83.08-14.723
C264.303,119.437,258.693,63.337,253.084,7.255z M91.131,16.72c18.335,0.109,23.3,7.739,29.446,19.981
c24.146,48.092-3.925,92.918-25.239,125.146c3.855,16.124,7.712,32.252,11.568,48.376c83.31-2.216,86.255,108.859,28.394,127.249
c20.391,66.648-69.765,92.072-88.338,46.272c0.701-3.504,1.402-7.011,2.104-10.516c6.455-4.587,11.014-5.638,19.981-3.155
c3.118,4.554,4.93,6.783,5.258,14.724c-3.031,5.075-6.147,5.788-2.103,9.464c34.421,22.119,59.37-18.465,51.531-51.53
C53.013,354.385-7.3,304.607,16.464,229.152c12.56-39.879,41.575-60.509,63.099-90.441C69.406,100.691,53.458,37.627,91.131,16.72z
M94.286,27.236c-27.187,17.962-17.569,72.817-9.465,105.165c0.351-0.701,0.702-1.402,1.052-2.104
c15.737-12.747,50.499-82.22,15.774-100.958C99.194,28.639,96.739,27.938,94.286,27.236z M86.925,171.312
c-17.209,23.01-38.111,44.916-47.324,75.719c-12.567,42.011,11.587,79.395,41.014,87.287c14.073,3.774,29.021-1.189,41.014-3.155
c0-1.402,0-2.804,0-4.207c-7.36-35.051-14.724-70.113-22.084-105.165c-24.299,5.054-45.932,43.274-32.601,74.667
c5.608,7.711,11.218,15.425,16.826,23.136c-1.402,0-2.805,0-4.206,0c-31.384-15.551-30.442-66.646-9.465-92.544
c6.799-8.395,18.998-9.923,27.343-16.826C94.635,200.104,93.974,177.967,86.925,171.312z M109.009,219.688
c8.412,34.701,16.827,69.412,25.239,104.113C172.229,297.401,164.674,221.407,109.009,219.688z M331.958,276.477
c-0.188,25.195-21.731,102.605-35.756,112.526c-8.288,5.863-32.662,13.989-43.118,4.206c-3.886-2.521-4.219-3.958-4.207-10.516
c6.199-14.664,30.017-25.876,49.428-16.827c3.337-18.992,16.644-43.981,12.62-62.047c-0.351,0-0.702,0-1.052,0
c-7.049,0.944-63.298,9.471-64.15,10.517c-13.705,27.973-22.646,121.783-77.821,84.132c0-3.505,0-7.011,0-10.517
c8.457-12.669,24.219-19.98,45.22-16.826c1.588-11.785,18.99-73.682,25.24-77.822c11.615-5.578,29.193,0.255,43.118-2.103
C301.044,287.886,312.064,277.23,331.958,276.477z M316.183,286.993c-15.666,10.852-43.111,10.694-66.253,13.671
c-0.351,1.402-0.701,2.805-1.052,4.207c0.701,0,1.402,0,2.104,0c17.538,7.495,54.068-5.281,65.202-11.568
C316.183,291.2,316.183,289.096,316.183,286.993z"
variants={pathVariants}
initial="hidden"
animate="visible"
/>
</svg>
</Box>

Animating svg text using framer motion in React

I would like to animate the outline of letters of an svg text in React using Framer Motion, such that the line starts at certain point, and then completed gradually over a duration. I have the following example code
import { useState, useRef, useEffect } from "react";
import { motion } from "framer-motion";
import "./styles.css";
export default function App() {
const [letterLength, setLetterLength] = useState(0);
const letterRef = useRef();
useEffect(() => {
setLetterLength(letterRef.current.getTotalLength());
}, []);
return (
<svg
id="logo"
width="998"
height="108"
viewBox="0 0 998 108"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<mask
id="path-1-outside-1"
maskUnits="userSpaceOnUse"
x="0.867188"
y="0.21875"
width="998"
height="108"
fill="black"
>
<rect fill="white" x="0.867188" y="0.21875" width="998" height="108" />
<path d="M15.3672 105H1.86719V2.625H15.3672V105Z" />
</mask>
<motion.path
initial={{
strokeDasharray: letterLength,
strokeDashoffset: letterLength
}}
animate={{
strokeDasharray: letterLength,
strokeDashoffset: 0
}}
transition={{ duration: 2 }}
d="M15.3672 105H1.86719V2.625H15.3672V105Z"
stroke="#EAE3DC"
strokeWidth="2"
mask="url(#path-1-outside-1)"
ref={letterRef}
/>
</svg>
);
}
The above code however, behaves strangely. It seems that the line is animated multiple times, before the desired result, which I don't want. I don't have this issue when animating the letters using vanilla JS and CSS. I think the issue has to do with the state variables, but I am not sure what it is exactly.
This is the code on codesandbox.
The weirdness is coming from the size of the dash array being animated. I don't think this was your intention, but letterLength is initialized to 0 and then changed to 230 on the second render.
I found this out by just setting letterLength to a const value.
I would suggest not messing with refs here and just using percentages
<motion.path
initial={{
strokeDasharray: "100%",
strokeDashoffset: "100%"
}}
animate={{
strokeDashoffset: "0%"
}}
transition={{ duration: 2 }}
d="M15.3672 105H1.86719V2.625H15.3672V105Z"
stroke="#EAE3DC"
strokeWidth="2"
mask="url(#path-1-outside-1)"
/>
Like this: https://codesandbox.io/s/framer-motion-animate-stroke-with-dasharrayoffset-ezyuj?file=/src/App.js
Note: I have yet to find a nice way of using refs in animation without just hiding the elements with opacity during the ref initialization. Let me know if you find anything on the subject 🧐
**Edit from later in the day: **
You can also just set pathLength to 100 so you know the length ahead of time.
<motion.path
// this line is the important part
pathLength={100}
initial={{
strokeDasharray: 100,
strokeDashoffset: 100
}}
animate={{
strokeDashoffset: 0
}}
transition={{ duration: 2 }}
d="M15.3672 105H1.86719V2.625H15.3672V105Z"
stroke="#aceca1"
strokeWidth="2"
mask="url(#path-1-outside-1)"
/>
Thanks #kirdes https://discordapp.com/channels/341919693348536320/716908973713784904/855851823578218507

How do you animate fill with Framer Motion?

I'm using the following to try to animate a simple square from red to green with framer motion. If I use the exact same code with opacity, everything works. Likewise if I transform the square with x and y. But with fill, as soon as the square renders it changes fill, and there is no transition. After a few hours of trying to understand why this is, I can't find an answer.
In the docs it says "For instance, physical properties like x or scale will be animated via a spring simulation. Whereas values like opacity or color will be animated with a tween."
It makes no mention of whether I need to do more to create this tween.
Any help would be hugely appreciated, thanks.
const pathVariants = {
notSwitched: {
fill: 'red',
},
switched: {
fill: 'green',
},
}
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="600"
height="300"
viewBox="0 0 600 300"
>
<motion.path
initial="switched"
animate="notSwitched"
variants={pathVariants}
id="square"
d="M38,212V88H162V212Z"
/>
</svg>
)

Manipulate svg circle origin to create rotation animation around center

I have a loader (spinner) drawn on a page via two <circle />. Need to spin both paths in a different direction with origin centered, so, circles spin around the center of an SVG and don't translate, per say.
Trying to animate it transform: rotate(360deg). Paths go haywire and have origin somewhere else. Tried managing viewBox for intended results and didn't succeed.
import React, { PureComponent } from 'react';
import styled from 'styled-components';
import { prop } from 'styled-tools';
class Loader extends PureComponent {
render() {
return (
<Spinner
xmlns="http://www.w3.org/2000/svg"
width="200"
height="200"
preserveAspectRatio="xMidYMid"
viewBox="0 0 100 100"
>
<circle
className='outer'
cx="50"
cy="50"
r="40"
fill="none"
stroke="#374a67"
stroke-dasharray="63 63"
stroke-linecap="round"
stroke-width="4"
/>
<circle
className='inner'
cx="50"
cy="50"
r="35"
fill="none"
stroke="#d50000"
stroke-dasharray="55 55"
stroke-dashoffset="55"
stroke-linecap="round"
stroke-width="4"
/>
</Spinner>
)
}
}
const Spinner = styled.svg`
& .outer {
animation: rotate 2s linear infinite;
}
& .inner {
animation: reverseRotate 2s linear infinite;
}
#keyframes rotate {
100% {
transform: rotate(360deg);
}
}
#keyframes reverseRotate {
100% {
transform: rotate(-360deg);
}
}
`;
export default Loader;
Don't know how to make an actual working snippet out of my piece of code, sry
Here's an example of my current animation:
You need to set the transform-origin in the center of your svg. However you may do it differently. Instead of animating the transform you may animate the stroke-dashoffset like this:
.outer {
stroke-dashoffset:0;
animation: rotate 2s linear infinite;
}
.inner {
animation: reverseRotate 2s linear infinite;
}
#keyframes rotate {
100% {
stroke-dashoffset:126px;
}
}
#keyframes reverseRotate {
100% {
stroke-dashoffset:-55px;
}
}
svg{border:1px solid}
<svg xmlns="http://www.w3.org/2000/svg"
width="200"
height="200"
preserveAspectRatio="xMidYMid"
viewBox="0 0 100 100"
>
<circle
class='outer'
cx="50"
cy="50"
r="40"
fill="none"
stroke="#374a67"
stroke-dasharray="63"
stroke-linecap="round"
stroke-width="4"
/>
<circle
class='inner'
cx="50"
cy="50"
r="35"
fill="none"
stroke="#d50000"
stroke-dasharray="55"
stroke-dashoffset="55"
stroke-linecap="round"
stroke-width="4"
/>
</svg>
Welcome to Stack.
You need to make a few small tweaks to get it working.
Just use one animation that goes from 0% to 100%.
Animate from 0deg to 360deg
#keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
For the reverse animation, you can reverse the direction using
animation-direction: alternate; in your CSS

How to animate svg picture after a button click in React

I'm building a simple App in react. And I'm trying to change an svg fill color when I click on like button. I've researched around and many developers use React Transition Group as a way, but I'm having hard time understanding how it works in this case.
Each time I click on like button, it increments the number and I want to change the color of svg background when it does. I've tried this way but it doesn't work. What am I doing wrong here?
Here is code.
<button onClick={this.props.increment.bind(null,index)}>
<span>{this.state.portfLikes}</span>
<CSSTransition
key="heart"
classNames="anim"
timeout={{ enter: 500, exit: 300 }}>
<span className="like-icon" key="heart-icon"><Like/></span>
</CSSTransition>
</button>
Like svg as a component.
import React from 'react'
class Like extends React.Component{
render() {
return (
<svg viewBox="0 0 64 64" width="64px" xmlns="http://www.w3.org/2000/svg">
<g id="Layer_1"><g><circle cx="32" cy="32" fill="#a3bed7" r="32"/></g><g opacity="0.2"><g><path d="M49.982,31.003c-0.094-5.522-4.574-10.442-10.107-10.442c-3.2,0-6.019,1.674-7.875,4.131 c-1.856-2.457-4.676-4.131-7.875-4.131c-5.533,0-10.012,4.921-10.107,10.442H14c0,0.034,0.007,0.065,0.007,0.099 c0,0.025-0.007,0.049-0.007,0.076c0,0.155,0.038,0.272,0.045,0.421c0.495,14.071,17.813,19.84,17.813,19.84 s17.572-5.762,18.092-19.818C49.959,31.464,50,31.34,50,31.178c0-0.027-0.007-0.052-0.007-0.076c0-0.036,0.007-0.065,0.007-0.099 H49.982z" fill="#231F20"/></g></g><g><g><path d="M49.982,29.003c-0.094-5.522-4.574-10.442-10.107-10.442c-3.2,0-6.019,1.674-7.875,4.131 c-1.856-2.457-4.676-4.131-7.875-4.131c-5.533,0-10.012,4.921-10.107,10.442H14c0,0.034,0.007,0.065,0.007,0.099c0,0.025-0.007,0.049-0.007,0.076c0,0.155,0.038,0.272,0.045,0.421c0.495,14.071,17.813,19.84,17.813,19.84 s17.572-5.762,18.092-19.818C49.959,29.464,50,29.34,50,29.178c0-0.027-0.007-0.052-0.007-0.076c0-0.036,0.007-0.065,0.007-0.099 H49.982z" fill="#f5f5f5"/></g></g></g><g id="Layer_2"/></svg>
)
}
}
export default Like
CSS
Note: I've picked opacity as a test to see if the animation works.
.anim-enter {
opacity: 0.12;
}
.anim-enter.anim-enter-active svg {
opacity: 0.5;
transition: opacity 500ms ease-in;
}
.anim-exit {
opacity: 1;
}
.anim-exit.anim-exit-active {
opacity: 0.01;
transition: opacity 300ms ease-in;
}
I'm also open to suggestions about other performance friendly react animation tools.
I see Sebastian has already provided an answer for using css styles and the hover pseudo element. It did seem like you wanted it to happen on a click though.
CSSTransition is only really needed when you want to animate elements as they are added to the DOM. If the items already exist, you just use all the typical css jazz to animate items (transitions and transforms).
Here is an example using react lifecycle functions to add an event listener and animate the heart when a button is clicked.
See the codepen here https://codepen.io/bluesixty/pen/gGzbrj?editors=0111.
Note: there is something goofy with codepen in the callback on the eventlistener. Codepen is not calling animatingDone to change the state back to false. (I tested on my local and it works correctly). Just refresh the window to reset it.
Javascript
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
animate: false
};
}
//------------------------------------------------------------------------
// Handle the click animations
//------------------------------------------------------------------------
componentDidMount() {
const svgToPulse = this.itemToPulse;
svgToPulse.addEventListener("animationend", this.animatingDone);
}
componentWillUnmount() {
const svgToPulse = this.itemToPulse;
svgToPulse.removeEventListener("animationend", this.animatingDone);
}
animatingDone() {
this.setState({ animate: false });
}
pulse() {
this.setState({ animate: true });
}
render() {
return (
<div>
<button onClick={() => this.pulse()}>Clicky</button>
<svg
ref={e => {
this.itemToPulse = e;
}}
className={this.state.animate ? " animate-pulse" : ""}
viewBox="0 0 64 64"
width="64px"
xmlns="http://www.w3.org/2000/svg"
>
<g id="Layer_1">
<g>
<circle cx="32" cy="32" fill="#a3bed7" r="32" />
</g>
<g opacity="0.2">
<g>
<path
d="M49.982,31.003c-0.094-5.522-4.574-10.442-10.107-10.442c-3.2,0-6.019,1.674-7.875,4.131 c-1.856-2.457-4.676-4.131-7.875-4.131c-5.533,0-10.012,4.921-10.107,10.442H14c0,0.034,0.007,0.065,0.007,0.099 c0,0.025-0.007,0.049-0.007,0.076c0,0.155,0.038,0.272,0.045,0.421c0.495,14.071,17.813,19.84,17.813,19.84 s17.572-5.762,18.092-19.818C49.959,31.464,50,31.34,50,31.178c0-0.027-0.007-0.052-0.007-0.076c0-0.036,0.007-0.065,0.007-0.099 H49.982z"
fill="#231F20"
/>
</g>
</g>
<g>
<g>
<path
d="M49.982,29.003c-0.094-5.522-4.574-10.442-10.107-10.442c-3.2,0-6.019,1.674-7.875,4.131 c-1.856-2.457-4.676-4.131-7.875-4.131c-5.533,0-10.012,4.921-10.107,10.442H14c0,0.034,0.007,0.065,0.007,0.099c0,0.025-0.007,0.049-0.007,0.076c0,0.155,0.038,0.272,0.045,0.421c0.495,14.071,17.813,19.84,17.813,19.84 s17.572-5.762,18.092-19.818C49.959,29.464,50,29.34,50,29.178c0-0.027-0.007-0.052-0.007-0.076c0-0.036,0.007-0.065,0.007-0.099 H49.982z"
fill="#f5f5f5"
/>
</g>
</g>
</g>
<g id="Layer_2" />
</svg>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
And the css
.animate-pulse {
animation: animate-pulse 300ms;
}
#keyframes animate-pulse {
0% {transform: scale(1.2)}
50% {transform: scale(1.5)}
100% {transform: scale(1.2)}
}
How it works
Clicking the button sets the animate state to true
React responds to the state change and the animate-pulse class is added to the svg element
the eventlistener waits for the animation to end and makes the call to set the animate state back to false.
react responds to the state change and removes the animate-pulse class
You might try vanilla CSS - see this simplified example with a button:
<button>
<svg height="20" width="20"><circle cx=10 cy=10 r=8 /></svg>
</button>
per se, it's just a button with a circle. I format this using CSS:
button circle {
fill: yellow;
}
as soon as I extend the CSS to do an animation on focus, I think I already achieve what you are looking for
button circle {
fill: yellow;
transition: fill 2s;
}
button:focus circle {
fill: green;
}
The button changes from yellow to green when clicked. Instead of focus, you could also assign CSS classes.
I've put the exact same code into a codepen if you want to try directly: https://codepen.io/sebredhh/pen/NaMPoP

Resources