How do you animate fill with Framer Motion? - reactjs

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

Related

make SVG document responsive with the viewbox based on aspect ratio

I have created this codesandbox to illustrate the problem.
I have this ResponsiveSVG component:
export function ResponsiveSVG({
height,
width,
children,
origin = { x: 0, y: 0 },
preserveAspectRatio = "xMidYMid meet",
innerRef,
className,
...props
}: ResponsiveSVGProps): JSX.Element {
const aspect = height === 0 ? 1 : width / height;
const adjustedHeight = Math.ceil(width / aspect);
return (
<div
data-testid="cutting-svg-container"
style={{
position: 'relative',
overflow: 'visible',
height: '1px',
}}
>
<svg
style={{ overflow: 'visible' }}
className={className}
preserveAspectRatio={preserveAspectRatio}
width={width}
height={adjustedHeight}
viewBox={`${origin.x} ${origin.y} ${width} ${height}`}
ref={innerRef}
{...props}
>
{children}
</svg>
</div>
);
}
I would like my ResponsiveSVG component to know how to fit its content to the full width of the container while keeping the aspect ratio the same.
In the example I have a circle as a child of the svg document:
<ResponsiveSVG width={width} height={height}>
<circle cx={width / 2} cy={height / 2} r={radius} />
</ResponsiveSVG>
On desktop, it looks like this:
But in mobile view, it looks like this:
My calculations make the viewBox width and height the same as the actual width and height I am passing in, so no change happens.
The viewBox coordinates are exactly the same as the viewport.
Passing the width and height of the containing element to the viewBox is actually the wrong direction. viewBox defines the canvas on which the svg content elements are drawn: if you draw a circle with r="50", the viewBox width value must be at least 100, otherwise, it will never fit. If the center is at cx=50, the viewBox x value must not be greater than 0, otherwise the left side is cut off.
Responsiveness in SVG is achieved because there needs not to be any relation between the viewBox values and the width and height of the <svg> element. The canvas the child elements are drawn on is always fitted to the parent element dimensions.
In short, if all you want to achieve is to fit a circle in a <svg> element, choose a arbitrary radius r, set cx=cy=r and viewBox="0 0 2r 2r", and it will work. There is no need to know anything explicit about the parent size.
If you want your <svg> element to be the width of the container, set it so: width: 100%. The default height is auto, so no need to write that or preserveAspectRatio. It will size itself such that the canvas defined by viewBox fits itself.
<div
style="
position: relative;
overflow: visible;
height: 1px;
"
>
<svg
style="
width: 100%;
overflow: visible;
"
viewBox="0 0 100 100"
>
<circle cx="50" cy="50" r="50" />
</svg>
</div>

Stretch a SVG image along the x-axis

I'm looking to stretch my SVG image along the horizontal axis, which is acting as a background for some HTML divs. It is a simple bar that takes up the middle third horizontally :
It must not move on the vertical, and when horizontal is stretched, the image also must stretch so that the bar takes up the middle third.
Currently if I stretch horizontally, it maintains its vertical position which is good, but the SVG does not take up a third but maintains its original width:
Here is the React code :
function BackgroundSVG() {
let outerDiv = {
position:"relative",
border: "3px solid red",
}
let svgStyle = {
position:"absolute",
top:"0px",
border: "3px solid pink",
zIndex: -1,
width: "100%",
height: "200px"
}
return (
<div style={outerDiv}>
<div>Lorem ipsum .......</div>
<svg style={svgStyle}
viewBox={`0 0 ${600} ${200} `}
xmlns="http://www.w3.org/2000/svg">
<path d={`M ${200} ${100} H ${400}`} strokeWidth={12} stroke="blue"/>
</svg>
</div>
);
}
Here is a code pen
https://codepen.io/oliverwatkins/pen/MWqWmQz
How do I stretch the image along the x-axis, while preserving things on the y-axis?
I have tried to play around with the widths and heights but that does not seem to work. I assume that if I keep width at 100% and height at a 200px that this would solve the problem.
Default preserveAspectRatio attribute value is xMidYMid meet
so that browser try to maintain image aspect ratio.
For stretching SVG image, we need to set
preserveAspectRatio="none"

Framer Motion - Different Animations in Mapped elements

I am trying to loop over some data and map them to an animated SVG morph but every component instance has different paths.
Framer motion seems to animate them exactly the same even though there paths are different. Any idea how do I make the animations between elements different instead of all of them matching?
Here's a codesandbox describing the issue. Notice that the repeated Dog blob is running through the exact paths animations, even though paths passed to <animated.path
Inside the Blob component for the motion.path element, perhaps the d: paths should be d: gallery.paths?
Otherwise it seems that the paths to be animated for both Blob would be the original const paths without randomized by randomItem.multiple(), hence the exactly same morphing animations.
Update
For the second issue, it seems that both clipPath share the same id, which is a global value, unlike the scoped key. This resulted in just the first clip path was used, and is solved by giving each of them unique id.
With this fix I can see 2 different morphing animations on my end now, so hopefully this solved all the problems.
<g>
<defs>
<clipPath id={`shape-${gallery.key}`}>
<LayoutGroup id={gallery.key}>
<motion.path
layoutId={gallery.key}
style={{
fill: "#9565c8",
transform: "translate(200px, 200px)",
}}
animate={{
d: gallery.paths,
repeatCount: Infinity,
autoReverse: true,
}}
transition={{
repeat: Infinity,
ease: "easeInOut",
duration: gallery.paths.length,
reverse: true,
repeatType: "reverse",
bounce: true,
bounceStiffness: 10,
}}
/>
</LayoutGroup>
</clipPath>
</defs>
<image
style={{ position: "absolute", top: 100, left: 100 }}
clipPath={`url(#shape-${gallery.key})`}
xlinkHref={gallery.image}
/>
</g>

Draw a dashed path using Framer Motion in React

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

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

Resources