Why framer-motion does not animate array of items, like single items? - reactjs

I'm using framer-motion to animate a list.
I want items to slide from left, one by one.
Following the docs, I can animate the items one by one, when they are single, but not when mapping through an array.
const items = ["one", "two", "three"];
function App() {
const list = {
visible: {
opacity: 1,
transition: {
when: "beforeChildren",
staggerChildren: 0.3,
},
},
hidden: {
opacity: 0,
transition: {
when: "afterChildren",
},
},
};
const item = {
visible: { opacity: 1, x: 0 },
hidden: {
opacity: 0,
x: "-100vw",
},
};
return (
<motion.ul initial="hidden" animate="visible" variants={list}>
// This does not work as expected.
{items.map((item, i) => (
<motion.li variants={item}>{i}</motion.li>
))}
// This works fine!
<motion.li variants={item}>item</motion.li>
<motion.li variants={item}>item</motion.li>
<motion.li variants={item}>item</motion.li>
</motion.ul>
);
}
What am I doing wrong?
What is the way of using staggerChildren: 0.3, in an array?
Thanks

I've found a way!
Credits to Leigh
const items = ["one", "two", "three"];
function App() {
const itemVariants = {
initial: { x: "-100vw", opacity: 0 },
animate: { x: 0, opacity: 1 },
};
return (
<ul>
{items.map((item, i) => (
<motion.li
variants={itemVariants}
initial="initial"
animate="animate"
transition={{ duration: 0.3, delay: i * 0.8 }}>
{i}
</motion.li>
))}
</ul>
);
}

Related

How do I create a dynamic useTransition react-spring animation with no from?

I have a react function that tries to live stream a bunch of mouse cursors for/to the people in the "room".
I'm updating/adding each mouse cursor with a web socket connection.
For some reason I can only get the animated react-spring element to show if I set a 'from'. It needs to be from the mouse's previous position and not from the corner / 0, 0 / fixed position.
const transitions = useTransition(mice, {
enter: item => [
{ left: item.x, top: item.y },
],
leave: item => [
{ left: item.x, top: item.y },
],
delay: 0,
})
return (
<>
{transitions((props, item) => (
<animated.div
className='mouseCursor'
key={item.id}
style={props}>
</animated.div>
))}
</>
)
This seems to have done the trick...
import { useTransition, animated, config } from 'react-spring';
// ...
const transitions = useTransition(mice, {
config: { ...config.molasses, duration: 100 },
from: {
position: 'absolute', left: 0, top: 0,
},
enter: item => [
{ left: item.x, top: item.y },
],
leave: item => [
{ left: item.x, top: item.y },
],
update: item => [
{ left: item.x, top: item.y },
],
keys: item => item.key,
delay: 0,
})

How to animate prop transform for svg with React-Native and Reanimated 2

import React, { FC } from "react";
import { G } from "react-native-svg";
import Animated, { useAnimatedProps, useDerivedValue, withSpring } from "react-native-reanimated";
import { CanvasControlledValuesProps } from "./helpers/types";
import { Candle } from "./Candle";
import { CANDLE_WIDTH } from "../../constants/sizes";
const AnimatedGroup = Animated.createAnimatedComponent(G);
export const Canvas: FC<CanvasControlledValuesProps> = ({
scaleY,
scaleX,
translateX,
offsetByY,
data,
initialDomain,
}) => {
const props = useAnimatedProps(() => {
return {
x: withSpring(translateX.value, {
damping: 20,
stiffness: 90,
}),
y: withSpring(offsetByY.value, {
damping: 20,
stiffness: 90,
}),
transform: { scale: [scaleX.value, scaleY.value] },
};
});
return (
<AnimatedGroup animatedProps={props}>
{data.map((candle, index) => (
<Candle key={index} width={CANDLE_WIDTH} {...{ candle, index }} domain={initialDomain} />
))}
</AnimatedGroup>
);
};
Good day! I need to increase or decrease the content of the AnimatedGroup, so I decided to use G but there was a problem: the scale is not applied for the AnimatedGroup, why it's like that? I have not used Aniamted.View since the quality of the Svg content, inside which the AnimatedGroup is located, will be lost.
const style = useAnimatedStyle(() => {
return {
transform: [
{
translateX: offsetByX.value,
},
{
translateX: withSpring(translateX.value, springConfig),
},
{
translateY: withSpring(adaptation.value.offsetByY, springConfig),
},
{
scaleX: scaleX.value,
},
{
scaleY: withSpring(adaptation.value.scaleY, springConfig),
},
],
};
});
return (
<AnimatedGroup style={style}>
{data.map((candle, index) => (
<Candle key={index} width={CANDLE_WIDTH} {...{ candle, index }} domain={initialDomain} />
))}
</AnimatedGroup>
);
The solution is to add animated styles for the Animated Group. Or you can use the matrix property like this:
const props = useAnimatedProps(() => {
return {
x: withSpring(translateX.value, {
damping: 20,
stiffness: 90,
}),
y: withSpring(offsetByY.value, {
damping: 20,
stiffness: 90,
}),
matrix: [scaleX.value, 0, 0, scaleY.value, 0, 0],
};
});
You can read about the work of matrix here https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform

