Jump to content
Search Community

Custom GSAP Cursor on Dynamic Elements

Dhanwanth test
Moderator Tag

Go to solution Solved by Cassie,

Recommended Posts

Hey!

I'm creating this site that utilizes a custom cursor made in GSAP based on this

See the Pen VwOembm by mustafauncuoglu (@mustafauncuoglu) on CodePen

. On my site, I am using Filepond for file uploads and it dynamically creates its elements. I'd like the cursor to scale up and lose some of its opacity when hovering over button elements (action button class: .filepond--file-action-button) and span elements (download icon class: .filepond--download-icon & browse text class: .filepond--label-action). I've attached a codepen that shows how the cursor correctly hovers over regular HTML button and span elements but doesn't when you hover over "Browse" or the action button/download icon (upload a file to the Filepond element to see these, your file is not being uploaded). The codepen has an error in the JS as I was trying to figure out how to make the code work, by updating the hoverTargetsto select the dynamically created Filepond elements using MutationObservers.

You can put the following JS instead of the one with errors, which is me trying to work around this situation, but the animation is jarring and it just feels like there is a much more efficient way to make it work:
 

// Register FilePond plugins
FilePond.registerPlugin(
    FilePondPluginImageExifOrientation,
    FilePondPluginImagePreview,
    FilePondPluginPdfPreview,
    FilePondPluginMediaPreview,
    FilePondPluginGetFile
);

// Initialize FilePond
const inputElement = document.querySelector('input[type="file"]');
const pond = FilePond.create(inputElement, {
    allowDownloadByUrl: false,
    pdfComponentExtraParams: 'toolbar=0&view=fitW&page=1'
});

const targets = "button, span, .filepond--label-action, .filepond--download-icon, .filepond--file-action-button";

