diff --git a/package.json b/package.json index cd1f1a4..4ce557e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-mouse-follower", - "version": "1.1.7", + "version": "2.0.1", "description": "React mouse follower is a package based on react and framer motion. It provides components to add and customise cool mouse follower to your cursor", "repository": { "type": "git", @@ -25,7 +25,8 @@ "chromatic": "chromatic --exit-zero-on-changes" }, "dependencies": { - "framer-motion": "^10.12.18" + "framer-motion": "^10.12.18", + "zustand": "^4.4.1" }, "peerDependencies": { "react": "^18.2.0", diff --git a/src/component/follower.tsx b/src/component/follower.tsx new file mode 100644 index 0000000..976018c --- /dev/null +++ b/src/component/follower.tsx @@ -0,0 +1,5 @@ +import { FollowerInitialiserComponent } from './follower_init.js'; + +export function Follower() { + return ; +} diff --git a/src/component/follower_div.tsx b/src/component/follower_div.tsx index a050fe7..a8e85ba 100644 --- a/src/component/follower_div.tsx +++ b/src/component/follower_div.tsx @@ -1,14 +1,15 @@ import { MousePosition, MouseSettings } from '../types/index.js'; -import { motion } from 'framer-motion'; +import { AnimatePresence, motion } from 'framer-motion'; export function FollowerDiv({ pos, options }: { pos: MousePosition; options: MouseSettings }) { const calculatePosition = (): MousePosition => { - if (options.customLocation != undefined) { + if (options.customLocation != null) { return { x: options.customLocation.x, y: options.customLocation.y }; - } else if (options.customPosition != undefined) { + } else if (options.customPosition != null) { const rect = options.customPosition.current.getBoundingClientRect(); - const x = rect.left + rect.width / 2 - options.radius; - const y = rect.top + rect.height / 2 - options.radius; + const radius = options.radius ? options.radius : 12 / 2; + const x = rect.left + rect.width / 2 - radius; + const y = rect.top + rect.height / 2 - radius; return { x, y }; } else { return { x: pos.x, y: pos.y }; @@ -20,22 +21,20 @@ export function FollowerDiv({ pos, options }: { pos: MousePosition; options: Mou x: pos.x, y: pos.y, scale: 0, + backgroundColor: options.backgroundColor || 'black', + zIndex: options.zIndex || -5, + mixBlendMode: options.mixBlendMode || 'initial', }} animate={{ x: calculatePosition().x, y: calculatePosition().y, - scale: options.scale || 1, + scale: options.scale != null ? options.scale : 1, rotate: options.rotate || 0, - }} - exit={{ - x: pos.x, - y: pos.y, - scale: 0, - }} - style={{ backgroundColor: options.backgroundColor || 'black', - mixBlendMode: options.mixBlendMode || 'initial', zIndex: options.zIndex || -5, + mixBlendMode: options.mixBlendMode || 'initial', + }} + style={{ position: 'fixed', inset: 0, pointerEvents: 'none', @@ -72,21 +71,36 @@ export function FollowerDiv({ pos, options }: { pos: MousePosition; options: Mou }} > {options.text && !options.backgroundElement ? ( -

- {options.text} -

