ReactJS: Calculating direction of 'joystick' while dragging (using Framer Motion) and updating div effects depending on position

Viewed 370

I am learning how to use ReactJS by making a mock-up personal website. The target design of the homepage is a joystick that, on drag, allows you to select which page you'd like to go to next. There are four buttons, positioned on the top, right, left, and bottom of the central circular GUI.

My idea was, basically: click joystick, drag towards option. If you approach option, it highlights it, and if if you get close enough to the option it navigates you to that page.

I am using framer motion's drag and onDragStart / onDragEnd / onDrag props to try and detect the direction of the drag. Basically, I am getting the dragged element's clientX and clientY position fields (from the event packet emitted by the onDrag functional prop), and comparing it to the dragged element's original clientX and clientY positions. If the change in X is greater than the change in Y, I know that the closest option is on the X-axis, and I can perform some more calculations to figure out whether it's on the left or right side... ditto for the Y axis. I know that these calculations work properly and quickly enough: WITHOUT using any [state, setState] to assign the hover effect to the target div, the script properly indicates and console.logs the correct 'link' it should go to.

The issue is, once I try and use the same code with React's useState function (effectively telling the relevant component to re-render with a hover effect instead of just console.logging the direction), it doesn't work. The hover effect gets stuck on the about page, no matter which direction I try and drag the joystick towards.

Here's some code to show what I'm doing. Again, I started learning React yesterday, so please let me know of any proper practices or more effective ways to do things if you see them.

const linkPaths = {
    top: "about",
    right: "resume",
    bot: "projects",
    left: "bots",
};

const Home = () => {
    const [isHovering, setIsHovering] = useState(false);
    const [joystickIsClicked, setJoystickIsClicked] = useState(false);
    const [hoveredLink, setHoveredLink] = useState("");

    var draggingTowards = "";
    var prevDraggingTowards = "";

    var dragStartX, dragStartY;

    const joystickWidth = 90;
    const outerCircleWidth = 600;
    const innerCircleWidth = 550;

    return (
            <Wrapper>
                <Content>
                    <OuterCircle width={outerCircleWidth} isHovering={isHovering}> 
                        <InnerCircle
                            width={innerCircleWidth}
                            isHovering={isHovering}
                            joystickIsClicked={joystickIsClicked}
                        >
                            <JoystickContainer
                                layout
                                drag
                                dragElastic={0}
                                width={joystickWidth}
                                dragConstraints={{
                                    top: -innerCircleWidth / 4,
                                    left: -innerCircleWidth / 4,
                                    right: innerCircleWidth / 4,
                                    bottom: innerCircleWidth / 4,
                                }}
                                whileHover={{
                                    scale: 1.2,
                                    cursor: "pointer",
                                    backgroundImage:
                                        "linear-gradient(black, black), linear-gradient(to bottom right, cyan, magenta)",
                                }}
                                onHoverStart={() => {
                                    setIsHovering(true);
                                }}
                                onHoverEnd={() => {
                                    setIsHovering(false);
                                    setJoystickIsClicked(false);
                                }}
                                onTapStart={() => {
                                    setJoystickIsClicked(true);
                                }}
                                onTap={() => {
                                    setJoystickIsClicked(false);
                                    console.log("finished tap");
                                }}
                                onDragStart={(event, info) => {
                                    dragStartX = event.clientX;
                                    dragStartY = event.clientY;
                                    // console.log(`(${dragStartX}, ${dragStartY})`);
                                }}
                                onDragEnd={(event, info) => {
                                    // console.log(`(${event.clientX}, ${event.clientY})`);
                                }}
                                onDrag={(event, info) => {
                                    if (Math.abs(event.clientX - dragStartX) > Math.abs(event.clientY - dragStartY)) {
                                        // more movement in x direction
                                        if (event.clientX > dragStartX) {
                                            draggingTowards = linkPaths.right;
                                        } else {
                                            draggingTowards = linkPaths.left;
                                        }
                                    }
                                    //more movement in y direction
                                    else {
                                        if (event.clientY > dragStartY) draggingTowards = linkPaths.bot;
                                        else draggingTowards = linkPaths.top;
                                    }

                                    if (draggingTowards !== prevDraggingTowards) {
                                        // only update state function on a change of direction dragging
                                        setHoveredLink(draggingTowards);
                                        prevDraggingTowards = draggingTowards;
                                    }
                                    console.log(hoveredLink);
                                }}
                            </JoystickContainer>
                        </InnerCircle>
                    </OuterCircle>
                </Content>
            </Wrapper>

Is there some key thing regarding stateful values and functions in React that I'm missing? I wondered if the re-rendering of the prop was causing issues with the drag, but if that's the case I am unsure how to proceed.

GUI Layout with the effect visible -- dragging direction stuck on "about"

Console.logs of failing hoveredLink state

1 Answers

Note: This was originally added as part of OP's question, I just edited it out and posted it as an answer. I take no credit for the following content.

Turns out that Framer Motion does not work well with the clientX and clientY packages. When you change the state, those variables end up messing up and defaulting to some value. Instead, what I did to fix this, was I used the info event that is also sent out during Framer Motion's drag event. The packet is structured like this:

{
  delta: {x, y} // a Point that tracks the change in X and Y from the last packet
  offset: {x, y} // didn't really figure this out, but I assume it's error propagation from the delta packet.
  point: {x, y} // the new Point location of the dragged div
  velocity: {x, y} // x and y velocity of the dragged div
}

Essentially, I just made two variables in the React component: deltaX and deltaY. Whenever the dragged component moved, I added the info.delta.x and info.delta.y to the deltaX and deltaY variables, respectively. This gave me a really good 'position' of the dragged element. Wish that FM's clientX and clientY worked as advertised, but this was a decent enough workaround for my own purposes.

TL;DR:

Found a pretty janky solution that is computation intensive but it worked flawlessly on multiple devices without any visible jank. Issue stems from Framer Motion's drag packets emitting clientX / clientY location objects that are undefined when the state of the component is changed – basically, re-rendering components messes with the geometry somehow. Instead of using the clientX / clientY fields of the event packet, I used the delta.x / delta.y fields of the info packet.

For anyone else struggling with some of the framer motion event packets, it's not just you! But there are ways to get around it. Feel free to message me if you'd like to see the full code -- I didn't paste the finished code here to avoid too long of a post.

Related