Hey guys I wanted to add timer along with these gestures to the following react-spring example. I've been struck with this since a long time. It'll be of great assistance. Also I'm working with makestyles from material ui. It would be an additional help if you can tell me how to convert the css in to make styles too. Link to the code: https://codesandbox.io/embed/j0y0vpz59
import { render } from "react-dom";
import React, { useState, useEffect } from "react";
import { useSprings, animated, interpolate } from "react-spring";
import { useGesture } from "react-use-gesture";
import img1 from "./assets/slideImg1.jpg";
import img2 from "./assets/slideImg2.gif";
import img3 from "./assets/slideImg3.gif";
import img4 from "./assets/slideImg4.jpg";
import "./index.css";
const cards = [
img1,
img2,
img3,
img4,
"https://upload.wikimedia.org/wikipedia/en/thumb/8/88/RWS_Tarot_02_High_Priestess.jpg/690px-RWS_Tarot_02_High_Priestess.jpg",
"https://upload.wikimedia.org/wikipedia/en/d/de/RWS_Tarot_01_Magician.jpg",
];
// These two are just helpers, they curate spring data, values that are later being interpolated into css
const to = (i) => ({
x: 0,
y: i * -4,
scale: 1,
rot: -10 Math.random() * 20,
delay: i * 100,
});
const from = (i) => ({ x: 0, rot: 0, scale: 1.5, y: -1000 });
// This is being used down there in the view, it interpolates rotation and scale into a css transform
const trans = (r, s) =>
`perspective(1500px) rotateX(30deg) rotateY(${
r / 10
}deg) rotateZ(${r}deg) scale(${s})`;
function Deck() {
const [gone] = useState(() => new Set()); // The set flags all the cards that are flicked out
const [fly, setFly] = useState(false);
const [props, set] = useSprings(cards.length, (i) => ({
...to(i),
from: from(i),
})); // Create a bunch of springs using the helpers above
// Create a gesture, we're interested in down-state, delta (current-pos - click-pos), direction and velocity
const bind = useGesture(
({
args: [index],
down,
delta: [xDelta],
distance,
direction: [xDir],
velocity,
}) => {
const trigger = velocity > 0.2; // If you flick hard enough it should trigger the card to fly out
const dir = xDir < 0 ? -1 : 1; // Direction should either point left or right
if (!down && trigger) gone.add(index); // If button/finger's up and trigger velocity is reached, we flag the card ready to fly out
set((i) => {
if (index !== i) return; // We're only interested in changing spring-data for the current spring
const isGone = gone.has(index);
const x = isGone ? (200 window.innerWidth) * dir : down ? xDelta : 0; // When a card is gone it flys out left or right, otherwise goes back to zero
const rot = xDelta / 100 (isGone ? dir * 10 * velocity : 0); // How much the card tilts, flicking it harder makes it rotate faster
const scale = down ? 1.1 : 1; // Active cards lift up a bit
return {
x,
rot,
scale,
delay: undefined,
config: { friction: 50, tension: down ? 800 : isGone ? 200 : 500 },
};
});
if (!down && gone.size === cards.length)
setTimeout(() => gone.clear() || set((i) => to(i)), 600);
}
);
useEffect(() => {
setFly(false);
const timerId = setInterval(() => {
setFly(true);
}, 1000);
return () => clearInterval(timerId);
}, [fly]);
// Now we're just mapping the animated values to our view, that's it. Btw, this component only renders once. :-)
return props.map(({ x, y, rot, scale }, i) => (
<animated.div
key={i}
style={{
transform: interpolate(
[x, y],
(x, y) => `translate3d(${x}px,${y}px,0)`
),
}}
>
{/* This is the card itself, we're binding our gesture to it (and inject its index so we know which is which) */}
<animated.div
{...bind(i)}
style={{
transform: interpolate([rot, scale], trans),
backgroundImage: `url(${cards[i]})`,
}}
/>
</animated.div>
));
}
render(<Deck />, document.getElementById("root"));
//css
* {
box-sizing: border-box;
}
html,
body {
overscroll-behavior-y: contain;
margin: 0;
padding: 0;
height: 100%;
width: 100%;
user-select: none;
font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir,
helvetica neue, helvetica, ubuntu, roboto, noto, segoe ui, arial, sans-serif;
position: fixed;
overflow: hidden;
}
#root {
background: lightblue;
position: fixed;
overflow: hidden;
width: 100%;
height: 100%;
cursor: url("https://uploads.codesandbox.io/uploads/user/b3e56831-8b98-4fee-b941-0e27f39883ab/Ad1_-cursor.png")
39 39,
auto;
}
#root > div {
position: absolute;
width: 100vw;
height: 100vh;
will-change: transform;
display: flex;
align-items: center;
justify-content: center;
}
#root > div > div {
background-color: white;
background-size: auto 85%;
background-repeat: no-repeat;
background-position: center center;
width: 45vh;
max-width: 300px;
height: 85vh;
max-height: 570px;
will-change: transform;
border-radius: 10px;
box-shadow: 0 12.5px 100px -10px rgba(50, 50, 73, 0.4),
0 10px 10px -10px rgba(50, 50, 73, 0.3);
}
```[enter image description here][1]
[1]: https://i.stack.imgur.com/D77CT.png
CodePudding user response:
This is a great question!
The solution is to add a useEffect
hook that calls a setInterval
timer to update the springs.
First, the code sandbox: https://codesandbox.io/s/epic-mendeleev-w8zm7s?file=/src/index.js
And here is the useEffect hook. Notice that the callback returns a cleanup function that clears the intervals and timers.
useEffect(() => {
const timers = [];
const interval = setInterval(() => {
const index = cards.length - gone.size - 1;
gone.add(index);
set((i) => {
if (index !== i) return;
const dir = i % 2 === 0 ? -1 : 1;
const velocity = 1;
return {
x: (200 window.innerWidth) * dir,
rot: 0 / 100 dir * 10 * velocity,
scale: 1.1,
delay: undefined,
config: { friction: 50, tension: 200 },
};
});
if (gone.size === cards.length) {
const timer = setTimeout(() => {
gone.clear();
set((i) => to(i));
}, 1000);
timers.push(timer);
}
}, 3000);
return () => {
clearInterval(interval);
timers.forEach((timer) => clearTimeout(timer));
};
}, []);
When the interval callback is executed, first we determine which card to swipe off the screen. Then we add that to the gone
Set that is also shared with the useGesture
hook. This is important, because if the user swipes a card off the screen, we want to move forward and swipe the next card.
const index = cards.length - gone.size - 1;
gone.add(index);
Then we call the set
callback given to us from useSprings
. This is what does the animation. I copied most of the code from the set callback in the useGesture.
set((i) => {
if (index !== i) return;
const dir = i % 2 === 0 ? -1 : 1;
const velocity = 1;
return {
x: (200 window.innerWidth) * dir,
rot: 0 / 100 dir * 10 * velocity,
scale: 1.1,
delay: undefined,
config: { friction: 50, tension: 200 },
};
});
And finally, if we've reached the end of the list, we set a 1 second timer, and clear the list, starting over.
if (gone.size === cards.length) {
const timer = setTimeout(() => {
gone.clear();
set((i) => to(i));
}, 1000);
timers.push(timer);
}