cursor-tracking

ui

移动光标就可以玩了

src

cursor-tracking.tsx

TSX
"use client"

import { delay, wrap, MotionValue } from "motion";
import { usePointerPosition, useCursorState } from "motion-plus/react";
import { useTransform, useMotionValueEvent, AnimatePresence, motion } from "motion/react";

import { useRef, useState } from "react"

import { cn } from "@/lib/tailwind";

interface Target {
    id: number;
    index: number;
    x: number;
    y: number;
}

interface CursorTrackingProps {
    trigger?: number;
    width?: number;
    height?: number;
    className?: string;
    images: string[];
}

export function CursorTracking({
                                   trigger = 120,
                                   width = 280,
                                   height = 210,
                                   className,
                                   images,
                               }: CursorTrackingProps) {


    const pointer = usePointerPosition();
    const { zone } = useCursorState();

    const sum = useRef<number>(0);

    const id = useRef<number>(0);
    const ix = useRef<number>(0);

    const [ targets, setTargets ] = useState<Target[]>([]);

    const distance: MotionValue<number> = useTransform(() => {
        const x = pointer.x.get();
        const y = pointer.y.get();
        const deltaX = x - (pointer.x.getPrevious() ?? x);
        const deltaY = y - (pointer.y.getPrevious() ?? y);
        return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
    });

    useMotionValueEvent(distance, "change", (latest) => {

        sum.current += latest;

        if (sum.current >= trigger) {
            getImage(pointer.x.get(), pointer.y.get());
            sum.current = 0;
        }
    })

    function getImage(x: number, y: number) {

        const one: Target = {
            id: id.current++,
            index: ix.current,
            x: x - width / 2,
            y: y - height / 2,
        }

        setTargets((prev) => [ ...prev, one ]);
        ix.current = wrap(0, images.length, ix.current + 1);

        delay(() => {
            setTargets((prev) => prev.filter(img => img.id !== one.id));
        }, 1.2);

    }

    return (
        <div className={ cn("overflow-hidden relative", className) } data-cursor-zone="zone">
            <AnimatePresence>

                {
                    zone === "zone" && targets.map((img) => (
                        <motion.img
                            style={ {
                                position: "absolute",
                                left: `${ img.x }px`,
                                top: `${ img.y }px`,
                                width: width,
                                height: height,
                                willChange: "opacity, transform",
                                objectPosition: "center"
                            } }

                            initial={ { opacity: 0, scale: 0.5 } }
                            animate={ { opacity: 1, scale: 1 } }
                            transition={ { duration: 0.1 } }
                            exit={ { opacity: 0, scale: 0.5, transition: { duration: 0.3 } } }

                            src={ images[img.index] }
                            alt=""
                            key={ img.id }/>
                    ))
                }

            </AnimatePresence>
        </div>
    )
}

page.tsx

TSX
import { CursorTracking } from "@/examples/cursor-tracking/cursor-tracking";

export default function Page() {

    const images = [
        "https://cdn.pixabay.com/photo/2016/05/03/16/10/morning-1369446_1280.jpg",
        "https://cdn.pixabay.com/photo/2016/11/29/06/46/adult-1867889_1280.jpg",
        "https://cdn.pixabay.com/photo/2019/03/01/14/54/girl-4028008_1280.jpg",
        "https://cdn.pixabay.com/photo/2015/03/01/04/11/beauty-654309_1280.jpg",
        "https://cdn.pixabay.com/photo/2015/03/19/02/54/woman-680300_1280.jpg",
        "https://cdn.pixabay.com/photo/2016/04/10/12/27/nature-1319767_1280.jpg",
    ];

    return (
        <div className="h-screen relative bg-slate-100">

            <CursorTracking className="h-full cursor-pointer"
                            images={ images }/>

            <h1 className="absolute z-50 size-fit inset-0 m-auto pointer-events-none">
                <span className="text-3xl md:text-6xl font-bold duration-300">
                    移动光标就可以玩了
                </span>
            </h1>

        </div>
    )
}

video