// Function to wait for an element to be available in the DOM
function waitForElm(selector) {
    return new Promise(resolve => {
        if (document.querySelector(selector)) {
            return resolve(document.querySelector(selector));
        }

        const observer = new MutationObserver(mutations => {
            if (document.querySelector(selector)) {
                observer.disconnect();
                resolve(document.querySelector(selector));
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    });
}

// Define updateHoverTargets function
function updateHoverTargets(settings, data, cursor, dot) {
    settings.hoverTarget = document.querySelectorAll(targets);
    if (settings.hasHover && settings.hoverTarget.length) {
        settings.hoverTarget.forEach((target) => {
            target.addEventListener("mouseenter", () => {
                data.hoverTarget = target;
                data.isHover = true;
                gsap.to(cursor, {
                    scale: 1.5
                });
                gsap.to(dot, {
                    scale: 1.5
                });
                if (data.idleAnim) {
                    data.idleAnim.kill();
                    data.idleAnim = null;
                }
                settings.onMove(data);
            });
            target.addEventListener("mouseleave", () => {
                data.hoverTarget = target;
                data.isHover = false;
                gsap.to(cursor, {
                    scale: 1
                });
                gsap.to(dot, {
                    scale: 1
                });
                settings.onMove(data);
            });
        });
    }
}

// Initialize custom cursor
function customCursor(options, updateHoverTargets) {
    const settings = Object.assign({
            targetClass: "custom-cursor",
            dotClass: "custom-cursor-dot",
            wrapper: document.body,
            speed: 0.2,
            movingDelay: 300,
            idleTime: 2000,
            hideAfter: 5000,
            hasHover: true,
            hoverTarget: document.querySelectorAll(targets),
            touchDevices: false,
            onMove: function(data) {}
        },
        options
    );

    const data = {};
    const checkTouch = !settings.touchDevices && "ontouchstart" in document.documentElement;
    let timer = null,
        idleTimer = null,
        hideTimer = null,
        idleAnim = null;

    if (checkTouch || !settings.wrapper) return;

    const cursor = document.createElement("div");
    cursor.className = settings.targetClass;
    const dot = document.createElement("div");
    dot.className = settings.dotClass;
    settings.wrapper.appendChild(cursor);
    settings.wrapper.appendChild(dot);

    let position = {
        x: window.innerWidth / 2,
        y: window.innerHeight / 2
    };
    let dotPosition = {
        x: position.x,
        y: position.y
    };
    const setX = gsap.quickSetter(cursor, "x", "px");
    const setY = gsap.quickSetter(cursor, "y", "px");
    const setDotX = gsap.quickSetter(dot, "x", "px");
    const setDotY = gsap.quickSetter(dot, "y", "px");

    data.cursor = cursor;
    data.isHover = false;

    window.addEventListener("mousemove", init);

    function init() {
        window.removeEventListener("mousemove", init);

        window.addEventListener("mousemove", (e) => {
            dotPosition.x = e.clientX;
            dotPosition.y = e.clientY;

            data.isMoving = true;
            settings.onMove(data);

            clearTimeout(timer);
            clearTimeout(idleTimer);
            clearTimeout(hideTimer);

            if (idleAnim) {
                idleAnim.kill();
                idleAnim = null;
                gsap.to([cursor, dot], {
                    scale: 1
                });
            }

            idleTimer = setTimeout(() => {
                idleAnimation();
            }, settings.idleTime);

            hideTimer = setTimeout(() => {
                gsap.to([cursor, dot], {
                    opacity: 0
                });
            }, settings.hideAfter);

            timer = setTimeout(() => {
                data.isMoving = false;
                settings.onMove(data);
            }, settings.movingDelay);
        });

        document.addEventListener("mouseleave", () => {
            data.isInViewport = false;
            settings.onMove(data);
        });

        document.addEventListener("mouseenter", (e) => {
            dotPosition.x = position.x = e.clientX;
            dotPosition.y = position.y = e.clientY;

            data.isInViewport = true;
            settings.onMove(data);
        });

        gsap.ticker.add((time, deltaTime) => {
            const fpms = 60 / 1000;
            const delta = deltaTime * fpms;
            const dt = 1 - Math.pow(1 - settings.speed, delta);
            position.x += (dotPosition.x - position.x) * dt;
            position.y += (dotPosition.y - position.y) * dt;
            setDotX(dotPosition.x);
            setDotY(dotPosition.y);
            setX(position.x);
            setY(position.y);
        });

        data.isInViewport = true;
    }

    function idleAnimation() {
        if (!data.isMoving && !data.isHover) {
            idleAnim = gsap.to([cursor, dot], {
                scale: 1.2,
                repeat: -1,
                yoyo: true,
                duration: 0.5
            });
        }
    }

    updateHoverTargets(settings, data, cursor, dot);
}

// Initialize the custom cursor with options
const ccOptions = {
    targetClass: "custom-cursor",
    dotClass: "custom-cursor-dot",
    hasHover: true,
    idleTime: 2000,
    onMove: function(data) {
        if (data.isInViewport) {
            if (data.isMoving) {
                if (data.isHover) {
                    gsap.to(data.cursor, {
                        opacity: 0.5,
                        scale: 1.5
                    });
                    gsap.to(document.querySelector(".custom-cursor-dot"), {
                        opacity: 0.5,
                        scale: 1.5,
                        height: 8,
                        width: 8,
                        borderRadius: 50
                    });
                } else {
                    gsap.to(data.cursor, {
                        opacity: 1,
                        scale: 1
                    });
                    gsap.to(document.querySelector(".custom-cursor-dot"), {
                        opacity: 1,
                        scale: 1,
                        height: 8,
                        width: 8,
                        borderRadius: 50
                    });
                }
            } else {
                gsap.to(data.cursor, {
                    opacity: 0.5
                });
                gsap.to(document.querySelector(".custom-cursor-dot"), {
                    opacity: 0.5
                });
            }
        } else {
            gsap.to(data.cursor, {
                opacity: 0
            });
            gsap.to(document.querySelector(".custom-cursor-dot"), {
                opacity: 0
            });
        }
    }
};

customCursor(ccOptions, updateHoverTargets);

// Update hover targets when FilePond initializes the "Browse" text
waitForElm('.filepond--label-action').then((elm) => {
    console.log('Element is ready');
    updateHoverTargets(ccOptions, {
        hoverTarget: document.querySelectorAll(targets),
        isHover: false,
        isMoving: false,
        isInViewport: true
    }, document.querySelector('.custom-cursor'), document.querySelector('.custom-cursor-dot'));
});

// Use MutationObserver to detect changes within the FilePond UI
var observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
        console.log("mutated");
        updateHoverTargets(ccOptions, {
            hoverTarget: document.querySelectorAll(targets),
            isHover: false,
            isMoving: false,
            isInViewport: true
        }, document.querySelector('.custom-cursor'), document.querySelector('.custom-cursor-dot'));
    });
});

// Configuration of the observer and target
var config = { attributes: true, childList: true, characterData: true, subtree: true };
var target = document.querySelector(".filepond--root");  // Observe the root element of FilePond
observer.observe(target, config);


Any help is much appreciated, I've been trying to decipher this but it has gotten me stuck for days!

 

Thanks,

Dhanwanth!

See the Pen jOoxGKb by Dhanwanth-Parameswar (@Dhanwanth-Parameswar) on CodePen

Link to comment
Share on other sites

  • Solution

Hey there!

 

I can't really advise on waiting for filepond to load. There is a callback for when it's loaded but it's pretty unreliable.


I've added a little delayedCall and that works alright for now.

But what I can advise on is only adding new targets, in your example you're adding additional event listeners to everything and overwriting the animation loads of times. Just wait until it's loaded and then add some event listeners to the new target elements in filepond.

See the Pen Exzrbra?editors=0011 by GreenSock (@GreenSock) on CodePen



Hope this helps.

 

Also it may be worth writing your own if you're a bit confused, it's not too hard and this is quite a fiddly demo.

This might help

See the Pen OJEvrrg?editors=0010 by GreenSock (@GreenSock) on CodePen



And this

See the Pen xxpbORN by GreenSock (@GreenSock) on CodePen

  • Like 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...