+ + + {options.text} + + ) : null} - {options.backgroundElement ? options.backgroundElement : null} + + {options.backgroundElement ? ( + + {options.backgroundElement} + + ) : null} + diff --git a/src/component/follower_init.tsx b/src/component/follower_init.tsx index c5e259c..2194940 100644 --- a/src/component/follower_init.tsx +++ b/src/component/follower_init.tsx @@ -1,10 +1,14 @@ import { useEffect, useState } from 'react'; -import { AnimatePresence } from 'framer-motion'; -import type { MousePosition, MouseSettings } from '../types/index.js'; +import { MouseSettings, type MousePosition } from '../types/index.js'; import { FollowerDiv } from './follower_div.js'; +import useMouseStore from '../store/index.js'; +import { AnimatePresence } from 'framer-motion'; + +const defaultRadius = 12 / 2; -export function FollowerInitialiserComponent({ options }: { options: MouseSettings }) { +export function FollowerInitialiserComponent() { const [isHovering, setIsHovering] = useState(false); + const options = useMouseStore((store) => store.curSettings); useEffect(() => { const handleMouseLeave = () => { @@ -25,20 +29,31 @@ export function FollowerInitialiserComponent({ options }: { options: MouseSettin }; }, []); - return ; + return ( + + ); } -function PositionHandler({ options, show }: { options: MouseSettings; show: boolean }) { +function ManagePosition({ options }: { options: MouseSettings }) { + const [pos, setPos] = useState({ x: 0, y: 0, }); + useEffect(() => { const mouseMove = (event: any) => { - setPos({ - x: event.clientX - options.radius, - y: event.clientY - options.radius, - }); + if (options.radius != null) { + setPos({ + x: event.clientX - options.radius, + y: event.clientY - options.radius, + }); + } else { + setPos({ + x: event.clientX - defaultRadius, + y: event.clientY - defaultRadius, + }); + } }; window.addEventListener('mousemove', mouseMove); return () => { @@ -46,5 +61,9 @@ function PositionHandler({ options, show }: { options: MouseSettings; show: bool }; }, [options?.radius]); - return {show ? : null}; + return ( + + {options.visible !== false ? : null} + + ); } diff --git a/src/component/index.ts b/src/component/index.ts index 98816b1..fbbdb95 100644 --- a/src/component/index.ts +++ b/src/component/index.ts @@ -1 +1,2 @@ export * from './update_follower.js'; +export * from './follower.js'; diff --git a/src/component/update_follower.tsx b/src/component/update_follower.tsx index 328f1e8..7ea87ae 100644 --- a/src/component/update_follower.tsx +++ b/src/component/update_follower.tsx @@ -1,6 +1,6 @@ import { CSSProperties, ReactNode, useContext } from 'react'; -import { MousePropertiesContext } from '../context/mouse.context.js'; import { MouseSettings } from '../types/index.js'; +import useMouseStore from '../store/index.js'; export function UpdateFollower({ mouseOptions, @@ -19,7 +19,7 @@ export function UpdateFollower({ onClick?: () => void; children?: ReactNode; }) { - const { addLayer, removeLayer } = useContext(MousePropertiesContext); + const { addLayer, removeLayer } = useMouseStore((state) => ({ addLayer: state.pushLayer, removeLayer: state.popLayer })); function handleMouseEnter() { addLayer(mouseOptions); if (onMouseEnter) { diff --git a/src/context/index.ts b/src/context/index.ts deleted file mode 100644 index 7bdb6e0..0000000 --- a/src/context/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { FollowerProvider } from './mouse.context.js'; - -export { FollowerProvider }; diff --git a/src/context/mouse.context.tsx b/src/context/mouse.context.tsx deleted file mode 100644 index f645fc6..0000000 --- a/src/context/mouse.context.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { ReactNode, createContext, useRef } from 'react'; -import { FollowerInitialiserComponent } from '../component/follower_init.js'; -import { useStack } from '../hook/stack.hook.js'; - -import type { MouseSettings } from '../types/index.js'; - -interface ContextInterface { - addLayer: (options: MouseSettings) => void; - removeLayer: () => MouseSettings | undefined; - peekStack: () => MouseSettings | undefined; - clearStack: () => void; - logStack: () => void; -} - -export const MousePropertiesContext = createContext(null); - -export const FollowerProvider = ({ visible, children }: { visible?: boolean; children?: ReactNode }) => { - const layerStack = useStack(); - const addLayer = (layerOptions: MouseSettings) => { - layerStack.push(layerOptions); - }; - - const removeLayer = () => { - return layerStack.pop(); - }; - - const value: ContextInterface = { - addLayer, - removeLayer, - clearStack: layerStack.clear, - logStack: layerStack.logStack, - peekStack: layerStack.peek, - }; - - return ( - - {visible !== false ? : null} - {children} - - ); -}; diff --git a/src/hook/control_options.hook.tsx b/src/hook/control_options.hook.tsx index c5d16a2..a2a5879 100644 --- a/src/hook/control_options.hook.tsx +++ b/src/hook/control_options.hook.tsx @@ -1,13 +1,14 @@ -import { useContext } from 'react'; -import { MousePropertiesContext } from '../context/mouse.context.js'; +import useMouseStore from '../store/index.js'; export function useControlOptions() { - const { addLayer, removeLayer, clearStack, logStack, peekStack } = useContext(MousePropertiesContext); + const store = useMouseStore((state) => ({ + addOptionLayer: state.pushLayer, + removePreviousLayer: state.popLayer, + clearLayers: state.clearLayers, + })); + return { - addOptionLayer: addLayer, - removePreviousLayer: removeLayer, - clearLayers: clearStack, - logLayers: logStack, - topLayer: peekStack, + // logLayers: logStack, + ...store, }; } diff --git a/src/hook/stack.hook.tsx b/src/hook/stack.hook.tsx deleted file mode 100644 index 399d765..0000000 --- a/src/hook/stack.hook.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { useEffect, useState } from 'react'; -import type { MouseSettings } from '../types/index.js'; - -const defaultMouseProperties: MouseSettings = { - radius: 12 / 2, -}; - -export const useStack = (): { - stack: MouseSettings[]; - push: (options: MouseSettings) => void; - pop: () => MouseSettings | undefined; - peek: () => MouseSettings | undefined; - isEmpty: () => boolean; - clear: () => void; - size: () => number; - logStack: () => void; -} => { - const [stack, setStack] = useState([defaultMouseProperties]); - - const push = (options: MouseSettings): void => { - setStack((prevStack) => { - const item: MouseSettings = { - ...defaultMouseProperties, - ...prevStack[prevStack.length - 1], - ...options, - }; - return [...prevStack, item]; - }); - }; - - const pop = (): MouseSettings | undefined => { - let item = {}; - setStack((prevStack) => { - item = prevStack.pop(); - return [...prevStack]; - }); - return item; - }; - - const peek = (): MouseSettings | undefined => { - if (stack.length > 0) { - return stack[stack.length - 1]; - } - return defaultMouseProperties; - }; - - const isEmpty = (): boolean => { - return stack.length === 0; - }; - - const clear = (): void => { - setStack([]); - }; - - const size = (): number => { - return stack.length; - }; - - const logStack = (): void => { - console.log('logging all layers'); - stack.forEach((item, i) => { - console.log(i, item); - }); - }; - - // useEffect(() => { - // logStack(); - // }, [stack]); - - return { - stack, - push, - pop, - peek, - isEmpty, - clear, - size, - logStack, - }; -}; diff --git a/src/index.ts b/src/index.ts index a46d21e..4d1cba8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,2 @@ export * from './component/index.js'; -export * from './context/index.js'; export * from './hook/index.js'; diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..bdf64b8 --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,54 @@ +// import { create } from 'zustand'; +import { create, StateCreator, StoreApi, SetState, GetState } from 'zustand'; +import { MouseSettings } from '../types/index.js'; + +interface useMouseStoreInterface { + curSettings: MouseSettings; + layers: MouseSettings[]; + + pushLayer: (newLayer: MouseSettings) => void; + popLayer: () => void; + clearLayers: () => void; +} + +const log = + (config: StateCreator) => + (set: SetState, get: GetState, api: StoreApi) => + config( + (args) => { + console.log(' applying', args); + set(args); + console.log(' new state', get()); + }, + get, + api, + ); + +const useMouseStore = create( + log((set) => ({ + curSettings: {}, + layers: [], + + pushLayer: (newLayer: MouseSettings) => + set((state) => { + const newCur = { ...state.curSettings, ...newLayer }; + state.layers.push(newCur); + return { layers: state.layers, curSettings: newCur }; + }), + popLayer: () => + set((state) => { + if (state.layers.length > 1) { + state.layers.pop(); + return { layers: state.layers, curSettings: state.layers.at(state.layers.length - 1) }; + } else { + return { layers: [], curSettings: {} }; + } + }), + clearLayers: () => + set((state) => { + return { layers: [], curSettings: {} }; + }), + })), +); + +export default useMouseStore; diff --git a/src/stories/FollowerBasic.stories.tsx b/src/stories/FollowerBasic.stories.tsx index 5e50c71..954b18c 100644 --- a/src/stories/FollowerBasic.stories.tsx +++ b/src/stories/FollowerBasic.stories.tsx @@ -1,17 +1,18 @@ import React from 'react'; import type { Meta } from '@storybook/react'; -import { FollowerProvider, UpdateFollower } from '../index'; +import { Follower, UpdateFollower } from '../index'; import * as DivStories from './FollowerContainer.stories'; const meta: Meta = { title: 'Context/FollowerProvider', - component: FollowerProvider, + component: Follower, decorators: [ (Story) => ( - + <> + - + ), ], argTypes: { diff --git a/src/stories/UpdateFollower.stories.tsx b/src/stories/UpdateFollower.stories.tsx index 1e309e4..06e97e9 100644 --- a/src/stories/UpdateFollower.stories.tsx +++ b/src/stories/UpdateFollower.stories.tsx @@ -1,7 +1,7 @@ import React, { useRef, useState } from 'react'; import type { Meta } from '@storybook/react'; -import { FollowerProvider, UpdateFollower } from '../index'; +import { Follower, UpdateFollower } from '../index'; import * as DivStories from './FollowerContainer.stories'; import './css/update_follower.css'; @@ -11,6 +11,14 @@ const meta: Meta = { parameters: { layout: 'fullscreen', }, + decorators: [ + (Story) => ( + <> + + + + ), + ], }; export default meta; @@ -18,41 +26,39 @@ export default meta; export const NestedUpdateCalls: Meta = { decorators: [ () => ( - - -
+ +
+ +
+
+ - -
-
- -
-
+
-
+ ), ], }; @@ -63,32 +69,30 @@ export const CustomPosition: Meta = { const containerRef = useRef(null); const [isHovering, setIsHovering] = useState(false); return ( - -
- { - setIsHovering(true); - }} - onMouseLeave={() => { - setIsHovering(false); - }} +
+ { + setIsHovering(true); + }} + onMouseLeave={() => { + setIsHovering(false); + }} + > +
-
-
-
-
-
- -
- +
+
+
+
+
+
); }, ], @@ -98,22 +102,49 @@ export const Rotate: Meta = { decorators: [ () => { return ( - +
+ +
+ +
+
+
+
+
+ ); + }, + ], +}; + +export const FollowSpeed: Meta = { + decorators: [ + () => { + return ( +
@@ -121,38 +152,7 @@ export const Rotate: Meta = {
-
- ); - }, - ], -}; - -export const FollowSpeed: Meta = { - decorators: [ - () => { - return ( - - -
- -
- -
-
-
-
-
-
-
+ ); }, ], diff --git a/tsconfig.json b/tsconfig.json index b6ad655..e247d53 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "target": "es6", "useDefineForClassFields": true, "lib": ["es5", "es2015", "es2016", "dom", "esnext", "es2020"], - "module": "ESNext", + "module": "NodeNext", "moduleResolution": "nodenext", "noImplicitAny": true, "skipLibCheck": true, diff --git a/yarn.lock b/yarn.lock index e58ed11..ff5e0f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7056,6 +7056,11 @@ use-resize-observer@^9.1.0: dependencies: "@juggle/resize-observer" "^3.3.1" +use-sync-external-store@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" @@ -7349,3 +7354,10 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zustand@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.4.1.tgz#0cd3a3e4756f21811bd956418fdc686877e8b3b0" + integrity sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw== + dependencies: + use-sync-external-store "1.2.0"