Implement a random positioning but to also cover the viewport in React/Next.js

I've got a bunch of images flashing on a preloader screen of a Next.js application with GSAP like a collage and I'm using a ${Math.floor((Math.random() - 0.5)* 500)}px to randomly splatter them on the screen. The issue I'm have is that although it works very well, 'random' doesn't mean it covers the entire screen and it often leave gaps in the view. Is there a way of still assigning random positioning but also covering the entire the viewport?
import { useEffect, useRef, useState, useContext } from 'react'
import { gsap, Linear } from 'gsap'
import imagesLoaded from 'imagesloaded'
import styles from '../../styles/Preloader.module.scss'
import ScrollContext from "../Context"
const images = [
{
src: "/preloader/img.png",
landscape: true,
alt: "Code",
styles: {
position: "relative",
top: `${Math.floor((Math.random() - 0.5)* 500)}px`,
left: `${Math.floor((Math.random() - 0.6)* 900)}px`
}
},
{
src: "/preloader/img.png",
landscape: true,
alt: "Styleguide",
styles: {
position: "relative",
top: `${Math.floor((Math.random() - 0.5)* 500)}px`,
left: `${Math.floor((Math.random() - 0.6)* 900)}px`
}
},
{
src: "/preloader/img.png",
landscape: true,
alt: "Office",
styles: {
position: "relative",
top: `${Math.floor((Math.random() - 0.5)* 500)}px`,
left: `${Math.floor((Math.random() - 0.6)* 900)}px`
}
},
{
src: "/preloader/img.png",
landscape: true,
alt: "Device screens",
styles: {
position: "relative",
top: `${Math.floor((Math.random() - 0.5)* 500)}px`,
left: `${Math.floor((Math.random() - 0.6)* 900)}px`
}
},
{
src: "/preloader/img.png",
landscape: true,
alt: "Zoom meetings",
styles: {
position: "relative",
top: `${Math.floor((Math.random() - 0.5)* 500)}px`,
left: `${Math.floor((Math.random() - 0.6)* 900)}px`
}
},
]
const Preloader = () => {
let [progress, setProgress] = useState(0);
const scrollingStatus = useContext(ScrollContext);
const {scrolable, setScrollable} = scrollingStatus;
const indicatorRef = useRef(null);
const welcomeRef = useRef(null);
const imgRef = useRef(null);
const TLImages = gsap.timeline({ paused: true });
const [rendered, setRendered] = useState(false);
useEffect(() => {
setRendered(true)
}, [])
useEffect(() => {
if (typeof document !== "undefined") {
document.body.style.overflowY = scrolable ? "auto" : "hidden";
}
console.log(scrolable)
}, [scrolable]);
useEffect(() => {
if (welcomeRef.current) {
gsap.fromTo(welcomeRef.current, { alpha: 0, duration: 40 }, { alpha: 1 });
setScrollable(false);
}
TLImages.to(".imagesWrapper", { alpha: 1, ease: Linear.easeNone });
TLImages.add(
gsap.fromTo(
".images",
{ visibility: "hidden", stagger: 0.5 },
{ visibility: "visible", stagger: 0.5 }
)
);
TLImages.add(
gsap.to(".preloader", {
alpha: 0,
onComplete: function () {
setScrollable(true); //scroll for the rest of the home page
},
})
);
}, []);
useEffect(() => {
setTimeout(() => {
let loadedCount = 0;
let loadingProgress = 0;
var imgLoad = imagesLoaded("#__next");
if (imgLoad.images.length === 0) {
loadingProgress = 100;
doneLoading();
}
imgLoad.on("progress", function (instance) {
var imagesToLoad = instance.images.length;
loadProgress(imagesToLoad);
});
function loadProgress(imagesToLoad) {
loadedCount++;
loadingProgress = (loadedCount / imagesToLoad) * 100;
setProgress(loadingProgress);
if (loadingProgress === 100) {
doneLoading();
}
}
function doneLoading() {
gsap.to(indicatorRef.current, {
width: "100%",
onComplete: function () {
gsap.to(welcomeRef.current, {
alpha: 0,
duration: 0.4,
onComplete: function () {
TLImages.play();
},
});
},
});
}
}, 1000);
}, []);
return (
<div className={`${styles.preloader_container} preloader`} style={scrolable ? { zIndex: "-1" } : {zIndex: "99999"}}>
<div className={styles.preloader_wrapper}>
<div className={styles.preloader_inner_wrapper}>
<div className={styles.future} ref={welcomeRef}>
<p className={styles.mid_uppercase}>Do you want to see what the future of digital services looks like?</p>
<div className={styles.progress_bar}>
<div className={styles.progress} ref={indicatorRef}></div>
</div>
</div>
<div className={`${styles.images} imagesWrapper`}>
<div className={styles.imagesInner}>
{images.map((item, idx) => (
<figure
ref={imgRef}
className={`${styles.figure} ${item.landscape ? styles.landscape : styles.portrait} images`}
key={`img-${idx}`}
>
<img src={item.src} alt={item.alt} style={rendered ? item.styles : {}} className={styles.image} />
</figure>
))}
</div>
</div>
</div>
</div>
</div>
);
};
export default Preloader

