Intro
So this is a pretty specific, but quite tricky problem I’m facing. I’m building a vertical gallery for a webpage in React, with use of LocomotiveScroll. The elements all start off with a height of 20vh such that 5 elements appear on the screen at any given time. I have a toggle however that sets the height of these elements to 33vh for a secondary view of the elements.
When the user has scrolled to index 6, meaning that the 6th tile is in the center of the screen (on the 20vh view), when the user clicks to change view and each element expands, the elements will expand from the top down, meaning that the element that was previously in the center of the screen will shoot down the page. This is of course due to the top-down nature of the DOM and how it’s rendered, but this isnt what I’m after.
Behaviour Wanted.
When at index n (nth element at center) on view A (20vh per element, 5 elements in viewport) and the viewing mode is toggled to view B (33vh per element, 3 elements in viewport), all elements m where m < n (elements above) expand upwards and all elements m > n (elements below) expand downwards. Simply put I want to keep the element at the center and expand the list of elements around this point.
Attempted Solution
As I’m using locomotiveScroll, the wrapper around the elements is translated up and down to move the elements within up and down in line with the given scroll inputs. An initial approach to solve this was to adjust this translateY value with the same transition function and duration that the elements are expanding their height with, but in the opposite direction.
Problem with solution
The ending state of this solution is correct, however despite the counter-translate having the same animation timing and bezier curve as the elements expanding down in the opposite direction, the element fails to remain at the center as the two opposing animations fail to counteract each other perfectly, leading to the elements moving up the page then being dragged back down as they translate up is countered by the elements expanding downwards.
This was expected, as relying on two animations being processed and executed in perfect synchronicity by the browser is extremely hopeful.
Answers Sought
I’m mainly looking for some advice on whether what i’m attempting here is even possible with how the page is currently built. As well as possible alternative approaches to consider for this, or existing codepen/demos where this has been done before that I could be pointed towards. I really can’t think of any work arounds to this that arent extremely expensive or complex, so feel like I’m missing something here.
Examples of behaviour wanted
This website has a horizontal arrangement of items, but the principle is the same in that it allows for toggling between views whereby the elements change sizes, but expand around the central point of the screen, not left to right as they would if they followed the behaviour discussed above.
Homepage.jsx
import React, {useState, useEffect, useRef} from "react"
import {AnimatePresence, motion} from "framer-motion";
const Homepage = () => {
const projectTileVariants = {
wide:{
height: "20dvh",
transition: {
duration: 1,
easing: [0, 0.55, 0.45, 1]
}
},
square: {
height: "33dvh",
transition: {
duration: 1,
easing: [0, 0.55, 0.45, 1]
}
}
}
useEffect(() => {
if(scrollRef.current){
let tmpScroll = new LocomotiveScroll({
el: scrollRef.current,
// ... unimportant initialisation params
});
tmpScroll.on('scroll', (args) => {
//... logic for setting activeIndex value
//... omitted as long and not-relevant
})
}
}, [scrollRef]);
useEffect(() => {
// If viewingMode is true, set to 20dvh
// if not then we set to 33vh, where we want to expand arounf a central point
if(viewingMode){
setDynamicHeight("20dvh")
}else{
let translateAmount = -1 * (activeIndex -1)*(33*window.innerHeight/100);
// Adds a tmp-anim class such that the change in transform will be animated and not instantly jump
// tmp-anim has the same transition duration and bezier curve as that found in projectTileVariants
tileSectionRef.current.classList.add("tmp-anim")
tileSectionRef.current.style.transform = "translateY("+translateAmount+"px)";
// removes transition so scrolling can continue
setTimeout(() => {
tileSectionRef.current.style.transition = ""
}, 1000)
setDynamicHeight("33dvh")
}
}, [viewingMode])
return(
<>
<div className="home_content_wrapper">
<div className="scroll_wrapper"
id="main-container"
data-scroll-id="hey"
data-scroll-container
ref={scrollRef}
>
<div
className="vertical_project_wrapper"
data-scroll-section
id="main_content_scroll_container"
ref={tileSectionRef}
>
{
projData.map((projItem, p_idx) => {
return(
<motion.div
className={"home_project_tile"}
ref={el => vertItemsRefs.current[p_idx] = el}
animate={viewingMode ? "wide" : "square"}
variants={projectTileVariants}
>
<div className="background_content" />
</motion.div>
)
})
}
</div>
</div>
</div>
</>
)
}
Homepage.css
.home_project_tile{
position: relative;
opacity: 0.4;
padding: 10px;
height: calc(20dvh);
width: calc(1.65 * 20dvh);
transform: scale3d(1, 1, 1);
transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
box-sizing: border-box;
}
.tmp-anim{
transition: transform 1s cubic-bezier(0, 0.55, 0.45, 1) !important;
}
.vertical_project_wrapper{
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
transform: translate(-50%, 0);
}
.scroll_wrapper{
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-start;
}