diff --git a/.prettierrc b/.prettierrc index 88b4771..70764d8 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,6 +1,6 @@ { "singleQuote": true, - "printWidth": 200, + "printWidth": 130, "proseWrap": "always", "tabWidth": 2, "useTabs": false, diff --git a/src/component/follower_div.tsx b/src/component/follower_div.tsx new file mode 100644 index 0000000..0304dc7 --- /dev/null +++ b/src/component/follower_div.tsx @@ -0,0 +1,67 @@ +import { MousePosition, MouseSettings } from '../types'; +import { motion } from 'framer-motion'; + +export function FollowerDiv({ pos, options, radius }: { pos: MousePosition; options: MouseSettings; radius?: number }) { + return ( + + + + {options.backgroundElement ? options.backgroundElement : null} + + + + ); +} diff --git a/src/component/follower_init.tsx b/src/component/follower_init.tsx new file mode 100644 index 0000000..5f4817e --- /dev/null +++ b/src/component/follower_init.tsx @@ -0,0 +1,50 @@ +import { useEffect, useState } from 'react'; +import { AnimatePresence } from 'framer-motion'; +import type { MousePosition, MouseSettings } from '../types'; +import { FollowerDiv } from './follower_div'; + +export function FollowerInitialiserComponent({ options, radius }: { options: MouseSettings; radius: number }) { + const [isHovering, setIsHovering] = useState(false); + const [pos, setPos] = useState({ + x: 0, + y: 0, + }); + + useEffect(() => { + const handleMouseLeave = () => { + setIsHovering(false); + }; + + const handleMouseEnter = () => { + setIsHovering(true); + }; + + const body = document.querySelector('body'); + body.addEventListener('mouseleave', handleMouseLeave); + body.addEventListener('mouseenter', handleMouseEnter); + + return () => { + body.removeEventListener('mouseleave', handleMouseLeave); + body.removeEventListener('mouseenter', handleMouseEnter); + }; + }, []); + + useEffect(() => { + function mouseMove(event: any) { + setPos({ + x: event.clientX - radius, + y: event.clientY - radius, + }); + } + window.addEventListener('mousemove', mouseMove); + return () => { + window.removeEventListener('mousemove', mouseMove); + }; + }, []); + + return ( + + {isHovering ? : null} + + ); +} diff --git a/src/component/index.ts b/src/component/index.ts index c2d82d6..fc2ea56 100644 --- a/src/component/index.ts +++ b/src/component/index.ts @@ -1,2 +1,3 @@ -export * from './Button'; -export * from './mouse_circle'; +export * from './follower_init'; +export * from './update_follower'; +export * from './follower_div'; diff --git a/src/component/mouse_circle.tsx b/src/component/mouse_circle.tsx deleted file mode 100644 index 3949e2c..0000000 --- a/src/component/mouse_circle.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { MousePropertiesContext } from '../context'; -import { motion } from 'framer-motion'; -import { useContext, useEffect, useState } from 'react'; -import { AnimatePresence } from 'framer-motion'; -import type { MousePosition } from '../types'; - -export function MouseCircleComponent() { - const { options, radius } = useContext(MousePropertiesContext); - const [isHovering, setIsHovering] = useState(false); - const [pos, setPos] = useState({ - x: 0, - y: 0, - }); - - useEffect(() => { - const handleMouseLeave = () => { - setIsHovering(false); - }; - - const handleMouseEnter = () => { - setIsHovering(true); - }; - - const body = document.querySelector('body'); - body.addEventListener('mouseleave', handleMouseLeave); - body.addEventListener('mouseenter', handleMouseEnter); - - return () => { - body.removeEventListener('mouseleave', handleMouseLeave); - body.removeEventListener('mouseenter', handleMouseEnter); - }; - }, []); - - useEffect(() => { - function mouseMove(event: any) { - setPos({ - x: event.clientX - radius, - y: event.clientY - radius, - }); - } - window.addEventListener('mousemove', mouseMove); - return () => { - window.removeEventListener('mousemove', mouseMove); - }; - }, []); - - return ( - - {isHovering ? ( - - - {options.backgroundElement ? options.backgroundElement : null} - - - ) : null} - - ); -} diff --git a/src/component/update_follower.tsx b/src/component/update_follower.tsx new file mode 100644 index 0000000..1ca8f23 --- /dev/null +++ b/src/component/update_follower.tsx @@ -0,0 +1,30 @@ +import { CSSProperties, ReactNode, useContext, useState } from 'react'; +import { MousePropertiesContext } from '..'; +import { MouseSettings } from '../types'; + +export function UpdateFollower({ + mouseOptions, + style, + className, + children, +}: { + mouseOptions: MouseSettings; + style: CSSProperties; + className: string; + children: ReactNode; +}) { + const { addLayer, removeLayer } = useContext(MousePropertiesContext); + const [id, setId] = useState(1000); + function handleMouseEnter() { + const id = addLayer(mouseOptions); + setId(id); + } + function handleMouseLeave() { + removeLayer(id); + } + return ( + + {children} + + ); +} diff --git a/src/context/mouse.context.tsx b/src/context/mouse.context.tsx index ef6e505..9b50539 100644 --- a/src/context/mouse.context.tsx +++ b/src/context/mouse.context.tsx @@ -1,36 +1,52 @@ -import { useState, createContext, ReactNode, CSSProperties } from 'react'; -import type { MousePosition, Props } from '../types'; +import { useState, createContext } from 'react'; +import { FollowerInitialiserComponent } from '..'; +import { Stack } from '../util/stack'; + +import type { Props, MouseSettings } from '../types'; interface ContextInterface { options: MouseSettings; - setOptions: (options: MouseSettings) => void; radius: number; + addLayer: (options: MouseSettings) => number; + removeLayer: (id: number) => void; } -export interface MouseSettings { - zIndex?: CSSProperties['zIndex']; - backgroundColor?: CSSProperties['backgroundColor']; - backgroundElement?: JSX.Element; - scale?: number; - rotate?: number; - customPosition?: MousePosition; - mixBlendMode?: CSSProperties['mixBlendMode']; - invert?: boolean; -} +const defaultProperties = { + inverted: false, +}; export const MousePropertiesContext = createContext(null); -export const MousePropertiesProvider = ({ children }: Props) => { - const radius = 12 / 2; +export const FollowerProvider = ({ children }: Props) => { + const radius: number = 12 / 2; + const layerStack = new Stack(); + const [options, setOptions] = useState(defaultProperties); - const [options, setOptions] = useState({ - invert: false, - }); + const addLayer = (layerOptions: MouseSettings): number => { + const properties = { ...options, ...layerOptions }; + const id = layerStack.push(properties); + setOptions(properties); + return id; + }; - const value = { + const removeLayer = (id: number) => { + const previousOptions = layerStack.pop(id); + if (previousOptions) { + setOptions(previousOptions); + } + }; + + const value: ContextInterface = { options, - setOptions, radius, + addLayer, + removeLayer, }; - return {children}; + + return ( + + + {children} + + ); }; diff --git a/src/stories/assets/main_bg.mp4 b/src/stories/assets/main_bg.mp4 new file mode 100644 index 0000000..fc4be9a Binary files /dev/null and b/src/stories/assets/main_bg.mp4 differ diff --git a/src/stories/follower_basic.stories.tsx b/src/stories/follower_basic.stories.tsx new file mode 100644 index 0000000..743fd58 --- /dev/null +++ b/src/stories/follower_basic.stories.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; + +import { FollowerProvider, FollowerInitialiserComponent, FollowerDiv } from '../index'; + +const meta: Meta = { + component: FollowerInitialiserComponent, + decorators: [ + (Story) => ( + + + + + + ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => { + return ; + }, +}; diff --git a/src/stories/follower_div.stories.tsx b/src/stories/follower_div.stories.tsx new file mode 100644 index 0000000..283ba0a --- /dev/null +++ b/src/stories/follower_div.stories.tsx @@ -0,0 +1,124 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +// @ts-ignore +import mainVideo from './assets/main_bg.mp4'; + +import { FollowerDiv } from '../index'; + +const meta: Meta = { + component: FollowerDiv, + decorators: [ + (Story) => ( + + + + ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + options: {}, + pos: { + x: 10, + y: 10, + }, + }, +}; + +export const Scale: Story = { + args: { + options: { + scale: 10, + }, + pos: { + x: 100, + y: 100, + }, + }, +}; + +export const Radius: Story = { + args: { + options: {}, + pos: { + x: 20, + y: 20, + }, + radius: 40, + }, +}; + +export const DifferentColor: Story = { + args: { + options: { + backgroundColor: 'purple', + }, + pos: { + x: 10, + y: 10, + }, + }, +}; + +export const CustomPosition: Story = { + args: { + options: { + customPosition: { + x: 500, + y: 200, + }, + }, + pos: { + x: 10, + y: 10, + }, + }, +}; + +export const CustomElement: Story = { + args: { + options: { + scale: 20, + backgroundElement: ( + + + + ), + }, + pos: { + x: 150, + y: 150, + }, + }, +}; + +export const Inverted: Story = { + args: { + options: { + inverted: true, + }, + pos: { + x: 10, + y: 10, + }, + }, + parameters: { + backgrounds: { + default: 'black', + values: [ + { name: 'black', value: '#000' }, + { name: 'white', value: '#fff' }, + ], + }, + }, +}; diff --git a/src/types/index.ts b/src/types/index.ts index f0b141a..dfdb3c5 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -8,3 +8,14 @@ export interface MousePosition { x: number; y: number; } + +export interface MouseSettings { + zIndex?: React.CSSProperties['zIndex']; + backgroundColor?: React.CSSProperties['backgroundColor']; + backgroundElement?: JSX.Element; + scale?: number; + rotate?: number; + customPosition?: MousePosition; + mixBlendMode?: React.CSSProperties['mixBlendMode']; + inverted?: boolean; +} diff --git a/src/util/stack.ts b/src/util/stack.ts new file mode 100644 index 0000000..ed282c6 --- /dev/null +++ b/src/util/stack.ts @@ -0,0 +1,60 @@ +import type { MouseSettings } from '../types'; + +interface StackItem { + id: number; + options: MouseSettings; +} + +export class Stack { + private stack: StackItem[]; + private currentId: number; + + constructor() { + this.stack = []; + this.currentId = 0; + } + + push(options: MouseSettings): number { + const item: StackItem = { + id: this.currentId, + options: { ...options }, + }; + this.stack.push(item); + this.currentId++; + return this.currentId; + } + + pop(id: number): MouseSettings | undefined { + let removedOptions: MouseSettings | undefined; + if (id > this.currentId) return undefined; + while (this.stack.length > 0) { + const item = this.stack.pop(); + if (item.id === id) { + removedOptions = item.options; + break; + } + } + + return removedOptions; + } + + peek(): MouseSettings | undefined { + if (this.stack.length > 0) { + return this.stack[this.stack.length - 1].options; + } + return undefined; + } + + isEmpty(): boolean { + return this.stack.length === 0; + } + + clear(): void { + this.stack = []; + this.currentId = 0; + } + + size(): number { + return this.stack.length; + } +}