Framer Motion length transition on SVG line

So if I know that I can transition an svg path with something like the below:
const pathVariants = {
inital: {
opacity: 0,
pathLength: 0
},
final: {
opacity: 1,
pathLength: 1,
transition: {
duration: 2,
ease: "easeInOut"
}
}
};
//...
<motion.path
variants={pathVariants}
/>
but I can't seem to get the syntax for a motion.line. I tried lineLength instead of pathLength, but that didn't seem to work. Thoughts?
looks like there is a workaround like so:
const lineVariants = {
animate: {
strokeDashoffset: 0,
transition: {
duration: 1
}
},
initial: {
strokeDashoffset: 100
}
};
//...
<motion.line
variants={lineVariants}
animate="animate"
initial="initial"
x1={lineX1}
y1={lineY1}
x2={lineX2}
y2={lineY2}
strokeWidth={stroke}
pathLength="100"
strokeDasharray="100"
/>

Why aren't Victory Charts components composable?

I'm trying to create a React component that is a Line and Scatter chart to look like this:
The React component for a single line with circles looks like this:
function Line ({ color, chartData }) {
return (
<VictoryGroup data={chartData}>
<VictoryLine
style={{ data: { stroke: color } }}
/>
<VictoryScatter
style={{
data: {
stroke: color,
fill: 'white',
strokeWidth: 3
}
}}
/>
</VictoryGroup>
);
}
I am trying to consume the component like so:
function MyCoolChart () {
return (
<VictoryChart>
<Line
color='#349CEE'
chartData={data1}
/>
<Line
color='#715EBD'
chartData={data2}
/>
</VictoryChart>
);
}
But the Line components aren't rendered. They're only rendered if I call them directly as a function, like so:
export default function MyCoolChart () {
return (
<VictoryChart>
{Line({ color: '#349CEE', chartData: data1 })}
{Line({ color: '#715EBD', chartData: data2 })}
</VictoryChart>
);
}
I'm trying to make a reusable component, so I'd rather not have to consume it as a function. I also want to understand why this is happening. Thanks!
For reference, the values of data1 and data2:
const data1 = [
{ x: 'M', y: 2 },
{ x: 'Tu', y: 3 },
{ x: 'W', y: 5 },
{ x: 'Th', y: 0 },
{ x: 'F', y: 7 }
];
const data2 = [
{ x: 'M', y: 1 },
{ x: 'Tu', y: 5 },
{ x: 'W', y: 5 },
{ x: 'Th', y: 7 },
{ x: 'F', y: 6 }
];
Thanks to #boygirl for responding to my github issue
Turns out Victory passes a few props down of its own that need to be passed along for things to be rendered correctly. Examples of this are domain and scale. Here's my updated component:
function Line ({ color, ...other }) {
return (
<VictoryGroup {...other}>
<VictoryLine
style={{ data: { stroke: color } }}
/>
<VictoryScatter
style={{
data: {
stroke: color,
fill: 'white',
strokeWidth: 3
}
}}
/>
</VictoryGroup>
);
}
And it is now consumed like so:
function MyCoolChart () {
return (
<VictoryChart>
<Line
color='#349CEE'
data={data1}
/>
<Line
color='#715EBD'
data={data2}
/>
</VictoryChart>
);
}

Resources