I'm trying to rotate a wheel based on user interaction. If the user is accelerating his scroll, the wheel should spin faster. Likewise, if the user is decelerating his scroll, the wheel should stop. I'm applying these conditions with styled-components and React state, using the Skrollr library.
Here is what I have:
import React from "react";
import styled, { createGlobalStyle, css, keyframes } from "styled-components";
export default function App() {
const [previousPosition, setPreviousPosition] = React.useState(0);
const [previousDelta, setPreviousDelta] = React.useState(0);
const [speed, setSpeed] = React.useState(1);
const [isStopping, setIsStopping] = React.useState(false);
React.useEffect(() => {
skrollr.init();
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [previousPosition, previousDelta]);
const handleScroll = React.useCallback(() => {
// get delta
const delta = window.pageYOffset - previousPosition;
if (previousDelta > delta) {
// is stopping
setIsStopping(true);
setSpeed(0);
} else {
// is accelerating
// calculate delta as a percentage
const deltaAsPercentage = (delta / previousPosition) * 100;
console.log("deltaAsPercentage", deltaAsPercentage);
setIsStopping(false);
setSpeed(deltaAsPercentage);
}
setPreviousPosition(window.pageYOffset);
}, [previousPosition, previousDelta]);
return (
<Container data-10000p="transform: translateX(0%)">
<GlobalStyles />
<WheelContainer speed={speed} isStopping={isStopping}>
<Image src="wheel.png" alt="wheel" />
</WheelContainer>
</Container>
);
}
const Container = styled.div`
position: fixed;
width: 100%;
display: flex;
top: 0;
left: 0;
`;
const spinForward = keyframes`
0% {
transform: rotateZ(0deg);
}
50% {
transform: rotateZ(180deg);
}
100% {
transform: rotateZ(360deg);
}
`;
const stopWheel = keyframes`
0% {
transform: rotateZ(0deg);
}
100% {
transform: rotateZ(360deg);
}
`;
const WheelContainer = styled.div`
${({ speed, isStopping }) =>
isStopping
? css`
animation: ${stopWheel} 1s ease-out;
`
: css`
animation: ${speed
? css`
${spinForward} ${speed}s linear infinite
`
: ""};
`}
`;
const Image = styled.img``;
const GlobalStyles = createGlobalStyle`
* {
&::-webkit-scrollbar {
display: none;
}
}
`;
I'm not great at math, so I'm doing my best with what I know.
The first thing that I do on scroll is determine whether or not the wheel should be stopping or accelerating. If it's stopping, I alter the state of the component and let WheelContainer
know that it should swap out the current animation. If the wheel shouldn't be stopping I keep the current animation and alter the speed of the rotation.
Anyway, I've got it kind of working. The issue that I'm running into is that it doesn't recognize a "slower scroll". For instance, the user could be scrolling quickly or slowly but scrolling nonetheless. A slow scroll shouldn't necessarily mean that it should come to a stop.
The other issue is that spinBack
seems to never be invoked. And even if it was, I'm having trouble figuring out how I'd be able to differentiate between a "slower" scroll and a backward spin.
Finally, I should note that the accelerated scroll seems to be recognized only on a mac's trackpad. I just plugged in an external mouse to test it out, and it doesn't really quite rotate as expected.
In addition to this, I feel like there is a better approach to this. Perhaps something with Math.sin
or Math.cos
, or something similar that I should've been paying more attention to in high school math class. The component just feels too bulky, when it seems like there is a much more simple approach.
Here's a Sandbox,
Here's my attempt:
The first problem for me was that the code was executing as the input was coming in. Felt it needed some little time delay for the function to calculate.
We'll do that with a setTimeout
.
Second: yup, you're right. We need a math/trig-like function that will give a value close to zero for very small values, and a value close to 1 for increasing values.
Third is...well, this was more of a personal thing — wasn't sure if this was intentional, but I noticed that the spinBack
function wouldn't work once you'd scrolled to the top (i.e. window.pageYOffset = 0
).
So, instead of scroll
eventListener, I used wheel
eventListener — this way, I could use the deltaY
property to see by how much it changed.
Fourth, I set the speed to be a function of distance covered by time.
Finally: the CSS speed thing was counter-intuitive for me at first — for some reason, the higher the value the slower it rotated! I kept wondering what was wrong till I realised my silly error!😅
Wrapping it all up, I'll only paste the sections I changed (I've put comments where I made changes):
// == adding these
var scrollTimeoutVar = null, scrollAmount = 0, totalScrollAmount = 0, scrollTimes = [];
var theMathFunction = (x) => {
return x/(2 + x); // <- you can use a different value from '2' to determine how fast your want the curve to grow
};
export default function App() {
const [previousPosition, setPreviousPosition] = React.useState(0);
const [previousDelta, setPreviousDelta] = React.useState(0);
const [speed, setSpeed] = React.useState(1);
const [isStopping, setIsStopping] = React.useState(false);
const [isMovingForward, setIsMovingForward] = React.useState(true);
React.useEffect(() => {
skrollr.init();
window.addEventListener("wheel", handleScroll); // <- changed from 'scroll'
return () => window.removeEventListener("wheel", handleScroll); // <- changed from 'scroll'
}, [previousPosition, previousDelta, isStopping]);
const handleScroll = React.useCallback((wheelEvent) => { // this time, we're passing in a wheel event
// console.log("wheelEvent", wheelEvent); // if you'd like to see it, uncomment
// get deltaY, which will be our distance covered
scrollAmount += wheelEvent.deltaY; // this will be reset every time a new scroll is recorded
totalScrollAmount += scrollAmount; // this one will never be reset
// add all the times that occured in the scroll
scrollTimes.push((new Date()).getTime());
setIsStopping(false);
// adding this here, to cancel the setTimeout when the user is done
if(scrollTimeoutVar !== null){
clearTimeout(scrollTimeoutVar);
};
scrollTimeoutVar = setTimeout(() => {
if(!isStopping){
setIsStopping(true);
// const delta = window.pageYOffset - previousPosition; <- no longer need this
// get time difference
const timeDiff = scrollTimes[scrollTimes.length - 1] - scrollTimes[0]; // when the scroll stopped - when it started
// get direction
// since our scrollAmount can either be positive or negative depending on the direction the user scrolled...
// ...we can use the cumulative amount to determine direction:
const isMovingForward = totalScrollAmount > 0; // no longer window.pageYOffset > previousPosition;
// console.log("isMovingForward", isMovingForward); // if you'd like to see it, uncomment
setIsMovingForward(isMovingForward);
// calculate delta as a percentage
// const deltaAsPercentage = (delta / previousPosition) * 100; <- we no longer need this
// setSpeed(deltaAsPercentage); <- we no longer need this
// our speed will be the simple physics equation of change in distance divided by change in time
const current_speed = Math.abs(scrollAmount/timeDiff); // we're using the 'absolute' function, so the speed is always a positive value regardless of the direction of scroll
const trig_speed = theMathFunction(current_speed); // give it a value between 0 and 1
// here's the counter-intuitive part
let speedToSet = 0;
// if it's currently movingforward...
if(isMovingForward){
// should we go faster, or slower? in this case, subtract to go FASTER, add to go SLOWER
speedToSet = scrollAmount > 0 ? (speed - trig_speed) : (speed + trig_speed);
}
// if not, it will be the reverse (because we're going in the OPPOSITE direction)
else{
// in this case, subtract to go SLOWER, add to go FASTER
speedToSet = scrollAmount > 0 ? (speed + trig_speed) : (speed - trig_speed);
}
// console.log("speedToSet", speedToSet); // if you'd like to see it, uncomment
setSpeed(speedToSet);
// set it back to zero, pending a new mouse scroll
scrollAmount = 0;
scrollTimes = [];
}
},100);
}, [previousPosition, previousDelta, isStopping]);
With the above, the rotation will accelerate in whatever direction the user scrolls their mouse (and will decelerate should they change direction).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With