Skip to content

Commit

Permalink
feat: cursor trail added (#419)
Browse files Browse the repository at this point in the history
Co-authored-by: “SrisharanVS” <“[email protected]”>
  • Loading branch information
SrisharanVS and “SrisharanVS” authored Dec 16, 2024
1 parent da0905a commit 24d06d2
Show file tree
Hide file tree
Showing 7 changed files with 356 additions and 313 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"eslint": "8.47.0",
"eslint-config-next": "13.4.13",
"framer-motion": "10.18.0",
"gsap": "^3.12.5",
"html-react-parser": "5.1.1",
"lenis": "^1.1.6",
"lucide-react": "0.316.0",
Expand Down
28 changes: 4 additions & 24 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { Poppins } from "next/font/google";
import { Metadata } from "next";
import ReduxProvider from "@/redux/ReduxProvider";
import { Toaster } from "sonner";

import AnimatedCursor from "react-animated-cursor";
import ScrollToTop from "@/components/ScrollToTop/scrolltotop";
import CursorTrail from "@/components/core/cursor/cursorTrail";
import CursorTrailHandler from "@/components/core/cursor/cursorTrailHandler";

import { Providers } from "./providers";
import LenisWrapper from "@/helper/leniswrapper";
Expand Down Expand Up @@ -57,27 +57,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
suppressHydrationWarning
>
<body className={`${poppinsFont.className} bg-white dark:bg-secondary`}>
<AnimatedCursor
innerSize={9}
outerSize={40}
color="2, 2, 2"
outerAlpha={.2}
innerScale={0.7}
outerScale={3}
clickables={[

'a',
'input[type="text"]',
'input[type="email"]',
'input[type="number"]',
'input[type="submit"]',
'input[type="image"]',
'label[for]',
'select',
'textarea',
'button',
'.link'
]} />
{/* Cursor trail handler and trail */}
<Toaster
position="top-right"
duration={3000}
Expand All @@ -87,7 +67,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
theme="light"
/>
<Providers>
<ReduxProvider>{children}</ReduxProvider>
<ReduxProvider><CursorTrail /><CursorTrailHandler />{children}</ReduxProvider>
</Providers>
<ScrollToTop />
</body>
Expand Down
80 changes: 80 additions & 0 deletions src/components/core/cursor/cursorTrail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"use client";

import { useSelector } from "react-redux";
import { useEffect, useRef } from "react";
import { RootState } from "@/redux/store";

const CursorTrail = () => {
const trailPositions = useSelector((state: RootState) => state.cursor.trailPositions);
const restPosition = useSelector((state: RootState) => state.cursor.position); // Last cursor position
const trailRef = useRef<HTMLDivElement[]>([]); // Refs for trail elements
const animationFrameRef = useRef<number | null>(null);

useEffect(() => {
const lerp = (start: number, end: number, factor: number) => {
return start + (end - start) * factor;
};

const animateTrail = () => {
if (trailRef.current) {
trailRef.current.forEach((el, index) => {
if (el) {
// Current element position
const currentX = parseFloat(el.style.left || "0");
const currentY = parseFloat(el.style.top || "0");

// Target position: Interpolate toward trail position or rest position
const targetX = index < trailPositions.length ? trailPositions[index].x : restPosition.x;
const targetY = index < trailPositions.length ? trailPositions[index].y : restPosition.y;

// Smoothly move toward the target position
const newX = lerp(currentX, targetX, 0.2); // Adjust 0.2 for faster catch-up
const newY = lerp(currentY, targetY, 0.2);

// Update element position
el.style.left = `${newX}px`;
el.style.top = `${newY}px`;
}
});
}

animationFrameRef.current = requestAnimationFrame(animateTrail);
};

animationFrameRef.current = requestAnimationFrame(animateTrail);

return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [trailPositions, restPosition]);

return (
<>
{Array(10)
.fill(0)
.map((_, index) => (
<div
key={index}
ref={(el) => {
if (el) trailRef.current[index] = el;
}}
style={{
position: "fixed",
left: `0px`, // Initial position
top: `0px`,
width: `${15 - index}px`, // Increase size here
height: `${15 - index}px`, // Increase size here
backgroundColor: `rgba(255, 0, 0, ${0.5 - index * 0.05})`, // Fade with index
borderRadius: "50%",
pointerEvents: "none",
transform: "translate(-50%, -50%)",
}}
/>
))}
</>
);
};

export default CursorTrail;
26 changes: 26 additions & 0 deletions src/components/core/cursor/cursorTrailHandler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use client";

import { useEffect } from "react";
import { useDispatch } from "react-redux";
import { AppDispatch } from "@/redux/store";
import { updateCursorPosition } from "@/redux/reducers/cursorReducer";

const CursorTrailHandler = () => {
const dispatch = useDispatch<AppDispatch>();

useEffect(() => {
const handleMouseMove = (event: MouseEvent) => {
dispatch(updateCursorPosition({ x: event.clientX, y: event.clientY }));
};

window.addEventListener("mousemove", handleMouseMove);

return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, [dispatch]);

return null;
};

export default CursorTrailHandler;
41 changes: 41 additions & 0 deletions src/redux/reducers/cursorReducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import PageLoader from "next/dist/client/page-loader";

interface Position {
x: number;
y: number;
}

interface CursorState {
position: Position;
trailPositions: Position[];
trailLength: number;
}

const initialState: CursorState = {
position: {x:0, y:0},
trailPositions: [],
trailLength:20,
}

const cursorSlice = createSlice({
name: "cursor",
initialState,
reducers: {
updateCursorPosition(state, action: PayloadAction<Position>) {
state.position = action.payload;
state.trailPositions.push(action.payload);
if(state.trailPositions.length > state.trailLength)
{
state.trailPositions.shift();
}
},
setTrailLength(state, action:PayloadAction<number>)
{
state.trailLength = action.payload;
},
},
});

export const { updateCursorPosition, setTrailLength } = cursorSlice.actions;
export default cursorSlice.reducer;
5 changes: 5 additions & 0 deletions src/redux/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ import { configureStore } from "@reduxjs/toolkit";
import postsReducer from "@/redux/reducers/postsReducer";
import authReducer from "@/redux/reducers/authReducer";
import bookmarkReducer from "@/redux/reducers/bookmarkReducer";
import cursorReducer from "@/redux/reducers/cursorReducer";

export const store = configureStore({
reducer: {
posts: postsReducer,
auth: authReducer,
bookmarks: bookmarkReducer,
cursor: cursorReducer,
},
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Loading

0 comments on commit 24d06d2

Please sign in to comment.