I have two issues.
Onload, the bottom links should render only once, but they are rendered twice.
When clicking the box links at the bottom, the current links should smoothly fade out and the new ones fade in.
Instead, the new ones initially fade in stacked with the previous ones.
what am I doing wrong here?
Sandbox Link
Relevant animation code:
export default function DynamicScreen({}: IProps) {
const { screenKey } = useParams()
const [isLeavingScreen, setIsLeavingScreen] = useState(false)
const [screen, setScreen] = useState<IDynamicScreen>()
const [screenLinks, setScreenLinks] = useState<IDynamicScreenLink[]>([])
const navigate = useNavigate()
const isFirstRenderRef = useRef(true)
useEffect(() => {
if (isFirstRenderRef.current) {
isFirstRenderRef.current = false
return
}
}, [])
useEffect(() => {
const loadScreen = async () => {
console.log("Screen - Loading")
const { data: _screen } = await api.getScreen(screenKey!)
setScreen(_screen)
setScreenLinks(
[_screen.link1?.[0], _screen.link2?.[0], _screen.link3?.[0]].filter(
Boolean,
),
)
console.log("Screen - Loaded")
}
loadScreen()
}, [screenKey])
const springApiLory = useSpringRef()
const springLoryStyle = useSpring({
from: { opacity: 0, scale: 0 },
to: { opacity: 1, scale: 1 },
ref: springApiLory,
})
const springApiTitle = useSpringRef()
const springTitleStyle = useSpring({
from: { opacity: 0, transform: "translateY(-25px)" },
to: !isLeavingScreen
? { opacity: 1, transform: "translateY(0)" }
: {
opacity: 0,
transform: "translateY(-25px)",
},
ref: springApiTitle,
})
const springApiScriptHtml = useSpringRef()
const springScriptHtmlStyle = useSpring({
from: { opacity: 0, transform: "translateX(-25px)" },
to: !isLeavingScreen
? { opacity: 1, transform: "translateY(0)" }
: {
opacity: 0,
transform: "translateX(-25px)",
},
ref: springApiScriptHtml,
})
const springApiLinks = useSpringRef()
const linkTransition = useTransition(isLeavingScreen ? [] : screenLinks, {
ref: springApiLinks,
from: { opacity: 0 },
enter: { opacity: 1 },
leave: isLeavingScreen ? { opacity: 0 } : {},
trail: 100,
})
const chain = [
springApiLory,
springApiTitle,
springApiScriptHtml,
springApiLinks,
]
useChain(
!isLeavingScreen ? chain : chain.reverse(),
[...Array(chain.length).keys()].map(calcStaggerDelay),
)
const goToLink = (link: string) => {
setIsLeavingScreen(true)
setTimeout(() => {
setScreenLinks([])
navigate(`/${link}`)
setIsLeavingScreen(false)
}, 1000)
}
if (!screen) return null
return (
<>
<div className="dynamic-screen-top">
<div className="left">
<animated.div style={springTitleStyle}>
<h1>{screen.title}</h1>
</animated.div>
<animated.div style={springScriptHtmlStyle}>
<div
dangerouslySetInnerHTML={{
__html: screen.scriptHtml,
}}
/>
</animated.div>
{/* TODO screen.actions */}
</div>
<animated.div style={springLoryStyle} className="right">
💁♀️
</animated.div>
</div>
<div className="dynamic-screen-bottom">
{linkTransition((style, screenLink) => (
<animated.div
style={style}
className="dynamic-screen-bottom-link-wrap"
>
<DynamicScreenLink
linkKey={screenLink!.key}
onClick={goToLink}
text={screenLink!.text}
imageUrl={screenLink!.imageUrl}
size={screenLink!.size}
/>
</animated.div>
))}
</div>
</>
)
}
interface IProps {}
const calcStaggerDelay = (i: number) => i * 0.1 + 0.1
I don't know if this helps you continue, but what i did first of all is to set screenLinks the to an empty array when navigating to new page
const goToLink = (link: string) => {
setScreenLinks([]);
setIsLeavingScreen(true);
setTimeout(() => {
navigate(`/${link}`);
setIsLeavingScreen(false);
}, 300);
};
and also, if isLeavingScreen, then set the width to zero, in order for the next screenLinks to take whole space
const linkTransition = useTransition(isLeavingScreen ? [] : screenLinks, {
ref: springApiLinks,
from: { opacity: 0 },
enter: { opacity: 1 },
leave: isLeavingScreen ? { display: "none", width: 0 } : {},
trail: 100
});
demo
Related
I cannot find how to do a 3D carousel (aka slideshow) with React being able to show at least three elements.
There seems not to be up-to-date libraries or components for that in npm :(
Here is what it should look like:
After experimenting for a while, here is how I manage to do it using framer motion:
import './styles.css';
import { AnimatePresence, motion } from 'framer-motion';
import { useState } from 'react';
export default function App() {
const [[activeIndex, direction], setActiveIndex] = useState([0, 0]);
const items = ['🍔', '🍕', '🌭', '🍗'];
// we want the scope to be always to be in the scope of the array so that the carousel is endless
const indexInArrayScope =
((activeIndex % items.length) + items.length) % items.length;
// so that the carousel is endless, we need to repeat the items twice
// then, we slice the the array so that we only have 3 items visible at the same time
const visibleItems = [...items, ...items].slice(
indexInArrayScope,
indexInArrayScope + 3
);
const handleClick = newDirection => {
setActiveIndex(prevIndex => [prevIndex[0] + newDirection, newDirection]);
};
return (
<div className="main-wrapper">
<div className="wrapper">
{/*AnimatePresence is necessary to show the items after they are deleted because only max. 3 are shown*/}
<AnimatePresence mode="popLayout" initial={false}>
{visibleItems.map((item) => {
// The layout prop makes the elements change its position as soon as a new one is added
// The key tells framer-motion that the elements changed its position
return (
<motion.div
className="card"
key={item}
layout
custom={{
direction,
position: () => {
if (item === visibleItems[0]) {
return 'left';
} else if (item === visibleItems[1]) {
return 'center';
} else {
return 'right';
}
},
}}
variants={variants}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 1 }}
>
{item}
</motion.div>
);
})}
</AnimatePresence>
</div>
<div className="buttons">
<motion.button
whileTap={{ scale: 0.8 }}
onClick={() => handleClick(-1)}
>
◀︎
</motion.button>
<motion.button whileTap={{ scale: 0.8 }} onClick={() => handleClick(1)}>
▶︎
</motion.button>
</div>
</div>
);
}
const variants = {
enter: ({ direction }) => {
return { scale: 0.2, x: direction < 1 ? 50 : -50, opacity: 0 };
},
center: ({ position }) => {
return {
scale: position() === 'center' ? 1 : 0.7,
x: 0,
zIndex: zIndex[position()],
opacity: 1,
};
},
exit: ({ direction }) => {
return { scale: 0.2, x: direction < 1 ? -50 : 50, opacity: 0 };
},
};
const zIndex = {
left: 1,
center: 2,
right: 1,
};
Here is a code sandbox with the solution:
https://codesandbox.io/s/react-3d-carousel-wth-framer-motion-rtn6vx?file=/src/App.js
I have a list of two elements scenes rendered as <Rnd /> components from "react-rnd". What I'd like to happen is when I drag one element close to another one they should swap positions, in the list and in the UI. so [first, second] becomes [second, first]. The <Rnd /> position is controlled, this is the code I'm using:
import { useState } from "react";
import "./styles.css";
import { Rnd, Position, DraggableData } from "react-rnd";
interface IScene {
id: string;
name: string;
position: Position;
}
function Scene({
scene,
activeScene,
setActiveScene,
onDrag
}: {
scene: IScene;
activeScene: string;
setActiveScene: (id: string) => void;
onDrag: (d: DraggableData) => void;
}) {
const [dragged, setDragged] = useState(false);
return (
<Rnd
default={{
x: 0,
y: 0,
width: "200px",
height: "100px"
}}
position={scene.position}
onDragStart={() => setDragged(true)}
onDragStop={() => setDragged(false)}
onDrag={(_, d) => onDrag(d)}
onMouseDown={() => setActiveScene(scene.id)}
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: "gray",
transition: dragged ? "" : "transform 0.5s",
border: "1px solid",
borderColor: activeScene === scene.id ? "white" : "gray",
zIndex: activeScene === scene.id ? "1" : "0"
}}
>
{scene.name}
</Rnd>
);
}
const initialScenesState = [
{
id: "1",
name: "first",
position: {
x: 0,
y: 0
}
},
{
id: "2",
name: "second",
position: {
x: 200,
y: 0
}
}
];
export default function App() {
const [scenes, setScenes] = useState<IScene[]>(initialScenesState);
const [activeScene, setActiveScene] = useState<string>("");
const handleStackScenes = () => {
setScenes((scenes) => {
let currentPosition = 0;
return scenes.map((scene) => {
const result = {
...scene,
position: {
x: currentPosition,
y: 0
}
};
currentPosition += 200;
return result;
});
});
};
const swapScenes = (first: IScene, second: IScene) => {
setScenes((scenes) => {
return scenes.map((scene) => {
if (scene.id === first.id) {
return second;
} else if (scene.id === second.id) {
return first;
} else return scene;
});
});
handleStackScenes();
};
const handleDrag = (scene: IScene) => (d: DraggableData) => {
console.log(d.x);
for (let i = 0; i < scenes.length; i++) {
if (
Math.abs(scenes[i].position.x - d.x) < 30 &&
scenes[i].id !== scene.id
) {
swapScenes(scene, scenes[i]);
}
}
};
console.log(scenes);
return (
<div className="App">
{scenes.map((scene) => (
<Scene
key={scene.id}
scene={scene}
activeScene={activeScene}
setActiveScene={setActiveScene}
onDrag={handleDrag(scene)}
/>
))}
</div>
);
}
The problem that I'm facing with my code, is that when I drag the left element to the right one, the swap happens exactly how I wanted, in the other way around the swap happens but I don't see a transition effect, when I checked what happens on the console, it seems that on the second case, the dom elements don't swap, but just the content and it the first case the actual dom elements move. What am I doing wrong?
EDIT: CodeSandBox
I trying to update human.x using requestAnimationFrame and useState. I want to add elements called "humans" and use requestAnimationFrame to update the position to humans walk in "world" (using css properties). The method addHumans work but requestAnimationFrame update and empty my list.
World.tsx
import { useState, useEffect } from 'react'
import Human from './Human'
import IHuman from './Human'
type IHuman = {
id: number;
x: number;
y: number;
color: string;
}
export default function World() {
const [humans, setHumans] = useState<IHuman[]>([])
const addHuman = () => {
setHumans([...humans, {id: Math.random(), x: 0, y: 0, color: 'red'}])
}
const animate = () => {
setHumans(humans?.map((human, i) => {
return {...human, x: human.x + 1}
}));
// console.log(humans)
requestAnimationFrame(animate);
}
useEffect(() => {
requestAnimationFrame(animate);
}, []); // run once
return (
<>
<button onClick={addHuman}>new human</button>
<main>
{humans?.map(human => {
return <Human key = {human.id} x = {human.x} y = {human.y} color = {human.color} />
})}
</main>
</>
)
}
Human.tsx
export default function Human(props: any) {
return (
<div key={props.id} className="human" style={{ top: props.y + 'px', left: props.x + 'px', backgroundColor: props.color }}>{props.id}</div>
)
}
This is called a stale state. Update your code like this:
setHumans(humans => humans?.map((human, i) => {
return {...human, x: human.x + 1}
}));
I am creating a screen in functional component where I have to execute an animation when there is any event occurs ... This event could occur 1000 times when screen is open ... so I have implemented a custom component which takes position on screen and animates ....
const FloatingComponent = (props) => {
const animationView = useSharedValue(1)
const animationOpacityView = useSharedValue(1)
const animationViewStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateY: withTiming(animationView.value, {
duration: 3500
}),
}
],
opacity: withTiming(animationOpacityView.value, {
duration: 1500
})
}
})
useEffect(() => {
console.log('Component animation called')
animationView.value = -((Screen.width * 0.25))
animationOpacityView.value = 0
});
return (
<Animated.View style={[Styles.handImg, { top: props.topDistance }, animationViewStyle]}>
<Image
style={Styles.handImage}
source={require('../../../assets/images/hand.png')}
/>
</Animated.View>
);
};
To create it dynamically I implemented it like this
const driverFactory = (itemNumber) => {
console.log(itemNumber)
return (<FloatingComponent id={1} topDistance={(Screen.width * 0.25) * itemNumber} />);
};
but it never show up and executes ....
while if I add this
<FloatingDriver id={5} topDistance={(Screen.width * 0.25) * 6} />
to main screen return it always executes .... but by these I can not create n number of components at any time when i receive notification ...
I would have a parent component manage these n child components and their rendering. Here is an example showing how one might render all the components with animations and handle showing/removing them as needed.
import React from 'react';
import {Animated, Button, SafeAreaView, StyleSheet} from 'react-native';
const Particle = ({particle: {x, y}, onFinish}) => {
const opacity = React.useRef(new Animated.Value(0)).current;
React.useEffect(() => {
Animated.timing(opacity, {
toValue: 1,
duration: 2000,
useNativeDriver: true,
}).start(() => {
onFinish();
});
}, []);
return (
<Animated.View
style={{
position: 'absolute',
left: x - 10,
bottom: y - 10,
width: 20,
height: 20,
backgroundColor: 'yellow',
opacity,
}}
/>
);
};
const ParticleSystem = () => {
const [layoutRectangle, setLayoutRectangle] = React.useState();
const [particleList, setParticleList] = React.useState([]);
const currentRef = React.useRef();
const addRandomParticle = React.useCallback(() => {
if (layoutRectangle) {
const newParticle = {
id: Math.floor(Math.random() * Number.MAX_SAFE_INTEGER),
x: Math.random() * layoutRectangle.width,
y: Math.random() * layoutRectangle.height,
};
setParticleList([newParticle, ...particleList]);
}
}, [layoutRectangle, particleList]);
const removeParticle = React.useCallback(
(particle) => {
setParticleList(particleList.filter(({id}) => id !== particle.id));
},
[particleList],
);
currentRef.current = removeParticle;
return (
<SafeAreaView
style={styles.screen}
onLayout={(event) => setLayoutRectangle(event.nativeEvent.layout)}>
{particleList.map((particle) => (
<Particle
key={particle.id}
particle={particle}
onFinish={() => {
currentRef.current(particle);
}}
/>
))}
<Button title="Add particle" onPress={() => addRandomParticle()} />
</SafeAreaView>
);
};
export default ParticleSystem;
const styles = StyleSheet.create({
screen: {
flex: 1,
backgroundColor: 'black',
},
});
I tried to using this react dnd on the react typescript. sample not working on the type script project , any one know how to do that correctly on the react typescript
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
// fake data generator
const getItems = (count, offset = 0) =>
Array.from({ length: count }, (v, k) => k).map(k => ({
id: `item-${k + offset}`,
content: `Item: ${k + offset}, Random value: ${Math.round(Math.random() * 100)}`,
color: Math.random () > 0.66 ? 'pink': Math.random() > 0.5 ? 'lightgreen' : 'beige'
}))
// a little function to help us with reordering the result
const reorder = (list, startIndex, endIndex) => {
const result = Array.from(list)
const [removed] = result.splice(startIndex, 1)
result.splice(endIndex, 0, removed)
return result
}
/**
* Moves an item from one list to another list.
*/
const move = (source, destination, droppableSource, droppableDestination) => {
const sourceClone = Array.from(source)
const destClone = Array.from(destination)
const [removed] = sourceClone.splice(droppableSource.index, 1)
destClone.splice(droppableDestination.index, 0, removed)
const result = {}
result[droppableSource.droppableId] = sourceClone
result[droppableDestination.droppableId] = destClone
return result
}
const grid = 4
const getItemStyle = (isDragging, draggableStyle) => ({
// some basic styles to make the items look a bit nicer
userSelect: 'none',
padding: grid * 2,
margin: `0 0 ${grid}px 0`,
// change background colour if dragging
background: isDragging ? 'lightgreen' : 'lightgrey',
// styles we need to apply on draggables
...draggableStyle
})
const getListStyle = isDraggingOver => ({
background: isDraggingOver ? 'lightblue' : '#eee',
padding: grid,
margin: '3px',
width: 250
})
class App extends Component {
state = {
list1: getItems(5,1),
list2: getItems(4, 6),
list3: getItems(6, 10)
}
/**
* A semi-generic way to handle multiple lists. Matches
* the IDs of the droppable container to the names of the
* source arrays stored in the state.
*/
droppableIds = {
droppable1: 'list1',
droppable2: 'list2',
droppable3: 'list3'
}
getList = id => this.state[this.droppableIds[id]]
onDragEnd = result => {
const { source, destination } = result
// dropped outside the list
if (!destination) { return }
if (source.droppableId === destination.droppableId) {
const items = reorder(
this.getList(source.droppableId),
source.index,
destination.index
)
let copiedState = Object.assign({}, this.state)
if (source.droppableId === 'droppable1') {
copiedState.list1 = items
} else if (source.droppableId === 'droppable2') {
copiedState.list2 = items
} else if (source.droppableId === 'droppable3') {
copiedState.list3 = items
}
this.setState(copiedState)
} else {
const result = move(
this.getList(source.droppableId),
this.getList(destination.droppableId),
source,
destination
)
console.warn('result', result)
this.setState({
list1: result.droppable1 ? result.droppable1 : this.state.list1,
list2: result.droppable2 ? result.droppable2 : this.state.list2,
list3: result.droppable3 ? result.droppable3 : this.state.list3
})
}
}
// Normally you would want to split things out into separate components.
// But in this example everything is just done in one place for simplicity
render() {
const lists = [
{
droppableId: 'droppable1',
listId: 'list1',
title: 'List A'
},
{
droppableId: 'droppable2',
listId: 'list2',
title: 'List B'
},
{
droppableId: 'droppable3',
listId: 'list3',
title: 'List C'
},
]
return (
<div style={{ display: 'flex' }}>
<DragDropContext onDragEnd={this.onDragEnd}>
{lists.map((list, listIndex) =>
<Droppable key={'list-droppable-' + listIndex} droppableId={list.droppableId}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
style={getListStyle(snapshot.isDraggingOver)}>
<h4>
{list.title}
</h4>
{this.state[list.listId] && this.state[list.listId].map((item, index) => (
<Draggable
key={item.id}
draggableId={item.id}
index={index}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{ ...provided.draggableProps }
{ ...provided.dragHandleProps }
style={getItemStyle(
snapshot.isDragging,
provided.draggableProps.style
)}>
<div style={{ background: item.color }}>
{item.content}
</div>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
)}
</DragDropContext>
</div>
)
}
}
// Put the things into the DOM!
ReactDOM.render(<App />, document.getElementById('root'));
Try something like this.
import React, { Component } from "react";
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
// fake data generator
const getItems = (count: number, offset: number): object =>
Array.from({ length: count }, (v, k): number => k).map(
(k: number): {} => ({
id: `item-${k + offset}`,
content: `Item: ${k + offset}, Random value: ${Math.round(
Math.random() * 100
)}`,
color:
Math.random() > 0.66
? "pink"
: Math.random() > 0.5
? "lightgreen"
: "beige"
})
);
// a little function to help us with reordering the result
const reorder = (list: [], startIndex: number, endIndex: number): object => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
/**
* Moves an item from one list to another list.
*/
const move = (
source: any,
destination: any,
droppableSource: any,
droppableDestination: any
): {} => {
const sourceClone = Array.from(source);
const destClone = Array.from(destination);
const [removed] = sourceClone.splice(droppableSource.index, 1);
destClone.splice(droppableDestination.index, 0, removed);
let result: any = {};
result[droppableSource.droppableId] = sourceClone;
result[droppableDestination.droppableId] = destClone;
return result;
};
const grid = 4;
const getItemStyle = (isDragging: boolean, draggableStyle: any): object => ({
// some basic styles to make the items look a bit nicer
userSelect: "none",
padding: grid * 2,
margin: `0 0 ${grid}px 0`,
// change background colour if dragging
background: isDragging ? "lightgreen" : "lightgrey",
// styles we need to apply on draggables
...draggableStyle
});
const getListStyle = (isDraggingOver: boolean): object => ({
background: isDraggingOver ? "lightblue" : "#eee",
padding: grid,
margin: "3px",
width: 250
});
interface State {
list1: object;
list2: object;
list3: object;
}
class App extends Component<State> {
state: State = {
list1: getItems(5, 1),
list2: getItems(4, 6),
list3: getItems(6, 10)
};
/**
* A semi-generic way to handle multiple lists. Matches
* the IDs of the droppable container to the names of the
* source arrays stored in the state.
*/
droppableIds = {
droppable1: "list1",
droppable2: "list2",
droppable3: "list3"
};
getList = (id: string): any => this.state[this.droppableIds[id]];
onDragEnd = (result: any) => {
const { source, destination } = result;
// dropped outside the list
if (!destination) {
return;
}
if (source.droppableId === destination.droppableId) {
const items: object = reorder(
this.getList(source.droppableId),
source.index,
destination.index
);
let copiedState: any = Object.assign({}, this.state);
if (source.droppableId === "droppable1") {
copiedState.list1 = items;
} else if (source.droppableId === "droppable2") {
copiedState.list2 = items;
} else if (source.droppableId === "droppable3") {
copiedState.list3 = items;
}
this.setState(copiedState);
} else {
const result: any = move(
this.getList(source.droppableId),
this.getList(destination.droppableId),
source,
destination
);
this.setState({
list1: result.droppable1 ? result.droppable1 : this.state.list1,
list2: result.droppable2 ? result.droppable2 : this.state.list2,
list3: result.droppable3 ? result.droppable3 : this.state.list3
});
}
};
render() {
const lists = [
{
droppableId: "droppable1",
listId: "list1",
title: "List A"
},
{
droppableId: "droppable2",
listId: "list2",
title: "List B"
},
{
droppableId: "droppable3",
listId: "list3",
title: "List C"
}
];
return (
<div style={{ display: "flex" }}>
<DragDropContext onDragEnd={this.onDragEnd}>
{lists.map((list, listIndex) => (
<Droppable
key={"list-droppable-" + listIndex}
droppableId={list.droppableId}
>
{(provided: any, snapshot: any) => (
<div
ref={provided.innerRef}
style={getListStyle(snapshot.isDraggingOver)}
>
<h4>{list.title}</h4>
{this.state[list.listId] &&
this.state[list.listId].map((item: any, index: number) => (
<Draggable
key={item.id}
draggableId={item.id}
index={index}
>
{(provided: any, snapshot: any) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(
snapshot.isDragging,
provided.draggableProps.style
)}
>
<div style={{ background: item.color }}>
{item.content}
</div>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
))}
</DragDropContext>
</div>
);
}
}
export default App;
And here is a working example codesandox