Issue
I'm trying to make a div with three columns where all three columns scolling to bottom with an animation.
When the bottom is reached, they should start scrolling to the top. Then when top is reached they should start scrolling again to bottom and so on.
I'm using React and I try to do it by adding the top position of every column to the state and change it in a setInterval. Then I try to detect if the bottom of the top is reached and then I change the top position accordingly.
The problem is it scrolls to the bottom, but then, for some reasen it wont scroll to top. I'm not sure why exactly the next code is not working.
The code is as follows:
import React, { useState, useEffect } from "react";
import { map } from "lodash";
const column1InitialYPosition = -300;
const column2InitialYPosition = -150;
const column3InitialYPosition = 0;
const intervalTime = 500;
const imageUrl =
"https://images.freeimages.com/variants/jzAZ1zYpxh11gSbgYPMheWun/f4a36f6589a0e50e702740b15352bc00e4bfaf6f58bd4db850e167794d05993d";
const column1Images = [imageUrl, imageUrl, imageUrl, imageUrl, imageUrl];
const column2Images = [imageUrl, imageUrl, imageUrl, imageUrl, imageUrl];
const column3Images = [imageUrl, imageUrl, imageUrl, imageUrl, imageUrl];
const isElementInViewport = el => {
let r, html;
if (!el || 1 !== el.nodeType) {
return false;
}
html = document.documentElement;
r = el.getBoundingClientRect();
return !!r && r.bottom >= 0 && r.top <= html.clientHeight;
};
export default function App() {
const [column1YPosition, setColumn1YPosition] = useState(
column1InitialYPosition
);
const [column2YPosition, setColumn2YPosition] = useState(
column2InitialYPosition
);
const [column3YPosition, setColumn3YPosition] = useState(
column3InitialYPosition
);
let intervalScroll;
useEffect(() => {
setTimeout(() => startScrollInterval(), 3000);
return () => removeScrollToBottomInterval();
}, []);
const calculateNewPosition = (
currentPosition,
maxPosition,
minPosition,
bottomReached,
topReached
) => {
let newPosition = bottomReached
? currentPosition + 100
: currentPosition - 100;
newPosition = -1 * newPosition > maxPosition ? -maxPosition : newPosition;
newPosition = newPosition > minPosition ? minPosition - 1 : newPosition;
return newPosition;
};
const startScrollInterval = () => {
const screenHeight = window ? window?.innerHeight : 0;
const column1Bottom = document.getElementById("column-1-bottom");
const column1Top = document.getElementById("column-1-top");
const column1 = document.getElementById("column-1");
const column1MaxTop = column1 ? column1?.scrollHeight - screenHeight : 0;
const column2Bottom = document.getElementById("column-2-bottom");
const column2Top = document.getElementById("column-2-top");
const column2 = document.getElementById("column-2");
const column2MaxTop = column2 ? column2?.scrollHeight - screenHeight : 0;
const column3Bottom = document.getElementById("column-3-bottom");
const column3Top = document.getElementById("column-3-top");
const column3 = document.getElementById("column-3");
const column3MaxTop = column3 ? column3?.scrollHeight - screenHeight : 0;
const intervalHandler = () => {
const column1BottomReached = isElementInViewport(column1Bottom);
const column1TopReached = isElementInViewport(column1Top);
const column2BottomReached = isElementInViewport(column2Bottom);
const column2TopReached = isElementInViewport(column2Top);
const column3BottomReached = isElementInViewport(column3Bottom);
const column3TopReached = isElementInViewport(column3Top);
setColumn1YPosition(column1YPosition => {
return calculateNewPosition(
column1YPosition,
column1MaxTop,
column1InitialYPosition,
column1BottomReached,
column1TopReached
);
});
setColumn2YPosition(column2YPosition => {
return calculateNewPosition(
column2YPosition,
column2MaxTop,
column2InitialYPosition,
column2BottomReached,
column2TopReached
);
});
setColumn3YPosition(column3YPosition => {
return calculateNewPosition(
column3YPosition,
column3MaxTop,
column3InitialYPosition,
column3BottomReached,
column3TopReached
);
});
};
intervalScroll = setInterval(intervalHandler, intervalTime);
};
const removeScrollToBottomInterval = () => {
clearInterval(intervalScroll);
intervalScroll = null;
};
return (
<div className="relative w-full h-screen overflow-hidden">
<div className="grid grid-cols-3 px-24 gap-24">
<div className="relative overflow-hidden h-screen no-scrollbar">
<div
id="column-1"
className="absolute inset-0 grid grid-cols-1 gap-24"
style={{
transition: "all 6s linear",
top: column1YPosition
}}
>
{map(column1Images, (image, i) => {
const isFirst = i === 0;
const isLast = i === column1Images?.length - 1;
return (
<div key={`column-1-vehicle-${i}`} className="relative w-full">
<img src={image} alt="Vehicle" className="w-full h-80" />
{isFirst && (
<div
id="column-1-top"
className="absolute opacity-1 w-full h-20"
style={{ top: -1 * column1InitialYPosition }}
/>
)}
{isLast && (
<div
id="column-1-bottom"
className="absolute bottom-0 opacity-1 w-full h-20"
/>
)}
</div>
);
})}
</div>
</div>
<div className="relative overflow-hidden h-screen no-scrollbar">
<div
id="column-2"
className="absolute inset-0 grid grid-cols-1 gap-24"
style={{
transition: "all 5s linear",
top: column2YPosition
}}
>
{map(column2Images, (image, i) => {
const isFirst = i === 0;
const isLast = i === column2Images?.length - 1;
return (
<div key={`column-2-vehicle-${i}`} className="relative w-full">
<img src={image} alt="Vehicle" className="w-full h-80" />
{isFirst && (
<div
id="column-2-top"
className="absolute opacity-1 w-full h-20"
style={{ top: -1 * column2InitialYPosition }}
/>
)}
{isLast && (
<div
id="column-2-bottom"
className="absolute bottom-0 opacity-1 w-full h-20"
/>
)}
</div>
);
})}
</div>
</div>
<div className="relative overflow-hidden h-screen no-scrollbar">
<div
id="column-3"
className="absolute inset-0 grid grid-cols-1 gap-24"
style={{
transition: "all 6s linear",
top: column3YPosition
}}
>
{map(column3Images, (image, i) => {
const isFirst = i === 0;
const isLast = i === column3Images?.length - 1;
return (
<div key={`column-3-vehicle-${i}`} className="relative w-full">
<img src={image} alt="Vehicle" className="w-full h-80" />
{isFirst && (
<div
id="column-3-top"
className="absolute opacity-1 w-full h-20"
style={{ top: -1 * column3InitialYPosition }}
/>
)}
{isLast && (
<div
id="column-3-bottom"
className="absolute bottom-0 opacity-1 w-full h-20"
/>
)}
</div>
);
})}
</div>
</div>
</div>
</div>
);
}
I've added an example it to the sandbox. Here is the link.
Any idea how to solve it?
Solution
My solution will be based just on the first column, so you'll scale it up for N columns.
At first define these global variables :
// +1 => we are moving up, -1 => down
var direction = 1;
// was previously top reached or not
var wasTopReached = false;
// was previously bottom reached or not
var wasBottomReached = false;
Then re-work your column1 position calculation function as :
const calculateNewPositionForCol1 = (
currentPosition,
maxPosition,
minPosition,
bottomReached,
topReached
) => {
direction = (bottomReached && !wasBottomReached) ||
(topReached && !wasTopReached) ? -direction : direction;
[wasBottomReached, wasTopReached] = [bottomReached, topReached];
let newPosition = currentPosition + direction * 100;
return newPosition;
};
In short,- you need to change direction of movement just once per same wall hit,- not multiple times as was earlier in your function calculateNewPosition()
before. Here's your demo fixed for a first column.
Answered By - Agnius Vasiliauskas
Answer Checked By - - Terry (ReactFix Volunteer)